@agoric/eventual-send
Advanced tools
Comparing version 0.7.0 to 0.8.0
@@ -6,2 +6,26 @@ # Change Log | ||
# [0.8.0](https://github.com/Agoric/agoric-sdk/compare/@agoric/eventual-send@0.8.0-alpha.0...@agoric/eventual-send@0.8.0) (2020-04-13) | ||
**Note:** Version bump only for package @agoric/eventual-send | ||
# [0.8.0-alpha.0](https://github.com/Agoric/agoric-sdk/compare/@agoric/eventual-send@0.7.0...@agoric/eventual-send@0.8.0-alpha.0) (2020-04-12) | ||
### Bug Fixes | ||
* shorten HandledPromises to propagate handlers ([2ed50d2](https://github.com/Agoric/agoric-sdk/commit/2ed50d24c1b80959748bcaf0d04f1c4cd25f4242)) | ||
### Features | ||
* add the returnedP as the last argument to the handler ([1f83d99](https://github.com/Agoric/agoric-sdk/commit/1f83d994f48b659f3c49c4b5eb2b50ea7bb7b7a3)) | ||
# [0.7.0](https://github.com/Agoric/agoric-sdk/compare/@agoric/eventual-send@0.7.0-alpha.0...@agoric/eventual-send@0.7.0) (2020-04-02) | ||
@@ -8,0 +32,0 @@ |
@@ -130,4 +130,5 @@ 'use strict'; | ||
let presenceToPromise; | ||
let promiseToHandler; | ||
let promiseToUnsettledHandler; | ||
let promiseToPresence; // only for HandledPromise.unwrap | ||
let forwardedPromiseToPromise; // forwarding, union-find-ish | ||
function ensureMaps() { | ||
@@ -137,7 +138,52 @@ if (!presenceToHandler) { | ||
presenceToPromise = new WeakMap(); | ||
promiseToHandler = new WeakMap(); | ||
promiseToUnsettledHandler = new WeakMap(); | ||
promiseToPresence = new WeakMap(); | ||
forwardedPromiseToPromise = new WeakMap(); | ||
} | ||
} | ||
/** | ||
* You can imagine a forest of trees in which the roots of each tree is an | ||
* unresolved HandledPromise or a non-Promise, and each node's parent is the | ||
* HandledPromise to which it was forwarded. We maintain that mapping of | ||
* forwarded HandledPromise to its resolution in forwardedPromiseToPromise. | ||
* | ||
* We use something like the description of "Find" with "Path splitting" | ||
* to propagate changes down to the children efficiently: | ||
* https://en.wikipedia.org/wiki/Disjoint-set_data_structure | ||
* | ||
* @param {*} target Any value. | ||
* @returns {*} If the target was a HandledPromise, the most-resolved parent of it, otherwise the target. | ||
*/ | ||
function shorten(target) { | ||
let p = target; | ||
// Find the most-resolved value for p. | ||
while (forwardedPromiseToPromise.has(p)) { | ||
p = forwardedPromiseToPromise.get(p); | ||
} | ||
const presence = promiseToPresence.get(p); | ||
if (presence) { | ||
// Presences are final, so it is ok to propagate | ||
// this upstream. | ||
while (target !== p) { | ||
const parent = forwardedPromiseToPromise.get(target); | ||
forwardedPromiseToPromise.delete(target); | ||
promiseToUnsettledHandler.delete(target); | ||
promiseToPresence.set(target, presence); | ||
target = parent; | ||
} | ||
} else { | ||
// We propagate p and remove all other unsettled handlers | ||
// upstream. | ||
// Note that everything except presences is covered here. | ||
while (target !== p) { | ||
const parent = forwardedPromiseToPromise.get(target); | ||
forwardedPromiseToPromise.set(target, p); | ||
promiseToUnsettledHandler.delete(target); | ||
target = parent; | ||
} | ||
} | ||
return target; | ||
} | ||
// This special handler accepts Promises, and forwards | ||
@@ -149,3 +195,3 @@ // handled Promises to their corresponding fulfilledHandler. | ||
function HandledPromise(executor, unfulfilledHandler = undefined) { | ||
function HandledPromise(executor, unsettledHandler = undefined) { | ||
if (new.target === undefined) { | ||
@@ -156,43 +202,76 @@ throw new Error('must be invoked with "new"'); | ||
let handledReject; | ||
let fulfilled = false; | ||
const superExecutor = (resolve, reject) => { | ||
let resolved = false; | ||
let resolvedTarget = null; | ||
let handledP; | ||
let continueForwarding = () => {}; | ||
const superExecutor = (superResolve, superReject) => { | ||
handledResolve = value => { | ||
fulfilled = true; | ||
resolve(value); | ||
if (resolved) { | ||
return resolvedTarget; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
value = shorten(value); | ||
let targetP; | ||
if ( | ||
promiseToUnsettledHandler.has(value) || | ||
promiseToPresence.has(value) | ||
) { | ||
targetP = value; | ||
} else { | ||
// We're resolving to a non-promise, so remove our handler. | ||
promiseToUnsettledHandler.delete(handledP); | ||
targetP = presenceToPromise.get(value); | ||
} | ||
// Ensure our data structure is a propert tree (avoid cycles). | ||
if (targetP && targetP !== handledP) { | ||
forwardedPromiseToPromise.set(handledP, targetP); | ||
} else { | ||
forwardedPromiseToPromise.delete(handledP); | ||
} | ||
// Remove stale unsettled handlers, set to canonical form. | ||
shorten(handledP); | ||
// Ensure our unsettledHandler is cleaned up if not already. | ||
if (promiseToUnsettledHandler.has(handledP)) { | ||
handledP.then(_ => promiseToUnsettledHandler.delete(handledP)); | ||
} | ||
// Finish the resolution. | ||
superResolve(value); | ||
resolved = true; | ||
resolvedTarget = value; | ||
// We're resolved, so forward any postponed operations to us. | ||
continueForwarding(); | ||
return resolvedTarget; | ||
}; | ||
handledReject = err => { | ||
fulfilled = true; | ||
reject(err); | ||
if (resolved) { | ||
return; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
promiseToUnsettledHandler.delete(handledP); | ||
resolved = true; | ||
superReject(err); | ||
continueForwarding(); | ||
}; | ||
}; | ||
const handledP = harden( | ||
Reflect.construct(Promise, [superExecutor], new.target), | ||
); | ||
handledP = harden(Reflect.construct(Promise, [superExecutor], new.target)); | ||
ensureMaps(); | ||
let continueForwarding = () => {}; | ||
if (!unfulfilledHandler) { | ||
// Create a simple unfulfilledHandler that just postpones until the | ||
const makePostponedHandler = () => { | ||
// Create a simple postponedHandler that just postpones until the | ||
// fulfilledHandler is set. | ||
// | ||
// This is insufficient for actual remote handled Promises | ||
// (too many round-trips), but is an easy way to create a | ||
// local handled Promise. | ||
const interlockP = new Promise((resolve, reject) => { | ||
continueForwarding = (err = null, targetP = undefined) => { | ||
if (err !== null) { | ||
reject(err); | ||
return; | ||
} | ||
// Box the target promise so that it isn't further resolved. | ||
resolve([targetP]); | ||
// Return undefined. | ||
}; | ||
let donePostponing; | ||
const interlockP = new Promise(resolve => { | ||
donePostponing = () => resolve(); | ||
}); | ||
// A failed interlock should not be recorded as an unhandled rejection. | ||
// It will bubble up to the HandledPromise itself. | ||
interlockP.catch(_ => {}); | ||
const makePostponed = postponedOperation => { | ||
const makePostponedOperation = postponedOperation => { | ||
// Just wait until the handler is resolved/rejected. | ||
@@ -203,8 +282,5 @@ return function postpone(x, ...args) { | ||
interlockP | ||
.then(([targetP]) => { | ||
.then(_ => { | ||
// If targetP is a handled promise, use it, otherwise x. | ||
const nextPromise = targetP || x; | ||
resolve( | ||
HandledPromise[postponedOperation](nextPromise, ...args), | ||
); | ||
resolve(HandledPromise[postponedOperation](x, ...args)); | ||
}) | ||
@@ -216,6 +292,14 @@ .catch(reject); | ||
unfulfilledHandler = { | ||
get: makePostponed('get'), | ||
applyMethod: makePostponed('applyMethod'), | ||
const postponedHandler = { | ||
get: makePostponedOperation('get'), | ||
applyMethod: makePostponedOperation('applyMethod'), | ||
}; | ||
return [postponedHandler, donePostponing]; | ||
}; | ||
if (!unsettledHandler) { | ||
// This is insufficient for actual remote handled Promises | ||
// (too many round-trips), but is an easy way to create a | ||
// local handled Promise. | ||
[unsettledHandler, continueForwarding] = makePostponedHandler(); | ||
} | ||
@@ -228,20 +312,24 @@ | ||
}; | ||
validateHandler(unfulfilledHandler); | ||
validateHandler(unsettledHandler); | ||
// Until the handled promise is resolved, we use the unfulfilledHandler. | ||
promiseToHandler.set(handledP, unfulfilledHandler); | ||
// Until the handled promise is resolved, we use the unsettledHandler. | ||
promiseToUnsettledHandler.set(handledP, unsettledHandler); | ||
const rejectHandled = reason => { | ||
if (fulfilled) { | ||
if (resolved) { | ||
return; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
handledReject(reason); | ||
continueForwarding(reason); | ||
}; | ||
let resolvedPresence = null; | ||
const resolveWithPresence = presenceHandler => { | ||
if (fulfilled) { | ||
return resolvedPresence; | ||
if (resolved) { | ||
return resolvedTarget; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
try { | ||
@@ -252,21 +340,15 @@ // Sanity checks. | ||
// Validate and install our mapped target (i.e. presence). | ||
resolvedPresence = Object.create(null); | ||
resolvedTarget = Object.create(null); | ||
// Create table entries for the presence mapped to the | ||
// fulfilledHandler. | ||
presenceToPromise.set(resolvedPresence, handledP); | ||
promiseToPresence.set(handledP, resolvedPresence); | ||
presenceToHandler.set(resolvedPresence, presenceHandler); | ||
presenceToPromise.set(resolvedTarget, handledP); | ||
promiseToPresence.set(handledP, resolvedTarget); | ||
presenceToHandler.set(resolvedTarget, presenceHandler); | ||
// Remove the mapping, as our presenceHandler should be | ||
// used instead. | ||
promiseToHandler.delete(handledP); | ||
// We committed to this presence, so resolve. | ||
handledResolve(resolvedPresence); | ||
continueForwarding(); | ||
return resolvedPresence; | ||
handledResolve(resolvedTarget); | ||
return resolvedTarget; | ||
} catch (e) { | ||
handledReject(e); | ||
continueForwarding(); | ||
throw e; | ||
@@ -277,5 +359,8 @@ } | ||
const resolveHandled = async (target, deprecatedPresenceHandler) => { | ||
if (fulfilled) { | ||
return undefined; | ||
if (resolved) { | ||
return; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
try { | ||
@@ -288,33 +373,7 @@ if (deprecatedPresenceHandler) { | ||
// Resolve with the target when it's ready. | ||
// Resolve the target. | ||
handledResolve(target); | ||
const existingUnfulfilledHandler = promiseToHandler.get(target); | ||
if (existingUnfulfilledHandler) { | ||
// Reuse the unfulfilled handler. | ||
promiseToHandler.set(handledP, existingUnfulfilledHandler); | ||
return continueForwarding(null, target); | ||
} | ||
// See if the target is a presence we already know of. | ||
let presence; | ||
try { | ||
presence = HandledPromise.unwrap(target); | ||
} catch (e) { | ||
presence = await target; | ||
} | ||
const existingPresenceHandler = presenceToHandler.get(presence); | ||
if (existingPresenceHandler) { | ||
promiseToHandler.set(handledP, existingPresenceHandler); | ||
promiseToPresence.set(handledP, presence); | ||
return continueForwarding(null, handledP); | ||
} | ||
// Remove the mapping, as we don't need a handler. | ||
promiseToHandler.delete(handledP); | ||
return continueForwarding(); | ||
} catch (e) { | ||
handledReject(e); | ||
} | ||
return continueForwarding(); | ||
}; | ||
@@ -457,10 +516,8 @@ | ||
handle = (p, operation, ...args) => { | ||
handle = (p, operation, ...opArgs) => { | ||
ensureMaps(); | ||
const unfulfilledHandler = promiseToHandler.get(p); | ||
p = shorten(p); | ||
const unsettledHandler = promiseToUnsettledHandler.get(p); | ||
let executor; | ||
if ( | ||
unfulfilledHandler && | ||
typeof unfulfilledHandler[operation] === 'function' | ||
) { | ||
if (unsettledHandler && typeof unsettledHandler[operation] === 'function') { | ||
executor = (resolve, reject) => { | ||
@@ -470,4 +527,7 @@ // We run in a future turn to prevent synchronous attacks, | ||
.then(() => | ||
// and resolve to the answer from the specific unfulfilled handler, | ||
resolve(unfulfilledHandler[operation](p, ...args)), | ||
// and resolve to the answer from the specific unsettled handler, | ||
// opArgs are something like [prop] or [method, args], | ||
// so we don't risk the user's args leaking into this expansion. | ||
// eslint-disable-next-line no-use-before-define | ||
resolve(unsettledHandler[operation](p, ...opArgs, returnedP)), | ||
) | ||
@@ -488,3 +548,6 @@ .catch(reject); | ||
// and resolve to the forwardingHandler's operation. | ||
resolve(forwardingHandler[operation](o, ...args)); | ||
// opArgs are something like [prop] or [method, args], | ||
// so we don't risk the user's args leaking into this expansion. | ||
// eslint-disable-next-line no-use-before-define | ||
resolve(forwardingHandler[operation](o, ...opArgs, returnedP)); | ||
}) | ||
@@ -495,6 +558,7 @@ .catch(reject); | ||
// We return a handled promise with the default unfulfilled handler. | ||
// We return a handled promise with the default unsettled handler. | ||
// This prevents a race between the above Promise.resolves and | ||
// pipelining. | ||
return new HandledPromise(executor); | ||
const returnedP = new HandledPromise(executor); | ||
return returnedP; | ||
}; | ||
@@ -501,0 +565,0 @@ |
@@ -124,4 +124,5 @@ import harden from '@agoric/harden'; | ||
let presenceToPromise; | ||
let promiseToHandler; | ||
let promiseToUnsettledHandler; | ||
let promiseToPresence; // only for HandledPromise.unwrap | ||
let forwardedPromiseToPromise; // forwarding, union-find-ish | ||
function ensureMaps() { | ||
@@ -131,7 +132,52 @@ if (!presenceToHandler) { | ||
presenceToPromise = new WeakMap(); | ||
promiseToHandler = new WeakMap(); | ||
promiseToUnsettledHandler = new WeakMap(); | ||
promiseToPresence = new WeakMap(); | ||
forwardedPromiseToPromise = new WeakMap(); | ||
} | ||
} | ||
/** | ||
* You can imagine a forest of trees in which the roots of each tree is an | ||
* unresolved HandledPromise or a non-Promise, and each node's parent is the | ||
* HandledPromise to which it was forwarded. We maintain that mapping of | ||
* forwarded HandledPromise to its resolution in forwardedPromiseToPromise. | ||
* | ||
* We use something like the description of "Find" with "Path splitting" | ||
* to propagate changes down to the children efficiently: | ||
* https://en.wikipedia.org/wiki/Disjoint-set_data_structure | ||
* | ||
* @param {*} target Any value. | ||
* @returns {*} If the target was a HandledPromise, the most-resolved parent of it, otherwise the target. | ||
*/ | ||
function shorten(target) { | ||
let p = target; | ||
// Find the most-resolved value for p. | ||
while (forwardedPromiseToPromise.has(p)) { | ||
p = forwardedPromiseToPromise.get(p); | ||
} | ||
const presence = promiseToPresence.get(p); | ||
if (presence) { | ||
// Presences are final, so it is ok to propagate | ||
// this upstream. | ||
while (target !== p) { | ||
const parent = forwardedPromiseToPromise.get(target); | ||
forwardedPromiseToPromise.delete(target); | ||
promiseToUnsettledHandler.delete(target); | ||
promiseToPresence.set(target, presence); | ||
target = parent; | ||
} | ||
} else { | ||
// We propagate p and remove all other unsettled handlers | ||
// upstream. | ||
// Note that everything except presences is covered here. | ||
while (target !== p) { | ||
const parent = forwardedPromiseToPromise.get(target); | ||
forwardedPromiseToPromise.set(target, p); | ||
promiseToUnsettledHandler.delete(target); | ||
target = parent; | ||
} | ||
} | ||
return target; | ||
} | ||
// This special handler accepts Promises, and forwards | ||
@@ -143,3 +189,3 @@ // handled Promises to their corresponding fulfilledHandler. | ||
function HandledPromise(executor, unfulfilledHandler = undefined) { | ||
function HandledPromise(executor, unsettledHandler = undefined) { | ||
if (new.target === undefined) { | ||
@@ -150,43 +196,76 @@ throw new Error('must be invoked with "new"'); | ||
let handledReject; | ||
let fulfilled = false; | ||
const superExecutor = (resolve, reject) => { | ||
let resolved = false; | ||
let resolvedTarget = null; | ||
let handledP; | ||
let continueForwarding = () => {}; | ||
const superExecutor = (superResolve, superReject) => { | ||
handledResolve = value => { | ||
fulfilled = true; | ||
resolve(value); | ||
if (resolved) { | ||
return resolvedTarget; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
value = shorten(value); | ||
let targetP; | ||
if ( | ||
promiseToUnsettledHandler.has(value) || | ||
promiseToPresence.has(value) | ||
) { | ||
targetP = value; | ||
} else { | ||
// We're resolving to a non-promise, so remove our handler. | ||
promiseToUnsettledHandler.delete(handledP); | ||
targetP = presenceToPromise.get(value); | ||
} | ||
// Ensure our data structure is a propert tree (avoid cycles). | ||
if (targetP && targetP !== handledP) { | ||
forwardedPromiseToPromise.set(handledP, targetP); | ||
} else { | ||
forwardedPromiseToPromise.delete(handledP); | ||
} | ||
// Remove stale unsettled handlers, set to canonical form. | ||
shorten(handledP); | ||
// Ensure our unsettledHandler is cleaned up if not already. | ||
if (promiseToUnsettledHandler.has(handledP)) { | ||
handledP.then(_ => promiseToUnsettledHandler.delete(handledP)); | ||
} | ||
// Finish the resolution. | ||
superResolve(value); | ||
resolved = true; | ||
resolvedTarget = value; | ||
// We're resolved, so forward any postponed operations to us. | ||
continueForwarding(); | ||
return resolvedTarget; | ||
}; | ||
handledReject = err => { | ||
fulfilled = true; | ||
reject(err); | ||
if (resolved) { | ||
return; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
promiseToUnsettledHandler.delete(handledP); | ||
resolved = true; | ||
superReject(err); | ||
continueForwarding(); | ||
}; | ||
}; | ||
const handledP = harden( | ||
Reflect.construct(Promise, [superExecutor], new.target), | ||
); | ||
handledP = harden(Reflect.construct(Promise, [superExecutor], new.target)); | ||
ensureMaps(); | ||
let continueForwarding = () => {}; | ||
if (!unfulfilledHandler) { | ||
// Create a simple unfulfilledHandler that just postpones until the | ||
const makePostponedHandler = () => { | ||
// Create a simple postponedHandler that just postpones until the | ||
// fulfilledHandler is set. | ||
// | ||
// This is insufficient for actual remote handled Promises | ||
// (too many round-trips), but is an easy way to create a | ||
// local handled Promise. | ||
const interlockP = new Promise((resolve, reject) => { | ||
continueForwarding = (err = null, targetP = undefined) => { | ||
if (err !== null) { | ||
reject(err); | ||
return; | ||
} | ||
// Box the target promise so that it isn't further resolved. | ||
resolve([targetP]); | ||
// Return undefined. | ||
}; | ||
let donePostponing; | ||
const interlockP = new Promise(resolve => { | ||
donePostponing = () => resolve(); | ||
}); | ||
// A failed interlock should not be recorded as an unhandled rejection. | ||
// It will bubble up to the HandledPromise itself. | ||
interlockP.catch(_ => {}); | ||
const makePostponed = postponedOperation => { | ||
const makePostponedOperation = postponedOperation => { | ||
// Just wait until the handler is resolved/rejected. | ||
@@ -197,8 +276,5 @@ return function postpone(x, ...args) { | ||
interlockP | ||
.then(([targetP]) => { | ||
.then(_ => { | ||
// If targetP is a handled promise, use it, otherwise x. | ||
const nextPromise = targetP || x; | ||
resolve( | ||
HandledPromise[postponedOperation](nextPromise, ...args), | ||
); | ||
resolve(HandledPromise[postponedOperation](x, ...args)); | ||
}) | ||
@@ -210,6 +286,14 @@ .catch(reject); | ||
unfulfilledHandler = { | ||
get: makePostponed('get'), | ||
applyMethod: makePostponed('applyMethod'), | ||
const postponedHandler = { | ||
get: makePostponedOperation('get'), | ||
applyMethod: makePostponedOperation('applyMethod'), | ||
}; | ||
return [postponedHandler, donePostponing]; | ||
}; | ||
if (!unsettledHandler) { | ||
// This is insufficient for actual remote handled Promises | ||
// (too many round-trips), but is an easy way to create a | ||
// local handled Promise. | ||
[unsettledHandler, continueForwarding] = makePostponedHandler(); | ||
} | ||
@@ -222,20 +306,24 @@ | ||
}; | ||
validateHandler(unfulfilledHandler); | ||
validateHandler(unsettledHandler); | ||
// Until the handled promise is resolved, we use the unfulfilledHandler. | ||
promiseToHandler.set(handledP, unfulfilledHandler); | ||
// Until the handled promise is resolved, we use the unsettledHandler. | ||
promiseToUnsettledHandler.set(handledP, unsettledHandler); | ||
const rejectHandled = reason => { | ||
if (fulfilled) { | ||
if (resolved) { | ||
return; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
handledReject(reason); | ||
continueForwarding(reason); | ||
}; | ||
let resolvedPresence = null; | ||
const resolveWithPresence = presenceHandler => { | ||
if (fulfilled) { | ||
return resolvedPresence; | ||
if (resolved) { | ||
return resolvedTarget; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
try { | ||
@@ -246,21 +334,15 @@ // Sanity checks. | ||
// Validate and install our mapped target (i.e. presence). | ||
resolvedPresence = Object.create(null); | ||
resolvedTarget = Object.create(null); | ||
// Create table entries for the presence mapped to the | ||
// fulfilledHandler. | ||
presenceToPromise.set(resolvedPresence, handledP); | ||
promiseToPresence.set(handledP, resolvedPresence); | ||
presenceToHandler.set(resolvedPresence, presenceHandler); | ||
presenceToPromise.set(resolvedTarget, handledP); | ||
promiseToPresence.set(handledP, resolvedTarget); | ||
presenceToHandler.set(resolvedTarget, presenceHandler); | ||
// Remove the mapping, as our presenceHandler should be | ||
// used instead. | ||
promiseToHandler.delete(handledP); | ||
// We committed to this presence, so resolve. | ||
handledResolve(resolvedPresence); | ||
continueForwarding(); | ||
return resolvedPresence; | ||
handledResolve(resolvedTarget); | ||
return resolvedTarget; | ||
} catch (e) { | ||
handledReject(e); | ||
continueForwarding(); | ||
throw e; | ||
@@ -271,5 +353,8 @@ } | ||
const resolveHandled = async (target, deprecatedPresenceHandler) => { | ||
if (fulfilled) { | ||
return undefined; | ||
if (resolved) { | ||
return; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
try { | ||
@@ -282,33 +367,7 @@ if (deprecatedPresenceHandler) { | ||
// Resolve with the target when it's ready. | ||
// Resolve the target. | ||
handledResolve(target); | ||
const existingUnfulfilledHandler = promiseToHandler.get(target); | ||
if (existingUnfulfilledHandler) { | ||
// Reuse the unfulfilled handler. | ||
promiseToHandler.set(handledP, existingUnfulfilledHandler); | ||
return continueForwarding(null, target); | ||
} | ||
// See if the target is a presence we already know of. | ||
let presence; | ||
try { | ||
presence = HandledPromise.unwrap(target); | ||
} catch (e) { | ||
presence = await target; | ||
} | ||
const existingPresenceHandler = presenceToHandler.get(presence); | ||
if (existingPresenceHandler) { | ||
promiseToHandler.set(handledP, existingPresenceHandler); | ||
promiseToPresence.set(handledP, presence); | ||
return continueForwarding(null, handledP); | ||
} | ||
// Remove the mapping, as we don't need a handler. | ||
promiseToHandler.delete(handledP); | ||
return continueForwarding(); | ||
} catch (e) { | ||
handledReject(e); | ||
} | ||
return continueForwarding(); | ||
}; | ||
@@ -451,10 +510,8 @@ | ||
handle = (p, operation, ...args) => { | ||
handle = (p, operation, ...opArgs) => { | ||
ensureMaps(); | ||
const unfulfilledHandler = promiseToHandler.get(p); | ||
p = shorten(p); | ||
const unsettledHandler = promiseToUnsettledHandler.get(p); | ||
let executor; | ||
if ( | ||
unfulfilledHandler && | ||
typeof unfulfilledHandler[operation] === 'function' | ||
) { | ||
if (unsettledHandler && typeof unsettledHandler[operation] === 'function') { | ||
executor = (resolve, reject) => { | ||
@@ -464,4 +521,7 @@ // We run in a future turn to prevent synchronous attacks, | ||
.then(() => | ||
// and resolve to the answer from the specific unfulfilled handler, | ||
resolve(unfulfilledHandler[operation](p, ...args)), | ||
// and resolve to the answer from the specific unsettled handler, | ||
// opArgs are something like [prop] or [method, args], | ||
// so we don't risk the user's args leaking into this expansion. | ||
// eslint-disable-next-line no-use-before-define | ||
resolve(unsettledHandler[operation](p, ...opArgs, returnedP)), | ||
) | ||
@@ -482,3 +542,6 @@ .catch(reject); | ||
// and resolve to the forwardingHandler's operation. | ||
resolve(forwardingHandler[operation](o, ...args)); | ||
// opArgs are something like [prop] or [method, args], | ||
// so we don't risk the user's args leaking into this expansion. | ||
// eslint-disable-next-line no-use-before-define | ||
resolve(forwardingHandler[operation](o, ...opArgs, returnedP)); | ||
}) | ||
@@ -489,6 +552,7 @@ .catch(reject); | ||
// We return a handled promise with the default unfulfilled handler. | ||
// We return a handled promise with the default unsettled handler. | ||
// This prevents a race between the above Promise.resolves and | ||
// pipelining. | ||
return new HandledPromise(executor); | ||
const returnedP = new HandledPromise(executor); | ||
return returnedP; | ||
}; | ||
@@ -495,0 +559,0 @@ |
@@ -130,4 +130,5 @@ (function (global, factory) { | ||
let presenceToPromise; | ||
let promiseToHandler; | ||
let promiseToUnsettledHandler; | ||
let promiseToPresence; // only for HandledPromise.unwrap | ||
let forwardedPromiseToPromise; // forwarding, union-find-ish | ||
function ensureMaps() { | ||
@@ -137,7 +138,52 @@ if (!presenceToHandler) { | ||
presenceToPromise = new WeakMap(); | ||
promiseToHandler = new WeakMap(); | ||
promiseToUnsettledHandler = new WeakMap(); | ||
promiseToPresence = new WeakMap(); | ||
forwardedPromiseToPromise = new WeakMap(); | ||
} | ||
} | ||
/** | ||
* You can imagine a forest of trees in which the roots of each tree is an | ||
* unresolved HandledPromise or a non-Promise, and each node's parent is the | ||
* HandledPromise to which it was forwarded. We maintain that mapping of | ||
* forwarded HandledPromise to its resolution in forwardedPromiseToPromise. | ||
* | ||
* We use something like the description of "Find" with "Path splitting" | ||
* to propagate changes down to the children efficiently: | ||
* https://en.wikipedia.org/wiki/Disjoint-set_data_structure | ||
* | ||
* @param {*} target Any value. | ||
* @returns {*} If the target was a HandledPromise, the most-resolved parent of it, otherwise the target. | ||
*/ | ||
function shorten(target) { | ||
let p = target; | ||
// Find the most-resolved value for p. | ||
while (forwardedPromiseToPromise.has(p)) { | ||
p = forwardedPromiseToPromise.get(p); | ||
} | ||
const presence = promiseToPresence.get(p); | ||
if (presence) { | ||
// Presences are final, so it is ok to propagate | ||
// this upstream. | ||
while (target !== p) { | ||
const parent = forwardedPromiseToPromise.get(target); | ||
forwardedPromiseToPromise.delete(target); | ||
promiseToUnsettledHandler.delete(target); | ||
promiseToPresence.set(target, presence); | ||
target = parent; | ||
} | ||
} else { | ||
// We propagate p and remove all other unsettled handlers | ||
// upstream. | ||
// Note that everything except presences is covered here. | ||
while (target !== p) { | ||
const parent = forwardedPromiseToPromise.get(target); | ||
forwardedPromiseToPromise.set(target, p); | ||
promiseToUnsettledHandler.delete(target); | ||
target = parent; | ||
} | ||
} | ||
return target; | ||
} | ||
// This special handler accepts Promises, and forwards | ||
@@ -149,3 +195,3 @@ // handled Promises to their corresponding fulfilledHandler. | ||
function HandledPromise(executor, unfulfilledHandler = undefined) { | ||
function HandledPromise(executor, unsettledHandler = undefined) { | ||
if (new.target === undefined) { | ||
@@ -156,43 +202,76 @@ throw new Error('must be invoked with "new"'); | ||
let handledReject; | ||
let fulfilled = false; | ||
const superExecutor = (resolve, reject) => { | ||
let resolved = false; | ||
let resolvedTarget = null; | ||
let handledP; | ||
let continueForwarding = () => {}; | ||
const superExecutor = (superResolve, superReject) => { | ||
handledResolve = value => { | ||
fulfilled = true; | ||
resolve(value); | ||
if (resolved) { | ||
return resolvedTarget; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
value = shorten(value); | ||
let targetP; | ||
if ( | ||
promiseToUnsettledHandler.has(value) || | ||
promiseToPresence.has(value) | ||
) { | ||
targetP = value; | ||
} else { | ||
// We're resolving to a non-promise, so remove our handler. | ||
promiseToUnsettledHandler.delete(handledP); | ||
targetP = presenceToPromise.get(value); | ||
} | ||
// Ensure our data structure is a propert tree (avoid cycles). | ||
if (targetP && targetP !== handledP) { | ||
forwardedPromiseToPromise.set(handledP, targetP); | ||
} else { | ||
forwardedPromiseToPromise.delete(handledP); | ||
} | ||
// Remove stale unsettled handlers, set to canonical form. | ||
shorten(handledP); | ||
// Ensure our unsettledHandler is cleaned up if not already. | ||
if (promiseToUnsettledHandler.has(handledP)) { | ||
handledP.then(_ => promiseToUnsettledHandler.delete(handledP)); | ||
} | ||
// Finish the resolution. | ||
superResolve(value); | ||
resolved = true; | ||
resolvedTarget = value; | ||
// We're resolved, so forward any postponed operations to us. | ||
continueForwarding(); | ||
return resolvedTarget; | ||
}; | ||
handledReject = err => { | ||
fulfilled = true; | ||
reject(err); | ||
if (resolved) { | ||
return; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
promiseToUnsettledHandler.delete(handledP); | ||
resolved = true; | ||
superReject(err); | ||
continueForwarding(); | ||
}; | ||
}; | ||
const handledP = harden( | ||
Reflect.construct(Promise, [superExecutor], new.target), | ||
); | ||
handledP = harden(Reflect.construct(Promise, [superExecutor], new.target)); | ||
ensureMaps(); | ||
let continueForwarding = () => {}; | ||
if (!unfulfilledHandler) { | ||
// Create a simple unfulfilledHandler that just postpones until the | ||
const makePostponedHandler = () => { | ||
// Create a simple postponedHandler that just postpones until the | ||
// fulfilledHandler is set. | ||
// | ||
// This is insufficient for actual remote handled Promises | ||
// (too many round-trips), but is an easy way to create a | ||
// local handled Promise. | ||
const interlockP = new Promise((resolve, reject) => { | ||
continueForwarding = (err = null, targetP = undefined) => { | ||
if (err !== null) { | ||
reject(err); | ||
return; | ||
} | ||
// Box the target promise so that it isn't further resolved. | ||
resolve([targetP]); | ||
// Return undefined. | ||
}; | ||
let donePostponing; | ||
const interlockP = new Promise(resolve => { | ||
donePostponing = () => resolve(); | ||
}); | ||
// A failed interlock should not be recorded as an unhandled rejection. | ||
// It will bubble up to the HandledPromise itself. | ||
interlockP.catch(_ => {}); | ||
const makePostponed = postponedOperation => { | ||
const makePostponedOperation = postponedOperation => { | ||
// Just wait until the handler is resolved/rejected. | ||
@@ -203,8 +282,5 @@ return function postpone(x, ...args) { | ||
interlockP | ||
.then(([targetP]) => { | ||
.then(_ => { | ||
// If targetP is a handled promise, use it, otherwise x. | ||
const nextPromise = targetP || x; | ||
resolve( | ||
HandledPromise[postponedOperation](nextPromise, ...args), | ||
); | ||
resolve(HandledPromise[postponedOperation](x, ...args)); | ||
}) | ||
@@ -216,6 +292,14 @@ .catch(reject); | ||
unfulfilledHandler = { | ||
get: makePostponed('get'), | ||
applyMethod: makePostponed('applyMethod'), | ||
const postponedHandler = { | ||
get: makePostponedOperation('get'), | ||
applyMethod: makePostponedOperation('applyMethod'), | ||
}; | ||
return [postponedHandler, donePostponing]; | ||
}; | ||
if (!unsettledHandler) { | ||
// This is insufficient for actual remote handled Promises | ||
// (too many round-trips), but is an easy way to create a | ||
// local handled Promise. | ||
[unsettledHandler, continueForwarding] = makePostponedHandler(); | ||
} | ||
@@ -228,20 +312,24 @@ | ||
}; | ||
validateHandler(unfulfilledHandler); | ||
validateHandler(unsettledHandler); | ||
// Until the handled promise is resolved, we use the unfulfilledHandler. | ||
promiseToHandler.set(handledP, unfulfilledHandler); | ||
// Until the handled promise is resolved, we use the unsettledHandler. | ||
promiseToUnsettledHandler.set(handledP, unsettledHandler); | ||
const rejectHandled = reason => { | ||
if (fulfilled) { | ||
if (resolved) { | ||
return; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
handledReject(reason); | ||
continueForwarding(reason); | ||
}; | ||
let resolvedPresence = null; | ||
const resolveWithPresence = presenceHandler => { | ||
if (fulfilled) { | ||
return resolvedPresence; | ||
if (resolved) { | ||
return resolvedTarget; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
try { | ||
@@ -252,21 +340,15 @@ // Sanity checks. | ||
// Validate and install our mapped target (i.e. presence). | ||
resolvedPresence = Object.create(null); | ||
resolvedTarget = Object.create(null); | ||
// Create table entries for the presence mapped to the | ||
// fulfilledHandler. | ||
presenceToPromise.set(resolvedPresence, handledP); | ||
promiseToPresence.set(handledP, resolvedPresence); | ||
presenceToHandler.set(resolvedPresence, presenceHandler); | ||
presenceToPromise.set(resolvedTarget, handledP); | ||
promiseToPresence.set(handledP, resolvedTarget); | ||
presenceToHandler.set(resolvedTarget, presenceHandler); | ||
// Remove the mapping, as our presenceHandler should be | ||
// used instead. | ||
promiseToHandler.delete(handledP); | ||
// We committed to this presence, so resolve. | ||
handledResolve(resolvedPresence); | ||
continueForwarding(); | ||
return resolvedPresence; | ||
handledResolve(resolvedTarget); | ||
return resolvedTarget; | ||
} catch (e) { | ||
handledReject(e); | ||
continueForwarding(); | ||
throw e; | ||
@@ -277,5 +359,8 @@ } | ||
const resolveHandled = async (target, deprecatedPresenceHandler) => { | ||
if (fulfilled) { | ||
return undefined; | ||
if (resolved) { | ||
return; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
try { | ||
@@ -288,33 +373,7 @@ if (deprecatedPresenceHandler) { | ||
// Resolve with the target when it's ready. | ||
// Resolve the target. | ||
handledResolve(target); | ||
const existingUnfulfilledHandler = promiseToHandler.get(target); | ||
if (existingUnfulfilledHandler) { | ||
// Reuse the unfulfilled handler. | ||
promiseToHandler.set(handledP, existingUnfulfilledHandler); | ||
return continueForwarding(null, target); | ||
} | ||
// See if the target is a presence we already know of. | ||
let presence; | ||
try { | ||
presence = HandledPromise.unwrap(target); | ||
} catch (e) { | ||
presence = await target; | ||
} | ||
const existingPresenceHandler = presenceToHandler.get(presence); | ||
if (existingPresenceHandler) { | ||
promiseToHandler.set(handledP, existingPresenceHandler); | ||
promiseToPresence.set(handledP, presence); | ||
return continueForwarding(null, handledP); | ||
} | ||
// Remove the mapping, as we don't need a handler. | ||
promiseToHandler.delete(handledP); | ||
return continueForwarding(); | ||
} catch (e) { | ||
handledReject(e); | ||
} | ||
return continueForwarding(); | ||
}; | ||
@@ -457,10 +516,8 @@ | ||
handle = (p, operation, ...args) => { | ||
handle = (p, operation, ...opArgs) => { | ||
ensureMaps(); | ||
const unfulfilledHandler = promiseToHandler.get(p); | ||
p = shorten(p); | ||
const unsettledHandler = promiseToUnsettledHandler.get(p); | ||
let executor; | ||
if ( | ||
unfulfilledHandler && | ||
typeof unfulfilledHandler[operation] === 'function' | ||
) { | ||
if (unsettledHandler && typeof unsettledHandler[operation] === 'function') { | ||
executor = (resolve, reject) => { | ||
@@ -470,4 +527,7 @@ // We run in a future turn to prevent synchronous attacks, | ||
.then(() => | ||
// and resolve to the answer from the specific unfulfilled handler, | ||
resolve(unfulfilledHandler[operation](p, ...args)), | ||
// and resolve to the answer from the specific unsettled handler, | ||
// opArgs are something like [prop] or [method, args], | ||
// so we don't risk the user's args leaking into this expansion. | ||
// eslint-disable-next-line no-use-before-define | ||
resolve(unsettledHandler[operation](p, ...opArgs, returnedP)), | ||
) | ||
@@ -488,3 +548,6 @@ .catch(reject); | ||
// and resolve to the forwardingHandler's operation. | ||
resolve(forwardingHandler[operation](o, ...args)); | ||
// opArgs are something like [prop] or [method, args], | ||
// so we don't risk the user's args leaking into this expansion. | ||
// eslint-disable-next-line no-use-before-define | ||
resolve(forwardingHandler[operation](o, ...opArgs, returnedP)); | ||
}) | ||
@@ -495,6 +558,7 @@ .catch(reject); | ||
// We return a handled promise with the default unfulfilled handler. | ||
// We return a handled promise with the default unsettled handler. | ||
// This prevents a race between the above Promise.resolves and | ||
// pipelining. | ||
return new HandledPromise(executor); | ||
const returnedP = new HandledPromise(executor); | ||
return returnedP; | ||
}; | ||
@@ -501,0 +565,0 @@ |
{ | ||
"name": "@agoric/eventual-send", | ||
"version": "0.7.0", | ||
"version": "0.8.0", | ||
"description": "Extend a Promise class to implement the eventual-send API", | ||
@@ -48,3 +48,3 @@ "main": "dist/eventual-send.cjs.js", | ||
}, | ||
"gitHead": "536b8e521e881357ca2945533b027ab0bd4b85e5" | ||
"gitHead": "a5fe2624fedcf3b8adf46ed6c157c29fd459b2ed" | ||
} |
266
src/index.js
@@ -55,4 +55,5 @@ /* global HandledPromise */ | ||
let presenceToPromise; | ||
let promiseToHandler; | ||
let promiseToUnsettledHandler; | ||
let promiseToPresence; // only for HandledPromise.unwrap | ||
let forwardedPromiseToPromise; // forwarding, union-find-ish | ||
function ensureMaps() { | ||
@@ -62,7 +63,52 @@ if (!presenceToHandler) { | ||
presenceToPromise = new WeakMap(); | ||
promiseToHandler = new WeakMap(); | ||
promiseToUnsettledHandler = new WeakMap(); | ||
promiseToPresence = new WeakMap(); | ||
forwardedPromiseToPromise = new WeakMap(); | ||
} | ||
} | ||
/** | ||
* You can imagine a forest of trees in which the roots of each tree is an | ||
* unresolved HandledPromise or a non-Promise, and each node's parent is the | ||
* HandledPromise to which it was forwarded. We maintain that mapping of | ||
* forwarded HandledPromise to its resolution in forwardedPromiseToPromise. | ||
* | ||
* We use something like the description of "Find" with "Path splitting" | ||
* to propagate changes down to the children efficiently: | ||
* https://en.wikipedia.org/wiki/Disjoint-set_data_structure | ||
* | ||
* @param {*} target Any value. | ||
* @returns {*} If the target was a HandledPromise, the most-resolved parent of it, otherwise the target. | ||
*/ | ||
function shorten(target) { | ||
let p = target; | ||
// Find the most-resolved value for p. | ||
while (forwardedPromiseToPromise.has(p)) { | ||
p = forwardedPromiseToPromise.get(p); | ||
} | ||
const presence = promiseToPresence.get(p); | ||
if (presence) { | ||
// Presences are final, so it is ok to propagate | ||
// this upstream. | ||
while (target !== p) { | ||
const parent = forwardedPromiseToPromise.get(target); | ||
forwardedPromiseToPromise.delete(target); | ||
promiseToUnsettledHandler.delete(target); | ||
promiseToPresence.set(target, presence); | ||
target = parent; | ||
} | ||
} else { | ||
// We propagate p and remove all other unsettled handlers | ||
// upstream. | ||
// Note that everything except presences is covered here. | ||
while (target !== p) { | ||
const parent = forwardedPromiseToPromise.get(target); | ||
forwardedPromiseToPromise.set(target, p); | ||
promiseToUnsettledHandler.delete(target); | ||
target = parent; | ||
} | ||
} | ||
return target; | ||
} | ||
// This special handler accepts Promises, and forwards | ||
@@ -74,3 +120,3 @@ // handled Promises to their corresponding fulfilledHandler. | ||
function HandledPromise(executor, unfulfilledHandler = undefined) { | ||
function HandledPromise(executor, unsettledHandler = undefined) { | ||
if (new.target === undefined) { | ||
@@ -81,43 +127,76 @@ throw new Error('must be invoked with "new"'); | ||
let handledReject; | ||
let fulfilled = false; | ||
const superExecutor = (resolve, reject) => { | ||
let resolved = false; | ||
let resolvedTarget = null; | ||
let handledP; | ||
let continueForwarding = () => {}; | ||
const superExecutor = (superResolve, superReject) => { | ||
handledResolve = value => { | ||
fulfilled = true; | ||
resolve(value); | ||
if (resolved) { | ||
return resolvedTarget; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
value = shorten(value); | ||
let targetP; | ||
if ( | ||
promiseToUnsettledHandler.has(value) || | ||
promiseToPresence.has(value) | ||
) { | ||
targetP = value; | ||
} else { | ||
// We're resolving to a non-promise, so remove our handler. | ||
promiseToUnsettledHandler.delete(handledP); | ||
targetP = presenceToPromise.get(value); | ||
} | ||
// Ensure our data structure is a propert tree (avoid cycles). | ||
if (targetP && targetP !== handledP) { | ||
forwardedPromiseToPromise.set(handledP, targetP); | ||
} else { | ||
forwardedPromiseToPromise.delete(handledP); | ||
} | ||
// Remove stale unsettled handlers, set to canonical form. | ||
shorten(handledP); | ||
// Ensure our unsettledHandler is cleaned up if not already. | ||
if (promiseToUnsettledHandler.has(handledP)) { | ||
handledP.then(_ => promiseToUnsettledHandler.delete(handledP)); | ||
} | ||
// Finish the resolution. | ||
superResolve(value); | ||
resolved = true; | ||
resolvedTarget = value; | ||
// We're resolved, so forward any postponed operations to us. | ||
continueForwarding(); | ||
return resolvedTarget; | ||
}; | ||
handledReject = err => { | ||
fulfilled = true; | ||
reject(err); | ||
if (resolved) { | ||
return; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
promiseToUnsettledHandler.delete(handledP); | ||
resolved = true; | ||
superReject(err); | ||
continueForwarding(); | ||
}; | ||
}; | ||
const handledP = harden( | ||
Reflect.construct(Promise, [superExecutor], new.target), | ||
); | ||
handledP = harden(Reflect.construct(Promise, [superExecutor], new.target)); | ||
ensureMaps(); | ||
let continueForwarding = () => {}; | ||
if (!unfulfilledHandler) { | ||
// Create a simple unfulfilledHandler that just postpones until the | ||
const makePostponedHandler = () => { | ||
// Create a simple postponedHandler that just postpones until the | ||
// fulfilledHandler is set. | ||
// | ||
// This is insufficient for actual remote handled Promises | ||
// (too many round-trips), but is an easy way to create a | ||
// local handled Promise. | ||
const interlockP = new Promise((resolve, reject) => { | ||
continueForwarding = (err = null, targetP = undefined) => { | ||
if (err !== null) { | ||
reject(err); | ||
return; | ||
} | ||
// Box the target promise so that it isn't further resolved. | ||
resolve([targetP]); | ||
// Return undefined. | ||
}; | ||
let donePostponing; | ||
const interlockP = new Promise(resolve => { | ||
donePostponing = () => resolve(); | ||
}); | ||
// A failed interlock should not be recorded as an unhandled rejection. | ||
// It will bubble up to the HandledPromise itself. | ||
interlockP.catch(_ => {}); | ||
const makePostponed = postponedOperation => { | ||
const makePostponedOperation = postponedOperation => { | ||
// Just wait until the handler is resolved/rejected. | ||
@@ -128,8 +207,5 @@ return function postpone(x, ...args) { | ||
interlockP | ||
.then(([targetP]) => { | ||
.then(_ => { | ||
// If targetP is a handled promise, use it, otherwise x. | ||
const nextPromise = targetP || x; | ||
resolve( | ||
HandledPromise[postponedOperation](nextPromise, ...args), | ||
); | ||
resolve(HandledPromise[postponedOperation](x, ...args)); | ||
}) | ||
@@ -141,6 +217,14 @@ .catch(reject); | ||
unfulfilledHandler = { | ||
get: makePostponed('get'), | ||
applyMethod: makePostponed('applyMethod'), | ||
const postponedHandler = { | ||
get: makePostponedOperation('get'), | ||
applyMethod: makePostponedOperation('applyMethod'), | ||
}; | ||
return [postponedHandler, donePostponing]; | ||
}; | ||
if (!unsettledHandler) { | ||
// This is insufficient for actual remote handled Promises | ||
// (too many round-trips), but is an easy way to create a | ||
// local handled Promise. | ||
[unsettledHandler, continueForwarding] = makePostponedHandler(); | ||
} | ||
@@ -153,20 +237,24 @@ | ||
}; | ||
validateHandler(unfulfilledHandler); | ||
validateHandler(unsettledHandler); | ||
// Until the handled promise is resolved, we use the unfulfilledHandler. | ||
promiseToHandler.set(handledP, unfulfilledHandler); | ||
// Until the handled promise is resolved, we use the unsettledHandler. | ||
promiseToUnsettledHandler.set(handledP, unsettledHandler); | ||
const rejectHandled = reason => { | ||
if (fulfilled) { | ||
if (resolved) { | ||
return; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
handledReject(reason); | ||
continueForwarding(reason); | ||
}; | ||
let resolvedPresence = null; | ||
const resolveWithPresence = presenceHandler => { | ||
if (fulfilled) { | ||
return resolvedPresence; | ||
if (resolved) { | ||
return resolvedTarget; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
try { | ||
@@ -177,21 +265,15 @@ // Sanity checks. | ||
// Validate and install our mapped target (i.e. presence). | ||
resolvedPresence = Object.create(null); | ||
resolvedTarget = Object.create(null); | ||
// Create table entries for the presence mapped to the | ||
// fulfilledHandler. | ||
presenceToPromise.set(resolvedPresence, handledP); | ||
promiseToPresence.set(handledP, resolvedPresence); | ||
presenceToHandler.set(resolvedPresence, presenceHandler); | ||
presenceToPromise.set(resolvedTarget, handledP); | ||
promiseToPresence.set(handledP, resolvedTarget); | ||
presenceToHandler.set(resolvedTarget, presenceHandler); | ||
// Remove the mapping, as our presenceHandler should be | ||
// used instead. | ||
promiseToHandler.delete(handledP); | ||
// We committed to this presence, so resolve. | ||
handledResolve(resolvedPresence); | ||
continueForwarding(); | ||
return resolvedPresence; | ||
handledResolve(resolvedTarget); | ||
return resolvedTarget; | ||
} catch (e) { | ||
handledReject(e); | ||
continueForwarding(); | ||
throw e; | ||
@@ -202,5 +284,8 @@ } | ||
const resolveHandled = async (target, deprecatedPresenceHandler) => { | ||
if (fulfilled) { | ||
return undefined; | ||
if (resolved) { | ||
return; | ||
} | ||
if (forwardedPromiseToPromise.has(handledP)) { | ||
throw new TypeError('internal: already forwarded'); | ||
} | ||
try { | ||
@@ -213,33 +298,7 @@ if (deprecatedPresenceHandler) { | ||
// Resolve with the target when it's ready. | ||
// Resolve the target. | ||
handledResolve(target); | ||
const existingUnfulfilledHandler = promiseToHandler.get(target); | ||
if (existingUnfulfilledHandler) { | ||
// Reuse the unfulfilled handler. | ||
promiseToHandler.set(handledP, existingUnfulfilledHandler); | ||
return continueForwarding(null, target); | ||
} | ||
// See if the target is a presence we already know of. | ||
let presence; | ||
try { | ||
presence = HandledPromise.unwrap(target); | ||
} catch (e) { | ||
presence = await target; | ||
} | ||
const existingPresenceHandler = presenceToHandler.get(presence); | ||
if (existingPresenceHandler) { | ||
promiseToHandler.set(handledP, existingPresenceHandler); | ||
promiseToPresence.set(handledP, presence); | ||
return continueForwarding(null, handledP); | ||
} | ||
// Remove the mapping, as we don't need a handler. | ||
promiseToHandler.delete(handledP); | ||
return continueForwarding(); | ||
} catch (e) { | ||
handledReject(e); | ||
} | ||
return continueForwarding(); | ||
}; | ||
@@ -382,10 +441,8 @@ | ||
handle = (p, operation, ...args) => { | ||
handle = (p, operation, ...opArgs) => { | ||
ensureMaps(); | ||
const unfulfilledHandler = promiseToHandler.get(p); | ||
p = shorten(p); | ||
const unsettledHandler = promiseToUnsettledHandler.get(p); | ||
let executor; | ||
if ( | ||
unfulfilledHandler && | ||
typeof unfulfilledHandler[operation] === 'function' | ||
) { | ||
if (unsettledHandler && typeof unsettledHandler[operation] === 'function') { | ||
executor = (resolve, reject) => { | ||
@@ -395,4 +452,7 @@ // We run in a future turn to prevent synchronous attacks, | ||
.then(() => | ||
// and resolve to the answer from the specific unfulfilled handler, | ||
resolve(unfulfilledHandler[operation](p, ...args)), | ||
// and resolve to the answer from the specific unsettled handler, | ||
// opArgs are something like [prop] or [method, args], | ||
// so we don't risk the user's args leaking into this expansion. | ||
// eslint-disable-next-line no-use-before-define | ||
resolve(unsettledHandler[operation](p, ...opArgs, returnedP)), | ||
) | ||
@@ -413,3 +473,6 @@ .catch(reject); | ||
// and resolve to the forwardingHandler's operation. | ||
resolve(forwardingHandler[operation](o, ...args)); | ||
// opArgs are something like [prop] or [method, args], | ||
// so we don't risk the user's args leaking into this expansion. | ||
// eslint-disable-next-line no-use-before-define | ||
resolve(forwardingHandler[operation](o, ...opArgs, returnedP)); | ||
}) | ||
@@ -420,6 +483,7 @@ .catch(reject); | ||
// We return a handled promise with the default unfulfilled handler. | ||
// We return a handled promise with the default unsettled handler. | ||
// This prevents a race between the above Promise.resolves and | ||
// pipelining. | ||
return new HandledPromise(executor); | ||
const returnedP = new HandledPromise(executor); | ||
return returnedP; | ||
}; | ||
@@ -426,0 +490,0 @@ |
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
93187
2085