Favivon - Correção

This commit is contained in:
2026-05-30 19:59:39 -03:00
parent 76ddaa815d
commit d7dfd221f0
32859 changed files with 5459654 additions and 404 deletions
+367
View File
@@ -0,0 +1,367 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.52.0](https://github.com/protectwise/troika/compare/v0.51.1...v0.52.0) (2024-11-11)
**Note:** Version bump only for package troika-worker-utils
## [0.51.1](https://github.com/protectwise/troika/compare/v0.51.0...v0.51.1) (2024-11-11)
### Bug Fixes
* **troika-worker-utils:** Avoid no-workers log errors when forcing main thread ([d396e51](https://github.com/protectwise/troika/commit/d396e51708d151d9cee04394d3cd8ac87c1a7dc0)), closes [#337](https://github.com/protectwise/troika/issues/337)
# [0.51.0](https://github.com/protectwise/troika/compare/v0.50.3...v0.51.0) (2024-11-10)
### Features
* **troika-worker-utils:** Add a handle to force running worker modules on main thread ([4f8bc13](https://github.com/protectwise/troika/commit/4f8bc13cf32a1665e2177ab2f62f60f2fe17ad66))
# [0.50.0](https://github.com/protectwise/troika/compare/v0.49.1...v0.50.0) (2024-10-11)
**Note:** Version bump only for package troika-worker-utils
# [0.49.0](https://github.com/protectwise/troika/compare/v0.48.1...v0.49.0) (2023-10-08)
**Note:** Version bump only for package troika-worker-utils
# [0.48.0](https://github.com/protectwise/troika/compare/v0.47.2...v0.48.0) (2023-09-09)
**Note:** Version bump only for package troika-worker-utils
## [0.47.2](https://github.com/protectwise/troika/compare/v0.47.1...v0.47.2) (2023-05-15)
**Note:** Version bump only for package troika-worker-utils
# [0.47.0](https://github.com/protectwise/troika/compare/v0.46.3...v0.47.0) (2022-12-15)
### Features
* remove custom Thenable polyfill in favor of native promises ([7af402e](https://github.com/protectwise/troika/commit/7af402e254675ca2fc182467a65d2d4f860845e4))
# [0.46.0](https://github.com/protectwise/troika/compare/v0.45.0...v0.46.0) (2022-03-05)
**Note:** Version bump only for package troika-worker-utils
# [0.45.0](https://github.com/protectwise/troika/compare/v0.44.0...v0.45.0) (2022-01-02)
**Note:** Version bump only for package troika-worker-utils
# [0.44.0](https://github.com/protectwise/troika/compare/v0.43.1-alpha.0...v0.44.0) (2021-11-14)
**Note:** Version bump only for package troika-worker-utils
# [0.43.0](https://github.com/protectwise/troika/compare/v0.42.0...v0.43.0) (2021-09-20)
### Bug Fixes
* remove warnings about many open worker module requests ([164fb8f](https://github.com/protectwise/troika/commit/164fb8fe836b95fffe7ca94fcf96d536c7281f7e)), closes [#156](https://github.com/protectwise/troika/issues/156)
### Features
* **troika-worker-utils:** add a terminateWorker function ([33b8455](https://github.com/protectwise/troika/commit/33b8455c3794e13a342a7910d6c2c4beddd295da))
# [0.42.0](https://github.com/protectwise/troika/compare/v0.41.2...v0.42.0) (2021-05-17)
**Note:** Version bump only for package troika-worker-utils
# [0.41.0](https://github.com/protectwise/troika/compare/v0.40.0...v0.41.0) (2021-04-19)
**Note:** Version bump only for package troika-worker-utils
# [0.40.0](https://github.com/protectwise/troika/compare/v0.39.2...v0.40.0) (2021-02-28)
**Note:** Version bump only for package troika-worker-utils
# [0.39.0](https://github.com/protectwise/troika/compare/v0.38.1...v0.39.0) (2021-02-15)
**Note:** Version bump only for package troika-worker-utils
## [0.38.1](https://github.com/protectwise/troika/compare/v0.38.0...v0.38.1) (2021-02-03)
### Bug Fixes
* **troika-worker-utils:** properly track open requests count ([a01d903](https://github.com/protectwise/troika/commit/a01d903245eee3b9798bcfac7397108fb3bb03e7))
# [0.38.0](https://github.com/protectwise/troika/compare/v0.37.0...v0.38.0) (2021-01-24)
**Note:** Version bump only for package troika-worker-utils
# [0.37.0](https://github.com/protectwise/troika/compare/v0.36.1...v0.37.0) (2021-01-18)
**Note:** Version bump only for package troika-worker-utils
# [0.36.0](https://github.com/protectwise/troika/compare/v0.35.0...v0.36.0) (2020-12-04)
**Note:** Version bump only for package troika-worker-utils
# [0.35.0](https://github.com/protectwise/troika/compare/v0.34.2...v0.35.0) (2020-11-16)
**Note:** Version bump only for package troika-worker-utils
## [0.34.1](https://github.com/protectwise/troika/compare/v0.34.0...v0.34.1) (2020-10-20)
### Bug Fixes
* check for process env 'test' ([4f7f8f2](https://github.com/protectwise/troika/commit/4f7f8f24a9d4f4b655b61e1e16e19061c1911b02))
* check if process is undefined ([2b6d56a](https://github.com/protectwise/troika/commit/2b6d56af78e175a74fb03442efdc0a10d18fa4c8))
# [0.34.0](https://github.com/protectwise/troika/compare/v0.33.1...v0.34.0) (2020-10-19)
**Note:** Version bump only for package troika-worker-utils
# [0.33.0](https://github.com/protectwise/troika/compare/v0.32.0...v0.33.0) (2020-10-02)
### Bug Fixes
* add "sideEffects":false to package.json files to assist treeshaking ([61109b2](https://github.com/protectwise/troika/commit/61109b2e3d21dc794ef66b3f28cf63bbdd34150e))
* add PURE annotations to make troika-three-text treeshakeable ([8e76b5c](https://github.com/protectwise/troika/commit/8e76b5c31a3cbda86595654ba9d66d8d635e44a1))
* remove redundant "browser" and defunct "jsnext:main" fields from package.json files ([0abec40](https://github.com/protectwise/troika/commit/0abec40e3af06d3ae4d990bf198d871b46730f1f))
# [0.32.0](https://github.com/protectwise/troika/compare/v0.31.0...v0.32.0) (2020-09-16)
**Note:** Version bump only for package troika-worker-utils
# [0.31.0](https://github.com/protectwise/troika/compare/v0.30.2...v0.31.0) (2020-08-11)
**Note:** Version bump only for package troika-worker-utils
# [0.30.0](https://github.com/protectwise/troika/compare/v0.29.0...v0.30.0) (2020-07-16)
### Bug Fixes
* **troika-worker-utils:** decrease main thread message level from warn to log ([d7cee6d](https://github.com/protectwise/troika/commit/d7cee6d534c6a01f9c3bc984015f7f824f0b458f))
# [0.29.0](https://github.com/protectwise/troika/compare/v0.28.1...v0.29.0) (2020-07-06)
**Note:** Version bump only for package troika-worker-utils
# [0.28.0](https://github.com/protectwise/troika/compare/v0.27.1...v0.28.0) (2020-06-09)
**Note:** Version bump only for package troika-worker-utils
# [0.27.0](https://github.com/protectwise/troika/compare/v0.26.1...v0.27.0) (2020-06-02)
**Note:** Version bump only for package troika-worker-utils
## [0.26.1](https://github.com/protectwise/troika/compare/v0.26.0...v0.26.1) (2020-05-26)
### Bug Fixes
* **troika-worker-modules:** silence fallback warning in non-browser environments ([3dedb8f](https://github.com/protectwise/troika/commit/3dedb8f2b338e9345c107831863152b115ca50d2))
# [0.26.0](https://github.com/protectwise/troika/compare/v0.25.0...v0.26.0) (2020-05-24)
### Features
* **troika-worker-utils:** add main thread fallback when web workers are not allowed ([c754d0b](https://github.com/protectwise/troika/commit/c754d0b2e716eadb2e478bb12fb2880d8a4ad63f))
# [0.25.0](https://github.com/protectwise/troika/compare/v0.24.1...v0.25.0) (2020-05-19)
**Note:** Version bump only for package troika-worker-utils
# [0.24.0](https://github.com/protectwise/troika/compare/v0.23.0...v0.24.0) (2020-04-27)
**Note:** Version bump only for package troika-worker-utils
# [0.23.0](https://github.com/protectwise/troika/compare/v0.22.0...v0.23.0) (2020-04-16)
### Features
* **troika-worker-modules:** improve rehydration of functions in worker ([8f63090](https://github.com/protectwise/troika/commit/8f63090a5ad4fa3569faeade8e5c532ebfb065c5)), closes [#31](https://github.com/protectwise/troika/issues/31)
# [0.22.0](https://github.com/protectwise/troika/compare/v0.21.0...v0.22.0) (2020-04-02)
**Note:** Version bump only for package troika-worker-utils
# [0.21.0](https://github.com/protectwise/troika/compare/v0.20.0...v0.21.0) (2020-03-27)
**Note:** Version bump only for package troika-worker-utils
# [0.20.0](https://github.com/protectwise/troika/compare/v0.19.0...v0.20.0) (2020-03-16)
### Features
* **troika-worker-utils:** export function for stringifying functions ([977634b](https://github.com/protectwise/troika/commit/977634b5eecb41e4e7aa61addf5b7bfd721ab9e2))
# [0.19.0](https://github.com/protectwise/troika/compare/v0.19.0-alpha.0...v0.19.0) (2020-02-28)
**Note:** Version bump only for package troika-worker-utils
# [0.18.0](https://github.com/protectwise/troika/compare/v0.17.1...v0.18.0) (2020-02-21)
**Note:** Version bump only for package troika-worker-utils
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2019 ProtectWise
Copyright (c) 2021 Jason Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+85
View File
@@ -0,0 +1,85 @@
# `troika-worker-utils`
This package provides utilities for making Web Workers easier to use.
## Worker Modules
Troika's "Worker Modules" system, exposed by the `defineWorkerModule` export, allows you to define a function that will be executed with a web worker. This provides a simple interface for moving chunks of logic off the main thread, which can be critical in WebGL/WebXR scenes where frame rate cannot be interrupted by long-running code.
Similar utilities like [Greenlet](https://github.com/developit/greenlet) have existed for a while. However, `defineWorkerModule` introduces the ability for worker modules to _depend on one another_. This means you can define modular chunks of code in separate functions, and then inject them into a worker where they can reference and call each other.
### `defineWorkerModule(options)`
This function defines a Worker Module. It takes an `options` object that can contain the following:
#### options.init
Required. This is the main function that initializes the module; it will be executed within the Worker the first time it is invoked. If any `dependencies` are defined, the resolved values of those dependencies will be passed in as arguments.
Its return value becomes the module's "value". That can be:
- A function, which can be called any number of times from the main thread by calling the function returned from `defineWorkerModule()`.
- Any other value, which will be used as the value passed to the `init` of other worker modules using it as a dependency.
> Note: As with any function-in-worker utility, the `init` function must not use any variables from the parent closure in which it is defined; its internal content must be completely standalone. Any external values you want to use must be passed in as `dependencies`.
#### options.dependencies
An optional array of dependencies required by the init function. Dependencies can be:
- Primitives like strings, numbers, booleans
- Functions; these will be stringified and rehydrated within the worker so they must not depend on anything from their parent closures
- Other worker modules created by `defineWorkerModule`; these will be resolved within the worker, and therefore modules that provide functions can be called without having to cross the worker/main thread boundary.
#### options.getTransferables
An optional function that will be run in the worker just before posting the response value from a module call back to the main thread. This function will be passed that response value, and if it returns an array then that will be used as the "transferables" parameter to `postMessage`. Use this if there are values in the response that can/should be transfered rather than cloned.
#### options.name
An optional descriptive name for this module; this can be useful for debugging (it will be inserted as a comment into the Blob sent to the worker) but is not currently used for anything else.
#### options.workerId
By default all modules will run in the same dedicated worker, but if you want to use multiple workers you can pass a `workerId` string to indicate a specific worker to spawn. Note that each worker is completely standalone and no data or state will be shared between them. If a worker module is used as a dependency by worker modules using different `workerId`s, then that dependency will be re-registered in each worker.
#### Return Value
The value returned by `defineWorkerModule` is a function. If your `options.init` returned a function, then this will be how you can invoke that within the worker. Call it, and it will give you a Promise for its return value.
### Contrived Example
```js
import { defineWorkerModule } from 'troika-worker-utils'
// A simple module with a value:
const workerModuleA = defineWorkerModule({
init: function() {
return Math.PI
}
})
// A module that depends on the previous module:
const workerModuleB = defineWorkerModule({
dependencies: [
workerModuleA
],
init: function(moduleAValue) {
// moduleAValue here is "I'm the value of Module A!" from the first init function
// This return function can be invoked by calling workerModuleB in the main thread:
let callCount = 0
return function(arg) {
return `Called module B ${++callCount} times, `
+ `with arg "${arg}". Module A's value was ${moduleAValue}.`
}
}
})
workerModuleB('foo') // "Called module B 1 times, with arg "foo". Module A's value was 3.141592653589793."
workerModuleB('bar') // "Called module B 2 times, with arg "bar". Module A's value was 3.141592653589793."
```
+327
View File
@@ -0,0 +1,327 @@
/*global require,global,_testDataBucket*/
require('./_jsdom-worker.js')
const {defineWorkerModule} = require('../src/WorkerModules.js')
beforeEach(() => {
// Functions that are run in the worker can place breadcrumbs in this object that
// `expect` matchers can then retrieve and test later on.
global._testDataBucket = {}
})
test('requires an `options` argument', () => {
expect(() => {
defineWorkerModule()
}).toThrow()
})
test('requires an `init` function option', () => {
expect(() => {
defineWorkerModule({notInit: 'blerp'})
}).toThrow()
expect(() => {
defineWorkerModule({init: 'not_a_function'})
}).toThrow()
expect(() => {
defineWorkerModule({init: () => {}})
}).not.toThrow()
})
test('returns a function', () => {
expect(typeof defineWorkerModule({init: () => {}})).toBe('function')
})
describe('calling the module function', () => {
test('throws if init did not return a function', () => {
const moduleFn = defineWorkerModule({
init: function() {
return 'not a function'
}
})
return expect(moduleFn()).rejects.toThrow()
})
test('does not throw if init returns a function', () => {
const moduleFn = defineWorkerModule({
init: function() {
return function() {}
}
})
return expect(moduleFn()).resolves.toBe(undefined)
})
test('returns a thenable', () => {
const moduleFn = defineWorkerModule({
init: function() {return function() {}}
})
expect(moduleFn()).toEqual(expect.objectContaining({then: expect.any(Function)}))
})
test('invokes (a clone of) the init function', function() {
const moduleFn = defineWorkerModule({
init: function() {
_testDataBucket.initWasCalled = true
return function() {}
}
})
return moduleFn().then(() => {
expect(_testDataBucket.initWasCalled).toBe(true)
})
})
test('invokes a function returned by the init function and resolves the thenable with its return value', () => {
const moduleFn = defineWorkerModule({
init: function() {
return function() {
_testDataBucket.initReturnFnWasCalled = true
return 'this is the return value'
}
}
})
return moduleFn().then((result) => {
expect(_testDataBucket.initReturnFnWasCalled).toBe(true)
expect(result).toEqual('this is the return value')
})
})
test('waits for a thenable returned by the init function', () => {
const moduleFn = defineWorkerModule({
init: function() {
return function() {
return new Promise(resolve => {
setTimeout(() => {
resolve('promise resolution value')
}, 50)
})
}
}
})
return moduleFn().then((result) => {
expect(result).toEqual('promise resolution value')
})
})
test('throws if the init function throws', () => {
const moduleFn = defineWorkerModule({
init: function() {
const err = new Error('blerp')
err.noLog = true
throw err
}
})
return expect(moduleFn()).rejects.toThrow()
})
})
describe('dependencies', () => {
test('are passed to the init function', () => {
const moduleFn = defineWorkerModule({
dependencies: [
1,
true,
'three'
],
init: function(one, two, three) {
expect(one).toBe(1)
expect(two).toBe(true)
expect(three).toBe('three')
return function() {}
}
})
return moduleFn()
})
test('can be functions', () => {
const moduleFn = defineWorkerModule({
dependencies: [
function() {
_testDataBucket.firstDepFnCalled = true
},
function(arg1, arg2) {
_testDataBucket.secondDepFnArgs = [arg1, arg2]
}
],
init: function(dep1, dep2) {
expect(typeof dep1).toBe('function')
expect(typeof dep2).toBe('function')
dep1()
dep2('hey', 'jude')
return function() {}
}
})
return moduleFn().then(() => {
expect(_testDataBucket.firstDepFnCalled).toBe(true)
expect(_testDataBucket.secondDepFnArgs).toEqual(['hey', 'jude'])
})
})
test('can be other worker modules', () => {
const depMod1 = defineWorkerModule({
dependencies: [123, 456],
init: function(n1, n2) {
return n1 + n2
}
})
const depMod2 = defineWorkerModule({
dependencies: ['abc', 'xyz'],
init: function(s1, s2) {
return s2 + s1
}
})
const depMod3 = defineWorkerModule({
dependencies: [depMod1, depMod2],
init: function(d1, d2) {
return d1 + d2
}
})
const moduleFn = defineWorkerModule({
dependencies: [
depMod1,
depMod2,
depMod3
],
init: function(dep1, dep2, dep3) {
expect(dep1).toEqual(579)
expect(dep2).toEqual('xyzabc')
expect(dep3).toEqual('579xyzabc')
return function() {
return [dep1, dep2, dep3]
}
}
})
return moduleFn().then((result) => {
expect(result).toEqual([579, 'xyzabc', '579xyzabc'])
})
})
})
describe('getTransferables', () => {
test('is called before result is sent to main thread', () => {
_testDataBucket.callOrder = []
const moduleFn = defineWorkerModule({
init: function() {
_testDataBucket.callOrder.push('init')
return function() {
_testDataBucket.callOrder.push('module')
return {data: new Uint8Array(32)}
}
},
getTransferables: function (result) {
_testDataBucket.callOrder.push('getTransferables')
return result.data
}
})
return moduleFn().then(() => {
_testDataBucket.callOrder.push('response')
}).then(() => {
expect(_testDataBucket.callOrder).toEqual(['init', 'module', 'getTransferables', 'response'])
})
})
test('is called with the module return value as param', () => {
const moduleFn = defineWorkerModule({
init: function() {
return function() {
return {data: new Uint8Array(32)}
}
},
getTransferables: function (result) {
_testDataBucket.getTransferablesParam = result
return result.data
}
})
return moduleFn().then(() => {
expect(_testDataBucket.getTransferablesParam).toEqual({data: expect.any(Uint8Array)})
})
})
// TODO not working yet, see comment below
test.skip('its return value is used as the `transferables` in postMessage', () => {
const moduleFn = defineWorkerModule({
init: function() {
// Hack: override the worker's postMessage with one we can spy on
// TODO this doesn't work, because `init` is invoked in the worker's global scope but
// the WorkerModule framework code (that calls postMessage) is wrapped in a narrower
// scope by the jsdom-worker polyfill. Will need to find a way around that before this
// test can work.
const _origPostMessage = postMessage
// eslint-disable-next-line no-global-assign
postMessage = function(e, transferList) {
_testDataBucket.postMessageTransferList = transferList
_origPostMessage(e)
}
return function() {
return {data: new Uint8Array(32)}
}
},
getTransferables: function (result) {
return [result.data]
}
})
return moduleFn().then(() => {
expect(_testDataBucket.postMessageTransferList).toEqual([expect.any(Uint8Array)])
})
})
})
describe('multiple workers', () => {
test('different `workerId`s execute in different workers', () => {
const commonDep = defineWorkerModule({
init: function() {
return Math.random() //give a unique value per worker
}
})
// 1a and 1b share a worker so their dependency should init with the same value;
// 2 is a different worker so the dependency should init with a different value.
const module1a = defineWorkerModule({
workerId: 'worker1',
dependencies: [commonDep],
init: function(depVal) {
return () => depVal
}
})
const module1b = defineWorkerModule({
workerId: 'worker1',
dependencies: [commonDep],
init: function(depVal) {
return () => depVal
}
})
const module2 = defineWorkerModule({
workerId: 'worker2',
dependencies: [commonDep],
init: function(depVal) {
return () => depVal
}
})
const module3 = defineWorkerModule({
workerId: null, //default worker
dependencies: [commonDep],
init: function(depVal) {
return () => depVal
}
})
return Promise.all([module1a(), module1b(), module2(), module3()]).then(([result1a, result1b, result2, result3]) => {
expect(typeof result1a).toEqual('number')
expect(typeof result1b).toEqual('number')
expect(typeof result2).toEqual('number')
expect(typeof result3).toEqual('number')
expect(result1a).toEqual(result1b)
expect(result2).not.toEqual(result1a)
expect(result3).not.toEqual(result1a)
expect(result3).not.toEqual(result2)
})
})
})
+135
View File
@@ -0,0 +1,135 @@
/*global global*/
/**
* This is a forked version of the MIT-licensed jsdom-worker polyfill, taken from
* https://github.com/developit/jsdom-worker/commit/4a400f72ec901fc926f0301ab4680f9f2c318439
*
* We keep a local forked copy because:
* 1) That repo appears to be unmaintained and some of its commits have not made it to npm
* 2) We need to patch in some extra support for things like importScripts.
*
* All edits are marked with [TROIKA EDIT] below.
*/
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}`;
};
// [TROIKA EDIT]: add revokeObjectURL as no-op:
global.URL.revokeObjectURL = function() {}
}
// [TROIKA EDIT]: `fetch` is overwritten with jsdom's version on each suite run, but
// URL.$$objects hangs around between runs, so it wouldn't be re-patched. We move it
// to its own conditional.
if (!global.fetch || !global.fetch._patched) {
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);
};
global.fetch._patched = true
}
// [TROIKA EDIT]: To enable a synchronous Blob read without jsdom support for FileReaderSync,
// we override the Blob constructor to save a reference to its original text input.
const blobContents = new WeakMap()
const _Blob = global.Blob;
global.Blob = function(...args) {
let b = new _Blob(...args)
blobContents.set(b, args[0])
return b
}
if (!global.document) {
global.document = {};
}
function Event(type) { this.type = type; }
Event.prototype.initEvent = Object;
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,
importScripts(...urls) {
// [TROIKA EDIT]: Implement importScripts, for text-based blob urls only for now:
urls.forEach(url => {
if (url.match(/^blob:/)) {
let id = url.match(/[^/]+$/)[0];
let code = blobContents.get(global.URL.$$objects[id]).join('');
let vars = 'var self=this,global=self';
for (let k in scope) vars += `,${k}=self.${k}`;
new Function(vars+';\n'+code).call(scope);
} else {
throw Error('importScripts only supports blob urls for now.')
}
})
}
},
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 = () => {
// [TROIKA EDIT]: don't throw.
// 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}`;
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); });
};
+424
View File
@@ -0,0 +1,424 @@
/**
* Main content for the worker that handles the loading and execution of
* modules within it.
*/
function workerBootstrap() {
var modules = Object.create(null);
// Handle messages for registering a module
function registerModule(ref, callback) {
var id = ref.id;
var name = ref.name;
var dependencies = ref.dependencies; if ( dependencies === void 0 ) dependencies = [];
var init = ref.init; if ( init === void 0 ) init = function(){};
var getTransferables = ref.getTransferables; if ( getTransferables === void 0 ) getTransferables = null;
// Only register once
if (modules[id]) { return }
try {
// If any dependencies are modules, ensure they're registered and grab their value
dependencies = dependencies.map(function (dep) {
if (dep && dep.isWorkerModule) {
registerModule(dep, function (depResult) {
if (depResult instanceof Error) { throw depResult }
});
dep = modules[dep.id].value;
}
return dep
});
// Rehydrate functions
init = rehydrate(("<" + name + ">.init"), init);
if (getTransferables) {
getTransferables = rehydrate(("<" + name + ">.getTransferables"), getTransferables);
}
// Initialize the module and store its value
var value = null;
if (typeof init === 'function') {
value = init.apply(void 0, dependencies);
} else {
console.error('worker module init function failed to rehydrate');
}
modules[id] = {
id: id,
value: value,
getTransferables: getTransferables
};
callback(value);
} catch(err) {
if (!(err && err.noLog)) {
console.error(err);
}
callback(err);
}
}
// Handle messages for calling a registered module's result function
function callModule(ref, callback) {
var ref$1;
var id = ref.id;
var args = ref.args;
if (!modules[id] || typeof modules[id].value !== 'function') {
callback(new Error(("Worker module " + id + ": not found or its 'init' did not return a function")));
}
try {
var result = (ref$1 = modules[id]).value.apply(ref$1, args);
if (result && typeof result.then === 'function') {
result.then(handleResult, function (rej) { return callback(rej instanceof Error ? rej : new Error('' + rej)); });
} else {
handleResult(result);
}
} catch(err) {
callback(err);
}
function handleResult(result) {
try {
var tx = modules[id].getTransferables && modules[id].getTransferables(result);
if (!tx || !Array.isArray(tx) || !tx.length) {
tx = undefined; //postMessage is very picky about not passing null or empty transferables
}
callback(result, tx);
} catch(err) {
console.error(err);
callback(err);
}
}
}
function rehydrate(name, str) {
var result = void 0;
self.troikaDefine = function (r) { return result = r; };
var url = URL.createObjectURL(
new Blob(
[("/** " + (name.replace(/\*/g, '')) + " **/\n\ntroikaDefine(\n" + str + "\n)")],
{type: 'application/javascript'}
)
);
try {
importScripts(url);
} catch(err) {
console.error(err);
}
URL.revokeObjectURL(url);
delete self.troikaDefine;
return result
}
// Handler for all messages within the worker
self.addEventListener('message', function (e) {
var ref = e.data;
var messageId = ref.messageId;
var action = ref.action;
var data = ref.data;
try {
// Module registration
if (action === 'registerModule') {
registerModule(data, function (result) {
if (result instanceof Error) {
postMessage({
messageId: messageId,
success: false,
error: result.message
});
} else {
postMessage({
messageId: messageId,
success: true,
result: {isCallable: typeof result === 'function'}
});
}
});
}
// Invocation
if (action === 'callModule') {
callModule(data, function (result, transferables) {
if (result instanceof Error) {
postMessage({
messageId: messageId,
success: false,
error: result.message
});
} else {
postMessage({
messageId: messageId,
success: true,
result: result
}, transferables || undefined);
}
});
}
} catch(err) {
postMessage({
messageId: messageId,
success: false,
error: err.stack
});
}
});
}
/**
* Fallback for `defineWorkerModule` that behaves identically but runs in the main
* thread, for when the execution environment doesn't support web workers or they
* are disallowed due to e.g. CSP security restrictions.
*/
function defineMainThreadModule(options) {
var moduleFunc = function() {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return moduleFunc._getInitResult().then(function (initResult) {
if (typeof initResult === 'function') {
return initResult.apply(void 0, args)
} else {
throw new Error('Worker module function was called but `init` did not return a callable function')
}
})
};
moduleFunc._getInitResult = function() {
// We can ignore getTransferables in main thread. TODO workerId?
var dependencies = options.dependencies;
var init = options.init;
// Resolve dependencies
dependencies = Array.isArray(dependencies) ? dependencies.map(function (dep) {
if (dep) {
// If it's a worker module, use its main thread impl
dep = dep.onMainThread || dep;
// If it's a main thread worker module, use its init return value
if (dep._getInitResult) {
dep = dep._getInitResult();
}
}
return dep
}) : [];
// Invoke init with the resolved dependencies
var initPromise = Promise.all(dependencies).then(function (deps) {
return init.apply(null, deps)
});
// Cache the resolved promise for subsequent calls
moduleFunc._getInitResult = function () { return initPromise; };
return initPromise
};
return moduleFunc
}
var supportsWorkers = function () {
var supported = false;
// Only attempt worker initialization in browsers; elsewhere it would just be
// noise e.g. loading into a Node environment for SSR.
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
try {
// TODO additional checks for things like importScripts within the worker?
// Would need to be an async check.
var worker = new Worker(
URL.createObjectURL(new Blob([''], { type: 'application/javascript' }))
);
worker.terminate();
supported = true;
} catch (err) {
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') ; else {
console.log(
("Troika createWorkerModule: web workers not allowed; falling back to main thread execution. Cause: [" + (err.message) + "]")
);
}
}
}
// Cached result
supportsWorkers = function () { return supported; };
return supported
};
var _workerModuleId = 0;
var _messageId = 0;
var _allowInitAsString = false;
var workers = Object.create(null);
var registeredModules = Object.create(null); //workerId -> Set<unregisterFn>
var openRequests = Object.create(null);
/**
* Define a module of code that will be executed with a web worker. This provides a simple
* interface for moving chunks of logic off the main thread, and managing their dependencies
* among one another.
*
* @param {object} options
* @param {function} options.init
* @param {array} [options.dependencies]
* @param {function} [options.getTransferables]
* @param {string} [options.name]
* @param {string} [options.workerId]
* @return {function(...[*]): {then}}
*/
function defineWorkerModule(options) {
if ((!options || typeof options.init !== 'function') && !_allowInitAsString) {
throw new Error('requires `options.init` function')
}
var dependencies = options.dependencies;
var init = options.init;
var getTransferables = options.getTransferables;
var workerId = options.workerId;
var onMainThread = defineMainThreadModule(options);
if (workerId == null) {
workerId = '#default';
}
var id = "workerModule" + (++_workerModuleId);
var name = options.name || id;
var registrationPromise = null;
dependencies = dependencies && dependencies.map(function (dep) {
// Wrap raw functions as worker modules with no dependencies
if (typeof dep === 'function' && !dep.workerModuleData) {
_allowInitAsString = true;
dep = defineWorkerModule({
workerId: workerId,
name: ("<" + name + "> function dependency: " + (dep.name)),
init: ("function(){return (\n" + (stringifyFunction(dep)) + "\n)}")
});
_allowInitAsString = false;
}
// Grab postable data for worker modules
if (dep && dep.workerModuleData) {
dep = dep.workerModuleData;
}
return dep
});
function moduleFunc() {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
if (!supportsWorkers()) {
return onMainThread.apply(void 0, args)
}
// Register this module if needed
if (!registrationPromise) {
registrationPromise = callWorker(workerId,'registerModule', moduleFunc.workerModuleData);
var unregister = function () {
registrationPromise = null;
registeredModules[workerId].delete(unregister);
}
;(registeredModules[workerId] || (registeredModules[workerId] = new Set())).add(unregister);
}
// Invoke the module, returning a promise
return registrationPromise.then(function (ref) {
var isCallable = ref.isCallable;
if (isCallable) {
return callWorker(workerId,'callModule', {id: id, args: args})
} else {
throw new Error('Worker module function was called but `init` did not return a callable function')
}
})
}
moduleFunc.workerModuleData = {
isWorkerModule: true,
id: id,
name: name,
dependencies: dependencies,
init: stringifyFunction(init),
getTransferables: getTransferables && stringifyFunction(getTransferables)
};
moduleFunc.onMainThread = onMainThread;
return moduleFunc
}
/**
* Terminate an active Worker by a workerId that was passed to defineWorkerModule.
* This only terminates the Worker itself; the worker module will remain available
* and if you call it again its Worker will be respawned.
* @param {string} workerId
*/
function terminateWorker(workerId) {
// Unregister all modules that were registered in that worker
if (registeredModules[workerId]) {
registeredModules[workerId].forEach(function (unregister) {
unregister();
});
}
// Terminate the Worker object
if (workers[workerId]) {
workers[workerId].terminate();
delete workers[workerId];
}
}
/**
* Stringifies a function into a form that can be deserialized in the worker
* @param fn
*/
function stringifyFunction(fn) {
var str = fn.toString();
// If it was defined in object method/property format, it needs to be modified
if (!/^function/.test(str) && /^\w+\s*\(/.test(str)) {
str = 'function ' + str;
}
return str
}
function getWorker(workerId) {
var worker = workers[workerId];
if (!worker) {
// Bootstrap the worker's content
var bootstrap = stringifyFunction(workerBootstrap);
// Create the worker from the bootstrap function content
worker = workers[workerId] = new Worker(
URL.createObjectURL(
new Blob(
[("/** Worker Module Bootstrap: " + (workerId.replace(/\*/g, '')) + " **/\n\n;(" + bootstrap + ")()")],
{type: 'application/javascript'}
)
)
);
// Single handler for response messages from the worker
worker.onmessage = function (e) {
var response = e.data;
var msgId = response.messageId;
var callback = openRequests[msgId];
if (!callback) {
throw new Error('WorkerModule response with empty or unknown messageId')
}
delete openRequests[msgId];
callback(response);
};
}
return worker
}
// Issue a call to the worker with a callback to handle the response
function callWorker(workerId, action, data) {
return new Promise(function (resolve, reject) {
var messageId = ++_messageId;
openRequests[messageId] = function (response) {
if (response.success) {
resolve(response.result);
} else {
reject(new Error(("Error in worker " + action + " call: " + (response.error))));
}
};
getWorker(workerId).postMessage({
messageId: messageId,
action: action,
data: data
});
})
}
export { defineWorkerModule, stringifyFunction, terminateWorker };
+436
View File
@@ -0,0 +1,436 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.troika_worker_utils = {}));
}(this, (function (exports) { 'use strict';
/**
* Main content for the worker that handles the loading and execution of
* modules within it.
*/
function workerBootstrap() {
var modules = Object.create(null);
// Handle messages for registering a module
function registerModule(ref, callback) {
var id = ref.id;
var name = ref.name;
var dependencies = ref.dependencies; if ( dependencies === void 0 ) dependencies = [];
var init = ref.init; if ( init === void 0 ) init = function(){};
var getTransferables = ref.getTransferables; if ( getTransferables === void 0 ) getTransferables = null;
// Only register once
if (modules[id]) { return }
try {
// If any dependencies are modules, ensure they're registered and grab their value
dependencies = dependencies.map(function (dep) {
if (dep && dep.isWorkerModule) {
registerModule(dep, function (depResult) {
if (depResult instanceof Error) { throw depResult }
});
dep = modules[dep.id].value;
}
return dep
});
// Rehydrate functions
init = rehydrate(("<" + name + ">.init"), init);
if (getTransferables) {
getTransferables = rehydrate(("<" + name + ">.getTransferables"), getTransferables);
}
// Initialize the module and store its value
var value = null;
if (typeof init === 'function') {
value = init.apply(void 0, dependencies);
} else {
console.error('worker module init function failed to rehydrate');
}
modules[id] = {
id: id,
value: value,
getTransferables: getTransferables
};
callback(value);
} catch(err) {
if (!(err && err.noLog)) {
console.error(err);
}
callback(err);
}
}
// Handle messages for calling a registered module's result function
function callModule(ref, callback) {
var ref$1;
var id = ref.id;
var args = ref.args;
if (!modules[id] || typeof modules[id].value !== 'function') {
callback(new Error(("Worker module " + id + ": not found or its 'init' did not return a function")));
}
try {
var result = (ref$1 = modules[id]).value.apply(ref$1, args);
if (result && typeof result.then === 'function') {
result.then(handleResult, function (rej) { return callback(rej instanceof Error ? rej : new Error('' + rej)); });
} else {
handleResult(result);
}
} catch(err) {
callback(err);
}
function handleResult(result) {
try {
var tx = modules[id].getTransferables && modules[id].getTransferables(result);
if (!tx || !Array.isArray(tx) || !tx.length) {
tx = undefined; //postMessage is very picky about not passing null or empty transferables
}
callback(result, tx);
} catch(err) {
console.error(err);
callback(err);
}
}
}
function rehydrate(name, str) {
var result = void 0;
self.troikaDefine = function (r) { return result = r; };
var url = URL.createObjectURL(
new Blob(
[("/** " + (name.replace(/\*/g, '')) + " **/\n\ntroikaDefine(\n" + str + "\n)")],
{type: 'application/javascript'}
)
);
try {
importScripts(url);
} catch(err) {
console.error(err);
}
URL.revokeObjectURL(url);
delete self.troikaDefine;
return result
}
// Handler for all messages within the worker
self.addEventListener('message', function (e) {
var ref = e.data;
var messageId = ref.messageId;
var action = ref.action;
var data = ref.data;
try {
// Module registration
if (action === 'registerModule') {
registerModule(data, function (result) {
if (result instanceof Error) {
postMessage({
messageId: messageId,
success: false,
error: result.message
});
} else {
postMessage({
messageId: messageId,
success: true,
result: {isCallable: typeof result === 'function'}
});
}
});
}
// Invocation
if (action === 'callModule') {
callModule(data, function (result, transferables) {
if (result instanceof Error) {
postMessage({
messageId: messageId,
success: false,
error: result.message
});
} else {
postMessage({
messageId: messageId,
success: true,
result: result
}, transferables || undefined);
}
});
}
} catch(err) {
postMessage({
messageId: messageId,
success: false,
error: err.stack
});
}
});
}
/**
* Fallback for `defineWorkerModule` that behaves identically but runs in the main
* thread, for when the execution environment doesn't support web workers or they
* are disallowed due to e.g. CSP security restrictions.
*/
function defineMainThreadModule(options) {
var moduleFunc = function() {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return moduleFunc._getInitResult().then(function (initResult) {
if (typeof initResult === 'function') {
return initResult.apply(void 0, args)
} else {
throw new Error('Worker module function was called but `init` did not return a callable function')
}
})
};
moduleFunc._getInitResult = function() {
// We can ignore getTransferables in main thread. TODO workerId?
var dependencies = options.dependencies;
var init = options.init;
// Resolve dependencies
dependencies = Array.isArray(dependencies) ? dependencies.map(function (dep) {
if (dep) {
// If it's a worker module, use its main thread impl
dep = dep.onMainThread || dep;
// If it's a main thread worker module, use its init return value
if (dep._getInitResult) {
dep = dep._getInitResult();
}
}
return dep
}) : [];
// Invoke init with the resolved dependencies
var initPromise = Promise.all(dependencies).then(function (deps) {
return init.apply(null, deps)
});
// Cache the resolved promise for subsequent calls
moduleFunc._getInitResult = function () { return initPromise; };
return initPromise
};
return moduleFunc
}
var supportsWorkers = function () {
var supported = false;
// Only attempt worker initialization in browsers; elsewhere it would just be
// noise e.g. loading into a Node environment for SSR.
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
try {
// TODO additional checks for things like importScripts within the worker?
// Would need to be an async check.
var worker = new Worker(
URL.createObjectURL(new Blob([''], { type: 'application/javascript' }))
);
worker.terminate();
supported = true;
} catch (err) {
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') ; else {
console.log(
("Troika createWorkerModule: web workers not allowed; falling back to main thread execution. Cause: [" + (err.message) + "]")
);
}
}
}
// Cached result
supportsWorkers = function () { return supported; };
return supported
};
var _workerModuleId = 0;
var _messageId = 0;
var _allowInitAsString = false;
var workers = Object.create(null);
var registeredModules = Object.create(null); //workerId -> Set<unregisterFn>
var openRequests = Object.create(null);
/**
* Define a module of code that will be executed with a web worker. This provides a simple
* interface for moving chunks of logic off the main thread, and managing their dependencies
* among one another.
*
* @param {object} options
* @param {function} options.init
* @param {array} [options.dependencies]
* @param {function} [options.getTransferables]
* @param {string} [options.name]
* @param {string} [options.workerId]
* @return {function(...[*]): {then}}
*/
function defineWorkerModule(options) {
if ((!options || typeof options.init !== 'function') && !_allowInitAsString) {
throw new Error('requires `options.init` function')
}
var dependencies = options.dependencies;
var init = options.init;
var getTransferables = options.getTransferables;
var workerId = options.workerId;
var onMainThread = defineMainThreadModule(options);
if (workerId == null) {
workerId = '#default';
}
var id = "workerModule" + (++_workerModuleId);
var name = options.name || id;
var registrationPromise = null;
dependencies = dependencies && dependencies.map(function (dep) {
// Wrap raw functions as worker modules with no dependencies
if (typeof dep === 'function' && !dep.workerModuleData) {
_allowInitAsString = true;
dep = defineWorkerModule({
workerId: workerId,
name: ("<" + name + "> function dependency: " + (dep.name)),
init: ("function(){return (\n" + (stringifyFunction(dep)) + "\n)}")
});
_allowInitAsString = false;
}
// Grab postable data for worker modules
if (dep && dep.workerModuleData) {
dep = dep.workerModuleData;
}
return dep
});
function moduleFunc() {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
if (!supportsWorkers()) {
return onMainThread.apply(void 0, args)
}
// Register this module if needed
if (!registrationPromise) {
registrationPromise = callWorker(workerId,'registerModule', moduleFunc.workerModuleData);
var unregister = function () {
registrationPromise = null;
registeredModules[workerId].delete(unregister);
}
;(registeredModules[workerId] || (registeredModules[workerId] = new Set())).add(unregister);
}
// Invoke the module, returning a promise
return registrationPromise.then(function (ref) {
var isCallable = ref.isCallable;
if (isCallable) {
return callWorker(workerId,'callModule', {id: id, args: args})
} else {
throw new Error('Worker module function was called but `init` did not return a callable function')
}
})
}
moduleFunc.workerModuleData = {
isWorkerModule: true,
id: id,
name: name,
dependencies: dependencies,
init: stringifyFunction(init),
getTransferables: getTransferables && stringifyFunction(getTransferables)
};
moduleFunc.onMainThread = onMainThread;
return moduleFunc
}
/**
* Terminate an active Worker by a workerId that was passed to defineWorkerModule.
* This only terminates the Worker itself; the worker module will remain available
* and if you call it again its Worker will be respawned.
* @param {string} workerId
*/
function terminateWorker(workerId) {
// Unregister all modules that were registered in that worker
if (registeredModules[workerId]) {
registeredModules[workerId].forEach(function (unregister) {
unregister();
});
}
// Terminate the Worker object
if (workers[workerId]) {
workers[workerId].terminate();
delete workers[workerId];
}
}
/**
* Stringifies a function into a form that can be deserialized in the worker
* @param fn
*/
function stringifyFunction(fn) {
var str = fn.toString();
// If it was defined in object method/property format, it needs to be modified
if (!/^function/.test(str) && /^\w+\s*\(/.test(str)) {
str = 'function ' + str;
}
return str
}
function getWorker(workerId) {
var worker = workers[workerId];
if (!worker) {
// Bootstrap the worker's content
var bootstrap = stringifyFunction(workerBootstrap);
// Create the worker from the bootstrap function content
worker = workers[workerId] = new Worker(
URL.createObjectURL(
new Blob(
[("/** Worker Module Bootstrap: " + (workerId.replace(/\*/g, '')) + " **/\n\n;(" + bootstrap + ")()")],
{type: 'application/javascript'}
)
)
);
// Single handler for response messages from the worker
worker.onmessage = function (e) {
var response = e.data;
var msgId = response.messageId;
var callback = openRequests[msgId];
if (!callback) {
throw new Error('WorkerModule response with empty or unknown messageId')
}
delete openRequests[msgId];
callback(response);
};
}
return worker
}
// Issue a call to the worker with a callback to handle the response
function callWorker(workerId, action, data) {
return new Promise(function (resolve, reject) {
var messageId = ++_messageId;
openRequests[messageId] = function (response) {
if (response.success) {
resolve(response.result);
} else {
reject(new Error(("Error in worker " + action + " call: " + (response.error))));
}
};
getWorker(workerId).postMessage({
messageId: messageId,
action: action,
data: data
});
})
}
exports.defineWorkerModule = defineWorkerModule;
exports.stringifyFunction = stringifyFunction;
exports.terminateWorker = terminateWorker;
Object.defineProperty(exports, '__esModule', { value: true });
})));
+10
View File
@@ -0,0 +1,10 @@
'use strict';(function(k,n){"object"===typeof exports&&"undefined"!==typeof module?n(exports):"function"===typeof define&&define.amd?define(["exports"],n):(k="undefined"!==typeof globalThis?globalThis:k||self,n(k.troika_worker_utils={}))})(this,function(k){function n(){function b(a,d){var c=a.id,h=a.name,l=a.dependencies;void 0===l&&(l=[]);var g=a.init;void 0===g&&(g=function(){});a=a.getTransferables;void 0===a&&(a=null);if(!f[c])try{l=l.map(function(a){a&&a.isWorkerModule&&(b(a,function(a){if(a instanceof
Error)throw a;}),a=f[a.id].value);return a}),g=e("<"+h+">.init",g),a&&(a=e("<"+h+">.getTransferables",a)),h=null,"function"===typeof g?h=g.apply(void 0,l):console.error("worker module init function failed to rehydrate"),f[c]={id:c,value:h,getTransferables:a},d(h)}catch(m){m&&m.noLog||console.error(m),d(m)}}function c(a,d){function b(a){try{var b=f[e].getTransferables&&f[e].getTransferables(a);b&&Array.isArray(b)&&b.length||(b=void 0);d(a,b)}catch(v){console.error(v),d(v)}}var c,e=a.id;a=a.args;f[e]&&
"function"===typeof f[e].value||d(Error("Worker module "+e+": not found or its 'init' did not return a function"));try{var g=(c=f[e]).value.apply(c,a);g&&"function"===typeof g.then?g.then(b,function(a){return d(a instanceof Error?a:Error(""+a))}):b(g)}catch(m){d(m)}}function e(a,b){var d=void 0;self.troikaDefine=function(a){return d=a};a=URL.createObjectURL(new Blob(["/** "+a.replace(/\*/g,"")+" **/\n\ntroikaDefine(\n"+b+"\n)"],{type:"application/javascript"}));try{importScripts(a)}catch(h){console.error(h)}URL.revokeObjectURL(a);
delete self.troikaDefine;return d}var f=Object.create(null);self.addEventListener("message",function(a){var d=a.data,e=d.messageId;a=d.action;d=d.data;try{"registerModule"===a&&b(d,function(a){a instanceof Error?postMessage({messageId:e,success:!1,error:a.message}):postMessage({messageId:e,success:!0,result:{isCallable:"function"===typeof a}})}),"callModule"===a&&c(d,function(a,b){a instanceof Error?postMessage({messageId:e,success:!1,error:a.message}):postMessage({messageId:e,success:!0,result:a},
b||void 0)})}catch(h){postMessage({messageId:e,success:!1,error:h.stack})}})}function z(b){var c=function(){for(var b=[],f=arguments.length;f--;)b[f]=arguments[f];return c._getInitResult().then(function(a){if("function"===typeof a)return a.apply(void 0,b);throw Error("Worker module function was called but `init` did not return a callable function");})};c._getInitResult=function(){var e=b.dependencies,f=b.init;e=Array.isArray(e)?e.map(function(a){a&&(a=a.onMainThread||a,a._getInitResult&&(a=a._getInitResult()));
return a}):[];var a=Promise.all(e).then(function(a){return f.apply(null,a)});c._getInitResult=function(){return a};return a};return c}function w(b){function c(){for(var a=[],b=arguments.length;b--;)a[b]=arguments[b];if(!x())return k.apply(void 0,a);if(!g){g=y(d,"registerModule",c.workerModuleData);var e=function(){g=null;p[d].delete(e)};(p[d]||(p[d]=new Set)).add(e)}return g.then(function(b){if(b.isCallable)return y(d,"callModule",{id:h,args:a});throw Error("Worker module function was called but `init` did not return a callable function");
})}if(!(b&&"function"===typeof b.init||t))throw Error("requires `options.init` function");var e=b.dependencies,f=b.init,a=b.getTransferables,d=b.workerId,k=z(b);null==d&&(d="#default");var h="workerModule"+ ++A,l=b.name||h,g=null;e=e&&e.map(function(a){"function"!==typeof a||a.workerModuleData||(t=!0,a=w({workerId:d,name:"<"+l+"> function dependency: "+a.name,init:"function(){return (\n"+q(a)+"\n)}"}),t=!1);a&&a.workerModuleData&&(a=a.workerModuleData);return a});c.workerModuleData={isWorkerModule:!0,
id:h,name:l,dependencies:e,init:q(f),getTransferables:a&&q(a)};c.onMainThread=k;return c}function q(b){b=b.toString();!/^function/.test(b)&&/^\w+\s*\(/.test(b)&&(b="function "+b);return b}function B(b){var c=r[b];c||(c=q(n),c=r[b]=new Worker(URL.createObjectURL(new Blob(["/** Worker Module Bootstrap: "+b.replace(/\*/g,"")+" **/\n\n;("+c+")()"],{type:"application/javascript"}))),c.onmessage=function(b){b=b.data;var c=b.messageId,a=u[c];if(!a)throw Error("WorkerModule response with empty or unknown messageId");
delete u[c];a(b)});return c}function y(b,c,e){return new Promise(function(f,a){var d=++C;u[d]=function(b){b.success?f(b.result):a(Error("Error in worker "+c+" call: "+b.error))};B(b).postMessage({messageId:d,action:c,data:e})})}var x=function(){var b=!1;if("undefined"!==typeof window&&"undefined"!==typeof window.document)try{(new Worker(URL.createObjectURL(new Blob([""],{type:"application/javascript"})))).terminate(),b=!0}catch(c){"undefined"!==typeof process&&"test"===process.env.NODE_ENV||console.log("Troika createWorkerModule: web workers not allowed; falling back to main thread execution. Cause: ["+
c.message+"]")}x=function(){return b};return b},A=0,C=0,t=!1,r=Object.create(null),p=Object.create(null),u=Object.create(null);k.defineWorkerModule=w;k.stringifyFunction=q;k.terminateWorker=function(b){p[b]&&p[b].forEach(function(b){b()});r[b]&&(r[b].terminate(),delete r[b])};Object.defineProperty(k,"__esModule",{value:!0})})
+17
View File
@@ -0,0 +1,17 @@
{
"name": "troika-worker-utils",
"version": "0.52.0",
"description": "Utilities for executing code in Web Workers",
"author": "Jason Johnston <jason.johnston@protectwise.com>",
"repository": {
"type": "git",
"url": "https://github.com/protectwise/troika.git",
"directory": "packages/troika-worker-utils"
},
"license": "MIT",
"sideEffects": false,
"main": "dist/troika-worker-utils.umd.js",
"module": "dist/troika-worker-utils.esm.js",
"module:src": "src/index.js",
"gitHead": "d2eab048d7eac8856e3d19b4254cb746a52a4d4d"
}
+180
View File
@@ -0,0 +1,180 @@
import { workerBootstrap } from './workerBootstrap.js'
import { defineMainThreadModule } from './mainThreadFallback.js'
import { supportsWorkers } from './supportsWorkers.js'
let _workerModuleId = 0
let _messageId = 0
let _allowInitAsString = false
const workers = Object.create(null)
const registeredModules = Object.create(null) //workerId -> Set<unregisterFn>
const openRequests = Object.create(null)
/**
* Define a module of code that will be executed with a web worker. This provides a simple
* interface for moving chunks of logic off the main thread, and managing their dependencies
* among one another.
*
* @param {object} options
* @param {function} options.init
* @param {array} [options.dependencies]
* @param {function} [options.getTransferables]
* @param {string} [options.name]
* @param {string} [options.workerId]
* @return {function(...[*]): {then}}
*/
export function defineWorkerModule(options) {
if ((!options || typeof options.init !== 'function') && !_allowInitAsString) {
throw new Error('requires `options.init` function')
}
let {dependencies, init, getTransferables, workerId} = options
const onMainThread = defineMainThreadModule(options)
if (workerId == null) {
workerId = '#default'
}
const id = `workerModule${++_workerModuleId}`
const name = options.name || id
let registrationPromise = null
dependencies = dependencies && dependencies.map(dep => {
// Wrap raw functions as worker modules with no dependencies
if (typeof dep === 'function' && !dep.workerModuleData) {
_allowInitAsString = true
dep = defineWorkerModule({
workerId,
name: `<${name}> function dependency: ${dep.name}`,
init: `function(){return (\n${stringifyFunction(dep)}\n)}`
})
_allowInitAsString = false
}
// Grab postable data for worker modules
if (dep && dep.workerModuleData) {
dep = dep.workerModuleData
}
return dep
})
function moduleFunc(...args) {
if (!supportsWorkers()) {
return onMainThread(...args)
}
// Register this module if needed
if (!registrationPromise) {
registrationPromise = callWorker(workerId,'registerModule', moduleFunc.workerModuleData)
const unregister = () => {
registrationPromise = null
registeredModules[workerId].delete(unregister)
}
;(registeredModules[workerId] || (registeredModules[workerId] = new Set())).add(unregister)
}
// Invoke the module, returning a promise
return registrationPromise.then(({isCallable}) => {
if (isCallable) {
return callWorker(workerId,'callModule', {id, args})
} else {
throw new Error('Worker module function was called but `init` did not return a callable function')
}
})
}
moduleFunc.workerModuleData = {
isWorkerModule: true,
id,
name,
dependencies,
init: stringifyFunction(init),
getTransferables: getTransferables && stringifyFunction(getTransferables)
}
moduleFunc.onMainThread = onMainThread;
return moduleFunc
}
/**
* Terminate an active Worker by a workerId that was passed to defineWorkerModule.
* This only terminates the Worker itself; the worker module will remain available
* and if you call it again its Worker will be respawned.
* @param {string} workerId
*/
export function terminateWorker(workerId) {
// Unregister all modules that were registered in that worker
if (registeredModules[workerId]) {
registeredModules[workerId].forEach(unregister => {
unregister()
})
}
// Terminate the Worker object
if (workers[workerId]) {
workers[workerId].terminate()
delete workers[workerId]
}
}
/**
* Stringifies a function into a form that can be deserialized in the worker
* @param fn
*/
export function stringifyFunction(fn) {
let str = fn.toString()
// If it was defined in object method/property format, it needs to be modified
if (!/^function/.test(str) && /^\w+\s*\(/.test(str)) {
str = 'function ' + str
}
return str
}
function getWorker(workerId) {
let worker = workers[workerId]
if (!worker) {
// Bootstrap the worker's content
const bootstrap = stringifyFunction(workerBootstrap)
// Create the worker from the bootstrap function content
worker = workers[workerId] = new Worker(
URL.createObjectURL(
new Blob(
[`/** Worker Module Bootstrap: ${workerId.replace(/\*/g, '')} **/\n\n;(${bootstrap})()`],
{type: 'application/javascript'}
)
)
)
// Single handler for response messages from the worker
worker.onmessage = e => {
const response = e.data
const msgId = response.messageId
const callback = openRequests[msgId]
if (!callback) {
throw new Error('WorkerModule response with empty or unknown messageId')
}
delete openRequests[msgId]
callback(response)
}
}
return worker
}
// Issue a call to the worker with a callback to handle the response
function callWorker(workerId, action, data) {
return new Promise((resolve, reject) => {
const messageId = ++_messageId
openRequests[messageId] = response => {
if (response.success) {
resolve(response.result)
} else {
reject(new Error(`Error in worker ${action} call: ${response.error}`))
}
}
getWorker(workerId).postMessage({
messageId,
action,
data
})
})
}
+3
View File
@@ -0,0 +1,3 @@
// Troika worker utility exports
export {defineWorkerModule, terminateWorker, stringifyFunction} from './WorkerModules.js'
+44
View File
@@ -0,0 +1,44 @@
/**
* Fallback for `defineWorkerModule` that behaves identically but runs in the main
* thread, for when the execution environment doesn't support web workers or they
* are disallowed due to e.g. CSP security restrictions.
*/
export function defineMainThreadModule(options) {
let moduleFunc = function(...args) {
return moduleFunc._getInitResult().then(initResult => {
if (typeof initResult === 'function') {
return initResult(...args)
} else {
throw new Error('Worker module function was called but `init` did not return a callable function')
}
})
}
moduleFunc._getInitResult = function() {
// We can ignore getTransferables in main thread. TODO workerId?
let {dependencies, init} = options
// Resolve dependencies
dependencies = Array.isArray(dependencies) ? dependencies.map(dep => {
if (dep) {
// If it's a worker module, use its main thread impl
dep = dep.onMainThread || dep
// If it's a main thread worker module, use its init return value
if (dep._getInitResult) {
dep = dep._getInitResult()
}
}
return dep
}) : []
// Invoke init with the resolved dependencies
let initPromise = Promise.all(dependencies).then(deps => {
return init.apply(null, deps)
})
// Cache the resolved promise for subsequent calls
moduleFunc._getInitResult = () => initPromise
return initPromise
}
return moduleFunc
}
+31
View File
@@ -0,0 +1,31 @@
let supportsWorkers = () => {
let supported = false
// Only attempt worker initialization in browsers; elsewhere it would just be
// noise e.g. loading into a Node environment for SSR.
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
try {
// TODO additional checks for things like importScripts within the worker?
// Would need to be an async check.
let worker = new Worker(
URL.createObjectURL(new Blob([''], { type: 'application/javascript' }))
)
worker.terminate()
supported = true
} catch (err) {
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
// No console log for node env 'test' (e.g. tests with Jest)
} else {
console.log(
`Troika createWorkerModule: web workers not allowed; falling back to main thread execution. Cause: [${err.message}]`
)
}
}
}
// Cached result
supportsWorkers = () => supported
return supported
}
export { supportsWorkers }
+148
View File
@@ -0,0 +1,148 @@
/**
* Main content for the worker that handles the loading and execution of
* modules within it.
*/
export function workerBootstrap() {
const modules = Object.create(null)
// Handle messages for registering a module
function registerModule({id, name, dependencies=[], init=function(){}, getTransferables=null}, callback) {
// Only register once
if (modules[id]) return
try {
// If any dependencies are modules, ensure they're registered and grab their value
dependencies = dependencies.map(dep => {
if (dep && dep.isWorkerModule) {
registerModule(dep, depResult => {
if (depResult instanceof Error) throw depResult
})
dep = modules[dep.id].value
}
return dep
})
// Rehydrate functions
init = rehydrate(`<${name}>.init`, init)
if (getTransferables) {
getTransferables = rehydrate(`<${name}>.getTransferables`, getTransferables)
}
// Initialize the module and store its value
let value = null
if (typeof init === 'function') {
value = init(...dependencies)
} else {
console.error('worker module init function failed to rehydrate')
}
modules[id] = {
id,
value,
getTransferables
}
callback(value)
} catch(err) {
if (!(err && err.noLog)) {
console.error(err)
}
callback(err)
}
}
// Handle messages for calling a registered module's result function
function callModule({id, args}, callback) {
if (!modules[id] || typeof modules[id].value !== 'function') {
callback(new Error(`Worker module ${id}: not found or its 'init' did not return a function`))
}
try {
const result = modules[id].value(...args)
if (result && typeof result.then === 'function') {
result.then(handleResult, rej => callback(rej instanceof Error ? rej : new Error('' + rej)))
} else {
handleResult(result)
}
} catch(err) {
callback(err)
}
function handleResult(result) {
try {
let tx = modules[id].getTransferables && modules[id].getTransferables(result)
if (!tx || !Array.isArray(tx) || !tx.length) {
tx = undefined //postMessage is very picky about not passing null or empty transferables
}
callback(result, tx)
} catch(err) {
console.error(err)
callback(err)
}
}
}
function rehydrate(name, str) {
let result = void 0
self.troikaDefine = r => result = r
let url = URL.createObjectURL(
new Blob(
[`/** ${name.replace(/\*/g, '')} **/\n\ntroikaDefine(\n${str}\n)`],
{type: 'application/javascript'}
)
)
try {
importScripts(url)
} catch(err) {
console.error(err)
}
URL.revokeObjectURL(url)
delete self.troikaDefine
return result
}
// Handler for all messages within the worker
self.addEventListener('message', e => {
const {messageId, action, data} = e.data
try {
// Module registration
if (action === 'registerModule') {
registerModule(data, result => {
if (result instanceof Error) {
postMessage({
messageId,
success: false,
error: result.message
})
} else {
postMessage({
messageId,
success: true,
result: {isCallable: typeof result === 'function'}
})
}
})
}
// Invocation
if (action === 'callModule') {
callModule(data, (result, transferables) => {
if (result instanceof Error) {
postMessage({
messageId,
success: false,
error: result.message
})
} else {
postMessage({
messageId,
success: true,
result
}, transferables || undefined)
}
})
}
} catch(err) {
postMessage({
messageId,
success: false,
error: err.stack
})
}
})
}