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
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user