Browse Source

Update Poll#schedule() to be asynchronous, accept partial state data, and to automatically handle ready state.

Afshin T. Darian 6 years ago
parent
commit
94b8f44128
1 changed files with 93 additions and 101 deletions
  1. 93 101
      packages/coreutils/src/poll.ts

+ 93 - 101
packages/coreutils/src/poll.ts

@@ -175,17 +175,18 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
       ...(options.frequency || {})
     };
     this.name = options.name || Private.DEFAULT_NAME;
-    this.ready = (options.when || Promise.resolve())
-      .then(() => {
+
+    // Schedule poll ticks after `when` promise is settled.
+    (options.when || Promise.resolve())
+      .then(_ => {
         if (this.isDisposed) {
           return;
         }
 
-        this.schedule({
+        this._locked = false;
+        return this.schedule({
           interval: Private.IMMEDIATE,
-          payload: null,
-          phase: 'when-resolved',
-          timestamp: new Date().getTime()
+          phase: 'when-resolved'
         });
       })
       .catch(reason => {
@@ -193,14 +194,13 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
           return;
         }
 
-        this.schedule({
+        console.warn(`Poll (${this.name}) started despite rejection.`, reason);
+
+        this._locked = false;
+        return this.schedule({
           interval: Private.IMMEDIATE,
-          payload: null,
-          phase: 'when-rejected',
-          timestamp: new Date().getTime()
+          phase: 'when-rejected'
         });
-
-        console.warn(`Poll (${this.name}) started despite rejection.`, reason);
       });
   }
 
@@ -297,6 +297,7 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
       return;
     }
 
+    this._queue.length = 0;
     this._state = {
       ...Private.DISPOSED_STATE,
       timestamp: new Date().getTime()
@@ -312,23 +313,12 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
    *
    * @returns A promise that resolves after tick is scheduled and never rejects.
    */
-  async refresh(): Promise<void> {
-    if (this.isDisposed) {
-      return;
-    }
-
-    if (this.state.phase === 'constructed') {
-      await this.ready;
-    }
-
-    if (this.state.phase !== 'refreshed') {
-      this.schedule({
-        interval: Private.IMMEDIATE,
-        payload: null,
-        phase: 'refreshed',
-        timestamp: new Date().getTime()
-      });
-    }
+  refresh(): Promise<void> {
+    return this.schedule({
+      cancel: last => last.phase === 'refreshed',
+      interval: Private.IMMEDIATE,
+      phase: 'refreshed'
+    });
   }
 
   /**
@@ -336,23 +326,12 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
    *
    * @returns A promise that resolves after tick is scheduled and never rejects.
    */
-  async start(): Promise<void> {
-    if (this.isDisposed) {
-      return;
-    }
-
-    if (this.state.phase === 'constructed') {
-      await this.ready;
-    }
-
-    if (this.state.phase === 'standby' || this.state.phase === 'stopped') {
-      this.schedule({
-        interval: Private.IMMEDIATE,
-        payload: null,
-        phase: 'started',
-        timestamp: new Date().getTime()
-      });
-    }
+  start(): Promise<void> {
+    return this.schedule({
+      cancel: last => last.phase !== 'standby' && last.phase !== 'stopped',
+      interval: Private.IMMEDIATE,
+      phase: 'started'
+    });
   }
 
   /**
@@ -360,59 +339,70 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
    *
    * @returns A promise that resolves after tick is scheduled and never rejects.
    */
-  async stop(): Promise<void> {
-    if (this.isDisposed) {
-      return;
-    }
-
-    if (this.state.phase === 'constructed') {
-      await this.ready;
-    }
-
-    if (this.state.phase !== 'stopped') {
-      this.schedule({
-        interval: Private.NEVER,
-        payload: null,
-        phase: 'stopped',
-        timestamp: new Date().getTime()
-      });
-    }
+  stop(): Promise<void> {
+    return this.schedule({
+      cancel: last => last.phase === 'stopped',
+      interval: Private.NEVER,
+      phase: 'stopped'
+    });
   }
 
   /**
-   * A promise that resolves after the poll has scheduled its first tick.
+   * Schedule the next poll tick.
    *
-   * #### Notes
-   * This accessor is protected to allow sub-classes to implement methods that
-   * can `await this.ready` if `this.state.phase === 'constructed'`.
+   * @param next - The next poll state data to schedule. Defaults to standby.
    *
-   * A poll should handle ready state without needing to expose it to clients.
-   */
-  protected readonly ready: Promise<void>;
-
-  /**
-   * Schedule the next poll tick.
+   * @param next.cancel - Cancels state transition if function returns `true`.
    *
-   * @param state - The new poll state data to schedule.
+   * @returns A promise that resolves when the next poll state is active.
    *
    * #### Notes
    * This method is protected to allow sub-classes to implement methods that can
    * schedule poll ticks.
-   *
-   * This method synchronously modifies poll state so it should be invoked with
-   * consideration given to the poll state that will be overwritten. It should
-   * be invoked only once in any synchronous context, otherwise the `ticked`
-   * signal and the `tick` promise will fall out of sync with each other.
-   *
-   * It will typically be invoked in a method that returns a promise, allowing
-   * the caller e.g. to `await this.ready` before scheduling a new tick.
    */
-  protected schedule(state: IPoll.State<T, U>): void {
-    const last = this.state;
+  protected async schedule(
+    next: Partial<
+      IPoll.State & { cancel: ((last: IPoll.State) => boolean) }
+    > = {}
+  ): Promise<void> {
+    if (this.isDisposed) {
+      return;
+    }
+
     const pending = this._tick;
-    const scheduled = new PromiseDelegate<this>();
+    const queue = this._queue;
+    let last = this.state;
+
+    // The `when` promise in the constructor options acts as a gate.
+    if (last.phase === 'constructed') {
+      if (next.phase !== 'when-rejected' && next.phase !== 'when-resolved') {
+        await pending.promise;
+      }
+    }
+
+    // Update `last` after pending promise settles.
+    last = this.state;
+
+    // If scheduling is locked enqueue next state.
+    if (this._locked) {
+      queue.push(next);
+      return;
+    }
+
+    // Check if the phase transition should be canceled.
+    if (next.cancel && next.cancel(last)) {
+      return;
+    }
 
     // Update poll state.
+    const scheduled = new PromiseDelegate<this>();
+    const state: IPoll.State<T, U> = {
+      interval: this.frequency.interval,
+      payload: null,
+      phase: 'standby',
+      timestamp: new Date().getTime(),
+      ...next
+    };
     this._state = state;
     this._tick = scheduled;
 
@@ -423,9 +413,10 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
       clearTimeout(this._timeout);
     }
 
-    // Emit the ticked signal and resolve the pending promise.
+    // Emit ticked signal, resolve pending promise, and await its settlement.
     this._ticked.emit(this.state);
     pending.resolve(this);
+    await pending.promise;
 
     // Schedule next execution and cache its timeout handle.
     const execute = () => {
@@ -439,8 +430,15 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
       state.interval === Private.IMMEDIATE
         ? requestAnimationFrame(execute)
         : state.interval === Private.NEVER
-        ? -1
-        : setTimeout(execute, state.interval);
+          ? -1
+          : setTimeout(execute, state.interval);
+
+    this._locked = false;
+
+    // Unwind the queue if necessary.
+    if (queue.length) {
+      return this.schedule(queue.shift());
+    }
   }
 
   /**
@@ -458,12 +456,7 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
 
     // If in standby mode schedule next tick without calling the factory.
     if (standby) {
-      this.schedule({
-        interval: this.frequency.interval,
-        payload: null,
-        phase: 'standby',
-        timestamp: new Date().getTime()
-      });
+      void this.schedule();
       return;
     }
 
@@ -475,11 +468,9 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
           return;
         }
 
-        this.schedule({
-          interval: this.frequency.interval,
+        void this.schedule({
           payload: resolved,
-          phase: this.state.phase === 'rejected' ? 'reconnected' : 'resolved',
-          timestamp: new Date().getTime()
+          phase: this.state.phase === 'rejected' ? 'reconnected' : 'resolved'
         });
       })
       .catch((rejected: U) => {
@@ -487,11 +478,10 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
           return;
         }
 
-        this.schedule({
+        void this.schedule({
           interval: Private.sleep(this.frequency, this.state),
           payload: rejected,
-          phase: 'rejected',
-          timestamp: new Date().getTime()
+          phase: 'rejected'
         });
       });
   }
@@ -499,6 +489,8 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
   private _disposed = new Signal<this, void>(this);
   private _factory: Poll.Factory<T, U>;
   private _frequency: IPoll.Frequency;
+  private _locked = true;
+  private _queue = new Array<Partial<IPoll.State>>();
   private _standby: Poll.Standby | (() => boolean | Poll.Standby);
   private _state: IPoll.State<T, U>;
   private _tick = new PromiseDelegate<this>();