Files
captoys/lib/src/patterns.ts
T
dckc 357e4561c4 capsite/lib: endo's own matcher data, smallcaps, and MapStoreDo
Matchers are now exactly what @endo/patterns makes: tagged records as
from pass-style's makeTagged (M.string() is match:string with payload
[], M.any() is match:any with payload undefined). Structured clone drops
their symbol-keyed and non-enumerable properties, so they cross DO
storage and Workers RPC in marshal's smallcaps encoding (the subset
patterns need).

MapStoreDo is a MapStore as a DO of its own: its creator fixes label and
(encoded) shapes once; its id is full authority, and readOnly() hands
out the queries alone.

Generated-by: Claude Opus 5.5
2026-10-09 02:32:22 -05:00

130 lines
4.9 KiB
TypeScript

/**
* @file a work-alike for enough of @endo/patterns: `M.string()`, `M.any()`,
* and literal patterns (a string, number, boolean, or null matches only
* itself, so `"cookie"` as a key shape admits exactly one key). Matchers
* are the same data endo makes: tagged records, as from @endo/pass-style's
* `makeTagged` (frozen here, where endo would harden).
*
* Structured clone (DO storage, Workers RPC) drops a tagged record's
* symbol-keyed and non-enumerable properties, so patterns cross those
* boundaries in endo's smallcaps encoding: see {@link toSmallcaps}.
* @see https://github.com/endojs/endo/tree/master/packages/patterns
* @see https://github.com/endojs/endo/blob/master/packages/marshal/src/encodeToSmallcaps.js
*/
const PASS_STYLE = Symbol.for("passStyle");
export type Tagged = Readonly<{ [Symbol.toStringTag]: string; payload: unknown }>;
export type Pattern = Tagged | string | number | boolean | null;
/** @endo/pass-style's `makeTagged` */
export function makeTagged(tag: string, payload: unknown): Tagged {
return Object.freeze(
Object.create(Object.prototype, {
[PASS_STYLE]: { value: "tagged" },
[Symbol.toStringTag]: { value: tag },
payload: { value: payload, enumerable: true },
}),
) as Tagged;
}
const isTagged = (x: unknown): x is Tagged =>
typeof x === "object" && x !== null && (x as Record<symbol, unknown>)[PASS_STYLE] === "tagged";
const StringShape = makeTagged("match:string", Object.freeze([]));
const AnyShape = makeTagged("match:any", undefined);
export const M = Object.freeze({
string: (): Tagged => StringShape,
any: (): Tagged => AnyShape,
});
export function matches(specimen: unknown, pattern: Pattern): boolean {
if (typeof pattern === "object" && pattern !== null) {
const tag = isTagged(pattern) ? pattern[Symbol.toStringTag] : undefined;
switch (tag) {
case "match:string":
return typeof specimen === "string";
case "match:any":
return true;
default:
throw new TypeError("not a pattern");
}
}
return Object.is(specimen, pattern);
}
/** the pattern as written in code, e.g. `M.string()` or `"cookie"` */
export function describe(pattern: Pattern): string {
if (typeof pattern === "object" && pattern !== null) {
return `M.${pattern[Symbol.toStringTag].replace(/^match:/, "")}()`;
}
return JSON.stringify(pattern);
}
/**
* throw unless `specimen` matches `pattern`. Unlike endo's, the message
* names the pattern and `label` but never the specimen: specimens here
* can be credentials, and messages cross RPC and land in logs.
*/
export function mustMatch(specimen: unknown, pattern: Pattern, label: string): void {
if (!matches(specimen, pattern)) {
throw new TypeError(`${label}: must match ${describe(pattern)}`);
}
}
/** a smallcaps-encoded passable, as JSON-able data */
export type Smallcaps = null | boolean | number | string | Smallcaps[] | { "#tag": string; payload: Smallcaps };
/** smallcaps quotes strings that start with one of these with `!` */
const startsSpecial = (s: string) => /^[!-\-]/.test(s);
/**
* the subset of @endo/marshal's smallcaps encoding that patterns here use:
* primitives, arrays, and tagged records (not a whole CapData: no `#`
* body prefix, no slots)
*/
export function toSmallcaps(passable: unknown): Smallcaps {
if (passable === undefined) return "#undefined";
if (passable === null || typeof passable === "boolean") return passable;
if (typeof passable === "number") {
if (Number.isNaN(passable)) return "#NaN";
if (passable === Infinity) return "#Infinity";
if (passable === -Infinity) return "#-Infinity";
return Object.is(passable, -0) ? 0 : passable;
}
if (typeof passable === "string") return startsSpecial(passable) ? `!${passable}` : passable;
if (Array.isArray(passable)) return passable.map(toSmallcaps);
if (isTagged(passable)) {
return { "#tag": toSmallcaps(passable[Symbol.toStringTag]) as string, payload: toSmallcaps(passable.payload) };
}
throw new TypeError("toSmallcaps: unsupported passable");
}
/** the inverse of {@link toSmallcaps} */
export function fromSmallcaps(encoding: Smallcaps): unknown {
if (encoding === null || typeof encoding === "boolean" || typeof encoding === "number") return encoding;
if (typeof encoding === "string") {
if (encoding.startsWith("!")) return encoding.slice(1);
switch (encoding) {
case "#undefined":
return undefined;
case "#NaN":
return NaN;
case "#Infinity":
return Infinity;
case "#-Infinity":
return -Infinity;
}
if (startsSpecial(encoding)) throw new TypeError("fromSmallcaps: unsupported special string");
return encoding;
}
if (Array.isArray(encoding)) return Object.freeze(encoding.map(fromSmallcaps));
if (typeof encoding === "object" && "#tag" in encoding) {
const { "#tag": tag, payload, ...rest } = encoding;
if (Object.keys(rest).length > 0) throw new TypeError("fromSmallcaps: extra properties on a tagged record");
return makeTagged(fromSmallcaps(tag) as string, fromSmallcaps(payload));
}
throw new TypeError("fromSmallcaps: unsupported encoding");
}