├── .gitignore ├── .travis.yml ├── README.md ├── greenlet.js ├── greenlet.test.js ├── index.d.ts └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /package-lock.json 4 | /npm-debug.log 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | dist: trusty 5 | sudo: false 6 | addons: 7 | chrome: stable 8 | cache: 9 | npm: true 10 | directories: 11 | - node_modules 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Greenlet 3 |

4 | 5 | ## Greenlet [![npm](https://img.shields.io/npm/v/greenlet.svg)](https://npm.im/greenlet) [![travis](https://travis-ci.org/developit/greenlet.svg?branch=master)](https://travis-ci.org/developit/greenlet) [![gzip size](http://img.badgesize.io/https://unpkg.com/greenlet/dist/greenlet.js?compression=gzip)](https://unpkg.com/greenlet/dist/greenlet.umd.js) [![install size](https://packagephobia.now.sh/badge?p=greenlet)](https://packagephobia.now.sh/result?p=greenlet) 6 | 7 | > Move an async function into its own thread. 8 | > 9 | > A simplified single-function version of [workerize](https://github.com/developit/workerize), offering [the same performance as direct Worker usage](https://esbench.com/bench/5b16b61af2949800a0f61ce3). 10 | 11 | The name is somewhat of a poor choice, but it was [available on npm](https://npm.im/greenlet). 12 | 13 | _Greenlet supports IE10+, since it uses [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers). For NodeJS usage, Web Workers must be polyfilled using a library like [node-webworker](https://github.com/pgriess/node-webworker)._ 14 | 15 | ## Installation & Usage 16 | 17 | ```sh 18 | npm i -S greenlet 19 | ``` 20 | 21 | Accepts an async function with, produces a copy of it that runs within a Web Worker. 22 | 23 | > ⚠️ **Caveat:** the function you pass cannot rely on its surrounding scope, since it is executed in an isolated context. 24 | 25 | ``` 26 | greenlet(Function) -> Function 27 | ``` 28 | 29 | > ‼️ **Important:** never call greenlet() dynamically. Doing so creates a new Worker thread for every call: 30 | 31 | ```diff 32 | -const BAD = () => greenlet(x => x)('bad') // creates a new thread on every call 33 | +const fn = greenlet(x => x); 34 | +const GOOD = () => fn('good'); // uses the same thread on every call 35 | ``` 36 | 37 | Since Greenlets can't rely on surrounding scope anyway, it's best to always create them at the "top" of your module. 38 | 39 | 40 | ## Example 41 | 42 | Greenlet is most effective when the work being done has relatively small inputs/outputs. 43 | 44 | One such example would be fetching a network resource when only a subset of the resulting information is needed: 45 | 46 | ```js 47 | import greenlet from 'greenlet' 48 | 49 | let getName = greenlet( async username => { 50 | let url = `https://api.github.com/users/${username}` 51 | let res = await fetch(url) 52 | let profile = await res.json() 53 | return profile.name 54 | }) 55 | 56 | console.log(await getName('developit')) 57 | ``` 58 | 59 | [🔄 **Run this example on JSFiddle**](https://jsfiddle.net/developit/mf9fbma5/) 60 | 61 | 62 | ## Transferable ready 63 | 64 | Greenlet will even accept and optimize [transferables](https://developer.mozilla.org/en-US/docs/Web/API/Transferable) as arguments to and from a greenlet worker function. 65 | 66 | 67 | ## Browser support 68 | 69 | Thankfully, Web Workers have been around for a while and [are broadly supported](https://caniuse.com/#feat=webworkers) by Chrome, Firefox, Safari, Edge, and Internet Explorer 10+. 70 | 71 | If you still need to support older browsers, you can just check for the presence of `window.Worker`: 72 | 73 | ```js 74 | if (window.Worker) { 75 | ... 76 | } else { 77 | ... 78 | } 79 | ``` 80 | 81 | ### CSP 82 | 83 | If your app has a [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy), Greenlet requires `worker-src blob:` and `script-src blob:` in your config. 84 | 85 | ## License & Credits 86 | 87 | > In addition to the contributors, credit goes to [@sgb-io](https://github.com/sgb-io) for his annotated exploration of Greenlet's source. This prompted a refactor that clarified the code and allowed for further size optimizations. 88 | 89 | [MIT License](https://oss.ninja/mit/developit) 90 | -------------------------------------------------------------------------------- /greenlet.js: -------------------------------------------------------------------------------- 1 | /** Move an async function into its own thread. 2 | * @param {Function} asyncFunction An (async) function to run in a Worker. 3 | * @public 4 | */ 5 | export default function greenlet(asyncFunction) { 6 | // A simple counter is used to generate worker-global unique ID's for RPC: 7 | let currentId = 0; 8 | 9 | // Outward-facing promises store their "controllers" (`[request, reject]`) here: 10 | const promises = {}; 11 | 12 | // Use a data URI for the worker's src. It inlines the target function and an RPC handler: 13 | const script = '$$='+asyncFunction+';onmessage='+(e => { 14 | /* global $$ */ 15 | 16 | // Invoking within then() captures exceptions in the supplied async function as rejections 17 | Promise.resolve(e.data[1]).then( 18 | v => $$.apply($$, v) 19 | ).then( 20 | // success handler - callback(id, SUCCESS(0), result) 21 | // if `d` is transferable transfer zero-copy 22 | d => { 23 | postMessage([e.data[0], 0, d], [d].filter(x => ( 24 | (x instanceof ArrayBuffer) || 25 | (x instanceof MessagePort) || 26 | (self.ImageBitmap && x instanceof ImageBitmap) 27 | ))); 28 | }, 29 | // error handler - callback(id, ERROR(1), error) 30 | er => { postMessage([e.data[0], 1, '' + er]); } 31 | ); 32 | }); 33 | const workerURL = URL.createObjectURL(new Blob([script])); 34 | // Create an "inline" worker (1:1 at definition time) 35 | const worker = new Worker(workerURL); 36 | 37 | /** Handle RPC results/errors coming back out of the worker. 38 | * Messages coming from the worker take the form `[id, status, result]`: 39 | * id - counter-based unique ID for the RPC call 40 | * status - 0 for success, 1 for failure 41 | * result - the result or error, depending on `status` 42 | */ 43 | worker.onmessage = e => { 44 | // invoke the promise's resolve() or reject() depending on whether there was an error. 45 | promises[e.data[0]][e.data[1]](e.data[2]); 46 | 47 | // ... then delete the promise controller 48 | promises[e.data[0]] = null; 49 | }; 50 | 51 | // Return a proxy function that forwards calls to the worker & returns a promise for the result. 52 | return function (args) { 53 | args = [].slice.call(arguments); 54 | return new Promise(function () { 55 | // Add the promise controller to the registry 56 | promises[++currentId] = arguments; 57 | 58 | // Send an RPC call to the worker - call(id, params) 59 | // The filter is to provide a list of transferables to send zero-copy 60 | worker.postMessage([currentId, args], args.filter(x => ( 61 | (x instanceof ArrayBuffer) || 62 | (x instanceof MessagePort) || 63 | (self.ImageBitmap && x instanceof ImageBitmap) 64 | ))); 65 | }); 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /greenlet.test.js: -------------------------------------------------------------------------------- 1 | import greenlet from 'greenlet'; 2 | 3 | describe('greenlet', () => { 4 | it('should return an async function', () => { 5 | let g = greenlet( () => 'one' ); 6 | expect(g).toEqual(jasmine.any(Function)); 7 | expect(g()).toEqual(jasmine.any(Promise)); 8 | }); 9 | 10 | it('should invoke sync functions', async () => { 11 | let foo = greenlet( a => 'foo: '+a ); 12 | 13 | let ret = await foo('test'); 14 | expect(ret).toEqual('foo: test'); 15 | }); 16 | 17 | it('should forward arguments', async () => { 18 | let foo = greenlet(function() { 19 | return { 20 | args: [].slice.call(arguments) 21 | }; 22 | }); 23 | 24 | let ret = await foo('a', 'b', 'c', { position: 4 }); 25 | expect(ret).toEqual({ 26 | args: ['a', 'b', 'c', { position: 4 }] 27 | }); 28 | }); 29 | 30 | it('should invoke async functions', async () => { 31 | let bar = greenlet( a => new Promise( resolve => { 32 | resolve('bar: '+a); 33 | })); 34 | 35 | let ret = await bar('test'); 36 | expect(ret).toEqual('bar: test'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | type AsyncFunction = (...args: S) => Promise; 2 | 3 | type MaybeAsyncFunction = (...args: S) => (T | Promise); 4 | 5 | export default function greenlet(fn: MaybeAsyncFunction): AsyncFunction; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "greenlet", 3 | "version": "1.1.0", 4 | "description": "Move an async function into its own thread.", 5 | "source": "greenlet.js", 6 | "main": "dist/greenlet.js", 7 | "module": "dist/greenlet.m.js", 8 | "types": "./index.d.ts", 9 | "scripts": { 10 | "prepare": "microbundle", 11 | "test": "eslint *.js && npm run -s prepare && karmatic --no-coverage", 12 | "release": "npm run -s prepare && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" 13 | }, 14 | "eslintConfig": { 15 | "extends": "eslint-config-developit", 16 | "rules": { 17 | "prefer-spread": 0, 18 | "prefer-rest-params": 0 19 | } 20 | }, 21 | "files": [ 22 | "greenlet.js", 23 | "index.d.ts", 24 | "dist" 25 | ], 26 | "repository": "developit/greenlet", 27 | "keywords": [ 28 | "greenlet", 29 | "thread", 30 | "async", 31 | "worker", 32 | "web worker" 33 | ], 34 | "author": "Jason Miller (http://jasonformat.com)", 35 | "license": "MIT", 36 | "homepage": "https://github.com/developit/greenlet", 37 | "devDependencies": { 38 | "eslint": "^4.16.0", 39 | "eslint-config-developit": "^1.1.1", 40 | "karmatic": "^1.4.0", 41 | "microbundle": "^0.4.3", 42 | "webpack": "^4.29.6" 43 | } 44 | } 45 | --------------------------------------------------------------------------------