From 2e9513153bbd6da16a319e5164efa6d11baea121 Mon Sep 17 00:00:00 2001 From: John Johnson II Date: Mon, 30 Dec 2019 01:25:47 -0700 Subject: [PATCH 1/2] generator and async generator support fixes #38 First thing to note about this is that I broke the microbundle build. I'll attempt to fix the generator function to not use things microbundle doesn't expect, but I'm not sure how far I'll get with that. Which that's also something tying up this pull request: https://github.com/developit/greenlet/pull/50 One other thing to note about this is that I decided on returning a promise of the asyncIterator from the generator function. This is due to the fact that the function from the user only ever gets evaluated as an executable function on the worker side. --- src/index.js | 50 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/index.js b/src/index.js index 75a5b44..9268d99 100644 --- a/src/index.js +++ b/src/index.js @@ -36,11 +36,30 @@ 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 => !d.hasOwnProperty("genId") ? d : (async function* workerAsyncGenPassthrough () { + const genId = d.genId; + try { + let result = {done: false}; + let value; + while (!result.done) { + result = await worker.call(method, [value], 0, genId); + if (result.done) { break; } + value = yield result.value; + } + return result.value; + } + catch (err) { + await worker.call(method, ['' + err], 2, genId); + throw err; + } + finally { + await worker.call(method, [undefined], 1, genId); + } + })()); worker.rpcMethods = {}; setup(worker, worker.rpcMethods, callbacks); worker.expose = methodName => { @@ -51,10 +70,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 +85,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,6 +108,8 @@ 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)); else callback[0](data.result); } From 3a8bd4324ecbc6007999208fc881845156627825 Mon Sep 17 00:00:00 2001 From: John Johnson II Date: Tue, 31 Dec 2019 17:28:17 -0700 Subject: [PATCH 2/2] build passes, also added linting and tests --- loader.js | 8 +- package.json | 6 +- src/index.js | 46 +++++--- src/index.test.js | 276 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 312 insertions(+), 24 deletions(-) create mode 100644 src/index.test.js 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 9268d99..c465ce6 100644 --- a/src/index.js +++ b/src/index.js @@ -40,26 +40,35 @@ export default function workerize(code, options) { let id = `rpc${++counter}`; callbacks[id] = [resolve, reject]; worker.postMessage({ type: 'RPC', id, genId, method, genStatus, params }); - }).then(d => !d.hasOwnProperty("genId") ? d : (async function* workerAsyncGenPassthrough () { - const genId = d.genId; - try { - let result = {done: false}; - let value; - while (!result.done) { - result = await worker.call(method, [value], 0, genId); - if (result.done) { break; } - value = yield result.value; - } - return result.value; - } - catch (err) { - await worker.call(method, ['' + err], 2, genId); + }).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; } - finally { - await worker.call(method, [undefined], 1, genId); - } - })()); + }; + })(); + }); worker.rpcMethods = {}; setup(worker, worker.rpcMethods, callbacks); worker.expose = methodName => { @@ -111,6 +120,7 @@ function setup(ctx, rpcMethods, callbacks) { // 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() + ]); + }); +});