Skip to content

Commit

Permalink
wip(gems): use gemNamespace to abstract over zones + test restart
Browse files Browse the repository at this point in the history
  • Loading branch information
kumavis committed Sep 14, 2024
1 parent 2a5b57b commit f908f73
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 140 deletions.
113 changes: 66 additions & 47 deletions packages/gems/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Expand All @@ -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
Expand Down
63 changes: 13 additions & 50 deletions packages/gems/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/*
Expand All @@ -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 = {
Expand All @@ -63,15 +26,13 @@ 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;
store.set({ count });
return count;
},
async getCount() {
// const { store } = this.state;
const store = getStore(this.self);
const { count } = store.get();
return count;
Expand All @@ -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
Expand Down
58 changes: 15 additions & 43 deletions packages/gems/test/util.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
}
};

0 comments on commit f908f73

Please sign in to comment.