From 806264ac3894ba78acb09fbf08bef17e00311c4d Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Mon, 3 Jul 2023 23:40:31 -0700 Subject: [PATCH] feat(exo): revocables --- packages/exo/src/exo-makers.js | 37 ++++++++- packages/exo/test/test-revoke-heap-class.js | 89 +++++++++++++++++++++ 2 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 packages/exo/test/test-revoke-heap-class.js diff --git a/packages/exo/src/exo-makers.js b/packages/exo/src/exo-makers.js index 94f42706ab..8b18d64b6a 100644 --- a/packages/exo/src/exo-makers.js +++ b/packages/exo/src/exo-makers.js @@ -62,11 +62,28 @@ export const initEmpty = () => emptyRecord; * Each property is distinct, is checked and changed separately. */ +/** + * @callback Revoker + * @param {object} exo + * @returns {boolean} + */ + +/** + * @typedef {Record} RevokerKit + */ + +/** + * @callback GetRevoker + * @param {Revoker | RevokerKit} revoke + * @returns {void} + */ + /** * @template C * @typedef {object} FarClassOptions * @property {(context: C) => void} [finish] * @property {StateShape} [stateShape] + * @property {GetRevoker} [getRevoker] */ /** @@ -81,7 +98,7 @@ export const initEmpty = () => emptyRecord; */ export const defineExoClass = (tag, interfaceGuard, init, methods, options) => { harden(methods); - const { finish = undefined } = options || {}; + const { finish = undefined, getRevoker = undefined } = options || {}; /** @type {WeakMap, M>>} */ const contextMap = new WeakMap(); const proto = defendPrototype( @@ -91,6 +108,11 @@ export const defineExoClass = (tag, interfaceGuard, init, methods, options) => { true, interfaceGuard, ); + if (getRevoker) { + const revoke = self => contextMap.delete(self); + harden(revoke); + getRevoker(revoke); + } let instanceCount = 0; /** * @param {Parameters} args @@ -135,11 +157,11 @@ export const defineExoClassKit = ( 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, @@ -148,6 +170,13 @@ export const defineExoClassKit = ( true, interfaceGuardKit, ); + if (getRevoker) { + const revokerKit = objectMap( + contextMapKit, + contextMap => facet => contextMap.delete(facet), + ); + getRevoker(revokerKit); + } let instanceCount = 0; /** * @param {Parameters} args diff --git a/packages/exo/test/test-revoke-heap-class.js b/packages/exo/test/test-revoke-heap-class.js new file mode 100644 index 0000000000..7d1b8b9087 --- /dev/null +++ b/packages/exo/test/test-revoke-heap-class.js @@ -0,0 +1,89 @@ +// 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() + // 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 revokerKit; + 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) { + revokerKit = r; + }, + }, + ); + const { up: upCounter, down: downCounter } = makeCounterKit(3); + t.is(upCounter.incr(5), 8); + t.is(downCounter.decr(), 7); + t.is(revokerKit.up(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(downCounter.decr(), 6); +});