diff --git a/packages/client-utils/src/main.js b/packages/client-utils/src/main.js index baf2ecde2d5..be9b4869eb8 100644 --- a/packages/client-utils/src/main.js +++ b/packages/client-utils/src/main.js @@ -1,5 +1,6 @@ export * from './rpc.js'; export * from './sync-tools.js'; +export * from './vstorage.js'; export * from './vstorage-kit.js'; export * from './wallet-utils.js'; diff --git a/packages/client-utils/src/vstorage-kit.js b/packages/client-utils/src/vstorage-kit.js index 71863eccf9a..74598d82914 100644 --- a/packages/client-utils/src/vstorage-kit.js +++ b/packages/client-utils/src/vstorage-kit.js @@ -1,135 +1,17 @@ -/* global Buffer */ import { boardSlottingMarshaller, makeBoardRemote, } from '@agoric/vats/tools/board-utils.js'; +import { makeVStorage } from './vstorage.js'; export { boardSlottingMarshaller }; /** * @import {MinimalNetworkConfig} from './rpc.js'; * @import {TypedPublished} from './types.js'; + * @import {VStorage} from './vstorage.js'; */ -/** - * @param {object} powers - * @param {typeof window.fetch} powers.fetch - * @param {MinimalNetworkConfig} config - */ -export const makeVStorage = (powers, config) => { - /** @param {string} path */ - const getJSON = path => { - const url = config.rpcAddrs[0] + path; - // console.warn('fetching', url); - return powers.fetch(url, { keepalive: true }).then(res => res.json()); - }; - // height=0 is the same as omitting height and implies the highest block - const url = (path = 'published', { kind = 'children', height = 0 } = {}) => - `/abci_query?path=%22/custom/vstorage/${kind}/${path}%22&height=${height}`; - - const readStorage = (path = 'published', { kind = 'children', height = 0 }) => - getJSON(url(path, { kind, height })) - .catch(err => { - throw Error(`cannot read ${kind} of ${path}: ${err.message}`); - }) - .then(data => { - const { - result: { response }, - } = data; - if (response?.code !== 0) { - /** @type {any} */ - const err = Error( - `error code ${response?.code} reading ${kind} of ${path}: ${response.log}`, - ); - err.code = response?.code; - err.codespace = response?.codespace; - throw err; - } - return data; - }); - - return { - url, - decode({ result: { response } }) { - const { code } = response; - if (code !== 0) { - throw response; - } - const { value } = response; - return Buffer.from(value, 'base64').toString(); - }, - /** - * - * @param {string} path - * @returns {Promise} latest vstorage value at path - */ - async readLatest(path = 'published') { - const raw = await readStorage(path, { kind: 'data' }); - return this.decode(raw); - }, - async keys(path = 'published') { - const raw = await readStorage(path, { kind: 'children' }); - return JSON.parse(this.decode(raw)).children; - }, - /** - * @param {string} path - * @param {number} [height] default is highest - * @returns {Promise<{blockHeight: number, values: string[]}>} - */ - async readAt(path, height = undefined) { - const raw = await readStorage(path, { kind: 'data', height }); - const txt = this.decode(raw); - /** @type {{ value: string }} */ - const { value } = JSON.parse(txt); - return JSON.parse(value); - }, - /** - * Read values going back as far as available - * - * @param {string} path - * @param {number | string} [minHeight] - * @returns {Promise} - */ - async readFully(path, minHeight = undefined) { - const parts = []; - // undefined the first iteration, to query at the highest - let blockHeight; - await null; - do { - // console.debug('READING', { blockHeight }); - let values; - try { - ({ blockHeight, values } = await this.readAt( - path, - blockHeight && Number(blockHeight) - 1, - )); - // console.debug('readAt returned', { blockHeight }); - } catch (err) { - if ( - // CosmosSDK ErrInvalidRequest with particular message text; - // misrepresentation of pruned data - // TODO replace after incorporating a fix to - // https://github.com/cosmos/cosmos-sdk/issues/19992 - err.codespace === 'sdk' && - err.code === 18 && - err.message.match(/pruned/) - ) { - // console.error(err); - break; - } - throw err; - } - parts.push(values); - // console.debug('PUSHED', values); - // console.debug('NEW', { blockHeight, minHeight }); - if (minHeight && Number(blockHeight) <= Number(minHeight)) break; - } while (blockHeight > 0); - return parts.flat(); - }, - }; -}; -/** @typedef {ReturnType} VStorage */ - /** @deprecated */ export const makeFromBoard = () => { const cache = new Map(); diff --git a/packages/client-utils/src/vstorage.js b/packages/client-utils/src/vstorage.js new file mode 100644 index 00000000000..45afd4d8590 --- /dev/null +++ b/packages/client-utils/src/vstorage.js @@ -0,0 +1,125 @@ +/* global Buffer */ + +/** + * @import {MinimalNetworkConfig} from './rpc.js'; + */ + +/** + * @param {object} powers + * @param {typeof window.fetch} powers.fetch + * @param {MinimalNetworkConfig} config + */ +export const makeVStorage = (powers, config) => { + /** @param {string} path */ + const getJSON = path => { + const url = config.rpcAddrs[0] + path; + // console.warn('fetching', url); + return powers.fetch(url, { keepalive: true }).then(res => res.json()); + }; + // height=0 is the same as omitting height and implies the highest block + const url = (path = 'published', { kind = 'children', height = 0 } = {}) => + `/abci_query?path=%22/custom/vstorage/${kind}/${path}%22&height=${height}`; + + const readStorage = (path = 'published', { kind = 'children', height = 0 }) => + getJSON(url(path, { kind, height })) + .catch(err => { + throw Error(`cannot read ${kind} of ${path}: ${err.message}`); + }) + .then(data => { + const { + result: { response }, + } = data; + if (response?.code !== 0) { + /** @type {any} */ + const err = Error( + `error code ${response?.code} reading ${kind} of ${path}: ${response.log}`, + ); + err.code = response?.code; + err.codespace = response?.codespace; + throw err; + } + return data; + }); + + const vstorage = { + url, + decode({ result: { response } }) { + const { code } = response; + if (code !== 0) { + throw response; + } + const { value } = response; + return Buffer.from(value, 'base64').toString(); + }, + /** + * + * @param {string} path + * @returns {Promise} latest vstorage value at path + */ + async readLatest(path = 'published') { + const raw = await readStorage(path, { kind: 'data' }); + return vstorage.decode(raw); + }, + async keys(path = 'published') { + const raw = await readStorage(path, { kind: 'children' }); + return JSON.parse(vstorage.decode(raw)).children; + }, + /** + * @param {string} path + * @param {number} [height] default is highest + * @returns {Promise<{blockHeight: number, values: string[]}>} + */ + async readAt(path, height = undefined) { + const raw = await readStorage(path, { kind: 'data', height }); + const txt = vstorage.decode(raw); + /** @type {{ value: string }} */ + const { value } = JSON.parse(txt); + return JSON.parse(value); + }, + /** + * Read values going back as far as available + * + * @param {string} path + * @param {number | string} [minHeight] + * @returns {Promise} + */ + async readFully(path, minHeight = undefined) { + const parts = []; + // undefined the first iteration, to query at the highest + let blockHeight; + await null; + do { + // console.debug('READING', { blockHeight }); + let values; + try { + ({ blockHeight, values } = await vstorage.readAt( + path, + blockHeight && Number(blockHeight) - 1, + )); + // console.debug('readAt returned', { blockHeight }); + } catch (err) { + if ( + // CosmosSDK ErrInvalidRequest with particular message text; + // misrepresentation of pruned data + // TODO replace after incorporating a fix to + // https://github.com/cosmos/cosmos-sdk/issues/19992 + err.codespace === 'sdk' && + err.code === 18 && + err.message.match(/pruned/) + ) { + // console.error(err); + break; + } + throw err; + } + parts.push(values); + // console.debug('PUSHED', values); + // console.debug('NEW', { blockHeight, minHeight }); + if (minHeight && Number(blockHeight) <= Number(minHeight)) break; + } while (blockHeight > 0); + return parts.flat(); + }, + }; + return vstorage; +}; +/** @typedef {ReturnType} VStorage */ diff --git a/packages/client-utils/test/vstorage.test.js b/packages/client-utils/test/vstorage.test.js new file mode 100644 index 00000000000..17de8f02700 --- /dev/null +++ b/packages/client-utils/test/vstorage.test.js @@ -0,0 +1,18 @@ +/* eslint-env node */ +// @ts-check +import test from 'ava'; +import { makeVStorage } from '../src/vstorage.js'; + +/** @type {any} */ +const fetch = () => Promise.resolve({}); + +test('readFully can be used without instance binding', async t => { + const vstorage = makeVStorage({ fetch }, { chainName: '', rpcAddrs: [''] }); + const { readFully } = vstorage; + + // Mock implementation to avoid actual network calls + vstorage.readAt = async () => ({ blockHeight: 0, values: ['test'] }); + + // This would throw if readFully required 'this' binding + await t.notThrowsAsync(() => readFully('some/path')); +});