@agoric/zoe
Version:
Zoe: the Smart Contract Framework for Offer Enforcement
271 lines (253 loc) • 8.74 kB
JavaScript
import { Fail } from '@endo/errors';
import { StorageNodeShape } from '@agoric/internal';
import { prepareDurablePublishKit } from '@agoric/notifier';
import {
makeFakeMarshaller,
makeFakeStorage,
} from '@agoric/notifier/tools/testSupports.js';
import { mustMatch } from '@agoric/store';
import { M, makeScalarBigMapStore, prepareExoClass } from '@agoric/vat-data';
import { E } from '@endo/eventual-send';
/**
* @import {TypedPattern} from '@agoric/internal';
*/
/**
* Recorders support publishing data to vstorage.
*
* `Recorder` is similar to `Publisher` (in that they send out data) but has different signatures:
* - methods are async because they await remote calls to Marshaller and StorageNode
* - method names convey the durability
* - omits fail()
* - adds getStorageNode() from its durable state
*
* Other names such as StoredPublisher or ChainStoragePublisher were considered, but found to be sometimes confused with *durability*, another trait of this class.
*/
/**
* @template T
* @typedef {{ getStorageNode(): Awaited<import('@endo/far').FarRef<StorageNode>>, getStoragePath(): Promise<string>, write(value: T): Promise<void>, writeFinal(value: T): Promise<void> }} Recorder
*/
/**
* @template T
* @typedef {Pick<PublishKit<T>, 'subscriber'> & { recorder: Recorder<T> }} RecorderKit
*/
/**
* @template T
* @typedef {Pick<PublishKit<T>, 'subscriber'> & { recorderP: ERef<Recorder<T>> }} EventualRecorderKit
*/
/**
* Wrap a Publisher to record all the values to chain storage.
*
* @param {import('@agoric/vat-data').Baggage} baggage
* @param {ERef<Marshaller>} marshaller
*/
export const prepareRecorder = (baggage, marshaller) => {
const makeRecorder = prepareExoClass(
baggage,
'Recorder',
M.interface('Recorder', {
getStorageNode: M.call().returns(StorageNodeShape),
getStoragePath: M.call().returns(M.promise(/* string */)),
write: M.call(M.any()).returns(M.promise()),
writeFinal: M.call(M.any()).returns(M.promise()),
}),
/**
* @template T
* @param {PublishKit<T>['publisher']} publisher
* @param {Awaited<import('@endo/far').FarRef<StorageNode>>} storageNode
* @param {TypedPattern<any>} [valueShape]
*/
(
publisher,
storageNode,
valueShape = /** @type {TypedPattern<any>} */ (M.any()),
) => {
return {
closed: false,
publisher,
storageNode,
storagePath: /** @type {string | undefined} */ (undefined),
valueShape,
};
},
{
getStorageNode() {
return this.state.storageNode;
},
/**
* Memoizes the remote call to the storage node
*
* @returns {Promise<string>}
*/
async getStoragePath() {
const { storagePath: heldPath } = this.state;
// end synchronous prelude
await null;
if (heldPath !== undefined) {
return heldPath;
}
const path = await E(this.state.storageNode).getPath();
this.state.storagePath = path;
return path;
},
/**
* Marshalls before writing to storage or publisher to help ensure the two streams match.
*
* @param {any} value
* @returns {Promise<void>}
*/
async write(value) {
const { closed, publisher, storageNode, valueShape } = this.state;
!closed || Fail`cannot write to closed recorder`;
mustMatch(value, valueShape);
const encoded = await E(marshaller).toCapData(value);
const serialized = JSON.stringify(encoded);
await E(storageNode).setValue(serialized);
// below here differs from writeFinal()
return publisher.publish(value);
},
/**
* Like `write` but prevents future writes and terminates the publisher.
*
* @param {any} value
* @returns {Promise<void>}
*/
async writeFinal(value) {
const { closed, publisher, storageNode, valueShape } = this.state;
!closed || Fail`cannot write to closed recorder`;
mustMatch(value, valueShape);
const encoded = await E(marshaller).toCapData(value);
const serialized = JSON.stringify(encoded);
await E(storageNode).setValue(serialized);
// below here differs from write()
this.state.closed = true;
return publisher.finish(value);
},
},
);
return makeRecorder;
};
harden(prepareRecorder);
/** @typedef {ReturnType<typeof prepareRecorder>} MakeRecorder */
/**
* `makeRecorderKit` is suitable for making a durable `RecorderKit` which can be held in Exo state.
*
* @see {defineERecorderKit}
*
* @param {{makeRecorder: MakeRecorder, makeDurablePublishKit: ReturnType<typeof prepareDurablePublishKit>}} makers
*/
export const defineRecorderKit = ({ makeRecorder, makeDurablePublishKit }) => {
/**
* @template T
* @param {StorageNode | Awaited<import('@endo/far').FarRef<StorageNode>>} storageNode
* @param {TypedPattern<T>} [valueShape]
* @returns {RecorderKit<T>}
*/
const makeRecorderKit = (storageNode, valueShape) => {
const { subscriber, publisher } = makeDurablePublishKit();
const recorder = makeRecorder(
publisher,
/** @type { Awaited<import('@endo/far').FarRef<StorageNode>> } */ (
storageNode
),
valueShape,
);
return harden({ subscriber, recorder });
};
return makeRecorderKit;
};
/** @typedef {ReturnType<typeof defineRecorderKit>} MakeRecorderKit */
/**
* `makeERecorderKit` is for closures that must return a `subscriber` synchronously but can defer the `recorder`.
*
* @see {defineRecorderKit}
*
* @param {{makeRecorder: MakeRecorder, makeDurablePublishKit: ReturnType<typeof prepareDurablePublishKit>}} makers
*/
export const defineERecorderKit = ({ makeRecorder, makeDurablePublishKit }) => {
/**
* @template T
* @param {ERef<StorageNode>} storageNodeP
* @param {TypedPattern<T>} [valueShape]
* @returns {EventualRecorderKit<T>}
*/
const makeERecorderKit = (storageNodeP, valueShape) => {
const { publisher, subscriber } = makeDurablePublishKit();
const recorderP = E.when(storageNodeP, storageNode =>
makeRecorder(
publisher,
// @ts-expect-error Casting because it's remote
/** @type { import('@endo/far').FarRef<StorageNode> } */ (storageNode),
valueShape,
),
);
return { subscriber, recorderP };
};
return makeERecorderKit;
};
harden(defineERecorderKit);
/** @typedef {ReturnType<typeof defineERecorderKit>} MakeERecorderKit */
/**
* Convenience wrapper to prepare the DurablePublishKit and Recorder kinds.
* Note that because prepareRecorder() can only be called once per baggage,
* this should only be used when there is no need for an EventualRecorderKit.
* When there is, prepare the kinds separately and pass to the kit definers.
*
* @param {import('@agoric/vat-data').Baggage} baggage
* @param {ERef<Marshaller>} marshaller
*/
export const prepareRecorderKit = (baggage, marshaller) => {
const makeDurablePublishKit = prepareDurablePublishKit(
baggage,
'Durable Publish Kit',
);
const makeRecorder = prepareRecorder(baggage, marshaller);
return defineRecorderKit({ makeDurablePublishKit, makeRecorder });
};
/**
* Convenience wrapper for DurablePublishKit and Recorder kinds.
*
* NB: this defines two durable kinds. Must be called at most once per baggage.
*
* `makeRecorderKit` is suitable for making a durable `RecorderKit` which can be held in Exo state.
* `makeERecorderKit` is for closures that must return a `subscriber` synchronously but can defer the `recorder`.
*
* @param {import('@agoric/vat-data').Baggage} baggage
* @param {ERef<Marshaller>} marshaller
*/
export const prepareRecorderKitMakers = (baggage, marshaller) => {
const makeDurablePublishKit = prepareDurablePublishKit(
baggage,
'Durable Publish Kit',
);
const makeRecorder = prepareRecorder(baggage, marshaller);
const makeRecorderKit = defineRecorderKit({
makeRecorder,
makeDurablePublishKit,
});
const makeERecorderKit = defineERecorderKit({
makeRecorder,
makeDurablePublishKit,
});
return {
makeDurablePublishKit,
makeRecorder,
makeRecorderKit,
makeERecorderKit,
};
};
/**
* For use in tests
*/
export const prepareMockRecorderKitMakers = () => {
const baggage = makeScalarBigMapStore('mock recorder baggage');
const marshaller = makeFakeMarshaller();
return {
...prepareRecorderKitMakers(baggage, marshaller),
storageNode: makeFakeStorage('mock recorder storage'),
};
};
export const RecorderKitShape = {
recorder: M.remotable(),
subscriber: M.remotable(),
};
harden(RecorderKitShape);