From b804736497525da3fd8cb96e892d06cd2a68ea25 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Wed, 22 Feb 2023 21:36:47 -0600 Subject: [PATCH 1/5] feat(vat-data): export overlooked `provideDurableWeakSetStore` --- packages/vat-data/src/index.js | 1 + packages/vat-data/src/vat-data-bindings.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/vat-data/src/index.js b/packages/vat-data/src/index.js index cb0e4076ed7..05cc557beec 100644 --- a/packages/vat-data/src/index.js +++ b/packages/vat-data/src/index.js @@ -21,6 +21,7 @@ export { provideDurableMapStore, provideDurableWeakMapStore, provideDurableSetStore, + provideDurableWeakSetStore, // deprecated defineKind, defineKindMulti, diff --git a/packages/vat-data/src/vat-data-bindings.js b/packages/vat-data/src/vat-data-bindings.js index 8a7fd78bc41..b9a2a067035 100644 --- a/packages/vat-data/src/vat-data-bindings.js +++ b/packages/vat-data/src/vat-data-bindings.js @@ -152,6 +152,8 @@ export const makeStoreUtils = VatData => { makeScalarBigWeakMapStore, // eslint-disable-next-line no-shadow -- these literally do shadow the globals makeScalarBigSetStore, + // eslint-disable-next-line no-shadow -- these literally do shadow the globals + makeScalarBigWeakSetStore, } = VatData; /** @@ -187,10 +189,22 @@ export const makeStoreUtils = VatData => { ); harden(provideDurableSetStore); + /** + * @param {import('./types').Baggage} baggage + * @param {string} name + * @param {Omit} options + */ + const provideDurableWeakSetStore = (baggage, name, options = {}) => + provide(baggage, name, () => + makeScalarBigWeakSetStore(name, { durable: true, ...options }), + ); + harden(provideDurableWeakSetStore); + return harden({ provideDurableMapStore, provideDurableWeakMapStore, provideDurableSetStore, + provideDurableWeakSetStore, }); }; @@ -199,4 +213,5 @@ export const { provideDurableMapStore, provideDurableWeakMapStore, provideDurableSetStore, + provideDurableWeakSetStore, } = globalStoreUtils; From f9536de1747333ad003d9f4a2acd8ac3b14bbf92 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Wed, 22 Feb 2023 21:38:31 -0600 Subject: [PATCH 2/5] docs(patterns): consistent type constraints for `makeExo` --- packages/store/src/patterns/exo-makers.js | 40 +++++++++++++---------- packages/store/test/test-heap-classes.js | 1 + 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/store/src/patterns/exo-makers.js b/packages/store/src/patterns/exo-makers.js index 59ac8a8255a..1bedce9cb62 100644 --- a/packages/store/src/patterns/exo-makers.js +++ b/packages/store/src/patterns/exo-makers.js @@ -47,15 +47,14 @@ export const initEmpty = () => emptyRecord; */ /** - * @template A args to init - * @template S state from init - * @template {Record} T methods + * @template {(...args: any[]) => any} I init function + * @template {Record} M methods * @param {string} tag * @param {any} interfaceGuard - * @param {(...args: A[]) => S} init - * @param {T & ThisType<{ self: T, state: S }>} methods - * @param {FarClassOptions>} [options] - * @returns {(...args: A[]) => (T & import('@endo/eventual-send').RemotableBrand<{}, T>)} + * @param {I} init + * @param {M & ThisType<{ self: M, state: ReturnType }>} methods + * @param {FarClassOptions, M>>} [options] + * @returns {(...args: Parameters) => (M & import('@endo/eventual-send').RemotableBrand<{}, M>)} */ export const defineExoClass = ( tag, @@ -64,7 +63,7 @@ export const defineExoClass = ( methods, { finish = undefined } = {}, ) => { - /** @type {WeakMap>} */ + /** @type {WeakMap, M>>} */ const contextMap = new WeakMap(); const prototype = defendPrototype( tag, @@ -73,14 +72,17 @@ export const defineExoClass = ( true, interfaceGuard, ); + /** + * @param {Parameters} args + */ const makeInstance = (...args) => { // Be careful not to freeze the state record const state = seal(init(...args)); - /** @type {T} */ + /** @type {M} */ // @ts-expect-error could be instantiated with different subtype const self = harden({ __proto__: prototype }); // Be careful not to freeze the state record - /** @type {Context} */ + /** @type {Context,M>} */ const context = freeze({ state, self }); contextMap.set(self, context); if (finish) { @@ -94,15 +96,14 @@ export const defineExoClass = ( harden(defineExoClass); /** - * @template A args to init - * @template S state from init - * @template {Record>} F methods + * @template {(...args: any[]) => any} I init function + * @template {Record>} F facet methods * @param {string} tag * @param {any} interfaceGuardKit - * @param {(...args: A[]) => S} init - * @param {F & ThisType<{ facets: F, state: S }> } methodsKit - * @param {FarClassOptions>} [options] - * @returns {(...args: A[]) => F} + * @param {I} init + * @param {F & ThisType<{ facets: F, state: ReturnType }> } methodsKit + * @param {FarClassOptions,F>>} [options] + * @returns {(...args: Parameters) => F} */ export const defineExoClassKit = ( tag, @@ -119,6 +120,9 @@ export const defineExoClassKit = ( true, interfaceGuardKit, ); + /** + * @param {Parameters} args + */ const makeInstanceKit = (...args) => { // Be careful not to freeze the state record const state = seal(init(...args)); @@ -145,7 +149,7 @@ export const defineExoClassKit = ( harden(defineExoClassKit); /** - * @template {Record} T + * @template {Record} T * @param {string} tag * @param {InterfaceGuard | undefined} interfaceGuard CAVEAT: static typing does not yet support `callWhen` transformation * @param {T} methods diff --git a/packages/store/test/test-heap-classes.js b/packages/store/test/test-heap-classes.js index fe23d88a6a3..afb0a47e166 100644 --- a/packages/store/test/test-heap-classes.js +++ b/packages/store/test/test-heap-classes.js @@ -106,6 +106,7 @@ test('test makeExo', t => { t.throws(() => upCounter.incr(-3), { message: 'In "incr" method of (upCounter): arg 0?: -3 - Must be >= 0', }); + // @ts-expect-error deliberately bad arg for testing t.throws(() => upCounter.incr('foo'), { message: 'In "incr" method of (upCounter): arg 0?: string "foo" - Must be a number', From 8b5e8e411423917bcb805aeacdba222eff35edd5 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Wed, 22 Feb 2023 21:41:45 -0600 Subject: [PATCH 3/5] feat(zone): first cut at `@agoric/zone` --- .github/workflows/test-all-packages.yml | 2 + packages/zone/durable.js | 1 + packages/zone/heap.js | 1 + packages/zone/jsconfig.json | 19 ++++++ packages/zone/package.json | 46 ++++++++++++++ packages/zone/src/durable.js | 82 +++++++++++++++++++++++++ packages/zone/src/heap.js | 40 ++++++++++++ packages/zone/src/index.js | 35 +++++++++++ packages/zone/src/virtual.js | 68 ++++++++++++++++++++ packages/zone/virtual.js | 1 + 10 files changed, 295 insertions(+) create mode 100644 packages/zone/durable.js create mode 100644 packages/zone/heap.js create mode 100644 packages/zone/jsconfig.json create mode 100644 packages/zone/package.json create mode 100644 packages/zone/src/durable.js create mode 100644 packages/zone/src/heap.js create mode 100644 packages/zone/src/index.js create mode 100644 packages/zone/src/virtual.js create mode 100644 packages/zone/virtual.js diff --git a/.github/workflows/test-all-packages.yml b/.github/workflows/test-all-packages.yml index 564e7457d02..102da4de14b 100644 --- a/.github/workflows/test-all-packages.yml +++ b/.github/workflows/test-all-packages.yml @@ -201,6 +201,8 @@ jobs: run: cd packages/time && yarn ${{ steps.vars.outputs.test }} | $TEST_COLLECT - name: yarn test (swingset-liveslots) run: cd packages/swingset-liveslots && yarn ${{ steps.vars.outputs.test }} | $TEST_COLLECT + - name: yarn test (zone) + run: cd packages/zone && yarn ${{ steps.vars.outputs.test }} | $TEST_COLLECT # The meta-test! - name: Check for untested packages diff --git a/packages/zone/durable.js b/packages/zone/durable.js new file mode 100644 index 00000000000..cabf929916f --- /dev/null +++ b/packages/zone/durable.js @@ -0,0 +1 @@ +export * from './src/durable.js'; diff --git a/packages/zone/heap.js b/packages/zone/heap.js new file mode 100644 index 00000000000..deb751bdf4f --- /dev/null +++ b/packages/zone/heap.js @@ -0,0 +1 @@ +export * from './src/heap.js'; diff --git a/packages/zone/jsconfig.json b/packages/zone/jsconfig.json new file mode 100644 index 00000000000..d8f08ac74d1 --- /dev/null +++ b/packages/zone/jsconfig.json @@ -0,0 +1,19 @@ +// This file can contain .js-specific Typescript compiler config. +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "checkJs": true, + "noEmit": true, + "downlevelIteration": true, + "strictNullChecks": true, + "noImplicitThis": true, + "moduleResolution": "node", + }, + "include": [ + "*.js", + "scripts", + "src", + "test", + ], +} diff --git a/packages/zone/package.json b/packages/zone/package.json new file mode 100644 index 00000000000..493ac159626 --- /dev/null +++ b/packages/zone/package.json @@ -0,0 +1,46 @@ +{ + "name": "@agoric/zone", + "version": "0.1.0", + "description": "Allocation zone abstraction for objects on the heap, persistent stores, etc.", + "type": "module", + "repository": "https://github.com/Agoric/agoric-sdk", + "main": "./src/index.js", + "scripts": { + "build": "exit 0", + "test": "true || ava", + "test:c8": "true || c8 $C8_OPTIONS ava --config=ava-nesm.config.js", + "test:xs": "exit 0", + "lint-fix": "yarn lint:eslint --fix", + "lint": "run-s --continue-on-error lint:*", + "lint:types": "tsc -p jsconfig.json", + "lint:eslint": "eslint ." + }, + "exports": { + ".": "./src/index.js", + "./durable.js": "./durable.js", + "./heap.js": "./heap.js", + "./virtual.js": "./virtual.js" + }, + "keywords": [], + "author": "Agoric", + "license": "Apache-2.0", + "dependencies": { + "@agoric/store": "^0.8.3", + "@agoric/vat-data": "^0.4.3", + "@endo/far": "^0.2.14" + }, + "devDependencies": {}, + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=14.15.0" + }, + "ava": { + "files": [ + "test/**/test-*.js" + ], + "timeout": "20m", + "workerThreads": false + } +} diff --git a/packages/zone/src/durable.js b/packages/zone/src/durable.js new file mode 100644 index 00000000000..aa484f26017 --- /dev/null +++ b/packages/zone/src/durable.js @@ -0,0 +1,82 @@ +import { + canBeDurable, + makeScalarMapStore, + prepareExo, + prepareExoClass, + prepareExoClassKit, + provideDurableMapStore, + provideDurableSetStore, + provideDurableWeakMapStore, + provideDurableWeakSetStore, + M, +} from '@agoric/vat-data'; + +import { Far } from '@endo/far'; + +/** + * @param {() => import('@agoric/vat-data').Baggage} getBaggage + */ +const attachDurableStores = getBaggage => { + /** @type {import('.').Zone['mapStore']} */ + const mapStore = (label, options) => + provideDurableMapStore(getBaggage(), label, options); + /** @type {import('.').Zone['setStore']} */ + const setStore = (label, options) => + provideDurableSetStore(getBaggage(), label, options); + /** @type {import('.').Zone['weakSetStore']} */ + const weakSetStore = (label, options) => + provideDurableWeakSetStore(getBaggage(), label, options); + /** @type {import('.').Zone['weakMapStore']} */ + const weakMapStore = (label, options) => + provideDurableWeakMapStore(getBaggage(), label, options); + + /** @type {import('.').Stores} */ + return Far('durableStores', { + // eslint-disable-next-line no-use-before-define + detached: () => detachedDurableStores, + isStorable: canBeDurable, + mapStore, + setStore, + weakMapStore, + weakSetStore, + }); +}; + +/** @type {import('.').Stores} */ +export const detachedDurableStores = attachDurableStores(() => + makeScalarMapStore('detached'), +); + +/** + * Create a zone whose objects persist between Agoric vat upgrades. + * + * @param {import('@agoric/vat-data').Baggage} baggage + * @returns {import('.').Zone} + */ +export const makeDurableZone = baggage => { + /** @type {import('.').Zone['exoClass']} */ + const exoClass = (...args) => prepareExoClass(baggage, ...args); + /** @type {import('.').Zone['exoClassKit']} */ + const exoClassKit = (...args) => prepareExoClassKit(baggage, ...args); + /** @type {import('.').Zone['exo']} */ + const exo = (...args) => prepareExo(baggage, ...args); + + const attachedStores = attachDurableStores(() => baggage); + + /** @type {import('.').Zone['subZone']} */ + const subZone = (label, options = {}) => { + const subBaggage = provideDurableMapStore(baggage, label, options); + return makeDurableZone(subBaggage); + }; + + return Far('durableZone', { + exo, + exoClass, + exoClassKit, + subZone, + ...attachedStores, + }); +}; +harden(makeDurableZone); + +export { M }; diff --git a/packages/zone/src/heap.js b/packages/zone/src/heap.js new file mode 100644 index 00000000000..204866956e3 --- /dev/null +++ b/packages/zone/src/heap.js @@ -0,0 +1,40 @@ +import { + makeExo, + defineExoClass, + defineExoClassKit, + makeScalarMapStore, + makeScalarSetStore, + makeScalarWeakMapStore, + makeScalarWeakSetStore, + M, +} from '@agoric/store'; + +import { Far } from '@endo/far'; + +/** + * @type {import('.').Stores} + */ +const heapStores = Far('heapStores', { + detached: () => heapStores, + isStorable: _specimen => true, + + setStore: makeScalarSetStore, + mapStore: makeScalarMapStore, + weakMapStore: makeScalarWeakMapStore, + weakSetStore: makeScalarWeakSetStore, +}); + +/** + * A heap (in-memory) zone that uses the default exo and store implementations. + * + * @type {import('.').Zone} + */ +export const heapZone = Far('heapZone', { + exoClass: defineExoClass, + exoClassKit: defineExoClassKit, + exo: makeExo, + subZone: (_label, _options) => heapZone, + ...heapStores, +}); + +export { M }; diff --git a/packages/zone/src/index.js b/packages/zone/src/index.js new file mode 100644 index 00000000000..ebed4fee707 --- /dev/null +++ b/packages/zone/src/index.js @@ -0,0 +1,35 @@ +import { makeExo, defineExoClass, defineExoClassKit } from '@agoric/store'; + +export * from './heap.js'; + +// References to allow the below typeofs to succeed. +makeExo; +defineExoClass; +defineExoClassKit; + +/** + * @typedef {ExoZone & Stores} Zone A bag of methods for creating defensible objects and + * collections with the same allocation semantics (ephemeral, persistent, etc) + */ + +/** + * @typedef {object} ExoZone + * @property {typeof makeExo} exo create a singleton exo-object instance bound to this zone + * @property {typeof defineExoClass} exoClass create a maker function that can be used to create exo-objects bound to this zone + * @property {typeof defineExoClassKit} exoClassKit create a "kit" maker function that can be used to create a record of exo-objects sharing the same state + * @property {(label: string, options?: StoreOptions) => Zone} subZone create a new Zone that can be passed to untrusted consumers without exposing the storage of the parent zone + */ + +/** + * @typedef {object} Stores + * @property {() => Stores} detached obtain store providers which are detached (the stores are anonymous rather than bound to `label` in the zone) + * @property {(specimen: unknown) => boolean} isStorable return true if the specimen can be stored in the zone, whether as exo-object state or in a store + * @property {(label: string, options?: StoreOptions) => MapStore} mapStore provide a Map-like store named `label` in the zone + * @property {(label: string, options?: StoreOptions) => SetStore} setStore provide a Set-like store named `label` in the zone + * @property {( + * label: string, options?: StoreOptions) => WeakMapStore + * } weakMapStore provide a WeakMap-like store named `label` in the zone + * @property {( + * label: string, options?: StoreOptions) => WeakSetStore + * } weakSetStore provide a WeakSet-like store named `label` in the zone + */ diff --git a/packages/zone/src/virtual.js b/packages/zone/src/virtual.js new file mode 100644 index 00000000000..fea6a47d95d --- /dev/null +++ b/packages/zone/src/virtual.js @@ -0,0 +1,68 @@ +import { + canBeDurable, + defineVirtualExoClass, + defineVirtualExoClassKit, + makeScalarBigMapStore, + makeScalarBigSetStore, + makeScalarBigWeakMapStore, + makeScalarBigWeakSetStore, + M, +} from '@agoric/vat-data'; + +import { Far } from '@endo/far'; + +const emptyRecord = harden({}); +const initEmpty = harden(() => emptyRecord); + +/** + * This implementation of `defineVirtualExo` only exists to ensure there are no + * gaps in the virtualZone API. + * + * @type {import('.').Zone['exo']} + */ +const defineVirtualExo = ( + label, + interfaceGuard, + methods, + options = undefined, +) => { + const defineKindOptions = + /** @type {import('@agoric/vat-data').DefineKindOptions<{ self: typeof methods }>} */ ( + options + ); + const makeInstance = defineVirtualExoClass( + label, + interfaceGuard, + initEmpty, + methods, + defineKindOptions, + ); + return makeInstance(); +}; + +/** @type {import('.').Stores} */ +export const detachedVirtualStores = Far('virtualStores', { + detached: () => detachedVirtualStores, + isStorable: canBeDurable, + mapStore: makeScalarBigMapStore, + setStore: makeScalarBigSetStore, + weakMapStore: makeScalarBigWeakMapStore, + weakSetStore: makeScalarBigWeakSetStore, +}); + +/** + * A zone that utilizes external storage to reduce the memory footprint of the + * current vat. + * + * @type {import('.').Zone} + */ +export const virtualZone = Far('virtualZone', { + exo: defineVirtualExo, + exoClass: defineVirtualExoClass, + exoClassKit: defineVirtualExoClassKit, + subZone: (_label, _options = {}) => virtualZone, + + ...detachedVirtualStores, +}); + +export { M }; diff --git a/packages/zone/virtual.js b/packages/zone/virtual.js new file mode 100644 index 00000000000..a635f7d39bd --- /dev/null +++ b/packages/zone/virtual.js @@ -0,0 +1 @@ +export * from './src/virtual.js'; From e6ce708e6cd5aef5203f86dfd53a79b4af51c278 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Wed, 22 Feb 2023 22:04:08 -0600 Subject: [PATCH 4/5] chore(agoric-cli): regenerate `sdk-package-names.js` --- packages/agoric-cli/src/sdk-package-names.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/agoric-cli/src/sdk-package-names.js b/packages/agoric-cli/src/sdk-package-names.js index e71a084aa26..4849d48329f 100644 --- a/packages/agoric-cli/src/sdk-package-names.js +++ b/packages/agoric-cli/src/sdk-package-names.js @@ -43,5 +43,6 @@ export default [ "@agoric/xsnap", "@agoric/xsnap-lockdown", "@agoric/zoe", + "@agoric/zone", "agoric" ]; From 667bfb0dd79d9171b65666e280d0a231b301eb57 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Sun, 5 Mar 2023 20:53:22 -0600 Subject: [PATCH 5/5] docs(store): better `MethodGuardMaker` example --- packages/store/src/types.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/store/src/types.js b/packages/store/src/types.js index 7f2e84875b6..1c559b21ccd 100644 --- a/packages/store/src/types.js +++ b/packages/store/src/types.js @@ -753,10 +753,17 @@ /** * @typedef {any} MethodGuardMaker - * a parameter list like foo(a, b, c = d, …e) => f should be guarded by - * something like - * foo: M.call(AShape, BShape).optional(CShape).rest(EShape).returns(FShape) - * optional is for optional (=) params. rest is for … (varargs) params + * A method name and parameter/return signature like: + * ```js + * foo(a, b, c = d, ...e) => f + * ``` + * should be guarded by something like: + * ```js + * { + * ...otherMethodGuards, + * foo: M.call(AShape, BShape).optional(CShape).rest(EShape).returns(FShape), + * } + * ``` */ /** @typedef {{ klass: 'methodGuard', callKind: 'sync' | 'async', returnGuard: unknown }} MethodGuard */