Skip to content

Commit 25e3d36

Browse files
committed
Make transfer handlers type-safe
1 parent bf179bf commit 25e3d36

File tree

4 files changed

+135
-50
lines changed

4 files changed

+135
-50
lines changed

src/comlink.ts

Lines changed: 99 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export { Endpoint };
2525
export const proxyMarker = Symbol("Comlink.proxy");
2626
export const createEndpoint = Symbol("Comlink.endpoint");
2727
export const releaseProxy = Symbol("Comlink.releaseProxy");
28+
2829
const throwMarker = Symbol("Comlink.thrown");
2930

3031
/**
@@ -176,51 +177,108 @@ export type Local<T> =
176177
}
177178
: unknown);
178179

179-
export interface TransferHandler {
180-
canHandle(obj: any): boolean;
181-
serialize(obj: any): [any, Transferable[]];
182-
deserialize(obj: any): any;
180+
const isObject = (val: unknown): val is object =>
181+
(typeof val === "object" && val !== null) || typeof val === "function";
182+
183+
/**
184+
* Customizes the serialization of certain values as determined by `canHandle()`.
185+
*
186+
* @template T The input type being handled by this transfer handler.
187+
* @template S The serialized type sent over the wire.
188+
*/
189+
export interface TransferHandler<T, S> {
190+
/**
191+
* Gets called for every value to determine whether this transfer handler
192+
* should serialize the value, which includes checking that it is of the right
193+
* type (but can perform checks beyond that as well).
194+
*/
195+
canHandle(value: unknown): value is T;
196+
197+
/**
198+
* Gets called with the value if `canHandle()` returned `true` to produce a
199+
* value that can be sent in a message, consisting of structured-cloneable
200+
* values and/or transferrable objects.
201+
*/
202+
serialize(value: T): [S, Transferable[]];
203+
204+
/**
205+
* Gets called to deserialize an incoming value that was serialized in the
206+
* other thread with this transfer handler (known through the name it was
207+
* registered under).
208+
*/
209+
deserialize(value: S): T;
183210
}
184211

185-
export const transferHandlers = new Map<string, TransferHandler>([
186-
[
187-
"proxy",
188-
{
189-
canHandle: obj => obj && obj[proxyMarker],
190-
serialize(obj) {
191-
const { port1, port2 } = new MessageChannel();
192-
expose(obj, port1);
193-
return [port2, [port2]];
194-
},
195-
deserialize: (port: MessagePort) => {
196-
port.start();
197-
return wrap(port);
198-
}
199-
}
200-
],
201-
[
202-
"throw",
203-
{
204-
canHandle: obj => typeof obj === "object" && throwMarker in obj,
205-
serialize({ value }) {
206-
const isError = value instanceof Error;
207-
let serialized = { isError, value };
208-
if (isError) {
209-
serialized.value = {
210-
message: value.message,
211-
stack: value.stack
212-
};
213-
}
214-
return [serialized, []];
215-
},
216-
deserialize(serialized) {
217-
if (serialized.isError) {
218-
throw Object.assign(new Error(), serialized.value);
212+
/**
213+
* Internal transfer handle to handle objects marked to proxy.
214+
*/
215+
const proxyTransferHandler: TransferHandler<object, MessagePort> = {
216+
canHandle: (val): val is ProxyMarked =>
217+
isObject(val) && (val as ProxyMarked)[proxyMarker],
218+
serialize(obj) {
219+
const { port1, port2 } = new MessageChannel();
220+
expose(obj, port1);
221+
return [port2, [port2]];
222+
},
223+
deserialize(port) {
224+
port.start();
225+
return wrap(port);
226+
}
227+
};
228+
229+
interface ThrownValue {
230+
[throwMarker]: unknown; // just needs to be present
231+
value: unknown;
232+
}
233+
type SerializedThrownValue =
234+
| { isError: true; value: Error }
235+
| { isError: false; value: unknown };
236+
237+
/**
238+
* Internal transfer handler to handle thrown exceptions.
239+
*/
240+
const throwTransferHandler: TransferHandler<
241+
ThrownValue,
242+
SerializedThrownValue
243+
> = {
244+
canHandle: (value): value is ThrownValue =>
245+
isObject(value) && throwMarker in value,
246+
serialize({ value }) {
247+
let serialized: SerializedThrownValue;
248+
if (value instanceof Error) {
249+
serialized = {
250+
isError: true,
251+
value: {
252+
message: value.message,
253+
name: value.name,
254+
stack: value.stack
219255
}
220-
throw serialized.value;
221-
}
256+
};
257+
} else {
258+
serialized = { isError: false, value };
259+
}
260+
return [serialized, []];
261+
},
262+
deserialize(serialized) {
263+
if (serialized.isError) {
264+
throw Object.assign(
265+
new Error(serialized.value.message),
266+
serialized.value
267+
);
222268
}
223-
]
269+
throw serialized.value;
270+
}
271+
};
272+
273+
/**
274+
* Allows customizing the serialization of certain values.
275+
*/
276+
export const transferHandlers = new Map<
277+
string,
278+
TransferHandler<unknown, unknown>
279+
>([
280+
["proxy", proxyTransferHandler],
281+
["throw", throwTransferHandler]
224282
]);
225283

226284
export function expose(obj: any, ep: Endpoint = self as any) {

src/protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export interface HandlerWireValue {
5454
id?: string;
5555
type: WireValueType.HANDLER;
5656
name: string;
57-
value: {};
57+
value: unknown;
5858
}
5959

6060
export type WireValue = RawWireValue | HandlerWireValue;

tests/same_window.comlink.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,21 @@ describe("Comlink in the same realm", function() {
162162
}
163163
});
164164

165+
it("can rethrow null", async function() {
166+
const thing = Comlink.wrap(this.port1);
167+
Comlink.expose(_ => {
168+
throw null;
169+
}, this.port2);
170+
try {
171+
await thing();
172+
throw "Should have thrown";
173+
} catch (err) {
174+
expect(err).to.not.equal("Should have thrown");
175+
expect(err).to.equal(null);
176+
expect(typeof err).to.equal("object");
177+
}
178+
});
179+
165180
it("can work with parameterized functions", async function() {
166181
const thing = Comlink.wrap(this.port1);
167182
Comlink.expose((a, b) => a + b, this.port2);

tests/type-checks.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
import {
2-
assert,
3-
Has,
4-
NotHas,
5-
IsAny,
6-
IsExact,
7-
IsNever
8-
} from "conditional-type-checks";
1+
import { assert, Has, NotHas, IsAny, IsExact } from "conditional-type-checks";
92

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

@@ -355,4 +348,23 @@ async function closureSoICanUseAwait() {
355348
})
356349
);
357350
}
351+
352+
// Transfer handlers
353+
{
354+
const urlTransferHandler: Comlink.TransferHandler<URL, string> = {
355+
canHandle: (val): val is URL => {
356+
assert<IsExact<typeof val, unknown>>(true);
357+
return val instanceof URL;
358+
},
359+
serialize: url => {
360+
assert<IsExact<typeof url, URL>>(true);
361+
return [url.href, []];
362+
},
363+
deserialize: str => {
364+
assert<IsExact<typeof str, string>>(true);
365+
return new URL(str);
366+
}
367+
};
368+
Comlink.transferHandlers.set("URL", urlTransferHandler);
369+
}
358370
}

0 commit comments

Comments
 (0)