diff --git a/packages/cosmic-swingset/src/launch-chain.js b/packages/cosmic-swingset/src/launch-chain.js index 4d182d29490..c8a5c4fe33c 100644 --- a/packages/cosmic-swingset/src/launch-chain.js +++ b/packages/cosmic-swingset/src/launch-chain.js @@ -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'; @@ -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', }); } diff --git a/packages/internal/src/ses-utils.js b/packages/internal/src/ses-utils.js index b6f27e6cdc3..26308c37218 100644 --- a/packages/internal/src/ses-utils.js +++ b/packages/internal/src/ses-utils.js @@ -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 @@ -171,25 +172,59 @@ export const assertAllDefined = obj => { }; /** - * @template {Record} T - * @template {Partial<{ [K in keyof T]: true }>} U - * @param {T} target - * @param {U} [permits] - * @returns {keyof U extends keyof T ? Pick : never} + * Attenuate `specimen` to only properties allowed by `permit`. + * + * @template T + * @template {Permit} P + * @param {T} specimen + * @param {P} permit + * @param {>(attenuation: U, permit: SubP) => U} [transform] + * @returns {Attenuated} */ -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} */ (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} SubP + * @type {(specimen: SubT, permit: SubP) => Attenuated} + */ + const extract = (subSpecimen, subPermit) => { + if (subPermit === true || typeof subPermit === 'string') { + return /** @type {Attenuated} */ (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} */ diff --git a/packages/internal/src/types.ts b/packages/internal/src/types.ts index 9d5420ff3df..ce036dd5992 100644 --- a/packages/internal/src/types.ts +++ b/packages/internal/src/types.ts @@ -15,6 +15,27 @@ export type TotalMap = Omit, 'get'> & { export type TotalMapFrom> = M extends Map ? TotalMap : 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 = + | true + | string + | Partial<{ [K in keyof T]: K extends string ? Permit : never }>; + +export type Attenuated> = P extends object + ? { + [K in keyof P]: K extends keyof T + ? P[K] extends Permit + ? Attenuated + : never + : never; + } + : T; + export declare class Callback any> { private iface: I; diff --git a/packages/internal/test/snapshots/exports.test.js.md b/packages/internal/test/snapshots/exports.test.js.md index 9f95df26959..6c74f524c5f 100644 --- a/packages/internal/test/snapshots/exports.test.js.md +++ b/packages/internal/test/snapshots/exports.test.js.md @@ -21,6 +21,7 @@ Generated by [AVA](https://avajs.dev). 'aggregateTryFinally', 'allValues', 'assertAllDefined', + 'attenuate', 'bindAllMethods', 'cast', 'deepCopyJsonable', @@ -36,7 +37,6 @@ Generated by [AVA](https://avajs.dev). 'mustMatch', 'objectMap', 'objectMetaMap', - 'pick', 'pureDataMarshaller', 'synchronizedTee', 'untilTrue', diff --git a/packages/internal/test/snapshots/exports.test.js.snap b/packages/internal/test/snapshots/exports.test.js.snap index 6acc3441b55..8efed672d75 100644 Binary files a/packages/internal/test/snapshots/exports.test.js.snap and b/packages/internal/test/snapshots/exports.test.js.snap differ diff --git a/packages/internal/test/types.test-d.ts b/packages/internal/test/types.test-d.ts index e9ae4ef4d2e..f31a4472966 100644 --- a/packages/internal/test/types.test-d.ts +++ b/packages/internal/test/types.test-d.ts @@ -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 = null as any; const remoteStorageNode: Remote = null as any; diff --git a/packages/swing-store/src/swingStore.js b/packages/swing-store/src/swingStore.js index d579e3ad43c..f321b53802a 100644 --- a/packages/swing-store/src/swingStore.js +++ b/packages/swing-store/src/swingStore.js @@ -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'; @@ -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,