diff --git a/packages/swingset-liveslots/src/cache.js b/packages/swingset-liveslots/src/cache.js index 4186aa9a464f..b69bf297c9ee 100644 --- a/packages/swingset-liveslots/src/cache.js +++ b/packages/swingset-liveslots/src/cache.js @@ -17,7 +17,7 @@ import { Fail } from '@agoric/assert'; /** * @callback CacheDelete * @param {string} key - * @returns {void} + * @returns {boolean} * * @callback CacheFlush * @returns {void} @@ -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()]; diff --git a/packages/swingset-liveslots/src/virtualObjectManager.js b/packages/swingset-liveslots/src/virtualObjectManager.js index 301cc8f0e0f1..f1d6615f3ab8 100644 --- a/packages/swingset-liveslots/src/virtualObjectManager.js +++ b/packages/swingset-liveslots/src/virtualObjectManager.js @@ -920,15 +920,25 @@ export const makeVirtualObjectManager = ( const makeContext = (baseRef, state) => { // baseRef came from valToSlot, so must be in slotToVal const val = requiredValForSlot(baseRef); + const revoke = () => contextCache.delete(baseRef); // val is either 'self' or the facet record if (multifaceted) { - return harden({ state, facets: val }); + return harden({ + state, + facets: val, + revoke, + }); } else { - return harden({ state, self: val }); + return harden({ + state, + self: val, + revoke, + }); } }; - // The contextCache holds the {state,self} or {state,facets} "context" + // The contextCache holds the + // { state, self, revoke } or { state, facets, revoke } "context" // object, needed by behavior functions. We keep this in a (per-crank) // cache because creating one requires knowledge of the state property // names, which requires a DB read. The property names are fixed at diff --git a/packages/vat-data/src/exo-utils.js b/packages/vat-data/src/exo-utils.js index 1eff5dffa26e..f80d0004c6ac 100644 --- a/packages/vat-data/src/exo-utils.js +++ b/packages/vat-data/src/exo-utils.js @@ -81,6 +81,13 @@ export const makeExoUtils = VatData => { ); harden(prepareKindMulti); + /** + * TODO reuse types ClassContext, ClassContextKit, etc from @endo/exo + * + * @callback RevokeExo + * @returns {boolean} + */ + // TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 /** * @template {(...args: any) => any} I init state function @@ -90,11 +97,13 @@ export const makeExoUtils = VatData => { * @param {I} init * @param {T & ThisType<{ * self: T, - * state: ReturnType + * state: ReturnType, + * revoke: RevokeExo, * }>} methods * @param {DefineKindOptions<{ * self: T, - * state: ReturnType + * state: ReturnType, + * revoke: RevokeExo, * }>} [options] * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} */ @@ -115,11 +124,13 @@ export const makeExoUtils = VatData => { * @param {I} init * @param {T & ThisType<{ * facets: T, - * state: ReturnType + * state: ReturnType, + * revoke: RevokeExo, * }> } facets * @param {DefineKindOptions<{ * facets: T, - * state: ReturnType + * state: ReturnType, + * revoke: RevokeExo, * }>} [options] * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} */ @@ -146,11 +157,13 @@ export const makeExoUtils = VatData => { * @param {I} init * @param {T & ThisType<{ * self: T, - * state: ReturnType + * state: ReturnType, + * revoke: RevokeExo, * }>} methods * @param {DefineKindOptions<{ * self: T, - * state: ReturnType + * state: ReturnType, + * revoke: RevokeExo, * }>} [options] * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} */ @@ -177,11 +190,13 @@ export const makeExoUtils = VatData => { * @param {I} init * @param {T & ThisType<{ * facets: T, - * state: ReturnType + * state: ReturnType, + * revoke: RevokeExo, * }> } facets * @param {DefineKindOptions<{ * facets: T, - * state: ReturnType + * state: ReturnType, + * revoke: RevokeExo, * }>} [options] * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} */ @@ -209,11 +224,13 @@ export const makeExoUtils = VatData => { * @param {I} init * @param {T & ThisType<{ * self: T, - * state: ReturnType + * state: ReturnType, + * revoke: RevokeExo, * }>} methods * @param {DefineKindOptions<{ * self: T, - * state: ReturnType + * state: ReturnType, + * revoke: RevokeExo, * }>} [options] * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} */ @@ -244,11 +261,13 @@ export const makeExoUtils = VatData => { * @param {I} init * @param {T & ThisType<{ * facets: T, - * state: ReturnType + * state: ReturnType, + * revoke: RevokeExo, * }> } facets * @param {DefineKindOptions<{ * facets: T, - * state: ReturnType + * state: ReturnType, + * revoke: RevokeExo, * }>} [options] * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} */ diff --git a/packages/vat-data/test/test-revoke-virtual.js b/packages/vat-data/test/test-revoke-virtual.js new file mode 100644 index 000000000000..2a5ae2b2f9d0 --- /dev/null +++ b/packages/vat-data/test/test-revoke-virtual.js @@ -0,0 +1,126 @@ +// Modeled on test-revoke-heap-classes.js + +import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; +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()), + done: M.call().returns(M.boolean()), +}); + +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 => { + const makeUpCounter = defineVirtualExoClass( + 'UpCounter', + UpCounterI, + /** @param {number} x */ + (x = 0) => ({ x }), + { + incr(y = 1) { + const { state } = this; + state.x += y; + return state.x; + }, + done() { + const { revoke } = this; + return revoke(); + }, + }, + ); + const upCounter = makeUpCounter(3); + t.is(upCounter.incr(5), 8); + t.is(upCounter.done(), 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 => { + 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; + }, + done() { + const { revoke } = this; + return revoke(); + }, + }, + down: { + decr(y = 1) { + const { state } = this; + state.x -= y; + return state.x; + }, + }, + }, + ); + const { up: upCounter, down: downCounter } = makeCounterKit(3); + t.is(upCounter.incr(5), 8); + t.is(downCounter.decr(), 7); + t.is(upCounter.done(), 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.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; + }, + done() { + const { revoke } = this; + return revoke(); + }, + }, + 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', + }); +});