Skip to content

Commit

Permalink
feat(internal): Generalize single-level pick utility to recursive `…
Browse files Browse the repository at this point in the history
…attenuate`
  • Loading branch information
gibson042 committed Feb 11, 2025
1 parent 009b113 commit e39c802
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 63 deletions.
14 changes: 7 additions & 7 deletions packages/cosmic-swingset/src/launch-chain.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
} from '@agoric/swingset-vat';
import { waitUntilQuiescent } from '@agoric/internal/src/lib-nodejs/waitUntilQuiescent.js';
import { openSwingStore } from '@agoric/swing-store';
import { pick, BridgeId as BRIDGE_ID } from '@agoric/internal';
import { attenuate, BridgeId as BRIDGE_ID } from '@agoric/internal';
import { makeWithQueue } from '@agoric/internal/src/queue.js';
import * as ActionType from '@agoric/internal/src/action-types.js';

Expand Down Expand Up @@ -1278,11 +1278,11 @@ export async function launchAndShareInternals({
*/
export async function launch(options) {
const launchResult = await launchAndShareInternals(options);
return pick(launchResult, {
blockingSend: true,
shutdown: true,
writeSlogObject: true,
savedHeight: true,
savedChainSends: true,
return attenuate(launchResult, {
blockingSend: 'pick',
shutdown: 'pick',
writeSlogObject: 'pick',
savedHeight: 'pick',
savedChainSends: 'pick',
});
}
75 changes: 55 additions & 20 deletions packages/internal/src/ses-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ import { makeQueue } from '@endo/stream';
// @ts-ignore TS7016 The 'jessie.js' library may need to update its package.json or typings
import { asyncGenerate } from 'jessie.js';

/** @import {ERef} from '@endo/far'; */
/** @import {Permit, Attenuated} from './types.js'; */

export { objectMap, objectMetaMap, fromUniqueEntries };

const { fromEntries, keys, values } = Object;

/** @import {ERef} from '@endo/far' */

/**
* @template T
* @typedef {{ [KeyType in keyof T]: T[KeyType] } & {}} Simplify flatten the
Expand Down Expand Up @@ -171,25 +172,59 @@ export const assertAllDefined = obj => {
};

/**
* @template {Record<PropertyKey, unknown>} T
* @template {Partial<{ [K in keyof T]: true }>} U
* @param {T} target
* @param {U} [permits]
* @returns {keyof U extends keyof T ? Pick<T, keyof U> : never}
* Attenuate `specimen` to only properties allowed by `permit`.
*
* @template T
* @template {Permit<T>} P
* @param {T} specimen
* @param {P} permit
* @param {<U, SubP extends Permit<U>>(attenuation: U, permit: SubP) => U} [transform]
* @returns {Attenuated<T, P>}
*/
export const pick = (
target,
permits = /** @type {U} */ (objectMap(target, () => true)),
) => {
const attenuation = objectMap(permits, (permit, key) => {
permit === true || Fail`internal: ${q(key)} permit must be true`;
// eslint-disable-next-line no-restricted-syntax
key in target || Fail`internal: target is missing ${q(key)}`;
// eslint-disable-next-line no-restricted-syntax
return target[key];
});
// @ts-expect-error cast
return attenuation;
export const attenuate = (specimen, permit, transform = x => x) => {
// Entry-point arguments get special checks and error messages.
if (permit === true || typeof permit === 'string') {
return /** @type {Attenuated<T, P>} */ (specimen);
} else if (permit === null || typeof permit !== 'object') {
throw Fail`invalid permit: ${q(permit)}`;
} else if (specimen === null || typeof specimen !== 'object') {
throw Fail`specimen must be an object for permit ${q(permit)}`;
}

/** @type {string[]} */
const path = [];
/**
* @template SubT
* @template {Permit<SubT>} SubP
* @type {(specimen: SubT, permit: SubP) => Attenuated<SubT, SubP>}
*/
const extract = (subSpecimen, subPermit) => {
if (subPermit === true || typeof subPermit === 'string') {
return /** @type {Attenuated<SubT, SubP>} */ (subSpecimen);
} else if (subPermit === null || typeof subPermit !== 'object') {
throw Fail`invalid permit at path ${q(path)}: ${q(subPermit)}`;
} else if (subSpecimen === null || typeof subSpecimen !== 'object') {
throw Fail`specimen at path ${q(path)} must be an object for permit ${q(subPermit)}`;
}
const attenuated = Object.fromEntries(
Object.entries(subPermit).map(([subKey, deepPermit]) => {
path.push(subKey);
// eslint-disable-next-line no-restricted-syntax
subKey in subSpecimen || Fail`specimen is missing path ${q(path)}`;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TS7053 does not manifest in all import sites
// eslint-disable-next-line no-restricted-syntax
const deepSpecimen = subSpecimen[subKey];
const entry = [subKey, extract(deepSpecimen, deepPermit)];
path.pop();
return entry;
}),
);
return transform(attenuated, subPermit);
};

// @ts-expect-error
return extract(specimen, permit);
};

/** @type {IteratorResult<undefined, never>} */
Expand Down
21 changes: 21 additions & 0 deletions packages/internal/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ export type TotalMap<K, V> = Omit<Map<K, V>, 'get'> & {
export type TotalMapFrom<M extends Map<any, any>> =
M extends Map<infer K, infer V> ? TotalMap<K, V> : never;

/**
* A permit is either `true` or a string (both meaning no attenuation, with a
* string serving as a grouping label for convenience and/or diagram
* generation), or an object whose keys identify child properties and whose
* corresponding values are theirselves (recursive) Permits.
*/
export type Permit<T> =
| true
| string
| Partial<{ [K in keyof T]: K extends string ? Permit<T[K]> : never }>;

export type Attenuated<T, P extends Permit<T>> = P extends object
? {
[K in keyof P]: K extends keyof T
? P[K] extends Permit<T[K]>
? Attenuated<T[K], P[K]>
: never
: never;
}
: T;

export declare class Callback<I extends (...args: any[]) => any> {
private iface: I;

Expand Down
2 changes: 1 addition & 1 deletion packages/internal/test/snapshots/exports.test.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Generated by [AVA](https://avajs.dev).
'aggregateTryFinally',
'allValues',
'assertAllDefined',
'attenuate',
'bindAllMethods',
'cast',
'deepCopyJsonable',
Expand All @@ -36,7 +37,6 @@ Generated by [AVA](https://avajs.dev).
'mustMatch',
'objectMap',
'objectMetaMap',
'pick',
'pureDataMarshaller',
'synchronizedTee',
'untilTrue',
Expand Down
Binary file modified packages/internal/test/snapshots/exports.test.js.snap
Binary file not shown.
28 changes: 27 additions & 1 deletion packages/internal/test/types.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
import { expectNotType, expectType } from 'tsd';
import { E, type ERef } from '@endo/far';
import type { Remote } from '../src/types.js';
import { attenuate } from '../src/ses-utils.js';
import type { Permit, Remote } from '../src/types.js';
import type { StorageNode } from '../src/lib-chainStorage.js';

{
const obj = {
m1: () => {},
m2: () => {},
data: {
log: ['string'],
counter: 0,
},
internalData: {
realCount: 0,
},
};
expectType<{ m1: () => void }>(attenuate(obj, { m1: true }));
expectType<{ m2: () => void }>(attenuate(obj, { m2: 'pick' }));
expectType<{ data: { log: string[]; counter: number } }>(
attenuate(obj, { data: 'pick' }),
);
expectType<{ m1: () => void; data: { log: string[] } }>(
attenuate(obj, { m1: 'pick', data: { log: true } }),
);
expectNotType<{ m1: () => void; m2: () => void; data: { log: string[] } }>(
attenuate(obj, { m1: 'pick', data: { log: true } }),
);
}

const eventualStorageNode: ERef<StorageNode> = null as any;
const remoteStorageNode: Remote<StorageNode> = null as any;

Expand Down
59 changes: 25 additions & 34 deletions packages/swing-store/src/swingStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import sqlite3 from 'better-sqlite3';

import { Fail, q } from '@endo/errors';

import { pick } from '@agoric/internal';
import { attenuate } from '@agoric/internal';

import { dbFileInDirectory } from './util.js';
import { makeKVStore, getKeyType } from './kvStore.js';
Expand Down Expand Up @@ -565,41 +565,32 @@ export function makeSwingStore(path, forceReset, options = {}) {
return db;
}

const transcriptStore = pick(
transcriptStoreInternal,
/** @type {const} */ ({
initTranscript: true,
rolloverSpan: true,
rolloverIncarnation: true,
getCurrentSpanBounds: true,
addItem: true,
readSpan: true,
stopUsingTranscript: true,
deleteVatTranscripts: true,
}),
);
const transcriptStore = attenuate(transcriptStoreInternal, {
initTranscript: 'pick',
rolloverSpan: 'pick',
rolloverIncarnation: 'pick',
getCurrentSpanBounds: 'pick',
addItem: 'pick',
readSpan: 'pick',
stopUsingTranscript: 'pick',
deleteVatTranscripts: 'pick',
});

const snapStore = pick(
snapStoreInternal,
/** @type {const} */ ({
loadSnapshot: true,
saveSnapshot: true,
deleteAllUnusedSnapshots: true,
deleteVatSnapshots: true,
stopUsingLastSnapshot: true,
getSnapshotInfo: true,
}),
);
const snapStore = attenuate(snapStoreInternal, {
loadSnapshot: 'pick',
saveSnapshot: 'pick',
deleteAllUnusedSnapshots: 'pick',
deleteVatSnapshots: 'pick',
stopUsingLastSnapshot: 'pick',
getSnapshotInfo: 'pick',
});

const bundleStore = pick(
bundleStoreInternal,
/** @type {const} */ ({
addBundle: true,
hasBundle: true,
getBundle: true,
deleteBundle: true,
}),
);
const bundleStore = attenuate(bundleStoreInternal, {
addBundle: 'pick',
hasBundle: 'pick',
getBundle: 'pick',
deleteBundle: 'pick',
});

const kernelStorage = {
kvStore: kernelKVStore,
Expand Down

0 comments on commit e39c802

Please sign in to comment.