diff --git a/packages/eventual-send/src/E.js b/packages/eventual-send/src/E.js index 63ae120d84b..591d083cd21 100644 --- a/packages/eventual-send/src/E.js +++ b/packages/eventual-send/src/E.js @@ -68,6 +68,7 @@ export default function makeE(HandledPromise) { E.G = makeEGetterProxy; E.resolve = HandledPromise.resolve; + E.unwrap = HandledPromise.unwrap; return harden(E); } diff --git a/packages/eventual-send/src/index.js b/packages/eventual-send/src/index.js index 103fe7c9ebc..2e9a152152d 100644 --- a/packages/eventual-send/src/index.js +++ b/packages/eventual-send/src/index.js @@ -53,11 +53,13 @@ export function makeHandledPromise(Promise) { let presenceToHandler; let presenceToPromise; let promiseToHandler; + let promiseToPresence; // only for HandledPromise.unwrap function ensureMaps() { if (!presenceToHandler) { presenceToHandler = new WeakMap(); presenceToPromise = new WeakMap(); promiseToHandler = new WeakMap(); + promiseToPresence = new WeakMap(); } } @@ -109,6 +111,9 @@ export function makeHandledPromise(Promise) { // Return undefined. }; }); + // A failed interlock should not be recorded as an unhandled rejection. + // It will bubble up to the HandledPromise itself. + interlockP.catch(_ => {}); const makePostponed = postponedOperation => { // Just wait until the handler is resolved/rejected. @@ -167,6 +172,7 @@ export function makeHandledPromise(Promise) { // Create table entries for the presence mapped to the // fulfilledHandler. presenceToPromise.set(resolvedPresence, handledP); + promiseToPresence.set(handledP, resolvedPresence); presenceToHandler.set(resolvedPresence, presenceHandler); // Remove the mapping, as our presenceHandler should be @@ -206,10 +212,16 @@ export function makeHandledPromise(Promise) { } // See if the target is a presence we already know of. - const presence = await target; + 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); } @@ -284,6 +296,30 @@ export function makeHandledPromise(Promise) { promiseResolve().then(_ => new HandledPromise(executeThen)), ); }, + // TODO verify that this is safe to provide universally, i.e., + // that by itself it doesn't provide access to mutable state in + // ways that violate normal ocap module purity rules. The claim + // that it does not rests on the handled promise itself being + // necessary to perceive this mutable state. In that sense, we + // can think of the right to perceive it, and of access to the + // target, as being in the handled promise. Note that a .then on + // the handled promise will already provide async access to the + // target, so the only additional authorities are: 1) + // synchronous access for handled promises only, and thus 2) the + // ability to tell, from the client side, whether a promise is + // handled. Or, at least, the ability to tell given that the + // promise is already fulfilled. + unwrap(value) { + ensureMaps(); + const pr = presenceToPromise.get(value) || value; + const presence = promiseToPresence.get(pr); + if (!presence) { + throw TypeError( + `Value is not a presence nor a HandledPromise resolved to a presence`, + ); + } + return presence; + }, }); defineProperties(HandledPromise, getOwnPropertyDescriptors(staticMethods)); diff --git a/packages/eventual-send/test/test-e.js b/packages/eventual-send/test/test-e.js index 96ef0db3e3c..45071f95b2b 100644 --- a/packages/eventual-send/test/test-e.js +++ b/packages/eventual-send/test/test-e.js @@ -1,5 +1,16 @@ import test from 'tape-promise/tape'; -import { E } from '../src/index'; +import { E, HandledPromise } from '../src/index'; + +test('E reexports', async t => { + try { + t.equals(E.resolve, HandledPromise.resolve, 'E reexports resolve'); + t.equals(E.unwrap, HandledPromise.unwrap, 'E reexports unwrap'); + } catch (e) { + t.isNot(e, e, 'unexpected exception'); + } finally { + t.end(); + } +}); test('E method calls', async t => { try { diff --git a/packages/eventual-send/test/test-hp.js b/packages/eventual-send/test/test-hp.js index 2cd21b9fd8c..c6ddf71f8f7 100644 --- a/packages/eventual-send/test/test-hp.js +++ b/packages/eventual-send/test/test-hp.js @@ -45,3 +45,88 @@ test('chained properties', async t => { t.end(); } }); + +test('HandledPromise.unwrap', async t => { + try { + t.throws( + () => HandledPromise.unwrap({}), + TypeError, + `unwrapped non-presence throws`, + ); + const p0 = new Promise(_ => {}); + t.throws( + () => HandledPromise.unwrap(p0), + TypeError, + `unwrapped unfulfilled Promise throws`, + ); + const p1 = new Promise(resolve => { + resolve({}); + }); + t.throws( + () => HandledPromise.unwrap(p1), + TypeError, + `unwrapped resolved Promise throws`, + ); + const p2 = new Promise((_, reject) => { + reject(Error('p2')); + }); + // Prevent unhandled promise rejection. + p2.catch(_ => {}); + t.throws( + () => HandledPromise.unwrap(p2), + TypeError, + `unwrapped rejected Promise throws`, + ); + const hp0 = new HandledPromise(_ => {}); + t.throws( + () => HandledPromise.unwrap(hp0), + TypeError, + 'unfulfilled HandledPromise throws', + ); + const hp1 = new HandledPromise(resolve => { + resolve({}); + }); + t.throws( + () => HandledPromise.unwrap(hp1), + TypeError, + 'resolved HandledPromise throws', + ); + const hp2 = new HandledPromise((_, reject) => { + reject(Error('hp2')); + }); + // Prevent unhandled promise rejection. + hp2.catch(_ => {}); + t.throws( + () => HandledPromise.unwrap(hp2), + TypeError, + 'rejected HandledPromise throws', + ); + let presence; + const hp3 = new HandledPromise((_res, _rej, resolveWithPresence) => { + presence = resolveWithPresence({}); + }); + t.equals(typeof presence, 'object', `typeof presence is object`); + t.equals( + HandledPromise.unwrap(hp3), + presence, + `unwrapped HandledPromise is presence`, + ); + t.equals( + HandledPromise.unwrap(presence), + presence, + `unwrapped presence is presence`, + ); + const hp4 = new HandledPromise(resolve => { + resolve(hp3); + }); + t.equals( + HandledPromise.unwrap(hp4), + presence, + `unwrapped forwarded HandledPromise is presence`, + ); + } catch (e) { + t.isNot(e, e, 'unexpected exception'); + } finally { + t.end(); + } +});