Skip to content

Commit 47fd2c1

Browse files
authored
Merge pull request #1666 from endojs/markm-revocables
feat(exo): revocables
2 parents 9956f04 + c9bcb8c commit 47fd2c1

File tree

2 files changed

+168
-7
lines changed

2 files changed

+168
-7
lines changed

packages/exo/src/exo-makers.js

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

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

8-
const { create, seal, freeze, defineProperty } = Object;
8+
const { create, seal, freeze, defineProperty, values } = Object;
99

1010
const { getEnvironmentOption } = makeEnvironmentCaptor(globalThis);
1111
const DEBUG = getEnvironmentOption('DEBUG', '');
@@ -62,11 +62,24 @@ export const initEmpty = () => emptyRecord;
6262
* Each property is distinct, is checked and changed separately.
6363
*/
6464

65+
/**
66+
* @callback Revoker
67+
* @param {any} exo
68+
* @returns {boolean}
69+
*/
70+
71+
/**
72+
* @callback ReceiveRevoker
73+
* @param {Revoker} revoke
74+
* @returns {void}
75+
*/
76+
6577
/**
6678
* @template C
6779
* @typedef {object} FarClassOptions
6880
* @property {(context: C) => void} [finish]
6981
* @property {StateShape} [stateShape]
82+
* @property {ReceiveRevoker} [receiveRevoker]
7083
*/
7184

7285
/**
@@ -79,9 +92,15 @@ export const initEmpty = () => emptyRecord;
7992
* @param {FarClassOptions<ClassContext<ReturnType<I>, M>>} [options]
8093
* @returns {(...args: Parameters<I>) => (M & import('@endo/eventual-send').RemotableBrand<{}, M>)}
8194
*/
82-
export const defineExoClass = (tag, interfaceGuard, init, methods, options) => {
95+
export const defineExoClass = (
96+
tag,
97+
interfaceGuard,
98+
init,
99+
methods,
100+
options = {},
101+
) => {
83102
harden(methods);
84-
const { finish = undefined } = options || {};
103+
const { finish = undefined, receiveRevoker = undefined } = options;
85104
/** @type {WeakMap<M,ClassContext<ReturnType<I>, M>>} */
86105
const contextMap = new WeakMap();
87106
const proto = defendPrototype(
@@ -113,6 +132,13 @@ export const defineExoClass = (tag, interfaceGuard, init, methods, options) => {
113132
self
114133
);
115134
};
135+
136+
if (receiveRevoker) {
137+
const revoke = self => contextMap.delete(self);
138+
harden(revoke);
139+
receiveRevoker(revoke);
140+
}
141+
116142
return harden(makeInstance);
117143
};
118144
harden(defineExoClass);
@@ -132,14 +158,14 @@ export const defineExoClassKit = (
132158
interfaceGuardKit,
133159
init,
134160
methodsKit,
135-
options,
161+
options = {},
136162
) => {
137163
harden(methodsKit);
138-
const { finish = undefined } = options || {};
164+
const { finish = undefined, receiveRevoker = undefined } = options;
139165
const contextMapKit = objectMap(methodsKit, () => new WeakMap());
140166
const getContextKit = objectMap(
141-
methodsKit,
142-
(_v, name) => facet => contextMapKit[name].get(facet),
167+
contextMapKit,
168+
contextMap => facet => contextMap.get(facet),
143169
);
144170
const prototypeKit = defendPrototypeKit(
145171
tag,
@@ -172,6 +198,14 @@ export const defineExoClassKit = (
172198
}
173199
return context.facets;
174200
};
201+
202+
if (receiveRevoker) {
203+
const revoke = aFacet =>
204+
values(contextMapKit).some(contextMap => contextMap.delete(aFacet));
205+
harden(revoke);
206+
receiveRevoker(revoke);
207+
}
208+
175209
return harden(makeInstanceKit);
176210
};
177211
harden(defineExoClassKit);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
receiveRevoker(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+
receiveRevoker(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.is(revoke(upCounter), false);
87+
t.throws(() => upCounter.incr(3), {
88+
message:
89+
'"In \\"incr\\" method of (Counter up)" may only be applied to a valid instance: "[Alleged: Counter up]"',
90+
});
91+
t.is(revoke(downCounter), true);
92+
t.is(revoke(downCounter), false);
93+
t.throws(() => downCounter.decr(), {
94+
message:
95+
'"In \\"decr\\" method of (Counter down)" may only be applied to a valid instance: "[Alleged: Counter down]"',
96+
});
97+
});
98+
99+
test('test facet cross-talk', t => {
100+
const makeCounterKit = defineExoClassKit(
101+
'Counter',
102+
{ up: UpCounterI, down: DownCounterI },
103+
/** @param {number} x */
104+
(x = 0) => ({ x }),
105+
{
106+
up: {
107+
incr(y = 1) {
108+
const { state } = this;
109+
state.x += y;
110+
return state.x;
111+
},
112+
},
113+
down: {
114+
decr(y = 1) {
115+
const { state } = this;
116+
state.x -= y;
117+
return state.x;
118+
},
119+
},
120+
},
121+
);
122+
const { up: upCounter, down: downCounter } = makeCounterKit(3);
123+
t.throws(() => apply(upCounter.incr, downCounter, [2]), {
124+
message:
125+
'"In \\"incr\\" method of (Counter up)" may only be applied to a valid instance: "[Alleged: Counter down]"',
126+
});
127+
});

0 commit comments

Comments
 (0)