@remote-ui/rpc
Advanced tools
Comparing version
@@ -98,3 +98,3 @@ "use strict"; | ||
function _createForOfIteratorHelper(o) { if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (o = _unsupportedIterableToArray(o))) { var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var it, normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } | ||
function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } | ||
@@ -161,3 +161,3 @@ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } | ||
_listener = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(event) { | ||
var _i2, _arr2, _listener2, data, stackFrame, _data2, id, target, args, func, result, _toWire7, _toWire8, serializedResult, transferables, name, message, stack; | ||
var _i3, _arr2, _listener2, data, stackFrame, _data2, id, target, args, func, result, _toWire7, _toWire8, serializedResult, transferables, name, message, stack; | ||
@@ -168,4 +168,4 @@ return regeneratorRuntime.wrap(function _callee$(_context) { | ||
case 0: | ||
for (_i2 = 0, _arr2 = _toConsumableArray(eventListeners); _i2 < _arr2.length; _i2++) { | ||
_listener2 = _arr2[_i2]; | ||
for (_i3 = 0, _arr2 = _toConsumableArray(eventListeners); _i3 < _arr2.length; _i3++) { | ||
_listener2 = _arr2[_i3]; | ||
@@ -312,3 +312,3 @@ _listener2(event); | ||
callable: function (_callable) { | ||
function callable(_x2) { | ||
function callable() { | ||
return _callable.apply(this, arguments); | ||
@@ -322,3 +322,3 @@ } | ||
return callable; | ||
}(function (newCallable) { | ||
}(function () { | ||
// If no callable methods are supplied initially, we use a Proxy instead, | ||
@@ -328,19 +328,14 @@ // so all methods end up being treated as callable by default. | ||
var _iterator2 = _createForOfIteratorHelper(newCallable), | ||
_step2; | ||
for (var _len = arguments.length, newCallable = new Array(_len), _key2 = 0; _key2 < _len; _key2++) { | ||
newCallable[_key2] = arguments[_key2]; | ||
} | ||
try { | ||
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { | ||
var _method = _step2.value; | ||
Object.defineProperty(call, _method, { | ||
value: handlerForCall(_method), | ||
writable: false, | ||
configurable: true, | ||
enumerable: true | ||
}); | ||
} | ||
} catch (err) { | ||
_iterator2.e(err); | ||
} finally { | ||
_iterator2.f(); | ||
for (var _i2 = 0, _newCallable = newCallable; _i2 < _newCallable.length; _i2++) { | ||
var _method = _newCallable[_i2]; | ||
Object.defineProperty(call, _method, { | ||
value: handlerForCall(_method), | ||
writable: false, | ||
configurable: true, | ||
enumerable: true | ||
}); | ||
} | ||
@@ -400,4 +395,4 @@ }), | ||
for (var _len = arguments.length, args = new Array(_len), _key2 = 0; _key2 < _len; _key2++) { | ||
args[_key2] = arguments[_key2]; | ||
for (var _len2 = arguments.length, args = new Array(_len2), _key3 = 0; _key3 < _len2; _key3++) { | ||
args[_key3] = arguments[_key3]; | ||
} | ||
@@ -446,3 +441,3 @@ | ||
transferables.push.apply(transferables, _toConsumableArray(nestedTransferables)); | ||
return _objectSpread({}, object, _defineProperty({}, key, result)); | ||
return _objectSpread(_objectSpread({}, object), {}, _defineProperty({}, key, result)); | ||
}, {}); | ||
@@ -483,3 +478,3 @@ return [result, transferables]; | ||
return Object.keys(value).reduce(function (object, key) { | ||
return _objectSpread({}, object, _defineProperty({}, key, fromWire(value[key], retainedBy))); | ||
return _objectSpread(_objectSpread({}, object), {}, _defineProperty({}, key, fromWire(value[key], retainedBy))); | ||
}, {}); | ||
@@ -486,0 +481,0 @@ } |
@@ -67,3 +67,3 @@ "use strict"; | ||
function _createForOfIteratorHelper(o) { if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (o = _unsupportedIterableToArray(o))) { var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var it, normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } | ||
function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } | ||
@@ -70,0 +70,0 @@ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } |
@@ -56,3 +56,3 @@ "use strict"; | ||
function _createForOfIteratorHelper(o) { if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (o = _unsupportedIterableToArray(o))) { var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var it, normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } | ||
function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } | ||
@@ -59,0 +59,0 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } |
@@ -62,3 +62,3 @@ "use strict"; | ||
function _createForOfIteratorHelper(o) { if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (o = _unsupportedIterableToArray(o))) { var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var it, normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } | ||
function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } | ||
@@ -65,0 +65,0 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } |
@@ -164,3 +164,3 @@ "use strict"; | ||
callable(newCallable) { | ||
callable(...newCallable) { | ||
// If no callable methods are supplied initially, we use a Proxy instead, | ||
@@ -167,0 +167,0 @@ // so all methods end up being treated as callable by default. |
@@ -14,3 +14,3 @@ import { MessageEndpoint, RemoteCallable, FunctionStrategy, FunctionStrategyOptions } from './types'; | ||
}): void; | ||
callable(methods: string[]): void; | ||
callable(...methods: string[]): void; | ||
terminate(): void; | ||
@@ -17,0 +17,0 @@ } |
@@ -123,3 +123,3 @@ "use strict"; | ||
}, | ||
callable(newCallable) { | ||
callable(...newCallable) { | ||
// If no callable methods are supplied initially, we use a Proxy instead, | ||
@@ -126,0 +126,0 @@ // so all methods end up being treated as callable by default. |
@@ -10,14 +10,9 @@ export interface MessageEndpoint { | ||
}; | ||
declare type RemoteCallableField<T> = T extends (...args: infer Args) => infer TypeReturned ? (...args: Args) => Promise<ForcePromiseWrapped<TypeReturned>> : never; | ||
declare type ForcePromiseWrapped<T> = T extends infer U | Promise<infer U> ? ForcePromise<U> : ForcePromise<T>; | ||
declare type ForcePromise<T> = T extends Promise<any> ? T : T extends (...args: infer Args) => infer TypeReturned ? (...args: Args) => Promise<ForcePromiseWrapped<TypeReturned>> : T extends (infer ArrayElement)[] ? ForcePromiseArray<ArrayElement> : T extends object ? { | ||
[K in keyof T]: ForcePromiseWrapped<T[K]>; | ||
declare type RemoteCallableField<T> = T extends (...args: infer Args) => infer TypeReturned ? (...args: Args) => AlwaysAsync<TypeReturned> : never; | ||
declare type AlwaysAsync<T> = T extends Promise<any> ? T : T extends infer U | Promise<infer U> ? Promise<U> : T extends (...args: infer Args) => infer TypeReturned ? (...args: Args) => AlwaysAsync<TypeReturned> : T extends (infer ArrayElement)[] ? AlwaysAsync<ArrayElement>[] : T extends readonly (infer ArrayElement)[] ? readonly AlwaysAsync<ArrayElement>[] : T extends object ? { | ||
[K in keyof T]: AlwaysAsync<T[K]>; | ||
} : T; | ||
interface ForcePromiseArray<T> extends Array<ForcePromiseWrapped<T>> { | ||
} | ||
export declare type SafeRpcArgument<T> = T extends (...args: infer Args) => infer TypeReturned ? TypeReturned extends Promise<any> ? (...args: Args) => TypeReturned : (...args: Args) => TypeReturned | Promise<TypeReturned> : T extends (infer ArrayElement)[] ? SafeRpcArgumentArray<ArrayElement> : T extends object ? { | ||
export declare type SafeRpcArgument<T> = T extends (...args: infer Args) => infer TypeReturned ? TypeReturned extends Promise<any> ? (...args: Args) => TypeReturned : (...args: Args) => TypeReturned | Promise<TypeReturned> : T extends (infer ArrayElement)[] ? SafeRpcArgument<ArrayElement>[] : T extends readonly (infer ArrayElement)[] ? readonly SafeRpcArgument<ArrayElement>[] : T extends object ? { | ||
[K in keyof T]: SafeRpcArgument<T[K]>; | ||
} : T; | ||
interface SafeRpcArgumentArray<T> extends Array<SafeRpcArgument<T>> { | ||
} | ||
export declare const RETAIN_METHOD: unique symbol; | ||
@@ -24,0 +19,0 @@ export declare const RELEASE_METHOD: unique symbol; |
{ | ||
"name": "@remote-ui/rpc", | ||
"version": "0.0.11-alpha.0", | ||
"description": "An RPC library with strong support for simulating the transfer of functions via postMessage", | ||
"version": "0.0.11", | ||
"publishConfig": { | ||
@@ -8,2 +9,3 @@ "access": "public" | ||
"license": "MIT", | ||
"sideEffects": false, | ||
"devDependencies": { | ||
@@ -13,3 +15,3 @@ "@types/uuid": "^8.0.0", | ||
}, | ||
"gitHead": "b5226d46807aa50774447d3d9d710739b377f5b1" | ||
"gitHead": "38ab5544914dc588ea275bcaa54ed9f2d9de61d8" | ||
} |
@@ -1,32 +0,21 @@ | ||
import type {MessageEndpoint, RemoteCallable} from './types'; | ||
import { | ||
StackFrame, | ||
isMemoryManageable, | ||
RETAINED_BY, | ||
RETAIN_METHOD, | ||
RELEASE_METHOD, | ||
} from './memory'; | ||
import type {Retainer} from './memory'; | ||
MessageEndpoint, | ||
RemoteCallable, | ||
FunctionStrategy, | ||
FunctionStrategyOptions, | ||
} from './types'; | ||
import {Retainer, StackFrame} from './memory'; | ||
import {createMessengerFunctionStrategy} from './strategies'; | ||
type Encoded = [ArrayBuffer, ArrayBuffer]; | ||
const CALL = 0; | ||
const APPLY = 0; | ||
const RESULT = 1; | ||
const TERMINATE = 2; | ||
const RELEASE = 3; | ||
const FUNCTION_APPLY = 5; | ||
const FUNCTION_RESULT = 6; | ||
interface MessageMap { | ||
[CALL]: [string, string, any]; | ||
[RESULT]: [string, Error?, any?]; | ||
[TERMINATE]: []; | ||
[RELEASE]: [string]; | ||
[FUNCTION_APPLY]: [string, string, any]; | ||
[FUNCTION_RESULT]: [string, Error?, any?]; | ||
} | ||
const FUNCTION = '_@f'; | ||
interface Options<T = unknown> { | ||
uuid?(): string; | ||
createFunctionStrategy?( | ||
options: FunctionStrategyOptions, | ||
): FunctionStrategy<unknown>; | ||
callable?: (keyof T)[]; | ||
@@ -37,5 +26,6 @@ } | ||
readonly call: RemoteCallable<T>; | ||
readonly functions: FunctionStrategy<unknown>; | ||
replace(messenger: MessageEndpoint): void; | ||
expose(api: {[key: string]: Function | undefined}): void; | ||
callable(methods: string[]): void; | ||
callable(...methods: string[]): void; | ||
terminate(): void; | ||
@@ -46,19 +36,87 @@ } | ||
initialMessenger: MessageEndpoint, | ||
{uuid = defaultUuid, callable}: Options<T> = {}, | ||
{ | ||
uuid = defaultUuid, | ||
createFunctionStrategy = createMessengerFunctionStrategy, | ||
callable, | ||
}: Options<T> = {}, | ||
): Endpoint<T> { | ||
let terminated = false; | ||
let messenger = initialMessenger; | ||
const eventListeners = new Set<Function>(); | ||
const functions = createFunctionStrategy({ | ||
uuid, | ||
toWire, | ||
fromWire, | ||
messenger: { | ||
postMessage: (...args) => messenger.postMessage(...args), | ||
addEventListener: (_, listener) => { | ||
eventListeners.add(listener); | ||
}, | ||
removeEventListener: (_, listener) => { | ||
eventListeners.delete(listener); | ||
}, | ||
}, | ||
}); | ||
const activeApi = new Map<string, Function>(); | ||
const functionsToId = new Map<Function, string>(); | ||
const idsToFunction = new Map<string, Function>(); | ||
const idsToProxy = new Map<string, Function>(); | ||
const callIdsToResolver = new Map< | ||
string, | ||
( | ||
...args: MessageMap[typeof FUNCTION_RESULT] | MessageMap[typeof RESULT] | ||
) => void | ||
>(); | ||
function terminate() { | ||
terminated = true; | ||
activeApi.clear(); | ||
messenger.removeEventListener('message', listener); | ||
if (functions.terminate != null) { | ||
functions.terminate(); | ||
} | ||
} | ||
async function listener(event: MessageEvent) { | ||
for (const listener of [...eventListeners]) { | ||
listener(event); | ||
} | ||
const {data} = event; | ||
if (data == null) { | ||
return; | ||
} | ||
switch (data[0]) { | ||
case TERMINATE: { | ||
terminate(); | ||
break; | ||
} | ||
case APPLY: { | ||
const stackFrame = new StackFrame(); | ||
const [, id, target, args] = data; | ||
const func = activeApi.get(target); | ||
try { | ||
if (func == null) { | ||
throw new Error( | ||
`No '${target}' method is exposed on this endpoint`, | ||
); | ||
} | ||
const result = await func(...(fromWire(args, [stackFrame]) as any[])); | ||
const [serializedResult, transferables] = toWire(result); | ||
messenger.postMessage( | ||
[RESULT, id, undefined, serializedResult], | ||
transferables, | ||
); | ||
} catch (error) { | ||
const {name, message, stack} = error; | ||
messenger.postMessage([RESULT, id, {name, message, stack}]); | ||
} finally { | ||
stackFrame.release(); | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
messenger.addEventListener('message', listener); | ||
let call: any; | ||
@@ -104,6 +162,5 @@ | ||
messenger.addEventListener('message', listener); | ||
return { | ||
call, | ||
functions, | ||
replace(newMessenger) { | ||
@@ -127,3 +184,3 @@ const oldMessenger = messenger; | ||
}, | ||
callable(newCallable) { | ||
callable(...newCallable) { | ||
// If no callable methods are supplied initially, we use a Proxy instead, | ||
@@ -148,3 +205,3 @@ // so all methods end up being treated as callable by default. | ||
} else { | ||
send(TERMINATE, []); | ||
messenger.postMessage([TERMINATE]); | ||
} | ||
@@ -154,239 +211,2 @@ }, | ||
function send<Type extends keyof MessageMap>( | ||
type: Type, | ||
args: MessageMap[Type], | ||
transferables?: Transferable[], | ||
) { | ||
messenger.postMessage([type, args], transferables); | ||
} | ||
function encode(value: unknown): [any, Transferable[]?] { | ||
if (typeof value !== 'object') { | ||
return [value]; | ||
} | ||
const strings: string[] = []; | ||
const stringsToIndex = new Map<string, number>(); | ||
const encodedValue = encodeWithEnvironment(value, { | ||
store(value) { | ||
if (typeof value === 'function') { | ||
const currentId = functionsToId.get(value); | ||
if (currentId != null) return storeString(currentId); | ||
const id = uuid(); | ||
functionsToId.set(value, id); | ||
idsToFunction.set(id, value); | ||
return storeString(id); | ||
} else { | ||
return storeString(value); | ||
} | ||
}, | ||
}); | ||
const encodedStrings = encodeWithEnvironment(strings); | ||
const encoded = [encodedValue, encodedStrings]; | ||
return [encoded, encoded]; | ||
function storeString(value: string) { | ||
const currentIndex = stringsToIndex.get(value); | ||
if (currentIndex != null) return currentIndex; | ||
const index = strings.length; | ||
strings.push(value); | ||
stringsToIndex.set(value, index); | ||
return index; | ||
} | ||
} | ||
function decode(encoded: unknown, retainedBy?: Iterable<Retainer>) { | ||
if ( | ||
!Array.isArray(encoded) || | ||
!(encoded[0] instanceof ArrayBuffer) || | ||
!(encoded[1] instanceof ArrayBuffer) | ||
) { | ||
return encoded; | ||
} | ||
const [encodedData, encodedStrings] = encoded as Encoded; | ||
const strings = decodeWithEnvironment(encodedStrings) as string[]; | ||
const data = decodeWithEnvironment(encodedData, { | ||
getString: (index) => strings[index], | ||
getFunction(idIndex) { | ||
const id = strings[idIndex]; | ||
if (idsToProxy.has(id)) { | ||
return idsToProxy.get(id)! as any; | ||
} | ||
let retainCount = 0; | ||
let released = false; | ||
const release = () => { | ||
retainCount -= 1; | ||
if (retainCount === 0) { | ||
released = true; | ||
idsToProxy.delete(id); | ||
send(RELEASE, [id]); | ||
} | ||
}; | ||
const retain = () => { | ||
retainCount += 1; | ||
}; | ||
const retainers = new Set(retainedBy); | ||
const proxy = (...args: any[]) => { | ||
if (released) { | ||
throw new Error( | ||
'You attempted to call a function that was already released.', | ||
); | ||
} | ||
if (!idsToProxy.has(id)) { | ||
throw new Error( | ||
'You attempted to call a function that was already revoked.', | ||
); | ||
} | ||
const callId = uuid(); | ||
const done = waitForResult(callId, retainedBy); | ||
const [encoded, transferables] = encode(args); | ||
send(FUNCTION_APPLY, [callId, id, encoded], transferables); | ||
return done; | ||
}; | ||
Object.defineProperties(proxy, { | ||
[RELEASE_METHOD]: {value: release, writable: false}, | ||
[RETAIN_METHOD]: {value: retain, writable: false}, | ||
[RETAINED_BY]: {value: retainers, writable: false}, | ||
}); | ||
for (const retainer of retainers) { | ||
retainer.add(proxy as any); | ||
} | ||
idsToProxy.set(id, proxy); | ||
return proxy as any; | ||
}, | ||
}); | ||
return data; | ||
} | ||
async function listener(event: MessageEvent) { | ||
const {data} = event; | ||
if (data == null || !Array.isArray(data)) { | ||
return; | ||
} | ||
switch (data[0]) { | ||
case TERMINATE: { | ||
terminate(); | ||
break; | ||
} | ||
case CALL: { | ||
const stackFrame = new StackFrame(); | ||
const [id, property, args] = data[1] as MessageMap[typeof CALL]; | ||
const func = activeApi.get(property); | ||
try { | ||
if (func == null) { | ||
throw new Error( | ||
`No '${property}' method is exposed on this endpoint`, | ||
); | ||
} | ||
const [encoded, transferables] = encode( | ||
await func(...(decode(args, [stackFrame]) as any[])), | ||
); | ||
send(RESULT, [id, undefined, encoded], transferables); | ||
} catch (error) { | ||
const {name, message, stack} = error; | ||
send(RESULT, [id, {name, message, stack}]); | ||
} finally { | ||
stackFrame.release(); | ||
} | ||
break; | ||
} | ||
case RESULT: { | ||
const [callId] = data[1] as MessageMap[typeof RESULT]; | ||
callIdsToResolver.get(callId)!( | ||
...(data[1] as MessageMap[typeof RESULT]), | ||
); | ||
callIdsToResolver.delete(callId); | ||
break; | ||
} | ||
case RELEASE: { | ||
const [id] = data[1] as MessageMap[typeof RELEASE]; | ||
const func = idsToFunction.get(id); | ||
if (func) { | ||
idsToFunction.delete(id); | ||
functionsToId.delete(func); | ||
} | ||
break; | ||
} | ||
case FUNCTION_RESULT: { | ||
const [callId] = data[1] as MessageMap[typeof FUNCTION_RESULT]; | ||
callIdsToResolver.get(callId)!( | ||
...(data[1] as MessageMap[typeof FUNCTION_RESULT]), | ||
); | ||
callIdsToResolver.delete(callId); | ||
break; | ||
} | ||
case FUNCTION_APPLY: { | ||
const stackFrame = new StackFrame(); | ||
const [ | ||
callId, | ||
funcId, | ||
args, | ||
] = data[1] as MessageMap[typeof FUNCTION_APPLY]; | ||
const func = idsToFunction.get(funcId); | ||
if (func == null) { | ||
const {name, message, stack} = new Error( | ||
'You attempted to call a function that was already released.', | ||
); | ||
send(FUNCTION_RESULT, [callId, {name, message, stack}]); | ||
return; | ||
} | ||
try { | ||
const retainedBy = isMemoryManageable(func) | ||
? [stackFrame, ...func[RETAINED_BY]] | ||
: [stackFrame]; | ||
const [encoded, transferables] = encode( | ||
await func(...(decode(args, retainedBy) as any[])), | ||
); | ||
send(FUNCTION_RESULT, [callId, undefined, encoded], transferables); | ||
} catch (error) { | ||
const {name, message, stack} = error; | ||
send(FUNCTION_RESULT, [callId, {name, message, stack}]); | ||
} finally { | ||
stackFrame.release(); | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
function handlerForCall(property: string | number | symbol) { | ||
@@ -401,496 +221,103 @@ return (...args: any[]) => { | ||
const id = uuid(); | ||
const done = waitForResult(id); | ||
const [encoded, transferables] = encode(args); | ||
const done = new Promise<any>((resolve, reject) => { | ||
messenger.addEventListener('message', function listener({data}) { | ||
if (data == null || data[0] !== RESULT || data[1] !== id) { | ||
return; | ||
} | ||
send(CALL, [id, property as string, encoded], transferables); | ||
messenger.removeEventListener('message', listener); | ||
return done; | ||
}; | ||
} | ||
const [, , errorResult, value] = data; | ||
function waitForResult(id: string, retainedBy?: Iterable<Retainer>) { | ||
return new Promise<any>((resolve, reject) => { | ||
callIdsToResolver.set(id, (_, errorResult, value) => { | ||
if (errorResult == null) { | ||
resolve(value && decode(value, retainedBy)); | ||
} else { | ||
const error = new Error(); | ||
Object.assign(error, errorResult); | ||
reject(error); | ||
} | ||
if (errorResult == null) { | ||
resolve(fromWire(value)); | ||
} else { | ||
const error = new Error(); | ||
Object.assign(error, errorResult); | ||
reject(error); | ||
} | ||
}); | ||
}); | ||
}); | ||
} | ||
function terminate() { | ||
terminated = true; | ||
activeApi.clear(); | ||
functionsToId.clear(); | ||
idsToFunction.clear(); | ||
idsToProxy.clear(); | ||
callIdsToResolver.clear(); | ||
messenger.removeEventListener('message', listener); | ||
} | ||
} | ||
const [serializedArgs, transferables] = toWire(args); | ||
messenger.postMessage( | ||
[APPLY, id, property, serializedArgs], | ||
transferables, | ||
); | ||
function defaultUuid() { | ||
return `${uuidSegment()}-${uuidSegment()}-${uuidSegment()}-${uuidSegment()}`; | ||
} | ||
function uuidSegment() { | ||
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16); | ||
} | ||
const POW_2_24 = 5.960464477539063e-8; | ||
const POW_2_32 = 4294967296; | ||
const POW_2_53 = 9007199254740992; | ||
interface EncodeEnvironment { | ||
store(value: string | Function): number; | ||
} | ||
function encodeWithEnvironment(value: unknown, env?: EncodeEnvironment) { | ||
let data = new ArrayBuffer(256); | ||
let dataView = new DataView(data); | ||
let lastLength: number; | ||
let offset = 0; | ||
function prepareWrite(length: number) { | ||
let newByteLength = data.byteLength; | ||
const requiredLength = offset + length; | ||
while (newByteLength < requiredLength) newByteLength <<= 1; | ||
if (newByteLength !== data.byteLength) { | ||
const oldDataView = dataView; | ||
data = new ArrayBuffer(newByteLength); | ||
dataView = new DataView(data); | ||
const uint32count = (offset + 3) >> 2; | ||
for (let i = 0; i < uint32count; ++i) | ||
dataView.setUint32(i << 2, oldDataView.getUint32(i << 2)); | ||
} | ||
lastLength = length; | ||
return dataView; | ||
return done; | ||
}; | ||
} | ||
function commitWrite() { | ||
offset += lastLength; | ||
} | ||
function toWire(value: unknown): [any, Transferable[]?] { | ||
if (typeof value === 'object') { | ||
if (value == null) { | ||
return [value]; | ||
} | ||
function writeFloat64(value: number) { | ||
prepareWrite(8).setFloat64(offset, value); | ||
commitWrite(); | ||
} | ||
const transferables: Transferable[] = []; | ||
function writeUint8(value: number) { | ||
prepareWrite(1).setUint8(offset, value); | ||
commitWrite(); | ||
} | ||
if (Array.isArray(value)) { | ||
const result = value.map((item) => { | ||
const [result, nestedTransferables = []] = toWire(item); | ||
transferables.push(...nestedTransferables); | ||
return result; | ||
}); | ||
function writeUint16(value: number) { | ||
prepareWrite(2).setUint16(offset, value); | ||
commitWrite(); | ||
} | ||
return [result, transferables]; | ||
} | ||
function writeUint32(value: number) { | ||
prepareWrite(4).setUint32(offset, value); | ||
commitWrite(); | ||
} | ||
const result = Object.keys(value).reduce((object, key) => { | ||
const [result, nestedTransferables = []] = toWire((value as any)[key]); | ||
transferables.push(...nestedTransferables); | ||
return {...object, [key]: result}; | ||
}, {}); | ||
function writeUint64(value: number) { | ||
const low = value % POW_2_32; | ||
const high = (value - low) / POW_2_32; | ||
const dataView = prepareWrite(8); | ||
dataView.setUint32(offset, high); | ||
dataView.setUint32(offset + 4, low); | ||
commitWrite(); | ||
} | ||
function writeUint8Array(value: number[] | Uint8Array) { | ||
const dataView = prepareWrite(value.length); | ||
for (let i = 0; i < value.length; ++i) | ||
dataView.setUint8(offset + i, value[i]); | ||
commitWrite(); | ||
} | ||
function writeTypeAndLength(type: number, length: number) { | ||
if (length < 24) { | ||
writeUint8((type << 5) | length); | ||
} else if (length < 0x100) { | ||
writeUint8((type << 5) | 24); | ||
writeUint8(length); | ||
} else if (length < 0x10000) { | ||
writeUint8((type << 5) | 25); | ||
writeUint16(length); | ||
} else if (length < 0x100000000) { | ||
writeUint8((type << 5) | 26); | ||
writeUint32(length); | ||
} else { | ||
writeUint8((type << 5) | 27); | ||
writeUint64(length); | ||
return [result, transferables]; | ||
} | ||
} | ||
function writeNumber(value: number) { | ||
if (Math.floor(value) === value) { | ||
if (value >= 0 && value <= POW_2_53) return writeTypeAndLength(0, value); | ||
if (-POW_2_53 <= value && value < 0) | ||
return writeTypeAndLength(1, -(value + 1)); | ||
if (typeof value === 'function') { | ||
const [result, transferables] = functions.toWire(value); | ||
return [{[FUNCTION]: result}, transferables]; | ||
} | ||
writeUint8(0xfb); | ||
writeFloat64(value); | ||
return [value]; | ||
} | ||
function writeString(value: string) { | ||
const data: number[] = []; | ||
for (let i = 0; i < value.length; ++i) { | ||
let charCode = value.charCodeAt(i); | ||
if (charCode < 0x80) { | ||
data.push(charCode); | ||
} else if (charCode < 0x800) { | ||
data.push(0xc0 | (charCode >> 6)); | ||
data.push(0x80 | (charCode & 0x3f)); | ||
} else if (charCode < 0xd800) { | ||
data.push(0xe0 | (charCode >> 12)); | ||
data.push(0x80 | ((charCode >> 6) & 0x3f)); | ||
data.push(0x80 | (charCode & 0x3f)); | ||
} else { | ||
charCode = (charCode & 0x3ff) << 10; | ||
charCode |= value.charCodeAt(++i) & 0x3ff; | ||
charCode += 0x10000; | ||
data.push(0xf0 | (charCode >> 18)); | ||
data.push(0x80 | ((charCode >> 12) & 0x3f)); | ||
data.push(0x80 | ((charCode >> 6) & 0x3f)); | ||
data.push(0x80 | (charCode & 0x3f)); | ||
function fromWire<Input = unknown, Output = unknown>( | ||
value: Input, | ||
retainedBy: Retainer[] = [], | ||
): Output { | ||
if (typeof value === 'object') { | ||
if (value == null) { | ||
return value as any; | ||
} | ||
} | ||
writeTypeAndLength(3, data.length); | ||
writeUint8Array(data); | ||
} | ||
function encodeItem(value: unknown) { | ||
if (value === false) return writeUint8(0xf4); | ||
if (value === true) return writeUint8(0xf5); | ||
if (value === null) return writeUint8(0xf6); | ||
if (value === undefined) return writeUint8(0xf7); | ||
if (typeof value === 'number') return writeNumber(value); | ||
if (typeof value === 'string') { | ||
if (env) { | ||
writeTypeAndLength(3, 0); | ||
writeNumber(env.store(value)); | ||
} else { | ||
writeString(value); | ||
if (Array.isArray(value)) { | ||
return value.map((value) => fromWire(value, retainedBy)) as any; | ||
} | ||
return; | ||
} | ||
if (Array.isArray(value)) { | ||
writeTypeAndLength(4, value.length); | ||
for (const item of value) encodeItem(item); | ||
return; | ||
} | ||
if (value instanceof Uint8Array) { | ||
writeTypeAndLength(2, value.length); | ||
writeUint8Array(value); | ||
return; | ||
} | ||
if (typeof value === 'function') { | ||
if (env == null) { | ||
throw new Error( | ||
`Can’t store a function without an encoding environment.`, | ||
); | ||
if (FUNCTION in value) { | ||
return functions.fromWire((value as any)[FUNCTION], retainedBy) as any; | ||
} | ||
// Using additional information 10 for a function, 6..20 are unassigned. | ||
// @see https://tools.ietf.org/html/rfc7049#section-2.4 | ||
writeTypeAndLength(6, 10); | ||
writeNumber(env.store(value)); | ||
return; | ||
return Object.keys(value).reduce( | ||
(object, key) => ({ | ||
...object, | ||
[key]: fromWire((value as any)[key], retainedBy), | ||
}), | ||
{}, | ||
) as any; | ||
} | ||
const keys = Object.keys(value as any); | ||
writeTypeAndLength(5, keys.length); | ||
for (const key of keys) { | ||
encodeItem(key); | ||
encodeItem((value as any)[key]); | ||
} | ||
return value as any; | ||
} | ||
encodeItem(value); | ||
if ('slice' in data) return data.slice(0, offset); | ||
const ret = new ArrayBuffer(offset); | ||
const retView = new DataView(ret); | ||
for (let i = 0; i < offset; ++i) retView.setUint8(i, dataView.getUint8(i)); | ||
return ret; | ||
} | ||
interface DecodeEnvironment { | ||
getString(id: number): string; | ||
getFunction(id: number): Function; | ||
function defaultUuid() { | ||
return `${uuidSegment()}-${uuidSegment()}-${uuidSegment()}-${uuidSegment()}`; | ||
} | ||
function decodeWithEnvironment(data: ArrayBuffer, env?: DecodeEnvironment) { | ||
let offset = 0; | ||
const dataView = new DataView(data); | ||
const ret = decodeItem(); | ||
if (offset !== data.byteLength) throw new Error('Remaining bytes'); | ||
return ret; | ||
function commitRead<T>(length: number, value: T) { | ||
offset += length; | ||
return value; | ||
} | ||
function readArrayBuffer(length: number) { | ||
return commitRead(length, new Uint8Array(data, offset, length)); | ||
} | ||
function readFloat16() { | ||
const tempArrayBuffer = new ArrayBuffer(4); | ||
const tempDataView = new DataView(tempArrayBuffer); | ||
const value = readUint16(); | ||
const sign = value & 0x8000; | ||
let exponent = value & 0x7c00; | ||
const fraction = value & 0x03ff; | ||
if (exponent === 0x7c00) exponent = 0xff << 10; | ||
else if (exponent !== 0) exponent += (127 - 15) << 10; | ||
else if (fraction !== 0) return (sign ? -1 : 1) * fraction * POW_2_24; | ||
tempDataView.setUint32( | ||
0, | ||
(sign << 16) | (exponent << 13) | (fraction << 13), | ||
); | ||
return tempDataView.getFloat32(0); | ||
} | ||
function readFloat32() { | ||
return commitRead(4, dataView.getFloat32(offset)); | ||
} | ||
function readFloat64() { | ||
return commitRead(8, dataView.getFloat64(offset)); | ||
} | ||
function readUint8() { | ||
return commitRead(1, dataView.getUint8(offset)); | ||
} | ||
function readUint16() { | ||
return commitRead(2, dataView.getUint16(offset)); | ||
} | ||
function readUint32() { | ||
return commitRead(4, dataView.getUint32(offset)); | ||
} | ||
function readUint64() { | ||
return readUint32() * POW_2_32 + readUint32(); | ||
} | ||
function readBreak() { | ||
if (dataView.getUint8(offset) !== 0xff) return false; | ||
offset += 1; | ||
return true; | ||
} | ||
function readLength(additionalInformation: number) { | ||
if (additionalInformation < 24) return additionalInformation; | ||
if (additionalInformation === 24) return readUint8(); | ||
if (additionalInformation === 25) return readUint16(); | ||
if (additionalInformation === 26) return readUint32(); | ||
if (additionalInformation === 27) return readUint64(); | ||
if (additionalInformation === 31) return -1; | ||
throw new Error('Invalid length encoding'); | ||
} | ||
function readIndefiniteStringLength(majorType: number) { | ||
const initialByte = readUint8(); | ||
if (initialByte === 0xff) return -1; | ||
const length = readLength(initialByte & 0x1f); | ||
if (length < 0 || initialByte >> 5 !== majorType) { | ||
throw new Error('Invalid indefinite length element'); | ||
} | ||
return length; | ||
} | ||
function appendUtf16Data(utf16data: number[], appendLength: number) { | ||
let length = appendLength; | ||
for (let i = 0; i < length; ++i) { | ||
let value = readUint8(); | ||
if (value & 0x80) { | ||
if (value < 0xe0) { | ||
value = ((value & 0x1f) << 6) | (readUint8() & 0x3f); | ||
length -= 1; | ||
} else if (value < 0xf0) { | ||
value = | ||
((value & 0x0f) << 12) | | ||
((readUint8() & 0x3f) << 6) | | ||
(readUint8() & 0x3f); | ||
length -= 2; | ||
} else { | ||
value = | ||
((value & 0x0f) << 18) | | ||
((readUint8() & 0x3f) << 12) | | ||
((readUint8() & 0x3f) << 6) | | ||
(readUint8() & 0x3f); | ||
length -= 3; | ||
} | ||
} | ||
if (value < 0x10000) { | ||
utf16data.push(value); | ||
} else { | ||
value -= 0x10000; | ||
utf16data.push(0xd800 | (value >> 10)); | ||
utf16data.push(0xdc00 | (value & 0x3ff)); | ||
} | ||
} | ||
} | ||
function decodeString(totalLength: number, majorType: number) { | ||
const data: number[] = []; | ||
let length = totalLength; | ||
if (length < 0) { | ||
while ((length = readIndefiniteStringLength(majorType)) >= 0) | ||
appendUtf16Data(data, length); | ||
} else appendUtf16Data(data, length); | ||
return String.fromCharCode(...data); | ||
} | ||
function decodeItem(): unknown { | ||
const initialByte = readUint8(); | ||
const majorType = initialByte >> 5; | ||
const additionalInformation = initialByte & 0x1f; | ||
if (majorType === 7) { | ||
switch (additionalInformation) { | ||
case 25: | ||
return readFloat16(); | ||
case 26: | ||
return readFloat32(); | ||
case 27: | ||
return readFloat64(); | ||
} | ||
} | ||
let length = readLength(additionalInformation); | ||
if (length < 0 && (majorType < 2 || majorType > 6)) { | ||
throw new Error('Invalid length'); | ||
} | ||
switch (majorType) { | ||
case 0: | ||
return length; | ||
case 1: | ||
return -1 - length; | ||
case 2: { | ||
if (length < 0) { | ||
const elements = []; | ||
let fullArrayLength = 0; | ||
while ((length = readIndefiniteStringLength(majorType)) >= 0) { | ||
fullArrayLength += length; | ||
elements.push(readArrayBuffer(length)); | ||
} | ||
const fullArray = new Uint8Array(fullArrayLength); | ||
let fullArrayOffset = 0; | ||
for (const element of elements) { | ||
fullArray.set(element, fullArrayOffset); | ||
fullArrayOffset += element.length; | ||
} | ||
return fullArray; | ||
} | ||
return readArrayBuffer(length); | ||
} | ||
case 3: { | ||
return env | ||
? env.getString(decodeItem() as any) | ||
: decodeString(length, majorType); | ||
} | ||
case 4: { | ||
let retArray; | ||
if (length < 0) { | ||
retArray = []; | ||
while (!readBreak()) retArray.push(decodeItem()); | ||
} else { | ||
retArray = new Array(length); | ||
for (let i = 0; i < length; ++i) retArray[i] = decodeItem(); | ||
} | ||
return retArray; | ||
} | ||
case 5: { | ||
const retObject: {[key: string]: unknown} = {}; | ||
for (let i = 0; i < length || (length < 0 && !readBreak()); ++i) { | ||
const key: string = decodeItem() as any; | ||
retObject[key] = decodeItem(); | ||
} | ||
return retObject; | ||
} | ||
case 6: { | ||
switch (length) { | ||
case 10: { | ||
if (env == null) { | ||
throw new Error( | ||
`Can’t parse a function without a decoding environment`, | ||
); | ||
} | ||
return env.getFunction(decodeItem() as any); | ||
} | ||
default: { | ||
return undefined; | ||
} | ||
} | ||
} | ||
case 7: | ||
switch (length) { | ||
case 20: | ||
return false; | ||
case 21: | ||
return true; | ||
case 22: | ||
return null; | ||
case 23: | ||
return undefined; | ||
default: | ||
return undefined; | ||
} | ||
} | ||
} | ||
function uuidSegment() { | ||
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16); | ||
} |
export {createEndpoint} from './endpoint'; | ||
export type {Endpoint} from './endpoint'; | ||
export { | ||
createChannelFunctionStrategy, | ||
createMessengerFunctionStrategy, | ||
} from './strategies'; | ||
export {fromMessagePort, fromWebWorker} from './adaptors'; | ||
@@ -13,2 +17,8 @@ export { | ||
export type {Retainer, MemoryManageable} from './memory'; | ||
export type {RemoteCallable, SafeRpcArgument, MessageEndpoint} from './types'; | ||
export type { | ||
FunctionStrategy, | ||
FunctionStrategyOptions, | ||
RemoteCallable, | ||
SafeRpcArgument, | ||
MessageEndpoint, | ||
} from './types'; |
@@ -18,2 +18,51 @@ import {createEndpoint} from '../endpoint'; | ||
describe('functions', () => { | ||
it('is available on the endpoint', () => { | ||
const {port1} = new MessageChannel(); | ||
const functions = {} as any; | ||
const endpoint = createEndpoint(fromMessagePort(port1), { | ||
createFunctionStrategy: () => functions, | ||
}); | ||
expect(endpoint).toHaveProperty('functions', functions); | ||
}); | ||
it('is called to serialize and deserialize functions', async () => { | ||
const name = 'Chris'; | ||
const intermediateValue = 'FUNCTION_STANDIN'; | ||
const functionStrategy = { | ||
toWire: jest.fn<[string], any[]>(() => [intermediateValue]), | ||
fromWire: jest.fn(() => () => name), | ||
has: () => false, | ||
}; | ||
const createFunctionStrategy = jest.fn(() => functionStrategy); | ||
const {port1, port2} = new MessageChannel(); | ||
const endpoint1 = createEndpoint<{ | ||
greet(user: {getName(): string}): string; | ||
}>(fromMessagePort(port1), { | ||
createFunctionStrategy, | ||
}); | ||
const endpoint2 = createEndpoint(fromMessagePort(port2), { | ||
createFunctionStrategy, | ||
}); | ||
endpoint2.expose({ | ||
greet: async (user: {getName(): Promise<string>}) => | ||
`Hello, ${await user.getName()}`, | ||
}); | ||
expect(createFunctionStrategy).toHaveBeenCalledWith({ | ||
messenger: expect.any(Object), | ||
toWire: expect.any(Function), | ||
fromWire: expect.any(Function), | ||
uuid: expect.any(Function), | ||
}); | ||
expect(await endpoint1.call.greet({getName: () => 'Chris'})).toBe( | ||
'Hello, Chris', | ||
); | ||
}); | ||
}); | ||
describe('#replace()', () => { | ||
@@ -84,2 +133,13 @@ it('replaces the underlying messenger', async () => { | ||
it('calls terminate on the function strategy', () => { | ||
const spy = jest.fn(); | ||
const {port1} = new MessageChannel(); | ||
const endpoint = createEndpoint(fromMessagePort(port1), { | ||
createFunctionStrategy: () => ({terminate: spy} as any), | ||
}); | ||
endpoint.terminate(); | ||
expect(spy).toHaveBeenCalled(); | ||
}); | ||
it('throws an error when calling a method on a terminated endpoint', () => { | ||
@@ -86,0 +146,0 @@ const {port1} = new MessageChannel(); |
@@ -19,21 +19,19 @@ export interface MessageEndpoint { | ||
) => infer TypeReturned | ||
? (...args: Args) => Promise<ForcePromiseWrapped<TypeReturned>> | ||
? (...args: Args) => AlwaysAsync<TypeReturned> | ||
: never; | ||
type ForcePromiseWrapped<T> = T extends infer U | Promise<infer U> | ||
? ForcePromise<U> | ||
: ForcePromise<T>; | ||
type ForcePromise<T> = T extends Promise<any> | ||
type AlwaysAsync<T> = T extends Promise<any> | ||
? T | ||
: T extends infer U | Promise<infer U> | ||
? Promise<U> | ||
: T extends (...args: infer Args) => infer TypeReturned | ||
? (...args: Args) => Promise<ForcePromiseWrapped<TypeReturned>> | ||
? (...args: Args) => AlwaysAsync<TypeReturned> | ||
: T extends (infer ArrayElement)[] | ||
? ForcePromiseArray<ArrayElement> | ||
? AlwaysAsync<ArrayElement>[] | ||
: T extends readonly (infer ArrayElement)[] | ||
? readonly AlwaysAsync<ArrayElement>[] | ||
: T extends object | ||
? {[K in keyof T]: ForcePromiseWrapped<T[K]>} | ||
? {[K in keyof T]: AlwaysAsync<T[K]>} | ||
: T; | ||
interface ForcePromiseArray<T> extends Array<ForcePromiseWrapped<T>> {} | ||
export type SafeRpcArgument<T> = T extends ( | ||
@@ -46,3 +44,5 @@ ...args: infer Args | ||
: T extends (infer ArrayElement)[] | ||
? SafeRpcArgumentArray<ArrayElement> | ||
? SafeRpcArgument<ArrayElement>[] | ||
: T extends readonly (infer ArrayElement)[] | ||
? readonly SafeRpcArgument<ArrayElement>[] | ||
: T extends object | ||
@@ -52,8 +52,6 @@ ? {[K in keyof T]: SafeRpcArgument<T[K]>} | ||
interface SafeRpcArgumentArray<T> extends Array<SafeRpcArgument<T>> {} | ||
export const RETAIN_METHOD = Symbol('retain'); | ||
export const RELEASE_METHOD = Symbol('release'); | ||
export const RETAINED_BY = Symbol('retainedBy'); | ||
export const RETAIN_METHOD = Symbol.for('Remote::Retain'); | ||
export const RELEASE_METHOD = Symbol.for('Remote::Release'); | ||
export const RETAINED_BY = Symbol.for('Remote::RetainedBy'); | ||
export interface Retainer { | ||
@@ -68,1 +66,20 @@ add(manageable: MemoryManageable): void; | ||
} | ||
export interface FunctionStrategy<T> { | ||
toWire(value: Function): [T, Transferable[]?]; | ||
fromWire(value: T, retainedBy?: Retainer[]): Function; | ||
revoke?(value: Function): void; | ||
exchange?(value: Function, newValue: Function): void; | ||
terminate?(): void; | ||
has(value: Function): boolean; | ||
} | ||
export interface FunctionStrategyOptions { | ||
readonly messenger: MessageEndpoint; | ||
uuid(): string; | ||
toWire(value: unknown): [any, Transferable[]?]; | ||
fromWire<Input = unknown, Output = unknown>( | ||
value: Input, | ||
retainedBy?: Retainer[], | ||
): Output; | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
441425
3.82%100
8.7%4681
8.01%0
-100%326
Infinity%