diff --git a/packages/eventual-send/package.json b/packages/eventual-send/package.json index 232e1a42da..388018c587 100644 --- a/packages/eventual-send/package.json +++ b/packages/eventual-send/package.json @@ -18,6 +18,12 @@ "lint:types": "tsc", "lint:eslint": "eslint '**/*.js'" }, + "exports": { + "./package.json": "./package.json", + ".": "./src/no-shim.js", + "./shim.js": "./shim.js", + "./utils.js": "./utils.js" + }, "repository": { "type": "git", "url": "git+https://github.com/endojs/endo.git" diff --git a/packages/eventual-send/utils.js b/packages/eventual-send/utils.js new file mode 100644 index 0000000000..4fee534df0 --- /dev/null +++ b/packages/eventual-send/utils.js @@ -0,0 +1 @@ +export { getMethodNames } from './src/local.js'; diff --git a/packages/exo/src/exo-tools.js b/packages/exo/src/exo-tools.js index 813659c208..f1e7abfa47 100644 --- a/packages/exo/src/exo-tools.js +++ b/packages/exo/src/exo-tools.js @@ -1,4 +1,4 @@ -import { getMethodNames } from '@endo/eventual-send/src/local.js'; +import { getMethodNames } from '@endo/eventual-send/utils.js'; import { E, Far } from '@endo/far'; import { listDifference, @@ -426,18 +426,20 @@ export const defendPrototype = ( ); } - const getInterfaceGuardMethod = { - [GET_INTERFACE_GUARD]() { - return interfaceGuard; - }, - }[GET_INTERFACE_GUARD]; - prototype[GET_INTERFACE_GUARD] = bindMethod( - `In ${q(GET_INTERFACE_GUARD)} method of (${tag})`, - contextProvider, - getInterfaceGuardMethod, - thisfulMethods, - undefined, - ); + if (interfaceGuard && !(GET_INTERFACE_GUARD in prototype)) { + const getInterfaceGuardMethod = { + [GET_INTERFACE_GUARD]() { + return interfaceGuard; + }, + }[GET_INTERFACE_GUARD]; + prototype[GET_INTERFACE_GUARD] = bindMethod( + `In ${q(GET_INTERFACE_GUARD)} method of (${tag})`, + contextProvider, + getInterfaceGuardMethod, + thisfulMethods, + undefined, + ); + } return Far( tag, diff --git a/packages/exo/src/get-interface.js b/packages/exo/src/get-interface.js index 1d1d894d36..8014070c2c 100644 --- a/packages/exo/src/get-interface.js +++ b/packages/exo/src/get-interface.js @@ -18,6 +18,6 @@ export const GET_INTERFACE_GUARD = Symbol.for('getInterfaceGuard'); * [GET_INTERFACE_GUARD]: () => * import('@endo/patterns').InterfaceGuard<{ * [K in keyof M]: import('@endo/patterns').MethodGuard - * }> | undefined + * }> * }} GetInterfaceGuard */ diff --git a/packages/exo/test/test-exo-wobbly-point.js b/packages/exo/test/test-exo-wobbly-point.js index 84be64755b..5156cf5787 100644 --- a/packages/exo/test/test-exo-wobbly-point.js +++ b/packages/exo/test/test-exo-wobbly-point.js @@ -10,10 +10,9 @@ /* eslint-disable-next-line import/order */ import { test } from './prepare-test-env-ava.js'; -// TODO enable import of getMethodNames without deep import // eslint-disable-next-line import/order -import { getMethodNames } from '@endo/eventual-send/src/local.js'; -import { passStyleOf, Far } from '@endo/pass-style'; +import { getMethodNames } from '@endo/eventual-send/utils.js'; +import { passStyleOf, Far, GET_METHOD_NAMES } from '@endo/pass-style'; import { M } from '@endo/patterns'; import { defineExoClass } from '../src/exo-makers.js'; import { GET_INTERFACE_GUARD } from '../src/get-interface.js'; @@ -102,6 +101,7 @@ test('ExoPoint instances', t => { t.false(pt instanceof ExoPoint); t.deepEqual(getMethodNames(pt), [ GET_INTERFACE_GUARD, + GET_METHOD_NAMES, 'getX', 'getY', 'setY', @@ -154,6 +154,7 @@ test('FarWobblyPoint inheritance', t => { t.is(passStyleOf(wpt), 'remotable'); t.deepEqual(getMethodNames(wpt), [ GET_INTERFACE_GUARD, + GET_METHOD_NAMES, 'getX', 'getY', 'setY', diff --git a/packages/exo/test/test-heap-classes.js b/packages/exo/test/test-heap-classes.js index 91ff0a1ee3..a5ef1346a5 100644 --- a/packages/exo/test/test-heap-classes.js +++ b/packages/exo/test/test-heap-classes.js @@ -172,7 +172,9 @@ test('missing interface', t => { message: 'In "makeSayHello" method of (greeterMaker): result: "[Symbol(passStyle)]" property expected: "[Function ]"', }); - t.is(greeterMaker[GET_INTERFACE_GUARD](), undefined); + t.throws(() => greeterMaker[GET_INTERFACE_GUARD](), { + message: 'greeterMaker[GET_INTERFACE_GUARD] is not a function', + }); }); const SloppyGreeterI = M.interface('greeter', {}, { sloppy: true }); diff --git a/packages/pass-style/index.js b/packages/pass-style/index.js index ef54b29c9f..da985131a2 100644 --- a/packages/pass-style/index.js +++ b/packages/pass-style/index.js @@ -24,7 +24,12 @@ export { export { passStyleOf, assertPassable } from './src/passStyleOf.js'; export { makeTagged } from './src/makeTagged.js'; -export { Remotable, Far, ToFarFunction } from './src/make-far.js'; +export { + Remotable, + Far, + ToFarFunction, + GET_METHOD_NAMES, +} from './src/make-far.js'; export { assertRecord, diff --git a/packages/pass-style/package.json b/packages/pass-style/package.json index 82fc4f8d9e..f0142e5284 100644 --- a/packages/pass-style/package.json +++ b/packages/pass-style/package.json @@ -33,6 +33,7 @@ "test": "ava" }, "dependencies": { + "@endo/eventual-send": "^0.17.6", "@endo/promise-kit": "^0.2.60", "@fast-check/ava": "^1.1.5" }, diff --git a/packages/pass-style/src/make-far.js b/packages/pass-style/src/make-far.js index d77ad60c66..8087380069 100644 --- a/packages/pass-style/src/make-far.js +++ b/packages/pass-style/src/make-far.js @@ -1,5 +1,6 @@ /// +import { getMethodNames } from '@endo/eventual-send/utils.js'; import { assertChecker, PASS_STYLE } from './passStyle-helpers.js'; import { assertIface, getInterfaceOf, RemotableHelper } from './remotable.js'; @@ -128,9 +129,40 @@ export const Remotable = ( }; harden(Remotable); +/** + * The name of the automatically added default meta-method for obtaining a + * list of all methods of an object declared with `Far`, or an object that + * inherits from an object declared with `Far`. + * + * Modeled on `GET_INTERFACE_GUARD` from + * + * TODO Name to be bikeshed. Perhaps even whether it is a + * string or symbol to be bikeshed. + * + * TODO Beware that an exo's interface can change across an upgrade, + * so remotes that cache it can become stale. + */ +export const GET_METHOD_NAMES = Symbol.for('getMethodNames'); + +/** + * Note that the returned method is a thisful method! It must be so that + * it works as expected with far-object inheritance. + * + * @returns {(string|symbol)[]} + */ +const getMethodNamesMethod = harden({ + [GET_METHOD_NAMES]() { + getMethodNames(this); + }, +})[GET_METHOD_NAMES]; + /** * A concise convenience for the most common `Remotable` use. * + * For far objects (as opposed to far functions), also adds a miranda + * `GET_METHOD_NAMES` method that returns an array of all the method names, + * if there is not yet any method named `GET_METHOD_NAMES`. (Hence "miranda") + * * @template {{}} T * @param {string} farName This name will be prepended with `Alleged: ` * for now to form the `Remotable` `iface` argument. @@ -138,6 +170,16 @@ harden(Remotable); */ export const Far = (farName, remotable = undefined) => { const r = remotable === undefined ? /** @type {T} */ ({}) : remotable; + if (typeof r === 'object' && !(GET_METHOD_NAMES in r)) { + // This test excludes far functions, since we currently consider them + // to only have a call-behavior, with no callable methods. + // Beware: Mutates the input argument! But `Remotable` + // * requires the object to be mutable + // * does further mutations, + // * hardens the mutated object before returning it. + // so this mutation is not unprecedented. But it is surprising! + r[GET_METHOD_NAMES] = getMethodNamesMethod; + } return Remotable(`Alleged: ${farName}`, undefined, r); }; harden(Far); diff --git a/packages/pass-style/test/test-far-class-instances.js b/packages/pass-style/test/test-far-class-instances.js index 7b98427ce4..216bb88737 100644 --- a/packages/pass-style/test/test-far-class-instances.js +++ b/packages/pass-style/test/test-far-class-instances.js @@ -2,11 +2,10 @@ /* eslint-disable max-classes-per-file */ import { test } from './prepare-test-env-ava.js'; -// TODO enable import of getMethodNames without deep import // eslint-disable-next-line import/order -import { getMethodNames } from '@endo/eventual-send/src/local.js'; +import { getMethodNames } from '@endo/eventual-send/utils.js'; import { passStyleOf } from '../src/passStyleOf.js'; -import { Far } from '../src/make-far.js'; +import { Far, GET_METHOD_NAMES } from '../src/make-far.js'; /** * Classes whose instances should be Far objects may find it convenient to @@ -48,7 +47,7 @@ class FarSubclass2 extends FarSubclass1 { test('far class instances', t => { const fb = new FarBaseClass(); t.is(passStyleOf(fb), 'remotable'); - t.deepEqual(getMethodNames(fb), ['constructor']); + t.deepEqual(getMethodNames(fb), [GET_METHOD_NAMES, 'constructor']); t.assert(new fb.constructor() instanceof FarBaseClass); t.throws(() => fb.constructor(), { @@ -60,13 +59,18 @@ test('far class instances', t => { t.is(passStyleOf(fs1), 'remotable'); t.is(fs1.double(4), 8); t.assert(new fs1.constructor() instanceof FarSubclass1); - t.deepEqual(getMethodNames(fs1), ['constructor', 'double']); + t.deepEqual(getMethodNames(fs1), [GET_METHOD_NAMES, 'constructor', 'double']); const fs2 = new FarSubclass2(3); t.is(passStyleOf(fs2), 'remotable'); t.is(fs2.double(4), 8); t.is(fs2.doubleAdd(4), 11); - t.deepEqual(getMethodNames(fs2), ['constructor', 'double', 'doubleAdd']); + t.deepEqual(getMethodNames(fs2), [ + GET_METHOD_NAMES, + 'constructor', + 'double', + 'doubleAdd', + ]); const yField = new WeakMap(); class FarSubclass3 extends FarSubclass1 { @@ -84,7 +88,12 @@ test('far class instances', t => { t.is(passStyleOf(fs3), 'remotable'); t.is(fs3.double(4), 8); t.is(fs3.doubleAdd(4), 11); - t.deepEqual(getMethodNames(fs3), ['constructor', 'double', 'doubleAdd']); + t.deepEqual(getMethodNames(fs3), [ + GET_METHOD_NAMES, + 'constructor', + 'double', + 'doubleAdd', + ]); }); test('far class instance hardened empty', t => { diff --git a/packages/pass-style/test/test-far-wobbly-point.js b/packages/pass-style/test/test-far-wobbly-point.js index 34905c093d..3bacc36717 100644 --- a/packages/pass-style/test/test-far-wobbly-point.js +++ b/packages/pass-style/test/test-far-wobbly-point.js @@ -7,11 +7,10 @@ /* eslint-disable max-classes-per-file */ import { test } from './prepare-test-env-ava.js'; -// TODO enable import of getMethodNames without deep import // eslint-disable-next-line import/order -import { getMethodNames } from '@endo/eventual-send/src/local.js'; +import { getMethodNames } from '@endo/eventual-send/utils.js'; import { passStyleOf } from '../src/passStyleOf.js'; -import { Far } from '../src/make-far.js'; +import { Far, GET_METHOD_NAMES } from '../src/make-far.js'; const { apply } = Reflect; @@ -66,6 +65,7 @@ test('FarPoint instances', t => { t.is(passStyleOf(pt), 'remotable'); t.assert(pt instanceof FarPoint); t.deepEqual(getMethodNames(pt), [ + GET_METHOD_NAMES, 'constructor', 'getX', 'getY', @@ -104,6 +104,7 @@ test('FarWobblyPoint inheritance', t => { t.assert(wpt instanceof FarPoint); t.is(passStyleOf(wpt), 'remotable'); t.deepEqual(getMethodNames(wpt), [ + GET_METHOD_NAMES, 'constructor', 'getX', 'getY',