diff --git a/loader.js b/loader.js index cf3256a..42d6134 100644 --- a/loader.js +++ b/loader.js @@ -2,8 +2,8 @@ try { module.exports = require('workerize-loader'); } catch (e) { - console.warn("Warning: workerize-loader is not installed."); + console.warn('Warning: workerize-loader is not installed.'); module.exports = function() { - throw "To use workerize as a loader, you must install workerize-loader."; - } -} \ No newline at end of file + throw 'To use workerize as a loader, you must install workerize-loader.'; + }; +} diff --git a/package.json b/package.json index d5e8577..36beb54 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build": "microbundle", "prepublishOnly": "npm run build", "release": "npm t && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish", - "test": "echo \"Error: no test specified\" && exit 0" + "test": "eslint *.js && npm run -s build && karmatic --no-coverage" }, "eslintConfig": { "extends": "eslint-config-developit", @@ -35,6 +35,8 @@ "devDependencies": { "eslint": "^4.16.0", "eslint-config-developit": "^1.1.1", - "microbundle": "^0.4.3" + "microbundle": "^0.4.3", + "karmatic": "^1.4.0", + "webpack": "^4.29.6" } } diff --git a/src/index.js b/src/index.js index 75a5b44..c465ce6 100644 --- a/src/index.js +++ b/src/index.js @@ -36,10 +36,38 @@ export default function workerize(code, options) { URL.revokeObjectURL(url); term.call(worker); }; - worker.call = (method, params) => new Promise( (resolve, reject) => { + worker.call = (method, params, genStatus=0, genId=undefined) => new Promise( (resolve, reject) => { let id = `rpc${++counter}`; callbacks[id] = [resolve, reject]; - worker.postMessage({ type: 'RPC', id, method, params }); + worker.postMessage({ type: 'RPC', id, genId, method, genStatus, params }); + }).then((d) => { + if (!d.hasOwnProperty('genId')) { + return d; + } + return (() => { + const genId = d.genId; + return { + done: false, + async next (value) { + if (this.done) { return { value: undefined, done: true }; } + const result = await worker.call(method, [value], 0, genId); + if (result.done) { return this.return(result.value); } + return result; + }, + async return (value) { + await worker.call(method, [value], 1, genId); + this.done = true; + return { value, done: true }; + }, + async throw (err) { + await worker.call(method, [err], 0, genId); + throw err; + }, + [Symbol.asyncIterator] () { + return this; + } + }; + })(); }); worker.rpcMethods = {}; setup(worker, worker.rpcMethods, callbacks); @@ -51,10 +79,13 @@ export default function workerize(code, options) { for (i in exports) if (!(i in worker)) worker.expose(i); return worker; } - function setup(ctx, rpcMethods, callbacks) { + let gencounter = 0; + let GENS = {}; ctx.addEventListener('message', ({ data }) => { let id = data.id; + let genId = data.genId; + let genStatus = data.genStatus; if (data.type!=='RPC' || id==null) return; if (data.method) { let method = rpcMethods[data.method]; @@ -63,8 +94,22 @@ function setup(ctx, rpcMethods, callbacks) { } else { Promise.resolve() - .then( () => method.apply(null, data.params) ) - .then( result => { ctx.postMessage({ type: 'RPC', id, result }); }) + // Either use a generator or call a method. + .then( () => !GENS[genId] ? method.apply(null, data.params) : GENS[genId][genStatus](data.params[0])) + .then( result => { + if (method.constructor.name === 'AsyncGeneratorFunction' || method.constructor.name === 'GeneratorFunction') { + if (!GENS[genId]) { + GENS[++gencounter] = [result.next.bind(result), result.return.bind(result), result.throw.bind(result)]; + // return an initial message of success. + // genId should only be sent to the main thread when initializing the generator + return ctx.postMessage({ type: 'RPC', id, genId: gencounter, result: { value: undefined, done: false } }); + } + } + ctx.postMessage({ type: 'RPC', id, result }); + if (result.done) { + GENS[genId] = null; + } + }) .catch( err => { ctx.postMessage({ type: 'RPC', id, error: ''+err }); }); } } @@ -72,7 +117,10 @@ function setup(ctx, rpcMethods, callbacks) { let callback = callbacks[id]; if (callback==null) throw Error(`Unknown callback ${id}`); delete callbacks[id]; + // genId should only be sent to the main thread when initializing the generator + if(data.genId) { data.result.genId = data.genId; } if (data.error) callback[1](Error(data.error)); + // genId should only be sent to the main thread when initializing the generator else callback[0](data.result); } }); diff --git a/src/index.test.js b/src/index.test.js new file mode 100644 index 0000000..3829e4a --- /dev/null +++ b/src/index.test.js @@ -0,0 +1,276 @@ +import workerize from './index.js'; + +describe('workerize', () => { + it('should return an async function', () => { + const w = workerize(` + export function f(url) { + return 'one' + } + `); + + + expect(w.f).toEqual(jasmine.any(Function)); + expect(w.f()).toEqual(jasmine.any(Promise)); + }); + + it('should be able to return multiple exported functions', () => { + const w = workerize(` + export function f(url) { + return 'one' + } + + export function g(url) { + return 'two' + } + `); + + + expect(w.f).toEqual(jasmine.any(Function)); + expect(w.g).toEqual(jasmine.any(Function)); + }); + + it('should not expose non exported functions', () => { + const w = workerize(` + function f () { + + } + `); + + + expect(w.f).toEqual(undefined); + }); + + it('should return an async generator function', async () => { + const w = workerize(` + export function * g(url) { + return 'one' + } + `); + // expect that it has an iterator + const p = w.g(); + expect(p).toEqual(jasmine.any(Promise)); + expect((await p)[Symbol.asyncIterator]).toEqual(jasmine.any(Function)); + }); + + it('should invoke sync functions', async () => { + const w = workerize(` + export function foo (a) { + return 'foo: '+a; + }; + `); + + let ret = await w.foo('test'); + expect(ret).toEqual('foo: test'); + }); + + it('should forward arguments', async () => { + const w = workerize(` + export function foo() { + return { + args: [].slice.call(arguments) + }; + } + `); + + let ret = await w.foo('a', 'b', 'c', { position: 4 }); + expect(ret).toEqual({ + args: ['a', 'b', 'c', { position: 4 }] + }); + }); + + it('should invoke async functions', async () => { + let w = workerize(` + export function bar (a) { + return new Promise(resolve => { + resolve('bar: ' + a); + }) + }; + `); + + let ret = await w.bar('test'); + expect(ret).toEqual('bar: test'); + }); + + it('should take values from next', async () => { + let w = workerize(` + export function* g () { + const num2 = yield 1; + yield 2 + num2; + } + `); + + const it = await w.g(); + expect((await it.next()).value).toEqual(1); + expect((await it.next(2)).value).toEqual(4); + }); + + it('should return both done as true and the value', async () => { + // eslint-disable-next-line require-yield + function* f (num1) { + return num1; + } + let w = workerize(`export ${Function.prototype.toString.call(f)}`); + + const it = await w.f(3); + const it2 = f(3); + const { done, value } = (await it.next()); + const { done: done2, value: value2 } = (await it2.next()); + + expect(value).toEqual(value2); + expect(done).toEqual(done2); + }); + + it('should only iterate yielded values with for await of', async () => { + let w = workerize(` + export function* g() { + yield 3; + yield 1; + yield 4; + return 1; + } + `); + + const arr = []; + for await (const item of await w.g()) { + arr.push(item); + } + + expect(arr[0]).toEqual(3); + expect(arr[1]).toEqual(1); + expect(arr[2]).toEqual(4); + expect(arr[3]).toEqual(undefined); + }); + + it('should return early with return method of async iterator', async () => { + let w = workerize(` + export function* g() { + yield 1; + yield 2; + yield 3; + return 4; + } + `); + + + const it = await w.g(); + expect([ + await it.next(), + await it.next(), + await it.return(7), + await it.next(), + await it.next() + ]).toEqual([ + { value: 1, done: false }, + { value: 2, done: false }, + { value: 7, done: true }, + { value: undefined, done: true }, + { value: undefined, done: true } + ]); + }); + + it('should throw early with return method of async iterator', async () => { + let w = workerize(` + export function* g() { + yield 1; + yield 2; + yield 3; + return 4; + } + `); + + + const it = await w.g(); + // expect this to reject! + await (async () => ([ + await it.next(), + await it.return(), + await it.throw('foo'), + await it.next(), + await it.next() + ]))().then(() => { + throw new Error('Promise should not have resolved'); + }, () => { /** since it should error, we recover and ignore the error */}); + }); + + it('should act like an equivalent async iterator', async () => { + async function* g () { + const num2 = yield 1; + yield 2 + num2; + yield 3; + return 4; + } + + let w = workerize(`export ${Function.prototype.toString.call(g)}`); + + + const it = await w.g(); + const it2 = g(); + expect([ + await it.next(), + await it.next(2), + await it.next(), + await it.next(), + await it.next() + ]).toEqual([ + await it2.next(), + await it2.next(2), + await it2.next(), + await it2.next(), + await it2.next() + ]); + }); + + it('should throw like an equivalent async iterator', async () => { + async function* g () { + const num2 = yield 1; + yield 2 + num2; + yield 3; + return 4; + } + + let w = workerize(`export ${Function.prototype.toString.call(g)}`); + + + const it = await w.g(); + const it2 = g(); + expect([ + await it.next(), + await it.next(2), + await it.throw().catch(e => 2), + await it.return(), + await it.throw().catch(e => 3) + ]).toEqual([ + await it2.next(), + await it2.next(2), + await it2.throw().catch(e => 2), + await it2.return(), + await it2.throw().catch(e => 3) + ]); + }); + + it('should return like an equivalent async iterator', async () => { + async function* g () { + const num2 = yield 1; + yield 2 + num2; + yield 3; + return 4; + } + + let w = workerize(`export ${Function.prototype.toString.call(g)}`); + + + const it = await w.g(); + const it2 = g(); + expect([ + await it.next(), + await it.next(2), + await it.return(), + await it.return() + ]).toEqual([ + await it2.next(), + await it2.next(2), + await it2.return(), + await it2.return() + ]); + }); +});