Skip to content

Commit

Permalink
feat(inter-cli): board client preserving identity from vstorage
Browse files Browse the repository at this point in the history
  • Loading branch information
dckc committed Jun 20, 2023
1 parent 7e3de57 commit 2d7a2c5
Show file tree
Hide file tree
Showing 2 changed files with 284 additions and 0 deletions.
178 changes: 178 additions & 0 deletions packages/inter-cli/src/lib/boardClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// @ts-check
import { Far } from '@endo/far';
import { makeMarshal } from '@endo/marshal';
import { assertCapData } from '@agoric/internal/src/lib-chainStorage.js';
import { BrandShape, DisplayInfoShape, IssuerShape } from '@agoric/ertp';
import { M, mustMatch } from '@endo/patterns';
import { deeplyFulfilledObject } from '@agoric/internal';
import { extractStreamCellValue } from './vstorage.js';

const { Fail } = assert;

export const makeBoardContext = () => {
/** @type {Map<string, {}>} */
const idToValue = new Map();
/** @type {Map<unknown, string>} */
const valueToId = new Map();

/**
* Provide a remotable for each slot.
*
* @param {string} slot
* @param {string} [iface] non-empty if present
*/
const provide = (slot, iface) => {
if (idToValue.has(slot)) {
return idToValue.get(slot) || Fail`cannot happen`; // XXX check this statically?
}
if (!iface) throw Fail`1st occurrence must provide iface`;
const json = { _: iface };
// XXX ok to leave iface alone?
/** @type {{}} */
const value = Far(iface, { toJSON: () => json });
idToValue.set(slot, value);
valueToId.set(value, slot);
return value;
};

/** Read-only board */
const board = {
/** @param {unknown} value */
getId: value => {
valueToId.has(value) || Fail`unknown value: ${value}`;
return valueToId.get(value) || Fail`cannot happen`; // XXX check this statically?
},

/** @param {string} id */
getValue: id => {
assert.typeof(id, 'string');
idToValue.has(id) || Fail`unknown id: ${id}`;
return idToValue.get(id) || Fail`cannot happen`; // XXX check this statically?
},
};

const marshaller = makeMarshal(board.getId, provide, {
serializeBodyFormat: 'smallcaps',
});

return harden({
board,
register: provide,
marshaller,
/**
* Unmarshall capData, creating a Remotable for each boardID slot.
*
* @type {(cd: import("@endo/marshal").CapData<string>) => unknown }
*/
ingest: marshaller.fromCapData,
});
};

/** @param {QueryDataResponseT} queryDataResponse */
const extractCapData = queryDataResponse => {
const str = extractStreamCellValue(queryDataResponse);
const x = harden(JSON.parse(str));
assertCapData(x);
return x;
};

// XXX where is this originally defined? vat-bank?
/**
* @typedef {{
* brand: Brand,
* denom: string,
* displayInfo: DisplayInfo,
* issuer: Issuer,
* issuerName: string,
* proposedName: string,
* }} VBankAssetDetail
*/
const AssetDetailShape = harden({
brand: BrandShape,
denom: M.string(),
displayInfo: DisplayInfoShape,
issuer: IssuerShape,
issuerName: M.string(),
proposedName: M.string(),
});
const InstanceShape = M.remotable('Instance');
const kindInfo = /** @type {const} */ ({
brand: {
shape: BrandShape,
coerce: x => /** @type {Brand} */ (x),
},
instance: {
shape: InstanceShape,
coerce: x => /** @type {Instance} */ (x),
},
vbankAsset: {
shape: AssetDetailShape,
coerce: x => /** @type {VBankAssetDetail} */ (x),
},
});

/**
* @param {ReturnType<typeof makeBoardContext>} boardCtx
* @param {import('@agoric/cosmic-proto/vstorage/query.js').QueryClientImpl} queryService
*/
const makeAgoricNames = async (boardCtx, queryService) => {
/**
* @template T
* @param {keyof typeof kindInfo} kind
* @param {(x: any) => T} _coerce
*/
const getKind = async (kind, _coerce) => {
const queryDataResponse = await queryService.Data({
path: `published.agoricNames.${kind}`,
});
const capData = extractCapData(queryDataResponse);
const xs = boardCtx.ingest(capData);
mustMatch(xs, M.arrayOf([M.string(), kindInfo[kind].shape]));
/** @type {[string, ReturnType<typeof _coerce>][]} */
// @ts-expect-error runtime checked
const entries = xs;
const record = harden(Object.fromEntries(entries));
return record;
};
const agoricNames = await deeplyFulfilledObject(
harden({
brand: getKind('brand', kindInfo.brand.coerce),
instance: getKind('instance', kindInfo.instance.coerce),
vbankAsset: getKind('vbankAsset', kindInfo.vbankAsset.coerce),
}),
);
return agoricNames;
};

