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

Maintain backwards compatibility with wire format from 4.3.0 #629

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ module.exports = function (config) {
pattern: "dist/**/*.@(mjs|js)",
included: false,
},
{
pattern: "node_modules/comlink/dist/esm/**/*.@(mjs|js)",
type: "module",
},
{
pattern: "tests/*.test.js",
type: "module",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@rollup/plugin-typescript": "11.0.0",
"chai": "^4.3.7",
"conditional-type-checks": "1.0.6",
"comlink": "4.3.0",
"husky": "8.0.3",
"karma": "6.4.1",
"karma-chai": "0.1.0",
Expand Down
146 changes: 117 additions & 29 deletions src/comlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import {
Endpoint,
EventSource,
Message,
V430MessageType,
MessageType,
PostMessageWithOrigin,
WireValue,
V430WireValueType,
WireValueType,
MessageTypeMap,
} from "./protocol";
export type { Endpoint };

Expand Down Expand Up @@ -199,8 +202,11 @@ export interface TransferHandler<T, S> {
* 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).
*
* @param value the serialized value to build a T from
* @param isV430 true if this was deserialized from a version of comlink older than 4.3.0
*/
deserialize(value: S): T;
deserialize(value: S, isV430: boolean): T;
}

/**
Expand All @@ -214,9 +220,9 @@ const proxyTransferHandler: TransferHandler<object, MessagePort> = {
expose(obj, port1);
return [port2, [port2]];
},
deserialize(port) {
deserialize(port, isV430) {
port.start();
return wrap(port);
return wrap(port, undefined, isV430);
},
};

Expand Down Expand Up @@ -305,43 +311,56 @@ export function expose(
}
const { id, type, path } = {
path: [] as string[],
...(ev.data as Message),
...(ev.data as Message | WireValue),
};

const isV430 = typeof type === "number";
const argumentList = (ev.data.argumentList || []).map(fromWireValue);

markV430Ports(...argumentList);

let returnValue;
try {
const parent = path.slice(0, -1).reduce((obj, prop) => obj[prop], obj);
const rawValue = path.reduce((obj, prop) => obj[prop], obj);
switch (type) {
case V430MessageType.GET:
case MessageType.GET:
{
returnValue = rawValue;
}
break;
case V430MessageType.SET:
case MessageType.SET:
{
parent[path.slice(-1)[0]] = fromWireValue(ev.data.value);
const value = fromWireValue(ev.data.value);
markV430Ports(value);
parent[path.slice(-1)[0]] = value;
returnValue = true;
}
break;
case V430MessageType.APPLY:
case MessageType.APPLY:
{
returnValue = rawValue.apply(parent, argumentList);
}
break;
case V430MessageType.CONSTRUCT:
case MessageType.CONSTRUCT:
{
const value = new rawValue(...argumentList);
returnValue = proxy(value);
}
break;
case V430MessageType.ENDPOINT:
case MessageType.ENDPOINT:
{
const { port1, port2 } = new MessageChannel();
expose(obj, port2);
returnValue = transfer(port1, [port1]);
}
break;
case V430MessageType.RELEASE:
case MessageType.RELEASE:
{
returnValue = undefined;
Expand All @@ -358,7 +377,7 @@ export function expose(
return { value, [throwMarker]: 0 };
})
.then((returnValue) => {
const [wireValue, transferables] = toWireValue(returnValue);
const [wireValue, transferables] = toWireValue(returnValue, isV430);
ep.postMessage({ ...wireValue, id }, transferables);
if (type === MessageType.RELEASE) {
// detach and deactive after sending release response above.
Expand All @@ -371,10 +390,13 @@ export function expose(
})
.catch((error) => {
// Send Serialization Error To Caller
const [wireValue, transferables] = toWireValue({
value: new TypeError("Unserializable return value"),
[throwMarker]: 0,
});
const [wireValue, transferables] = toWireValue(
{
value: new TypeError("Unserializable return value"),
[throwMarker]: 0,
},
isV430
);
ep.postMessage({ ...wireValue, id }, transferables);
});
} as any);
Expand All @@ -387,11 +409,34 @@ function isMessagePort(endpoint: Endpoint): endpoint is MessagePort {
return endpoint.constructor.name === "MessagePort";
}

function isEndpoint(endpoint: object): endpoint is Endpoint {
return (
"postMessage" in endpoint &&
"addEventListener" in endpoint &&
"removeEventListener" in endpoint
);
}

function markV430Ports(...args: any[]) {
for (const arg of args) {
if (isObject(arg) && isEndpoint(arg) && isMessagePort(arg)) {
v430Endpoints.add(arg);
}
}
}

function closeEndPoint(endpoint: Endpoint) {
if (isMessagePort(endpoint)) endpoint.close();
}

export function wrap<T>(ep: Endpoint, target?: any): Remote<T> {
export function wrap<T>(
ep: Endpoint,
target?: any,
isV430: boolean = false
): Remote<T> {
if (isV430) {
v430Endpoints.add(ep);
}
return createProxy<T>(ep, [], target) as any;
}

Expand All @@ -401,9 +446,9 @@ function throwIfProxyReleased(isReleased: boolean) {
}
}

function releaseEndpoint(ep: Endpoint) {
function releaseEndpoint(ep: Endpoint, isV430: boolean) {
return requestResponseMessage(ep, {
type: MessageType.RELEASE,
type: msgType(MessageType.RELEASE, isV430),
}).then(() => {
closeEndPoint(ep);
});
Expand All @@ -420,14 +465,16 @@ interface FinalizationRegistry<T> {
}
declare var FinalizationRegistry: FinalizationRegistry<Endpoint>;

const v430Endpoints = new WeakSet<Endpoint>();
const proxyCounter = new WeakMap<Endpoint, number>();
const proxyFinalizers =
"FinalizationRegistry" in globalThis &&
new FinalizationRegistry((ep: Endpoint) => {
const newCount = (proxyCounter.get(ep) || 0) - 1;
proxyCounter.set(ep, newCount);
if (newCount === 0) {
releaseEndpoint(ep);
releaseEndpoint(ep, v430Endpoints.has(ep));
v430Endpoints.delete(ep);
}
});

Expand All @@ -451,13 +498,14 @@ function createProxy<T>(
target: object = function () {}
): Remote<T> {
let isProxyReleased = false;
const isV430 = v430Endpoints.has(ep);
const proxy = new Proxy(target, {
get(_target, prop) {
throwIfProxyReleased(isProxyReleased);
if (prop === releaseProxy) {
return () => {
unregisterProxy(proxy);
releaseEndpoint(ep);
releaseEndpoint(ep, isV430);
isProxyReleased = true;
};
}
Expand All @@ -466,7 +514,7 @@ function createProxy<T>(
return { then: () => proxy };
}
const r = requestResponseMessage(ep, {
type: MessageType.GET,
type: msgType(MessageType.GET, isV430),
path: path.map((p) => p.toString()),
}).then(fromWireValue);
return r.then.bind(r);
Expand All @@ -477,11 +525,11 @@ function createProxy<T>(
throwIfProxyReleased(isProxyReleased);
// FIXME: ES6 Proxy Handler `set` methods are supposed to return a
// boolean. To show good will, we return true asynchronously ¯\_(ツ)_/¯
const [value, transferables] = toWireValue(rawValue);
const [value, transferables] = toWireValue(rawValue, isV430);
return requestResponseMessage(
ep,
{
type: MessageType.SET,
type: msgType(MessageType.SET, isV430),
path: [...path, prop].map((p) => p.toString()),
value,
},
Expand All @@ -493,18 +541,21 @@ function createProxy<T>(
const last = path[path.length - 1];
if ((last as any) === createEndpoint) {
return requestResponseMessage(ep, {
type: MessageType.ENDPOINT,
type: msgType(MessageType.ENDPOINT, isV430),
}).then(fromWireValue);
}
// We just pretend that `bind()` didn’t happen.
if (last === "bind") {
return createProxy(ep, path.slice(0, -1));
}
const [argumentList, transferables] = processArguments(rawArgumentList);
const [argumentList, transferables] = processArguments(
rawArgumentList,
isV430
);
return requestResponseMessage(
ep,
{
type: MessageType.APPLY,
type: msgType(MessageType.APPLY, isV430),
path: path.map((p) => p.toString()),
argumentList,
},
Expand All @@ -513,11 +564,14 @@ function createProxy<T>(
},
construct(_target, rawArgumentList) {
throwIfProxyReleased(isProxyReleased);
const [argumentList, transferables] = processArguments(rawArgumentList);
const [argumentList, transferables] = processArguments(
rawArgumentList,
isV430
);
return requestResponseMessage(
ep,
{
type: MessageType.CONSTRUCT,
type: msgType(MessageType.CONSTRUCT, isV430),
path: path.map((p) => p.toString()),
argumentList,
},
Expand All @@ -533,8 +587,11 @@ function myFlat<T>(arr: (T | T[])[]): T[] {
return Array.prototype.concat.apply([], arr);
}

function processArguments(argumentList: any[]): [WireValue[], Transferable[]] {
const processed = argumentList.map(toWireValue);
function processArguments(
argumentList: any[],
isV430: boolean
): [WireValue[], Transferable[]] {
const processed = argumentList.map((arg) => toWireValue(arg, isV430));
return [processed.map((v) => v[0]), myFlat(processed.map((v) => v[1]))];
}

Expand All @@ -561,13 +618,13 @@ export function windowEndpoint(
};
}

function toWireValue(value: any): [WireValue, Transferable[]] {
function toWireValue(value: any, isV430: boolean): [WireValue, Transferable[]] {
for (const [name, handler] of transferHandlers) {
if (handler.canHandle(value)) {
const [serializedValue, transferables] = handler.serialize(value);
return [
{
type: WireValueType.HANDLER,
type: isV430 ? V430WireValueType.HANDLER : WireValueType.HANDLER,
name,
value: serializedValue,
},
Expand All @@ -577,7 +634,7 @@ function toWireValue(value: any): [WireValue, Transferable[]] {
}
return [
{
type: WireValueType.RAW,
type: isV430 ? V430WireValueType.RAW : WireValueType.RAW,
value,
},
transferCache.get(value) || [],
Expand All @@ -586,8 +643,11 @@ function toWireValue(value: any): [WireValue, Transferable[]] {

function fromWireValue(value: WireValue): any {
switch (value.type) {
case V430WireValueType.HANDLER:
case WireValueType.HANDLER:
return transferHandlers.get(value.name)!.deserialize(value.value);
const isV430 = value.type === V430WireValueType.HANDLER;
return transferHandlers.get(value.name)!.deserialize(value.value, isV430);
case V430WireValueType.RAW:
case WireValueType.RAW:
return value.value;
}
Expand All @@ -614,6 +674,34 @@ function requestResponseMessage(
});
}

function msgType<T extends MessageType>(
type: T,
isV430: boolean
): MessageTypeMap[T] | T {
if (!isV430) {
return type;
}

function mapMessageTypeToV430Type(type: MessageType): V430MessageType {
switch (type) {
case MessageType.GET:
return V430MessageType.GET;
case MessageType.SET:
return V430MessageType.SET;
case MessageType.APPLY:
return V430MessageType.APPLY;
case MessageType.CONSTRUCT:
return V430MessageType.CONSTRUCT;
case MessageType.ENDPOINT:
return V430MessageType.ENDPOINT;
case MessageType.RELEASE:
return V430MessageType.RELEASE;
}
}

return mapMessageTypeToV430Type(type) as MessageTypeMap[T];
}

function generateUUID(): string {
return new Array(4)
.fill(0)
Expand Down
Loading