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
This commit is contained in:
1 parent
10414c2ce6
commit
357e4561c4
5 files changed
+225
-26
No files matched your search
+100
-22
@@ -1,43 +1,67 @@
|
||||
/**
|
||||
* @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).
|
||||
* 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
|
||||
*/
|
||||
|
||||
type MatcherKind = "string" | "any";
|
||||
const PASS_STYLE = Symbol.for("passStyle");
|
||||
|
||||
/** a matcher made by {@link M}; recognized by identity, so it is meaningful
|
||||
* only in the realm that made it (it does not survive an RPC copy) */
|
||||
export type Matcher = Readonly<{ toString(): string }>;
|
||||
export type Pattern = Matcher | string | number | boolean | null;
|
||||
export type Tagged = Readonly<{ [Symbol.toStringTag]: string; payload: unknown }>;
|
||||
export type Pattern = Tagged | string | number | boolean | null;
|
||||
|
||||
const kinds = new WeakMap<object, MatcherKind>();
|
||||
/** @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 makeMatcher = (kind: MatcherKind, label: string): Matcher => {
|
||||
const matcher = Object.freeze({ toString: () => label });
|
||||
kinds.set(matcher, kind);
|
||||
return matcher;
|
||||
};
|
||||
const isTagged = (x: unknown): x is Tagged =>
|
||||
typeof x === "object" && x !== null && (x as Record<symbol, unknown>)[PASS_STYLE] === "tagged";
|
||||
|
||||
const STRING = makeMatcher("string", "M.string()");
|
||||
const ANY = makeMatcher("any", "M.any()");
|
||||
const StringShape = makeTagged("match:string", Object.freeze([]));
|
||||
const AnyShape = makeTagged("match:any", undefined);
|
||||
|
||||
export const M = Object.freeze({
|
||||
string: (): Matcher => STRING,
|
||||
any: (): Matcher => ANY,
|
||||
string: (): Tagged => StringShape,
|
||||
any: (): Tagged => AnyShape,
|
||||
});
|
||||
|
||||
export function matches(specimen: unknown, pattern: Pattern): boolean {
|
||||
if (typeof pattern === "object" && pattern !== null) {
|
||||
const kind = kinds.get(pattern);
|
||||
if (kind === "string") return typeof specimen === "string";
|
||||
if (kind === "any") return true;
|
||||
throw new TypeError("not a pattern");
|
||||
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
|
||||
@@ -45,7 +69,61 @@ export function matches(specimen: unknown, pattern: Pattern): boolean {
|
||||
*/
|
||||
export function mustMatch(specimen: unknown, pattern: Pattern, label: string): void {
|
||||
if (!matches(specimen, pattern)) {
|
||||
const shape = typeof pattern === "object" && pattern !== null ? String(pattern) : JSON.stringify(pattern);
|
||||
throw new TypeError(`${label}: must match ${shape}`);
|
||||
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");
|
||||
}
|
||||
Reference in new issue
Block a user