/** * @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"); 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 { const config = this.ctx.storage.kv.get("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 { return this.store().readOnly(); } }