347 lines
12 KiB
JavaScript
347 lines
12 KiB
JavaScript
|
/**
|
|||
|
* @license
|
|||
|
* Copyright 2019 Google LLC
|
|||
|
* SPDX-License-Identifier: Apache-2.0
|
|||
|
*/
|
|||
|
const proxyMarker = Symbol("Comlink.proxy");
|
|||
|
const createEndpoint = Symbol("Comlink.endpoint");
|
|||
|
const releaseProxy = Symbol("Comlink.releaseProxy");
|
|||
|
const finalizer = Symbol("Comlink.finalizer");
|
|||
|
const throwMarker = Symbol("Comlink.thrown");
|
|||
|
const isObject = (val) => (typeof val === "object" && val !== null) || typeof val === "function";
|
|||
|
/**
|
|||
|
* Internal transfer handle to handle objects marked to proxy.
|
|||
|
*/
|
|||
|
const proxyTransferHandler = {
|
|||
|
canHandle: (val) => isObject(val) && val[proxyMarker],
|
|||
|
serialize(obj) {
|
|||
|
const { port1, port2 } = new MessageChannel();
|
|||
|
expose(obj, port1);
|
|||
|
return [port2, [port2]];
|
|||
|
},
|
|||
|
deserialize(port) {
|
|||
|
port.start();
|
|||
|
return wrap(port);
|
|||
|
},
|
|||
|
};
|
|||
|
/**
|
|||
|
* Internal transfer handler to handle thrown exceptions.
|
|||
|
*/
|
|||
|
const throwTransferHandler = {
|
|||
|
canHandle: (value) => isObject(value) && throwMarker in value,
|
|||
|
serialize({ value }) {
|
|||
|
let serialized;
|
|||
|
if (value instanceof Error) {
|
|||
|
serialized = {
|
|||
|
isError: true,
|
|||
|
value: {
|
|||
|
message: value.message,
|
|||
|
name: value.name,
|
|||
|
stack: value.stack,
|
|||
|
},
|
|||
|
};
|
|||
|
}
|
|||
|
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.
|
|||
|
*/
|
|||
|
const transferHandlers = new Map([
|
|||
|
["proxy", proxyTransferHandler],
|
|||
|
["throw", throwTransferHandler],
|
|||
|
]);
|
|||
|
function isAllowedOrigin(allowedOrigins, origin) {
|
|||
|
for (const allowedOrigin of allowedOrigins) {
|
|||
|
if (origin === allowedOrigin || allowedOrigin === "*") {
|
|||
|
return true;
|
|||
|
}
|
|||
|
if (allowedOrigin instanceof RegExp && allowedOrigin.test(origin)) {
|
|||
|
return true;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
function expose(obj, ep = globalThis, allowedOrigins = ["*"]) {
|
|||
|
ep.addEventListener("message", function callback(ev) {
|
|||
|
if (!ev || !ev.data) {
|
|||
|
return;
|
|||
|
}
|
|||
|
if (!isAllowedOrigin(allowedOrigins, ev.origin)) {
|
|||
|
console.warn(`Invalid origin '${ev.origin}' for comlink proxy`);
|
|||
|
return;
|
|||
|
}
|
|||
|
const { id, type, path } = Object.assign({ path: [] }, ev.data);
|
|||
|
const argumentList = (ev.data.argumentList || []).map(fromWireValue);
|
|||
|
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 "GET" /* MessageType.GET */:
|
|||
|
{
|
|||
|
returnValue = rawValue;
|
|||
|
}
|
|||
|
break;
|
|||
|
case "SET" /* MessageType.SET */:
|
|||
|
{
|
|||
|
parent[path.slice(-1)[0]] = fromWireValue(ev.data.value);
|
|||
|
returnValue = true;
|
|||
|
}
|
|||
|
break;
|
|||
|
case "APPLY" /* MessageType.APPLY */:
|
|||
|
{
|
|||
|
returnValue = rawValue.apply(parent, argumentList);
|
|||
|
}
|
|||
|
break;
|
|||
|
case "CONSTRUCT" /* MessageType.CONSTRUCT */:
|
|||
|
{
|
|||
|
const value = new rawValue(...argumentList);
|
|||
|
returnValue = proxy(value);
|
|||
|
}
|
|||
|
break;
|
|||
|
case "ENDPOINT" /* MessageType.ENDPOINT */:
|
|||
|
{
|
|||
|
const { port1, port2 } = new MessageChannel();
|
|||
|
expose(obj, port2);
|
|||
|
returnValue = transfer(port1, [port1]);
|
|||
|
}
|
|||
|
break;
|
|||
|
case "RELEASE" /* MessageType.RELEASE */:
|
|||
|
{
|
|||
|
returnValue = undefined;
|
|||
|
}
|
|||
|
break;
|
|||
|
default:
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
catch (value) {
|
|||
|
returnValue = { value, [throwMarker]: 0 };
|
|||
|
}
|
|||
|
Promise.resolve(returnValue)
|
|||
|
.catch((value) => {
|
|||
|
return { value, [throwMarker]: 0 };
|
|||
|
})
|
|||
|
.then((returnValue) => {
|
|||
|
const [wireValue, transferables] = toWireValue(returnValue);
|
|||
|
ep.postMessage(Object.assign(Object.assign({}, wireValue), { id }), transferables);
|
|||
|
if (type === "RELEASE" /* MessageType.RELEASE */) {
|
|||
|
// detach and deactive after sending release response above.
|
|||
|
ep.removeEventListener("message", callback);
|
|||
|
closeEndPoint(ep);
|
|||
|
if (finalizer in obj && typeof obj[finalizer] === "function") {
|
|||
|
obj[finalizer]();
|
|||
|
}
|
|||
|
}
|
|||
|
})
|
|||
|
.catch((error) => {
|
|||
|
// Send Serialization Error To Caller
|
|||
|
const [wireValue, transferables] = toWireValue({
|
|||
|
value: new TypeError("Unserializable return value"),
|
|||
|
[throwMarker]: 0,
|
|||
|
});
|
|||
|
ep.postMessage(Object.assign(Object.assign({}, wireValue), { id }), transferables);
|
|||
|
});
|
|||
|
});
|
|||
|
if (ep.start) {
|
|||
|
ep.start();
|
|||
|
}
|
|||
|
}
|
|||
|
function isMessagePort(endpoint) {
|
|||
|
return endpoint.constructor.name === "MessagePort";
|
|||
|
}
|
|||
|
function closeEndPoint(endpoint) {
|
|||
|
if (isMessagePort(endpoint))
|
|||
|
endpoint.close();
|
|||
|
}
|
|||
|
function wrap(ep, target) {
|
|||
|
return createProxy(ep, [], target);
|
|||
|
}
|
|||
|
function throwIfProxyReleased(isReleased) {
|
|||
|
if (isReleased) {
|
|||
|
throw new Error("Proxy has been released and is not useable");
|
|||
|
}
|
|||
|
}
|
|||
|
function releaseEndpoint(ep) {
|
|||
|
return requestResponseMessage(ep, {
|
|||
|
type: "RELEASE" /* MessageType.RELEASE */,
|
|||
|
}).then(() => {
|
|||
|
closeEndPoint(ep);
|
|||
|
});
|
|||
|
}
|
|||
|
const proxyCounter = new WeakMap();
|
|||
|
const proxyFinalizers = "FinalizationRegistry" in globalThis &&
|
|||
|
new FinalizationRegistry((ep) => {
|
|||
|
const newCount = (proxyCounter.get(ep) || 0) - 1;
|
|||
|
proxyCounter.set(ep, newCount);
|
|||
|
if (newCount === 0) {
|
|||
|
releaseEndpoint(ep);
|
|||
|
}
|
|||
|
});
|
|||
|
function registerProxy(proxy, ep) {
|
|||
|
const newCount = (proxyCounter.get(ep) || 0) + 1;
|
|||
|
proxyCounter.set(ep, newCount);
|
|||
|
if (proxyFinalizers) {
|
|||
|
proxyFinalizers.register(proxy, ep, proxy);
|
|||
|
}
|
|||
|
}
|
|||
|
function unregisterProxy(proxy) {
|
|||
|
if (proxyFinalizers) {
|
|||
|
proxyFinalizers.unregister(proxy);
|
|||
|
}
|
|||
|
}
|
|||
|
function createProxy(ep, path = [], target = function () { }) {
|
|||
|
let isProxyReleased = false;
|
|||
|
const proxy = new Proxy(target, {
|
|||
|
get(_target, prop) {
|
|||
|
throwIfProxyReleased(isProxyReleased);
|
|||
|
if (prop === releaseProxy) {
|
|||
|
return () => {
|
|||
|
unregisterProxy(proxy);
|
|||
|
releaseEndpoint(ep);
|
|||
|
isProxyReleased = true;
|
|||
|
};
|
|||
|
}
|
|||
|
if (prop === "then") {
|
|||
|
if (path.length === 0) {
|
|||
|
return { then: () => proxy };
|
|||
|
}
|
|||
|
const r = requestResponseMessage(ep, {
|
|||
|
type: "GET" /* MessageType.GET */,
|
|||
|
path: path.map((p) => p.toString()),
|
|||
|
}).then(fromWireValue);
|
|||
|
return r.then.bind(r);
|
|||
|
}
|
|||
|
return createProxy(ep, [...path, prop]);
|
|||
|
},
|
|||
|
set(_target, prop, rawValue) {
|
|||
|
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);
|
|||
|
return requestResponseMessage(ep, {
|
|||
|
type: "SET" /* MessageType.SET */,
|
|||
|
path: [...path, prop].map((p) => p.toString()),
|
|||
|
value,
|
|||
|
}, transferables).then(fromWireValue);
|
|||
|
},
|
|||
|
apply(_target, _thisArg, rawArgumentList) {
|
|||
|
throwIfProxyReleased(isProxyReleased);
|
|||
|
const last = path[path.length - 1];
|
|||
|
if (last === createEndpoint) {
|
|||
|
return requestResponseMessage(ep, {
|
|||
|
type: "ENDPOINT" /* MessageType.ENDPOINT */,
|
|||
|
}).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);
|
|||
|
return requestResponseMessage(ep, {
|
|||
|
type: "APPLY" /* MessageType.APPLY */,
|
|||
|
path: path.map((p) => p.toString()),
|
|||
|
argumentList,
|
|||
|
}, transferables).then(fromWireValue);
|
|||
|
},
|
|||
|
construct(_target, rawArgumentList) {
|
|||
|
throwIfProxyReleased(isProxyReleased);
|
|||
|
const [argumentList, transferables] = processArguments(rawArgumentList);
|
|||
|
return requestResponseMessage(ep, {
|
|||
|
type: "CONSTRUCT" /* MessageType.CONSTRUCT */,
|
|||
|
path: path.map((p) => p.toString()),
|
|||
|
argumentList,
|
|||
|
}, transferables).then(fromWireValue);
|
|||
|
},
|
|||
|
});
|
|||
|
registerProxy(proxy, ep);
|
|||
|
return proxy;
|
|||
|
}
|
|||
|
function myFlat(arr) {
|
|||
|
return Array.prototype.concat.apply([], arr);
|
|||
|
}
|
|||
|
function processArguments(argumentList) {
|
|||
|
const processed = argumentList.map(toWireValue);
|
|||
|
return [processed.map((v) => v[0]), myFlat(processed.map((v) => v[1]))];
|
|||
|
}
|
|||
|
const transferCache = new WeakMap();
|
|||
|
function transfer(obj, transfers) {
|
|||
|
transferCache.set(obj, transfers);
|
|||
|
return obj;
|
|||
|
}
|
|||
|
function proxy(obj) {
|
|||
|
return Object.assign(obj, { [proxyMarker]: true });
|
|||
|
}
|
|||
|
function windowEndpoint(w, context = globalThis, targetOrigin = "*") {
|
|||
|
return {
|
|||
|
postMessage: (msg, transferables) => w.postMessage(msg, targetOrigin, transferables),
|
|||
|
addEventListener: context.addEventListener.bind(context),
|
|||
|
removeEventListener: context.removeEventListener.bind(context),
|
|||
|
};
|
|||
|
}
|
|||
|
function toWireValue(value) {
|
|||
|
for (const [name, handler] of transferHandlers) {
|
|||
|
if (handler.canHandle(value)) {
|
|||
|
const [serializedValue, transferables] = handler.serialize(value);
|
|||
|
return [
|
|||
|
{
|
|||
|
type: "HANDLER" /* WireValueType.HANDLER */,
|
|||
|
name,
|
|||
|
value: serializedValue,
|
|||
|
},
|
|||
|
transferables,
|
|||
|
];
|
|||
|
}
|
|||
|
}
|
|||
|
return [
|
|||
|
{
|
|||
|
type: "RAW" /* WireValueType.RAW */,
|
|||
|
value,
|
|||
|
},
|
|||
|
transferCache.get(value) || [],
|
|||
|
];
|
|||
|
}
|
|||
|
function fromWireValue(value) {
|
|||
|
switch (value.type) {
|
|||
|
case "HANDLER" /* WireValueType.HANDLER */:
|
|||
|
return transferHandlers.get(value.name).deserialize(value.value);
|
|||
|
case "RAW" /* WireValueType.RAW */:
|
|||
|
return value.value;
|
|||
|
}
|
|||
|
}
|
|||
|
function requestResponseMessage(ep, msg, transfers) {
|
|||
|
return new Promise((resolve) => {
|
|||
|
const id = generateUUID();
|
|||
|
ep.addEventListener("message", function l(ev) {
|
|||
|
if (!ev.data || !ev.data.id || ev.data.id !== id) {
|
|||
|
return;
|
|||
|
}
|
|||
|
ep.removeEventListener("message", l);
|
|||
|
resolve(ev.data);
|
|||
|
});
|
|||
|
if (ep.start) {
|
|||
|
ep.start();
|
|||
|
}
|
|||
|
ep.postMessage(Object.assign({ id }, msg), transfers);
|
|||
|
});
|
|||
|
}
|
|||
|
function generateUUID() {
|
|||
|
return new Array(4)
|
|||
|
.fill(0)
|
|||
|
.map(() => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16))
|
|||
|
.join("-");
|
|||
|
}
|
|||
|
|
|||
|
export { createEndpoint, expose, finalizer, proxy, proxyMarker, releaseProxy, transfer, transferHandlers, windowEndpoint, wrap };
|
|||
|
//# sourceMappingURL=comlink.js.map
|