diff --git a/packages/store/src/external/hydrate.js b/packages/store/src/external/hydrate.js index 9db9c9e1718..cf88f94066e 100644 --- a/packages/store/src/external/hydrate.js +++ b/packages/store/src/external/hydrate.js @@ -21,16 +21,13 @@ import { makeStore } from '../store'; * @returns {MakeHydrateExternalStore} */ export const makeHydrateExternalStoreMaker = makeBackingStore => { - const serialize = JSON.stringify; - const unserialize = JSON.parse; - - /** @type {WeakStore} */ + /** @type {WeakStore} */ const instanceToKey = makeWeakStore('instance'); - let lastStoreKey = 0; + let lastStoreId = 0; - // This has to be a strong store, since it is indexed by key. - const storeKeyToHydrate = makeStore('storeKey'); + // This has to be a strong store, since it is indexed by ID. + const storeIdToHydrate = makeStore('storeId'); /** * Create a data object that queues writes to the store. @@ -54,59 +51,60 @@ export const makeHydrateExternalStoreMaker = makeBackingStore => { return harden(activeData); }; - /** - * @type {BackingStore} - */ + /** @type {BackingStore} */ let backing; + + /** @type {HydrateHook} */ const hydrateHook = { getKey(value) { return instanceToKey.get(value); }, - load([storeKey, instanceKey]) { - const hydrate = storeKeyToHydrate.get(storeKey); - const store = backing.findStore(storeKey); + load([storeId, instanceId]) { + const hydrate = storeIdToHydrate.get(storeId); + const store = backing.getHydrateStore(storeId); - const data = unserialize(store.get(instanceKey)); - const markDirty = () => store.set(instanceKey, serialize(data)); + const data = store.get(instanceId); + const markDirty = () => store.set(instanceId, data); const activeData = makeActiveData(data, markDirty); const obj = hydrate(activeData); - instanceToKey.init(obj, [storeKey, instanceKey]); + instanceToKey.init(obj, [storeId, instanceId]); return obj; }, - drop(storeKey) { - storeKeyToHydrate.delete(storeKey); + drop(storeId) { + storeIdToHydrate.delete(storeId); }, }; backing = makeBackingStore(hydrateHook); + /** @type {MakeHydrateExternalStore} */ function makeHydrateExternalStore(instanceName, adaptArguments, makeHydrate) { - let lastInstanceKey = 0; + let lastInstanceId = 0; - lastStoreKey += 1; - const storeKey = `${lastStoreKey}`; - const store = backing.makeStore(storeKey, instanceName); + lastStoreId += 1; + const storeId = lastStoreId; + const hstore = backing.makeHydrateStore(storeId, instanceName); const initHydrate = makeHydrate(true); - storeKeyToHydrate.init(storeKey, makeHydrate(undefined)); + storeIdToHydrate.init(storeId, makeHydrate()); /** @type {ExternalStore<(...args: A) => T>} */ const estore = { makeInstance(...args) { const data = adaptArguments(...args); // Create a new object with the above guts. - lastInstanceKey += 1; - const instanceKey = `${lastInstanceKey}`; + lastInstanceId += 1; + const instanceId = lastInstanceId; initHydrate(data); // We store and reload it to sanity-check the initial state and also to // ensure that the new object has active data. - store.init(instanceKey, serialize(data)); - return hydrateHook.load([storeKey, instanceKey]); + hstore.init(instanceId, data); + return hydrateHook.load([storeId, instanceId]); }, makeWeakStore() { - return store.makeWeakStore(); + return hstore.makeWeakStore(); }, }; return estore; diff --git a/packages/store/src/external/memory.js b/packages/store/src/external/memory.js index 977c8b07ef6..6cc3f88f307 100644 --- a/packages/store/src/external/memory.js +++ b/packages/store/src/external/memory.js @@ -10,15 +10,15 @@ import '../types'; * secondary storage. * * @template {(...args: any[]) => ExternalInstance} M - * @param {string} instanceName + * @param {string} keyName * @param {M} maker * @returns {ExternalStore} */ -export function makeMemoryExternalStore(instanceName, maker) { +export function makeMemoryExternalStore(keyName, maker) { return harden({ makeInstance: maker, makeWeakStore() { - return makeWeakStore(instanceName); + return makeWeakStore(keyName); }, }); } diff --git a/packages/store/src/types.js b/packages/store/src/types.js index 1bf8c12dadc..81bb9a541c0 100644 --- a/packages/store/src/types.js +++ b/packages/store/src/types.js @@ -6,10 +6,12 @@ * @template K,V * @typedef {Object} Store - A safety wrapper around a Map * @property {(key: K) => boolean} has - Check if a key exists - * @property {(key: K, value: V) => void} init - Initialize the key only if it doesn't already exist - * @property {(key: K) => V} get - Return a value for the key. Throws - * if not found. - * @property {(key: K, value: V) => void} set - Set the key. Throws if not found. + * @property {(key: K, value: V) => void} init - Initialize the key only if it + * doesn't already exist + * @property {(key: K) => V} get - Return a value for the key. Throws if not + * found. + * @property {(key: K, value: V) => void} set - Set the key. Throws if not + * found. * @property {(key: K) => void} delete - Remove the key. Throws if not found. * @property {() => K[]} keys - Return an array of keys * @property {() => V[]} values - Return an array of values @@ -20,10 +22,12 @@ * @template K,V * @typedef {Object} WeakStore - A safety wrapper around a WeakMap * @property {(key: any) => boolean} has - Check if a key exists - * @property {(key: K, value: V) => void} init - Initialize the key only if it doesn't already exist - * @property {(key: any) => V} get - Return a value for the key. Throws - * if not found. - * @property {(key: K, value: V) => void} set - Set the key. Throws if not found. + * @property {(key: K, value: V) => void} init - Initialize the key only if it + * doesn't already exist + * @property {(key: any) => V} get - Return a value for the key. Throws if not + * found. + * @property {(key: K, value: V) => void} set - Set the key. Throws if not + * found. * @property {(key: K) => void} delete - Remove the key. Throws if not found. */ @@ -40,12 +44,15 @@ */ /** - * An external store for a given constructor. + * An external store for a given maker function. + * TODO: We should provide makers for other kinds of data structures. + * Weak sorted lists, weak priority queues, and many others. * - * @template {(...args: Array) => ExternalInstance} C + * @template {(...args: Array) => ExternalInstance} M * @typedef {Object} ExternalStore - * @property {C} makeInstance - * @property {MakeWeakStore, any>} makeWeakStore + * @property {M} makeInstance Create a fresh instance + * @property {MakeWeakStore, any>} makeWeakStore Create an + * external weak store indexed by an instance */ /** @@ -53,10 +60,12 @@ */ /** + * @typedef {[number, number]} HydrateKey + * @typedef {true} HydrateInit * @typedef {Object} HydrateHook - * @property {(value: any) => [string, string]} getKey - * @property {(key: [string, string]) => any} load - * @property {(storeKey: string) => void} drop + * @property {(value: any) => HydrateKey} getKey + * @property {(key: HydrateKey) => any} load + * @property {(storeId: number) => void} drop */ /** @@ -68,16 +77,21 @@ * @callback MakeHydrateExternalStore * @param {string} instanceKind * @param {(...args: A) => HydrateData} adaptArguments - * @param {(init: boolean | undefined) => (data: HydrateData) => T} makeHydrate + * @param {(init?: HydrateInit) => (data: HydrateData) => T} makeHydrate * @returns {ExternalStore<(...args: A) => T>} */ /** - * @typedef {Store & { makeWeakStore: () => WeakStore }}} ExternalInstanceStore + * @typedef {Object} HydrateStore The store needed to save closed-over + * per-instance data + * @property {(id: number, data: HydrateData) => void} init + * @property {(id: number) => HydrateData} get + * @property {(id: number, data: HydrateData) => void} set + * @property {() => WeakStore} makeWeakStore */ /** - * @typedef {Object} BackingStore - * @property {(storeId: string, instanceKind: string) => ExternalInstanceStore} makeStore - * @property {(storeId: string) => ExternalInstanceStore} findStore + * @typedef {Object} BackingStore This is the master store that reifies storeIds + * @property {(storeId: number, instanceKind: string) => HydrateStore} makeHydrateStore + * @property {(storeId: number) => HydrateStore} getHydrateStore */ diff --git a/packages/store/test/test-external-store.js b/packages/store/test/test-external-store.js index e09effe4152..eff9fd3c4d7 100644 --- a/packages/store/test/test-external-store.js +++ b/packages/store/test/test-external-store.js @@ -50,23 +50,39 @@ test('original sources', t => { test('rewritten code', t => { /** @type {HydrateHook} */ - let swingSetHydrateHook; - const makeSwingSetCollection = makeHydrateExternalStoreMaker(hydrateHook => { - swingSetHydrateHook = hydrateHook; + let vatHydrateHook; + const makeVatExternalStore = makeHydrateExternalStoreMaker(hydrateHook => { + vatHydrateHook = hydrateHook; + /** @type {Store} */ const idToStore = makeStore('storeId'); return { - findStore(storeId) { + getHydrateStore(storeId) { return idToStore.get(storeId); }, - makeStore(storeId, instanceKind) { + makeHydrateStore(storeId, instanceKind) { + // This implementation is totally leaky. const store = makeStore(`${instanceKind} ids`); - idToStore.init(storeId, store); - return { - ...store, + + // We use JSON here just as a minimal test. Real implementations will + // want something like @agoric/marshal. + /** @type {HydrateStore} */ + const hstore = { + init(id, data) { + store.init(id, JSON.stringify(data)); + }, + get(id) { + return JSON.parse(store.get(id)); + }, + set(id, data) { + store.set(id, JSON.stringify(data)); + }, makeWeakStore() { return makeWeakStore(instanceKind); }, }; + harden(hstore); + idToStore.init(storeId, hstore); + return hstore; }, }; }); @@ -86,7 +102,7 @@ test('rewritten code', t => { * * Declarations are not considered side-effects. */ - const store = makeSwingSetCollection( + const store = makeVatExternalStore( 'Hello instance', (msg = 'Hello') => ({ msg }), $hinit => $hdata => { @@ -108,13 +124,13 @@ test('rewritten code', t => { ); const h = runTests(t, store.makeInstance); - const key = swingSetHydrateHook.getKey(h); - t.deepEqual(key, ['1', '1']); - const h2 = swingSetHydrateHook.load(key); + const key = vatHydrateHook.getKey(h); + t.deepEqual(key, [1, 1]); + const h2 = vatHydrateHook.load(key); // We get a different representative, which shares the key. t.not(h2, h); - t.deepEqual(swingSetHydrateHook.getKey(h2), ['1', '1']); + t.deepEqual(vatHydrateHook.getKey(h2), [1, 1]); // The methods are there now, too. const last = h.getCount();