Skip to content

Commit

Permalink
Merge pull request #306 from rwjblue/autorun-microtask
Browse files Browse the repository at this point in the history
Use microtask queue to flush autoruns...
  • Loading branch information
rwjblue authored Feb 2, 2018
2 parents e65437d + 27690de commit 855b208
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 150 deletions.
5 changes: 4 additions & 1 deletion lib/backburner/deferred-action-queues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default class DeferredActionQueues {
@method flush
DeferredActionQueues.flush() calls Queue.flush()
*/
public flush() {
public flush(fromAutorun = false) {
let queue;
let queueName;
let numberOfQueues = this.queueNames.length;
Expand All @@ -59,6 +59,9 @@ export default class DeferredActionQueues {

if (queue.hasWork() === false) {
this.queueNameIndex++;
if (fromAutorun) {
return QUEUE_STATE.Pause;
}
} else {
if (queue.flush(false /* async */) === QUEUE_STATE.Pause) {
return QUEUE_STATE.Pause;
Expand Down
52 changes: 52 additions & 0 deletions lib/backburner/platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export interface IPlatform {
setTimeout(fn: Function, ms: number): any;
clearTimeout(id: any): void;
next(): any;
clearNext(timerId: any): void;
now(): number;
}

const SET_TIMEOUT = setTimeout;
const NOOP = () => {};

export function buildPlatform(flush: () => void): IPlatform {
let next;
let clearNext = NOOP;

if (typeof MutationObserver === 'function') {
let iterations = 0;
let observer = new MutationObserver(flush);
let node = document.createTextNode('');
observer.observe(node, { characterData: true });

next = () => {
iterations = ++iterations % 2;
node.data = '' + iterations;
return iterations;
};

} else if (typeof Promise === 'function') {
const autorunPromise = Promise.resolve();
next = () => autorunPromise.then(flush);

} else {
next = () => SET_TIMEOUT(flush, 0);
}

return {
setTimeout(fn, ms) {
return SET_TIMEOUT(fn, ms);
},

clearTimeout(timerId: number) {
return clearTimeout(timerId);
},

now() {
return Date.now();
},

next,
clearNext,
};
}
2 changes: 1 addition & 1 deletion lib/backburner/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default class Queue {
}
}

public flush(sync?) {
public flush(sync?: Boolean) {
let { before, after } = this.options;
let target;
let method;
Expand Down
149 changes: 85 additions & 64 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
export {
buildPlatform,
IPlatform
} from './backburner/platform';

import {
buildPlatform,
IPlatform,
} from './backburner/platform';
import {
findItem,
findTimer,
Expand All @@ -14,7 +23,6 @@ import Queue, { QUEUE_STATE } from './backburner/queue';
type Timer = any;

const noop = function() {};
const SET_TIMEOUT = setTimeout;

function parseArgs() {
let length = arguments.length;
Expand Down Expand Up @@ -126,14 +134,25 @@ let autorunsCompletedCount = 0;
let deferredActionQueuesCreatedCount = 0;
let nestedDeferredActionQueuesCreated = 0;

export interface IBackburnerOptions {
defaultQueue?: string;
onBegin?: (currentInstance: DeferredActionQueues, previousInstance: DeferredActionQueues) => void;
onEnd?: (currentInstance: DeferredActionQueues, nextInstance: DeferredActionQueues) => void;
onError?: (error: any, errorRecordedForStack?: any) => void;
onErrorTarget?: any;
onErrorMethod?: string;
mustYield?: () => boolean;
_buildPlatform?: (flush: () => void) => IPlatform;
}

export default class Backburner {
public static Queue = Queue;

public DEBUG = false;

public currentInstance: DeferredActionQueues | null = null;

public options: any;
public options: IBackburnerOptions;

public get counters() {
return {
Expand Down Expand Up @@ -183,49 +202,45 @@ export default class Backburner {

private _timerTimeoutId: number | null = null;
private _timers: any[] = [];
private _platform: {
setTimeout(fn: Function, ms: number): number;
clearTimeout(id: number): void;
next(fn: Function): number;
clearNext(id: any): void;
now(): number;
};
private _platform: IPlatform;

private _boundRunExpiredTimers: () => void;

private _autorun: number | null = null;
private _boundAutorunEnd: () => void;
private _defaultQueue: string;

constructor(queueNames: string[], options: any = {} ) {
constructor(queueNames: string[], options?: IBackburnerOptions) {
this.queueNames = queueNames;
this.options = options;
if (!this.options.defaultQueue) {
this.options.defaultQueue = queueNames[0];
this.options = options || {};
if (typeof this.options.defaultQueue === 'string') {
this._defaultQueue = this.options.defaultQueue;
} else {
this._defaultQueue = this.queueNames[0];
}

this._onBegin = this.options.onBegin || noop;
this._onEnd = this.options.onEnd || noop;

let _platform = this.options._platform || {};
let platform = Object.create(null);

platform.setTimeout = _platform.setTimeout || ((fn, ms) => setTimeout(fn, ms));
platform.clearTimeout = _platform.clearTimeout || ((id) => clearTimeout(id));
platform.next = _platform.next || ((fn) => SET_TIMEOUT(fn, 0));
platform.clearNext = _platform.clearNext || platform.clearTimeout;
platform.now = _platform.now || (() => Date.now());

this._platform = platform;

this._boundRunExpiredTimers = this._runExpiredTimers.bind(this);

this._boundAutorunEnd = () => {
autorunsCompletedCount++;

// if the autorun was already flushed, do nothing
if (this._autorun === null) { return; }

this._autorun = null;
this.end();
this._end(true /* fromAutorun */);
};

let builder = this.options._buildPlatform || buildPlatform;
this._platform = builder(this._boundAutorunEnd);
}

public get defaultQueue() {
return this._defaultQueue;
}
/*
@method begin
@return instantiated class DeferredActionQueues
Expand Down Expand Up @@ -257,40 +272,7 @@ export default class Backburner {

public end() {
endCount++;
let currentInstance = this.currentInstance;
let nextInstance: DeferredActionQueues | null = null;

if (currentInstance === null) {
throw new Error(`end called without begin`);
}

// Prevent double-finally bug in Safari 6.0.2 and iOS 6
// This bug appears to be resolved in Safari 6.0.5 and iOS 7
let finallyAlreadyCalled = false;
let result;
try {
result = currentInstance.flush();
} finally {
if (!finallyAlreadyCalled) {
finallyAlreadyCalled = true;

if (result === QUEUE_STATE.Pause) {
autorunsCreatedCount++;
const next = this._platform.next;
this._autorun = next(this._boundAutorunEnd);
} else {
this.currentInstance = null;

if (this.instanceStack.length > 0) {
nextInstance = this.instanceStack.pop() as DeferredActionQueues;
this.currentInstance = nextInstance;
}
endEventCount++;
this._trigger('end', currentInstance, nextInstance);
this._onEnd(currentInstance, nextInstance);
}
}
}
this._end(false);
}

public on(eventName, callback) {
Expand Down Expand Up @@ -564,6 +546,40 @@ export default class Backburner {
this._ensureInstance();
}

private _end(fromAutorun: boolean) {
let currentInstance = this.currentInstance;
let nextInstance: DeferredActionQueues | null = null;

if (currentInstance === null) {
throw new Error(`end called without begin`);
}

// Prevent double-finally bug in Safari 6.0.2 and iOS 6
// This bug appears to be resolved in Safari 6.0.5 and iOS 7
let finallyAlreadyCalled = false;
let result;
try {
result = currentInstance.flush(fromAutorun);
} finally {
if (!finallyAlreadyCalled) {
finallyAlreadyCalled = true;

if (result === QUEUE_STATE.Pause) {
this._scheduleAutorun();
} else {
this.currentInstance = null;

if (this.instanceStack.length > 0) {
nextInstance = this.instanceStack.pop() as DeferredActionQueues;
this.currentInstance = nextInstance;
}
this._trigger('end', currentInstance, nextInstance);
this._onEnd(currentInstance, nextInstance);
}
}
}
}

private _join(target, method, args) {
if (this.currentInstance === null) {
return this._run(target, method, args);
Expand Down Expand Up @@ -654,7 +670,7 @@ export default class Backburner {
/**
Trigger an event. Supports up to two arguments. Designed around
triggering transition events from one run loop instance to the
next, which requires an argument for the first instance and then
next, which requires an argument for the instance and then
an argument for the next instance.
@private
Expand Down Expand Up @@ -685,7 +701,7 @@ export default class Backburner {
let timers = this._timers;
let i = 0;
let l = timers.length;
let defaultQueue = this.options.defaultQueue;
let defaultQueue = this._defaultQueue;
let n = this._platform.now();

for (; i < l; i += 6) {
Expand Down Expand Up @@ -725,11 +741,16 @@ export default class Backburner {
private _ensureInstance(): DeferredActionQueues {
let currentInstance = this.currentInstance;
if (currentInstance === null) {
autorunsCreatedCount++;
currentInstance = this.begin();
const next = this._platform.next;
this._autorun = next(this._boundAutorunEnd);
this._scheduleAutorun();
}
return currentInstance;
}

private _scheduleAutorun() {
autorunsCreatedCount++;

const next = this._platform.next;
this._autorun = next();
}
}
40 changes: 39 additions & 1 deletion tests/autorun-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ QUnit.test('autorun', function(assert) {
assert.equal(step++, 0);

bb.schedule('zomg', null, () => {
assert.equal(step, 2);
assert.equal(step++, 2);
setTimeout(() => {
assert.ok(!bb.hasTimers(), 'The all timers are cleared');
done();
Expand Down Expand Up @@ -58,3 +58,41 @@ QUnit.test('autorun (joins next run if not yet flushed)', function(assert) {
two: { count: 1, order: 1 }
});
});

QUnit.test('autorun completes before items scheduled by later (via microtasks)', function(assert) {
let done = assert.async();
let bb = new Backburner(['first', 'second']);
let order = new Array();

// this later will be scheduled into the `first` queue when
// its timer is up
bb.later(() => {
order.push('second - later');
}, 0);

// scheduling this into the second queue so that we can confirm this _still_
// runs first (due to autorun resolving before scheduled timer)
bb.schedule('second', null, () => {
order.push('first - scheduled');
});

setTimeout(() => {
assert.deepEqual(order, ['first - scheduled', 'second - later']);
done();
}, 20);
});

QUnit.test('can be canceled (private API)', function(assert) {
assert.expect(0);

let done = assert.async();
let bb = new Backburner(['zomg']);

bb.schedule('zomg', null, () => {
assert.notOk(true, 'should not flush');
});

bb['_cancelAutorun']();

setTimeout(done, 10);
});
Loading

0 comments on commit 855b208

Please sign in to comment.