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 427f4f672b..8fa1984a41 100644 --- a/packages/exo/src/exo-tools.js +++ b/packages/exo/src/exo-tools.js @@ -1,4 +1,5 @@ -import { getMethodNames } from '@endo/eventual-send/src/local.js'; +import { getMethodNames } from '@endo/eventual-send/utils.js'; +import { hasOwnPropertyOf } from '@endo/pass-style'; import { E, Far } from '@endo/far'; import { listDifference, diff --git a/packages/exo/src/get-interface.js b/packages/exo/src/get-interface.js index 1d1d894d36..d51364db70 100644 --- a/packages/exo/src/get-interface.js +++ b/packages/exo/src/get-interface.js @@ -4,6 +4,8 @@ * The name of the automatically added default meta-method for * obtaining an exo's interface, if it has one. * + * Intended to be similar to `GET_METHOD_NAMES` from `@endo/pass-style`. + * * TODO Name to be bikeshed. Perhaps even whether it is a * string or symbol to be bikeshed. * @@ -18,6 +20,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/marshal/test/test-marshal-testing.js b/packages/marshal/test/test-marshal-testing.js index 241887b5d8..7721e61efb 100644 --- a/packages/marshal/test/test-marshal-testing.js +++ b/packages/marshal/test/test-marshal-testing.js @@ -1,14 +1,18 @@ import { test } from './prepare-test-env-ava.js'; // eslint-disable-next-line import/order -import { Far, passStyleOf, Remotable } from '@endo/pass-style'; +import { passStyleOf, Remotable } from '@endo/pass-style'; import { makeMarshal } from '../src/marshal.js'; const { create } = Object; -const alice = Far('alice'); -const bob1 = Far('bob'); -const bob2 = Far('bob'); +// Use the lower level `Remotable` rather than `Far` to make an empty +// far object, i.e., one without even the miranda meta methods like +// `GET_METHOD_NAMES`. Such an empty far object should be `t.deepEqual` +// to its remote presences. +const alice = Remotable('Alleged: alice'); +const bob1 = Remotable('Alleged: bob'); +const bob2 = Remotable('Alleged: bob'); const convertValToSlot = val => passStyleOf(val) === 'remotable' ? 'far' : val; 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..45bf1234db 100644 --- a/packages/pass-style/package.json +++ b/packages/pass-style/package.json @@ -33,11 +33,11 @@ "test": "ava" }, "dependencies": { + "@endo/eventual-send": "^0.17.6", "@endo/promise-kit": "^0.2.60", "@fast-check/ava": "^1.1.5" }, "devDependencies": { - "@endo/eventual-send": "^0.17.6", "@endo/init": "^0.5.60", "@endo/ses-ava": "^0.2.44", "ava": "^5.3.0", diff --git a/packages/pass-style/src/make-far.js b/packages/pass-style/src/make-far.js index d77ad60c66..693c743f74 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 `@endo/exo`. + * + * 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',