├── .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 |
3 |
4 |
5 | ## Greenlet [](https://npm.im/greenlet) [](https://travis-ci.org/developit/greenlet) [](https://unpkg.com/greenlet/dist/greenlet.umd.js) [](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 |
--------------------------------------------------------------------------------