Comparing version 2.3.0 to 2.3.1
@@ -19,6 +19,13 @@ /** | ||
export declare type Proxy = Function; | ||
export declare type Exposable = Function | Object; | ||
export interface TransferHandler { | ||
canHandle: (obj: {}) => Boolean; | ||
serialize: (obj: {}) => {}; | ||
deserialize: (obj: {}) => {}; | ||
} | ||
export declare const Comlink: { | ||
proxy: (endpoint: Window | Endpoint) => Function; | ||
proxyValue: (obj: {}) => {}; | ||
expose: (rootObj: Object | Function, endpoint: Window | Endpoint) => void; | ||
transferHandlers: Map<string, TransferHandler>; | ||
expose: (rootObj: Exposable, endpoint: Window | Endpoint) => void; | ||
}; |
@@ -14,9 +14,29 @@ /** | ||
export const Comlink = (function () { | ||
const TRANSFERABLE_TYPES = [ArrayBuffer, MessagePort]; | ||
const uid = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); | ||
const proxyValueSymbol = Symbol('proxyValue'); | ||
const throwSymbol = Symbol('throw'); | ||
const proxyTransferHandler = { | ||
canHandle: (obj) => obj && obj[proxyValueSymbol], | ||
serialize: (obj) => { | ||
const { port1, port2 } = new MessageChannel(); | ||
expose(obj, port1); | ||
return port2; | ||
}, | ||
deserialize: (obj) => { | ||
return proxy(obj); | ||
}, | ||
}; | ||
const throwTransferHandler = { | ||
canHandle: (obj) => obj && obj[throwSymbol], | ||
serialize: (obj) => obj.toString() + '\n' + obj.stack, | ||
deserialize: (obj) => { | ||
throw Error(obj); | ||
}, | ||
}; | ||
/* export */ const transferHandlers = new Map([ | ||
['PROXY', proxyTransferHandler], | ||
['THROW', throwTransferHandler], | ||
]); | ||
let pingPongMessageCounter = 0; | ||
const TRANSFERABLE_TYPES = [ArrayBuffer, MessagePort]; | ||
const proxyValueSymbol = Symbol('proxyValue'); | ||
// Symbols are not transferable. For the case where a parameter needs to be | ||
// proxy’d, we need to set some sort of transferable, secret marker. This is it. | ||
const transferMarker = '__omg_so_secret'; | ||
/* export */ function proxy(endpoint) { | ||
@@ -28,24 +48,9 @@ if (isWindow(endpoint)) | ||
activateEndpoint(endpoint); | ||
return batchingProxy(async (irequest) => { | ||
return cbProxy(async (irequest) => { | ||
let args = []; | ||
if (irequest.type === 'APPLY' || irequest.type === 'CONSTRUCT') { | ||
args = irequest.argumentsList = | ||
irequest.argumentsList.map(arg => { | ||
if (!isProxyValue(arg)) | ||
return arg; | ||
const { port1, port2 } = new MessageChannel(); | ||
expose(arg, port1); | ||
return { | ||
[transferMarker]: 'PROXY', | ||
endpoint: port2, | ||
}; | ||
}); | ||
} | ||
const response = await pingPongMessage(endpoint, irequest, transferableProperties(args)); | ||
if (irequest.type === 'APPLY' || irequest.type === 'CONSTRUCT') | ||
args = irequest.argumentsList.map(wrapValue); | ||
const response = await pingPongMessage(endpoint, Object.assign({}, irequest, { argumentsList: args }), transferableProperties(args)); | ||
const result = response.data; | ||
if (result.type === 'ERROR') | ||
throw Error(result.error); | ||
if (result.type === 'PROXY') | ||
return proxy(result.endpoint); | ||
return result.obj; | ||
return unwrapValue(result.value); | ||
}); | ||
@@ -69,32 +74,23 @@ } | ||
let obj = await irequest.callPath.reduce((obj, propName) => obj[propName], rootObj); | ||
const isAsyncGenerator = obj.constructor.name === 'AsyncGeneratorFunction'; | ||
let iresult = obj; | ||
let ierror; | ||
// If there is an arguments list, proxy-fy parameters as necessary | ||
if ('argumentsList' in irequest) { | ||
irequest.argumentsList = | ||
irequest.argumentsList.map(arg => { | ||
if (arg[transferMarker] === 'PROXY') | ||
return proxy(arg.endpoint); | ||
else | ||
return arg; | ||
}); | ||
} | ||
let args = []; | ||
if (irequest.type === 'APPLY' || irequest.type === 'CONSTRUCT') | ||
args = irequest.argumentsList.map(unwrapValue); | ||
if (irequest.type === 'APPLY') { | ||
try { | ||
iresult = await obj.apply(that, irequest.argumentsList); | ||
iresult = await obj.apply(that, args); | ||
} | ||
catch (e) { | ||
ierror = e; | ||
iresult = e; | ||
iresult[throwSymbol] = true; | ||
} | ||
} | ||
if (isAsyncGenerator) | ||
iresult = proxyValue(iresult); | ||
if (irequest.type === 'CONSTRUCT') { | ||
try { | ||
iresult = new obj(...(irequest.argumentsList || [])); // eslint-disable-line new-cap | ||
iresult = new obj(...args); // eslint-disable-line new-cap | ||
iresult = proxyValue(iresult); | ||
} | ||
catch (e) { | ||
ierror = e; | ||
iresult = e; | ||
iresult[throwSymbol] = true; | ||
} | ||
@@ -108,3 +104,3 @@ } | ||
} | ||
iresult = makeInvocationResult(iresult, ierror); | ||
iresult = makeInvocationResult(iresult); | ||
iresult.id = irequest.id; | ||
@@ -114,2 +110,60 @@ return endpoint.postMessage(iresult, transferableProperties([iresult])); | ||
} | ||
function wrapValue(arg) { | ||
// Is arg itself handled by a TransferHandler? | ||
for (const [key, transferHandler] of transferHandlers.entries()) { | ||
if (transferHandler.canHandle(arg)) { | ||
return { | ||
type: key, | ||
value: transferHandler.serialize(arg), | ||
}; | ||
} | ||
} | ||
// If not, traverse the entire object and find handled values. | ||
let wrappedChildren = []; | ||
for (const item of iterateAllProperties(arg)) { | ||
for (const [key, transferHandler] of transferHandlers.entries()) { | ||
if (transferHandler.canHandle(item.value)) { | ||
wrappedChildren.push({ | ||
path: item.path, | ||
wrappedValue: { | ||
type: key, | ||
value: item.value, | ||
}, | ||
}); | ||
} | ||
} | ||
} | ||
return { | ||
type: 'RAW', | ||
value: arg, | ||
wrappedChildren, | ||
}; | ||
} | ||
function unwrapValue(arg) { | ||
if (transferHandlers.has(arg.type)) { | ||
const transferHandler = transferHandlers.get(arg.type); | ||
return transferHandler.deserialize(arg.value); | ||
} | ||
else if (isRawWrappedValue(arg)) { | ||
for (const wrappedChildValue of (arg.wrappedChildren || [])) { | ||
if (!transferHandlers.has(wrappedChildValue.wrappedValue.type)) | ||
throw Error(`Unknown value type "${arg.type}" at ${wrappedChildValue.path.join('.')}`); | ||
const transferHandler = transferHandlers.get(wrappedChildValue.wrappedValue.type); | ||
const newValue = transferHandler.deserialize(wrappedChildValue.wrappedValue.value); | ||
replaceValueInObjectAtPath(arg.value, wrappedChildValue.path, newValue); | ||
} | ||
return arg.value; | ||
} | ||
else { | ||
throw Error(`Unknown value type "${arg.type}"`); | ||
} | ||
} | ||
function replaceValueInObjectAtPath(obj, path, newVal) { | ||
const lastKey = path.slice(-1)[0]; | ||
const lastObj = path.slice(0, -1).reduce((obj, key) => obj[key], obj); | ||
lastObj[lastKey] = newVal; | ||
} | ||
function isRawWrappedValue(arg) { | ||
return arg.type === 'RAW'; | ||
} | ||
function windowEndpoint(w) { | ||
@@ -173,15 +227,6 @@ if (self.constructor.name !== 'Window') | ||
} | ||
function asyncIteratorSupport() { | ||
return 'asyncIterator' in Symbol; | ||
} | ||
/** | ||
* `batchingProxy` creates a ES6 Proxy that batches `get`s until either | ||
* `construct` or `apply` is called. At that point the callback is invoked with | ||
* the accumulated call path. | ||
*/ | ||
function batchingProxy(cb) { | ||
let callPath = []; | ||
function cbProxy(cb, callPath = []) { | ||
return new Proxy(function () { }, { | ||
construct(_target, argumentsList, proxy) { | ||
const r = cb({ | ||
return cb({ | ||
type: 'CONSTRUCT', | ||
@@ -191,4 +236,2 @@ callPath, | ||
}); | ||
callPath = []; | ||
return r; | ||
}, | ||
@@ -198,12 +241,5 @@ apply(_target, _thisArg, argumentsList) { | ||
// The actual target for `bind()` is currently ignored. | ||
if (callPath[callPath.length - 1] === 'bind') { | ||
const localCallPath = callPath.slice(); | ||
callPath = []; | ||
return (...args) => cb({ | ||
type: 'APPLY', | ||
callPath: localCallPath.slice(0, -1), | ||
argumentsList: args, | ||
}); | ||
} | ||
const r = cb({ | ||
if (callPath[callPath.length - 1] === 'bind') | ||
return cbProxy(cb, callPath.slice(0, -1)); | ||
return cb({ | ||
type: 'APPLY', | ||
@@ -213,4 +249,2 @@ callPath, | ||
}); | ||
callPath = []; | ||
return r; | ||
}, | ||
@@ -221,7 +255,2 @@ get(_target, property, proxy) { | ||
} | ||
else if (asyncIteratorSupport() && property === Symbol.asyncIterator) { | ||
// For now, only async generators use `Symbol.asyncIterator` and they | ||
// return themselves, so we emulate that behavior here. | ||
return () => proxy; | ||
} | ||
else if (property === 'then') { | ||
@@ -232,8 +261,6 @@ const r = cb({ | ||
}); | ||
callPath = []; | ||
return Promise.resolve(r).then.bind(r); | ||
} | ||
else { | ||
callPath.push(property); | ||
return proxy; | ||
return cbProxy(cb, callPath.concat(property)); | ||
} | ||
@@ -254,13 +281,17 @@ }, | ||
} | ||
function* iterateAllProperties(obj) { | ||
if (!obj) | ||
function* iterateAllProperties(value, path = [], visited = null) { | ||
if (!value) | ||
return; | ||
if (typeof obj === 'string') | ||
if (!visited) | ||
visited = new WeakSet(); | ||
if (visited.has(value)) | ||
return; | ||
yield obj; | ||
let vals = Object.values(obj); | ||
if (Array.isArray(obj)) | ||
vals = obj; | ||
for (const val of vals) | ||
yield* iterateAllProperties(val); | ||
if (typeof value === 'string') | ||
return; | ||
if (typeof value === 'object') | ||
visited.add(value); | ||
yield { value, path }; | ||
let keys = Object.keys(value); | ||
for (const key of keys) | ||
yield* iterateAllProperties(value[key], [...path, key], visited); | ||
} | ||
@@ -270,40 +301,24 @@ function transferableProperties(obj) { | ||
for (const prop of iterateAllProperties(obj)) { | ||
if (isTransferable(prop)) | ||
r.push(prop); | ||
if (isTransferable(prop.value)) | ||
r.push(prop.value); | ||
} | ||
return r; | ||
} | ||
function isProxyValue(obj) { | ||
return obj && obj[proxyValueSymbol]; | ||
} | ||
function makeInvocationResult(obj, err = null) { | ||
if (err) { | ||
return { | ||
type: 'ERROR', | ||
error: ('stack' in err) ? err.stack : err.toString(), | ||
}; | ||
function makeInvocationResult(obj) { | ||
for (const [type, transferHandler] of transferHandlers.entries()) { | ||
if (transferHandler.canHandle(obj)) { | ||
const value = transferHandler.serialize(obj); | ||
return { | ||
value: { type, value }, | ||
}; | ||
} | ||
} | ||
// TODO We actually need to perform a structured clone tree | ||
// walk of the data as we want to allow: | ||
// return {foo: proxyValue(foo)}; | ||
// We also don't want to directly mutate the data as: | ||
// class A { | ||
// constructor() { this.b = {b: proxyValue(new B())} } | ||
// method1() { return this.b; } | ||
// method2() { this.b.foo; /* should work */ } | ||
// } | ||
if (isProxyValue(obj)) { | ||
const { port1, port2 } = new MessageChannel(); | ||
expose(obj, port1); | ||
return { | ||
type: 'PROXY', | ||
endpoint: port2, | ||
}; | ||
} | ||
return { | ||
type: 'OBJECT', | ||
obj, | ||
value: { | ||
type: 'RAW', | ||
value: obj, | ||
}, | ||
}; | ||
} | ||
return { proxy, proxyValue, expose }; | ||
return { proxy, proxyValue, transferHandlers, expose }; | ||
})(); |
@@ -1,1 +0,1 @@ | ||
export const Comlink=function(){function a(b){if(j(b)&&(b=d(b)),!e(b))throw Error('endpoint does not have all of addEventListener, removeEventListener and postMessage defined');return f(b),m(async(d)=>{let e=[];('APPLY'===d.type||'CONSTRUCT'===d.type)&&(e=d.argumentsList=d.argumentsList.map((a)=>{if(!q(a))return a;const{port1:b,port2:d}=new MessageChannel;return c(a,b),{[w]:'PROXY',endpoint:d}}));const f=await k(b,d,p(e)),g=f.data;if('ERROR'===g.type)throw Error(g.error);return'PROXY'===g.type?a(g.endpoint):g.obj})}function b(a){return a[v]=!0,a}function c(c,h){if(j(h)&&(h=d(h)),!e(h))throw Error('endpoint does not have all of addEventListener, removeEventListener and postMessage defined');f(h),g(h,async function(d){if(!d.data.id)return;const e=d.data;let f=await e.callPath.slice(0,-1).reduce((a,b)=>a[b],c),g=await e.callPath.reduce((a,b)=>a[b],c);const i='AsyncGeneratorFunction'===g.constructor.name;let j,k=g;if('argumentsList'in e&&(e.argumentsList=e.argumentsList.map((b)=>{return'PROXY'===b[w]?a(b.endpoint):b})),'APPLY'===e.type)try{k=await g.apply(f,e.argumentsList)}catch(a){j=a}if(i&&(k=b(k)),'CONSTRUCT'===e.type)try{k=new g(...(e.argumentsList||[])),k=b(k)}catch(a){j=a}return'SET'===e.type&&(g[e.property]=e.value,k=!0),k=r(k,j),k.id=e.id,h.postMessage(k,p([k]))})}function d(a){if('Window'!==self.constructor.name)throw Error('self is not a window');return{addEventListener:self.addEventListener.bind(self),removeEventListener:self.removeEventListener.bind(self),postMessage:(b,c)=>a.postMessage(b,'*',c)}}function e(a){return'addEventListener'in a&&'removeEventListener'in a&&'postMessage'in a}function f(a){i(a)&&a.start()}function g(a,b){a.addEventListener('message',b)}function h(a,b){a.removeEventListener('message',b)}function i(a){return'MessagePort'===a.constructor.name}function j(a){return['window','length','location','parent','opener'].every((b)=>b in a)}function k(a,b,c){const d=`${s}-${t++}`;return new Promise((e)=>{g(a,function b(c){c.data.id!==d||(h(a,b),e(c))}),b=Object.assign({},b,{id:d}),a.postMessage(b,c)})}function l(){return'asyncIterator'in Symbol}function m(a){let b=[];return new Proxy(function(){},{construct(c,d){const e=a({type:'CONSTRUCT',callPath:b,argumentsList:d});return b=[],e},apply(c,d,e){if('bind'===b[b.length-1]){const c=b.slice();return b=[],(...b)=>a({type:'APPLY',callPath:c.slice(0,-1),argumentsList:b})}const f=a({type:'APPLY',callPath:b,argumentsList:e});return b=[],f},get(c,d,e){if('then'===d&&0===b.length)return{then:()=>e};if(l()&&d===Symbol.asyncIterator)return()=>e;if('then'===d){const c=a({type:'GET',callPath:b});return b=[],Promise.resolve(c).then.bind(c)}return b.push(d),e},set(c,d,e){return a({type:'SET',callPath:b,property:d,value:e})}})}function n(a){return u.some((b)=>a instanceof b)}function*o(a){if(!a)return;if('string'==typeof a)return;yield a;let b=Object.values(a);Array.isArray(a)&&(b=a);for(const c of b)yield*o(c)}function p(a){const b=[];for(const c of o(a))n(c)&&b.push(c);return b}function q(a){return a&&a[v]}function r(a,b=null){if(b)return{type:'ERROR',error:'stack'in b?b.stack:b.toString()};if(q(a)){const{port1:b,port2:d}=new MessageChannel;return c(a,b),{type:'PROXY',endpoint:d}}return{type:'OBJECT',obj:a}}const s=Math.floor(Math.random()*Number.MAX_SAFE_INTEGER);let t=0;const u=[ArrayBuffer,MessagePort],v=Symbol('proxyValue'),w='__omg_so_secret';return{proxy:a,proxyValue:b,expose:c}}(); | ||
export const Comlink=function(){function a(a){if(n(a)&&(a=h(a)),!i(a))throw Error('endpoint does not have all of addEventListener, removeEventListener and postMessage defined');return j(a),p(async(b)=>{let c=[];('APPLY'===b.type||'CONSTRUCT'===b.type)&&(c=b.argumentsList.map(d));const f=await o(a,Object.assign({},b,{argumentsList:c}),r(c)),g=f.data;return e(g.value)})}function b(a){return a[w]=!0,a}function c(a,c){if(n(c)&&(c=h(c)),!i(c))throw Error('endpoint does not have all of addEventListener, removeEventListener and postMessage defined');j(c),k(c,async function(d){if(!d.data.id)return;const f=d.data;let g=await f.callPath.slice(0,-1).reduce((a,b)=>a[b],a),h=await f.callPath.reduce((a,b)=>a[b],a),i=h,j=[];if(('APPLY'===f.type||'CONSTRUCT'===f.type)&&(j=f.argumentsList.map(e)),'APPLY'===f.type)try{i=await h.apply(g,j)}catch(a){i=a,i[x]=!0}if('CONSTRUCT'===f.type)try{i=new h(...j),i=b(i)}catch(a){i=a,i[x]=!0}return'SET'===f.type&&(h[f.property]=f.value,i=!0),i=t(i),i.id=f.id,c.postMessage(i,r([i]))})}function d(a){for(const[b,c]of y.entries())if(c.canHandle(a))return{type:b,value:c.serialize(a)};let b=[];for(const c of s(a))for(const[a,d]of y.entries())d.canHandle(c.value)&&b.push({path:c.path,wrappedValue:{type:a,value:c.value}});return{type:'RAW',value:a,wrappedChildren:b}}function e(a){if(y.has(a.type)){const b=y.get(a.type);return b.deserialize(a.value)}if(g(a)){for(const b of a.wrappedChildren||[]){if(!y.has(b.wrappedValue.type))throw Error(`Unknown value type "${a.type}" at ${b.path.join('.')}`);const c=y.get(b.wrappedValue.type),d=c.deserialize(b.wrappedValue.value);f(a.value,b.path,d)}return a.value}throw Error(`Unknown value type "${a.type}"`)}function f(a,b,c){const d=b.slice(-1)[0],e=b.slice(0,-1).reduce((a,b)=>a[b],a);e[d]=c}function g(a){return'RAW'===a.type}function h(a){if('Window'!==self.constructor.name)throw Error('self is not a window');return{addEventListener:self.addEventListener.bind(self),removeEventListener:self.removeEventListener.bind(self),postMessage:(b,c)=>a.postMessage(b,'*',c)}}function i(a){return'addEventListener'in a&&'removeEventListener'in a&&'postMessage'in a}function j(a){m(a)&&a.start()}function k(a,b){a.addEventListener('message',b)}function l(a,b){a.removeEventListener('message',b)}function m(a){return'MessagePort'===a.constructor.name}function n(a){return['window','length','location','parent','opener'].every((b)=>b in a)}function o(a,b,c){const d=`${v}-${z++}`;return new Promise((e)=>{k(a,function b(c){c.data.id!==d||(l(a,b),e(c))}),b=Object.assign({},b,{id:d}),a.postMessage(b,c)})}function p(a,b=[]){return new Proxy(function(){},{construct(c,d){return a({type:'CONSTRUCT',callPath:b,argumentsList:d})},apply(c,d,e){return'bind'===b[b.length-1]?p(a,b.slice(0,-1)):a({type:'APPLY',callPath:b,argumentsList:e})},get(c,d,e){if('then'===d&&0===b.length)return{then:()=>e};if('then'===d){const c=a({type:'GET',callPath:b});return Promise.resolve(c).then.bind(c)}return p(a,b.concat(d))},set(c,d,e){return a({type:'SET',callPath:b,property:d,value:e})}})}function q(a){return u.some((b)=>a instanceof b)}function*s(a,b=[],c=null){if(a&&(c||(c=new WeakSet),!c.has(a))&&'string'!=typeof a){'object'==typeof a&&c.add(a),yield{value:a,path:b};let d=Object.keys(a);for(const e of d)yield*s(a[e],[...b,e],c)}}function r(a){const b=[];for(const c of s(a))q(c.value)&&b.push(c.value);return b}function t(a){for(const[b,c]of y.entries())if(c.canHandle(a)){const d=c.serialize(a);return{value:{type:b,value:d}}}return{value:{type:'RAW',value:a}}}const u=[ArrayBuffer,MessagePort],v=Math.floor(Math.random()*Number.MAX_SAFE_INTEGER),w=Symbol('proxyValue'),x=Symbol('throw'),y=new Map([['PROXY',{canHandle:(a)=>a&&a[w],serialize:(a)=>{const{port1:b,port2:d}=new MessageChannel;return c(a,b),d},deserialize:(b)=>{return a(b)}}],['THROW',{canHandle:(a)=>a&&a[x],serialize:(a)=>a.toString()+'\n'+a.stack,deserialize:(a)=>{throw Error(a)}}]]);let z=0;return{proxy:a,proxyValue:b,transferHandlers:y,expose:c}}(); |
@@ -16,9 +16,29 @@ "use strict"; | ||
self.Comlink = (function () { | ||
const TRANSFERABLE_TYPES = [ArrayBuffer, MessagePort]; | ||
const uid = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); | ||
const proxyValueSymbol = Symbol('proxyValue'); | ||
const throwSymbol = Symbol('throw'); | ||
const proxyTransferHandler = { | ||
canHandle: (obj) => obj && obj[proxyValueSymbol], | ||
serialize: (obj) => { | ||
const { port1, port2 } = new MessageChannel(); | ||
expose(obj, port1); | ||
return port2; | ||
}, | ||
deserialize: (obj) => { | ||
return proxy(obj); | ||
}, | ||
}; | ||
const throwTransferHandler = { | ||
canHandle: (obj) => obj && obj[throwSymbol], | ||
serialize: (obj) => obj.toString() + '\n' + obj.stack, | ||
deserialize: (obj) => { | ||
throw Error(obj); | ||
}, | ||
}; | ||
/* export */ const transferHandlers = new Map([ | ||
['PROXY', proxyTransferHandler], | ||
['THROW', throwTransferHandler], | ||
]); | ||
let pingPongMessageCounter = 0; | ||
const TRANSFERABLE_TYPES = [ArrayBuffer, MessagePort]; | ||
const proxyValueSymbol = Symbol('proxyValue'); | ||
// Symbols are not transferable. For the case where a parameter needs to be | ||
// proxy’d, we need to set some sort of transferable, secret marker. This is it. | ||
const transferMarker = '__omg_so_secret'; | ||
/* export */ function proxy(endpoint) { | ||
@@ -30,24 +50,9 @@ if (isWindow(endpoint)) | ||
activateEndpoint(endpoint); | ||
return batchingProxy(async (irequest) => { | ||
return cbProxy(async (irequest) => { | ||
let args = []; | ||
if (irequest.type === 'APPLY' || irequest.type === 'CONSTRUCT') { | ||
args = irequest.argumentsList = | ||
irequest.argumentsList.map(arg => { | ||
if (!isProxyValue(arg)) | ||
return arg; | ||
const { port1, port2 } = new MessageChannel(); | ||
expose(arg, port1); | ||
return { | ||
[transferMarker]: 'PROXY', | ||
endpoint: port2, | ||
}; | ||
}); | ||
} | ||
const response = await pingPongMessage(endpoint, irequest, transferableProperties(args)); | ||
if (irequest.type === 'APPLY' || irequest.type === 'CONSTRUCT') | ||
args = irequest.argumentsList.map(wrapValue); | ||
const response = await pingPongMessage(endpoint, Object.assign({}, irequest, { argumentsList: args }), transferableProperties(args)); | ||
const result = response.data; | ||
if (result.type === 'ERROR') | ||
throw Error(result.error); | ||
if (result.type === 'PROXY') | ||
return proxy(result.endpoint); | ||
return result.obj; | ||
return unwrapValue(result.value); | ||
}); | ||
@@ -71,32 +76,23 @@ } | ||
let obj = await irequest.callPath.reduce((obj, propName) => obj[propName], rootObj); | ||
const isAsyncGenerator = obj.constructor.name === 'AsyncGeneratorFunction'; | ||
let iresult = obj; | ||
let ierror; | ||
// If there is an arguments list, proxy-fy parameters as necessary | ||
if ('argumentsList' in irequest) { | ||
irequest.argumentsList = | ||
irequest.argumentsList.map(arg => { | ||
if (arg[transferMarker] === 'PROXY') | ||
return proxy(arg.endpoint); | ||
else | ||
return arg; | ||
}); | ||
} | ||
let args = []; | ||
if (irequest.type === 'APPLY' || irequest.type === 'CONSTRUCT') | ||
args = irequest.argumentsList.map(unwrapValue); | ||
if (irequest.type === 'APPLY') { | ||
try { | ||
iresult = await obj.apply(that, irequest.argumentsList); | ||
iresult = await obj.apply(that, args); | ||
} | ||
catch (e) { | ||
ierror = e; | ||
iresult = e; | ||
iresult[throwSymbol] = true; | ||
} | ||
} | ||
if (isAsyncGenerator) | ||
iresult = proxyValue(iresult); | ||
if (irequest.type === 'CONSTRUCT') { | ||
try { | ||
iresult = new obj(...(irequest.argumentsList || [])); // eslint-disable-line new-cap | ||
iresult = new obj(...args); // eslint-disable-line new-cap | ||
iresult = proxyValue(iresult); | ||
} | ||
catch (e) { | ||
ierror = e; | ||
iresult = e; | ||
iresult[throwSymbol] = true; | ||
} | ||
@@ -110,3 +106,3 @@ } | ||
} | ||
iresult = makeInvocationResult(iresult, ierror); | ||
iresult = makeInvocationResult(iresult); | ||
iresult.id = irequest.id; | ||
@@ -116,2 +112,60 @@ return endpoint.postMessage(iresult, transferableProperties([iresult])); | ||
} | ||
function wrapValue(arg) { | ||
// Is arg itself handled by a TransferHandler? | ||
for (const [key, transferHandler] of transferHandlers.entries()) { | ||
if (transferHandler.canHandle(arg)) { | ||
return { | ||
type: key, | ||
value: transferHandler.serialize(arg), | ||
}; | ||
} | ||
} | ||
// If not, traverse the entire object and find handled values. | ||
let wrappedChildren = []; | ||
for (const item of iterateAllProperties(arg)) { | ||
for (const [key, transferHandler] of transferHandlers.entries()) { | ||
if (transferHandler.canHandle(item.value)) { | ||
wrappedChildren.push({ | ||
path: item.path, | ||
wrappedValue: { | ||
type: key, | ||
value: item.value, | ||
}, | ||
}); | ||
} | ||
} | ||
} | ||
return { | ||
type: 'RAW', | ||
value: arg, | ||
wrappedChildren, | ||
}; | ||
} | ||
function unwrapValue(arg) { | ||
if (transferHandlers.has(arg.type)) { | ||
const transferHandler = transferHandlers.get(arg.type); | ||
return transferHandler.deserialize(arg.value); | ||
} | ||
else if (isRawWrappedValue(arg)) { | ||
for (const wrappedChildValue of (arg.wrappedChildren || [])) { | ||
if (!transferHandlers.has(wrappedChildValue.wrappedValue.type)) | ||
throw Error(`Unknown value type "${arg.type}" at ${wrappedChildValue.path.join('.')}`); | ||
const transferHandler = transferHandlers.get(wrappedChildValue.wrappedValue.type); | ||
const newValue = transferHandler.deserialize(wrappedChildValue.wrappedValue.value); | ||
replaceValueInObjectAtPath(arg.value, wrappedChildValue.path, newValue); | ||
} | ||
return arg.value; | ||
} | ||
else { | ||
throw Error(`Unknown value type "${arg.type}"`); | ||
} | ||
} | ||
function replaceValueInObjectAtPath(obj, path, newVal) { | ||
const lastKey = path.slice(-1)[0]; | ||
const lastObj = path.slice(0, -1).reduce((obj, key) => obj[key], obj); | ||
lastObj[lastKey] = newVal; | ||
} | ||
function isRawWrappedValue(arg) { | ||
return arg.type === 'RAW'; | ||
} | ||
function windowEndpoint(w) { | ||
@@ -175,15 +229,6 @@ if (self.constructor.name !== 'Window') | ||
} | ||
function asyncIteratorSupport() { | ||
return 'asyncIterator' in Symbol; | ||
} | ||
/** | ||
* `batchingProxy` creates a ES6 Proxy that batches `get`s until either | ||
* `construct` or `apply` is called. At that point the callback is invoked with | ||
* the accumulated call path. | ||
*/ | ||
function batchingProxy(cb) { | ||
let callPath = []; | ||
function cbProxy(cb, callPath = []) { | ||
return new Proxy(function () { }, { | ||
construct(_target, argumentsList, proxy) { | ||
const r = cb({ | ||
return cb({ | ||
type: 'CONSTRUCT', | ||
@@ -193,4 +238,2 @@ callPath, | ||
}); | ||
callPath = []; | ||
return r; | ||
}, | ||
@@ -200,12 +243,5 @@ apply(_target, _thisArg, argumentsList) { | ||
// The actual target for `bind()` is currently ignored. | ||
if (callPath[callPath.length - 1] === 'bind') { | ||
const localCallPath = callPath.slice(); | ||
callPath = []; | ||
return (...args) => cb({ | ||
type: 'APPLY', | ||
callPath: localCallPath.slice(0, -1), | ||
argumentsList: args, | ||
}); | ||
} | ||
const r = cb({ | ||
if (callPath[callPath.length - 1] === 'bind') | ||
return cbProxy(cb, callPath.slice(0, -1)); | ||
return cb({ | ||
type: 'APPLY', | ||
@@ -215,4 +251,2 @@ callPath, | ||
}); | ||
callPath = []; | ||
return r; | ||
}, | ||
@@ -223,7 +257,2 @@ get(_target, property, proxy) { | ||
} | ||
else if (asyncIteratorSupport() && property === Symbol.asyncIterator) { | ||
// For now, only async generators use `Symbol.asyncIterator` and they | ||
// return themselves, so we emulate that behavior here. | ||
return () => proxy; | ||
} | ||
else if (property === 'then') { | ||
@@ -234,8 +263,6 @@ const r = cb({ | ||
}); | ||
callPath = []; | ||
return Promise.resolve(r).then.bind(r); | ||
} | ||
else { | ||
callPath.push(property); | ||
return proxy; | ||
return cbProxy(cb, callPath.concat(property)); | ||
} | ||
@@ -256,13 +283,17 @@ }, | ||
} | ||
function* iterateAllProperties(obj) { | ||
if (!obj) | ||
function* iterateAllProperties(value, path = [], visited = null) { | ||
if (!value) | ||
return; | ||
if (typeof obj === 'string') | ||
if (!visited) | ||
visited = new WeakSet(); | ||
if (visited.has(value)) | ||
return; | ||
yield obj; | ||
let vals = Object.values(obj); | ||
if (Array.isArray(obj)) | ||
vals = obj; | ||
for (const val of vals) | ||
yield* iterateAllProperties(val); | ||
if (typeof value === 'string') | ||
return; | ||
if (typeof value === 'object') | ||
visited.add(value); | ||
yield { value, path }; | ||
let keys = Object.keys(value); | ||
for (const key of keys) | ||
yield* iterateAllProperties(value[key], [...path, key], visited); | ||
} | ||
@@ -272,40 +303,24 @@ function transferableProperties(obj) { | ||
for (const prop of iterateAllProperties(obj)) { | ||
if (isTransferable(prop)) | ||
r.push(prop); | ||
if (isTransferable(prop.value)) | ||
r.push(prop.value); | ||
} | ||
return r; | ||
} | ||
function isProxyValue(obj) { | ||
return obj && obj[proxyValueSymbol]; | ||
} | ||
function makeInvocationResult(obj, err = null) { | ||
if (err) { | ||
return { | ||
type: 'ERROR', | ||
error: ('stack' in err) ? err.stack : err.toString(), | ||
}; | ||
function makeInvocationResult(obj) { | ||
for (const [type, transferHandler] of transferHandlers.entries()) { | ||
if (transferHandler.canHandle(obj)) { | ||
const value = transferHandler.serialize(obj); | ||
return { | ||
value: { type, value }, | ||
}; | ||
} | ||
} | ||
// TODO We actually need to perform a structured clone tree | ||
// walk of the data as we want to allow: | ||
// return {foo: proxyValue(foo)}; | ||
// We also don't want to directly mutate the data as: | ||
// class A { | ||
// constructor() { this.b = {b: proxyValue(new B())} } | ||
// method1() { return this.b; } | ||
// method2() { this.b.foo; /* should work */ } | ||
// } | ||
if (isProxyValue(obj)) { | ||
const { port1, port2 } = new MessageChannel(); | ||
expose(obj, port1); | ||
return { | ||
type: 'PROXY', | ||
endpoint: port2, | ||
}; | ||
} | ||
return { | ||
type: 'OBJECT', | ||
obj, | ||
value: { | ||
type: 'RAW', | ||
value: obj, | ||
}, | ||
}; | ||
} | ||
return { proxy, proxyValue, expose }; | ||
return { proxy, proxyValue, transferHandlers, expose }; | ||
})(); |
@@ -1,1 +0,1 @@ | ||
'use strict';self.Comlink=function(){function a(b){if(j(b)&&(b=d(b)),!e(b))throw Error('endpoint does not have all of addEventListener, removeEventListener and postMessage defined');return f(b),m(async(d)=>{let e=[];('APPLY'===d.type||'CONSTRUCT'===d.type)&&(e=d.argumentsList=d.argumentsList.map((a)=>{if(!q(a))return a;const{port1:b,port2:d}=new MessageChannel;return c(a,b),{[w]:'PROXY',endpoint:d}}));const f=await k(b,d,p(e)),g=f.data;if('ERROR'===g.type)throw Error(g.error);return'PROXY'===g.type?a(g.endpoint):g.obj})}function b(a){return a[v]=!0,a}function c(c,h){if(j(h)&&(h=d(h)),!e(h))throw Error('endpoint does not have all of addEventListener, removeEventListener and postMessage defined');f(h),g(h,async function(d){if(!d.data.id)return;const e=d.data;let f=await e.callPath.slice(0,-1).reduce((a,b)=>a[b],c),g=await e.callPath.reduce((a,b)=>a[b],c);const i='AsyncGeneratorFunction'===g.constructor.name;let j,k=g;if('argumentsList'in e&&(e.argumentsList=e.argumentsList.map((b)=>{return'PROXY'===b[w]?a(b.endpoint):b})),'APPLY'===e.type)try{k=await g.apply(f,e.argumentsList)}catch(a){j=a}if(i&&(k=b(k)),'CONSTRUCT'===e.type)try{k=new g(...(e.argumentsList||[])),k=b(k)}catch(a){j=a}return'SET'===e.type&&(g[e.property]=e.value,k=!0),k=r(k,j),k.id=e.id,h.postMessage(k,p([k]))})}function d(a){if('Window'!==self.constructor.name)throw Error('self is not a window');return{addEventListener:self.addEventListener.bind(self),removeEventListener:self.removeEventListener.bind(self),postMessage:(b,c)=>a.postMessage(b,'*',c)}}function e(a){return'addEventListener'in a&&'removeEventListener'in a&&'postMessage'in a}function f(a){i(a)&&a.start()}function g(a,b){a.addEventListener('message',b)}function h(a,b){a.removeEventListener('message',b)}function i(a){return'MessagePort'===a.constructor.name}function j(a){return['window','length','location','parent','opener'].every((b)=>b in a)}function k(a,b,c){const d=`${s}-${t++}`;return new Promise((e)=>{g(a,function b(c){c.data.id!==d||(h(a,b),e(c))}),b=Object.assign({},b,{id:d}),a.postMessage(b,c)})}function l(){return'asyncIterator'in Symbol}function m(a){let b=[];return new Proxy(function(){},{construct(c,d){const e=a({type:'CONSTRUCT',callPath:b,argumentsList:d});return b=[],e},apply(c,d,e){if('bind'===b[b.length-1]){const c=b.slice();return b=[],(...b)=>a({type:'APPLY',callPath:c.slice(0,-1),argumentsList:b})}const f=a({type:'APPLY',callPath:b,argumentsList:e});return b=[],f},get(c,d,e){if('then'===d&&0===b.length)return{then:()=>e};if(l()&&d===Symbol.asyncIterator)return()=>e;if('then'===d){const c=a({type:'GET',callPath:b});return b=[],Promise.resolve(c).then.bind(c)}return b.push(d),e},set(c,d,e){return a({type:'SET',callPath:b,property:d,value:e})}})}function n(a){return u.some((b)=>a instanceof b)}function*o(a){if(!a)return;if('string'==typeof a)return;yield a;let b=Object.values(a);Array.isArray(a)&&(b=a);for(const c of b)yield*o(c)}function p(a){const b=[];for(const c of o(a))n(c)&&b.push(c);return b}function q(a){return a&&a[v]}function r(a,b=null){if(b)return{type:'ERROR',error:'stack'in b?b.stack:b.toString()};if(q(a)){const{port1:b,port2:d}=new MessageChannel;return c(a,b),{type:'PROXY',endpoint:d}}return{type:'OBJECT',obj:a}}const s=Math.floor(Math.random()*Number.MAX_SAFE_INTEGER);let t=0;const u=[ArrayBuffer,MessagePort],v=Symbol('proxyValue'),w='__omg_so_secret';return{proxy:a,proxyValue:b,expose:c}}(); | ||
'use strict';self.Comlink=function(){function a(a){if(n(a)&&(a=h(a)),!i(a))throw Error('endpoint does not have all of addEventListener, removeEventListener and postMessage defined');return j(a),p(async(b)=>{let c=[];('APPLY'===b.type||'CONSTRUCT'===b.type)&&(c=b.argumentsList.map(d));const f=await o(a,Object.assign({},b,{argumentsList:c}),r(c)),g=f.data;return e(g.value)})}function b(a){return a[w]=!0,a}function c(a,c){if(n(c)&&(c=h(c)),!i(c))throw Error('endpoint does not have all of addEventListener, removeEventListener and postMessage defined');j(c),k(c,async function(d){if(!d.data.id)return;const f=d.data;let g=await f.callPath.slice(0,-1).reduce((a,b)=>a[b],a),h=await f.callPath.reduce((a,b)=>a[b],a),i=h,j=[];if(('APPLY'===f.type||'CONSTRUCT'===f.type)&&(j=f.argumentsList.map(e)),'APPLY'===f.type)try{i=await h.apply(g,j)}catch(a){i=a,i[x]=!0}if('CONSTRUCT'===f.type)try{i=new h(...j),i=b(i)}catch(a){i=a,i[x]=!0}return'SET'===f.type&&(h[f.property]=f.value,i=!0),i=t(i),i.id=f.id,c.postMessage(i,r([i]))})}function d(a){for(const[b,c]of y.entries())if(c.canHandle(a))return{type:b,value:c.serialize(a)};let b=[];for(const c of s(a))for(const[a,d]of y.entries())d.canHandle(c.value)&&b.push({path:c.path,wrappedValue:{type:a,value:c.value}});return{type:'RAW',value:a,wrappedChildren:b}}function e(a){if(y.has(a.type)){const b=y.get(a.type);return b.deserialize(a.value)}if(g(a)){for(const b of a.wrappedChildren||[]){if(!y.has(b.wrappedValue.type))throw Error(`Unknown value type "${a.type}" at ${b.path.join('.')}`);const c=y.get(b.wrappedValue.type),d=c.deserialize(b.wrappedValue.value);f(a.value,b.path,d)}return a.value}throw Error(`Unknown value type "${a.type}"`)}function f(a,b,c){const d=b.slice(-1)[0],e=b.slice(0,-1).reduce((a,b)=>a[b],a);e[d]=c}function g(a){return'RAW'===a.type}function h(a){if('Window'!==self.constructor.name)throw Error('self is not a window');return{addEventListener:self.addEventListener.bind(self),removeEventListener:self.removeEventListener.bind(self),postMessage:(b,c)=>a.postMessage(b,'*',c)}}function i(a){return'addEventListener'in a&&'removeEventListener'in a&&'postMessage'in a}function j(a){m(a)&&a.start()}function k(a,b){a.addEventListener('message',b)}function l(a,b){a.removeEventListener('message',b)}function m(a){return'MessagePort'===a.constructor.name}function n(a){return['window','length','location','parent','opener'].every((b)=>b in a)}function o(a,b,c){const d=`${v}-${z++}`;return new Promise((e)=>{k(a,function b(c){c.data.id!==d||(l(a,b),e(c))}),b=Object.assign({},b,{id:d}),a.postMessage(b,c)})}function p(a,b=[]){return new Proxy(function(){},{construct(c,d){return a({type:'CONSTRUCT',callPath:b,argumentsList:d})},apply(c,d,e){return'bind'===b[b.length-1]?p(a,b.slice(0,-1)):a({type:'APPLY',callPath:b,argumentsList:e})},get(c,d,e){if('then'===d&&0===b.length)return{then:()=>e};if('then'===d){const c=a({type:'GET',callPath:b});return Promise.resolve(c).then.bind(c)}return p(a,b.concat(d))},set(c,d,e){return a({type:'SET',callPath:b,property:d,value:e})}})}function q(a){return u.some((b)=>a instanceof b)}function*s(a,b=[],c=null){if(a&&(c||(c=new WeakSet),!c.has(a))&&'string'!=typeof a){'object'==typeof a&&c.add(a),yield{value:a,path:b};let d=Object.keys(a);for(const e of d)yield*s(a[e],[...b,e],c)}}function r(a){const b=[];for(const c of s(a))q(c.value)&&b.push(c.value);return b}function t(a){for(const[b,c]of y.entries())if(c.canHandle(a)){const d=c.serialize(a);return{value:{type:b,value:d}}}return{value:{type:'RAW',value:a}}}const u=[ArrayBuffer,MessagePort],v=Math.floor(Math.random()*Number.MAX_SAFE_INTEGER),w=Symbol('proxyValue'),x=Symbol('throw'),y=new Map([['PROXY',{canHandle:(a)=>a&&a[w],serialize:(a)=>{const{port1:b,port2:d}=new MessageChannel;return c(a,b),d},deserialize:(b)=>{return a(b)}}],['THROW',{canHandle:(a)=>a&&a[x],serialize:(a)=>a.toString()+'\n'+a.stack,deserialize:(a)=>{throw Error(a)}}]]);let z=0;return{proxy:a,proxyValue:b,transferHandlers:y,expose:c}}(); |
@@ -25,9 +25,29 @@ /** | ||
exports.Comlink = (function () { | ||
const TRANSFERABLE_TYPES = [ArrayBuffer, MessagePort]; | ||
const uid = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); | ||
const proxyValueSymbol = Symbol('proxyValue'); | ||
const throwSymbol = Symbol('throw'); | ||
const proxyTransferHandler = { | ||
canHandle: (obj) => obj && obj[proxyValueSymbol], | ||
serialize: (obj) => { | ||
const { port1, port2 } = new MessageChannel(); | ||
expose(obj, port1); | ||
return port2; | ||
}, | ||
deserialize: (obj) => { | ||
return proxy(obj); | ||
}, | ||
}; | ||
const throwTransferHandler = { | ||
canHandle: (obj) => obj && obj[throwSymbol], | ||
serialize: (obj) => obj.toString() + '\n' + obj.stack, | ||
deserialize: (obj) => { | ||
throw Error(obj); | ||
}, | ||
}; | ||
/* export */ const transferHandlers = new Map([ | ||
['PROXY', proxyTransferHandler], | ||
['THROW', throwTransferHandler], | ||
]); | ||
let pingPongMessageCounter = 0; | ||
const TRANSFERABLE_TYPES = [ArrayBuffer, MessagePort]; | ||
const proxyValueSymbol = Symbol('proxyValue'); | ||
// Symbols are not transferable. For the case where a parameter needs to be | ||
// proxy’d, we need to set some sort of transferable, secret marker. This is it. | ||
const transferMarker = '__omg_so_secret'; | ||
/* export */ function proxy(endpoint) { | ||
@@ -39,24 +59,9 @@ if (isWindow(endpoint)) | ||
activateEndpoint(endpoint); | ||
return batchingProxy(async (irequest) => { | ||
return cbProxy(async (irequest) => { | ||
let args = []; | ||
if (irequest.type === 'APPLY' || irequest.type === 'CONSTRUCT') { | ||
args = irequest.argumentsList = | ||
irequest.argumentsList.map(arg => { | ||
if (!isProxyValue(arg)) | ||
return arg; | ||
const { port1, port2 } = new MessageChannel(); | ||
expose(arg, port1); | ||
return { | ||
[transferMarker]: 'PROXY', | ||
endpoint: port2, | ||
}; | ||
}); | ||
} | ||
const response = await pingPongMessage(endpoint, irequest, transferableProperties(args)); | ||
if (irequest.type === 'APPLY' || irequest.type === 'CONSTRUCT') | ||
args = irequest.argumentsList.map(wrapValue); | ||
const response = await pingPongMessage(endpoint, Object.assign({}, irequest, { argumentsList: args }), transferableProperties(args)); | ||
const result = response.data; | ||
if (result.type === 'ERROR') | ||
throw Error(result.error); | ||
if (result.type === 'PROXY') | ||
return proxy(result.endpoint); | ||
return result.obj; | ||
return unwrapValue(result.value); | ||
}); | ||
@@ -80,32 +85,23 @@ } | ||
let obj = await irequest.callPath.reduce((obj, propName) => obj[propName], rootObj); | ||
const isAsyncGenerator = obj.constructor.name === 'AsyncGeneratorFunction'; | ||
let iresult = obj; | ||
let ierror; | ||
// If there is an arguments list, proxy-fy parameters as necessary | ||
if ('argumentsList' in irequest) { | ||
irequest.argumentsList = | ||
irequest.argumentsList.map(arg => { | ||
if (arg[transferMarker] === 'PROXY') | ||
return proxy(arg.endpoint); | ||
else | ||
return arg; | ||
}); | ||
} | ||
let args = []; | ||
if (irequest.type === 'APPLY' || irequest.type === 'CONSTRUCT') | ||
args = irequest.argumentsList.map(unwrapValue); | ||
if (irequest.type === 'APPLY') { | ||
try { | ||
iresult = await obj.apply(that, irequest.argumentsList); | ||
iresult = await obj.apply(that, args); | ||
} | ||
catch (e) { | ||
ierror = e; | ||
iresult = e; | ||
iresult[throwSymbol] = true; | ||
} | ||
} | ||
if (isAsyncGenerator) | ||
iresult = proxyValue(iresult); | ||
if (irequest.type === 'CONSTRUCT') { | ||
try { | ||
iresult = new obj(...(irequest.argumentsList || [])); // eslint-disable-line new-cap | ||
iresult = new obj(...args); // eslint-disable-line new-cap | ||
iresult = proxyValue(iresult); | ||
} | ||
catch (e) { | ||
ierror = e; | ||
iresult = e; | ||
iresult[throwSymbol] = true; | ||
} | ||
@@ -119,3 +115,3 @@ } | ||
} | ||
iresult = makeInvocationResult(iresult, ierror); | ||
iresult = makeInvocationResult(iresult); | ||
iresult.id = irequest.id; | ||
@@ -125,2 +121,60 @@ return endpoint.postMessage(iresult, transferableProperties([iresult])); | ||
} | ||
function wrapValue(arg) { | ||
// Is arg itself handled by a TransferHandler? | ||
for (const [key, transferHandler] of transferHandlers.entries()) { | ||
if (transferHandler.canHandle(arg)) { | ||
return { | ||
type: key, | ||
value: transferHandler.serialize(arg), | ||
}; | ||
} | ||
} | ||
// If not, traverse the entire object and find handled values. | ||
let wrappedChildren = []; | ||
for (const item of iterateAllProperties(arg)) { | ||
for (const [key, transferHandler] of transferHandlers.entries()) { | ||
if (transferHandler.canHandle(item.value)) { | ||
wrappedChildren.push({ | ||
path: item.path, | ||
wrappedValue: { | ||
type: key, | ||
value: item.value, | ||
}, | ||
}); | ||
} | ||
} | ||
} | ||
return { | ||
type: 'RAW', | ||
value: arg, | ||
wrappedChildren, | ||
}; | ||
} | ||
function unwrapValue(arg) { | ||
if (transferHandlers.has(arg.type)) { | ||
const transferHandler = transferHandlers.get(arg.type); | ||
return transferHandler.deserialize(arg.value); | ||
} | ||
else if (isRawWrappedValue(arg)) { | ||
for (const wrappedChildValue of (arg.wrappedChildren || [])) { | ||
if (!transferHandlers.has(wrappedChildValue.wrappedValue.type)) | ||
throw Error(`Unknown value type "${arg.type}" at ${wrappedChildValue.path.join('.')}`); | ||
const transferHandler = transferHandlers.get(wrappedChildValue.wrappedValue.type); | ||
const newValue = transferHandler.deserialize(wrappedChildValue.wrappedValue.value); | ||
replaceValueInObjectAtPath(arg.value, wrappedChildValue.path, newValue); | ||
} | ||
return arg.value; | ||
} | ||
else { | ||
throw Error(`Unknown value type "${arg.type}"`); | ||
} | ||
} | ||
function replaceValueInObjectAtPath(obj, path, newVal) { | ||
const lastKey = path.slice(-1)[0]; | ||
const lastObj = path.slice(0, -1).reduce((obj, key) => obj[key], obj); | ||
lastObj[lastKey] = newVal; | ||
} | ||
function isRawWrappedValue(arg) { | ||
return arg.type === 'RAW'; | ||
} | ||
function windowEndpoint(w) { | ||
@@ -184,15 +238,6 @@ if (self.constructor.name !== 'Window') | ||
} | ||
function asyncIteratorSupport() { | ||
return 'asyncIterator' in Symbol; | ||
} | ||
/** | ||
* `batchingProxy` creates a ES6 Proxy that batches `get`s until either | ||
* `construct` or `apply` is called. At that point the callback is invoked with | ||
* the accumulated call path. | ||
*/ | ||
function batchingProxy(cb) { | ||
let callPath = []; | ||
function cbProxy(cb, callPath = []) { | ||
return new Proxy(function () { }, { | ||
construct(_target, argumentsList, proxy) { | ||
const r = cb({ | ||
return cb({ | ||
type: 'CONSTRUCT', | ||
@@ -202,4 +247,2 @@ callPath, | ||
}); | ||
callPath = []; | ||
return r; | ||
}, | ||
@@ -209,12 +252,5 @@ apply(_target, _thisArg, argumentsList) { | ||
// The actual target for `bind()` is currently ignored. | ||
if (callPath[callPath.length - 1] === 'bind') { | ||
const localCallPath = callPath.slice(); | ||
callPath = []; | ||
return (...args) => cb({ | ||
type: 'APPLY', | ||
callPath: localCallPath.slice(0, -1), | ||
argumentsList: args, | ||
}); | ||
} | ||
const r = cb({ | ||
if (callPath[callPath.length - 1] === 'bind') | ||
return cbProxy(cb, callPath.slice(0, -1)); | ||
return cb({ | ||
type: 'APPLY', | ||
@@ -224,4 +260,2 @@ callPath, | ||
}); | ||
callPath = []; | ||
return r; | ||
}, | ||
@@ -232,7 +266,2 @@ get(_target, property, proxy) { | ||
} | ||
else if (asyncIteratorSupport() && property === Symbol.asyncIterator) { | ||
// For now, only async generators use `Symbol.asyncIterator` and they | ||
// return themselves, so we emulate that behavior here. | ||
return () => proxy; | ||
} | ||
else if (property === 'then') { | ||
@@ -243,8 +272,6 @@ const r = cb({ | ||
}); | ||
callPath = []; | ||
return Promise.resolve(r).then.bind(r); | ||
} | ||
else { | ||
callPath.push(property); | ||
return proxy; | ||
return cbProxy(cb, callPath.concat(property)); | ||
} | ||
@@ -265,13 +292,17 @@ }, | ||
} | ||
function* iterateAllProperties(obj) { | ||
if (!obj) | ||
function* iterateAllProperties(value, path = [], visited = null) { | ||
if (!value) | ||
return; | ||
if (typeof obj === 'string') | ||
if (!visited) | ||
visited = new WeakSet(); | ||
if (visited.has(value)) | ||
return; | ||
yield obj; | ||
let vals = Object.values(obj); | ||
if (Array.isArray(obj)) | ||
vals = obj; | ||
for (const val of vals) | ||
yield* iterateAllProperties(val); | ||
if (typeof value === 'string') | ||
return; | ||
if (typeof value === 'object') | ||
visited.add(value); | ||
yield { value, path }; | ||
let keys = Object.keys(value); | ||
for (const key of keys) | ||
yield* iterateAllProperties(value[key], [...path, key], visited); | ||
} | ||
@@ -281,41 +312,25 @@ function transferableProperties(obj) { | ||
for (const prop of iterateAllProperties(obj)) { | ||
if (isTransferable(prop)) | ||
r.push(prop); | ||
if (isTransferable(prop.value)) | ||
r.push(prop.value); | ||
} | ||
return r; | ||
} | ||
function isProxyValue(obj) { | ||
return obj && obj[proxyValueSymbol]; | ||
} | ||
function makeInvocationResult(obj, err = null) { | ||
if (err) { | ||
return { | ||
type: 'ERROR', | ||
error: ('stack' in err) ? err.stack : err.toString(), | ||
}; | ||
function makeInvocationResult(obj) { | ||
for (const [type, transferHandler] of transferHandlers.entries()) { | ||
if (transferHandler.canHandle(obj)) { | ||
const value = transferHandler.serialize(obj); | ||
return { | ||
value: { type, value }, | ||
}; | ||
} | ||
} | ||
// TODO We actually need to perform a structured clone tree | ||
// walk of the data as we want to allow: | ||
// return {foo: proxyValue(foo)}; | ||
// We also don't want to directly mutate the data as: | ||
// class A { | ||
// constructor() { this.b = {b: proxyValue(new B())} } | ||
// method1() { return this.b; } | ||
// method2() { this.b.foo; /* should work */ } | ||
// } | ||
if (isProxyValue(obj)) { | ||
const { port1, port2 } = new MessageChannel(); | ||
expose(obj, port1); | ||
return { | ||
type: 'PROXY', | ||
endpoint: port2, | ||
}; | ||
} | ||
return { | ||
type: 'OBJECT', | ||
obj, | ||
value: { | ||
type: 'RAW', | ||
value: obj, | ||
}, | ||
}; | ||
} | ||
return { proxy, proxyValue, expose }; | ||
return { proxy, proxyValue, transferHandlers, expose }; | ||
})(); | ||
}); |
@@ -1,1 +0,1 @@ | ||
(function(a){if("object"==typeof module&&"object"==typeof module.exports){var b=a(require,exports);b!==void 0&&(module.exports=b)}else"function"==typeof define&&define.amd&&define(["require","exports"],a)})(function(a,b){"use strict";Object.defineProperty(b,"__esModule",{value:!0}),b.Comlink=function(){function a(b){if(j(b)&&(b=d(b)),!e(b))throw Error("endpoint does not have all of addEventListener, removeEventListener and postMessage defined");return f(b),m(async(d)=>{let e=[];("APPLY"===d.type||"CONSTRUCT"===d.type)&&(e=d.argumentsList=d.argumentsList.map((a)=>{if(!q(a))return a;const{port1:b,port2:d}=new MessageChannel;return c(a,b),{[w]:"PROXY",endpoint:d}}));const f=await k(b,d,p(e)),g=f.data;if("ERROR"===g.type)throw Error(g.error);return"PROXY"===g.type?a(g.endpoint):g.obj})}function b(a){return a[v]=!0,a}function c(c,h){if(j(h)&&(h=d(h)),!e(h))throw Error("endpoint does not have all of addEventListener, removeEventListener and postMessage defined");f(h),g(h,async function(d){if(!d.data.id)return;const e=d.data;let f=await e.callPath.slice(0,-1).reduce((a,b)=>a[b],c),g=await e.callPath.reduce((a,b)=>a[b],c);const i="AsyncGeneratorFunction"===g.constructor.name;let j,k=g;if("argumentsList"in e&&(e.argumentsList=e.argumentsList.map((b)=>{return"PROXY"===b[w]?a(b.endpoint):b})),"APPLY"===e.type)try{k=await g.apply(f,e.argumentsList)}catch(a){j=a}if(i&&(k=b(k)),"CONSTRUCT"===e.type)try{k=new g(...(e.argumentsList||[])),k=b(k)}catch(a){j=a}return"SET"===e.type&&(g[e.property]=e.value,k=!0),k=r(k,j),k.id=e.id,h.postMessage(k,p([k]))})}function d(a){if("Window"!==self.constructor.name)throw Error("self is not a window");return{addEventListener:self.addEventListener.bind(self),removeEventListener:self.removeEventListener.bind(self),postMessage:(b,c)=>a.postMessage(b,"*",c)}}function e(a){return"addEventListener"in a&&"removeEventListener"in a&&"postMessage"in a}function f(a){i(a)&&a.start()}function g(a,b){a.addEventListener("message",b)}function h(a,b){a.removeEventListener("message",b)}function i(a){return"MessagePort"===a.constructor.name}function j(a){return["window","length","location","parent","opener"].every((b)=>b in a)}function k(a,b,c){const d=`${s}-${t++}`;return new Promise((e)=>{g(a,function b(c){c.data.id!==d||(h(a,b),e(c))}),b=Object.assign({},b,{id:d}),a.postMessage(b,c)})}function l(){return"asyncIterator"in Symbol}function m(a){let b=[];return new Proxy(function(){},{construct(c,d){const e=a({type:"CONSTRUCT",callPath:b,argumentsList:d});return b=[],e},apply(c,d,e){if("bind"===b[b.length-1]){const c=b.slice();return b=[],(...b)=>a({type:"APPLY",callPath:c.slice(0,-1),argumentsList:b})}const f=a({type:"APPLY",callPath:b,argumentsList:e});return b=[],f},get(c,d,e){if("then"===d&&0===b.length)return{then:()=>e};if(l()&&d===Symbol.asyncIterator)return()=>e;if("then"===d){const c=a({type:"GET",callPath:b});return b=[],Promise.resolve(c).then.bind(c)}return b.push(d),e},set(c,d,e){return a({type:"SET",callPath:b,property:d,value:e})}})}function n(a){return u.some((b)=>a instanceof b)}function*o(a){if(!a)return;if("string"==typeof a)return;yield a;let b=Object.values(a);Array.isArray(a)&&(b=a);for(const c of b)yield*o(c)}function p(a){const b=[];for(const c of o(a))n(c)&&b.push(c);return b}function q(a){return a&&a[v]}function r(a,b=null){if(b)return{type:"ERROR",error:"stack"in b?b.stack:b.toString()};if(q(a)){const{port1:b,port2:d}=new MessageChannel;return c(a,b),{type:"PROXY",endpoint:d}}return{type:"OBJECT",obj:a}}const s=Math.floor(Math.random()*Number.MAX_SAFE_INTEGER);let t=0;const u=[ArrayBuffer,MessagePort],v=Symbol("proxyValue"),w="__omg_so_secret";return{proxy:a,proxyValue:b,expose:c}}()}); | ||
(function(a){if("object"==typeof module&&"object"==typeof module.exports){var b=a(require,exports);b!==void 0&&(module.exports=b)}else"function"==typeof define&&define.amd&&define(["require","exports"],a)})(function(a,b){"use strict";Object.defineProperty(b,"__esModule",{value:!0}),b.Comlink=function(){function a(a){if(n(a)&&(a=h(a)),!i(a))throw Error("endpoint does not have all of addEventListener, removeEventListener and postMessage defined");return j(a),p(async(b)=>{let c=[];("APPLY"===b.type||"CONSTRUCT"===b.type)&&(c=b.argumentsList.map(d));const f=await o(a,Object.assign({},b,{argumentsList:c}),r(c)),g=f.data;return e(g.value)})}function b(a){return a[w]=!0,a}function c(a,c){if(n(c)&&(c=h(c)),!i(c))throw Error("endpoint does not have all of addEventListener, removeEventListener and postMessage defined");j(c),k(c,async function(d){if(!d.data.id)return;const f=d.data;let g=await f.callPath.slice(0,-1).reduce((a,b)=>a[b],a),h=await f.callPath.reduce((a,b)=>a[b],a),i=h,j=[];if(("APPLY"===f.type||"CONSTRUCT"===f.type)&&(j=f.argumentsList.map(e)),"APPLY"===f.type)try{i=await h.apply(g,j)}catch(a){i=a,i[x]=!0}if("CONSTRUCT"===f.type)try{i=new h(...j),i=b(i)}catch(a){i=a,i[x]=!0}return"SET"===f.type&&(h[f.property]=f.value,i=!0),i=t(i),i.id=f.id,c.postMessage(i,r([i]))})}function d(a){for(const[b,c]of y.entries())if(c.canHandle(a))return{type:b,value:c.serialize(a)};let b=[];for(const c of s(a))for(const[a,d]of y.entries())d.canHandle(c.value)&&b.push({path:c.path,wrappedValue:{type:a,value:c.value}});return{type:"RAW",value:a,wrappedChildren:b}}function e(a){if(y.has(a.type)){const b=y.get(a.type);return b.deserialize(a.value)}if(g(a)){for(const b of a.wrappedChildren||[]){if(!y.has(b.wrappedValue.type))throw Error(`Unknown value type "${a.type}" at ${b.path.join(".")}`);const c=y.get(b.wrappedValue.type),d=c.deserialize(b.wrappedValue.value);f(a.value,b.path,d)}return a.value}throw Error(`Unknown value type "${a.type}"`)}function f(a,b,c){const d=b.slice(-1)[0],e=b.slice(0,-1).reduce((a,b)=>a[b],a);e[d]=c}function g(a){return"RAW"===a.type}function h(a){if("Window"!==self.constructor.name)throw Error("self is not a window");return{addEventListener:self.addEventListener.bind(self),removeEventListener:self.removeEventListener.bind(self),postMessage:(b,c)=>a.postMessage(b,"*",c)}}function i(a){return"addEventListener"in a&&"removeEventListener"in a&&"postMessage"in a}function j(a){m(a)&&a.start()}function k(a,b){a.addEventListener("message",b)}function l(a,b){a.removeEventListener("message",b)}function m(a){return"MessagePort"===a.constructor.name}function n(a){return["window","length","location","parent","opener"].every((b)=>b in a)}function o(a,b,c){const d=`${v}-${z++}`;return new Promise((e)=>{k(a,function b(c){c.data.id!==d||(l(a,b),e(c))}),b=Object.assign({},b,{id:d}),a.postMessage(b,c)})}function p(a,b=[]){return new Proxy(function(){},{construct(c,d){return a({type:"CONSTRUCT",callPath:b,argumentsList:d})},apply(c,d,e){return"bind"===b[b.length-1]?p(a,b.slice(0,-1)):a({type:"APPLY",callPath:b,argumentsList:e})},get(c,d,e){if("then"===d&&0===b.length)return{then:()=>e};if("then"===d){const c=a({type:"GET",callPath:b});return Promise.resolve(c).then.bind(c)}return p(a,b.concat(d))},set(c,d,e){return a({type:"SET",callPath:b,property:d,value:e})}})}function q(a){return u.some((b)=>a instanceof b)}function*s(a,b=[],c=null){if(a&&(c||(c=new WeakSet),!c.has(a))&&"string"!=typeof a){"object"==typeof a&&c.add(a),yield{value:a,path:b};let d=Object.keys(a);for(const e of d)yield*s(a[e],[...b,e],c)}}function r(a){const b=[];for(const c of s(a))q(c.value)&&b.push(c.value);return b}function t(a){for(const[b,c]of y.entries())if(c.canHandle(a)){const d=c.serialize(a);return{value:{type:b,value:d}}}return{value:{type:"RAW",value:a}}}const u=[ArrayBuffer,MessagePort],v=Math.floor(Math.random()*Number.MAX_SAFE_INTEGER),w=Symbol("proxyValue"),x=Symbol("throw"),y=new Map([["PROXY",{canHandle:(a)=>a&&a[w],serialize:(a)=>{const{port1:b,port2:d}=new MessageChannel;return c(a,b),d},deserialize:(b)=>{return a(b)}}],["THROW",{canHandle:(a)=>a&&a[x],serialize:(a)=>a.toString()+"\n"+a.stack,deserialize:(a)=>{throw Error(a)}}]]);let z=0;return{proxy:a,proxyValue:b,transferHandlers:y,expose:c}}()}); |
{ | ||
"name": "comlinkjs", | ||
"version": "2.3.0", | ||
"version": "2.3.1", | ||
"description": "", | ||
@@ -9,3 +9,3 @@ "main": "comlink.umd.js", | ||
"scripts": { | ||
"test": "npm run linter && npm run unittest", | ||
"test": "npm run linter && npm run unittest && npm run build", | ||
"unittest": "karma start", | ||
@@ -15,7 +15,7 @@ "linter": "eslint comlink.ts && eslint messagechanneladapter.ts", | ||
"watchtestharmony": "karma start --no-single-run --browsers ChromeCanaryHeadlessHarmony", | ||
"version": "sed -i .bak -E 's!comlinkjs@[0-9.]+!comlinkjs@'${npm_package_version}'!' README.md && git add README.md", | ||
"version": "sed -i.bak -e 's!comlinkjs@[0-9.]+!comlinkjs@'${npm_package_version}'!' README.md && git add README.md", | ||
"mypublish": "npm run build && npm run test && cp README.md package.json dist && npm publish dist", | ||
"build": "rm -rf dist && mkdir dist && npm run compile && npm run mangle_global && npm run minify", | ||
"compile": "tsc --outDir dist -m none && mv dist/comlink.{,global.}js && mv dist/messagechanneladapter.{,global.}js && tsc --outDir dist -m es2015 && mv dist/comlink.{,es6.}js && mv dist/messagechanneladapter.{,es6.}js && tsc -d --outDir dist -m umd && mv dist/comlink.{,umd.}js && mv dist/messagechanneladapter.{,umd.}js", | ||
"mangle_global": "sed -i .bak 's!exports.Comlink!self.Comlink!' dist/comlink.global.js && sed -i .bak 's!^.*\"__esModule\".*$!!' dist/comlink.global.js && sed -i .bak 's!exports.MessageChannelAdapter!self.MessageChannelAdapter!' dist/messagechanneladapter.global.js && sed -i .bak 's!^.*\"__esModule\".*$!!' dist/messagechanneladapter.global.js", | ||
"mangle_global": "sed -i.bak -e 's!exports.Comlink!self.Comlink!' dist/comlink.global.js && sed -i.bak 's!^.*\"__esModule\".*$!!' dist/comlink.global.js && sed -i.bak -e 's!exports.MessageChannelAdapter!self.MessageChannelAdapter!' dist/messagechanneladapter.global.js && sed -i.bak -e 's!^.*\"__esModule\".*$!!' dist/messagechanneladapter.global.js", | ||
"minify": "babili -o dist/comlink.global.{min.,}js && babili -o dist/comlink.es6.{min.,}js && babili -o dist/comlink.umd.{min.,}js && babili -o dist/messagechanneladapter.global.{min.,}js && babili -o dist/messagechanneladapter.es6.{min.,}js && babili -o dist/messagechanneladapter.umd.{min.,}js" | ||
@@ -42,3 +42,3 @@ }, | ||
"chai": "4.1.2", | ||
"eslint": "4.8.0", | ||
"eslint": "4.13.1", | ||
"eslint-config-google": "0.9.1", | ||
@@ -48,11 +48,11 @@ "karma": "1.7.1", | ||
"karma-chrome-launcher": "2.2.0", | ||
"karma-firefox-launcher": "1.0.1", | ||
"karma-firefox-launcher": "1.1.0", | ||
"karma-mocha": "1.3.0", | ||
"karma-safari-launcher": "1.0.0", | ||
"karma-typescript": "3.0.7", | ||
"mocha": "4.0.0", | ||
"typescript": "2.5.3", | ||
"typescript-eslint-parser": "8.0.0" | ||
"karma-typescript": "3.0.8", | ||
"mocha": "4.0.1", | ||
"typescript": "2.6.2", | ||
"typescript-eslint-parser": "11.0.0" | ||
}, | ||
"dependencies": {} | ||
} |
@@ -26,3 +26,3 @@ # Comlink | ||
**Size**: ~3.1k, ~1.3k gzip’d. | ||
**Size**: ~4.0k, ~1.6k gzip’d. | ||
@@ -118,3 +118,3 @@ ## Example | ||
### `expose(rootObj, endpoint)` | ||
### `Comlink.expose(rootObj, endpoint)` | ||
@@ -126,3 +126,3 @@ `expose` is the counter-part to `proxy`. It listens for RPC messages on | ||
### `proxyValue(value)` | ||
### `Comlink.proxyValue(value)` | ||
@@ -150,2 +150,51 @@ If structurally cloning a value is undesired (either for a function parameter or | ||
# TransferHandler | ||
Some types are neither transferable not structurally cloneable and can therefore | ||
not be `postMessage`’d. To remedy this, a `TransferHandler` offers a hook into the | ||
serialization and deserialization process to allow these types to be used with | ||
Comlink. `TransferHandler`s must fulfill the following interface: | ||
- `canHandle(obj)`: Should `true` if this `TransferHandler` is capable of | ||
(de)serializing the given object. | ||
- `serialize(obj)`: Serializes `obj` to something structurally cloneable. | ||
- `deserialize(obj)`: The inverse of `serialize`. | ||
## Example | ||
One example would be that using an instance of a class as a parameter to a remote | ||
function will invoke the function with a simple JSON object. The prototype gets | ||
lost when the instance gets structurally cloned. Let’s say the class | ||
`ComplexNumber` is used for some calculations. To make sure instances | ||
of `ComplexNumber` are handled correctly, the following `TransferHandler` can be | ||
used: | ||
```js | ||
const complexNumberTransferHandler = { | ||
canHandle(obj) { | ||
return obj instanceof ComplexNumber; | ||
}, | ||
serialize(obj) { | ||
return {re: obj.re, im: obj.im}; | ||
} | ||
deserialize(obj) { | ||
return new ComplexNumber(obj.re, obj.im); | ||
} | ||
}; | ||
``` | ||
This new `TransferHandler` can be registered with Comlink like this: | ||
```js | ||
Comlink.transferHandlers.set('COMPLEX', complexNumberTransferHandler); | ||
``` | ||
The string can be arbitrary but must be unique across all `TransferHandler`s. | ||
**Note:** The `TransferHandler` must be registered on _both_ sides of the | ||
Comlink channel. | ||
To see a more generic example see the [EventListener example] or the | ||
[Classes example]. | ||
# MessageChannelAdapter | ||
@@ -179,4 +228,6 @@ | ||
[PresentationConnection]: https://developer.mozilla.org/en-US/docs/Web/API/PresentationConnection | ||
[EventListener example]: https://github.com/GoogleChromeLabs/comlink/tree/master/docs/examples/eventlistener | ||
[Classes example]: https://github.com/GoogleChromeLabs/comlink/tree/master/docs/examples/classes | ||
--- | ||
License Apache-2.0 |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
91329
1312
229