From 74b5b1a943bd9fe307b12ec36f4ce71ab2c9998a Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Thu, 2 Nov 2017 19:56:25 -0700 Subject: [PATCH 1/3] refactor(groupBy): Remove Map polyfill - Removes Map polyfill - Removes FastMap impl - Tests in latest Chrome and Node show no discernable difference in performance between Map and Object has a hashtable for our use case. - Adds an error that is thrown immediately at runtime if Map does not exist. BREAKING CHANGE: Older runtimes will require Map to be polyfilled to use `groupBy` --- spec/util/FastMap-spec.ts | 81 ----------------------------------- spec/util/MapPolyfill-spec.ts | 78 --------------------------------- src/operators/groupBy.ts | 9 ++-- src/util/FastMap.ts | 30 ------------- src/util/Map.ts | 4 -- src/util/MapPolyfill.ts | 43 ------------------- 6 files changed, 6 insertions(+), 239 deletions(-) delete mode 100644 spec/util/FastMap-spec.ts delete mode 100644 spec/util/MapPolyfill-spec.ts delete mode 100644 src/util/FastMap.ts delete mode 100644 src/util/Map.ts delete mode 100644 src/util/MapPolyfill.ts diff --git a/spec/util/FastMap-spec.ts b/spec/util/FastMap-spec.ts deleted file mode 100644 index a9b8c520c6..0000000000 --- a/spec/util/FastMap-spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { expect } from 'chai'; -import { FastMap } from '../../src/util/FastMap'; - -/** @test {FastMap} */ -describe('FastMap', () => { - it('should exist', () => { - expect(FastMap).to.be.a('function'); - }); - - it('should accept string as keys', () => { - const map = new FastMap(); - const key1 = 'keyOne'; - const key2 = 'keyTwo'; - - map.set(key1, 'yo'); - map.set(key2, 'what up'); - - expect(map.get(key1)).to.equal('yo'); - expect(map.get(key2)).to.equal('what up'); - }); - - it('should allow setting keys twice', () => { - const map = new FastMap(); - const key1 = 'keyOne'; - - map.set(key1, 'sing'); - map.set(key1, 'yodel'); - - expect(map.get(key1)).to.equal('yodel'); - }); - - it('should have a delete method that removes keys', () => { - const map = new FastMap(); - const key1 = 'keyOne'; - - map.set(key1, 'sing'); - - expect(map.delete(key1)).to.be.true; - expect(map.get(key1)).to.be.a('null'); - }); - - it('should clear all', () => { - const map = new FastMap(); - const key1 = 'keyOne'; - const key2 = 'keyTwo'; - - map.set(key1, 'yo'); - map.set(key2, 'what up'); - - map.clear(); - - expect(map.get(key1)).to.be.a('undefined'); - expect(map.get(key2)).to.be.a('undefined'); - }); - - describe('prototype.forEach', () => { - it('should exist', () => { - const map = new FastMap(); - expect(map.forEach).to.be.a('function'); - }); - - it('should iterate over keys and values', () => { - const expectedKeys = ['a', 'b', 'c']; - const expectedValues = [1, 2, 3]; - const map = new FastMap(); - map.set('a', 1); - map.set('b', 2); - map.set('c', 3); - const thisArg = {}; - - map.forEach(function (value, key) { - expect(this).to.equal(thisArg); - expect(value).to.equal(expectedValues.shift()); - expect(key).to.equal(expectedKeys.shift()); - }, thisArg); - - expect(expectedValues.length).to.equal(0); - expect(expectedKeys.length).to.equal(0); - }); - }); -}); diff --git a/spec/util/MapPolyfill-spec.ts b/spec/util/MapPolyfill-spec.ts deleted file mode 100644 index 2fcc33fffd..0000000000 --- a/spec/util/MapPolyfill-spec.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { expect } from 'chai'; -import { MapPolyfill } from '../../src/util/MapPolyfill'; - -/** @test {MapPolyfill} */ -describe('MapPolyfill', () => { - it('should exist', () => { - expect(MapPolyfill).to.be.a('function'); - }); - - it('should act like a hashtable that accepts objects as keys', () => { - const map = new MapPolyfill(); - const key1 = {}; - const key2 = {}; - - map.set('test', 'hi'); - map.set(key1, 'yo'); - map.set(key2, 'what up'); - - expect(map.get('test')).to.equal('hi'); - expect(map.get(key1)).to.equal('yo'); - expect(map.get(key2)).to.equal('what up'); - expect(map.size).to.equal(3); - }); - - it('should allow setting keys twice', () => { - const map = new MapPolyfill(); - const key1 = {}; - - map.set(key1, 'sing'); - map.set(key1, 'yodel'); - - expect(map.get(key1)).to.equal('yodel'); - expect(map.size).to.equal(1); - }); - - it('should have a delete method that removes keys', () => { - const map = new MapPolyfill(); - const key1 = {}; - - map.set(key1, 'sing'); - expect(map.size).to.equal(1); - - map.delete(key1); - - expect(map.size).to.equal(0); - expect(map.get(key1)).to.be.a('undefined'); - }); - - describe('prototype.forEach', () => { - it('should exist', () => { - const map = new MapPolyfill(); - expect(map.forEach).to.be.a('function'); - }); - - it('should iterate over keys and values', () => { - const expectedKeys = ['a', 'b', 'c']; - const expectedValues = [1, 2, 3]; - const map = new MapPolyfill(); - map.set('a', 1); - map.set('b', 2); - map.set('c', 3); - - const thisArg = { - arg: 'value' - }; - - //intentionally not using lambda to avoid typescript's this context capture - map.forEach(function (value, key) { - expect(this).to.equal(thisArg); - expect(value).to.equal(expectedValues.shift()); - expect(key).to.equal(expectedKeys.shift()); - }, thisArg); - - expect(expectedValues.length).to.equal(0); - expect(expectedKeys.length).to.equal(0); - }); - }); -}); diff --git a/src/operators/groupBy.ts b/src/operators/groupBy.ts index 3ed28a6966..f94e941fea 100644 --- a/src/operators/groupBy.ts +++ b/src/operators/groupBy.ts @@ -3,10 +3,13 @@ import { Subscription } from '../Subscription'; import { Observable } from '../Observable'; import { Operator } from '../Operator'; import { Subject } from '../Subject'; -import { Map } from '../util/Map'; -import { FastMap } from '../util/FastMap'; import { OperatorFunction } from '../interfaces'; +/** Assert that map is present for this operator */ +if (!Map) { + throw new Error('Map not found, please polyfill'); +} + /* tslint:disable:max-line-length */ export function groupBy(keySelector: (value: T) => K): OperatorFunction>; export function groupBy(keySelector: (value: T) => K, elementSelector: void, durationSelector: (grouped: GroupedObservable) => Observable): OperatorFunction>; @@ -144,7 +147,7 @@ class GroupBySubscriber extends Subscriber implements RefCountSubscr let groups = this.groups; if (!groups) { - groups = this.groups = typeof key === 'string' ? new FastMap() : new Map(); + groups = this.groups = new Map>(); } let group = groups.get(key); diff --git a/src/util/FastMap.ts b/src/util/FastMap.ts deleted file mode 100644 index c7bd54a1b7..0000000000 --- a/src/util/FastMap.ts +++ /dev/null @@ -1,30 +0,0 @@ -export class FastMap { - private values: Object = {}; - - delete(key: string): boolean { - this.values[key] = null; - return true; - } - - set(key: string, value: any): FastMap { - this.values[key] = value; - return this; - } - - get(key: string): any { - return this.values[key]; - } - - forEach(cb: (value: any, key: any) => void, thisArg?: any): void { - const values = this.values; - for (let key in values) { - if (values.hasOwnProperty(key) && values[key] !== null) { - cb.call(thisArg, values[key], key); - } - } - } - - clear(): void { - this.values = {}; - } -} \ No newline at end of file diff --git a/src/util/Map.ts b/src/util/Map.ts deleted file mode 100644 index 563017c000..0000000000 --- a/src/util/Map.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { root } from './root'; -import { MapPolyfill } from './MapPolyfill'; - -export const Map = root.Map || (() => MapPolyfill)(); \ No newline at end of file diff --git a/src/util/MapPolyfill.ts b/src/util/MapPolyfill.ts deleted file mode 100644 index 7100f4b191..0000000000 --- a/src/util/MapPolyfill.ts +++ /dev/null @@ -1,43 +0,0 @@ -export class MapPolyfill { - public size = 0; - private _values: any[] = []; - private _keys: any[] = []; - - get(key: any) { - const i = this._keys.indexOf(key); - return i === -1 ? undefined : this._values[i]; - } - - set(key: any, value: any) { - const i = this._keys.indexOf(key); - if (i === -1) { - this._keys.push(key); - this._values.push(value); - this.size++; - } else { - this._values[i] = value; - } - return this; - } - - delete(key: any): boolean { - const i = this._keys.indexOf(key); - if (i === -1) { return false; } - this._values.splice(i, 1); - this._keys.splice(i, 1); - this.size--; - return true; - } - - clear(): void { - this._keys.length = 0; - this._values.length = 0; - this.size = 0; - } - - forEach(cb: Function, thisArg: any): void { - for (let i = 0; i < this.size; i++) { - cb.call(thisArg, this._values[i], this._keys[i]); - } - } -} \ No newline at end of file From 5eb6af7d90f9359a7e32fbd80230ef362b01189d Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Thu, 2 Nov 2017 20:41:25 -0700 Subject: [PATCH 2/3] refactor(asap): Remove setImmediate polyfill Gets rid of fancy polyfill and uses Promise to schedule instead, since Promise is required for several key parts of the library to work. Also setImmediate does not appear to be getting standardized. BREAKING CHANGE: Old runtimes must polyfill Promise in order to use ASAP scheduling. --- spec/util/Immediate-spec.ts | 728 ++---------------------------------- src/util/Immediate.ts | 249 +----------- 2 files changed, 48 insertions(+), 929 deletions(-) diff --git a/spec/util/Immediate-spec.ts b/spec/util/Immediate-spec.ts index 8c8eaea432..e95200fd04 100644 --- a/spec/util/Immediate-spec.ts +++ b/spec/util/Immediate-spec.ts @@ -1,703 +1,33 @@ import { expect } from 'chai'; -import * as sinon from 'sinon'; -import { ImmediateDefinition } from '../../src/util/Immediate'; -import * as Rx from '../../src/Rx'; - -declare const __root__: any; - -/** @test {ImmediateDefinition} */ -describe('ImmediateDefinition', () => { - let sandbox; - beforeEach(function () { - sandbox = sinon.sandbox.create(); - }); - - afterEach(function () { - sandbox.restore(); - }); - - it('should have setImmediate and clearImmediate methods', () => { - const result = new ImmediateDefinition(__root__); - expect(result.setImmediate).to.be.a('function'); - expect(result.clearImmediate).to.be.a('function'); - }); - - describe('when setImmediate exists on root', () => { - it('should use the setImmediate and clearImmediate methods from root', () => { - let setImmediateCalled = false; - let clearImmediateCalled = false; - - const root = { - setImmediate: () => { - setImmediateCalled = true; - }, - clearImmediate: () => { - clearImmediateCalled = true; - } - }; - - const result = new ImmediateDefinition(root); - - result.setImmediate(() => { - //noop - }); - result.clearImmediate(null); - - expect(setImmediateCalled).to.be.ok; - expect(clearImmediateCalled).to.be.ok; - }); - }); - - describe('prototype.createProcessNextTickSetImmediate()', () => { - it('should create the proper flavor of setImmediate using process.nextTick', () => { - const instance = { - root: { - process: { - nextTick: sinon.spy() - } - }, - runIfPresent: () => { - //noop - }, - partiallyApplied: sinon.spy(), - addFromSetImmediateArguments: sinon.stub().returns(123456) - }; - - const setImmediateImpl = ImmediateDefinition.prototype.createProcessNextTickSetImmediate.call(instance); - - expect(setImmediateImpl).to.be.a('function'); - - const action = () => { - //noop - }; - const handle = setImmediateImpl(action); - - expect(handle).to.equal(123456); - expect(instance.addFromSetImmediateArguments).have.been.called; - expect(instance.partiallyApplied).have.been.calledWith(instance.runIfPresent, handle); - }); - }); - - describe('prototype.createPostMessageSetImmediate()', () => { - it('should create the proper flavor of setImmediate using postMessage', () => { - let addEventListenerCalledWith = null; - - const instance = { - root: { - addEventListener: (name: any, callback: any) => { - addEventListenerCalledWith = [name, callback]; - }, - postMessage: sinon.spy(), - Math: { - random: sinon.stub().returns(42) - } - }, - runIfPresent: sinon.spy(), - addFromSetImmediateArguments: sinon.stub().returns(123456) - }; - - const setImmediateImpl = ImmediateDefinition.prototype.createPostMessageSetImmediate.call(instance); - - expect(setImmediateImpl).to.be.a('function'); - expect(addEventListenerCalledWith[0]).to.equal('message'); - - addEventListenerCalledWith[1]({ data: 'setImmediate$42$123456', source: instance.root }); - - expect(instance.runIfPresent).have.been.calledWith(123456); - - const action = () => { - //noop - }; - const handle = setImmediateImpl(action); - - expect(handle).to.equal(123456); - expect(instance.addFromSetImmediateArguments).have.been.called; - expect(instance.root.postMessage).have.been.calledWith('setImmediate$42$123456', '*'); - }); - }); - - describe('prototype.createMessageChannelSetImmediate', () => { - it('should create the proper flavor of setImmediate that uses message channels', () => { - const port1 = {}; - const port2 = { - postMessage: sinon.spy() - }; - - function MockMessageChannel() { - this.port1 = port1; - this.port2 = port2; - } - - const instance = { - root: { - MessageChannel: MockMessageChannel - }, - runIfPresent: sinon.spy(), - addFromSetImmediateArguments: sinon.stub().returns(123456) - }; - - const setImmediateImpl = ImmediateDefinition.prototype.createMessageChannelSetImmediate.call(instance); - - expect(setImmediateImpl).to.be.a('function'); - expect((port1).onmessage).to.be.a('function'); - - (port1).onmessage({ data: 'something' }); - - expect(instance.runIfPresent).have.been.calledWith('something'); - - const action = () => { - //noop - }; - const handle = setImmediateImpl(action); - - expect(handle).to.equal(123456); - expect(port2.postMessage).have.been.calledWith(123456); - }); - }); - - describe('prototype.createReadyStateChangeSetImmediate', () => { - it('should create the proper flavor of setImmediate that uses readystatechange on a DOM element', () => { - const fakeScriptElement = {}; - - const instance = { - root: { - document: { - createElement: sinon.stub().returns(fakeScriptElement), - documentElement: { - appendChild: sinon.spy(), - removeChild: sinon.spy(), - } - } - }, - runIfPresent: sinon.spy(), - addFromSetImmediateArguments: sinon.stub().returns(123456) - }; - - const setImmediateImpl = ImmediateDefinition.prototype.createReadyStateChangeSetImmediate.call(instance); - - expect(setImmediateImpl).to.be.a('function'); - - const action = () => { - //noop - }; - const handle = setImmediateImpl(action); - - expect(handle).to.equal(123456); - expect(instance.root.document.createElement).have.been.calledWith('script'); - expect((fakeScriptElement).onreadystatechange).to.be.a('function'); - expect(instance.root.document.documentElement.appendChild).have.been.calledWith(fakeScriptElement); - - (fakeScriptElement).onreadystatechange(); - - expect(instance.runIfPresent).have.been.calledWith(handle); - expect((fakeScriptElement).onreadystatechange).to.be.a('null'); - expect(instance.root.document.documentElement.removeChild).have.been.calledWith(fakeScriptElement); - }); - }); - - describe('when setImmediate does *not* exist on root', () => { - describe('when it can use process.nextTick', () => { - it('should use the post message impl', () => { - const nextTickImpl = () => { - //noop - }; - sandbox.stub(ImmediateDefinition.prototype, 'canUseProcessNextTick').returns(true); - sandbox.stub(ImmediateDefinition.prototype, 'canUsePostMessage').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUseMessageChannel').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUseReadyStateChange').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'createProcessNextTickSetImmediate').returns(nextTickImpl); - - const result = new ImmediateDefinition({}); - expect(ImmediateDefinition.prototype.canUseProcessNextTick).have.been.called; - expect(ImmediateDefinition.prototype.canUsePostMessage).not.have.been.called; - expect(ImmediateDefinition.prototype.canUseMessageChannel).not.have.been.called; - expect(ImmediateDefinition.prototype.canUseReadyStateChange).not.have.been.called; - expect(ImmediateDefinition.prototype.createProcessNextTickSetImmediate).have.been.called; - expect(result.setImmediate).to.equal(nextTickImpl); - }); - }); - - describe('when it cannot use process.nextTick', () => { - it('should use the post message impl', () => { - const postMessageImpl = () => { - //noop - }; - sandbox.stub(ImmediateDefinition.prototype, 'canUseProcessNextTick').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUsePostMessage').returns(true); - sandbox.stub(ImmediateDefinition.prototype, 'canUseMessageChannel').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUseReadyStateChange').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'createPostMessageSetImmediate').returns(postMessageImpl); - - const result = new ImmediateDefinition({}); - expect(ImmediateDefinition.prototype.canUseProcessNextTick).have.been.called; - expect(ImmediateDefinition.prototype.canUsePostMessage).have.been.called; - expect(ImmediateDefinition.prototype.canUseMessageChannel).not.have.been.called; - expect(ImmediateDefinition.prototype.canUseReadyStateChange).not.have.been.called; - expect(ImmediateDefinition.prototype.createPostMessageSetImmediate).have.been.called; - expect(result.setImmediate).to.equal(postMessageImpl); - }); - }); - - describe('when it cannot use process.nextTick or postMessage', () => { - it('should use the readystatechange impl', () => { - const messageChannelImpl = () => { - //noop - }; - sandbox.stub(ImmediateDefinition.prototype, 'canUseProcessNextTick').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUsePostMessage').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUseMessageChannel').returns(true); - sandbox.stub(ImmediateDefinition.prototype, 'canUseReadyStateChange').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'createMessageChannelSetImmediate').returns(messageChannelImpl); - - const result = new ImmediateDefinition({}); - expect(ImmediateDefinition.prototype.canUseProcessNextTick).have.been.called; - expect(ImmediateDefinition.prototype.canUsePostMessage).have.been.called; - expect(ImmediateDefinition.prototype.canUseMessageChannel).have.been.called; - expect(ImmediateDefinition.prototype.canUseReadyStateChange).not.have.been.called; - expect(ImmediateDefinition.prototype.createMessageChannelSetImmediate).have.been.called; - expect(result.setImmediate).to.equal(messageChannelImpl); - }); - }); - - describe('when it cannot use process.nextTick, postMessage or Message channels', () => { - it('should use the readystatechange impl', () => { - const readyStateChangeImpl = () => { - //noop - }; - sandbox.stub(ImmediateDefinition.prototype, 'canUseProcessNextTick').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUsePostMessage').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUseMessageChannel').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUseReadyStateChange').returns(true); - sandbox.stub(ImmediateDefinition.prototype, 'createReadyStateChangeSetImmediate').returns(readyStateChangeImpl); - - const result = new ImmediateDefinition({}); - expect(ImmediateDefinition.prototype.canUseProcessNextTick).have.been.called; - expect(ImmediateDefinition.prototype.canUsePostMessage).have.been.called; - expect(ImmediateDefinition.prototype.canUseMessageChannel).have.been.called; - expect(ImmediateDefinition.prototype.canUseReadyStateChange).have.been.called; - expect(ImmediateDefinition.prototype.createReadyStateChangeSetImmediate).have.been.called; - expect(result.setImmediate).to.equal(readyStateChangeImpl); - }); - }); - - describe('when no other methods to implement setImmediate are available', () => { - it('should use the setTimeout impl', () => { - const setTimeoutImpl = () => { - //noop - }; - sandbox.stub(ImmediateDefinition.prototype, 'canUseProcessNextTick').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUsePostMessage').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUseMessageChannel').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUseReadyStateChange').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'createSetTimeoutSetImmediate').returns(setTimeoutImpl); - - const result = new ImmediateDefinition({}); - expect(ImmediateDefinition.prototype.canUseProcessNextTick).have.been.called; - expect(ImmediateDefinition.prototype.canUsePostMessage).have.been.called; - expect(ImmediateDefinition.prototype.canUseMessageChannel).have.been.called; - expect(ImmediateDefinition.prototype.canUseReadyStateChange).have.been.called; - expect(ImmediateDefinition.prototype.createSetTimeoutSetImmediate).have.been.called; - expect(result.setImmediate).to.equal(setTimeoutImpl); - }); - }); - }); - - describe('partiallyApplied', () => { - describe('when passed a function as the first argument', () => { - it('should return a function that takes no arguments and will be called with the passed arguments', () => { - const fn = sinon.spy(); - const result = ImmediateDefinition.prototype.partiallyApplied(fn, 'arg1', 'arg2', 'arg3'); - - expect(result).to.be.a('function'); - expect(fn).not.have.been.called; - - result(); - - expect(fn).have.been.calledWith('arg1', 'arg2', 'arg3'); - }); - }); - - describe('when passed a non-function as an argument', () => { - it('should coerce to a string and convert to a function which will be called by the returned function', () => { - __root__.__wasCalled = null; - const fnStr = '__wasCalled = true;'; - const result = ImmediateDefinition.prototype.partiallyApplied(fnStr); - - expect(result).to.be.a('function'); - - result(); - - expect(__root__.__wasCalled).to.be.true; - - delete __root__.__wasCalled; - }); - }); - }); - - describe('prototype.identify', () => { - it('should use Object.toString to return an identifier string', () => { - function MockObject() { - //noop - } - sandbox.stub(MockObject.prototype, 'toString').returns('[object HEYO!]'); - - const instance = { - root: { - Object: MockObject - } - }; - - const result = (ImmediateDefinition).prototype.identify.call(instance); - - expect(result).to.equal('[object HEYO!]'); - }); - }); - - describe('prototype.canUseProcessNextTick', () => { - describe('when root.process does not identify as [object process]', () => { - it('should return false', () => { - const instance = { - root: { - process: {} - }, - identify: sinon.stub().returns('[object it-is-not-a-tumor]') - }; - - const result = ImmediateDefinition.prototype.canUseProcessNextTick.call(instance); - - expect(result).to.be.false; - expect(instance.identify).have.been.calledWith(instance.root.process); - }); - }); - - describe('when root.process identifies as [object process]', () => { - it('should return true', () => { - const instance = { - root: { - process: {} - }, - identify: sinon.stub().returns('[object process]') - }; - - const result = ImmediateDefinition.prototype.canUseProcessNextTick.call(instance); - - expect(result).to.be.true; - expect(instance.identify).have.been.calledWith(instance.root.process); - }); - }); - }); - - describe('prototype.canUsePostMessage', () => { - describe('when there is a global postMessage function', () => { - describe('and importScripts does NOT exist', () => { - it('should maintain any existing onmessage handler', () => { - const originalOnMessage = () => { - //noop - }; - const instance = { - root: { - onmessage: originalOnMessage - } - }; - - ImmediateDefinition.prototype.canUsePostMessage.call(instance); - expect(instance.root.onmessage).to.equal(originalOnMessage); - }); - - describe('and postMessage is synchronous', () => { - it('should return false', () => { - let postMessageCalled = false; - const instance = { - root: { - postMessage: function () { - postMessageCalled = true; - this.onmessage(); - } - } - }; - - const result = ImmediateDefinition.prototype.canUsePostMessage.call(instance); - expect(result).to.be.false; - expect(postMessageCalled).to.be.true; - }); - }); - - describe('and postMessage is asynchronous', () => { - it('should return true', () => { - let postMessageCalled = false; - const instance = { - root: { - postMessage: function () { - postMessageCalled = true; - const _onmessage = this.onmessage; - setTimeout(() => { _onmessage(); }); - } - } - }; - - const result = ImmediateDefinition.prototype.canUsePostMessage.call(instance); - expect(result).to.be.true; - expect(postMessageCalled).to.be.true; - }); - }); - }); - - describe('and importScripts *does* exist because it is a worker', () => { - it('should return false', () => { - const instance = { - root: { - postMessage: function () { - //noop - }, - importScripts: function () { - //noop - } - } - }; - - const result = ImmediateDefinition.prototype.canUsePostMessage.call(instance); - expect(result).to.be.false; - }); - }); - }); - - describe('when there is NOT a global postMessage function', () => { - it('should return false', () => { - const instance = { - root: {} - }; - - const result = ImmediateDefinition.prototype.canUsePostMessage.call(instance); - - expect(result).to.be.false; - }); - }); - }); - - describe('prototype.canUseMessageChannel', () => { - it('should return true if MessageChannel exists', () => { - const instance = { - root: { - MessageChannel: function () { - //noop - } - } - }; - - const result = ImmediateDefinition.prototype.canUseMessageChannel.call(instance); - - expect(result).to.be.true; - }); - - it('should return false if MessageChannel does NOT exist', () => { - const instance = { - root: {} - }; - - const result = ImmediateDefinition.prototype.canUseMessageChannel.call(instance); - - expect(result).to.be.false; - }); - }); - - describe('prototype.canUseReadyStateChange', () => { - describe('when there is a document in global scope', () => { - it('should return true if created script elements have an onreadystatechange property', () => { - const fakeScriptElement = { - onreadystatechange: null - }; - - const instance = { - root: { - document: { - createElement: sinon.stub().returns(fakeScriptElement) - } - } - }; - - const result = ImmediateDefinition.prototype.canUseReadyStateChange.call(instance); - - expect(result).to.be.true; - expect(instance.root.document.createElement).have.been.calledWith('script'); - }); - - it('should return false if created script elements do NOT have an onreadystatechange property', () => { - const fakeScriptElement = {}; - - const instance = { - root: { - document: { - createElement: sinon.stub().returns(fakeScriptElement) - } - } - }; - - const result = ImmediateDefinition.prototype.canUseReadyStateChange.call(instance); - - expect(result).to.be.false; - expect(instance.root.document.createElement).have.been.calledWith('script'); - }); - }); - - it('should return false if there is no document in global scope', () => { - const instance = { - root: {} - }; - - const result = ImmediateDefinition.prototype.canUseReadyStateChange.call(instance); - - expect(result).to.be.false; - }); - }); - - describe('prototype.addFromSetImmediateArguments', () => { - it('should add to tasksByHandle and increment the nextHandle', () => { - const partiallyAppliedResult = {}; - - const instance = { - tasksByHandle: {}, - nextHandle: 42, - partiallyApplied: sinon.stub().returns(partiallyAppliedResult) - }; - - const args = [() => { - //noop - }, 'foo', 'bar']; - - const handle = ImmediateDefinition.prototype.addFromSetImmediateArguments.call(instance, args); - - expect(handle).to.equal(42); - expect(instance.nextHandle).to.equal(43); - expect(instance.tasksByHandle[42]).to.equal(partiallyAppliedResult); - }); - }); - - describe('clearImmediate', () => { - it('should delete values from tasksByHandle', () => { - const setTimeoutImpl = () => { - //noop - }; - sandbox.stub(ImmediateDefinition.prototype, 'canUseProcessNextTick').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUsePostMessage').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUseMessageChannel').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'canUseReadyStateChange').returns(false); - sandbox.stub(ImmediateDefinition.prototype, 'createSetTimeoutSetImmediate').returns(setTimeoutImpl); - - const Immediate = new ImmediateDefinition({}); - Immediate.tasksByHandle[123456] = () => { - //noop - }; - - expect('123456' in Immediate.tasksByHandle).to.be.true; - - Immediate.clearImmediate(123456); - - expect('123456' in Immediate.tasksByHandle).to.be.false; - }); - }); - - describe('prototype.runIfPresent', () => { - it('should delay running the task if it is currently running a task', () => { - const mockApplied = () => { - //noop - }; - - const instance = { - root: { - setTimeout: sinon.spy(), - Object: Object - }, - currentlyRunningATask: true, - partiallyApplied: sinon.stub().returns(mockApplied) - }; - - ImmediateDefinition.prototype.runIfPresent.call(instance, 123456); - - expect(instance.partiallyApplied).have.been.calledWith((instance).runIfPresent, 123456); - expect(instance.root.setTimeout).have.been.calledWith(mockApplied, 0); - }); - - it('should not error if there is no task currently running and the handle passed is not found', () => { - expect(() => { - const instance = { - root: { - setTimeout: sinon.spy(), - Object: Object - }, - currentlyRunningATask: false, - tasksByHandle: {} - }; - - ImmediateDefinition.prototype.runIfPresent.call(instance, 888888); - }).not.to.throw(); - }); - - describe('when a task is found for the handle', () => { - it('should execute the task and clean up after', () => { - const instance = { - root: { - setTimeout: sinon.spy(), - Object: Object - }, - currentlyRunningATask: false, - tasksByHandle: {}, - clearImmediate: sinon.spy() - }; - - const spy = sinon.stub(); - - spy({ - task: function () { - expect(instance.currentlyRunningATask).to.be.true; - } - }); - instance.tasksByHandle[123456] = spy; - - ImmediateDefinition.prototype.runIfPresent.call(instance, 123456); - expect(instance.clearImmediate).have.been.calledWith(123456); - }); - }); - }); - - describe('prototype.createSetTimeoutSetImmediate', () => { - it('should create a proper setImmediate implementation that uses setTimeout', () => { - const mockApplied = () => { - //noop - }; - - const instance = { - root: { - setTimeout: sinon.spy() - }, - addFromSetImmediateArguments: sinon.stub().returns(123456), - runIfPresent: function () { - //noop - }, - partiallyApplied: sinon.stub().returns(mockApplied) - }; - - const setImmediateImpl = ImmediateDefinition.prototype.createSetTimeoutSetImmediate.call(instance); - - const handle = setImmediateImpl(); - - expect(handle).to.equal(123456); - expect(instance.addFromSetImmediateArguments).have.been.called; - expect(instance.root.setTimeout).have.been.calledWith(mockApplied, 0); - }); - }); - - describe('integration test', () => { - it('should work', (done: MochaDone) => { - const results = []; - Rx.Observable.from([1, 2, 3], Rx.Scheduler.asap) - .subscribe((x: number) => { - results.push(x); - }, () => { - done(new Error('should not be called')); - }, () => { - expect(results).to.deep.equal([1, 2, 3]); - done(); - }); +import { Immediate } from '../../src/util/Immediate'; + +describe('Immediate', () => { + it('should schedule on the next microtask', (done) => { + const results: number[] = []; + results.push(1); + setTimeout(() => results.push(5)); + Immediate.setImmediate(() => results.push(3)); + results.push(2); + Promise.resolve().then(() => results.push(4)); + + setTimeout(() => { + expect(results).to.deep.equal([1, 2, 3, 4, 5]); + done(); + }); + }); + + it('should cancel the task with clearImmediate', (done) => { + const results: number[] = []; + results.push(1); + setTimeout(() => results.push(5)); + const handle = Immediate.setImmediate(() => results.push(3)); + Immediate.clearImmediate(handle); + results.push(2); + Promise.resolve().then(() => results.push(4)); + + setTimeout(() => { + expect(results).to.deep.equal([1, 2, 4, 5]); + done(); }); }); }); diff --git a/src/util/Immediate.ts b/src/util/Immediate.ts index 918a41e298..b285b29ec5 100644 --- a/src/util/Immediate.ts +++ b/src/util/Immediate.ts @@ -2,237 +2,26 @@ Some credit for this helper goes to http://github.com/YuzuJS/setImmediate */ -import { root } from './root'; +let nextHandle = 0; -export class ImmediateDefinition { - setImmediate: (cb: () => void) => number; +const tasksByHandle: { [handle: string]: () => void } = {}; - clearImmediate: (handle: number) => void; - - private identify(o: any): string { - return this.root.Object.prototype.toString.call(o); - } - - tasksByHandle: any; - - nextHandle: number; - - currentlyRunningATask: boolean; - - constructor(private root: any) { - if (root.setImmediate && typeof root.setImmediate === 'function') { - this.setImmediate = root.setImmediate.bind(root); - this.clearImmediate = root.clearImmediate.bind(root); - } else { - this.nextHandle = 1; - this.tasksByHandle = {}; - this.currentlyRunningATask = false; - - // Don't get fooled by e.g. browserify environments. - if (this.canUseProcessNextTick()) { - // For Node.js before 0.9 - this.setImmediate = this.createProcessNextTickSetImmediate(); - } else if (this.canUsePostMessage()) { - // For non-IE10 modern browsers - this.setImmediate = this.createPostMessageSetImmediate(); - } else if (this.canUseMessageChannel()) { - // For web workers, where supported - this.setImmediate = this.createMessageChannelSetImmediate(); - } else if (this.canUseReadyStateChange()) { - // For IE 6–8 - this.setImmediate = this.createReadyStateChangeSetImmediate(); - } else { - // For older browsers - this.setImmediate = this.createSetTimeoutSetImmediate(); - } - - let ci = function clearImmediate(handle: any) { - delete (clearImmediate).instance.tasksByHandle[handle]; - }; - - (ci).instance = this; - - this.clearImmediate = ci; - } - } - - canUseProcessNextTick() { - return this.identify(this.root.process) === '[object process]'; - } - - canUseMessageChannel() { - return Boolean(this.root.MessageChannel); - } - - canUseReadyStateChange() { - const document = this.root.document; - return Boolean(document && 'onreadystatechange' in document.createElement('script')); - } - - canUsePostMessage() { - const root = this.root; - // The test against `importScripts` prevents this implementation from being installed inside a web worker, - // where `root.postMessage` means something completely different and can't be used for this purpose. - if (root.postMessage && !root.importScripts) { - let postMessageIsAsynchronous = true; - let oldOnMessage = root.onmessage; - root.onmessage = function() { - postMessageIsAsynchronous = false; - }; - root.postMessage('', '*'); - root.onmessage = oldOnMessage; - return postMessageIsAsynchronous; - } - - return false; - } - - // This function accepts the same arguments as setImmediate, but - // returns a function that requires no arguments. - partiallyApplied(handler: any, ...args: any[]) { - let fn = function result () { - const { handler, args } = result; - if (typeof handler === 'function') { - handler.apply(undefined, args); - } else { - (new Function('' + handler))(); - } - }; - - (fn).handler = handler; - (fn).args = args; - - return fn; - } - - addFromSetImmediateArguments(args: any[]) { - this.tasksByHandle[this.nextHandle] = this.partiallyApplied.apply(undefined, args); - return this.nextHandle++; - } - - createProcessNextTickSetImmediate() { - let fn = function setImmediate() { - const { instance } = (setImmediate); - let handle = instance.addFromSetImmediateArguments(arguments); - instance.root.process.nextTick(instance.partiallyApplied(instance.runIfPresent, handle)); - return handle; - }; - - (fn).instance = this; - - return fn; - } - - createPostMessageSetImmediate() { - // Installs an event handler on `global` for the `message` event: see - // * https://developer.mozilla.org/en/DOM/window.postMessage - // * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages - const root = this.root; - - let messagePrefix = 'setImmediate$' + root.Math.random() + '$'; - let onGlobalMessage = function globalMessageHandler(event: any) { - const instance = (globalMessageHandler).instance; - if (event.source === root && - typeof event.data === 'string' && - event.data.indexOf(messagePrefix) === 0) { - instance.runIfPresent(+event.data.slice(messagePrefix.length)); - } - }; - (onGlobalMessage).instance = this; - - root.addEventListener('message', onGlobalMessage, false); - - let fn = function setImmediate() { - const { messagePrefix, instance } = (setImmediate); - let handle = instance.addFromSetImmediateArguments(arguments); - instance.root.postMessage(messagePrefix + handle, '*'); - return handle; - }; - - (fn).instance = this; - (fn).messagePrefix = messagePrefix; - - return fn; - } - - runIfPresent(handle: any) { - // From the spec: 'Wait until any invocations of this algorithm started before this one have completed.' - // So if we're currently running a task, we'll need to delay this invocation. - if (this.currentlyRunningATask) { - // Delay by doing a setTimeout. setImmediate was tried instead, but in Firefox 7 it generated a - // 'too much recursion' error. - this.root.setTimeout(this.partiallyApplied(this.runIfPresent, handle), 0); - } else { - let task = this.tasksByHandle[handle]; - if (task) { - this.currentlyRunningATask = true; - try { - task(); - } finally { - this.clearImmediate(handle); - this.currentlyRunningATask = false; - } - } - } - } - - createMessageChannelSetImmediate() { - let channel = new this.root.MessageChannel(); - channel.port1.onmessage = (event: any) => { - let handle = event.data; - this.runIfPresent(handle); - }; - - let fn = function setImmediate() { - const { channel, instance } = (setImmediate); - let handle = instance.addFromSetImmediateArguments(arguments); - channel.port2.postMessage(handle); - return handle; - }; - - (fn).channel = channel; - (fn).instance = this; - - return fn; - } - - createReadyStateChangeSetImmediate() { - let fn = function setImmediate() { - const instance = (setImmediate).instance; - const root = instance.root; - const doc = root.document; - const html = doc.documentElement; - - let handle = instance.addFromSetImmediateArguments(arguments); - // Create a