Skip to content

Commit

Permalink
feat(gems): extRefController can trigger captp reconnect
Browse files Browse the repository at this point in the history
  • Loading branch information
kumavis committed Oct 1, 2024
1 parent d199df4 commit f1ea4a3
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 104 deletions.
1 change: 1 addition & 0 deletions packages/gems/src/custom-kind.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const makeCustomDurableKindWithContext = (
instanceData.init(instanceSlot, harden(context));
// register the slot with the value, so it can be stored
fakeStuff.registerEntry(instanceSlot, value, false);
return value;
};

const reanimate = instanceSlot => {
Expand Down
40 changes: 21 additions & 19 deletions packages/gems/src/delegated-presence.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
// immediately makes a presence for a promise
export const makeEventualFactoryDelegate = targetP => {
export const makeEventualFactoryLookupDelegate = getTarget => {
let presence;
new HandledPromise((_resolve, _reject, resolveWithPresence) => {
presence = resolveWithPresence({
applyMethod(_o, _prop, _args, _res) {
return HandledPromise.applyMethod(targetP, _prop, _args);
},
get(_o, _prop, _res) {
return HandledPromise.get(targetP, _prop);
},
// Coming soon...
// set(_o, _prop, _value, _res) {
// return HandledPromise.set(targetP, _prop, _value);
// },
// deleteProperty(_o, _prop, _res) {
// return HandledPromise.deleteProperty(targetP, _prop);
// },
});
});
return presence;
const promise = new HandledPromise(
(_resolve, _reject, resolveWithPresence) => {
presence = resolveWithPresence({
applyMethod(_o, _prop, _args, _res) {
return HandledPromise.applyMethod(getTarget(), _prop, _args);
},
get(_o, _prop, _res) {
return HandledPromise.get(getTarget(), _prop);
},
// Coming soon...
// set(_o, _prop, _value, _res) {
// return HandledPromise.set(targetP, _prop, _value);
// },
// deleteProperty(_o, _prop, _res) {
// return HandledPromise.deleteProperty(targetP, _prop);
// },
});
},
);
return { promise, presence };
};
12 changes: 9 additions & 3 deletions packages/gems/src/durable-captp.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { makeNetstringCapTP } from './daemon-vendor/connection.js';
import { installExtRefController } from './extref-controller.js';
import {
installExternalReferenceController,
makeCaptpOptionsForExtRefController,
} from './extref-controller.js';
import { makeSimplePresenceController } from './presence-controller.js';

export const makeDurableCaptp = (
name,
Expand All @@ -12,12 +16,14 @@ export const makeDurableCaptp = (
) => {
let captp;
const getCaptp = () => captp;
const { captpOpts: extRefOpts } = installExtRefController(
const presenceController = makeSimplePresenceController(getCaptp);
const extRefController = installExternalReferenceController(
name,
zone,
fakeVomKit,
getCaptp,
presenceController,
);
const extRefOpts = makeCaptpOptionsForExtRefController(extRefController);
const captpOpts = { ...opts, ...extRefOpts };
const netStringCaptpClient = makeNetstringCapTP(
name,
Expand Down
144 changes: 62 additions & 82 deletions packages/gems/src/extref-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,124 +4,104 @@ import { makeCustomDurableKindWithContext } from './custom-kind.js';
* @param {string} label
* @param {import('@agoric/zone').Zone} zone
* @param {any} fakeVomKit
* @param {() => any} getCaptp
* @param {import('./presence-controller').PresenceController} presenceController
*/
export const installExtRefController = (label, zone, fakeVomKit, getCaptp) => {
const imports = zone.setStore('imports');
const exports = zone.mapStore('exports');
export const installExternalReferenceController = (
label,
zone,
fakeVomKit,
presenceController,
) => {
const { makePresenceForSlot, cleanupPresenceForSlot } = presenceController;
const { fakeStuff } = fakeVomKit;

// TODO: "imports" should be a weakRefMap
// We want to cache the presence for a slot,
// but we don't need to hold on to it.
const imports = zone.mapStore('imports');
const exports = zone.mapStore('exports');

const makeExtRef = makeCustomDurableKindWithContext(fakeVomKit, zone, {
make: (context, value, remoteSlot) => {
make: (context, remoteSlot, iface) => {
context.ref = remoteSlot;
return value;
context.iface = iface;
return makePresenceForSlot(remoteSlot, iface);
},
reanimate: context => {
const { ref: remoteSlot } = context;
const captp = getCaptp();
// console.log(`## ${label} reanimate`, remoteSlot)
return captp.importSlot(remoteSlot);
const { ref: remoteSlot, iface } = context;
return makePresenceForSlot(remoteSlot, iface);
},
cleanup: context => {
const { ref: remoteSlot } = context;
imports.delete(remoteSlot);
// indicate no further GC needed
return false;
// We don't need to clear from imports here,
// because its already been dropped before this call can happen.
// Return GC hint -- true if potentially more GC needed.
return cleanupPresenceForSlot(remoteSlot);
},
});

// marks a captp imported presence as durable,
const registerImport = (remoteSlot, value) => {
const registerImport = (remoteSlot, iface) => {
// console.log(`- ${label} registerImport ${remoteSlot}`);
if (imports.has(remoteSlot)) {
return;
return imports.get(remoteSlot);
}
// console.log(`!! ${label} registerExtRef`, value, remoteSlot)
// Note: extRef === value
makeExtRef(value, remoteSlot);
imports.add(remoteSlot);
const extRef = makeExtRef(remoteSlot, iface);
imports.init(remoteSlot, extRef);
return extRef;
};

const registerExport = value => {
// console.log(`- ${label} registerExport ${value}`);
const durableSlot = fakeStuff.getSlotForVal(value);
if (durableSlot === undefined) {
throw new Error(
`(${label}) registerExport - value not registered: ${value}`,
);
throw new Error(`registerExport - value not registered: ${value}`);
}
if (!exports.has(durableSlot)) {
exports.init(durableSlot, value);
}
return durableSlot;
};
const unregisterExport = slot => {
if (exports.has(slot)) {
exports.delete(slot);
const unregisterExport = localSlot => {
// console.log(`- ${label} unregisterExport ${localSlot}`);
if (exports.has(localSlot)) {
exports.delete(localSlot);
return true;
}
return false;
};
const lookupExport = slot => {
if (!exports.has(slot)) {
const lookupExport = localSlot => {
// console.log(`- ${label} lookupExport ${localSlot}`);
if (!exports.has(localSlot)) {
throw new Error(
`(${label}) lookupExport - value not held for slot: ${slot}`,
`(${label}) lookupExport - value not held for slot: ${localSlot}`,
);
}
return exports.get(slot);
return exports.get(localSlot);
};

const isPromiseSlot = slot => {
return slot[0] === 'p';
};
return { registerImport, registerExport, unregisterExport, lookupExport };
};

const captpOpts = {
//
// standard options
//
gcImports: true,
exportHook(val, captpSlot) {
// NOTE: we only want to handle non-promises
if (isPromiseSlot(captpSlot)) {
return;
}
// console.log(`>> ${label} exportHook`, val, captpSlot, durableSlot)
// This value has been exported, so we add it to our table
registerExport(val);
export const makeCaptpOptionsForExtRefController = controller => {
const captpOptions = {
onBeforeImportHook: (slot, iface) => {
// Returns the presence for the remote slot.
return controller.registerImport(slot, iface);
},
importHook(val, captpSlot) {
// We only want to handle non-promises
if (isPromiseSlot(captpSlot)) {
return;
}
// console.log(`<< ${label} importHook`, val, captpSlot);
// We know the other side has used "valueToSlotHook" to provide a durable slot
const remoteDurableSlot = captpSlot;
// establish durability for this imported reference
registerImport(remoteDurableSlot, val);
onBeforeExportHook: value => {
// Returns the slot for the value.
return controller.registerExport(value);
},
//
// extended options
//
valueToSlotHook(val) {
// Captp is asking us to provide a slot for the value,
// we'll provide the durable slot
// TODO: could this slot collide with captp?
// maybe not because KindId/instanceId wont conflict
const durableSlot = fakeStuff.getSlotForVal(val);
// console.log(`>> ${label} valueToSlotHook`, val, durableSlot)
return durableSlot;
missingExportHook: slot => {
// The controller's export table has a longer lifetime than the captp session.
// Throws if not found.
return controller.lookupExport(slot);
},
missingExportHook(slot) {
// console.log(`$$ ${label} exporting missing slot`, slot)
// Captp is trying to use an export it doesn't have,
// so we need to provide it
const value = lookupExport(slot);
const captp = getCaptp();
captp.exportValue(value, slot);
},
gcHook(_val, slot) {
// console.log(`-- ${label} gcHook`, _val, slot)
// we can release this value
// NOTE: this will only work correctly if captp is using the vomkit WeakMap
unregisterExport(slot);
gcHook: slot => {
// Remote has reported that our export is no longer needed.
// Return GC hint -- true if potentially more GC needed.
return controller.unregisterExport(slot);
},
};

return { captpOpts };
return captpOptions;
};
108 changes: 108 additions & 0 deletions packages/gems/src/presence-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Remotable } from '@endo/captp';
import { makeEventualFactoryLookupDelegate } from './delegated-presence.js';

/**
* @typedef {object} PresenceController
* @property {function(string, string): any} makePresenceForSlot - Creates a presence for a given slot and interface.
* @property {function(string): boolean} cleanupPresenceForSlot - Cleans up the presence for a given slot.
* @property {function(): void} didDisconnect - Indicates when the connection is lost.
*/

/**
* @param {function(): any} getCaptp
* @returns {PresenceController}
*/
export const makeSimplePresenceController = getCaptp => {
// This is a cache of the presence for a slot, per captp session.
// It is cleared when the connection is lost.
// Individual items can be cleared via "cleanupPresenceForSlot".
const presenceForSlot = new Map();

// Provide a way to indicate when the connection is lost.
const didDisconnect = () => {
presenceForSlot.clear();
};

const makePresenceForSlot = (slot, iface) => {
if (presenceForSlot.has(slot)) {
return presenceForSlot.get(slot);
}
const captp = getCaptp();
const { settler } = captp.makeRemoteKit(slot);
const value = Remotable(iface, undefined, settler.resolveWithPresence());
presenceForSlot.set(slot, value);
captp.importSlot(value, slot);
return value;
};

const cleanupPresenceForSlot = slot => {
presenceForSlot.delete(slot);
// TODO: unsure if further GC needed
return false;
};

return { makePresenceForSlot, cleanupPresenceForSlot, didDisconnect };
};

/**
* @param {object} opts
* @param {() => Promise<any>} opts.getConnection
* @returns {PresenceController}
*/
export const makeReconnectingPresenceController = ({ getConnection }) => {
// This is a cache of the presence for a slot, per captp session.
// It is cleared when the connection is lost.
// Individual items can be cleared via "cleanupPresenceForSlot".
const presenceForSlot = new Map();

// Provide a way to indicate when the connection is lost.
// The cache MUST be cleared when the connection is lost,
// or the remotes will be broken.
const didDisconnect = () => {
console.log('+ presenceController didDisconnect');
presenceForSlot.clear();
};

const connectToRemoteSlot = async (slot, iface, delegatePresence) => {
console.log('+ presenceController connectToRemoteSlot', slot, iface);
// This is called for every message sent to the slot,
// so the cache is important.
// The value is good for the lifetime of the captp session.
// The value is not needed beyond the lifetime of the DelegatePresence,
// which is informed via the clearPresenceForSlot callback.
if (presenceForSlot.has(slot)) {
return presenceForSlot.get(slot);
}
const vatConnection = await getConnection();
// Create a new presence for the slot thats unregistered with CapTP.
const { captp } = vatConnection;
const { settler } = captp.makeRemoteKit(slot);
const value = Remotable(iface, undefined, settler.resolveWithPresence());
presenceForSlot.set(slot, value);
captp.importSlot(delegatePresence, slot);
return value;
};

const makePresenceForSlot = (slot, iface) => {
console.log('+ presenceController makePresenceForSlot', slot, iface);
const { presence: delegatePresence } = makeEventualFactoryLookupDelegate(
() => connectToRemoteSlot(slot, iface, delegatePresence),
);
const remotablePresence = Remotable(iface, undefined, delegatePresence);
console.log(
'+> presenceController makePresenceForSlot',
slot,
iface,
remotablePresence,
);
return remotablePresence;
};

const cleanupPresenceForSlot = slot => {
presenceForSlot.delete(slot);
// TODO: unsure if further GC needed
return false;
};

return { makePresenceForSlot, cleanupPresenceForSlot, didDisconnect };
};
Loading

0 comments on commit f1ea4a3

Please sign in to comment.