Skip to content

Commit b015165

Browse files
committed
feat(exo): revocables
1 parent 31298f7 commit b015165

File tree

2 files changed

+187
-7
lines changed

2 files changed

+187
-7
lines changed

packages/exo/src/exo-makers.js

+62-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { objectMap } from '@endo/patterns';
55

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

8-
const { create, seal, freeze, defineProperty } = Object;
8+
const { Fail, quote: q } = assert;
9+
const { create, seal, freeze, defineProperty, entries, values } = Object;
910

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

66+
/**
67+
* @callback Revoker
68+
* @param {object} exo
69+
* @returns {boolean}
70+
*/
71+
72+
/**
73+
* @callback GetRevoker
74+
* @param {Revoker} revoke
75+
* @returns {void}
76+
*/
77+
6578
/**
6679
* @template C
6780
* @typedef {object} FarClassOptions
6881
* @property {(context: C) => void} [finish]
6982
* @property {StateShape} [stateShape]
83+
* @property {GetRevoker} [getRevoker]
7084
*/
7185

7286
/**
@@ -79,9 +93,15 @@ export const initEmpty = () => emptyRecord;
7993
* @param {FarClassOptions<ClassContext<ReturnType<I>, M>>} [options]
8094
* @returns {(...args: Parameters<I>) => (M & import('@endo/eventual-send').RemotableBrand<{}, M>)}
8195
*/
82-
export const defineExoClass = (tag, interfaceGuard, init, methods, options) => {
96+
export const defineExoClass = (
97+
tag,
98+
interfaceGuard,
99+
init,
100+
methods,
101+
options = {},
102+
) => {
83103
harden(methods);
84-
const { finish = undefined } = options || {};
104+
const { finish = undefined, getRevoker = undefined } = options;
85105
/** @type {WeakMap<M,ClassContext<ReturnType<I>, M>>} */
86106
const contextMap = new WeakMap();
87107
const proto = defendPrototype(
@@ -113,6 +133,13 @@ export const defineExoClass = (tag, interfaceGuard, init, methods, options) => {
113133
self
114134
);
115135
};
136+
137+
if (getRevoker) {
138+
const revoke = self => contextMap.delete(self);
139+
harden(revoke);
140+
getRevoker(revoke);
141+
}
142+
116143
return harden(makeInstance);
117144
};
118145
harden(defineExoClass);
@@ -132,14 +159,14 @@ export const defineExoClassKit = (
132159
interfaceGuardKit,
133160
init,
134161
methodsKit,
135-
options,
162+
options = {},
136163
) => {
137164
harden(methodsKit);
138-
const { finish = undefined } = options || {};
165+
const { finish = undefined, getRevoker = undefined } = options;
139166
const contextMapKit = objectMap(methodsKit, () => new WeakMap());
140167
const getContextKit = objectMap(
141-
methodsKit,
142-
(_v, name) => facet => contextMapKit[name].get(facet),
168+
contextMapKit,
169+
contextMap => facet => contextMap.get(facet),
143170
);
144171
const prototypeKit = defendPrototypeKit(
145172
tag,
@@ -172,6 +199,34 @@ export const defineExoClassKit = (
172199
}
173200
return context.facets;
174201
};
202+
203+
if (getRevoker) {
204+
const revoke = aFacet => {
205+
let seenTrue = false;
206+
let facets;
207+
for (const contextMap of values(contextMapKit)) {
208+
if (contextMap.has(aFacet)) {
209+
seenTrue = true;
210+
facets = contextMap.get(aFacet).facets;
211+
break;
212+
}
213+
}
214+
if (!seenTrue) {
215+
return false;
216+
}
217+
// eslint-disable-next-line no-use-before-define
218+
for (const [facetName, facet] of entries(facets)) {
219+
const seen = contextMapKit[facetName].delete(facet);
220+
if (seen === false) {
221+
Fail`internal: inconsistent facet revocation ${q(facetName)}`;
222+
}
223+
}
224+
return seenTrue;
225+
};
226+
harden(revoke);
227+
getRevoker(revoke);
228+
}
229+
175230
return harden(makeInstanceKit);
176231
};
177232
harden(defineExoClassKit);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// eslint-disable-next-line import/order
2+
import { test } from './prepare-test-env-ava.js';
3+
4+
// eslint-disable-next-line import/order
5+
import { M } from '@endo/patterns';
6+
import { defineExoClass, defineExoClassKit } from '../src/exo-makers.js';
7+
8+
const { apply } = Reflect;
9+
10+
const UpCounterI = M.interface('UpCounter', {
11+
incr: M.call()
12+
// TODO M.number() should not be needed to get a better error message
13+
.optional(M.and(M.number(), M.gte(0)))
14+
.returns(M.number()),
15+
});
16+
17+
const DownCounterI = M.interface('DownCounter', {
18+
decr: M.call()
19+
// TODO M.number() should not be needed to get a better error message
20+
.optional(M.and(M.number(), M.gte(0)))
21+
.returns(M.number()),
22+
});
23+
24+
test('test revoke defineExoClass', t => {
25+
let revoke;
26+
const makeUpCounter = defineExoClass(
27+
'UpCounter',
28+
UpCounterI,
29+
/** @param {number} x */
30+
(x = 0) => ({ x }),
31+
{
32+
incr(y = 1) {
33+
const { state } = this;
34+
state.x += y;
35+
return state.x;
36+
},
37+
},
38+
{
39+
getRevoker(r) {
40+
revoke = r;
41+
},
42+
},
43+
);
44+
const upCounter = makeUpCounter(3);
45+
t.is(upCounter.incr(5), 8);
46+
t.is(revoke(upCounter), true);
47+
t.throws(() => upCounter.incr(1), {
48+
message:
49+
'"In \\"incr\\" method of (UpCounter)" may only be applied to a valid instance: "[Alleged: UpCounter]"',
50+
});
51+
});
52+
53+
test('test revoke defineExoClassKit', t => {
54+
let revoke;
55+
const makeCounterKit = defineExoClassKit(
56+
'Counter',
57+
{ up: UpCounterI, down: DownCounterI },
58+
/** @param {number} x */
59+
(x = 0) => ({ x }),
60+
{
61+
up: {
62+
incr(y = 1) {
63+
const { state } = this;
64+
state.x += y;
65+
return state.x;
66+
},
67+
},
68+
down: {
69+
decr(y = 1) {
70+
const { state } = this;
71+
state.x -= y;
72+
return state.x;
73+
},
74+
},
75+
},
76+
{
77+
getRevoker(r) {
78+
revoke = r;
79+
},
80+
},
81+
);
82+
const { up: upCounter, down: downCounter } = makeCounterKit(3);
83+
t.is(upCounter.incr(5), 8);
84+
t.is(downCounter.decr(), 7);
85+
t.is(revoke(upCounter), true);
86+
t.throws(() => upCounter.incr(3), {
87+
message:
88+
'"In \\"incr\\" method of (Counter up)" may only be applied to a valid instance: "[Alleged: Counter up]"',
89+
});
90+
t.is(revoke(downCounter), false);
91+
t.throws(() => downCounter.decr(), {
92+
message:
93+
'"In \\"decr\\" method of (Counter down)" may only be applied to a valid instance: "[Alleged: Counter down]"',
94+
});
95+
});
96+
97+
test('test facet cross-talk', t => {
98+
const makeCounterKit = defineExoClassKit(
99+
'Counter',
100+
{ up: UpCounterI, down: DownCounterI },
101+
/** @param {number} x */
102+
(x = 0) => ({ x }),
103+
{
104+
up: {
105+
incr(y = 1) {
106+
const { state } = this;
107+
state.x += y;
108+
return state.x;
109+
},
110+
},
111+
down: {
112+
decr(y = 1) {
113+
const { state } = this;
114+
state.x -= y;
115+
return state.x;
116+
},
117+
},
118+
},
119+
);
120+
const { up: upCounter, down: downCounter } = makeCounterKit(3);
121+
t.throws(() => apply(upCounter.incr, downCounter, [2]), {
122+
message:
123+
'"In \\"incr\\" method of (Counter up)" may only be applied to a valid instance: "[Alleged: Counter down]"',
124+
});
125+
});

0 commit comments

Comments
 (0)