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:
dckc committed 2026-10-09 02:32:22 -05:00
1 parent 10414c2ce6
commit 357e4561c4
5 files changed
+225 -26

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
/**
* @file a MapStore as a Durable Object of its own, so a store can be held
* apart from whoever reads it: the DO id is the store's full authority
* (e.g. the account owner's, to rotate a credential), while a reader is
* handed only {@link MapStoreDo.readOnly}.
*/
import { DurableObject } from "cloudflare:workers";
import { fromSmallcaps, matches, toSmallcaps, type Pattern, type Smallcaps } from "./patterns.js";
import { makeMapStore, type MapStore, type ReadonlyMapStore, type StoreShapes } from "./store.js";
/** store shapes as they cross RPC and sit in storage: smallcaps-encoded
* (structured clone would strip a matcher down to its payload) */
export type EncodedShapes = { keyShape?: Smallcaps; valueShape?: Smallcaps };
export const encodeShapes = ({ keyShape, valueShape }: StoreShapes): EncodedShapes => ({
...(keyShape === undefined ? {} : { keyShape: toSmallcaps(keyShape) }),
...(valueShape === undefined ? {} : { valueShape: toSmallcaps(valueShape) }),
});
const decodeShapes = ({ keyShape, valueShape }: EncodedShapes): StoreShapes => ({
...(keyShape === undefined ? {} : { keyShape: fromSmallcaps(keyShape) as Pattern }),
...(valueShape === undefined ? {} : { valueShape: fromSmallcaps(valueShape) as Pattern }),
});
type Config = { label: string; shapes: EncodedShapes };
/** a store whose label and shapes its creator fixes, once */
export class MapStoreDo extends DurableObject {
/** one-shot: fix the label and shapes (see {@link encodeShapes}); an
* identical re-configure is a no-op */
configure(label: string, shapes: EncodedShapes): void {
const decoded = decodeShapes(shapes);
for (const shape of [decoded.keyShape, decoded.valueShape]) {
if (shape !== undefined) matches(undefined, shape); // throws on a non-pattern
}
makeMapStore(label, decoded, this.ctx.storage.kv); // throws on a bad label
const config: Config = { label, shapes: encodeShapes(decoded) };
const existing = this.ctx.storage.kv.get<Config>("config");
if (existing) {
if (JSON.stringify(existing) !== JSON.stringify(config)) {
throw new Error("already configured with a different label or shapes");
}
return;
}
this.ctx.storage.kv.put("config", config);
}
/** the store, with full authority */
store(): MapStore<unknown> {
const config = this.ctx.storage.kv.get<Config>("config");
if (!config) throw new Error("not configured: this store was never set up");
return makeMapStore(config.label, decodeShapes(config.shapes), this.ctx.storage.kv);
}
/** the store's queries alone */
readOnly(): ReadonlyMapStore<unknown> {
return this.store().readOnly();
}
}