/**
* from @agoric/cosmic-proto/vstorage
*
* XXX import('@agoric/cosmic-proto/vstorage/query').QueryDataResponse doesn't worksomehow
*
* @typedef {Awaited<ReturnType<import('@agoric/cosmic-proto/vstorage/query.js').QueryClientImpl['Data']>>} QueryDataResponseT
*/

/**
* A boardClient unmarshals vstorage query responses preserving object identiy.
*
* @param {import('@agoric/cosmic-proto/vstorage/query.js').QueryClientImpl} queryService
*/
export const makeBoardClient = queryService => {
const boardCtx = makeBoardContext();
/** @type {Awaited<ReturnType<makeAgoricNames>>} */
let agoricNames;

return harden({
queryService,
provideAgoricNames: async () => {
if (agoricNames) return agoricNames;
agoricNames = await makeAgoricNames(boardCtx, queryService);
return agoricNames;
},
/** @type {(path: string) => Promise<unknown>} */
readLatestHead: path =>
queryService
.Data({ path })
.then(response => boardCtx.ingest(extractCapData(response))),
});
};
106 changes: 106 additions & 0 deletions packages/inter-cli/test/test-boardClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// @ts-check
import '@endo/init';
import { M, matches, keyEQ } from '@endo/patterns';
import { Far } from '@endo/far';
import { makeMarshal, makeTagged } from '@endo/marshal';

import test from 'ava';
import { fc } from '@fast-check/ava';

import { makeBoardContext } from '../src/lib/boardClient.js';

const arbBoardId = fc.integer().map(n => `board0${n}`);

const arbSlot = arbBoardId.chain(slot =>
fc.string({ minLength: 1 }).map(iface => ({ slot, iface })),
);

const RemotableShape = M.remotable();

test('boardProxy.provide() preserves identity', t => {
const bp = makeBoardContext();
fc.assert(
fc.property(
fc.record({ s1: arbSlot, s3: arbSlot, die: fc.nat(2) }),
({ s1, s3, die }) => {
const v1 = bp.register(s1.slot, s1.iface);
const { slot, iface = undefined } = die > 0 ? s3 : { slot: s1.slot };
const v2 = bp.register(slot, iface);
t.is(s1.slot === slot, v1 === v2);
t.true(matches(v1, RemotableShape));
t.true(matches(v2, RemotableShape));
},
),
);
});

const arbPrim = fc.oneof(
fc.constant(undefined),
fc.constant(null),
fc.boolean(),
fc.float(),
fc.bigInt(),
fc.string(),
fc.string().map(n => Symbol.for(n)), // TODO: well-known symbols
);

const arbRemotable = fc.string().map(iface => Far(iface));
// const arbPromise = fc.nat(16).map(_ => harden(new Promise(_resolve => {})));
// const arbCap = fc.oneof(arbRemotable, arbPromise);
// const arbError = fc.string().map(msg => new Error(msg));

// const arbAtom = fc.oneof(arbPrim, arbCap, arbError);

// const { passable: arbPassable } = fc.letrec(tie => ({
// passable: fc.oneof(arbAtom, tie('copyArray'), tie('copyRecord')),
// copyArray: fc.array(tie('passable')).map(harden),
// copyRecord: fc.dictionary(fc.string(), tie('passable')).map(harden),
// tagged: fc
// .record({ tag: fc.string(), payload: tie('passable') })
// .map(({ tag, payload }) => makeTagged(tag, payload)),
// }));

/**
* "Keys are Passable arbitrarily-nested pass-by-copy containers
* (CopyArray, CopyRecord, CopySet, CopyBag, CopyMap) in which every
* non-container leaf is either a Passable primitive value or a Remotable"
*
* See import('@endo/patterns').Key
*/
const { passable: arbKey } = fc.letrec(tie => ({
// NOTE: no published values are copySet, copyMap, or copyBag (yet?)
passable: fc.oneof(
arbPrim,
arbRemotable,
tie('copyArray'),
tie('copyRecord'),
),
copyArray: fc.array(tie('passable')).map(harden),
copyRecord: fc.dictionary(fc.string(), tie('passable')).map(harden),
tagged: fc
.record({ tag: fc.string(), payload: tie('passable') })
.map(({ tag, payload }) => makeTagged(tag, payload)),
}));

test('boardCtx ingest() preserves identity for passable keys', t => {
const ctx = makeBoardContext();
const valToSlot = new Map();
const m = makeMarshal(
v => {
if (valToSlot.has(v)) return valToSlot.get(v);
const slot = `board0${valToSlot.size}`;
valToSlot.set(v, slot);
return slot;
},
undefined,
{ serializeBodyFormat: 'smallcaps' },
);
fc.assert(
fc.property(arbKey, key => {
const { body, slots } = m.toCapData(key);
const ingested = ctx.ingest({ body, slots });
const reingested = ctx.ingest({ body, slots });
t.true(keyEQ(ingested, reingested));
}),
);
});

0 comments on commit 2d7a2c5

Please sign in to comment.