|
@@ -175,17 +175,17 @@ 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({
|
|
|
+ return this.schedule({
|
|
|
interval: Private.IMMEDIATE,
|
|
|
- payload: null,
|
|
|
- phase: 'when-resolved',
|
|
|
- timestamp: new Date().getTime()
|
|
|
+ phase: 'when-resolved'
|
|
|
});
|
|
|
})
|
|
|
.catch(reason => {
|
|
@@ -193,14 +193,12 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- this.schedule({
|
|
|
+ console.warn(`Poll (${this.name}) started despite rejection.`, reason);
|
|
|
+
|
|
|
+ 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);
|
|
|
});
|
|
|
}
|
|
|
|
|
@@ -312,23 +310,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 +323,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 +336,58 @@ 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>();
|
|
|
+
|
|
|
+ // The `when` promise in the constructor options acts as a gate.
|
|
|
+ if (this.state.phase === 'constructed') {
|
|
|
+ if (next.phase !== 'when-rejected' && next.phase !== 'when-resolved') {
|
|
|
+ await pending.promise;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if the phase transition should be canceled.
|
|
|
+ if (next.cancel && next.cancel(this.state)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
// Update poll state.
|
|
|
+ const last = this.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 +398,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 = () => {
|
|
@@ -458,12 +434,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 +446,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 +456,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'
|
|
|
});
|
|
|
});
|
|
|
}
|