Skip to content

Commit

Permalink
fix: improve API to punt serialization to the backing store
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed Oct 10, 2020
1 parent c50d6a8 commit fbfc0e7
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 64 deletions.
54 changes: 26 additions & 28 deletions packages/store/src/external/hydrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,13 @@ import { makeStore } from '../store';
* @returns {MakeHydrateExternalStore<A, T>}
*/
export const makeHydrateExternalStoreMaker = makeBackingStore => {
const serialize = JSON.stringify;
const unserialize = JSON.parse;

/** @type {WeakStore<T, [string, string]>} */
/** @type {WeakStore<T, HydrateKey>} */
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.
Expand All @@ -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<A, T>} */
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;
Expand Down
6 changes: 3 additions & 3 deletions packages/store/src/external/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import '../types';
* secondary storage.
*
* @template {(...args: any[]) => ExternalInstance} M
* @param {string} instanceName
* @param {string} keyName
* @param {M} maker
* @returns {ExternalStore<M>}
*/
export function makeMemoryExternalStore(instanceName, maker) {
export function makeMemoryExternalStore(keyName, maker) {
return harden({
makeInstance: maker,
makeWeakStore() {
return makeWeakStore(instanceName);
return makeWeakStore(keyName);
},
});
}
Expand Down
54 changes: 34 additions & 20 deletions packages/store/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
*/

Expand All @@ -40,23 +44,28 @@
*/

/**
* 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<any>) => ExternalInstance} C
* @template {(...args: Array<any>) => ExternalInstance} M
* @typedef {Object} ExternalStore
* @property {C} makeInstance
* @property {MakeWeakStore<ReturnType<C>, any>} makeWeakStore
* @property {M} makeInstance Create a fresh instance
* @property {MakeWeakStore<ReturnType<M>, any>} makeWeakStore Create an
* external weak store indexed by an instance
*/

/**
* @typedef {Record<string, any>} HydrateData
*/

/**
* @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
*/

/**
Expand All @@ -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<string, string> & { makeWeakStore: () => WeakStore<any, any> }}} 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<ExternalInstance, any>} 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
*/
42 changes: 29 additions & 13 deletions packages/store/test/test-external-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<number, HydrateStore>} */
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;
},
};
});
Expand All @@ -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 => {
Expand All @@ -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();
Expand Down

0 comments on commit fbfc0e7

Please sign in to comment.