From f908f7307214b669d79765f9b4c09fa8f5da5a5d Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 13 Sep 2024 17:03:14 -1000 Subject: [PATCH] wip(gems): use gemNamespace to abstract over zones + test restart --- packages/gems/src/index.js | 113 ++++++++++++++++++------------- packages/gems/test/index.test.js | 63 ++++------------- packages/gems/test/util.js | 58 ++++------------ 3 files changed, 94 insertions(+), 140 deletions(-) diff --git a/packages/gems/src/index.js b/packages/gems/src/index.js index 15b2bc7ae6..7826ce7588 100644 --- a/packages/gems/src/index.js +++ b/packages/gems/src/index.js @@ -52,6 +52,7 @@ export const makeMessageCapTP = ( export const makeKernel = (baggage) => { const zone = makeDurableZone(baggage); + const rootStore = zone.mapStore('rootStore'); const rootGemZone = zone.subZone('RootGemZone'); let kernel; // let gemCreationPowers; @@ -65,77 +66,95 @@ export const makeKernel = (baggage) => { // "GemNamespaces" are created bc zones dont allow you to lookup subZones by name more than once // so this loads the subZones and stores only once. registered child namespaces are cached. + // Additionally, you cant ask what subzones exist. so we need to keep track of them ourselves. + // So GemNamespaces serve as an abstraction for managing the gem class registration tree const loadGemNamespaceFromGemZone = (gemZone) => { + const childCache = new Map(); + const data = gemZone.mapStore('data') + const instances = gemZone.weakMapStore('instances') + const registry = gemZone.subZone('gemRegistry') + const childKeys = gemZone.setStore('childKeys') const namespace = { - data: gemZone.mapStore('data'), - instances: gemZone.weakMapStore('instances'), - registry: gemZone.subZone('gemRegistry'), - zone: gemZone, - children: {}, + initRecipe (gemRecipe) { + data.init('recipe', harden(gemRecipe)); + }, + getRecipe () { + return data.get('recipe'); + }, + getStoreForInstance (instance, initFn) { + if (!instances.has(instance)) { + const value = harden(initFn()); + instances.init(instance, value); + } + const store = { + get () { + return instances.get(instance); + }, + set (value) { + instances.set(instance, harden(value)); + }, + } + return store; + }, + // methods + lookupChild (name) { + if (childCache.has(name)) { + return childCache.get(name); + } + childKeys.add(name); + const childGemZone = registry.subZone(name); + const chilldNamespace = loadGemNamespaceFromGemZone(childGemZone); + childCache.set(name, chilldNamespace); + return chilldNamespace; + }, + getChildKeys () { + return Array.from(childKeys.values()); + }, + exoClass (...args) { + return gemZone.exoClass(...args); + }, } return namespace; } - const lookupChildGemNamespace = (parentGemNamespace, name) => { - if (parentGemNamespace.children[name]) { - return parentGemNamespace.children[name]; - } - const gemZone = parentGemNamespace.registry.subZone(name); - const namespace = loadGemNamespaceFromGemZone(gemZone); - parentGemNamespace.children[name] = namespace; - return namespace; - } // stores a gem recipe in the registry. for each gem, called once per universe. const registerGem = (parentGemNamespace, gemRecipe) => { - const gemNs = lookupChildGemNamespace(parentGemNamespace, gemRecipe.name); - gemNs.data.init('recipe', harden(gemRecipe)); + const { name } = gemRecipe; + const gemNs = parentGemNamespace.lookupChild(name); + gemNs.initRecipe(harden(gemRecipe)); + console.log('registered gem', name); } // used internally. defines a registered gem class. for each gem, called once per process. const loadGem = (parentGemNamespace, name) => { - const gemNs = lookupChildGemNamespace(parentGemNamespace, name); - const gemRecipe = gemNs.data.get('recipe'); - - const getStoreForInstance = (instance) => { - if (!gemNs.instances.has(instance)) { - const store = harden(init()); - gemNs.instances.init(instance, store); - } - const store = { - get () { - return gemNs.instances.get(instance); - }, - set (value) { - gemNs.instances.set(instance, harden(value)); - }, - } - return store; - } - + const gemNs = parentGemNamespace.lookupChild(name); + const gemRecipe = gemNs.getRecipe(); const { code } = gemRecipe; const compartment = new Compartment(); const constructGem = compartment.evaluate(code); const { interface: interfaceGuards, init, methods } = constructGem({ M, gemName: name, - getStore: getStoreForInstance, + getStore: (instance) => gemNs.getStoreForInstance(instance, init), }); - - return gemNs.zone.exoClass(name, interfaceGuards, initWithPassthrough, methods); + console.log('loaded gem', name); + return gemNs.exoClass(name, interfaceGuards, initWithPassthrough, methods); } // reincarnate all registered gems // TODO: should be as lazy as possible - // const walkGemRegistry = (gemZone) => { - // const gemRegistry = gemZone.mapStore('gemRegistry'); - // for (const [name, gemRecipe] of gemRegistry.entries()) { - // loadGem(gemZone, name); - // } - // } - // walkGemRegistry(rootGemZone); - const rootGemNamespace = loadGemNamespaceFromGemZone(rootGemZone); + + const walkGemRegistry = (gemNs) => { + for (const name of gemNs.getChildKeys()) { + loadGem(gemNs, name); + const childGemNs = gemNs.lookupChild(name); + walkGemRegistry(childGemNs); + } + } + walkGemRegistry(rootGemNamespace); + const registerRootGem = (gemRecipe) => { registerGem(rootGemNamespace, gemRecipe); } @@ -148,7 +167,7 @@ export const makeKernel = (baggage) => { return makeGem(); } - kernel = { registerGem: registerRootGem, makeGem: makeRootGem }; + kernel = { registerGem: registerRootGem, makeGem: makeRootGem, store: rootStore, ns: rootGemNamespace }; // const incarnateEvalGem = ({ name: childName, interface: childInterfaceGuards, code }) => { // // TODO: this could happen in another Realm diff --git a/packages/gems/test/index.test.js b/packages/gems/test/index.test.js index bb3ce39fb3..27eec30376 100644 --- a/packages/gems/test/index.test.js +++ b/packages/gems/test/index.test.js @@ -2,7 +2,7 @@ import test from '@endo/ses-ava/prepare-endo.js'; import '@agoric/swingset-liveslots/tools/setup-vat-data.js'; import { E } from '@endo/far'; import { M } from '@endo/patterns'; -import { makeScenario } from './util.js'; +import { makeVat } from './util.js'; /* @@ -14,43 +14,6 @@ TODO: */ -test('lifecycle - ping/gc', async t => { - const gemName = 'PingGem'; - const gemRecipe = { - name: gemName, - interface: M.interface(gemName, { - ping: M.callWhen().returns(M.string()), - }), - methods: { - async ping() { - return 'pong'; - }, - }, - }; - - const { aliceKit, bobKit } = makeScenario({ recipeForBoth: gemRecipe }); - // bob's bootstrap is alice and vice versa - const alice = await bobKit.captpKit.getBootstrap(); - - console.log('ping ->'); - console.log(' <-', await E(alice).ping()); - console.log('ping ->'); - console.log(' <-', await E(alice).ping()); - // await aliceKit.gem.wakeController.sleep(); - - console.log('ping ->'); - console.log(' <-', await E(alice).ping()); - - // console.log('...attempting to trigger timebased GC...'); - // await delay(10e3); - - console.log('ping ->'); - console.log(' <-', await E(alice).ping()); - - // this is just an example - t.pass(); -}); - test.only('persistence - simple json counter', async t => { const gemName = 'CounterGem'; const gemRecipe = { @@ -63,7 +26,6 @@ test.only('persistence - simple json counter', async t => { init: () => ({ count: 0 }), methods: { async increment() { - // const { store } = this.state; const store = getStore(this.self); let { count } = store.get(); count += 1; @@ -71,7 +33,6 @@ test.only('persistence - simple json counter', async t => { return count; }, async getCount() { - // const { store } = this.state; const store = getStore(this.self); const { count } = store.get(); return count; @@ -80,19 +41,21 @@ test.only('persistence - simple json counter', async t => { })}` }; - const { aliceKit, bobKit } = makeScenario({ recipeForBoth: gemRecipe }); - // bob's bootstrap is alice and vice versa - const alice = await bobKit.captpKit.getBootstrap(); + const vat = makeVat(); + let kernel = vat.restart(); + let counter = kernel.makeGem(gemRecipe); + kernel.store.init('counter', counter); - t.deepEqual(await E(alice).getCount(), 0); - await E(alice).increment(); - t.deepEqual(await E(alice).getCount(), 1); + t.deepEqual(await E(counter).getCount(), 0); + await E(counter).increment(); + t.deepEqual(await E(counter).getCount(), 1); - // await aliceKit.gem.wakeController.sleep(); + kernel = vat.restart(); + counter = kernel.store.get('counter'); - t.deepEqual(await E(alice).getCount(), 1); - await Promise.all([E(alice).increment(), E(alice).increment()]); - t.deepEqual(await E(alice).getCount(), 3); + t.deepEqual(await E(counter).getCount(), 1); + await Promise.all([E(counter).increment(), E(counter).increment()]); + t.deepEqual(await E(counter).getCount(), 3); }); // TODO: need to untangle captp remote refs for persistence diff --git a/packages/gems/test/util.js b/packages/gems/test/util.js index c0784fa3cb..d917a8b11e 100644 --- a/packages/gems/test/util.js +++ b/packages/gems/test/util.js @@ -1,9 +1,6 @@ -import { makePipe } from '@endo/stream'; -import { makeMessageCapTP, makeKernel } from '../src/index.js'; +import { makeKernel } from '../src/index.js'; import { reincarnate } from '@agoric/swingset-liveslots/tools/setup-vat-data.js'; -const never = new Promise(() => {}); - const setupWorld = (fakeStore) => { const { fakeVomKit } = reincarnate({ relaxDurabilityRules: false, fakeStore }); const { vom, cm, vrm } = fakeVomKit; @@ -16,45 +13,20 @@ const setupWorld = (fakeStore) => { return { baggage, flush }; }; -export const makeScenario = ({ - recipeForBoth, - recipeForAlice = recipeForBoth, - recipeForBob = recipeForBoth, -}) => { - const [writerA, readerB] = makePipe(); - const [writerB, readerA] = makePipe(); - - const fakeStore = new Map(); - const { baggage } = setupWorld(fakeStore); - - const kernel = makeKernel(baggage); - - const gemA = kernel.makeGem({ - ...recipeForAlice, - name: `${recipeForAlice.name}-alice`, - }); - const captpKitA = makeMessageCapTP( - 'Alice', - writerA, - readerA, - never, - gemA, - ); - - const gemB = kernel.makeGem({ - ...recipeForBob, - name: `${recipeForBob.name}-bob`, - }); - const captpKitB = makeMessageCapTP('Bob', writerB, readerB, never, gemB); +export const makeVat = () => { + let fakeStore, baggage, flush, kernel; + + const restart = () => { + if (flush) { + flush(); + } + fakeStore = new Map(fakeStore); + ({ baggage, flush } = setupWorld(fakeStore)); + kernel = makeKernel(baggage); + return kernel; + } return { - aliceKit: { - captpKit: captpKitA, - gem: gemA, - }, - bobKit: { - captpKit: captpKitB, - gem: gemB, - }, - }; + restart, + } };