feat(eventual-send): provide typing for `Remote<Primary, Local>`
ADD
packages/eventual-send/jsconfig.test.json
+{
+ "extends": "./jsconfig.json",
+ "include": ["test/test-types.js"]
+}

MODIFY
packages/eventual-send/package.json
packages/eventual-send/package.json
"lint-fix": "yarn lint:eslint --fix && yarn lint:types",
"lint-check": "yarn lint",
"lint": "yarn lint:types && yarn lint:eslint",
- "lint:types": "tsc -p jsconfig.json",
+ "lint:types": "tsc -p jsconfig.test.json",
"lint:eslint": "eslint '**/*.js'"
},
"repository": {

MODIFY
packages/eventual-send/src/index.d.ts
packages/eventual-send/src/index.d.ts
@@ -18,6 +18,20 @@ export type EOnly<T> = T extends (...args: infer P) => infer R
>
: ERef<T>;
+type FilteredKeys<T, U> = {
+ [P in keyof T]: T[P] extends U ? P : never;
+}[keyof T];
+
+type DataOnly<T> = Omit<T, FilteredKeys<T, Function>>;
+type FunctionOnly<T> = Pick<T, FilteredKeys<T, Function>> &
+ (T extends Function ? (...args: Parameters<T>) => ReturnType<T> : {});
+
+interface Remotable<T> {
+ __Remote__: T;
+}
+type Remote<Primary, Local = DataOnly<Primary>> = ERef<
+ Local & Remotable<Primary>
+>;
type Unpromise<T> = T extends ERef<infer U> ? U : T;
type Parameters<T> = T extends (...args: infer T) => any ? T : any;
@@ -59,7 +73,7 @@ type ResolveWithPresenceOptionsBag<T extends Object> = {
};
declare interface HandledPromiseStaticMethods {
- resolve<T>(x: T): Promise<Unpromise<T>>;
+ resolve<T>(x: T): Promise<Awaited<T>>;
resolve(): Promise<undefined>;
applyFunction(target: unknown, args: unknown[]): Promise<unknown>;
applyFunctionSendOnly(target: unknown, args: unknown[]): void;
@@ -95,14 +109,14 @@ declare function makeHandledPromise(): HandledPromiseConstructor;
type ESingleMethod<T> = {
readonly [P in keyof T]: (
...args: Parameters<T[P]>
- ) => Promise<Unpromise<ReturnType<T[P]>>>;
+ ) => Promise<Awaited<ReturnType<T[P]>>>;
};
type ESingleCall<T> = T extends Function
- ? ((...args: Parameters<T>) => Promise<Unpromise<ReturnType<T>>>) &
+ ? ((...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>>) &
ESingleMethod<Required<T>>
: ESingleMethod<Required<T>>;
type ESingleGet<T> = {
- readonly [P in keyof T]: Promise<Unpromise<T[P]>>;
+ readonly [P in keyof T]: Promise<Awaited<T[P]>>;
};
/* Same types for send-only. */
@@ -117,7 +131,7 @@ type ESingleGetOnly<T> = {
};
interface ESendOnly {
- <T>(x: T): ESingleCallOnly<Unpromise<T>>;
+ <T>(x: T): ESingleCallOnly<Awaited<T>>;
}
interface EProxy {
@@ -130,7 +144,13 @@ interface EProxy {
* @param {*} x target for method/function call
* @returns {ESingleCall} method/function call proxy
*/
- <T>(x: T): ESingleCall<Unpromise<T>>;
+ <T>(x: T): ESingleCall<
+ T extends Remotable<infer U>
+ ? FunctionOnly<U>
+ : Awaited<T> extends Remotable<infer U>
+ ? FunctionOnly<U>
+ : Awaited<T>
+ >;
/**
* E.get(x) returns a proxy on which you can get arbitrary properties.
@@ -141,13 +161,21 @@ interface EProxy {
* @param {*} x target for property get
* @returns {ESingleGet} property get proxy
*/
- readonly get: <T>(x: T) => ESingleGet<Unpromise<T>>;
+ readonly get: <T>(
+ x: T,
+ ) => ESingleGet<
+ T extends Remotable<infer U>
+ ? DataOnly<U>
+ : Awaited<T> extends Remotable<infer U>
+ ? DataOnly<U>
+ : Awaited<T>
+ >;
/**
* E.resolve(x) converts x to a handled promise. It is
* shorthand for HandledPromise.resolve(x)
*/
- readonly resolve: <T>(x: T) => Promise<Unpromise<T>>;
+ readonly resolve: <T>(x: T) => Promise<Awaited<T>>;
/**
* E.when(x, res, rej) is equivalent to
@@ -155,7 +183,7 @@ interface EProxy {
*/
readonly when: <T, U>(
x: T,
- onfulfilled?: (value: Unpromise<T>) => ERef<U>,
+ onfulfilled?: (value: Awaited<T>) => ERef<U>,
onrejected?: (reason: any) => ERef<U>,
) => Promise<U>;

MODIFY
packages/eventual-send/test/prepare-test-env-ava.js
packages/eventual-send/test/prepare-test-env-ava.js
@@ -3,5 +3,7 @@ import '@endo/lockdown/commit-debug.js';
import { wrapTest } from '@endo/ses-ava';
import rawTest from 'ava';
+export * from 'ava';
+
/** @type {typeof rawTest} */
export const test = wrapTest(rawTest);

ADD
packages/eventual-send/test/test-types.js
+// @ts-check
+import { test } from './prepare-test-env-ava.js';
+
+import { E } from './get-hp.js';
+
+/** @template T @typedef {import('../src/index').ERef<T>} ERef */
+/**
+ * @template Primary
+ * @template [Local=import('../src/index').DataOnly<Primary>]
+ * @typedef {import('@endo/eventual-send').Remote<Primary, Local>} Remote
+ */
+
+/**
+ * @template T
+ * @param {string} _iface
+ * @param {T} value
+ */
+const Far = (_iface, value) => {
+ return /** @type {T & { __Remote__: T }} */ (value);
+};
+
+/**
+ * Check the performance of the legacy ERef type.
+ *
+ * @param {import('./prepare-test-env-ava').ExecutionContext<unknown>} t
+ * @param {ERef<{ bar(): string, baz: number }>} a
+ */
+const foo = async (t, a) => {
+ const { baz } = await a;
+ t.is(baz, 42);
+
+ const bP = E(a).bar(); // bP is a promise for a string
+ t.is(Promise.resolve(bP), bP);
+ t.is(await bP, 'barRet');
+
+ // Should be type error, but isn't.
+ (await a).bar();
+
+ const bazP = E.get(a).baz; // bazP is a promise for a number
+ t.is(Promise.resolve(bazP), bazP);
+ t.is(await bazP, 42);
+
+ // Should be type error, but isn't.
+ const barP = E.get(a).bar;
+ t.is(Promise.resolve(barP), barP);
+ t.is(typeof (await barP), 'function');
+
+ t.is((await a).baz, 42);
+
+ // @ts-expect-error - calling a directly is not typed, but works.
+ a.bar();
+};
+
+/**
+ * Check the performance of Remote<T>.
+ *
+ * @param {import('./prepare-test-env-ava').ExecutionContext<unknown>} t
+ * @param {Remote<{ bar(): string, baz: number }>} a
+ */
+const foo2 = async (t, a) => {
+ const { baz } = await a;
+ t.is(baz, 42);
+
+ const bP = E(a).bar(); // bP is a promise for a string
+ t.is(Promise.resolve(bP), bP);
+ t.is(await bP, 'barRet');
+
+ // @ts-expect-error - awaiting remotes cannot get functions
+ (await a).bar();
+
+ const bazP = E.get(a).baz; // bazP is a promise for a number
+ t.is(Promise.resolve(bazP), bazP);
+ t.is(await bazP, 42);
+
+ // @ts-expect-error - E.get cannot obtain remote functions
+ const barP = E.get(a).bar;
+ t.is(Promise.resolve(barP), barP);
+ t.is(typeof (await barP), 'function');
+
+ t.is((await a).baz, 42);
+
+ // @ts-expect-error - calling a directly is not typed, but works.
+ a.bar();
+};
+
+test('check types', async t => {
+ const f = Far('foo remote', { bar: () => 'barRet', baz: 42 });
+
+ await foo(t, f);
+ await foo2(t, f);
+});

feat(eventual-send): provide typing for `Remote<Primary, Local>`