Skip to content

Commit

Permalink
Initial release!
Browse files Browse the repository at this point in the history
  • Loading branch information
developit committed Jan 31, 2018
0 parents commit dc9a7d4
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
package-lock.json
.DS_Store
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# jsdom-worker

This is an experimental implementation of the Web Worker API (specifically Dedicated Worker) for JSDOM.

It does not currently do any real threading, rather it implements the `Worker` interface but all work is done in the current thread. `jsdom-worker` runs wherever JSDOM runs, and does not require Node.

It supports both "inline" _(created via Blob)_ and standard _(loaded via URL)_ workers.

> **Hot Take:** this module likely works in the browser, where it could act as a simple inline worker "poorlyfill".
## Why?

Jest uses a JSDOM environment by default, which means it doesn't support Workers. This means it is impossible to test code that requires both NodeJS functionality _and_ Web Workers. `jsdom-worker` implements enough of the Worker spec that it is now possible to do so.

## Installation

`npm i jsdom-worker`

## Example

```js
import 'jsdom-global/register'
import 'jsdom-worker'

let code = `onmessage = e => postMessage(e.data*2)`
let worker = new Worker(URL.createObjectURL(new Blob([code])))
worker.onmessage = console.log
worker.postMessage(5) // 10
```

## Usage with Jest

For single tests, simply add `import 'jsdom-worker'` to your module.

Otherwise, add it via the [setupFiles](https://facebook.github.io/jest/docs/en/configuration.html#setupfiles-array) Jest config option:

```js
{
"setupFiles": [
"jsdom-worker"
]
}
```

## License

[MIT License](https://oss.ninja/mit/developit)
44 changes: 44 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "jsdom-worker",
"version": "0.1.0",
"description": "Experimental Web Worker API implementation for JSDOM.",
"main": "dist/jsdom-inline-worker.js",
"module": "dist/jsdom-inline-worker.m.js",
"scripts": {
"prepare": "microbundle --external all",
"test": "eslint src test && npm run -s prepare && jest"
},
"babel": {
"presets": [
"env"
]
},
"keywords": [
"jsdom",
"web worker"
],
"eslintConfig": {
"extends": "eslint-config-developit"
},
"author": "Jason Miller <jason@developit.ca> (http://jasonformat.com)",
"license": "MIT",
"files": [
"dist"
],
"devDependencies": {
"babel-jest": "^22.1.0",
"babel-preset-env": "^1.6.1",
"eslint": "^4.16.0",
"eslint-config-developit": "^1.1.1",
"jest": "^22.1.4",
"microbundle": "^0.4.3",
"node-fetch": "^1.7.3"
},
"peerDependencies": {
"node-fetch": "*"
},
"dependencies": {
"mitt": "^1.1.3",
"uuid-v4": "^0.1.0"
}
}
86 changes: 86 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import mitt from 'mitt';
import uuid from 'uuid-v4';
import fetch, { Response } from 'node-fetch';

if (!global.URL) global.URL = {};
if (!global.URL.$$objects) {
global.URL.$$objects = new Map();
global.URL.createObjectURL = blob => {
let id = uuid();
global.URL.$$objects[id] = blob;
return `blob:http://localhost/${id}`;
};

let oldFetch = global.fetch || fetch;
global.fetch = function(url, opts) {
if (url.match(/^blob:/)) {
return new Promise( (resolve, reject) => {
let fr = new FileReader();
fr.onload = () => {
let Res = global.Response || Response;
resolve(new Res(fr.result, { status: 200, statusText: 'OK' }));
};
fr.onerror = () => {
reject(fr.error);
};
let id = url.match(/[^/]+$/)[0];
fr.readAsText(global.URL.$$objects[id]);
});
}
return oldFetch.call(this, url, opts);
};
}

if (!global.document) {
global.document = {};
}

function Event(type) { this.type = type; }
if (!global.document.createEvent) {
global.document.createEvent = function(type) {
let Ctor = global[type] || Event;
return new Ctor(type);
};
}


global.Worker = function Worker(url) {
let messageQueue = [],
inside = mitt(),
outside = mitt(),
scope = {
onmessage: null,
dispatchEvent: inside.emit,
addEventListener: inside.on,
removeEventListener: inside.off,
postMessage(data) {
outside.emit('message', { data });
},
fetch: global.fetch
},
getScopeVar;
inside.on('message', e => { let f = getScopeVar('onmessage'); if (f) f.call(scope, e); });
this.addEventListener = outside.on;
this.removeEventListener = outside.off;
this.dispatchEvent = outside.emit;
outside.on('message', e => { this.onmessage && this.onmessage(e); });
this.postMessage = data => {
if (messageQueue!=null) messageQueue.push(data);
else inside.emit('message', { data });
};
this.terminate = () => {
throw Error('Not Supported');
};
global.fetch(url)
.then( r => r.text() )
.then( code => {
let vars = 'var self=this,global=self';
for (let k in scope) vars += `,${k}=self.${k}`;
// eval('(function() {'+vars+'\n'+code+'\n})').call(scope);
getScopeVar = eval('(function() {'+vars+'\n'+code+'\nreturn function(__){return eval(__)}})').call(scope);
let q = messageQueue;
messageQueue = null;
q.forEach(this.postMessage);
})
.catch( e => { outside.emit('error', e); console.error(e); });
};
14 changes: 14 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'jsdom-worker';

const sleep = t => new Promise( r => { setTimeout(r, t); });

describe('jsdom-worker', () => {
it('should work', async () => {
let code = `onmessage = e => { postMessage(e.data*2) }`;
let worker = new Worker(URL.createObjectURL(new Blob([code])));
worker.onmessage = jest.fn();
worker.postMessage(5);
await sleep(10);
expect(worker.onmessage).toHaveBeenCalledWith({ data: 10 });
});
});

0 comments on commit dc9a7d4

Please sign in to comment.