Skip to content

Commit

Permalink
fix(FutureAction): add support for periodic scheduling with setInterv…
Browse files Browse the repository at this point in the history
…al instead of setTimeout
  • Loading branch information
trxcllnt committed Mar 7, 2016
1 parent bcfee80 commit c4f5408
Showing 1 changed file with 76 additions and 7 deletions.
83 changes: 76 additions & 7 deletions src/scheduler/FutureAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class FutureAction<T> extends Subscription implements Action {
public id: number;
public state: T;
public delay: number;
private pending: boolean = false;

constructor(public scheduler: Scheduler,
public work: (x?: T) => Subscription | void) {
Expand All @@ -17,8 +18,14 @@ export class FutureAction<T> extends Subscription implements Action {
execute() {
if (this.isUnsubscribed) {
throw new Error('How did did we execute a canceled Action?');
} else {
try {
this.work(this.state);
} catch (e) {
this.unsubscribe();
throw e;
}
}
this.work(this.state);
}

schedule(state?: T, delay: number = 0): Action {
Expand All @@ -30,34 +37,96 @@ export class FutureAction<T> extends Subscription implements Action {

protected _schedule(state?: T, delay: number = 0): Action {

this.delay = delay;
// Always replace the current state with the new state.
this.state = state;

// Set the pending flag indicating that this action has been scheduled, or
// has recursively rescheduled itself.
this.pending = true;

const id = this.id;
// If this action has an intervalID and the specified delay matches the
// delay we used to create the intervalID, don't call `setInterval` again.
if (id != null && this.delay === delay) {
return this;
}

this.delay = delay;

// If this action has an intervalID, but was rescheduled with a different
// `delay` time, cancel the current intervalID and call `setInterval` with
// the new `delay` time.
if (id != null) {
this.id = null;
root.clearTimeout(id);
root.clearInterval(id);
}

this.id = root.setTimeout(() => {
this.id = null;
const {scheduler} = this;
//
// Important implementation note:
//
// By default, FutureAction only executes once. However, Actions have the
// ability to be rescheduled from within the scheduled callback (mimicking
// recursion for asynchronous methods). This allows us to implement single
// and repeated actions with the same code path without adding API surface
// area, and implement tail-call optimization over asynchronous boundaries.
//
// However, JS runtimes make a distinction between intervals scheduled by
// repeatedly calling `setTimeout` vs. a single `setInterval` call, with
// the latter providing a better guarantee of precision.
//
// In order to accommodate both single and repeatedly rescheduled actions,
// use `setInterval` here for both cases. By default, the interval will be
// canceled after its first execution, or if the action schedules itself to
// run again with a different `delay` time.
//
// If the action recursively schedules itself to run again with the same
// `delay` time, the interval is not canceled, but allowed to loop again.
// The check of whether the interval should be canceled or not is run every
// time the interval is executed. The first time an action fails to
// reschedule itself, the interval is canceled.
//
this.id = root.setInterval(() => {

this.pending = false;
const {id, scheduler} = this;
scheduler.actions.push(this);
scheduler.flush();

//
// Terminate this interval if the action didn't reschedule itself.
// Don't call `this.unsubscribe()` here, because the action could be
// rescheduled later. For example:
//
// ```
// scheduler.schedule(function doWork(counter) {
// /* ... I'm a busy worker bee ... */
// var originalAction = this;
// /* wait 100ms before rescheduling this action again */
// setTimeout(function () {
// originalAction.schedule(counter + 1);
// }, 100);
// }, 1000);
// ```

if (this.pending === false && id != null) {
this.id = null;
root.clearInterval(id);
}
}, delay);

return this;
}

protected _unsubscribe() {

this.pending = false;
const {id, scheduler} = this;
const {actions} = scheduler;
const index = actions.indexOf(this);

if (id != null) {
this.id = null;
root.clearTimeout(id);
root.clearInterval(id);
}

if (index !== -1) {
Expand Down

0 comments on commit c4f5408

Please sign in to comment.