Skip to content

Commit

Permalink
feat(exo): live instance testing
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Jan 13, 2024
1 parent 55a2774 commit e9bf65a
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 7 deletions.
67 changes: 60 additions & 7 deletions packages/exo/src/exo-makers.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,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.
Expand Down Expand Up @@ -153,6 +166,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<IsLiveInstance>} [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.
*/

/**
Expand Down Expand Up @@ -194,6 +216,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)}`;
Expand Down Expand Up @@ -228,11 +251,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);
Expand Down Expand Up @@ -264,6 +299,7 @@ export const defineExoClassKit = (
finish = undefined,
receiveRevoker = undefined,
receiveAmplifier = undefined,
receiveInstanceTester = undefined,
} = options;
const contextMapKit = objectMap(methodsKit, () => new WeakMap());
const getContextKit = objectMap(
Expand Down Expand Up @@ -303,26 +339,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);
Expand Down
116 changes: 116 additions & 0 deletions packages/exo/test/test-live-instance-heap-class-kits.js
Original file line number Diff line number Diff line change
@@ -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);
});

0 comments on commit e9bf65a

Please sign in to comment.