Skip to content

Commit

Permalink
Merge pull request GoogleChromeLabs#452 from felixfbecker/improve-tra…
Browse files Browse the repository at this point in the history
…nsfer-handler-types
  • Loading branch information
surma authored Apr 28, 2020
2 parents bf179bf + 25e3d36 commit fbe0c17
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 50 deletions.
140 changes: 99 additions & 41 deletions src/comlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { Endpoint };
export const proxyMarker = Symbol("Comlink.proxy");
export const createEndpoint = Symbol("Comlink.endpoint");
export const releaseProxy = Symbol("Comlink.releaseProxy");

const throwMarker = Symbol("Comlink.thrown");

/**
Expand Down Expand Up @@ -176,51 +177,108 @@ export type Local<T> =
}
: unknown);

export interface TransferHandler {
canHandle(obj: any): boolean;
serialize(obj: any): [any, Transferable[]];
deserialize(obj: any): any;
const isObject = (val: unknown): val is object =>
(typeof val === "object" && val !== null) || typeof val === "function";

/**
* Customizes the serialization of certain values as determined by `canHandle()`.
*
* @template T The input type being handled by this transfer handler.
* @template S The serialized type sent over the wire.
*/
export interface TransferHandler<T, S> {
/**
* Gets called for every value to determine whether this transfer handler
* should serialize the value, which includes checking that it is of the right
* type (but can perform checks beyond that as well).
*/
canHandle(value: unknown): value is T;

/**
* Gets called with the value if `canHandle()` returned `true` to produce a
* value that can be sent in a message, consisting of structured-cloneable
* values and/or transferrable objects.
*/
serialize(value: T): [S, Transferable[]];

/**
* Gets called to deserialize an incoming value that was serialized in the
* other thread with this transfer handler (known through the name it was
* registered under).
*/
deserialize(value: S): T;
}

export const transferHandlers = new Map<string, TransferHandler>([
[
"proxy",
{
canHandle: obj => obj && obj[proxyMarker],
serialize(obj) {
const { port1, port2 } = new MessageChannel();
expose(obj, port1);
return [port2, [port2]];
},
deserialize: (port: MessagePort) => {
port.start();
return wrap(port);
}
}
],
[
"throw",
{
canHandle: obj => typeof obj === "object" && throwMarker in obj,
serialize({ value }) {
const isError = value instanceof Error;
let serialized = { isError, value };
if (isError) {
serialized.value = {
message: value.message,
stack: value.stack
};
}
return [serialized, []];
},
deserialize(serialized) {
if (serialized.isError) {
throw Object.assign(new Error(), serialized.value);
/**
* Internal transfer handle to handle objects marked to proxy.
*/
const proxyTransferHandler: TransferHandler<object, MessagePort> = {
canHandle: (val): val is ProxyMarked =>
isObject(val) && (val as ProxyMarked)[proxyMarker],
serialize(obj) {
const { port1, port2 } = new MessageChannel();
expose(obj, port1);
return [port2, [port2]];
},
deserialize(port) {
port.start();
return wrap(port);
}
};

interface ThrownValue {
[throwMarker]: unknown; // just needs to be present
value: unknown;
}
type SerializedThrownValue =
| { isError: true; value: Error }
| { isError: false; value: unknown };

/**
* Internal transfer handler to handle thrown exceptions.
*/
const throwTransferHandler: TransferHandler<
ThrownValue,
SerializedThrownValue
> = {
canHandle: (value): value is ThrownValue =>
isObject(value) && throwMarker in value,
serialize({ value }) {
let serialized: SerializedThrownValue;
if (value instanceof Error) {
serialized = {
isError: true,
value: {
message: value.message,
name: value.name,
stack: value.stack
}
throw serialized.value;
}
};
} else {
serialized = { isError: false, value };
}
return [serialized, []];
},
deserialize(serialized) {
if (serialized.isError) {
throw Object.assign(
new Error(serialized.value.message),
serialized.value
);
}
]
throw serialized.value;
}
};

/**
* Allows customizing the serialization of certain values.
*/
export const transferHandlers = new Map<
string,
TransferHandler<unknown, unknown>
>([
["proxy", proxyTransferHandler],
["throw", throwTransferHandler]
]);

export function expose(obj: any, ep: Endpoint = self as any) {
Expand Down
2 changes: 1 addition & 1 deletion src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface HandlerWireValue {
id?: string;
type: WireValueType.HANDLER;
name: string;
value: {};
value: unknown;
}

export type WireValue = RawWireValue | HandlerWireValue;
Expand Down
15 changes: 15 additions & 0 deletions tests/same_window.comlink.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,21 @@ describe("Comlink in the same realm", function() {
}
});

it("can rethrow null", async function() {
const thing = Comlink.wrap(this.port1);
Comlink.expose(_ => {
throw null;
}, this.port2);
try {
await thing();
throw "Should have thrown";
} catch (err) {
expect(err).to.not.equal("Should have thrown");
expect(err).to.equal(null);
expect(typeof err).to.equal("object");
}
});

it("can work with parameterized functions", async function() {
const thing = Comlink.wrap(this.port1);
Comlink.expose((a, b) => a + b, this.port2);
Expand Down
28 changes: 20 additions & 8 deletions tests/type-checks.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import {
assert,
Has,
NotHas,
IsAny,
IsExact,
IsNever
} from "conditional-type-checks";
import { assert, Has, NotHas, IsAny, IsExact } from "conditional-type-checks";

import * as Comlink from "../src/comlink.js";

Expand Down Expand Up @@ -355,4 +348,23 @@ async function closureSoICanUseAwait() {
})
);
}

// Transfer handlers
{
const urlTransferHandler: Comlink.TransferHandler<URL, string> = {
canHandle: (val): val is URL => {
assert<IsExact<typeof val, unknown>>(true);
return val instanceof URL;
},
serialize: url => {
assert<IsExact<typeof url, URL>>(true);
return [url.href, []];
},
deserialize: str => {
assert<IsExact<typeof str, string>>(true);
return new URL(str);
}
};
Comlink.transferHandlers.set("URL", urlTransferHandler);
}
}

0 comments on commit fbe0c17

Please sign in to comment.