diff --git a/.github/workflows/test-all-packages.yml b/.github/workflows/test-all-packages.yml index 564e7457d020..102da4de14b9 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 000000000000..cabf929916fc --- /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 000000000000..deb751bdf4f9 --- /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 000000000000..d8f08ac74d1d --- /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 000000000000..493ac1596267 --- /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 000000000000..aa484f26017c --- /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 000000000000..204866956e3e --- /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 000000000000..ebed4fee7079 --- /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 000000000000..fea6a47d95d7 --- /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 000000000000..a635f7d39bd8 --- /dev/null +++ b/packages/zone/virtual.js @@ -0,0 +1 @@ +export * from './src/virtual.js';