Skip to content

Commit

Permalink
feat(internal): Introduce deepCopyJsonable
Browse files Browse the repository at this point in the history
  • Loading branch information
gibson042 committed Nov 12, 2024
1 parent f04fb4b commit f875bb0
Show file tree
Hide file tree
Showing 14 changed files with 47 additions and 42 deletions.
14 changes: 4 additions & 10 deletions packages/SwingSet/src/controller/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { initSwingStore } from '@agoric/swing-store';

import { mustMatch, M } from '@endo/patterns';
import { checkBundle } from '@endo/check-bundle/lite.js';
import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js';
import engineGC from '@agoric/internal/src/lib-nodejs/engine-gc.js';
import { startSubprocessWorker } from '@agoric/internal/src/lib-nodejs/spawnSubprocessWorker.js';
import { waitUntilQuiescent } from '@agoric/internal/src/lib-nodejs/waitUntilQuiescent.js';
Expand Down Expand Up @@ -263,13 +264,6 @@ export async function makeSwingsetController(

await kernel.start();

/**
* @param {T} x
* @returns {T}
* @template T
*/
const defensiveCopy = x => JSON.parse(JSON.stringify(x));

/**
* Validate and install a code bundle.
*
Expand Down Expand Up @@ -304,7 +298,7 @@ export async function makeSwingsetController(
writeSlogObject,

dump() {
return defensiveCopy(kernel.dump());
return deepCopyJsonable(kernel.dump());
},

verboseDebugMode(flag) {
Expand Down Expand Up @@ -340,11 +334,11 @@ export async function makeSwingsetController(
},

getStats() {
return defensiveCopy(kernel.getStats());
return deepCopyJsonable(kernel.getStats());
},

getStatus() {
return defensiveCopy(kernel.getStatus());
return deepCopyJsonable(kernel.getStatus());
},

getActivityhash() {
Expand Down
4 changes: 2 additions & 2 deletions packages/SwingSet/src/controller/initializeSwingset.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fs from 'fs';
import path from 'path';

import { assert, b, Fail } from '@endo/errors';
import { makeTracer } from '@agoric/internal';
import { deepCopyJsonable, makeTracer } from '@agoric/internal';
import { mustMatch } from '@agoric/store';
import bundleSource from '@endo/bundle-source';
import { resolve as resolveModuleSpecifier } from 'import-meta-resolve';
Expand Down Expand Up @@ -323,7 +323,7 @@ export async function initializeSwingset(
} = runtimeOptions;

// copy config so we can safely mess with it even if it's shared or hardened
config = JSON.parse(JSON.stringify(config));
config = deepCopyJsonable(config);
if (!config.bundles) {
config.bundles = {};
}
Expand Down
5 changes: 3 additions & 2 deletions packages/SwingSet/src/devices/bridge/device-bridge.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Fail } from '@endo/errors';
import { Far } from '@endo/far';
import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js';

function sanitize(data) {
if (data === undefined) {
Expand All @@ -8,7 +9,7 @@ function sanitize(data) {
if (data instanceof Error) {
data = data.stack;
}
return JSON.parse(JSON.stringify(data));
return deepCopyJsonable(data);
}

/**
Expand All @@ -32,7 +33,7 @@ export function buildRootDeviceNode(tools) {

function inboundCallback(...args) {
inboundHandler || Fail`inboundHandler not yet registered`;
const safeArgs = JSON.parse(JSON.stringify(args));
const safeArgs = deepCopyJsonable(args);
try {
SO(inboundHandler).inbound(...harden(safeArgs));
} catch (e) {
Expand Down
12 changes: 7 additions & 5 deletions packages/SwingSet/test/upgrade/upgrade-replay.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { test } from '../../tools/prepare-test-env-ava.js';

import { assert } from '@endo/errors';
import { kser } from '@agoric/kmarshal';
import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js';
import { initSwingStore } from '@agoric/swing-store';
import {
buildKernelBundles,
Expand All @@ -16,10 +17,6 @@ function bfile(name) {
return new URL(name, import.meta.url).pathname;
}

function copy(data) {
return JSON.parse(JSON.stringify(data));
}

async function run(c, method, args = []) {
assert(Array.isArray(args));
const kpid = c.queueToVatRoot('bootstrap', method, args);
Expand Down Expand Up @@ -49,7 +46,12 @@ test('replay after upgrade', async t => {

const ss1 = initSwingStore();
{
await initializeSwingset(copy(config), [], ss1.kernelStorage, initOpts);
await initializeSwingset(
deepCopyJsonable(config),
[],
ss1.kernelStorage,
initOpts,
);
const c1 = await makeSwingsetController(ss1.kernelStorage, {}, runtimeOpts);
t.teardown(c1.shutdown);
c1.pinVatRoot('bootstrap');
Expand Down
9 changes: 3 additions & 6 deletions packages/SwingSet/test/vat-admin/replay.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@
// eslint-disable-next-line import/order
import { test } from '../../tools/prepare-test-env-ava.js';

import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js';
import { kser } from '@agoric/kmarshal';
import { initSwingStore } from '@agoric/swing-store';
import { buildKernelBundles, buildVatController } from '../../src/index.js';

function copy(data) {
return JSON.parse(JSON.stringify(data));
}

test.before(async t => {
const kernelBundles = await buildKernelBundles();
t.context.data = { kernelBundles };
Expand All @@ -34,7 +31,7 @@ test.serial('replay dynamic vat', async t => {

const ss1 = initSwingStore();
{
const c1 = await buildVatController(copy(config), [], {
const c1 = await buildVatController(deepCopyJsonable(config), [], {
kernelStorage: ss1.kernelStorage,
kernelBundles: t.context.data.kernelBundles,
});
Expand All @@ -52,7 +49,7 @@ test.serial('replay dynamic vat', async t => {
const serialized = ss1.debug.serialize();
const ss2 = initSwingStore(null, { serialized });
{
const c2 = await buildVatController(copy(config), [], {
const c2 = await buildVatController(deepCopyJsonable(config), [], {
kernelStorage: ss2.kernelStorage,
});
t.teardown(c2.shutdown);
Expand Down
8 changes: 3 additions & 5 deletions packages/boot/test/upgrading/upgrade-vats.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @jessie.js/safe-await-separator -- test */
import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js';

import { BridgeId } from '@agoric/internal';
import { BridgeId, deepCopyJsonable } from '@agoric/internal';
import { buildVatController } from '@agoric/swingset-vat';
import { makeRunUtils } from '@agoric/swingset-vat/tools/run-utils.js';
import { Fail } from '@endo/errors';
Expand Down Expand Up @@ -434,8 +434,6 @@ test('upgrade vat-priceAuthority', async t => {
matchRef(t, reincarnatedRegistry.adminFacet, registry.adminFacet);
});

const dataOnly = obj => JSON.parse(JSON.stringify(obj));

test('upgrade vat-vow', async t => {
const bundles = {
vow: {
Expand Down Expand Up @@ -483,7 +481,7 @@ test('upgrade vat-vow', async t => {
};
await EV(vowRoot).makeLocalPromiseWatchers(localPromises);
await EV(vowRoot).makeLocalVowWatchers(localVows);
t.deepEqual(dataOnly(await EV(vowRoot).getWatcherResults()), {
t.deepEqual(deepCopyJsonable(await EV(vowRoot).getWatcherResults()), {
promiseForever: { status: 'unsettled' },
promiseFulfilled: { status: 'fulfilled', value: 'hello' },
promiseRejected: { status: 'rejected', reason: 'goodbye' },
Expand Down Expand Up @@ -533,7 +531,7 @@ test('upgrade vat-vow', async t => {
});
await EV(fakeVowKit.resolver).reject(upgradeRejection.reason);
t.timeout(600_000); // t.timeout.clear() not yet available in our ava version
t.deepEqual(dataOnly(await EV(vowRoot).getWatcherResults()), {
t.deepEqual(deepCopyJsonable(await EV(vowRoot).getWatcherResults()), {
promiseForever: upgradeRejection,
promiseFulfilled: { status: 'fulfilled', value: 'hello' },
promiseRejected: { status: 'rejected', reason: 'goodbye' },
Expand Down
4 changes: 2 additions & 2 deletions packages/cosmic-swingset/test/run-policy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import test from 'ava';
import { assert, q, Fail } from '@endo/errors';
import { E } from '@endo/far';
import { BridgeId, objectMap } from '@agoric/internal';
import { BridgeId, deepCopyJsonable, objectMap } from '@agoric/internal';
import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js';
import {
defaultBootstrapMessage,
Expand All @@ -26,7 +26,7 @@ import {
*/
const makeSourceDescriptors = src => {
const hardened = objectMap(src, sourceSpec => ({ sourceSpec }));
return JSON.parse(JSON.stringify(hardened));
return deepCopyJsonable(hardened);
};

/**
Expand Down
10 changes: 4 additions & 6 deletions packages/cosmic-swingset/tools/test-kit.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
QueuedActionType,
} from '@agoric/internal/src/action-types.js';
import { makeInitMsg } from '@agoric/internal/src/chain-utils.js';
import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js';
import { initSwingStore } from '@agoric/swing-store';
import { makeSlogSender } from '@agoric/telemetry';
import { launch } from '../src/launch-chain.js';
Expand All @@ -28,9 +29,6 @@ import { makeQueue, makeQueueStorageMock } from '../src/helpers/make-queue.js';
* @typedef {(input: T) => T} Replacer
*/

/** @type {Replacer<object>} */
const deepCopyData = obj => JSON.parse(JSON.stringify(obj));

/** @type {Replacer<object>} */
const stripUndefined = obj =>
Object.fromEntries(
Expand Down Expand Up @@ -62,7 +60,7 @@ export const defaultInitMessage = harden(
}),
);
export const defaultBootstrapMessage = harden({
...deepCopyData(defaultInitMessage),
...deepCopyJsonable(defaultInitMessage),
blockHeight: 1,
blockTime: Math.floor(Date.parse('2010-01-01T00:00Z') / 1000),
isBootstrap: true,
Expand Down Expand Up @@ -184,7 +182,7 @@ export const makeCosmicSwingsetTestKit = async (
await null;
/** @type {SwingSetConfig} */
let config = {
...deepCopyData(baseConfig),
...deepCopyJsonable(baseConfig),
...configOverrides,
...stripUndefined({ defaultManagerType }),
};
Expand All @@ -209,7 +207,7 @@ export const makeCosmicSwingsetTestKit = async (

if (fixupConfig) config = fixupConfig(config);

let initMessage = deepCopyData(defaultInitMessage);
let initMessage = deepCopyJsonable(defaultInitMessage);
if (fixupInitMessage) initMessage = fixupInitMessage(initMessage);
initMessage?.type === SwingsetMessageType.AG_COSMOS_INIT ||
Fail`initMessage must be AG_COSMOS_INIT`;
Expand Down
11 changes: 11 additions & 0 deletions packages/internal/src/js-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@
* dependent upon a hardened environment.
*/

/**
* Deep-copy a value by round-tripping it through JSON (which drops
* function/symbol/undefined values and properties that are non-enumerable
* and/or symbol-keyed, and rejects bigint values).
*
* @template T
* @param {T} value
* @returns {T}
*/
export const deepCopyJsonable = value => JSON.parse(JSON.stringify(value));

/**
* @param {any} value
* @param {string | undefined} name
Expand Down
1 change: 1 addition & 0 deletions packages/internal/test/snapshots/exports.test.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Generated by [AVA](https://avajs.dev).
'assertAllDefined',
'bindAllMethods',
'cast',
'deepCopyJsonable',
'deepMapObject',
'deeplyFulfilledObject',
'forever',
Expand Down
Binary file modified packages/internal/test/snapshots/exports.test.js.snap
Binary file not shown.
5 changes: 3 additions & 2 deletions packages/solo/src/vat-http.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { E } from '@endo/eventual-send';
import { makePromiseKit } from '@endo/promise-kit';
import { Far } from '@endo/marshal';

import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js';
import { makeNotifierKit } from '@agoric/notifier';
import { makeCache } from '@agoric/cache';
import { getReplHandler } from '@agoric/vats/src/repl.js';
Expand Down Expand Up @@ -82,7 +83,7 @@ export function buildRootObject(vatPowers) {
D(commandDevice).sendResponse(
count,
isException,
obj || JSON.parse(JSON.stringify(obj)),
obj || deepCopyJsonable(obj),
);

// Map an URL only to its latest handler.
Expand Down Expand Up @@ -186,7 +187,7 @@ export function buildRootObject(vatPowers) {
// Launder the data, since the command device tends to pass device nodes
// when there are empty objects, which screw things up for us.
// Analysis is in https://github.com/Agoric/agoric-sdk/pull/1956
const obj = JSON.parse(JSON.stringify(rawObj));
const obj = deepCopyJsonable(rawObj);
console.debug(
`vat-http.inbound (from browser) ${count}`,
JSON.stringify(obj, undefined, 2),
Expand Down
5 changes: 3 additions & 2 deletions packages/wallet/api/src/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
* types.js file.
*/
import { E } from '@endo/eventual-send';
import { makeNotifierKit, observeIteration } from '@agoric/notifier';
import { Far } from '@endo/marshal';
import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js';
import { makeNotifierKit, observeIteration } from '@agoric/notifier';

import { makeWalletRoot } from './lib-wallet.js';
import pubsub from './pubsub.js';
Expand Down Expand Up @@ -54,7 +55,7 @@ export function buildRootObject(vatPowers) {
const offerSubscriptions = new Map();

const httpSend = (obj, channelHandles) =>
E(http).send(JSON.parse(JSON.stringify(obj)), channelHandles);
E(http).send(deepCopyJsonable(obj), channelHandles);

const pushOfferSubscriptions = (channelHandle, offers) => {
const subs = offerSubscriptions.get(channelHandle);
Expand Down
1 change: 1 addition & 0 deletions packages/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"build": "exit 0"
},
"dependencies": {
"@agoric/internal": "^0.3.2",
"@agoric/wallet-ui": "0.1.3-solo.0",
"babel-eslint": "^10.0.3",
"eslint-plugin-eslint-comments": "^3.1.2",
Expand Down

0 comments on commit f875bb0

Please sign in to comment.