diff --git a/packages/react-web-worker/CHANGELOG.md b/packages/react-web-worker/CHANGELOG.md
index 1acc20c4e9..5c3b5cfa30 100644
--- a/packages/react-web-worker/CHANGELOG.md
+++ b/packages/react-web-worker/CHANGELOG.md
@@ -7,6 +7,10 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+## [1.1.0] - 2019-11-08
+
+- You can now pass options as the second argument to `useWorker`. These options are forwarded as the [options to the worker creator](../web-worker#customizing-worker-creation).
+
## [1.0.4] - 2019-11-07
### Fixed
diff --git a/packages/react-web-worker/README.md b/packages/react-web-worker/README.md
index 7024e017cd..f42625ecaa 100644
--- a/packages/react-web-worker/README.md
+++ b/packages/react-web-worker/README.md
@@ -35,18 +35,17 @@ function Home() {
const worker = useWorker(createWorker);
const [message, setMessage] = React.useState(null);
- useEffect(
- () => {
- (async () => {
- // Note: in your actual app code, make sure to check if Home
- // is still mounted before setting state asynchronously!
- const webWorkerMessage = await worker.hello('Tobi');
- setMessage(webWorkerMessage);
- })();
- },
- [worker],
- );
+ useEffect(() => {
+ (async () => {
+ // Note: in your actual app code, make sure to check if Home
+ // is still mounted before setting state asynchronously!
+ const webWorkerMessage = await worker.hello('Tobi');
+ setMessage(webWorkerMessage);
+ })();
+ }, [worker]);
return {message} ;
}
```
+
+You can optionally pass a second argument to `useWorker`, which will be used as the [options to the worker creator function](../web-worker#customizing-worker-creation).
diff --git a/packages/react-web-worker/src/hooks.ts b/packages/react-web-worker/src/hooks.ts
index 69f10b6a18..fa41295451 100644
--- a/packages/react-web-worker/src/hooks.ts
+++ b/packages/react-web-worker/src/hooks.ts
@@ -1,9 +1,12 @@
import {useEffect} from 'react';
-import {terminate} from '@shopify/web-worker';
+import {terminate, CreateWorkerOptions} from '@shopify/web-worker';
import {useLazyRef} from '@shopify/react-hooks';
-export function useWorker(creator: () => T) {
- const {current: worker} = useLazyRef(creator);
+export function useWorker(
+ creator: (options?: CreateWorkerOptions) => T,
+ options?: CreateWorkerOptions,
+) {
+ const {current: worker} = useLazyRef(() => creator(options));
useEffect(() => {
return () => {
diff --git a/packages/rpc/CHANGELOG.md b/packages/rpc/CHANGELOG.md
index 8a61c9456a..e55fbe75f4 100644
--- a/packages/rpc/CHANGELOG.md
+++ b/packages/rpc/CHANGELOG.md
@@ -7,6 +7,10 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Fixed
+
+- Made the default function strategy use the messenger, which makes is more broadly useful across different `postMessage` interfaces.
+
### Added
- `@shopify/rpc` package
diff --git a/packages/rpc/src/endpoint.ts b/packages/rpc/src/endpoint.ts
index ced9e753f0..4dbb22fe32 100644
--- a/packages/rpc/src/endpoint.ts
+++ b/packages/rpc/src/endpoint.ts
@@ -5,7 +5,7 @@ import {
FunctionStrategyOptions,
} from './types';
import {Retainer, StackFrame} from './memory';
-import {createChannelFunctionStrategy} from './strategies';
+import {createMessengerFunctionStrategy} from './strategies';
const APPLY = 0;
const RESULT = 1;
@@ -32,7 +32,7 @@ export function createEndpoint(
initialMessenger: MessageEndpoint,
{
uuid = defaultUuid,
- createFunctionStrategy = createChannelFunctionStrategy,
+ createFunctionStrategy = createMessengerFunctionStrategy,
}: Options = {},
): Endpoint {
let terminated = false;
diff --git a/packages/rpc/src/index.ts b/packages/rpc/src/index.ts
index 19768542d1..dabfd8d1a7 100644
--- a/packages/rpc/src/index.ts
+++ b/packages/rpc/src/index.ts
@@ -19,4 +19,5 @@ export {
FunctionStrategyOptions,
RemoteCallable,
SafeRpcArgument,
+ MessageEndpoint,
} from './types';
diff --git a/packages/web-worker/CHANGELOG.md b/packages/web-worker/CHANGELOG.md
index 3a611d06a0..e8a5f3da0b 100644
--- a/packages/web-worker/CHANGELOG.md
+++ b/packages/web-worker/CHANGELOG.md
@@ -7,6 +7,17 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+## [1.2.0] - 2019-11-08
+
+### Changed
+
+- Uses the new `@shopify/rpc` library for communication with the worker.
+
+### Added
+
+- You can now supply an optional options object to the `createWorkerFactory` functions. One option is currently supported: `createMessenger`, which allows you to customize the message channel for the worker.
+- To support creating workers that are not treated as same-origin, the library now provides a `createIframeWorkerMessenger` function. This function is passed to the new `createMessenger` API, and works by creating a message channel directly from the host page to a worker in a sandboxed `iframe`.
+
## [1.1.0] - 2019-10-21
### Added
diff --git a/packages/web-worker/README.md b/packages/web-worker/README.md
index b9213df8c7..150fecc070 100644
--- a/packages/web-worker/README.md
+++ b/packages/web-worker/README.md
@@ -47,7 +47,7 @@ const result = await worker.hello('world'); // 'Hello, world'
Note that more complex workers are allowed; it can export multiple functions, including default exports, and it can accept complex argument types [with some restrictions](#limitations):
```ts
-const worker = makeWorker();
+const worker = createWorker();
// Assuming worker was:
// export default function hello(name) {
@@ -82,119 +82,86 @@ const worker = createWorker();
terminate(worker);
```
-##### Naming the worker file
-
-By default, worker files created using `createWorkerFactory` are given incrementing IDs as the file name. This strategy is generally less than ideal for long-term caching, as the name of the file depends on the order in which it was encountered during the build. For long-term caching, it is better to provide a static name for the worker file. This can be done by supplying the [`webpackChunkName` comment](https://webpack.js.org/api/module-methods/#magic-comments) before your import:
-
-```tsx
-import {createWorkerFactory, terminate} from '@shopify/web-worker;
-
-// Note: only webpackChunkName is currently supported. Don’t try to use
-// other magic webpack comments.
-const createWorker = createWorkerFactory(() => import(/* webpackChunkName: 'myWorker' */ './worker'));
-```
+##### Customizing worker creation
-This name will be used as the prefix for the worker file. The worker will always end in `.worker.js`, and may also include additional hashes or other content (this library re-uses your `output.filename` and `output.chunkFilename` webpack options).
+By default, this library will create a worker by calling `new Worker` with a blob URL for the worker script. This is generally all you need, but some use cases may want to construct the worker differently. For example, you might want to construct a worker in a sandboxed iframe to ensure the worker is not treated as same-origin, or create a worker farm instead of a worker per script. To do so, you can supply the `createMessenger` option to the function provided by `createWorkerFactory`. This option should be a function that accepts a `URL` object for the location of the worker script, and return a `MessageEndpoint` compatible with being passed to `@shopify/rpc`’s `createEndpoint` API.
-#### Worker
+```ts
+import {fromMessagePort} from '@shopify/rpc';
+import {createWorkerFactory} from '@shopify/web-worker';
-Your worker can be written almost indistinguishably from a "normal" module. It can import other modules (including async `import()` statements), use modern JavaScript features, and more. The exported functions from your module form its public API, which the main thread code will call into as shown above. Note that only functions can be exported; this library is primarily meant to be used to create an imperative API for offloading work to a worker, for which only function exports are needed.
+/* imaginary abstraction that vends workers */
+const workerFarm = {};
-As noted in the browser section, worker code should be mindful of the [limitations](#limitations) of what can be passed into and out of a worker function.
+const createWorker = createWorkerFactory(() => import('./worker'));
+const worker = createWorker({
+ createMessenger(url) {
+ return workerFarm.workerWithUrl(url);
+ },
+});
+```
-Your worker functions should be careful to note that, if they accept any arguments that include functions, those functions should at least optionally return a promise. This is because, when this argument is passed from the main thread to the worker, it can only pass a function that returns a promise. To help you make sure you are respecting this condition, we provide a `SafeWorkerArgument` helper type you can use for all arguments that your worker accepts.
+An optimization many uses of `createMessenger` will want to make is to use a `MessageChannel` to directly connect the worker and the main page, even if the worker itself is constructed unconventionally (e.g., inside an iframe). As a convenience, the worker that is constructed by this library supports `postMessage`ing a special `{__replace: MessagePort}` object. When sent, the `MessagePort` will be used as an argument to the worker’s [`Endpoing#replace` method](../rpc#endpoint-replace), making it the communication channel for all messages between the parent page and worker.
```ts
-import {SafeWorkerArgument} from '@shopify/web-worker';
-
-export function greet(name: SafeWorkerArgument string>) {
- // name is `string | (() => string | Promise)` because a worker
- // can synchronously pass a `string` argument, but can only provide a
- // `() => Promise` function, since it will have to proxy over
- // message passing. Note that `() => string` is still allowed because
- // it could still be valid for another function in the worker to call
- // with a function of that type.
- return (
- typeof name === 'string'
- ? `Hello, ${name}`
- : Promise.resolve(name()).then((name) => `Hello, ${name}`)
- );
-}
-```
+import {fromMessagePort} from '@shopify/rpc';
+import {createWorkerFactory} from '@shopify/web-worker';
-The same [memory management concerns](#memory) apply to the worker as they do on the main thread.
+/* imaginary abstraction that creates workers in an iframe */
+const iframe = {};
-#### Limitations
+const createWorker = createWorkerFactory(() => import('./worker'));
+const worker = createWorker({
+ createMessenger(url) {
+ const {port1, port2} = new MessageChannel();
+ iframe.createWorker(url).postMessage({__replace: port2}, [port2]);
+
+ // In a real example, you'd want to also clean up the worker
+ // you've created in the iframe as part of the `terminate()` method.
+ return fromMessagePort(port1);
+ },
+});
+```
-There are two key limitations to be aware of when calling functions in a worker with the help of this library:
+###### `createIframeWorkerMessenger()`
-1. Only basic data structures are supported. Any function involved in a call across the main thread/ worker boundary can accept and return primitive types, objects, arrays, and functions. You can’t pass other data structures (like `Map`s or `Set`s), and if you pass class instances back and forth, only their own properties will be available on the other side.
-2. The worker can only export functions. Because the main thread can’t access the worker thread values synchronously, there is no way to implement arbitrary access to non-function exports without awkward use of promises.
+The `createIframeWorkerMessenger` is provided to make it easy to create a worker that is not treated as same-origin to the parent page. This function will, for each worker, create an iframe with a restrictive `sandbox` attribute and an anonymous origin, and will force that iframe to create a worker. It then passes a `MessagePort` through to the worker as the `postMessage` interface to use so that messages go directly between the worker and the original page.
-Additionally, when passing functions to and from the worker, developers may occasionally need to manually manage memory. This is detailed in the next section.
+```ts
+import {
+ createWorkerFactory,
+ createIframeWorkerMessenger,
+} from '@shopify/web-worker';
-#### Memory
+const createWorker = createWorkerFactory(() => import('./worker'));
+const worker = createWorker({
+ createMessenger: createIframeWorkerMessenger,
+});
+```
-Web worker’s can’t share memory with their parent, and functions can’t be serialized for `postMessage`. The implementation of passing functions between worker and parent is therefore implemented very differently from other data types: the worker and parent side keep references to functions that have been passed between the two, and they have a shared strategy for proxying calls from the "target" side back to the original source function.
+##### Naming the worker file
-This strategy is effective, but without extra intervention it will leak memory. Even if the parent and worker no longer have references to that function, it must still be retained because the parent can’t know that the worker no longer needs to call that function.
+By default, worker files created using `createWorkerFactory` are given incrementing IDs as the file name. This strategy is generally less than ideal for long-term caching, as the name of the file depends on the order in which it was encountered during the build. For long-term caching, it is better to provide a static name for the worker file. This can be done by supplying the [`webpackChunkName` comment](https://webpack.js.org/api/module-methods/#magic-comments) before your import:
-This library automatically implements some memory management for you. A function passed between the worker and parent is automatically retained for the lifetime of the original function call, and is subsequently released.
+```tsx
+import {createWorkerFactory, terminate} from '@shopify/web-worker;
-```ts
-// PARENT
-
-const worker = createWorker(/* ... */)();
-const funcForWorker = () => 'Tobi';
-
-worker
- // funcForWorker is retained on the main thread here...
- .greet(funcForWorker)
- // And is automatically released here because the worker
- // signals it no longer needs the function
- .then(result => console.log(result));
-
-// WORKER
-
-export async function greet(getName: () => Promise) {
- // Worker signals that it needs to retain `getName`, which
- // was passed from the parent.
-
- try {
- return `Hello, ${await getName()}`;
- } finally {
- // Once this function exits, the library defaults to releasing
- // `getName`, which signals to the main thread it can release
- // the original function.
- }
-}
+// Note: only webpackChunkName is currently supported. Don’t try to use
+// other magic webpack comments.
+const createWorker = createWorkerFactory(() => import(/* webpackChunkName: 'myWorker' */ './worker'));
```
-This covers most common memory management cases, but one important exception remains: if you save the function on to an object in context, it will be still be accessible to your program, but the source of the function will be told to release the reference to that function. In this case, if you try to call the function from the worker at a later time, you will receive an error indicating that the value has been released.
-
-To resolve this problem, this library provides `retain` and `release` functions. Calling these on an object will increment the number of "retainers", allowing the source function to be retained. Any time you call `retain`, you must eventually call `release`, when you know you will no longer call that function.
-
-```ts
-// WORKER
-
-import {retain, release} from '@shopify/web-worker';
+This name will be used as the prefix for the worker file. The worker will always end in `.worker.js`, and may also include additional hashes or other content (this library re-uses your `output.filename` and `output.chunkFilename` webpack options).
-export function setNameGetter(getName: () => Promise) {
- retain(getName);
+#### Worker
- if (self.getName) {
- release(self.getName);
- }
+Your worker can be written almost indistinguishably from a "normal" module. It can import other modules (including async `import()` statements), use modern JavaScript features, and more. The exported functions from your module form its public API, which the main thread code will call into as shown above. Note that only functions can be exported; this library is primarily meant to be used to create an imperative API for offloading work to a worker, for which only function exports are needed.
- self.getName = getName;
-}
+As noted in the browser section, worker code should be mindful of the [limitations](#limitations) of what can be passed into and out of a worker function.
-export async function greet() {
- return `Hello, ${self.getName ? await self.getName() : 'friend'}!`;
-}
-```
+#### Limitations
-Remember that any function passed between the worker and its parent, including functions attached as properties of objects, must be retained manually if you intend to call them outside the scope of the first function where they were passed over the bridge. To help make this easier, `release` and `retain` will automatically deeply release/ retain all functions when they are called with objects or arrays.
+This library implements the calling of functions on a worker using [`@shopify/rpc`](../rpc). As such, all the limitations and additional considerations in that library must be considered with the functions you expose from the worker. In particular, note the [memory management concerns](../rpc#memory) when passing functions to and from the worker. For convenience, the `release` and `retain` methods from `@shopify/rpc` are re-exported from this library.
### Tooling
diff --git a/packages/web-worker/package.json b/packages/web-worker/package.json
index ceb95c4bf8..b3e76f2799 100644
--- a/packages/web-worker/package.json
+++ b/packages/web-worker/package.json
@@ -24,6 +24,7 @@
},
"homepage": "https://github.com/Shopify/quilt/blob/master/packages/web-workers/README.md",
"dependencies": {
+ "@shopify/rpc": "^1.0.0",
"@types/webpack": "^4.0.0",
"loader-utils": "^1.0.0",
"webpack-virtual-modules": "^0.1.12"
diff --git a/packages/web-worker/src/create.ts b/packages/web-worker/src/create.ts
index e4e6367fb0..fecb260649 100644
--- a/packages/web-worker/src/create.ts
+++ b/packages/web-worker/src/create.ts
@@ -1,4 +1,6 @@
-import {createEndpoint, Endpoint} from './endpoint';
+import {createEndpoint, Endpoint, MessageEndpoint} from '@shopify/rpc';
+
+import {createWorkerMessenger} from './messenger';
const workerEndpointCache = new WeakMap['call'], Endpoint>();
@@ -23,8 +25,14 @@ export function getEndpoint(caller: any) {
return workerEndpointCache.get(caller);
}
+export interface CreateWorkerOptions {
+ createMessenger?(url: URL): MessageEndpoint;
+}
+
export function createWorkerFactory(script: () => Promise) {
- return function createWorker(): Endpoint['call'] {
+ return function createWorker({
+ createMessenger = createWorkerMessenger,
+ }: CreateWorkerOptions = {}): Endpoint['call'] {
// The babel plugin that comes with this package actually turns the argument
// into a string (the public path of the worker script). If it’s a function,
// it’s because we’re in an environment where we didn’t transform it into a
@@ -46,7 +54,7 @@ export function createWorkerFactory(script: () => Promise) {
// If we aren’t in an environment that supports Workers, just bail out
// with a dummy worker that throws for every method call.
- if (typeof Worker === 'undefined') {
+ if (typeof window === 'undefined') {
return new Proxy(
{},
{
@@ -61,21 +69,8 @@ export function createWorkerFactory(script: () => Promise) {
) as any;
}
- const absoluteScript = new URL(script, window.location.href).href;
-
- const workerScript = URL.createObjectURL(
- new Blob([`importScripts(${JSON.stringify(absoluteScript)})`]),
- );
-
- const worker = new Worker(workerScript);
-
- const originalTerminate = worker.terminate.bind(worker);
- worker.terminate = () => {
- URL.revokeObjectURL(workerScript);
- originalTerminate();
- };
-
- const endpoint = createEndpoint(worker);
+ const scriptUrl = new URL(script, window.location.href);
+ const endpoint = createEndpoint(createMessenger(scriptUrl));
const {call: caller} = endpoint;
workerEndpointCache.set(caller, endpoint);
diff --git a/packages/web-worker/src/endpoint.ts b/packages/web-worker/src/endpoint.ts
deleted file mode 100644
index 9489dc9b79..0000000000
--- a/packages/web-worker/src/endpoint.ts
+++ /dev/null
@@ -1,430 +0,0 @@
-import {PromisifyModule} from './types';
-import {
- RETAIN_METHOD,
- RELEASE_METHOD,
- RETAINED_BY,
- Retainer,
- MemoryManageable,
- isMemoryManageable,
-} from './memory';
-
-const ID = '_';
-const FUNCTION = '_@f';
-const RELEASE = '_@r';
-const REVOKE = '_@v';
-const APPLY = '_@a';
-const API_ENDPOINT = '_@i';
-const APPLY_RESULT = '_@ar';
-const APPLY_ERROR = '_@ae';
-const TERMINATE = '_@t';
-
-class StackFrame {
- private readonly memoryManaged = new Set();
-
- add(memoryManageable: MemoryManageable) {
- this.memoryManaged.add(memoryManageable);
- memoryManageable[RETAINED_BY].add(this);
- memoryManageable[RETAIN_METHOD]();
- }
-
- release() {
- for (const memoryManaged of this.memoryManaged) {
- memoryManaged[RETAINED_BY].delete(this);
- memoryManaged[RELEASE_METHOD]();
- }
-
- this.memoryManaged.clear();
- }
-}
-
-interface MessageEndpoint {
- postMessage(message: any, transferables?: Transferable[]): void;
- addEventListener(
- event: 'message',
- listener: (event: MessageEvent) => void,
- ): void;
- removeEventListener(
- event: 'message',
- listener: (event: MessageEvent) => void,
- ): void;
- terminate?(): void;
-}
-
-interface FunctionSerialization {
- [FUNCTION]: [string, MessagePort?];
-}
-
-interface ReleaseMessage {
- [RELEASE]: 1;
-}
-
-interface RevokeMessage {
- [REVOKE]: 1;
-}
-
-interface ApplyMessage {
- [APPLY]: any[];
-}
-
-interface ApplyApiEndpoint extends ApplyMessage {
- [API_ENDPOINT]: string;
-}
-
-interface ApplyResultMessage {
- [APPLY_RESULT]: any;
-}
-
-interface ApplyErrorMessage {
- [APPLY_ERROR]: {name: string; message: string; stack?: string};
-}
-
-interface TerminateMessage {
- [TERMINATE]: 1;
-}
-
-interface Options {
- uuid?(): string;
-}
-
-export interface Endpoint {
- call: PromisifyModule;
- expose(api: {[key: string]: Function | undefined}): void;
- revoke(value: Function): void;
- exchange(value: Function, newValue: Function): void;
- terminate(): void;
-}
-
-export function createEndpoint(
- messageEndpoint: MessageEndpoint,
- {uuid = defaultUuid}: Options = {},
-): Endpoint {
- let terminated = false;
- const functionStore = new Map();
- const functionProxies = new Map();
- const removeListeners = new WeakMap void>();
- const activeApi = new Map();
-
- makeCallable(messageEndpoint, (apiCall: ApplyApiEndpoint) =>
- activeApi.get(apiCall[API_ENDPOINT]),
- );
-
- messageEndpoint.addEventListener('message', ({data}) => {
- if (TERMINATE in data) {
- [functionStore, functionProxies, activeApi].forEach(map => map.clear());
- terminated = true;
- }
- });
-
- return {
- call: new Proxy(
- {},
- {
- get(_target, property) {
- return (...args: any[]) => {
- if (terminated) {
- throw new Error(
- 'You attempted to call a function on a terminated web worker.',
- );
- }
-
- return call(messageEndpoint, args, [], {[API_ENDPOINT]: property});
- };
- },
- },
- ) as any,
- expose(api: {[key: string]: Function | undefined}) {
- for (const key of Object.keys(api)) {
- const value = api[key];
-
- if (typeof value === 'function') {
- activeApi.set(key, value);
- } else {
- activeApi.delete(key);
- }
- }
- },
- revoke(value: Function) {
- if (!functionStore.has(value)) {
- throw new Error(
- 'You tried to revoke a function that is not currently stored.',
- );
- }
-
- const [, port] = functionStore.get(value)!;
- port.postMessage({[REVOKE]: 1} as RevokeMessage);
- port.close();
- functionStore.delete(value);
- },
- exchange(value: Function, newValue: Function) {
- if (!functionStore.has(value)) {
- throw new Error(
- 'You tried to exchange a value that is not currently stored.',
- );
- }
-
- const [id, port] = functionStore.get(value)!;
- makeCallable(port, () => newValue);
- functionStore.set(newValue, [id, port]);
- },
- terminate() {
- [functionStore, functionProxies, activeApi].forEach(map => map.clear());
- terminated = true;
-
- if (messageEndpoint.terminate) {
- messageEndpoint.terminate();
- } else {
- messageEndpoint.postMessage({[TERMINATE]: 1} as TerminateMessage);
- }
- },
- };
-
- function makeCallable(
- messageEndpoint: MessageEndpoint,
- getFunction: (data: any) => Function | undefined,
- ) {
- const remove = removeListeners.get(messageEndpoint);
-
- if (remove) {
- remove();
- }
-
- async function listener({data}) {
- if (!(APPLY in data)) {
- return;
- }
-
- const stackFrame = new StackFrame();
-
- try {
- const func = getFunction(data)!;
- const retainedBy = isMemoryManageable(func)
- ? [stackFrame, ...func[RETAINED_BY]]
- : [stackFrame];
-
- const result = await func(...fromWire(data[APPLY], retainedBy));
- const [serializedResult, transferables] = toWire(result);
- messageEndpoint.postMessage(
- {[ID]: data[ID], [APPLY_RESULT]: serializedResult},
- transferables,
- );
- } catch (error) {
- const {name, message, stack} = error;
- messageEndpoint.postMessage({
- [ID]: data[ID],
- [APPLY_ERROR]: {name, message, stack},
- });
- } finally {
- stackFrame.release();
- }
- }
-
- messageEndpoint.addEventListener('message', listener);
- removeListeners.set(messageEndpoint, () =>
- messageEndpoint.removeEventListener('message', listener),
- );
- }
-
- function call(
- messageEndpoint: MessageEndpoint,
- args: any[],
- retainedBy: Retainer[] = [],
- message: object = {},
- ) {
- const id = uuid();
- const done = new Promise((resolve, reject) => {
- messageEndpoint.addEventListener('message', function listener({data}) {
- if (data == null || data[ID] !== id) {
- return;
- }
-
- messageEndpoint.removeEventListener('message', listener);
-
- if (APPLY_ERROR in data) {
- const error = new Error();
- Object.assign(error, (data as ApplyErrorMessage)[APPLY_ERROR]);
- reject(error);
- } else {
- resolve(
- fromWire((data as ApplyResultMessage)[APPLY_RESULT], retainedBy),
- );
- }
- });
- });
-
- const [serializedArgs, transferables] = toWire(args);
- messageEndpoint.postMessage(
- {
- [ID]: id,
- [APPLY]: serializedArgs,
- ...message,
- } as ApplyMessage,
- transferables,
- );
-
- return done;
- }
-
- function toWire(value: unknown): [any, Transferable[]?] {
- if (typeof value === 'object') {
- if (value == null) {
- return [value];
- }
-
- const transferables: Transferable[] = [];
-
- if (Array.isArray(value)) {
- const result = value.map(item => {
- const [result, nestedTransferables = []] = toWire(item);
- transferables.push(...nestedTransferables);
- return result;
- });
-
- return [result, transferables];
- }
-
- const result = Object.keys(value).reduce((object, key) => {
- const [result, nestedTransferables = []] = toWire(value[key]);
- transferables.push(...nestedTransferables);
- return {...object, [key]: result};
- }, {});
-
- return [result, transferables];
- }
-
- if (typeof value === 'function') {
- if (functionStore.has(value)) {
- const [id] = functionStore.get(value)!;
- return [{[FUNCTION]: [id]} as FunctionSerialization];
- }
-
- const id = uuid();
- const {port1, port2} = new MessageChannel();
- makeCallable(port1, () => value);
- functionStore.set(value, [id, port1]);
-
- port1.addEventListener('message', function listener({data}) {
- if (data && RELEASE in data) {
- port1.removeEventListener('message', listener);
- port1.close();
- functionStore.delete(value);
- }
- });
-
- port1.start();
-
- return [{[FUNCTION]: [id, port2]} as FunctionSerialization, [port2]];
- }
-
- return [value];
- }
-
- function fromWire(value: unknown, retainedBy: Retainer[] = []) {
- if (typeof value === 'object') {
- if (value == null) {
- return value;
- }
-
- if (Array.isArray(value)) {
- return value.map(value => fromWire(value, retainedBy));
- }
-
- if (value[FUNCTION]) {
- const [id, port] = (value as FunctionSerialization)[FUNCTION];
-
- if (functionProxies.has(id)) {
- return functionProxies.get(id)!;
- }
-
- let retainCount = 0;
- let released = false;
- let revoked = false;
-
- const release = () => {
- retainCount -= 1;
-
- if (retainCount === 0) {
- released = true;
- functionProxies.delete(id);
- port!.postMessage({[RELEASE]: 1} as ReleaseMessage);
- port!.close();
- }
- };
-
- const retain = () => {
- retainCount += 1;
- };
-
- port!.addEventListener('message', ({data}) => {
- if (data && data[REVOKE]) {
- revoked = true;
- functionProxies.delete(id);
- port!.close();
- }
- });
-
- const retainers = new Set(retainedBy);
-
- const proxy = new Proxy(function() {}, {
- get(target, prop, receiver) {
- if (prop === 'apply' || prop === 'bind') {
- return receiver;
- }
-
- if (prop === RELEASE_METHOD) {
- return release;
- }
-
- if (prop === RETAIN_METHOD) {
- return retain;
- }
-
- if (prop === RETAINED_BY) {
- return retainers;
- }
-
- return Reflect.get(target, prop, receiver);
- },
- apply(_target, _this, args) {
- if (released) {
- throw new Error(
- 'You attempted to call a function that was already released.',
- );
- }
-
- if (revoked) {
- throw new Error(
- 'You attempted to call a function that was already revoked.',
- );
- }
-
- return call(port!, args, retainedBy);
- },
- });
-
- for (const retainer of retainers) {
- retainer.add(proxy as any);
- }
-
- functionProxies.set(id, proxy);
- port!.start();
-
- return proxy;
- }
-
- return Object.keys(value).reduce(
- (object, key) => ({...object, [key]: fromWire(value[key], retainedBy)}),
- {},
- );
- }
-
- return value;
- }
-}
-
-function defaultUuid() {
- return `${uuidSegment()}-${uuidSegment()}-${uuidSegment()}-${uuidSegment()}`;
-}
-
-function uuidSegment() {
- return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16);
-}
diff --git a/packages/web-worker/src/index.ts b/packages/web-worker/src/index.ts
index 08c45265e0..b1584cb711 100644
--- a/packages/web-worker/src/index.ts
+++ b/packages/web-worker/src/index.ts
@@ -1,3 +1,12 @@
-export {createWorkerFactory, terminate, expose} from './create';
-export {retain, release} from './memory';
-export {SafeWorkerArgument} from './types';
+export {
+ retain,
+ release,
+ SafeRpcArgument as SafeWorkerArgument,
+} from '@shopify/rpc';
+export {
+ expose,
+ terminate,
+ createWorkerFactory,
+ CreateWorkerOptions,
+} from './create';
+export {createWorkerMessenger, createIframeWorkerMessenger} from './messenger';
diff --git a/packages/web-worker/src/memory.ts b/packages/web-worker/src/memory.ts
deleted file mode 100644
index 9143532c36..0000000000
--- a/packages/web-worker/src/memory.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-export const RETAIN_METHOD = Symbol('retain');
-export const RELEASE_METHOD = Symbol('release');
-export const RETAINED_BY = Symbol('retainedBy');
-
-export interface Retainer {
- add(manageable: MemoryManageable): void;
-}
-
-export interface MemoryManageable {
- readonly [RETAINED_BY]: Set;
- [RETAIN_METHOD](): void;
- [RELEASE_METHOD](): void;
-}
-
-export function isMemoryManageable(value: unknown): value is MemoryManageable {
- return Boolean(
- value && (value as any)[RETAIN_METHOD] && (value as any)[RELEASE_METHOD],
- );
-}
-
-export function retain(value: any, {deep = true} = {}) {
- const canRetain = isMemoryManageable(value);
-
- if (canRetain) {
- value[RETAIN_METHOD]();
- }
-
- if (deep) {
- if (Array.isArray(value)) {
- return value.reduce(
- (canRetain, item) => retain(item, {deep}) || canRetain,
- canRetain,
- );
- } else if (typeof value === 'object' && value != null) {
- return Object.keys(value).reduce(
- (canRetain, key) => retain(value[key], {deep}) || canRetain,
- canRetain,
- );
- }
- }
-
- return canRetain;
-}
-
-export function release(value: any, {deep = true} = {}) {
- const canRelease = isMemoryManageable(value);
-
- if (canRelease) {
- value[RELEASE_METHOD]();
- }
-
- if (deep) {
- if (Array.isArray(value)) {
- return value.reduce(
- (canRelease, item) => release(item, {deep}) || canRelease,
- canRelease,
- );
- } else if (typeof value === 'object' && value != null) {
- return Object.keys(value).reduce(
- (canRelease, key) => release(value[key], {deep}) || canRelease,
- canRelease,
- );
- }
- }
-
- return canRelease;
-}
diff --git a/packages/web-worker/src/messenger/iframe.ts b/packages/web-worker/src/messenger/iframe.ts
new file mode 100644
index 0000000000..1e46f477e5
--- /dev/null
+++ b/packages/web-worker/src/messenger/iframe.ts
@@ -0,0 +1,44 @@
+import {MessageEndpoint} from '@shopify/rpc';
+
+const RUN = '__run';
+const IFRAME_SOURCE = `
+
+
+
+
+
+`;
+
+export function createIframeWorkerMessenger(url: URL): MessageEndpoint {
+ const {port1, port2} = new MessageChannel();
+
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute('sandbox', 'allow-scripts');
+ iframe.addEventListener('load', () => {
+ iframe.contentWindow!.postMessage({[RUN]: url.href}, '*', [port2]);
+ });
+ iframe.srcdoc = IFRAME_SOURCE;
+ document.body.appendChild(iframe);
+
+ return {
+ postMessage: (...args) => port1.postMessage(...args),
+ addEventListener: (...args) => port1.addEventListener(...args),
+ removeEventListener: (...args) => port1.removeEventListener(...args),
+ terminate() {
+ port1.close();
+ iframe.remove();
+ },
+ };
+}
diff --git a/packages/web-worker/src/messenger/index.ts b/packages/web-worker/src/messenger/index.ts
new file mode 100644
index 0000000000..99f088a3b1
--- /dev/null
+++ b/packages/web-worker/src/messenger/index.ts
@@ -0,0 +1,2 @@
+export {createWorkerMessenger} from './worker';
+export {createIframeWorkerMessenger} from './iframe';
diff --git a/packages/web-worker/src/messenger/worker.ts b/packages/web-worker/src/messenger/worker.ts
new file mode 100644
index 0000000000..107c6b0f8b
--- /dev/null
+++ b/packages/web-worker/src/messenger/worker.ts
@@ -0,0 +1,15 @@
+export function createWorkerMessenger(url: URL) {
+ const workerScript = URL.createObjectURL(
+ new Blob([`importScripts(${JSON.stringify(url.href)})`]),
+ );
+
+ const worker = new Worker(workerScript);
+
+ const originalTerminate = worker.terminate.bind(worker);
+ worker.terminate = () => {
+ URL.revokeObjectURL(workerScript);
+ originalTerminate();
+ };
+
+ return worker;
+}
diff --git a/packages/web-worker/src/types.ts b/packages/web-worker/src/types.ts
deleted file mode 100644
index 1d7f2f1675..0000000000
--- a/packages/web-worker/src/types.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-export type PromisifyModule = {[K in keyof T]: PromisifyExport};
-
-export type PromisifyExport = T extends (
- ...args: infer Args
-) => infer TypeReturned
- ? (...args: Args) => Promise>
- : never;
-
-type ForcePromiseWrapped = T extends infer U | Promise
- ? ForcePromise
- : ForcePromise;
-
-type ForcePromise = T extends Promise
- ? T
- : T extends (...args: infer Args) => infer TypeReturned
- ? (...args: Args) => Promise>
- : T extends Array
- ? ForcePromiseArray
- : T extends object
- ? {[K in keyof T]: ForcePromiseWrapped}
- : T;
-
-interface ForcePromiseArray extends Array> {}
-
-export type SafeWorkerArgument = T extends (
- ...args: infer Args
-) => infer TypeReturned
- ? TypeReturned extends Promise
- ? (...args: Args) => TypeReturned
- : (...args: Args) => TypeReturned | Promise
- : T extends Array
- ? SafeWorkerArgumentArray
- : T extends object
- ? {[K in keyof T]: SafeWorkerArgument}
- : T;
-
-interface SafeWorkerArgumentArray extends Array> {}
diff --git a/packages/web-worker/src/worker.ts b/packages/web-worker/src/worker.ts
index 9e2ec9c824..c76f208264 100644
--- a/packages/web-worker/src/worker.ts
+++ b/packages/web-worker/src/worker.ts
@@ -1,7 +1,17 @@
-import {createEndpoint} from './endpoint';
+import {createEndpoint, fromWebWorker} from '@shopify/rpc';
export function expose(api: any) {
- const endpoint = createEndpoint(self as any);
+ const endpoint = createEndpoint(fromWebWorker(self as any));
+
+ self.addEventListener('message', ({data}: MessageEvent) => {
+ if (data == null) {
+ return;
+ }
+
+ if (data.__replace != null) {
+ endpoint.replace(data.__replace);
+ }
+ });
Reflect.defineProperty(self, 'endpoint', {
value: endpoint,
diff --git a/yarn.lock b/yarn.lock
index 0f802c426a..1093a4dc42 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1506,7 +1506,7 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
-"@types/react-dom@16.8.3", "@types/react-dom@^16.0.11", "@types/react-dom@^16.8.3":
+"@types/react-dom@^16.0.11", "@types/react-dom@^16.8.3":
version "16.8.3"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.3.tgz#6131b7b6158bc7ed1925a3374b88b7c00481f0cb"
integrity sha512-HF5hD5YR3z9Mn6kXcW1VKe4AQ04ZlZj1EdLBae61hzQ3eEWWxMgNLUbIxeZp40BnSxqY1eAYLsH9QopQcxzScA==