Browse Source

Clean up poll API and handle waiting for `when` promise to resolve.

Afshin Darian 6 years ago
parent
commit
0f01313661
1 changed files with 43 additions and 41 deletions
  1. 43 41
      packages/coreutils/src/poll.ts

+ 43 - 41
packages/coreutils/src/poll.ts

@@ -39,20 +39,14 @@ export class Poll<T = any, U = any> implements IDisposable {
     this.name = name || 'unknown';
     this.variance = typeof variance === 'number' ? variance : 0.2;
     this._factory = factory;
-
-    // Create the first poll tick.
-    const poll = new PromiseDelegate<this>();
-    this._tick = poll;
-
-    // Set the initial poll tick state.
     this._state = {
       interval: this.interval,
       payload: null,
-      phase: 'started',
+      phase: 'instantiated',
       timestamp: new Date().getTime()
     };
 
-    // Schedule the first poll tick after the `when` promise is resolved.
+    // Schedule a poll tick after the `when` promise is resolved.
     (when || Promise.resolve())
       .then(() => {
         // Bail if disposed while `when` promise was in flight.
@@ -60,8 +54,8 @@ export class Poll<T = any, U = any> implements IDisposable {
           return;
         }
 
-        // Resolve the poll and schedule the next one.
-        this._resolve(poll, {
+        // Resolve the poll and schedule the next tick.
+        this._resolve(this._tick, {
           interval: this.interval,
           payload: null,
           phase: 'when-resolved',
@@ -74,8 +68,8 @@ export class Poll<T = any, U = any> implements IDisposable {
           return;
         }
 
-        // Resolve the poll and schedule the next one.
-        this._resolve(poll, {
+        // Resolve the poll and schedule the next tick.
+        this._resolve(this._tick, {
           interval: this.interval,
           payload: null,
           phase: 'when-rejected',
@@ -166,10 +160,17 @@ export class Poll<T = any, U = any> implements IDisposable {
    * Resolves the outstanding poll and schedules the next tick immediately.
    *
    * #### Notes
+   * If the poll has been instantiated but its `when` promise has not yet
+   * resolved, this call will be scheduled after the `when` promise resolves.
+   *
    * It is safe to call this method multiple times. It returns the outstanding
    * poll request if the current tick phase is `'refreshed'`.
    */
-  refresh(): Promise<this> {
+  async refresh(): Promise<this> {
+    if (this._state.phase === 'instantiated') {
+      await this._tick.promise;
+    }
+
     if (this._state.phase === 'refreshed') {
       return this._tick.promise;
     }
@@ -186,10 +187,17 @@ export class Poll<T = any, U = any> implements IDisposable {
    * Starts polling.
    *
    * #### Notes
+   * If the poll has been instantiated but its `when` promise has not yet
+   * resolved, this call will be scheduled after the `when` promise resolves.
+   *
    * It is safe to call this method multiple times. The poll will only start
    * if its current tick phase is `'standby'` or `'stopped'`.
    */
-  start(): Promise<this> {
+  async start(): Promise<this> {
+    if (this._state.phase === 'instantiated') {
+      await this._tick.promise;
+    }
+
     if (this._state.phase !== 'standby' && this._state.phase !== 'stopped') {
       return this._tick.promise;
     }
@@ -206,10 +214,17 @@ export class Poll<T = any, U = any> implements IDisposable {
    * Stops polling.
    *
    * #### Notes
-   * It is safe to call this method multiple times. The poll will stop
-   * if its current tick phase is not `'stopped'`.
+   * If the poll has been instantiated but its `when` promise has not yet
+   * resolved, this call will be scheduled after the `when` promise resolves.
+   *
+   * It is safe to call this method multiple times. The poll will only stop if
+   * its current tick phase is not `'stopped'`.
    */
-  stop(): Promise<this> {
+  async stop(): Promise<this> {
+    if (this._state.phase === 'instantiated') {
+      await this._tick.promise;
+    }
+
     if (this._state.phase === 'stopped') {
       return this._tick.promise;
     }
@@ -223,7 +238,7 @@ export class Poll<T = any, U = any> implements IDisposable {
   }
 
   /**
-   * Make a poll request.
+   * Execute a poll request.
    */
   private _request(poll: PromiseDelegate<this>): void {
     const { max, min, variance } = this;
@@ -239,20 +254,13 @@ export class Poll<T = any, U = any> implements IDisposable {
       return;
     }
 
-    // Generate a new poll promise and handle its resolution.
     this._factory(this._state)
       .then((resolved: T) => {
-        // Bail if disposed while poll promise was in flight.
-        if (this.isDisposed) {
-          return;
-        }
-
-        // Bail if this promise has already been superseded.
-        if (poll !== this._tick) {
+        // Bail if poll was disposed or superseded while promise was in flight.
+        if (this.isDisposed || poll !== this._tick) {
           return;
         }
 
-        // Resolve the poll and schedule the next one.
         this._resolve(poll, {
           interval: Private.jitter(this.interval, variance, min, max),
           payload: resolved,
@@ -261,17 +269,11 @@ export class Poll<T = any, U = any> implements IDisposable {
         });
       })
       .catch((rejected: U) => {
-        // Bail if disposed while poll promise was in flight.
-        if (this.isDisposed) {
-          return;
-        }
-
-        // Bail if this promise has already been superseded.
-        if (poll !== this._tick) {
+        // Bail if poll was disposed or superseded while promise was in flight.
+        if (this.isDisposed || poll !== this._tick) {
           return;
         }
 
-        // Resolve the poll and schedule next one with an increased interval.
         const increased = Math.min(this._state.interval * 2, max);
         this._resolve(poll, {
           interval: Private.jitter(increased, variance, min, max),
@@ -292,19 +294,18 @@ export class Poll<T = any, U = any> implements IDisposable {
     const { interval } = tick;
     const poll = new PromiseDelegate<this>();
     const request = () => {
-      // Bail if disposed during timeout.
       if (this.isDisposed) {
         return;
       }
       this._request(poll);
     };
 
-    // Cancel outstanding request if possible.
+    // Clear the schedule if possible.
     clearTimeout(this._timeout);
 
     // Update poll state and schedule the next tick.
-    this._tick = poll;
     this._state = tick;
+    this._tick = poll;
     this._timeout = interval
       ? interval === Infinity
         ? -1 // Never execute request.
@@ -323,7 +324,7 @@ export class Poll<T = any, U = any> implements IDisposable {
   private _disposed = new Signal<this, void>(this);
   private _factory: (tick: Poll.Tick<T, U>) => Promise<T>;
   private _state: Poll.Tick<T, U>;
-  private _tick: PromiseDelegate<this> | null = null;
+  private _tick: PromiseDelegate<this> | null = new PromiseDelegate<this>();
   private _ticked = new Signal<this, Poll.Tick<T, U>>(this);
   private _timeout = -1;
 }
@@ -336,6 +337,7 @@ export namespace Poll {
    * The phase of the poll when the current tick was scheduled.
    */
   export type Phase =
+    | 'instantiated'
     | 'reconnected'
     | 'refreshed'
     | 'rejected'
@@ -359,8 +361,8 @@ export namespace Poll {
      * The payload of the last poll resolution or rejection.
      *
      * #### Notes
-     * `payload` is `null` unless the `phase` is `'resolved'` or `'rejected'`.
-     * `payload` is of type `T` for resolutions and `U` for rejections.
+     * The payload is `null` unless the `phase` is `'reconnected`, `'resolved'`,
+     * or `'rejected'`. Its type is `T` for resolutions and `U` for rejections.
      */
     readonly payload: T | U | null;