diff --git a/packages/utils/LICENSE b/packages/common/LICENSE similarity index 100% rename from packages/utils/LICENSE rename to packages/common/LICENSE diff --git a/packages/utils/NEWS.md b/packages/common/NEWS.md similarity index 100% rename from packages/utils/NEWS.md rename to packages/common/NEWS.md diff --git a/packages/common/README.md b/packages/common/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/utils/SECURITY.md b/packages/common/SECURITY.md similarity index 100% rename from packages/utils/SECURITY.md rename to packages/common/SECURITY.md diff --git a/packages/common/index.js b/packages/common/index.js new file mode 100644 index 0000000000..abfd6c0fda --- /dev/null +++ b/packages/common/index.js @@ -0,0 +1,10 @@ +export * from './src/apply-labeling-error.js'; +export * from './src/from-unique-entries.js'; +export * from './src/ident-checker.js'; +export * from './src/list-difference.js'; +export * from './src/make-array-iterator.js'; +export * from './src/make-iterator.js'; +export * from './src/object-map.js'; +export * from './src/object-meta-assign.js'; +export * from './src/object-meta-map.js'; +export * from './src/throw-labeled.js'; diff --git a/packages/utils/package.json b/packages/common/package.json similarity index 97% rename from packages/utils/package.json rename to packages/common/package.json index 101fd4dd7a..d36ed31014 100644 --- a/packages/utils/package.json +++ b/packages/common/package.json @@ -1,12 +1,12 @@ { - "name": "@endo/utils", + "name": "@endo/common", "version": "1.0.1", "private": true, "description": null, "keywords": [], "author": "Endo contributors", "license": "Apache-2.0", - "homepage": "https://github.com/endojs/endo/tree/master/packages/skel#readme", + "homepage": "https://github.com/endojs/endo/tree/master/packages/common#readme", "repository": { "type": "git", "url": "git+https://github.com/endojs/endo.git" diff --git a/packages/common/src/apply-labeling-error.js b/packages/common/src/apply-labeling-error.js new file mode 100644 index 0000000000..514b09fb4e --- /dev/null +++ b/packages/common/src/apply-labeling-error.js @@ -0,0 +1,37 @@ +import { E } from '@endo/eventual-send'; +import { isPromise } from '@endo/promise-kit'; +import { throwLabeled } from './throw-labeled.js'; + +/** + * @template A,R + * @param {(...args: A[]) => R} func + * @param {A[]} args + * @param {string|number} [label] + * @returns {R} + */ +export const applyLabelingError = (func, args, label = undefined) => { + if (label === undefined) { + return func(...args); + } + let result; + try { + result = func(...args); + } catch (err) { + throwLabeled(err, label); + } + if (isPromise(result)) { + // Cannot be at-ts-expect-error because there is no type error locally. + // Rather, a type error only as imported into exo. + // @ts-ignore If result is a rejected promise, this will + // return a promise with a different rejection reason. But this + // confuses TypeScript because it types that case as `Promise` + // which is cool for a promise that will never fulfll. + // But TypeScript doesn't understand that this will only happen + // when `result` was a rejected promise. In only this case `R` + // should already allow `Promise` as a subtype. + return E.when(result, undefined, reason => throwLabeled(reason, label)); + } else { + return result; + } +}; +harden(applyLabelingError); diff --git a/packages/common/src/from-unique-entries.js b/packages/common/src/from-unique-entries.js new file mode 100644 index 0000000000..a2daad065f --- /dev/null +++ b/packages/common/src/from-unique-entries.js @@ -0,0 +1,30 @@ +const { fromEntries } = Object; +const { ownKeys } = Reflect; + +const { quote: q, Fail } = assert; + +/** + * Throws if multiple entries use the same property name. Otherwise acts + * like `Object.fromEntries` but hardens the result. + * Use it to protect from property names computed from user-provided data. + * + * @template K,V + * @param {Iterable<[K,V]>} allEntries + * @returns {{[k: K]: V}} + */ +export const fromUniqueEntries = allEntries => { + const entriesArray = [...allEntries]; + const result = harden(fromEntries(entriesArray)); + if (ownKeys(result).length === entriesArray.length) { + return result; + } + const names = new Set(); + for (const [name, _] of entriesArray) { + if (names.has(name)) { + Fail`collision on property name ${q(name)}: ${entriesArray}`; + } + names.add(name); + } + throw Fail`internal: failed to create object from unique entries`; +}; +harden(fromUniqueEntries); diff --git a/packages/common/src/ident-checker.js b/packages/common/src/ident-checker.js new file mode 100644 index 0000000000..e7c34cd36a --- /dev/null +++ b/packages/common/src/ident-checker.js @@ -0,0 +1,36 @@ +// TODO Complete migration of Checker type from @endo/pass-style to @endo/common +// by having @endo/pass-style, and everyone else who needs it, import it from +// @endo/common. +/** + * @callback Checker + * Internal to a useful pattern for writing checking logic + * (a "checkFoo" function) that can be used to implement a predicate + * (an "isFoo" function) or a validator (an "assertFoo" function). + * + * * A predicate ideally only returns `true` or `false` and rarely throws. + * * A validator throws an informative diagnostic when the predicate + * would have returned `false`, and simply returns `undefined` normally + * when the predicate would have returned `true`. + * * The internal checking function that they share is parameterized by a + * `Checker` that determines how to proceed with a failure condition. + * Predicates pass in an identity function as checker. Validators + * pass in `assertChecker` which is a trivial wrapper around `assert`. + * + * See the various uses for good examples. + * @param {boolean} cond + * @param {import('ses').Details} [details] + * @returns {boolean} + */ + +/** + * In the `assertFoo`/`isFoo`/`checkFoo` pattern, `checkFoo` has a `check` + * parameter of type `Checker`. `assertFoo` calls `checkFoo` passes + * `assertChecker` as the `check` argument. `isFoo` passes `identChecker` + * as the `check` argument. `identChecker` acts precisely like an + * identity function, but is typed as a `Checker` to indicate its + * intended use. + * + * @type {Checker} + */ +export const identChecker = (cond, _details) => cond; +harden(identChecker); diff --git a/packages/common/src/list-difference.js b/packages/common/src/list-difference.js new file mode 100644 index 0000000000..9074ca8c1c --- /dev/null +++ b/packages/common/src/list-difference.js @@ -0,0 +1,10 @@ +/** + * + * @param {Array} leftNames + * @param {Array} rightNames + */ +export const listDifference = (leftNames, rightNames) => { + const rightSet = new Set(rightNames); + return leftNames.filter(name => !rightSet.has(name)); +}; +harden(listDifference); diff --git a/packages/common/src/make-array-iterator.js b/packages/common/src/make-array-iterator.js new file mode 100644 index 0000000000..d3bcbc35db --- /dev/null +++ b/packages/common/src/make-array-iterator.js @@ -0,0 +1,25 @@ +import { makeIterator } from './make-iterator.js'; + +/** + * A `harden`ing analog of Array.prototype[Symbol.iterator]. + * + * @template [T=unknown] + * @param {Array} arr + * @returns {IterableIterator} + */ +export const makeArrayIterator = arr => { + const { length } = arr; + let i = 0; + return makeIterator(() => { + /** @type {T} */ + let value; + if (i < length) { + value = arr[i]; + i += 1; + return harden({ done: false, value }); + } + // @ts-expect-error The terminal value doesn't matter + return harden({ done: true, value }); + }); +}; +harden(makeArrayIterator); diff --git a/packages/common/src/make-iterator.js b/packages/common/src/make-iterator.js new file mode 100644 index 0000000000..c5f259afbc --- /dev/null +++ b/packages/common/src/make-iterator.js @@ -0,0 +1,15 @@ +/** + * Makes a one-shot iterable iterator from a provided `next` function. + * + * @template [T=unknown] + * @param {() => IteratorResult} next + * @returns {IterableIterator} + */ +export const makeIterator = next => { + const iter = harden({ + [Symbol.iterator]: () => iter, + next, + }); + return iter; +}; +harden(makeIterator); diff --git a/packages/common/src/object-map.js b/packages/common/src/object-map.js new file mode 100644 index 0000000000..115098517a --- /dev/null +++ b/packages/common/src/object-map.js @@ -0,0 +1,49 @@ +const { entries, fromEntries } = Object; + +/** + * By analogy with how `Array.prototype.map` will map the elements of + * an array to transformed elements of an array of the same shape, + * `objectMap` will do likewise for the string-named own enumerable + * properties of an object. + * + * Typical usage applies `objectMap` to a CopyRecord, i.e., + * an object for which `passStyleOf(original) === 'copyRecord'`. For these, + * none of the following edge cases arise. The result will be a CopyRecord + * with exactly the same property names, whose values are the mapped form of + * the original's values. + * + * When the original is not a CopyRecord, some edge cases to be aware of + * * No matter how mutable the original object, the returned object is + * hardened. + * * Only the string-named enumerable own properties of the original + * are mapped. All other properties are ignored. + * * If any of the original properties were accessors, `Object.entries` + * will cause its `getter` to be called and will use the resulting + * value. + * * No matter whether the original property was an accessor, writable, + * or configurable, all the properties of the returned object will be + * non-writable, non-configurable, data properties. + * * No matter what the original object may have inherited from, and + * no matter whether it was a special kind of object such as an array, + * the returned object will always be a plain object inheriting directly + * from `Object.prototype` and whose state is only these new mapped + * own properties. + * + * With these differences, even if the original object was not a CopyRecord, + * if all the mapped values are Passable, then the returned object will be + * a CopyRecord. + * + * @template {Record} O + * @template R map result + * @param {O} original + * @param {(value: O[keyof O], key: keyof O) => R} mapFn + * @returns {Record} + */ +export const objectMap = (original, mapFn) => { + const ents = entries(original); + const mapEnts = ents.map( + ([k, v]) => /** @type {[keyof O, R]} */ ([k, mapFn(v, k)]), + ); + return /** @type {Record} */ (harden(fromEntries(mapEnts))); +}; +harden(objectMap); diff --git a/packages/common/src/object-meta-assign.js b/packages/common/src/object-meta-assign.js new file mode 100644 index 0000000000..c6ccf8ef82 --- /dev/null +++ b/packages/common/src/object-meta-assign.js @@ -0,0 +1,26 @@ +const { getOwnPropertyDescriptors, defineProperties } = Object; + +/** + * Like `Object.assign` but at the reflective level of property descriptors + * rather than property values. + * + * Unlike `Object.assign`, this includes all own properties, whether enumerable + * or not. An original accessor property is copied by sharing its getter and + * setter, rather than calling the getter to obtain a value. If an original + * property is non-configurable, a property of the same name on a later original + * that would conflict instead causes the call to `objectMetaAssign` to throw an + * error. + * + * Returns the enhanced `target` after hardening. + * + * @param {any} target + * @param {any[]} originals + * @returns {any} + */ +export const objectMetaAssign = (target, ...originals) => { + for (const original of originals) { + defineProperties(target, getOwnPropertyDescriptors(original)); + } + return harden(target); +}; +harden(objectMetaAssign); diff --git a/packages/common/src/object-meta-map.js b/packages/common/src/object-meta-map.js new file mode 100644 index 0000000000..d50ef149d7 --- /dev/null +++ b/packages/common/src/object-meta-map.js @@ -0,0 +1,50 @@ +const { getOwnPropertyDescriptors, create, fromEntries } = Object; +const { ownKeys } = Reflect; + +/** + * Like `objectMap`, but at the reflective level of property descriptors + * rather than property values. + * + * Except for hardening, the edge case behavior is mostly the opposite of + * the `objectMap` edge cases. + * * No matter how mutable the original object, the returned object is + * hardened. + * * All own properties of the original are mapped, even if symbol-named + * or non-enumerable. + * * If any of the original properties were accessors, the descriptor + * containing the getter and setter are given to `metaMapFn`. + * * The own properties of the returned are according to the descriptors + * returned by `metaMapFn`. + * * The returned object will always be a plain object whose state is + * only these mapped own properties. It will inherit from the third + * argument if provided, defaulting to `Object.prototype` if omitted. + * + * Because a property descriptor is distinct from `undefined`, we bundle + * mapping and filtering together. When the `metaMapFn` returns `undefined`, + * that property is omitted from the result. + * + * @template {Record} O + * @param {O} original + * @param {( + * desc: TypedPropertyDescriptor, + * key: keyof O + * ) => (PropertyDescriptor | undefined)} metaMapFn + * @param {any} [proto] + * @returns {any} + */ +export const objectMetaMap = ( + original, + metaMapFn, + proto = Object.prototype, +) => { + const descs = getOwnPropertyDescriptors(original); + const keys = ownKeys(original); + + const descEntries = /** @type {[PropertyKey,PropertyDescriptor][]} */ ( + keys + .map(key => [key, metaMapFn(descs[key], key)]) + .filter(([_key, optDesc]) => optDesc !== undefined) + ); + return harden(create(proto, fromEntries(descEntries))); +}; +harden(objectMetaMap); diff --git a/packages/common/src/throw-labeled.js b/packages/common/src/throw-labeled.js new file mode 100644 index 0000000000..5779aefe4a --- /dev/null +++ b/packages/common/src/throw-labeled.js @@ -0,0 +1,20 @@ +const { details: X } = assert; + +/** + * @param {Error} innerErr + * @param {string|number} label + * @param {ErrorConstructor=} ErrorConstructor + * @returns {never} + */ +export const throwLabeled = (innerErr, label, ErrorConstructor = undefined) => { + if (typeof label === 'number') { + label = `[${label}]`; + } + const outerErr = assert.error( + `${label}: ${innerErr.message}`, + ErrorConstructor, + ); + assert.note(outerErr, X`Caused by ${innerErr}`); + throw outerErr; +}; +harden(throwLabeled); diff --git a/packages/utils/test/prepare-test-env-ava.js b/packages/common/test/prepare-test-env-ava.js similarity index 100% rename from packages/utils/test/prepare-test-env-ava.js rename to packages/common/test/prepare-test-env-ava.js diff --git a/packages/common/test/test-apply-labeling-error.js b/packages/common/test/test-apply-labeling-error.js new file mode 100644 index 0000000000..f18616c524 --- /dev/null +++ b/packages/common/test/test-apply-labeling-error.js @@ -0,0 +1,28 @@ +import { test } from './prepare-test-env-ava.js'; +import { applyLabelingError } from '../src/apply-labeling-error.js'; + +const { Fail } = assert; + +test('test applyLabelingError', async t => { + t.is( + applyLabelingError(x => x * 2, [8]), + 16, + ); + t.is( + applyLabelingError(x => x * 2, [8], 'foo'), + 16, + ); + t.is(await applyLabelingError(async x => x * 2, [8], 'foo'), 16); + t.throws(() => applyLabelingError(x => Fail`${x}`, ['e']), { + message: '"e"', + }); + t.throws(() => applyLabelingError(x => Fail`${x}`, ['e'], 'foo'), { + message: 'foo: "e"', + }); + await t.throwsAsync( + async () => applyLabelingError(x => Fail`${x}`, ['e'], 'foo'), + { + message: 'foo: "e"', + }, + ); +}); diff --git a/packages/common/test/test-from-unique-entries.js b/packages/common/test/test-from-unique-entries.js new file mode 100644 index 0000000000..78e19b16f0 --- /dev/null +++ b/packages/common/test/test-from-unique-entries.js @@ -0,0 +1,23 @@ +import { test } from './prepare-test-env-ava.js'; +import { fromUniqueEntries } from '../src/from-unique-entries.js'; + +test('test fromUniqueEntries', async t => { + t.deepEqual( + fromUniqueEntries([ + ['a', 1], + ['b', 2], + ]), + { a: 1, b: 2 }, + ); + + t.throws( + () => + fromUniqueEntries([ + ['a', 1], + ['a', 2], + ]), + { + message: 'collision on property name "a": [["a",1],["a",2]]', + }, + ); +}); diff --git a/packages/common/test/test-ident-checker.js b/packages/common/test/test-ident-checker.js new file mode 100644 index 0000000000..5a20e269a6 --- /dev/null +++ b/packages/common/test/test-ident-checker.js @@ -0,0 +1,7 @@ +import { test } from './prepare-test-env-ava.js'; +import { identChecker } from '../src/ident-checker.js'; + +test('test identChecker', async t => { + t.is(identChecker(true, 'x'), true); + t.is(identChecker(false, 'x'), false); +}); diff --git a/packages/common/test/test-list-difference.js b/packages/common/test/test-list-difference.js new file mode 100644 index 0000000000..6f5e1ff388 --- /dev/null +++ b/packages/common/test/test-list-difference.js @@ -0,0 +1,6 @@ +import { test } from './prepare-test-env-ava.js'; +import { listDifference } from '../src/list-difference.js'; + +test('test listDifference', async t => { + t.deepEqual(listDifference(['a', 'b', 'c'], ['b', 'c', 'd']), ['a']); +}); diff --git a/packages/common/test/test-make-array-iterator.js b/packages/common/test/test-make-array-iterator.js new file mode 100644 index 0000000000..856431c21c --- /dev/null +++ b/packages/common/test/test-make-array-iterator.js @@ -0,0 +1,18 @@ +import { test } from './prepare-test-env-ava.js'; +import { makeArrayIterator } from '../src/make-array-iterator.js'; + +// Also serves as an adequate test of make-iterator.js + +test('test makeArrayIterator', async t => { + const iter = makeArrayIterator([1, 2, 3]); + t.is(iter[Symbol.iterator](), iter); + t.deepEqual(iter.next(), { + done: false, + value: 1, + }); + t.deepEqual([...iter], [2, 3]); + t.deepEqual(iter.next(), { + done: true, + value: undefined, + }); +}); diff --git a/packages/common/test/test-object-map.js b/packages/common/test/test-object-map.js new file mode 100644 index 0000000000..202b1c17bf --- /dev/null +++ b/packages/common/test/test-object-map.js @@ -0,0 +1,9 @@ +import { test } from './prepare-test-env-ava.js'; +import { objectMap } from '../src/object-map.js'; + +test('test objectMap', async t => { + t.deepEqual( + objectMap({ a: 1, b: 2 }, n => n * 2), + { a: 2, b: 4 }, + ); +}); diff --git a/packages/common/test/test-object-meta-assign.js b/packages/common/test/test-object-meta-assign.js new file mode 100644 index 0000000000..f3f0459a1c --- /dev/null +++ b/packages/common/test/test-object-meta-assign.js @@ -0,0 +1,8 @@ +import { test } from './prepare-test-env-ava.js'; +import { objectMetaAssign } from '../src/object-meta-assign.js'; + +test('test objectMetaAssign', async t => { + t.deepEqual(objectMetaAssign({}, { a: 1 }, { a: 2, b: 3 }), { a: 2, b: 3 }); + + // TODO more testing +}); diff --git a/packages/common/test/test-object-meta-map.js b/packages/common/test/test-object-meta-map.js new file mode 100644 index 0000000000..2729c158b7 --- /dev/null +++ b/packages/common/test/test-object-meta-map.js @@ -0,0 +1,34 @@ +import { test } from './prepare-test-env-ava.js'; +import { objectMetaMap } from '../src/object-meta-map.js'; + +const { getOwnPropertyDescriptors, getPrototypeOf } = Object; + +test('test objectMetaMap', async t => { + const mapped = objectMetaMap( + { a: 1, b: 2, c: 3 }, + (desc, key) => + key === 'b' + ? undefined + : { + ...desc, + value: desc.value * 2, + enumerable: false, + }, + null, + ); + t.deepEqual(getOwnPropertyDescriptors(mapped), { + a: { + value: 2, + writable: false, + enumerable: false, + configurable: false, + }, + c: { + value: 6, + writable: false, + enumerable: false, + configurable: false, + }, + }); + t.is(getPrototypeOf(mapped), null); +}); diff --git a/packages/common/test/test-throw-labeled.js b/packages/common/test/test-throw-labeled.js new file mode 100644 index 0000000000..2780e16652 --- /dev/null +++ b/packages/common/test/test-throw-labeled.js @@ -0,0 +1,8 @@ +import { test } from './prepare-test-env-ava.js'; +import { throwLabeled } from '../src/throw-labeled.js'; + +test('test throwLabeled', async t => { + t.throws(() => throwLabeled(Error('e'), 'foo'), { + message: 'foo: e', + }); +}); diff --git a/packages/utils/tsconfig.build.json b/packages/common/tsconfig.build.json similarity index 100% rename from packages/utils/tsconfig.build.json rename to packages/common/tsconfig.build.json diff --git a/packages/utils/tsconfig.json b/packages/common/tsconfig.json similarity index 100% rename from packages/utils/tsconfig.json rename to packages/common/tsconfig.json diff --git a/packages/errors/package.json b/packages/errors/package.json index 934cc1f196..d7ba284e70 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -5,7 +5,7 @@ "keywords": [], "author": "Endo contributors", "license": "Apache-2.0", - "homepage": "https://github.com/endojs/endo/tree/master/packages/skel#readme", + "homepage": "https://github.com/endojs/endo/tree/master/packages/errors#readme", "repository": { "type": "git", "url": "git+https://github.com/endojs/endo.git" diff --git a/packages/exo/package.json b/packages/exo/package.json index 64a1ea610d..ecdcbf3863 100644 --- a/packages/exo/package.json +++ b/packages/exo/package.json @@ -32,12 +32,12 @@ "test": "ava" }, "dependencies": { + "@endo/common": "^1.0.1", "@endo/env-options": "^1.0.1", "@endo/eventual-send": "^1.0.1", "@endo/far": "^1.0.1", "@endo/pass-style": "^1.0.1", - "@endo/patterns": "^1.0.1", - "@endo/utils": "^1.0.1" + "@endo/patterns": "^1.0.1" }, "devDependencies": { "@endo/init": "^1.0.1", diff --git a/packages/exo/src/exo-makers.js b/packages/exo/src/exo-makers.js index aeea9451a8..eb1dd4441f 100644 --- a/packages/exo/src/exo-makers.js +++ b/packages/exo/src/exo-makers.js @@ -1,6 +1,6 @@ /// import { environmentOptionsListHas } from '@endo/env-options'; -import { objectMap } from '@endo/utils'; +import { objectMap } from '@endo/common'; import { defendPrototype, defendPrototypeKit } from './exo-tools.js'; diff --git a/packages/exo/src/exo-tools.js b/packages/exo/src/exo-tools.js index 08b6f4ba5c..1dfc7fbb0f 100644 --- a/packages/exo/src/exo-tools.js +++ b/packages/exo/src/exo-tools.js @@ -11,7 +11,7 @@ import { getInterfaceGuardPayload, getCopyMapEntries, } from '@endo/patterns'; -import { listDifference, objectMap } from '@endo/utils'; +import { listDifference, objectMap } from '@endo/common'; import { GET_INTERFACE_GUARD } from './get-interface.js'; /** @typedef {import('@endo/patterns').Method} Method */ diff --git a/packages/patterns/index.js b/packages/patterns/index.js index 0541af89c0..cbc64805db 100644 --- a/packages/patterns/index.js +++ b/packages/patterns/index.js @@ -79,13 +79,13 @@ export * from './src/types.js'; export { /** * @deprecated - * Import directly from `@endo/utils` instead. + * Import directly from `@endo/common` instead. */ listDifference, /** * @deprecated - * Import directly from `@endo/utils` instead. + * Import directly from `@endo/common` instead. */ objectMap, -} from '@endo/utils'; +} from '@endo/common'; diff --git a/packages/patterns/package.json b/packages/patterns/package.json index 5034bd83c1..a6e4e5cc6a 100644 --- a/packages/patterns/package.json +++ b/packages/patterns/package.json @@ -31,10 +31,10 @@ "test": "ava" }, "dependencies": { + "@endo/common": "^1.0.1", "@endo/eventual-send": "^1.0.1", "@endo/marshal": "^1.0.1", - "@endo/promise-kit": "^1.0.1", - "@endo/utils": "^1.0.1" + "@endo/promise-kit": "^1.0.1" }, "devDependencies": { "@endo/init": "^1.0.1", diff --git a/packages/patterns/src/keys/checkKey.js b/packages/patterns/src/keys/checkKey.js index 46eba6afeb..c1c7a01a7a 100644 --- a/packages/patterns/src/keys/checkKey.js +++ b/packages/patterns/src/keys/checkKey.js @@ -12,7 +12,7 @@ import { makeFullOrderComparatorKit, sortByRank, } from '@endo/marshal'; -import { identChecker } from '@endo/utils'; +import { identChecker } from '@endo/common'; import { checkElements, makeSetOfElements } from './copySet.js'; import { checkBagEntries, makeBagOfEntries } from './copyBag.js'; diff --git a/packages/patterns/src/keys/keycollection-operators.js b/packages/patterns/src/keys/keycollection-operators.js index ad4bede2d8..0aec3b8364 100644 --- a/packages/patterns/src/keys/keycollection-operators.js +++ b/packages/patterns/src/keys/keycollection-operators.js @@ -5,7 +5,7 @@ import { makeFullOrderComparatorKit, sortByRank, } from '@endo/marshal'; -import { makeIterator, makeArrayIterator } from '@endo/utils'; +import { makeIterator, makeArrayIterator } from '@endo/common'; /** @typedef {import('@endo/marshal').RankCompare} RankCompare */ /** @typedef {import('../types').KeyComparison} KeyComparison */ diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 3c40955b5d..cbef8d9f21 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -18,7 +18,7 @@ import { applyLabelingError, fromUniqueEntries, listDifference, -} from '@endo/utils'; +} from '@endo/common'; import { keyEQ, keyGT, keyGTE, keyLT, keyLTE } from '../keys/compareKeys.js'; import { diff --git a/packages/utils/README.md b/packages/utils/README.md deleted file mode 100644 index 8b13789179..0000000000 --- a/packages/utils/README.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/utils/index.js b/packages/utils/index.js deleted file mode 100644 index 6fe5c17341..0000000000 --- a/packages/utils/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './src/utils.js'; diff --git a/packages/utils/src/utils.js b/packages/utils/src/utils.js deleted file mode 100644 index 16f4f3cdc6..0000000000 --- a/packages/utils/src/utils.js +++ /dev/null @@ -1,302 +0,0 @@ -// @ts-check -import { E } from '@endo/eventual-send'; -import { isPromise } from '@endo/promise-kit'; - -const { - fromEntries, - entries, - getOwnPropertyDescriptors, - create, - defineProperties, -} = Object; -const { ownKeys } = Reflect; - -const { details: X, quote: q, Fail } = assert; - -// TODO Complete migration of Checker type from @endo/pass-style to @endo/utils -// by having @endo/pass-style, and everyone else who needs it, import it from -// @endo/utils. -/** - * @callback Checker - * Internal to a useful pattern for writing checking logic - * (a "checkFoo" function) that can be used to implement a predicate - * (an "isFoo" function) or a validator (an "assertFoo" function). - * - * * A predicate ideally only returns `true` or `false` and rarely throws. - * * A validator throws an informative diagnostic when the predicate - * would have returned `false`, and simply returns `undefined` normally - * when the predicate would have returned `true`. - * * The internal checking function that they share is parameterized by a - * `Checker` that determines how to proceed with a failure condition. - * Predicates pass in an identity function as checker. Validators - * pass in `assertChecker` which is a trivial wrapper around `assert`. - * - * See the various uses for good examples. - * @param {boolean} cond - * @param {import('ses').Details} [details] - * @returns {boolean} - */ - -/** - * In the `assertFoo`/`isFoo`/`checkFoo` pattern, `checkFoo` has a `check` - * parameter of type `Checker`. `assertFoo` calls `checkFoo` passes - * `assertChecker` as the `check` argument. `isFoo` passes `identChecker` - * as the `check` argument. `identChecker` acts precisely like an - * identity function, but is typed as a `Checker` to indicate its - * intended use. - * - * @type {Checker} - */ -export const identChecker = (cond, _details) => cond; -harden(identChecker); - -/** - * Throws if multiple entries use the same property name. Otherwise acts - * like `Object.fromEntries` but hardens the result. - * Use it to protect from property names computed from user-provided data. - * - * @template K,V - * @param {Iterable<[K,V]>} allEntries - * @returns {{[k: K]: V}} - */ -export const fromUniqueEntries = allEntries => { - const entriesArray = [...allEntries]; - const result = harden(fromEntries(entriesArray)); - if (ownKeys(result).length === entriesArray.length) { - return result; - } - const names = new Set(); - for (const [name, _] of entriesArray) { - if (names.has(name)) { - Fail`collision on property name ${q(name)}: ${entriesArray}`; - } - names.add(name); - } - throw Fail`internal: failed to create object from unique entries`; -}; -harden(fromUniqueEntries); - -/** - * By analogy with how `Array.prototype.map` will map the elements of - * an array to transformed elements of an array of the same shape, - * `objectMap` will do likewise for the string-named own enumerable - * properties of an object. - * - * Typical usage applies `objectMap` to a CopyRecord, i.e., - * an object for which `passStyleOf(original) === 'copyRecord'`. For these, - * none of the following edge cases arise. The result will be a CopyRecord - * with exactly the same property names, whose values are the mapped form of - * the original's values. - * - * When the original is not a CopyRecord, some edge cases to be aware of - * * No matter how mutable the original object, the returned object is - * hardened. - * * Only the string-named enumerable own properties of the original - * are mapped. All other properties are ignored. - * * If any of the original properties were accessors, `Object.entries` - * will cause its `getter` to be called and will use the resulting - * value. - * * No matter whether the original property was an accessor, writable, - * or configurable, all the properties of the returned object will be - * non-writable, non-configurable, data properties. - * * No matter what the original object may have inherited from, and - * no matter whether it was a special kind of object such as an array, - * the returned object will always be a plain object inheriting directly - * from `Object.prototype` and whose state is only these new mapped - * own properties. - * - * With these differences, even if the original object was not a CopyRecord, - * if all the mapped values are Passable, then the returned object will be - * a CopyRecord. - * - * @template {Record} O - * @template R map result - * @param {O} original - * @param {(value: O[keyof O], key: keyof O) => R} mapFn - * @returns {Record} - */ -export const objectMap = (original, mapFn) => { - const ents = entries(original); - const mapEnts = ents.map( - ([k, v]) => /** @type {[keyof O, R]} */ ([k, mapFn(v, k)]), - ); - return /** @type {Record} */ (harden(fromEntries(mapEnts))); -}; -harden(objectMap); - -/** - * Like `objectMap`, but at the reflective level of property descriptors - * rather than property values. - * - * Except for hardening, the edge case behavior is mostly the opposite of - * the `objectMap` edge cases. - * * No matter how mutable the original object, the returned object is - * hardened. - * * All own properties of the original are mapped, even if symbol-named - * or non-enumerable. - * * If any of the original properties were accessors, the descriptor - * containing the getter and setter are given to `metaMapFn`. - * * The own properties of the returned are according to the descriptors - * returned by `metaMapFn`. - * * The returned object will always be a plain object whose state is - * only these mapped own properties. It will inherit from the third - * argument if provided, defaulting to `Object.prototype` if omitted. - * - * Because a property descriptor is distinct from `undefined`, we bundle - * mapping and filtering together. When the `metaMapFn` returns `undefined`, - * that property is omitted from the result. - * - * @template {Record} O - * @param {O} original - * @param {( - * desc: TypedPropertyDescriptor, - * key: keyof O - * ) => (PropertyDescriptor | undefined)} metaMapFn - * @param {any} [proto] - * @returns {any} - */ -export const objectMetaMap = ( - original, - metaMapFn, - proto = Object.prototype, -) => { - const descs = getOwnPropertyDescriptors(original); - const keys = ownKeys(original); - - const descEntries = /** @type {[PropertyKey,PropertyDescriptor][]} */ ( - keys - .map(key => [key, metaMapFn(descs[key], key)]) - .filter(([_key, optDesc]) => optDesc !== undefined) - ); - return harden(create(proto, fromUniqueEntries(descEntries))); -}; -harden(objectMetaMap); - -/** - * Like `Object.assign` but at the reflective level of property descriptors - * rather than property values. - * - * Unlike `Object.assign`, this includes all own properties, whether enumerable - * or not. An original accessor property is copied by sharing its getter and - * setter, rather than calling the getter to obtain a value. If an original - * property is non-configurable, a property of the same name on a later original - * that would conflict instead causes the call to `objectMetaAssign` to throw an - * error. - * - * Returns the enhanced `target` after hardening. - * - * @param {any} target - * @param {any[]} originals - * @returns {any} - */ -export const objectMetaAssign = (target, ...originals) => { - for (const original of originals) { - defineProperties(target, getOwnPropertyDescriptors(original)); - } - return harden(target); -}; -harden(objectMetaAssign); - -/** - * - * @param {Array} leftNames - * @param {Array} rightNames - */ -export const listDifference = (leftNames, rightNames) => { - const rightSet = new Set(rightNames); - return leftNames.filter(name => !rightSet.has(name)); -}; -harden(listDifference); - -/** - * @param {Error} innerErr - * @param {string|number} label - * @param {ErrorConstructor=} ErrorConstructor - * @returns {never} - */ -export const throwLabeled = (innerErr, label, ErrorConstructor = undefined) => { - if (typeof label === 'number') { - label = `[${label}]`; - } - const outerErr = assert.error( - `${label}: ${innerErr.message}`, - ErrorConstructor, - ); - assert.note(outerErr, X`Caused by ${innerErr}`); - throw outerErr; -}; -harden(throwLabeled); - -/** - * @template A,R - * @param {(...args: A[]) => R} func - * @param {A[]} args - * @param {string|number} [label] - * @returns {R} - */ -export const applyLabelingError = (func, args, label = undefined) => { - if (label === undefined) { - return func(...args); - } - let result; - try { - result = func(...args); - } catch (err) { - throwLabeled(err, label); - } - if (isPromise(result)) { - // Cannot be at-ts-expect-error because there is no type error locally. - // Rather, a type error only as imported into exo. - // @ts-ignore If result is a rejected promise, this will - // return a promise with a different rejection reason. But this - // confuses TypeScript because it types that case as `Promise` - // which is cool for a promise that will never fulfll. - // But TypeScript doesn't understand that this will only happen - // when `result` was a rejected promise. In only this case `R` - // should already allow `Promise` as a subtype. - return E.when(result, undefined, reason => throwLabeled(reason, label)); - } else { - return result; - } -}; -harden(applyLabelingError); - -/** - * Makes a one-shot iterable iterator from a provided `next` function. - * - * @template [T=unknown] - * @param {() => IteratorResult} next - * @returns {IterableIterator} - */ -export const makeIterator = next => { - const iter = harden({ - [Symbol.iterator]: () => iter, - next, - }); - return iter; -}; -harden(makeIterator); - -/** - * A `harden`ing analog of Array.prototype[Symbol.iterator]. - * - * @template [T=unknown] - * @param {Array} arr - * @returns {IterableIterator} - */ -export const makeArrayIterator = arr => { - const { length } = arr; - let i = 0; - return makeIterator(() => { - /** @type {T} */ - let value; - if (i < length) { - value = arr[i]; - i += 1; - return harden({ done: false, value }); - } - // @ts-expect-error The terminal value doesn't matter - return harden({ done: true, value }); - }); -}; -harden(makeArrayIterator); diff --git a/packages/utils/test/test-index.js b/packages/utils/test/test-index.js deleted file mode 100644 index 9da523a4be..0000000000 --- a/packages/utils/test/test-index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { test } from './prepare-test-env-ava.js'; - -test('place holder', async t => { - t.pass(); -});