Skip to content

Commit

Permalink
client utils vstorage without instance binding (#10566)
Browse files Browse the repository at this point in the history
closes: #10563

## Description

It would help users of client-utils to be able to use VStorage methods as first-class functions. This removes using `this` as the namespace of the helper functions.

### Security Considerations
none

### Scaling Considerations
none

### Documentation Considerations
none

### Testing Considerations
New test (which failed before the change)

### Upgrade Considerations
none
  • Loading branch information
mergify[bot] authored Nov 26, 2024
2 parents d4385f0 + 13656e5 commit 1dd4589
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 120 deletions.
1 change: 1 addition & 0 deletions packages/client-utils/src/main.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
122 changes: 2 additions & 120 deletions packages/client-utils/src/vstorage-kit.js
Original file line number Diff line number Diff line change
@@ -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<string>} 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<string[]>}
*/
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<typeof makeVStorage>} VStorage */

/** @deprecated */
export const makeFromBoard = () => {
const cache = new Map();
Expand Down
125 changes: 125 additions & 0 deletions packages/client-utils/src/vstorage.js
Original file line number Diff line number Diff line change
@@ -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<string>} 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<string[]>}
*/
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<typeof makeVStorage>} VStorage */
18 changes: 18 additions & 0 deletions packages/client-utils/test/vstorage.test.js
Original file line number Diff line number Diff line change
@@ -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'));
});

0 comments on commit 1dd4589

Please sign in to comment.