diff --git a/packages/exo/src/exo-makers.js b/packages/exo/src/exo-makers.js index 934b7346cc..7393f5fe5b 100644 --- a/packages/exo/src/exo-makers.js +++ b/packages/exo/src/exo-makers.js @@ -113,6 +113,19 @@ export const initEmpty = () => emptyRecord; * @returns {F} */ +/** + * The power to test if a value is a live instance of the + * associated exo class, or a live facet instance of the + * associated exo class kit. In the later case, if a `facetName` is provided, + * then it tests only whether the argument is a facet instance of that + * facet of the associated exo class kit. + * + * @callback IsLiveInstance + * @param {any} exo + * @param {string} [facetName] + * @returns {boolean} + */ + // TODO Should we split FarClassOptions into distinct types for // class options vs class kit options? After all, `receiveAmplifier` // makes no sense for normal exo classes. @@ -157,6 +170,15 @@ export const initEmpty = () => emptyRecord; * An `Amplify` function is a function that takes a live facet instance of * this class kit as an argument, in which case it will return the facets * record, giving access to all the facet instances of the same cohort. + * + * @property {ReceivePower} [receiveInstanceTester] + * If a `receiveInstanceTester` function is provided, it will be called + * during the definition of the exo class or exo class kit with an + * `IsLiveInstance` function. The first argument of `IsLiveInstance` + * is the value to be tested. When it may be a facet instance of an + * exo class kit, the optional second argument, if provided, is + * a `facetName`. In that case, the function tests only if the first + * argument is an instance of that facet of the associated exo class kit. */ /** @@ -198,6 +220,7 @@ export const defineExoClass = ( finish = undefined, receiveRevoker = undefined, receiveAmplifier = undefined, + receiveInstanceTester = undefined, } = options; receiveAmplifier === undefined || Fail`Only facets of an exo class kit can be amplified ${q(tag)}`; @@ -232,11 +255,23 @@ export const defineExoClass = ( }; if (receiveRevoker) { - const revoke = self => contextMap.delete(self); + const revoke = exo => contextMap.delete(exo); harden(revoke); receiveRevoker(revoke); } + if (receiveInstanceTester) { + const isLiveInstance = (exo, facetName = undefined) => { + facetName === undefined || + Fail`facetName can only be used with an exo class kit: ${q( + tag, + )} has no facet ${q(facetName)}`; + return contextMap.has(exo); + }; + harden(isLiveInstance); + receiveInstanceTester(isLiveInstance); + } + return harden(makeInstance); }; harden(defineExoClass); @@ -268,6 +303,7 @@ export const defineExoClassKit = ( finish = undefined, receiveRevoker = undefined, receiveAmplifier = undefined, + receiveInstanceTester = undefined, } = options; const contextMapKit = objectMap(methodsKit, () => new WeakMap()); const getContextKit = objectMap( @@ -307,26 +343,43 @@ export const defineExoClassKit = ( }; if (receiveRevoker) { - const revoke = aFacet => - values(contextMapKit).some(contextMap => contextMap.delete(aFacet)); + const revoke = exoFacet => + values(contextMapKit).some(contextMap => contextMap.delete(exoFacet)); harden(revoke); receiveRevoker(revoke); } if (receiveAmplifier) { - const amplify = aFacet => { + const amplify = exoFacet => { for (const contextMap of values(contextMapKit)) { - if (contextMap.has(aFacet)) { - const { facets } = contextMap.get(aFacet); + if (contextMap.has(exoFacet)) { + const { facets } = contextMap.get(exoFacet); return facets; } } - throw Fail`Must be an unrevoked facet of ${q(tag)}: ${aFacet}`; + throw Fail`Must be an unrevoked facet of ${q(tag)}: ${exoFacet}`; }; harden(amplify); receiveAmplifier(amplify); } + if (receiveInstanceTester) { + const isLiveInstance = (exoFacet, facetName = undefined) => { + if (facetName === undefined) { + return values(contextMapKit).some(contextMap => + contextMap.has(exoFacet), + ); + } + assert.typeof(facetName, 'string'); + const contextMap = contextMapKit[facetName]; + contextMap !== undefined || + Fail`exo class kit ${q(tag)} has no facet named ${q(facetName)}`; + return contextMap.has(exoFacet); + }; + harden(isLiveInstance); + receiveInstanceTester(isLiveInstance); + } + return harden(makeInstanceKit); }; harden(defineExoClassKit); diff --git a/packages/exo/test/test-live-instance-heap-class-kits.js b/packages/exo/test/test-live-instance-heap-class-kits.js new file mode 100644 index 0000000000..302086a909 --- /dev/null +++ b/packages/exo/test/test-live-instance-heap-class-kits.js @@ -0,0 +1,116 @@ +// modeled on test-revoke-heap-classes.js + +// eslint-disable-next-line import/order +import { test } from './prepare-test-env-ava.js'; + +// eslint-disable-next-line import/order +import { M } from '@endo/patterns'; +import { defineExoClass, defineExoClassKit } from '../src/exo-makers.js'; + +const UpCounterI = M.interface('UpCounter', { + incr: M.call().optional(M.gte(0)).returns(M.number()), +}); + +const DownCounterI = M.interface('DownCounter', { + decr: M.call().optional(M.gte(0)).returns(M.number()), +}); + +test('test revoke defineExoClass', t => { + let revoke; + let isLiveInstance; + const makeUpCounter = defineExoClass( + 'UpCounter', + UpCounterI, + /** @param {number} x */ + (x = 0) => ({ x }), + { + incr(y = 1) { + const { state } = this; + state.x += y; + return state.x; + }, + }, + { + receiveRevoker(r) { + revoke = r; + }, + receiveInstanceTester(i) { + isLiveInstance = i; + }, + }, + ); + t.is(isLiveInstance(harden({})), false); + t.throws(() => isLiveInstance(harden({}), 'up'), { + message: + 'facetName can only be used with an exo class kit: "UpCounter" has no facet "up"', + }); + + const upCounter = makeUpCounter(3); + + t.is(isLiveInstance(upCounter), true); + t.throws(() => isLiveInstance(upCounter, 'up'), { + message: + 'facetName can only be used with an exo class kit: "UpCounter" has no facet "up"', + }); + t.is(revoke(upCounter), true); + t.is(isLiveInstance(upCounter), false); +}); + +test('test amplify defineExoClassKit', t => { + let revoke; + let isLiveInstance; + const makeCounterKit = defineExoClassKit( + 'Counter', + { up: UpCounterI, down: DownCounterI }, + /** @param {number} x */ + (x = 0) => ({ x }), + { + up: { + incr(y = 1) { + const { state } = this; + state.x += y; + return state.x; + }, + }, + down: { + decr(y = 1) { + const { state } = this; + state.x -= y; + return state.x; + }, + }, + }, + { + receiveRevoker(r) { + revoke = r; + }, + receiveInstanceTester(i) { + isLiveInstance = i; + }, + }, + ); + + t.is(isLiveInstance(harden({})), false); + t.is(isLiveInstance(harden({}), 'up'), false); + t.throws(() => isLiveInstance(harden({}), 'foo'), { + message: 'exo class kit "Counter" has no facet named "foo"', + }); + + const { up: upCounter, down: downCounter } = makeCounterKit(3); + + t.is(isLiveInstance(upCounter), true); + t.is(isLiveInstance(upCounter, 'up'), true); + t.is(isLiveInstance(upCounter, 'down'), false); + t.throws(() => isLiveInstance(upCounter, 'foo'), { + message: 'exo class kit "Counter" has no facet named "foo"', + }); + + t.is(revoke(upCounter), true); + + t.is(isLiveInstance(upCounter), false); + t.is(isLiveInstance(upCounter, 'up'), false); + t.is(isLiveInstance(upCounter, 'down'), false); + t.is(isLiveInstance(downCounter), true); + t.is(isLiveInstance(downCounter, 'up'), false); + t.is(isLiveInstance(downCounter, 'down'), true); +});