Skip to content

Commit

Permalink
feat(exo): revocables
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Jul 8, 2023
1 parent 6c1d875 commit a7e3119
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 7 deletions.
69 changes: 62 additions & 7 deletions packages/exo/src/exo-makers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { objectMap } from '@endo/patterns';

import { defendPrototype, defendPrototypeKit } from './exo-tools.js';

const { create, seal, freeze, defineProperty } = Object;
const { Fail, quote: q } = assert;
const { create, seal, freeze, defineProperty, entries, values } = Object;

const { getEnvironmentOption } = makeEnvironmentCaptor(globalThis);
const DEBUG = getEnvironmentOption('DEBUG', '');
Expand Down Expand Up @@ -62,11 +63,24 @@ export const initEmpty = () => emptyRecord;
* Each property is distinct, is checked and changed separately.
*/

/**
* @callback Revoker
* @param {object} exo
* @returns {boolean}
*/

/**
* @callback GetRevoker
* @param {Revoker} revoke
* @returns {void}
*/

/**
* @template C
* @typedef {object} FarClassOptions
* @property {(context: C) => void} [finish]
* @property {StateShape} [stateShape]
* @property {GetRevoker} [getRevoker]
*/

/**
Expand All @@ -79,9 +93,15 @@ export const initEmpty = () => emptyRecord;
* @param {FarClassOptions<ClassContext<ReturnType<I>, M>>} [options]
* @returns {(...args: Parameters<I>) => (M & import('@endo/eventual-send').RemotableBrand<{}, M>)}
*/
export const defineExoClass = (tag, interfaceGuard, init, methods, options) => {
export const defineExoClass = (
tag,
interfaceGuard,
init,
methods,
options = {},
) => {
harden(methods);
const { finish = undefined } = options || {};
const { finish = undefined, getRevoker = undefined } = options;
/** @type {WeakMap<M,ClassContext<ReturnType<I>, M>>} */
const contextMap = new WeakMap();
const proto = defendPrototype(
Expand Down Expand Up @@ -113,6 +133,13 @@ export const defineExoClass = (tag, interfaceGuard, init, methods, options) => {
self
);
};

if (getRevoker) {
const revoke = self => contextMap.delete(self);
harden(revoke);
getRevoker(revoke);
}

return harden(makeInstance);
};
harden(defineExoClass);
Expand All @@ -132,14 +159,14 @@ export const defineExoClassKit = (
interfaceGuardKit,
init,
methodsKit,
options,
options = {},
) => {
harden(methodsKit);
const { finish = undefined } = options || {};
const { finish = undefined, getRevoker = undefined } = options;
const contextMapKit = objectMap(methodsKit, () => new WeakMap());
const getContextKit = objectMap(
methodsKit,
(_v, name) => facet => contextMapKit[name].get(facet),
contextMapKit,
contextMap => facet => contextMap.get(facet),
);
const prototypeKit = defendPrototypeKit(
tag,
Expand Down Expand Up @@ -172,6 +199,34 @@ export const defineExoClassKit = (
}
return context.facets;
};

if (getRevoker) {
const revoke = aFacet => {
let seenTrue = false;
let facets;
for (const contextMap of values(contextMapKit)) {
if (contextMap.has(aFacet)) {
seenTrue = true;
facets = contextMap.get(aFacet).facets;
break;
}
}
if (!seenTrue) {
return false;
}
// eslint-disable-next-line no-use-before-define
for (const [facetName, facet] of entries(facets)) {
const seen = contextMapKit[facetName].delete(facet);
if (seen === false) {
Fail`internal: inconsistent facet revocation ${q(facetName)}`;
}
}
return seenTrue;
};
harden(revoke);
getRevoker(revoke);
}

return harden(makeInstanceKit);
};
harden(defineExoClassKit);
Expand Down
125 changes: 125 additions & 0 deletions packages/exo/test/test-revoke-heap-classes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// 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 { apply } = Reflect;

const UpCounterI = M.interface('UpCounter', {
incr: M.call()
// TODO M.number() should not be needed to get a better error message
.optional(M.and(M.number(), M.gte(0)))
.returns(M.number()),
});

const DownCounterI = M.interface('DownCounter', {
decr: M.call()
// TODO M.number() should not be needed to get a better error message
.optional(M.and(M.number(), M.gte(0)))
.returns(M.number()),
});

test('test revoke defineExoClass', t => {
let revoke;
const makeUpCounter = defineExoClass(
'UpCounter',
UpCounterI,
/** @param {number} x */
(x = 0) => ({ x }),
{
incr(y = 1) {
const { state } = this;
state.x += y;
return state.x;
},
},
{
getRevoker(r) {
revoke = r;
},
},
);
const upCounter = makeUpCounter(3);
t.is(upCounter.incr(5), 8);
t.is(revoke(upCounter), true);
t.throws(() => upCounter.incr(1), {
message:
'"In \\"incr\\" method of (UpCounter)" may only be applied to a valid instance: "[Alleged: UpCounter]"',
});
});

test('test revoke defineExoClassKit', t => {
let revoke;
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;
},
},
},
{
getRevoker(r) {
revoke = r;
},
},
);
const { up: upCounter, down: downCounter } = makeCounterKit(3);
t.is(upCounter.incr(5), 8);
t.is(downCounter.decr(), 7);
t.is(revoke(upCounter), true);
t.throws(() => upCounter.incr(3), {
message:
'"In \\"incr\\" method of (Counter up)" may only be applied to a valid instance: "[Alleged: Counter up]"',
});
t.is(revoke(downCounter), false);
t.throws(() => downCounter.decr(), {
message:
'"In \\"decr\\" method of (Counter down)" may only be applied to a valid instance: "[Alleged: Counter down]"',
});
});

test('test facet cross-talk', t => {
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;
},
},
},
);
const { up: upCounter, down: downCounter } = makeCounterKit(3);
t.throws(() => apply(upCounter.incr, downCounter, [2]), {
message:
'"In \\"incr\\" method of (Counter up)" may only be applied to a valid instance: "[Alleged: Counter down]"',
});
});

0 comments on commit a7e3119

Please sign in to comment.