Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Namespaced APIs #563

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
# Names should be added to this file as:
# Name <email address>
Surma <[email protected]>
Menecats <[email protected]>
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,61 @@ onconnect = function (event) {
// onconnect = (e) => Comlink.expose(obj, e.ports[0]);
```

### [`Namespaces`](./docs/examples/08-namespaces-example)

When you need to expose more objects from a thread you can use Comlink's namespaced APIs.
These APIs allow you to independently expose objects and access them later from the main thread.

**Note:** Also the `expose` and `wrap` APIs use namespaces under the hood, they use the default `Comlink.default` namespace.

**main.js**

```javascript
import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
async function init() {
// WebWorkers use `postMessage` and therefore work with Comlink.
const worker = new Worker("worker.js");

// 'wrap' uses the default namespace (Comlink.default)
const obj1 = Comlink.wrap(worker);

// this will connect to the 'my-namespace' namespace.
const obj2 = Comlink.wrapNamespaced(worker, "my-namespace");

alert(await obj2.sayHi("user"));

alert(`Counter: ${await obj1.counter}`);
await obj1.inc();
alert(`Counter: ${await obj1.counter}`);
}
init();
```

**worker.js**

```javascript
importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");

const obj1 = {
counter: 0,
inc() {
this.counter++;
},
};

const obj2 = {
sayHi(name) {
return "Hi, " + name;
},
};

// 'expose' uses the default namespace (Comlink.default)
Comlink.expose(obj1);

// this will expose obj2 under the 'my-namespace' namespace
Comlink.exposeNamespaced(obj2, "my-namespace");
```

**For additional examples, please see the [docs/examples](./docs/examples) directory in the project.**

## API
Expand All @@ -158,6 +213,14 @@ Comlink’s goal is to make _exposed_ values from one thread available in the ot

`wrap` wraps the _other_ end of the message channel and returns a proxy. The proxy will have all properties and functions of the exposed value, but access and invocations are inherently asynchronous. This means that a function that returns a number will now return _a promise_ for a number. **As a rule of thumb: If you are using the proxy, put `await` in front of it.** Exceptions will be caught and re-thrown on the other side.

### `Comlink.wrapNamespaced(endpoint, namespace)` and `Comlink.exposeNamespaced(value, namespace, endpoint?)`

To allow one thread to _expose_ more than one object independently from other objects already _exposed_ Comlink allows you to specify a custom namespace.

The `expose` API uses `exposeNamespaced` with `Comlink.default` as the default namespace.

The same is true for the `wrap` API that uses `wrapNamespaced` with `Comlink.default` as the default namespace.

### `Comlink.transfer(value, transferables)` and `Comlink.proxy(value)`

By default, every function parameter, return value and object property value is copied, in the sense of [structured cloning]. Structured cloning can be thought of as deep copying, but has some limitations. See [this table][structured clone table] for details.
Expand Down Expand Up @@ -209,6 +272,12 @@ Comlink.transferHandlers.set("EVENT", {

Note that this particular transfer handler won’t create an actual `Event`, but just an object that has the `event.target.id` and `event.target.classList` property. Often, this is enough. If not, the transfer handler can be easily augmented to provide all necessary data.

### `Comlink.defaultNamespace`

This constant holds the value of the default namespace used by `Comlink.wrap` and `Comlink.expose`.

Its value is `Comlink.default`.

### `Comlink.releaseProxy`

Every proxy created by Comlink has the `[releaseProxy]` method.
Expand Down
1 change: 1 addition & 0 deletions docs/examples/08-namespaces-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This example shows how to expose multiple objects under different namespaces.
25 changes: 25 additions & 0 deletions docs/examples/08-namespaces-example/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>

<script type="module">
import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
// import * as Comlink from "../../../dist/esm/comlink.mjs";

async function init() {
// WebWorkers use `postMessage` and therefore work with Comlink.
const worker = new Worker("worker.js");

// 'wrap' uses the default namespace (Comlink.default)
const obj1 = Comlink.wrap(worker);

// this will connect to the 'my-namespace' namespace.
const obj2 = Comlink.wrapNamespaced(worker, "my-namespace");

alert(await obj2.sayHi("user"));

alert(`Counter: ${await obj1.counter}`);
await obj1.inc();
alert(`Counter: ${await obj1.counter}`);
}

init();
</script>
34 changes: 34 additions & 0 deletions docs/examples/08-namespaces-example/worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright 2017 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
// importScripts("../../../dist/umd/comlink.js");

const obj1 = {
counter: 0,
inc() {
this.counter++;
},
};

const obj2 = {
sayHi(name) {
return "Hi, " + name;
},
};

// 'expose' uses the default namespace (Comlink.default)
Comlink.expose(obj1);

// this will expose obj2 under the 'my-namespace' namespace
Comlink.exposeNamespaced(obj2, "my-namespace");
51 changes: 40 additions & 11 deletions src/comlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ import {
Endpoint,
EventSource,
Message,
MessageNamespace,
MessageType,
PostMessageWithOrigin,
WireValue,
WireValueType,
} from "./protocol";
export { Endpoint };

export const defaultNamespace: MessageNamespace = "Comlink.default";

export const proxyMarker = Symbol("Comlink.proxy");
export const createEndpoint = Symbol("Comlink.endpoint");
export const releaseProxy = Symbol("Comlink.releaseProxy");
Expand Down Expand Up @@ -281,9 +284,17 @@ export const transferHandlers = new Map<
["throw", throwTransferHandler],
]);

export function expose(obj: any, ep: Endpoint = self as any) {
export function expose(obj: any, ep?: Endpoint) {
return exposeNamespaced(obj, defaultNamespace, ep);
}

export function exposeNamespaced(
obj: any,
namespace: MessageNamespace,
ep: Endpoint = self as any
) {
ep.addEventListener("message", function callback(ev: MessageEvent) {
if (!ev || !ev.data) {
if (!ev || !ev.data || (ev.data as Message).namespace !== namespace) {
return;
}
const { id, type, path } = {
Expand Down Expand Up @@ -342,7 +353,7 @@ export function expose(obj: any, ep: Endpoint = self as any) {
})
.then((returnValue) => {
const [wireValue, transferables] = toWireValue(returnValue);
ep.postMessage({ ...wireValue, id }, transferables);
ep.postMessage({ ...wireValue, id, namespace }, transferables);
if (type === MessageType.RELEASE) {
// detach and deactive after sending release response above.
ep.removeEventListener("message", callback as any);
Expand All @@ -364,7 +375,15 @@ function closeEndPoint(endpoint: Endpoint) {
}

export function wrap<T>(ep: Endpoint, target?: any): Remote<T> {
return createProxy<T>(ep, [], target) as any;
return wrapNamespaced(ep, defaultNamespace, target);
}

export function wrapNamespaced<T>(
ep: Endpoint,
namespace: MessageNamespace,
target?: any
): Remote<T> {
return createProxy<T>(ep, namespace, [], target) as any;
}

function throwIfProxyReleased(isReleased: boolean) {
Expand All @@ -375,6 +394,7 @@ function throwIfProxyReleased(isReleased: boolean) {

function createProxy<T>(
ep: Endpoint,
namespace: MessageNamespace,
path: (string | number | symbol)[] = [],
target: object = function () {}
): Remote<T> {
Expand All @@ -384,7 +404,7 @@ function createProxy<T>(
throwIfProxyReleased(isProxyReleased);
if (prop === releaseProxy) {
return () => {
return requestResponseMessage(ep, {
return requestResponseMessage(ep, namespace, {
type: MessageType.RELEASE,
path: path.map((p) => p.toString()),
}).then(() => {
Expand All @@ -397,13 +417,13 @@ function createProxy<T>(
if (path.length === 0) {
return { then: () => proxy };
}
const r = requestResponseMessage(ep, {
const r = requestResponseMessage(ep, namespace, {
type: MessageType.GET,
path: path.map((p) => p.toString()),
}).then(fromWireValue);
return r.then.bind(r);
}
return createProxy(ep, [...path, prop]);
return createProxy(ep, namespace, [...path, prop]);
},
set(_target, prop, rawValue) {
throwIfProxyReleased(isProxyReleased);
Expand All @@ -412,6 +432,7 @@ function createProxy<T>(
const [value, transferables] = toWireValue(rawValue);
return requestResponseMessage(
ep,
namespace,
{
type: MessageType.SET,
path: [...path, prop].map((p) => p.toString()),
Expand All @@ -424,17 +445,18 @@ function createProxy<T>(
throwIfProxyReleased(isProxyReleased);
const last = path[path.length - 1];
if ((last as any) === createEndpoint) {
return requestResponseMessage(ep, {
return requestResponseMessage(ep, namespace, {
type: MessageType.ENDPOINT,
}).then(fromWireValue);
}
// We just pretend that `bind()` didn’t happen.
if (last === "bind") {
return createProxy(ep, path.slice(0, -1));
return createProxy(ep, namespace, path.slice(0, -1));
}
const [argumentList, transferables] = processArguments(rawArgumentList);
return requestResponseMessage(
ep,
namespace,
{
type: MessageType.APPLY,
path: path.map((p) => p.toString()),
Expand All @@ -448,6 +470,7 @@ function createProxy<T>(
const [argumentList, transferables] = processArguments(rawArgumentList);
return requestResponseMessage(
ep,
namespace,
{
type: MessageType.CONSTRUCT,
path: path.map((p) => p.toString()),
Expand Down Expand Up @@ -526,13 +549,19 @@ function fromWireValue(value: WireValue): any {

function requestResponseMessage(
ep: Endpoint,
namespace: MessageNamespace,
msg: Message,
transfers?: Transferable[]
): Promise<WireValue> {
return new Promise((resolve) => {
const id = generateUUID();
ep.addEventListener("message", function l(ev: MessageEvent) {
if (!ev.data || !ev.data.id || ev.data.id !== id) {
if (
!ev.data ||
!ev.data.id ||
ev.data.id !== id ||
ev.data.namespace !== namespace
) {
return;
}
ep.removeEventListener("message", l as any);
Expand All @@ -541,7 +570,7 @@ function requestResponseMessage(
if (ep.start) {
ep.start();
}
ep.postMessage({ id, ...msg }, transfers);
ep.postMessage({ ...msg, id, namespace }, transfers);
});
}

Expand Down
13 changes: 11 additions & 2 deletions src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@ export const enum WireValueType {
}

export interface RawWireValue {
id?: string;
id?: MessageID;
namespace?: MessageNamespace;
type: WireValueType.RAW;
value: {};
}

export interface HandlerWireValue {
id?: string;
id?: MessageID;
namespace?: MessageNamespace;
type: WireValueType.HANDLER;
name: string;
value: unknown;
Expand All @@ -62,6 +64,7 @@ export interface HandlerWireValue {
export type WireValue = RawWireValue | HandlerWireValue;

export type MessageID = string;
export type MessageNamespace = string;

export const enum MessageType {
GET = "GET",
Expand All @@ -74,38 +77,44 @@ export const enum MessageType {

export interface GetMessage {
id?: MessageID;
namespace?: MessageNamespace;
type: MessageType.GET;
path: string[];
}

export interface SetMessage {
id?: MessageID;
namespace?: MessageNamespace;
type: MessageType.SET;
path: string[];
value: WireValue;
}

export interface ApplyMessage {
id?: MessageID;
namespace?: MessageNamespace;
type: MessageType.APPLY;
path: string[];
argumentList: WireValue[];
}

export interface ConstructMessage {
id?: MessageID;
namespace?: MessageNamespace;
type: MessageType.CONSTRUCT;
path: string[];
argumentList: WireValue[];
}

export interface EndpointMessage {
id?: MessageID;
namespace?: MessageNamespace;
type: MessageType.ENDPOINT;
}

export interface ReleaseMessage {
id?: MessageID;
namespace?: MessageNamespace;
type: MessageType.RELEASE;
path: string[];
}
Expand Down
Loading