diff --git a/.github/workflows/test-all-packages.yml b/.github/workflows/test-all-packages.yml index 7713271a8b4..735a6c52bb1 100644 --- a/.github/workflows/test-all-packages.yml +++ b/.github/workflows/test-all-packages.yml @@ -199,6 +199,9 @@ jobs: - name: yarn test (agoric-cli) if: (success() || failure()) run: cd packages/agoric-cli && yarn ${{ steps.vars.outputs.test }} | $TEST_COLLECT + - name: yarn test (base-zone) + if: (success() || failure()) + run: cd packages/base-zone && yarn ${{ steps.vars.outputs.test }} | $TEST_COLLECT - name: yarn test (cosmos) if: (success() || failure()) run: cd golang/cosmos && yarn ${{ steps.vars.outputs.test }} | $TEST_COLLECT diff --git a/packages/agoric-cli/src/sdk-package-names.js b/packages/agoric-cli/src/sdk-package-names.js index 19cb920dad6..0bb4cf85f05 100644 --- a/packages/agoric-cli/src/sdk-package-names.js +++ b/packages/agoric-cli/src/sdk-package-names.js @@ -4,6 +4,7 @@ export default [ "@agoric/access-token", "@agoric/assert", + "@agoric/base-zone", "@agoric/boot", "@agoric/cache", "@agoric/casting", diff --git a/packages/base-zone/CHANGELOG.md b/packages/base-zone/CHANGELOG.md new file mode 100644 index 00000000000..e4d87c4d45c --- /dev/null +++ b/packages/base-zone/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/packages/base-zone/README.md b/packages/base-zone/README.md new file mode 100644 index 00000000000..09cbb75e671 --- /dev/null +++ b/packages/base-zone/README.md @@ -0,0 +1,9 @@ +# Base Zone Library + +Each Zone provides an API that allows the allocation of [Exo objects](https://github.com/endojs/endo/tree/master/packages/exo#readme) and [Stores +(object collections)](../store/README.md) which use the same underlying persistence mechanism. This +allows library code to be agnostic to whether its objects are backed purely by +the JS heap (ephemeral), pageable out to disk (virtual) or can be revived after +a vat upgrade (durable). + +This library is used internally by [`@agoric/zone`](../zone/README.md); refer to it for more details. Unless you are an author of a new Zone backing store type, or want to use `makeHeapZone` without introducing build dependencies on `@agoric/vat-data`, you should instead use `@agoric/zone`. diff --git a/packages/base-zone/heap.js b/packages/base-zone/heap.js new file mode 100644 index 00000000000..16c192a7bca --- /dev/null +++ b/packages/base-zone/heap.js @@ -0,0 +1,3 @@ +// @jessie-check + +export * from './src/heap.js'; diff --git a/packages/base-zone/jsconfig.build.json b/packages/base-zone/jsconfig.build.json new file mode 100644 index 00000000000..f9ba41da4de --- /dev/null +++ b/packages/base-zone/jsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": [ + "./jsconfig.json", + "../../tsconfig-build-options.json" + ], + "exclude": [ + "test", + "scripts", + "**/exports.js", + ] +} diff --git a/packages/base-zone/jsconfig.json b/packages/base-zone/jsconfig.json new file mode 100644 index 00000000000..63eacb817e3 --- /dev/null +++ b/packages/base-zone/jsconfig.json @@ -0,0 +1,10 @@ +// This file can contain .js-specific Typescript compiler config. +{ + "extends": "../../tsconfig.json", + "include": [ + "*.js", + "scripts", + "src/**/*.js", + "test/**/*.js", + ], +} diff --git a/packages/base-zone/package.json b/packages/base-zone/package.json new file mode 100644 index 00000000000..aeb8a1fa955 --- /dev/null +++ b/packages/base-zone/package.json @@ -0,0 +1,52 @@ +{ + "name": "@agoric/base-zone", + "version": "0.1.0", + "description": "Allocation zone abstraction library and heap implementation", + "type": "module", + "repository": "https://github.com/Agoric/agoric-sdk", + "main": "./src/index.js", + "scripts": { + "build": "exit 0", + "prepack": "tsc --build jsconfig.build.json", + "postpack": "git clean -f '*.d.ts*'", + "test": "ava", + "test:c8": "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", + "./heap.js": "./heap.js", + "./tools/*": "./tools/*" + }, + "keywords": [], + "author": "Agoric", + "license": "Apache-2.0", + "dependencies": { + "@agoric/store": "^0.9.2", + "@endo/exo": "^0.2.3", + "@endo/far": "^0.2.19", + "@endo/pass-style": "^0.1.4", + "@endo/patterns": "^0.2.3" + }, + "devDependencies": { + "@endo/init": "^0.5.57", + "ava": "^5.3.0" + }, + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=14.15.0" + }, + "ava": { + "files": [ + "test/**/test-*.js" + ], + "timeout": "20m", + "workerThreads": false + } +} diff --git a/packages/base-zone/src/exports.d.ts b/packages/base-zone/src/exports.d.ts new file mode 100644 index 00000000000..f44506790f7 --- /dev/null +++ b/packages/base-zone/src/exports.d.ts @@ -0,0 +1,9 @@ +/* eslint-disable import/export */ + +// Module Types ////////////////////////////////////////////////////// +// +// Types exposed from modules. +// + +// @ts-expect-error TS1383: Only named exports may use 'export type'. +export type * from './types.js'; diff --git a/packages/base-zone/src/exports.js b/packages/base-zone/src/exports.js new file mode 100644 index 00000000000..b59f8e97105 --- /dev/null +++ b/packages/base-zone/src/exports.js @@ -0,0 +1,2 @@ +// Just a dummy to use exports.d.ts and satisfy runtime imports. +export {}; diff --git a/packages/zone/src/heap.js b/packages/base-zone/src/heap.js similarity index 92% rename from packages/zone/src/heap.js rename to packages/base-zone/src/heap.js index 08a63e3bb09..f968fa0be4f 100644 --- a/packages/zone/src/heap.js +++ b/packages/base-zone/src/heap.js @@ -1,5 +1,6 @@ // @ts-check // @jessie-check +/// import { Far } from '@endo/far'; import { makeExo, defineExoClass, defineExoClassKit } from '@endo/exo'; @@ -8,14 +9,14 @@ import { makeScalarSetStore, makeScalarWeakMapStore, makeScalarWeakSetStore, -} from '@agoric/vat-data'; +} from '@agoric/store'; import { makeOnceKit } from './make-once.js'; import { agoricVatDataKeys as keys } from './keys.js'; import { isPassable } from './is-passable.js'; /** - * @type {import('.').Stores} + * @type {import('./types.js').Stores} */ const detachedHeapStores = Far('heapStores', { detached: () => detachedHeapStores, @@ -31,7 +32,7 @@ const detachedHeapStores = Far('heapStores', { * Create a heap (in-memory) zone that uses the default exo and store implementations. * * @param {string} [baseLabel] - * @returns {import('.').Zone} + * @returns {import('./types.js').Zone} */ export const makeHeapZone = (baseLabel = 'heapZone') => { const { makeOnce, wrapProvider } = makeOnceKit(baseLabel, detachedHeapStores); diff --git a/packages/base-zone/src/index.js b/packages/base-zone/src/index.js new file mode 100644 index 00000000000..5815984e058 --- /dev/null +++ b/packages/base-zone/src/index.js @@ -0,0 +1,9 @@ +// @jessie-check + +// eslint-disable-next-line import/export +export * from './exports.js'; + +// Utilities for creating zones. +export * from './make-once.js'; +export * from './keys.js'; +export * from './is-passable.js'; diff --git a/packages/zone/src/is-passable.js b/packages/base-zone/src/is-passable.js similarity index 100% rename from packages/zone/src/is-passable.js rename to packages/base-zone/src/is-passable.js diff --git a/packages/zone/src/keys.js b/packages/base-zone/src/keys.js similarity index 78% rename from packages/zone/src/keys.js rename to packages/base-zone/src/keys.js index 0e0a1fe97ff..531900bcc5f 100644 --- a/packages/zone/src/keys.js +++ b/packages/base-zone/src/keys.js @@ -1,9 +1,16 @@ +// @ts-check + /** @param {string} label */ const kind = label => `${label}_kindHandle`; + /** @param {string} label */ const singleton = label => `${label}_singleton`; -/** @type {Record<'exoClass' | 'exoClassKit' | 'exo' | 'store' | 'zone', (label: string) => string[]>} */ +/** + * KeyMakers compatible with `@agoric/vat-data`. + * + * @type {import('./types.js').KeyMakers} + */ export const agoricVatDataKeys = { exoClass: label => harden([kind(label)]), exoClassKit: label => harden([kind(label)]), diff --git a/packages/zone/src/make-once.js b/packages/base-zone/src/make-once.js similarity index 98% rename from packages/zone/src/make-once.js rename to packages/base-zone/src/make-once.js index 1fbc8085c6b..e0b6f5d639e 100644 --- a/packages/zone/src/make-once.js +++ b/packages/base-zone/src/make-once.js @@ -7,7 +7,7 @@ harden(defaultLabelToKeys); /** * @param {string} debugName Only used internally for diagnostics, not available to user code - * @param {import('.').Stores} stores + * @param {import('./types.js').Stores} stores * @param {import('@agoric/swingset-liveslots').MapStore} [backingStore] */ export const makeOnceKit = (debugName, stores, backingStore = undefined) => { diff --git a/packages/base-zone/src/store-types.d.ts b/packages/base-zone/src/store-types.d.ts new file mode 100644 index 00000000000..91176037635 --- /dev/null +++ b/packages/base-zone/src/store-types.d.ts @@ -0,0 +1,3 @@ +// Prevent the compiler from complaining about the @agoric/store type not being +// defined if `"noImplicitAny": true` is set in tsconfig.json. +declare module '@agoric/store'; diff --git a/packages/base-zone/src/types.js b/packages/base-zone/src/types.js new file mode 100644 index 00000000000..d381432cfc9 --- /dev/null +++ b/packages/base-zone/src/types.js @@ -0,0 +1,33 @@ +// eslint-disable-next-line no-unused-vars +import { makeExo, defineExoClass, defineExoClassKit } from '@endo/exo'; + +/** @typedef {'exoClass' | 'exoClassKit' | 'exo' | 'store' | 'zone'} KeyCategories */ +/** @typedef {Record string[]>} KeyMakers */ + +/** + * @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 {(key: string, maker: (key: string) => T) => T} makeOnce create or retrieve a singleton object bound to this zone + * @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/base-zone/test/prepare-test-env-ava.js b/packages/base-zone/test/prepare-test-env-ava.js new file mode 100644 index 00000000000..278ff764af3 --- /dev/null +++ b/packages/base-zone/test/prepare-test-env-ava.js @@ -0,0 +1,6 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import '@endo/init/debug.js'; + +import test from 'ava'; + +export { test }; diff --git a/packages/base-zone/test/test-exos.js b/packages/base-zone/test/test-exos.js new file mode 100644 index 00000000000..a2954502ed0 --- /dev/null +++ b/packages/base-zone/test/test-exos.js @@ -0,0 +1,10 @@ +// eslint-disable-next-line import/order +import { test } from './prepare-test-env-ava.js'; + +import { makeHeapZone } from '../heap.js'; +import { testFirstZoneIncarnation } from '../tools/testers.js'; + +test('heapZone', t => { + const zone = makeHeapZone(); + testFirstZoneIncarnation(t, zone); +}); diff --git a/packages/base-zone/test/test-make-once.js b/packages/base-zone/test/test-make-once.js new file mode 100644 index 00000000000..885f74713ea --- /dev/null +++ b/packages/base-zone/test/test-make-once.js @@ -0,0 +1,8 @@ +import { test } from './prepare-test-env-ava.js'; + +import { makeHeapZone } from '../heap.js'; +import { testMakeOnce } from '../tools/testers.js'; + +test('heapZone', t => { + testMakeOnce(t, makeHeapZone()); +}); diff --git a/packages/base-zone/tools/greeter.js b/packages/base-zone/tools/greeter.js new file mode 100644 index 00000000000..5fc18420f4a --- /dev/null +++ b/packages/base-zone/tools/greeter.js @@ -0,0 +1,54 @@ +import { M } from '@endo/patterns'; + +export const bindAllMethodsTo = (obj, that = obj) => + Object.fromEntries( + Object.entries(obj).map(([name, fn]) => [name, fn.bind(that)]), + ); + +export const greetGuard = M.interface('Greeter', { + greet: M.call().optional(M.string()).returns(M.string()), +}); +export const greetFacet = { + greet(greeting = 'Hello') { + return `${greeting}, ${this.state.nick}`; + }, +}; + +export const adminGuard = M.interface('GreeterAdmin', { + setNick: M.call(M.string()).returns(), +}); +export const adminFacet = { + setNick(nick) { + this.state.nick = nick; + }, +}; + +export const combinedGuard = M.interface('GreeterWithAdmin', { + ...greetGuard.methodGuards, + ...adminGuard.methodGuards, +}); + +export const prepareGreeterSingleton = (zone, label, nick) => { + const myThis = Object.freeze({ state: { nick } }); + return zone.exo(label, combinedGuard, { + ...bindAllMethodsTo(greetFacet, myThis), + ...bindAllMethodsTo(adminFacet, myThis), + }); +}; + +export const prepareGreeter = zone => + zone.exoClass('Greeter', combinedGuard, nick => ({ nick }), { + ...greetFacet, + ...adminFacet, + }); + +export const prepareGreeterKit = zone => + zone.exoClassKit( + 'GreeterKit', + { greeter: greetGuard, admin: adminGuard }, + nick => ({ nick }), + { + greeter: greetFacet, + admin: adminFacet, + }, + ); diff --git a/packages/base-zone/tools/testers.js b/packages/base-zone/tools/testers.js new file mode 100644 index 00000000000..86d51c919ba --- /dev/null +++ b/packages/base-zone/tools/testers.js @@ -0,0 +1,120 @@ +import * as g from './greeter.js'; + +/** + * @param {import('ava').Assertions} t + * @param {import('../src/index.js').Zone} rootZone + */ +export const testMakeOnce = (t, rootZone) => { + const subZone = rootZone.subZone('sub'); + const a = subZone.makeOnce('a', () => 'A'); + t.is(a, 'A'); + t.throws(() => subZone.makeOnce('a', () => 'A'), { + message: /has already been used/, + }); + const nonPassable = harden({ + hello() { + return 'world'; + }, + }); + t.is(rootZone.isStorable(nonPassable), false); + t.is(subZone.isStorable(123), true); + t.throws(() => rootZone.makeOnce('nonPassable', () => nonPassable), { + message: /is not storable/, + }); +}; + +// CAUTION: Do not modify this list; it exists to ensure that future versions +// of @agoric/zone are compatible with the baggage created by older versions, +// including the legacy implementation of @agoric/vat-data. +export const agoricVatDataCompatibleKeys = harden( + [ + 'Greeter_kindHandle', + 'GreeterKit_kindHandle', + 'a_kindHandle', + 'a_singleton', + 'mappish', + 'subsub', + ].sort(), +); + +export const testGreeter = (t, nick, obj, adminObj = obj) => { + t.is(obj.greet('Greetings'), `Greetings, ${nick}`); + t.is(obj.greet(), `Hello, ${nick}`); + adminObj.setNick(`${nick}2`); + t.is(obj.greet('Greetings'), `Greetings, ${nick}2`); + t.is(obj.greet(), `Hello, ${nick}2`); + adminObj.setNick(nick); +}; + +const alreadyExceptionSpec = { + message: /has already been used/, +}; + +/** + * @template T + * @param {import('ava').Assertions} t + * @param {() => T} fn + * @param {*} spec + * @returns {T} + */ +const secondThrows = (t, fn, spec = alreadyExceptionSpec) => { + const ret = fn(); + t.throws(fn, spec); + return ret; +}; + +/** + * @param {import('ava').Assertions} t + * @param {import('../src/index').Zone} rootZone + */ +export const testFirstZoneIncarnation = (t, rootZone) => { + const subZone = secondThrows(t, () => rootZone.subZone('sub')); + const singly = secondThrows(t, () => + g.prepareGreeterSingleton(subZone, 'a', 'Singly'), + ); + testGreeter(t, 'Singly', singly); + + const makeGreeter = secondThrows(t, () => g.prepareGreeter(subZone)); + const classy = makeGreeter('Classy'); + testGreeter(t, 'Classy', classy); + + const makeGreeterKit = secondThrows(t, () => g.prepareGreeterKit(subZone)); + + const { greeter: kitty, admin: kittyAdmin } = makeGreeterKit('Kitty'); + testGreeter(t, 'Kitty', kitty, kittyAdmin); + + const mappish = secondThrows(t, () => subZone.mapStore('mappish')); + mappish.init('singly', singly); + mappish.init('classy', classy); + mappish.init('kitty', kitty); + mappish.init('kittyAdmin', kittyAdmin); + + secondThrows(t, () => subZone.subZone('subsub')); +}; + +/** + * @param {import('ava').Assertions} t + * @param {import('../src/index').Zone} rootZone + */ +export const testSecondZoneIncarnation = (t, rootZone) => { + const subZone = secondThrows(t, () => rootZone.subZone('sub')); + const mappish = secondThrows(t, () => subZone.mapStore('mappish')); + + const singlyReload = secondThrows(t, () => + g.prepareGreeterSingleton(subZone, 'a', 'Singly'), + ); + const makeGreeter = secondThrows(t, () => g.prepareGreeter(subZone)); + const makeGreeterKit = secondThrows(t, () => g.prepareGreeterKit(subZone)); + + const singly = mappish.get('singly'); + t.is(singlyReload, singly); + testGreeter(t, 'Singly', singly); + testGreeter(t, 'Classy', mappish.get('classy')); + testGreeter(t, 'Kitty', mappish.get('kitty'), mappish.get('kittyAdmin')); + + const classy2 = makeGreeter('Classy2'); + testGreeter(t, 'Classy2', classy2); + + const { greeter: kitty2, admin: kittyAdmin2 } = makeGreeterKit('Kitty2'); + testGreeter(t, 'Kitty2', kitty2, kittyAdmin2); +}; diff --git a/packages/internal/README.md b/packages/internal/README.md index 13160d6acac..032078bb0fd 100644 --- a/packages/internal/README.md +++ b/packages/internal/README.md @@ -10,11 +10,16 @@ Like all `@agoric` packages it follows Semantic Versioning. Unlike the others, i # Design -It must be the lowest agoric-sdk package in any import graph. Therefore it must never depend on another agoric-sdk package. If there's a module in an another agoric-package that has no agoric-sdk dependencies, it can be moved into this package. +It is meant to be a home for modules that have no Agoric-specific dependencies themselves. It does depend on a these other @agoric packages but they are all destined to migrate out of the repo, + +- base-zone +- store +- assert + +This package may not take dependencies on any others in this repository. It must never export ambient types. It should not be imported by deep imports. Eventually this will be enforced by [`exports`](https://nodejs.org/api/packages.html#exports) but the tooling isn't ready: - https://github.com/import-js/eslint-plugin-import/issues/1810 - https://github.com/microsoft/TypeScript/issues/33079 (or some related problem with JSdoc types) - diff --git a/packages/internal/package.json b/packages/internal/package.json index a343b6361ce..b23dc2ff15f 100755 --- a/packages/internal/package.json +++ b/packages/internal/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@agoric/assert": "^0.6.0", - "@agoric/zone": "^0.2.2", + "@agoric/base-zone": "^0.1.0", "@endo/far": "^0.2.19", "@endo/init": "^0.5.57", "@endo/marshal": "^0.8.6", diff --git a/packages/internal/src/callback.js b/packages/internal/src/callback.js index 858b44749d6..c2aa61e254d 100644 --- a/packages/internal/src/callback.js +++ b/packages/internal/src/callback.js @@ -193,7 +193,7 @@ harden(isCallback); * Prepare an attenuator class whose methods can be redirected via callbacks. * * @template {PropertyKey} M - * @param {import('@agoric/zone').Zone} zone The zone in which to allocate attenuators. + * @param {import('@agoric/base-zone').Zone} zone The zone in which to allocate attenuators. * @param {M[]} methodNames Methods to forward. * @param {object} opts * @param {import('@endo/patterns').InterfaceGuard} [opts.interfaceGuard] An interface guard for the @@ -305,7 +305,7 @@ harden(prepareAttenuator); /** * Prepare an attenuator whose methodNames are derived from the interfaceGuard. * - * @param {import('@agoric/zone').Zone} zone + * @param {import('@agoric/base-zone').Zone} zone * @param {import('@endo/patterns').InterfaceGuard} interfaceGuard * @param {object} [opts] * @param {string} [opts.tag] diff --git a/packages/internal/src/lib-chainStorage.js b/packages/internal/src/lib-chainStorage.js index 0d978249d07..b61f841971d 100644 --- a/packages/internal/src/lib-chainStorage.js +++ b/packages/internal/src/lib-chainStorage.js @@ -2,7 +2,7 @@ import { E } from '@endo/far'; import { M } from '@endo/patterns'; -import { makeHeapZone } from '@agoric/zone'; +import { makeHeapZone } from '@agoric/base-zone/heap.js'; import * as cb from './callback.js'; const { Fail } = assert; @@ -123,7 +123,7 @@ harden(assertPathSegment); */ /** - * @param {import('@agoric/zone').Zone} zone + * @param {import('@agoric/base-zone').Zone} zone */ export const prepareChainStorageNode = zone => { /** diff --git a/packages/internal/test/test-callback.js b/packages/internal/test/test-callback.js index 687156ec2c8..de88075dfa2 100644 --- a/packages/internal/test/test-callback.js +++ b/packages/internal/test/test-callback.js @@ -3,7 +3,7 @@ import '@endo/init'; import test from 'ava'; import { Far } from '@endo/far'; -import { makeHeapZone } from '@agoric/zone'; +import { makeHeapZone } from '@agoric/base-zone/heap.js'; import * as cb from '../src/callback.js'; test('near function callbacks', t => { diff --git a/packages/solo/src/vat-http.js b/packages/solo/src/vat-http.js index a635fe28d77..c78787f9b40 100644 --- a/packages/solo/src/vat-http.js +++ b/packages/solo/src/vat-http.js @@ -106,7 +106,7 @@ export function buildRootObject(vatPowers) { setCommandDevice(d) { commandDevice = d; - const replHandler = getReplHandler(replObjects, send, vatPowers); + const replHandler = getReplHandler(replObjects, send); registerURLHandler(replHandler, '/private/repl'); // Assign the captp handler. diff --git a/packages/store/package.json b/packages/store/package.json index ce564e9e57c..806e885d7f7 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -31,7 +31,6 @@ "homepage": "https://github.com/Agoric/agoric-sdk#readme", "dependencies": { "@agoric/assert": "^0.6.0", - "@agoric/internal": "^0.3.2", "@endo/exo": "^0.2.3", "@endo/marshal": "^0.8.6", "@endo/pass-style": "^0.1.4", diff --git a/packages/store/test/engine-gc.js b/packages/store/test/engine-gc.js new file mode 100644 index 00000000000..e71d1df60dd --- /dev/null +++ b/packages/store/test/engine-gc.js @@ -0,0 +1,22 @@ +import v8 from 'v8'; +import vm from 'vm'; + +/* global globalThis */ +let bestGC = globalThis.gc; +if (typeof bestGC !== 'function') { + // Node.js v8 wizardry. + v8.setFlagsFromString('--expose_gc'); + bestGC = vm.runInNewContext('gc'); + assert(bestGC); + // We leave --expose_gc turned on, otherwise AVA's shared workers + // may race and disable it before we manage to extract the + // binding. This won't cause 'gc' to be visible to new Compartments + // because SES strips out everything it doesn't recognize. + + // // Hide the gc global from new contexts/workers. + // v8.setFlagsFromString('--no-expose_gc'); +} + +// Export a const. +const engineGC = bestGC; +export default engineGC; diff --git a/packages/store/test/gc-and-finalize.js b/packages/store/test/gc-and-finalize.js new file mode 100644 index 00000000000..0c4f72d0c2b --- /dev/null +++ b/packages/store/test/gc-and-finalize.js @@ -0,0 +1,91 @@ +/* global setImmediate */ + +/* A note on our GC terminology: + * + * We define four states for any JS object to be in: + * + * REACHABLE: There exists a path from some root (live export or top-level + * global) to this object, making it ineligible for collection. Userspace vat + * code has a strong reference to it (and userspace is not given access to + * WeakRef, so it has no weak reference that might be used to get access). + * + * UNREACHABLE: There is no strong reference from a root to the object. + * Userspace vat code has no means to access the object, although liveslots + * might (via a WeakRef). The object is eligible for collection, but that + * collection has not yet happened. The liveslots WeakRef is still alive: if + * it were to call `.deref()`, it would get the object. + * + * COLLECTED: The JS engine has performed enough GC to notice the + * unreachability of the object, and has collected it. The liveslots WeakRef + * is dead: `wr.deref() === undefined`. Neither liveslots nor userspace has + * any way to reach the object, and never will again. A finalizer callback + * has been queued, but not yet executed. + * + * FINALIZED: The JS engine has run the finalizer callback. Once the + * callback completes, the object is thoroughly dead and unremembered, + * and no longer exists in one of these four states. + * + * The transition from REACHABLE to UNREACHABLE always happens as a result of + * a message delivery or resolution notification (e.g when userspace + * overwrites a variable, deletes a Map entry, or a callback on the promise + * queue which closed over some objects is retired and deleted). + * + * The transition from UNREACHABLE to COLLECTED can happen spontaneously, as + * the JS engine decides it wants to perform GC. It will also happen + * deliberately if we provoke a GC call with a magic function like `gc()` + * (when Node.js imports `engine-gc`, which is morally-equivalent to + * running with `--expose-gc`, or when XS is configured to provide it as a + * C-level callback). We can force GC, but we cannot prevent it from happening + * at other times. + * + * FinalizationRegistry callbacks are defined to run on their own turn, so + * the transition from COLLECTED to FINALIZED occurs at a turn boundary. + * Node.js appears to schedule these finalizers on the timer/IO queue, not + * the promise/microtask queue. So under Node.js, you need a `setImmediate()` + * or two to ensure that finalizers have had a chance to run. XS is different + * but responds well to similar techniques. + */ + +/* + * `gcAndFinalize` must be defined in the start compartment. It uses + * platform-specific features to provide a function which provokes a full GC + * operation: all "UNREACHABLE" objects should transition to "COLLECTED" + * before it returns. In addition, `gcAndFinalize()` returns a Promise. This + * Promise will resolve (with `undefined`) after all FinalizationRegistry + * callbacks have executed, causing all COLLECTED objects to transition to + * FINALIZED. If the caller can manage call gcAndFinalize with an empty + * promise queue, then their .then callback will also start with an empty + * promise queue, and there will be minimal uncollected unreachable objects + * in the heap when it begins. + * + * `gcAndFinalize` depends upon platform-specific tools to provoke a GC sweep + * and wait for finalizers to run: a `gc()` function, and `setImmediate`. If + * these tools do not exist, this function will do nothing, and return a + * dummy pre-resolved Promise. + */ + +export function makeGcAndFinalize(gcPower) { + if (typeof gcPower !== 'function') { + if (gcPower !== false) { + // We weren't explicitly disabled, so warn. + console.warn( + Error(`no gcPower() function; skipping finalizer provocation`), + ); + } + } + return async function gcAndFinalize() { + if (typeof gcPower !== 'function') { + return; + } + + // on Node.js, GC seems to work better if the promise queue is empty first + await new Promise(setImmediate); + // on xsnap, we must do it twice for some reason + await new Promise(setImmediate); + gcPower(); + // this gives finalizers a chance to run + await new Promise(setImmediate); + // Node.js seems to need another for promises to get cleared out + await new Promise(setImmediate); + }; +} diff --git a/packages/store/test/perf-patterns.js b/packages/store/test/perf-patterns.js index 5041693c808..479a68fd084 100644 --- a/packages/store/test/perf-patterns.js +++ b/packages/store/test/perf-patterns.js @@ -1,9 +1,6 @@ import '@endo/init/debug.js'; import { Far, makeTagged } from '@endo/marshal'; -import engineGC from '@agoric/internal/src/lib-nodejs/engine-gc.js'; -import { makeGcAndFinalize } from '@agoric/internal/src/lib-nodejs/gc-and-finalize.js'; - import { makeCopyBag, makeCopyMap, @@ -11,6 +8,9 @@ import { matches, M, } from '@endo/patterns'; +import engineGC from './engine-gc.js'; +import { makeGcAndFinalize } from './gc-and-finalize.js'; + import { AmountShape, BrandShape, diff --git a/packages/swingset-liveslots/src/vatDataTypes.d.ts b/packages/swingset-liveslots/src/vatDataTypes.d.ts index 7f51707c1d6..ff9770975f7 100644 --- a/packages/swingset-liveslots/src/vatDataTypes.d.ts +++ b/packages/swingset-liveslots/src/vatDataTypes.d.ts @@ -17,7 +17,10 @@ import type { export type { MapStore, Pattern }; -export type Baggage = MapStore; +// This needs `any` values. If they were `unknown`, code that uses Baggage +// would need explicit runtime checks or casts for every fetch, which is +// onerous. +export type Baggage = MapStore; type Tail = T extends [head: any, ...rest: infer Rest] ? Rest diff --git a/packages/zoe/src/contractSupport/durability.js b/packages/zoe/src/contractSupport/durability.js index 8712dfc626a..efde52783f3 100644 --- a/packages/zoe/src/contractSupport/durability.js +++ b/packages/zoe/src/contractSupport/durability.js @@ -113,9 +113,9 @@ harden(provideAll); * @returns {Promise>>} */ export const provideSingleton = (mapStore, key, makeValue, withValue) => { - const stored = - mapStore.has(key) || - E.when(makeValue(), v => mapStore.init(key, harden(v))); + const stored = mapStore.has(key) + ? undefined + : E.when(makeValue(), v => mapStore.init(key, harden(v))); return E.when(stored, () => { const value = mapStore.get(key); diff --git a/packages/zone/README.md b/packages/zone/README.md index 4b4fb0355e0..ca833b939a2 100644 --- a/packages/zone/README.md +++ b/packages/zone/README.md @@ -12,18 +12,18 @@ An example of making a Zone-aware vat might look something like this: ```js import { makeDurableZone } from '@agoric/zone/durable.js'; -import { zoneFrobulator } from 'frob-package'; -import { zoneWidget } from 'widget-package'; +import { prepareFrobulator } from 'frob-package'; +import { prepareWidget } from 'widget-package'; export const buildRootObject = (vatPowers, _args, baggage) => { const zone = makeDurableZone(baggage); // Ensure that Widgets cannot interfere with Frobs. - const makeWidget = zoneWidget(zone.subZone('Widgets')); + const makeWidget = prepareWidget(zone.subZone('Widgets')); // Create a collection of frobulators. const frobZone = zone.subZone('Frobs'); - const makeFrobulator = zoneFrobulator(frobZone); + const makeFrobulator = prepareFrobulator(frobZone); const widgetToFrob = frobZone.mapStore('widgetToFrob'); return Far('WidgetFrobulator', { diff --git a/packages/zone/heap.js b/packages/zone/heap.js index 16c192a7bca..698f628322e 100644 --- a/packages/zone/heap.js +++ b/packages/zone/heap.js @@ -1,3 +1,3 @@ // @jessie-check -export * from './src/heap.js'; +export * from '@agoric/base-zone/heap.js'; diff --git a/packages/zone/jsconfig.build.json b/packages/zone/jsconfig.build.json new file mode 100644 index 00000000000..6f797015ab1 --- /dev/null +++ b/packages/zone/jsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": [ + "./jsconfig.json", + "../../tsconfig-build-options.json" + ], + "exclude": [ + "scripts", + "test", + "**/exports.js", + ] +} diff --git a/packages/zone/jsconfig.json b/packages/zone/jsconfig.json index d8f08ac74d1..63eacb817e3 100644 --- a/packages/zone/jsconfig.json +++ b/packages/zone/jsconfig.json @@ -1,19 +1,10 @@ // 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", - }, + "extends": "../../tsconfig.json", "include": [ "*.js", "scripts", - "src", - "test", + "src/**/*.js", + "test/**/*.js", ], } diff --git a/packages/zone/package.json b/packages/zone/package.json index dfb6815df14..a5bad789ff5 100644 --- a/packages/zone/package.json +++ b/packages/zone/package.json @@ -7,6 +7,8 @@ "main": "./src/index.js", "scripts": { "build": "exit 0", + "prepack": "tsc --build jsconfig.build.json", + "postpack": "git clean -f '*.d.ts*'", "test": "ava", "test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js", "test:xs": "exit 0", @@ -25,8 +27,8 @@ "author": "Agoric", "license": "Apache-2.0", "dependencies": { + "@agoric/base-zone": "^0.1.0", "@agoric/vat-data": "^0.5.2", - "@endo/exo": "^0.2.3", "@endo/far": "^0.2.19", "@endo/pass-style": "^0.1.4" }, diff --git a/packages/zone/src/durable.js b/packages/zone/src/durable.js index 125732acead..22cfe4b733a 100644 --- a/packages/zone/src/durable.js +++ b/packages/zone/src/durable.js @@ -14,9 +14,11 @@ import { provideDurableWeakSetStore, } from '@agoric/vat-data'; -import { makeOnceKit } from './make-once.js'; -import { agoricVatDataKeys as keys } from './keys.js'; -import { isPassable } from './is-passable.js'; +import { + agoricVatDataKeys as keys, + isPassable, + makeOnceKit, +} from '@agoric/base-zone'; const { Fail } = assert; diff --git a/packages/zone/src/exports.d.ts b/packages/zone/src/exports.d.ts new file mode 100644 index 00000000000..16a7084b920 --- /dev/null +++ b/packages/zone/src/exports.d.ts @@ -0,0 +1,8 @@ +/* eslint-disable import/export */ + +// Module Types ////////////////////////////////////////////////////// +// +// Types exposed from modules. +// + +export type { ExoZone, Stores, Zone } from '@agoric/base-zone/src/exports.js'; diff --git a/packages/zone/src/exports.js b/packages/zone/src/exports.js new file mode 100644 index 00000000000..b59f8e97105 --- /dev/null +++ b/packages/zone/src/exports.js @@ -0,0 +1,2 @@ +// Just a dummy to use exports.d.ts and satisfy runtime imports. +export {}; diff --git a/packages/zone/src/index.js b/packages/zone/src/index.js index dcd493dca01..263a5dcf78b 100644 --- a/packages/zone/src/index.js +++ b/packages/zone/src/index.js @@ -1,38 +1,4 @@ -// @jessie-check +export { makeHeapZone } from '@agoric/base-zone/heap.js'; -import { makeExo, defineExoClass, defineExoClassKit } from '@endo/exo'; - -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 {(key: string, maker: (key: string) => T) => T} makeOnce create or retrieve a singleton object bound to this zone - * @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 - */ +// eslint-disable-next-line import/export +export * from './exports.js'; diff --git a/packages/zone/src/virtual.js b/packages/zone/src/virtual.js index 8438204de1b..95410799db0 100644 --- a/packages/zone/src/virtual.js +++ b/packages/zone/src/virtual.js @@ -11,9 +11,11 @@ import { makeScalarBigWeakSetStore, } from '@agoric/vat-data'; -import { makeOnceKit } from './make-once.js'; -import { agoricVatDataKeys as keys } from './keys.js'; -import { isPassable } from './is-passable.js'; +import { + agoricVatDataKeys as keys, + isPassable, + makeOnceKit, +} from '@agoric/base-zone'; const emptyRecord = harden({}); const initEmpty = harden(() => emptyRecord); diff --git a/packages/zone/test/test-exos.js b/packages/zone/test/test-exos.js index 009cbeb0b4a..c264e8e3d23 100644 --- a/packages/zone/test/test-exos.js +++ b/packages/zone/test/test-exos.js @@ -6,106 +6,20 @@ import { test, } from './prepare-test-env-ava.js'; -import { M } from '@endo/patterns'; import * as vatData from '@agoric/vat-data'; +import { agoricVatDataKeys as keys } from '@agoric/base-zone'; +import { + agoricVatDataCompatibleKeys, + testFirstZoneIncarnation, + testSecondZoneIncarnation, + testGreeter, +} from '@agoric/base-zone/tools/testers.js'; +import * as g from '@agoric/base-zone/tools/greeter.js'; + import { makeDurableZone } from '../durable.js'; import { makeHeapZone } from '../heap.js'; import { makeVirtualZone } from '../virtual.js'; -import { agoricVatDataKeys as keys } from '../src/keys.js'; - -/** @typedef {import('../src/index.js').Zone} Zone */ - -// CAUTION: Do not modify this list; it exists to ensure that future versions -// of @agoric/zone are compatible with the baggage created by older versions, -// including the legacy implementation of @agoric/vat-data. -const agoricVatDataCompatibleKeys = [ - 'Greeter_kindHandle', - 'GreeterKit_kindHandle', - 'a_kindHandle', - 'a_singleton', - 'mappish', - 'subsub', -].sort(); - -const bindAllMethodsTo = (obj, that = obj) => - Object.fromEntries( - Object.entries(obj).map(([name, fn]) => [name, fn.bind(that)]), - ); - -const greetGuard = M.interface('Greeter', { - greet: M.call().optional(M.string()).returns(M.string()), -}); -const greetFacet = { - greet(greeting = 'Hello') { - return `${greeting}, ${this.state.nick}`; - }, -}; - -const adminGuard = M.interface('GreeterAdmin', { - setNick: M.call(M.string()).returns(), -}); -const adminFacet = { - setNick(nick) { - this.state.nick = nick; - }, -}; - -const combinedGuard = M.interface('GreeterWithAdmin', { - ...greetGuard.methodGuards, - ...adminGuard.methodGuards, -}); - -const alreadyExceptionSpec = { - message: /has already been used/, -}; - -const prepareGreeterSingleton = (zone, label, nick) => { - const myThis = Object.freeze({ state: { nick } }); - return zone.exo(label, combinedGuard, { - ...bindAllMethodsTo(greetFacet, myThis), - ...bindAllMethodsTo(adminFacet, myThis), - }); -}; - -const prepareGreeter = zone => - zone.exoClass('Greeter', combinedGuard, nick => ({ nick }), { - ...greetFacet, - ...adminFacet, - }); - -const prepareGreeterKit = zone => - zone.exoClassKit( - 'GreeterKit', - { greeter: greetGuard, admin: adminGuard }, - nick => ({ nick }), - { - greeter: greetFacet, - admin: adminFacet, - }, - ); - -const testGreeter = (t, nick, obj, adminObj = obj) => { - t.is(obj.greet('Greetings'), `Greetings, ${nick}`); - t.is(obj.greet(), `Hello, ${nick}`); - adminObj.setNick(`${nick}2`); - t.is(obj.greet('Greetings'), `Greetings, ${nick}2`); - t.is(obj.greet(), `Hello, ${nick}2`); - adminObj.setNick(nick); -}; - -/** - * @template T - * @param {import('ava').Assertions} t - * @param {() => T} fn - * @param {*} spec - * @returns {T} - */ -const secondThrows = (t, fn, spec = alreadyExceptionSpec) => { - const ret = fn(); - t.throws(fn, spec); - return ret; -}; /** * @param {import('ava').Assertions} t @@ -115,20 +29,20 @@ const testFirstVatDataIncarnation = (t, baggage) => { const subBaggage = vatData.provideDurableMapStore(baggage, 'sub'); const myThis = Object.freeze({ state: { nick: 'Singly' } }); - const singly = vatData.prepareExo(subBaggage, 'a', combinedGuard, { - ...bindAllMethodsTo(greetFacet, myThis), - ...bindAllMethodsTo(adminFacet, myThis), + const singly = vatData.prepareExo(subBaggage, 'a', g.combinedGuard, { + ...g.bindAllMethodsTo(g.greetFacet, myThis), + ...g.bindAllMethodsTo(g.adminFacet, myThis), }); testGreeter(t, 'Singly', singly); const makeGreeter = vatData.prepareExoClass( subBaggage, 'Greeter', - combinedGuard, + g.combinedGuard, nick => ({ nick }), { - ...greetFacet, - ...adminFacet, + ...g.greetFacet, + ...g.adminFacet, }, ); const classy = makeGreeter('Classy'); @@ -137,11 +51,11 @@ const testFirstVatDataIncarnation = (t, baggage) => { const makeGreeterKit = vatData.prepareExoClassKit( subBaggage, 'GreeterKit', - { greeter: greetGuard, admin: adminGuard }, + { greeter: g.greetGuard, admin: g.adminGuard }, nick => ({ nick }), { - greeter: greetFacet, - admin: adminFacet, + greeter: g.greetFacet, + admin: g.adminFacet, }, ); const { greeter: kitty, admin: kittyAdmin } = makeGreeterKit('Kitty'); @@ -156,62 +70,6 @@ const testFirstVatDataIncarnation = (t, baggage) => { vatData.provideDurableMapStore(subBaggage, 'subsub'); }; -/** - * @param {import('ava').Assertions} t - * @param {Zone} rootZone - */ -const testFirstZoneIncarnation = (t, rootZone) => { - const subZone = secondThrows(t, () => rootZone.subZone('sub')); - const singly = secondThrows(t, () => - prepareGreeterSingleton(subZone, 'a', 'Singly'), - ); - testGreeter(t, 'Singly', singly); - - const makeGreeter = secondThrows(t, () => prepareGreeter(subZone)); - const classy = makeGreeter('Classy'); - testGreeter(t, 'Classy', classy); - - const makeGreeterKit = secondThrows(t, () => prepareGreeterKit(subZone)); - - const { greeter: kitty, admin: kittyAdmin } = makeGreeterKit('Kitty'); - testGreeter(t, 'Kitty', kitty, kittyAdmin); - - const mappish = secondThrows(t, () => subZone.mapStore('mappish')); - mappish.init('singly', singly); - mappish.init('classy', classy); - mappish.init('kitty', kitty); - mappish.init('kittyAdmin', kittyAdmin); - - secondThrows(t, () => subZone.subZone('subsub')); -}; - -/** - * @param {import('ava').Assertions} t - * @param {Zone} rootZone - */ -const testSecondZoneIncarnation = (t, rootZone) => { - const subZone = secondThrows(t, () => rootZone.subZone('sub')); - const mappish = secondThrows(t, () => subZone.mapStore('mappish')); - - const singlyReload = secondThrows(t, () => - prepareGreeterSingleton(subZone, 'a', 'Singly'), - ); - const makeGreeter = secondThrows(t, () => prepareGreeter(subZone)); - const makeGreeterKit = secondThrows(t, () => prepareGreeterKit(subZone)); - - const singly = mappish.get('singly'); - t.is(singlyReload, singly); - testGreeter(t, 'Singly', singly); - testGreeter(t, 'Classy', mappish.get('classy')); - testGreeter(t, 'Kitty', mappish.get('kitty'), mappish.get('kittyAdmin')); - - const classy2 = makeGreeter('Classy2'); - testGreeter(t, 'Classy2', classy2); - - const { greeter: kitty2, admin: kittyAdmin2 } = makeGreeterKit('Kitty2'); - testGreeter(t, 'Kitty2', kitty2, kittyAdmin2); -}; - test('heapZone', t => { const zone = makeHeapZone(); testFirstZoneIncarnation(t, zone);