Skip to content

Commit 097d5a1

Browse files
authored
Improve eventual types (#2849)
Refs: Agoric/agoric-sdk#11454 ## Description Narrows some eventual-send types. In particular, ensure that the result of an eventual call is a promise. Add an `EResult` type which in the future can represent the return type of an eventual send call where remotables are mapped as necessary to their "brand" type only to avoid the caller from relying on what may otherwise look like a local remotable instead of a presence. This mapping is implemented in the `EAwaitedResult` type for explicit usages, but not yet used in the type of `EResult` itself as it breaks too many consumers of `E` that rely on the currently "incorrect" typing. ### Security Considerations None ### Scaling Considerations None ### Documentation Considerations TBD ### Testing Considerations Integration tested in agoric-sdk: Agoric/agoric-sdk#12065 In a perfect world, I'd like to add some type tests of cases I've encountered. ### Compatibility Considerations None ### Upgrade Considerations Type only changes, which we usually don't consider breaking.
2 parents 9d0cbf4 + 9de3778 commit 097d5a1

File tree

5 files changed

+89
-39
lines changed

5 files changed

+89
-39
lines changed

packages/captp/src/loopback.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { makeFinalizingMap } from './finalize.js';
55

66
export { E };
77

8-
/** @import {ERef} from '@endo/eventual-send' */
8+
/** @import {ERef, EResult} from '@endo/eventual-send' */
99

1010
/**
1111
* Create an async-isolated channel to an object.
@@ -14,9 +14,9 @@ export { E };
1414
* @param {import('./captp.js').CapTPOptions} [nearOptions]
1515
* @param {import('./captp.js').CapTPOptions} [farOptions]
1616
* @returns {{
17-
* makeFar<T>(x: T): ERef<T>,
18-
* makeNear<T>(x: T): ERef<T>,
19-
* makeTrapHandler<T>(x: T): T,
17+
* makeFar<T>(x: T): Promise<EResult<T>>,
18+
* makeNear<T>(x: T): Promise<EResult<T>>,
19+
* makeTrapHandler<T>(name: string, x: T): T,
2020
* isOnlyNear(x: any): boolean,
2121
* isOnlyFar(x: any): boolean,
2222
* getNearStats(): any,
@@ -92,13 +92,14 @@ export const makeLoopback = (ourId, nearOptions, farOptions) => {
9292
refGetter =>
9393
/**
9494
* @param {T} x
95-
* @returns {Promise<T>}
95+
* @returns {Promise<EResult<T>>}
9696
*/
9797
async x => {
9898
lastNonce += 1;
9999
const myNonce = lastNonce;
100100
const val = await x;
101101
nonceToRef.set(myNonce, harden(val));
102+
// @ts-expect-error Type 'T | Awaited<T>' is not assignable to type 'EResult<T>'
102103
return E(refGetter).getRef(myNonce);
103104
};
104105

packages/daemon/src/networks/tcp-netstring.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const protocol = 'tcp+netstring+json+captp0';
1616
export const make = async (powers, context) => {
1717
const { servePort, connectPort } = makeSocketPowers({ net });
1818

19-
const cancelled = E(context).whenCancelled();
19+
const cancelled = /** @type {Promise<never>} */ (E(context).whenCancelled());
2020
const cancelServer = error => E(context).cancel(error);
2121

2222
/** @type {Array<string>} */
@@ -122,7 +122,9 @@ export const make = async (powers, context) => {
122122
const { port: portname, hostname: host } = new URL(address);
123123
const port = Number(portname);
124124

125-
const connectionCancelled = E(connectionContext).whenCancelled();
125+
const connectionCancelled = /** @type {Promise<never>} */ (
126+
E(connectionContext).whenCancelled()
127+
);
126128
const cancelConnection = () => E(connectionContext).cancel();
127129

128130
const {

packages/eventual-send/src/E.js

Lines changed: 76 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const { details: X, quote: q, Fail, error: makeError } = assert;
55
const { assign, freeze } = Object;
66

77
/**
8-
* @import { HandledPromiseConstructor } from './types.js';
8+
* @import { HandledPromiseConstructor, RemotableBrand, Callable, Settler } from './types.js';
99
*/
1010

1111
const onSend = makeMessageBreakpointTester('ENDO_SEND_BREAKPOINTS');
@@ -282,15 +282,15 @@ export default makeE;
282282
*
283283
* @template Primary The type of the primary reference.
284284
* @template [Local=DataOnly<Primary>] The local properties of the object.
285-
* @typedef {ERef<Local & import('./types.js').RemotableBrand<Local, Primary>>} FarRef
285+
* @typedef {ERef<Local & RemotableBrand<Local, Primary>>} FarRef
286286
*/
287287

288288
/**
289289
* `DataOnly<T>` means to return a record type `T2` consisting only of
290290
* properties that are *not* functions.
291291
*
292292
* @template T The type to be filtered.
293-
* @typedef {Omit<T, FilteredKeys<T, import('./types.js').Callable>>} DataOnly
293+
* @typedef {Omit<T, FilteredKeys<T, Callable>>} DataOnly
294294
*/
295295

296296
/**
@@ -304,24 +304,69 @@ export default makeE;
304304

305305
/**
306306
* The awaited return type of a function.
307+
* For the eventual result of an E call, @see {EResult} or @see {ECallableReturn}
307308
*
308309
* @template {(...args: any[]) => any} T
309310
* @typedef {T extends (...args: any[]) => infer R ? Awaited<R> : never} EReturn
310311
*/
311312

312313
/**
313-
* @template {import('./types.js').Callable} T
314+
* An eventual value where remotable objects are recursively mapped to Remote types
315+
*
316+
* @template T
317+
* @typedef {Awaited<T>} EResult
318+
*/
319+
320+
/**
321+
* Experimental type mapping remotable objects to Remote types
322+
*
323+
* @template T
314324
* @typedef {(
315-
* ReturnType<T> extends PromiseLike<infer U> // if function returns a promise
316-
* ? T // return the function
317-
* : (...args: Parameters<T>) => Promise<EReturn<T>> // make it return a promise
325+
* 0 extends (1 & T) // If T is any
326+
* ? T // Propagate the any type through the result
327+
* : T extends RemotableBrand<infer L, infer P> // If we have a Remotable
328+
* ? (P | RemotableBrand<L, P>) // map it to its "maybe remote" form (primary behavior or remotable presence)
329+
* : T extends PromiseLike<infer U> // If T is a promise
330+
* ? Promise<EAwaitedResult<Awaited<T>>> // map its resolution
331+
* : T extends (null | undefined | string | number | boolean | symbol | bigint | Callable) // Intersections of these types with objects are not mapped
332+
* ? T // primitives and non-remotable functions are passed-through
333+
* : T extends object //
334+
* ? { [P in keyof T]: EAwaitedResult<T[P]>; } // other objects are considered copy data and properties mapped
335+
* : T // in case anything wasn't covered, fallback to pass-through
336+
* )} EAwaitedResult
337+
*/
338+
339+
/**
340+
* The @see {EResult} return type of a remote function.
341+
*
342+
* @template {(...args: any[]) => any} T
343+
* @typedef {(
344+
* 0 extends (1 & T) // If T is any
345+
* ? any // Propagate the any type through the result
346+
* : T extends (...args: any[]) => infer R // Else infer the return type
347+
* ? EResult<R> // In the future, map the eventual result
348+
* : never
349+
* )} ECallableReturn
350+
*/
351+
352+
// TODO: Figure out a way to map generic callable return types, or at least better detect them.
353+
// See https://github.com/microsoft/TypeScript/issues/61838. Without that, `E(startGovernedUpgradable)`
354+
// in agoric-sdk doesn't propagate the start function type.
355+
/**
356+
* Maps a callable to its remotely called type
357+
*
358+
* @template {Callable} T
359+
* @typedef {(
360+
* ReturnType<T> extends PromiseLike<infer U> // Check if callable returns a promise
361+
* ? T // Bypass mapping to maintain any generic
362+
* : (...args: Parameters<T>) => Promise<ECallableReturn<T>> // Map it anyway to ensure promise return type
318363
* )} ECallable
319364
*/
320365

321366
/**
322367
* @template T
323368
* @typedef {{
324-
* readonly [P in keyof T]: T[P] extends import('./types.js').Callable
369+
* readonly [P in keyof T]: T[P] extends Callable
325370
* ? ECallable<T[P]>
326371
* : never;
327372
* }} EMethods
@@ -337,14 +382,14 @@ export default makeE;
337382
*/
338383

339384
/**
340-
* @template {import('./types.js').Callable} T
385+
* @template {Callable} T
341386
* @typedef {(...args: Parameters<T>) => Promise<void>} ESendOnlyCallable
342387
*/
343388

344389
/**
345390
* @template T
346391
* @typedef {{
347-
* readonly [P in keyof T]: T[P] extends import('./types.js').Callable
392+
* readonly [P in keyof T]: T[P] extends Callable
348393
* ? ESendOnlyCallable<T[P]>
349394
* : never;
350395
* }} ESendOnlyMethods
@@ -353,18 +398,22 @@ export default makeE;
353398
/**
354399
* @template T
355400
* @typedef {(
356-
* T extends import('./types.js').Callable
401+
* T extends Callable
357402
* ? ESendOnlyCallable<T> & ESendOnlyMethods<Required<T>>
358-
* : ESendOnlyMethods<Required<T>>
403+
* : 0 extends (1 & T)
404+
* ? never
405+
* : ESendOnlyMethods<Required<T>>
359406
* )} ESendOnlyCallableOrMethods
360407
*/
361408

362409
/**
363410
* @template T
364411
* @typedef {(
365-
* T extends import('./types.js').Callable
412+
* T extends Callable
366413
* ? ECallable<T> & EMethods<Required<T>>
367-
* : EMethods<Required<T>>
414+
* : 0 extends (1 & T)
415+
* ? never
416+
* : EMethods<Required<T>>
368417
* )} ECallableOrMethods
369418
*/
370419

@@ -389,9 +438,9 @@ export default makeE;
389438
*
390439
* @template T
391440
* @typedef {(
392-
* T extends import('./types.js').Callable
441+
* T extends Callable
393442
* ? (...args: Parameters<T>) => ReturnType<T> // a root callable, no methods
394-
* : Pick<T, FilteredKeys<T, import('./types.js').Callable>> // any callable methods
443+
* : Pick<T, FilteredKeys<T, Callable>> // any callable methods
395444
* )} PickCallable
396445
*/
397446

@@ -400,25 +449,21 @@ export default makeE;
400449
*
401450
* @template T
402451
* @typedef {(
403-
* T extends import('./types.js').RemotableBrand<infer L, infer R> // if a given T is some remote interface R
404-
* ? PickCallable<R> // then return the callable properties of R
405-
* : Awaited<T> extends import('./types.js').RemotableBrand<infer L, infer R> // otherwise, if the final resolution of T is some remote interface R
406-
* ? PickCallable<R> // then return the callable properties of R
407-
* : T extends PromiseLike<infer U> // otherwise, if T is a promise
408-
* ? Awaited<T> // then return resolved value T
409-
* : T // otherwise, return T
452+
* T extends RemotableBrand<infer L, infer R> // if a given T is some remote interface R
453+
* ? PickCallable<R> // then return the callable properties of R
454+
* : T extends PromiseLike<infer U> // otherwise, if T is a promise
455+
* ? RemoteFunctions<U> // recurse on the resolved value of T
456+
* : T // otherwise, return T
410457
* )} RemoteFunctions
411458
*/
412459

413460
/**
414461
* @template T
415462
* @typedef {(
416-
* T extends import('./types.js').RemotableBrand<infer L, infer R>
417-
* ? L
418-
* : Awaited<T> extends import('./types.js').RemotableBrand<infer L, infer R>
463+
* T extends RemotableBrand<infer L, infer R>
419464
* ? L
420465
* : T extends PromiseLike<infer U>
421-
* ? Awaited<T>
466+
* ? LocalRecord<U>
422467
* : T
423468
* )} LocalRecord
424469
*/
@@ -427,7 +472,7 @@ export default makeE;
427472
* @template [R = unknown]
428473
* @typedef {{
429474
* promise: Promise<R>;
430-
* settler: import('./types.js').Settler<R>;
475+
* settler: Settler<R>;
431476
* }} EPromiseKit
432477
*/
433478

@@ -438,11 +483,11 @@ export default makeE;
438483
*
439484
* @template T
440485
* @typedef {(
441-
* T extends import('./types.js').Callable
486+
* T extends Callable
442487
* ? (...args: Parameters<T>) => ERef<Awaited<EOnly<ReturnType<T>>>>
443-
* : T extends Record<PropertyKey, import('./types.js').Callable>
488+
* : T extends Record<PropertyKey, Callable>
444489
* ? {
445-
* [K in keyof T]: T[K] extends import('./types.js').Callable
490+
* [K in keyof T]: T[K] extends Callable
446491
* ? (...args: Parameters<T[K]>) => ERef<Awaited<EOnly<ReturnType<T[K]>>>>
447492
* : T[K];
448493
* }

packages/eventual-send/src/exports.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export type {
1313
EProxy,
1414
EOnly,
1515
EReturn,
16+
EResult,
17+
ECallableReturn,
1618
RemoteFunctions,
1719
LocalRecord,
1820
FilteredKeys,

packages/far/src/exports.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { FarRef, ERef, EOnly, EReturn } from '@endo/eventual-send';
1+
export { FarRef, ERef, EOnly, EReturn, EResult } from '@endo/eventual-send';

0 commit comments

Comments
 (0)