Skip to content

Commit

Permalink
feat(vat-data): virtual exos are revocable
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Dec 21, 2023
1 parent 4f72580 commit 6e89af8
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 4 deletions.
5 changes: 3 additions & 2 deletions packages/swingset-liveslots/src/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Fail } from '@agoric/assert';
/**
* @callback CacheDelete
* @param {string} key
* @returns {void}
* @returns {boolean}
*
* @callback CacheFlush
* @returns {void}
Expand Down Expand Up @@ -82,8 +82,9 @@ export function makeCache(readBacking, writeBacking, deleteBacking) {
},
delete: key => {
assert.typeof(key, 'string');
stash.delete(key);
const result = stash.delete(key);
dirtyKeys.add(key);
return result;
},
flush: () => {
const keys = [...dirtyKeys.keys()];
Expand Down
16 changes: 14 additions & 2 deletions packages/swingset-liveslots/src/vatDataTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ import type {
WeakMapStore,
WeakSetStore,
} from '@agoric/store';
import type {
FarClassOptions,
StateShape,
Context,
KitContext,
Revoker,
ReceiveRevoker,
} from '@endo/exo';
import type { makeWatchedPromiseManager } from './watchedPromises.js';

// TODO should be moved into @endo/patterns and eventually imported here
// instead of this local definition.
export type InterfaceGuardKit = Record<string, InterfaceGuard>;

export type { MapStore, Pattern };

// This needs `any` values. If they were `unknown`, code that uses Baggage
Expand Down Expand Up @@ -88,7 +95,12 @@ export type DefineKindOptions<C> = {
* If provided, it describes the shape of all state records of instances
* of this kind.
*/
stateShape?: { [name: string]: Pattern };
stateShape?: StateShape;

/**
* If provided, it is called with a revoke function as an argument.
*/
receiveRevoker?: ReceiveRevoker;

/**
* Intended for internal use only.
Expand Down
18 changes: 18 additions & 0 deletions packages/swingset-liveslots/src/virtualObjectManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ const makeContextCache = (makeState, makeContext) => {
const makeContextProvider = (contextCache, getSlotForVal) =>
harden(rep => contextCache.get(getSlotForVal(rep)));

const makeContextRevoker = (contextCache, getSlotForVal) =>
harden(rep => contextCache.delete(getSlotForVal(rep)));

const makeContextProviderKit = (contextCache, getSlotForVal, facetNames) => {
/** @type { Record<string, any> } */
const contextProviderKit = {};
Expand All @@ -174,6 +177,13 @@ const makeContextProviderKit = (contextCache, getSlotForVal, facetNames) => {
return harden(contextProviderKit);
};

// TODO BUG The returned function revokes the whole kit, i.e., all vrefs
// sharing the same baseRef. This makes me wonder whether I need to rethink
// revocation yet again. Perhaps an entire kit is the correct unit and
// the api should be reconcieved.
const makeContextFacetRevoker = (contextCache, getSlotForVal) =>
harden(rep => contextCache.delete(parseVatSlot(getSlotForVal(rep)).baseRef));

// The management of single Representatives (i.e. defineKind) is very similar
// to that of a cohort of facets (i.e. defineKindMulti). In this description,
// we use "self/facets" to refer to either 'self' or 'facets', as appropriate
Expand Down Expand Up @@ -718,6 +728,7 @@ export const makeVirtualObjectManager = (
finish = undefined,
stateShape = undefined,
thisfulMethods = false,
receiveRevoker = undefined,
} = options;
let {
// These are "let" rather than "const" only to accommodate code
Expand Down Expand Up @@ -1076,6 +1087,13 @@ export const makeVirtualObjectManager = (
return val;
};

if (receiveRevoker) {
const makeRevoker = multifaceted
? makeContextFacetRevoker
: makeContextRevoker;
receiveRevoker(makeRevoker(contextCache, getSlotForVal));
}

return makeNewInstance;
};

Expand Down
131 changes: 131 additions & 0 deletions packages/vat-data/test/test-revoke-virtual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Modeled on test-revoke-heap-classes.js

import { test } from './prepare-test-env-ava.js';

// eslint-disable-next-line import/order
import { M } from '@agoric/store';
import {
defineVirtualExoClass,
defineVirtualExoClassKit,
} from '../src/exo-utils.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 defineVirtualExoClass', t => {
let revoke;
const makeUpCounter = defineVirtualExoClass(
'UpCounter',
UpCounterI,
/** @param {number} x */
(x = 0) => ({ x }),
{
incr(y = 1) {
const { state } = this;
state.x += y;
return state.x;
},
},
{
receiveRevoker(r) {
revoke = r;
},
},
);
const upCounter = makeUpCounter(3);
t.is(upCounter.incr(5), 8);
// @ts-expect-error Does not understand that `revoke` is a function.
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 defineVirtualExoClassKit', t => {
let revoke;
const makeCounterKit = defineVirtualExoClassKit(
'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;
},
},
);
const { up: upCounter, down: downCounter } = makeCounterKit(3);
t.is(upCounter.incr(5), 8);
t.is(downCounter.decr(), 7);
// @ts-expect-error Does not understand that `revoke` is a function.
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]"',
});
// @ts-expect-error Does not understand that `revoke` is a function.
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 virtual facet cross-talk', t => {
const makeCounterKit = defineVirtualExoClassKit(
'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: 'illegal cross-facet access',
});
});

0 comments on commit 6e89af8

Please sign in to comment.