@brightbeamai/chap-coordinator 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -8
- package/dist/{api-59aU01Bs.d.cts → api-CjZQnj63.d.cts} +128 -14
- package/dist/{api-CxciGFHA.d.ts → api-DG0VFGVr.d.ts} +128 -14
- package/dist/api.cjs +3 -1
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +3 -1
- package/dist/api.js.map +1 -1
- package/dist/index.cjs +1336 -219
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -5
- package/dist/index.d.ts +19 -5
- package/dist/index.js +1336 -219
- package/dist/index.js.map +1 -1
- package/dist/jsonrpc.cjs +1 -0
- package/dist/jsonrpc.cjs.map +1 -1
- package/dist/jsonrpc.d.cts +1 -0
- package/dist/jsonrpc.d.ts +1 -0
- package/dist/jsonrpc.js +1 -0
- package/dist/jsonrpc.js.map +1 -1
- package/dist/methods.cjs.map +1 -1
- package/dist/methods.d.cts +36 -3
- package/dist/methods.d.ts +36 -3
- package/dist/storage/sqlite.cjs +81 -19
- package/dist/storage/sqlite.cjs.map +1 -1
- package/dist/storage/sqlite.d.cts +19 -9
- package/dist/storage/sqlite.d.ts +19 -9
- package/dist/storage/sqlite.js +81 -19
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/storage/store.cjs +20 -15
- package/dist/storage/store.cjs.map +1 -1
- package/dist/storage/store.d.cts +24 -6
- package/dist/storage/store.d.ts +24 -6
- package/dist/storage/store.js +20 -15
- package/dist/storage/store.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +56 -1
- package/dist/types.d.ts +56 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/storage/sqlite.ts","../../src/storage/store.ts"],"sourcesContent":["/**\n * SQLite-backed store using better-sqlite3.\n *\n * Schema, shared with the Python SqliteStore. Both write these tables, and\n * the same audit entries for the same calls; the objects are each\n * coordinator's own, so each restores a file it wrote, and chap-analytics\n * reads a file from either:\n *\n * CREATE TABLE chap_meta (\n * workspace TEXT PRIMARY KEY,\n * data TEXT NOT NULL, -- the workspace's own fields, JSON\n * version INTEGER NOT NULL, -- audit entries held\n * updated_at TEXT NOT NULL -- ISO-8601\n * );\n * CREATE TABLE chap_entries (\n * workspace TEXT NOT NULL,\n * seq INTEGER NOT NULL,\n * data TEXT NOT NULL, -- one audit entry, JSON\n * PRIMARY KEY (workspace, seq)\n * );\n * CREATE TABLE chap_objects (\n * workspace TEXT NOT NULL,\n * collection TEXT NOT NULL, -- members, tasks, ..., idempotency_keys\n * id TEXT NOT NULL,\n * data TEXT NOT NULL, -- one object, JSON\n * PRIMARY KEY (workspace, collection, id)\n * );\n *\n * A write adds the call's entries, updates the objects the call changed and\n * the workspace's own fields, in one transaction, so its cost does not grow\n * with the log. A write whose first entry is not the next seq the file holds\n * is refused with a StoreConflictError; see the concurrency model in\n * ./store.ts. Audit entries are only ever inserted, except when a workspace\n * is written whole: after `Coordinator.restore()`, and for a workspace an\n * application put in place itself.\n *\n * Files written by releases before 0.3.2 hold one JSON snapshot per workspace\n * in a table named chap_workspaces. Opening such a file moves each workspace\n * into the tables above, in one transaction, and renames the old table to\n * chap_workspaces_v1, which is kept as it was.\n *\n * A view named chap_workspaces takes the old table's place, with the same\n * columns, so a script that read the old table reads the current state; it\n * assembles each workspace when read, with collections keyed by id. Releases\n * before 0.3.2 refuse to open the file, since they cannot write to it.\n *\n * Usage:\n * import { Coordinator } from \"@brightbeamai/chap-coordinator\";\n * import { SqliteStore } from \"@brightbeamai/chap-coordinator/storage/sqlite\";\n *\n * const coord = new Coordinator({\n * store: new SqliteStore(\"./chap.db\"),\n * });\n *\n * Pass `\":memory:\"` for an in-memory SQLite instance (faster than the\n * MemoryStore for tests that need transactional semantics).\n *\n * better-sqlite3 is declared as an optionalDependency. If the binary\n * cannot be built (rare on Linux/macOS, common on locked-down CI), the\n * SqliteStore import will throw with a clear message and applications\n * can fall back to MemoryStore or another implementation.\n */\n\nimport { createRequire } from \"node:module\";\nimport {\n COLLECTIONS, IDEMPOTENCY_KEYS, StoreConflictError, assembleSnapshot, changeFromRecord,\n type Store, type StoreChange, type WorkspaceRecord,\n} from \"./store.js\";\n\n// better-sqlite3 has no published types in its own package, but the\n// `@types/better-sqlite3` package provides them. We require a structural\n// subset so the file type-checks even if the typings are absent.\ninterface SqliteDatabase {\n exec(sql: string): unknown;\n prepare(sql: string): SqliteStatement;\n pragma(name: string, options?: { simple?: boolean }): unknown;\n transaction<A extends unknown[], R>(fn: (...args: A) => R): SqliteTransaction<A, R>;\n close(): void;\n}\ninterface SqliteStatement {\n run(...params: unknown[]): { changes: number; lastInsertRowid: number | bigint };\n get(...params: unknown[]): unknown;\n all(...params: unknown[]): unknown[];\n}\ninterface SqliteTransaction<A extends unknown[], R = void> {\n (...args: A): R;\n immediate(...args: A): R;\n}\n\nexport interface SqliteStoreOptions {\n /**\n * Enable WAL journal mode for better concurrent-read performance.\n * Default true. Set false for `:memory:` databases or when the\n * filesystem does not support WAL (some network filesystems).\n */\n wal?: boolean;\n}\n\nconst SCHEMA = `\n CREATE TABLE IF NOT EXISTS chap_meta (\n workspace TEXT PRIMARY KEY,\n data TEXT NOT NULL,\n version INTEGER NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS chap_entries (\n workspace TEXT NOT NULL,\n seq INTEGER NOT NULL,\n data TEXT NOT NULL,\n PRIMARY KEY (workspace, seq)\n );\n CREATE TABLE IF NOT EXISTS chap_objects (\n workspace TEXT NOT NULL,\n collection TEXT NOT NULL,\n id TEXT NOT NULL,\n data TEXT NOT NULL,\n PRIMARY KEY (workspace, collection, id)\n );\n`;\n\n/** The table that releases before 0.3.2 wrote, one snapshot per workspace. */\nconst LEGACY_TABLE = \"chap_workspaces\";\n\n/** A read-only view in the old table's place, for scripts that read it. */\nconst LEGACY_VIEW = `CREATE VIEW IF NOT EXISTS ${LEGACY_TABLE} AS SELECT m.workspace AS id, json_set(m.data, ` +\n [\n \"'$.audit', json((SELECT json_group_array(json(e.data)) FROM \" +\n \"(SELECT data FROM chap_entries WHERE workspace = m.workspace ORDER BY seq) AS e))\",\n ...[...COLLECTIONS, IDEMPOTENCY_KEYS].map(c =>\n `'$.${c}', json((SELECT json_group_object(id, json(data)) FROM chap_objects ` +\n `WHERE workspace = m.workspace AND collection = '${c}'))`),\n ].join(\", \") +\n \") AS data, m.version AS version, m.updated_at AS updated_at FROM chap_meta AS m\";\n\nexport class SqliteStore implements Store {\n readonly db: SqliteDatabase;\n private readonly stmt: Record<string, SqliteStatement>;\n private readonly txWrite: SqliteTransaction<[StoreChange]>;\n private readonly txDelete: SqliteTransaction<[string]>;\n\n constructor(path: string, options: SqliteStoreOptions = {}) {\n let Database: new (path: string) => SqliteDatabase;\n try {\n // Resolved at construction so the dependency stays optional: importing\n // this module costs nothing if you never build a SqliteStore.\n //\n // createRequire rather than a bare require(). A bare one is undefined in\n // an ES module, so from source it threw ReferenceError and this catch\n // reported the dependency as missing when it was installed. The bundled\n // build masked it by shimming require, which is why it went unnoticed.\n // The CommonJS build has no import.meta.url, and resolves from\n // __filename, which an ES module does not define.\n const here = typeof __filename === \"string\" ? __filename : import.meta.url;\n Database = createRequire(here)(\"better-sqlite3\");\n } catch (err) {\n const detail = err instanceof Error ? `: ${err.message.split(\"\\n\")[0]}` : \"\";\n throw new Error(\n \"SqliteStore requires the `better-sqlite3` package. \" +\n \"Install it with `npm install better-sqlite3` \" +\n `or use MemoryStore for non-persistent workloads${detail}`,\n );\n }\n\n this.db = new Database(path);\n if (path !== \":memory:\" && options.wal !== false) {\n this.db.pragma(\"journal_mode = WAL\");\n }\n this.db.exec(SCHEMA);\n\n const prepare = (sql: string) => this.db.prepare(sql);\n this.stmt = {\n held: prepare(\"SELECT MAX(seq) AS last FROM chap_entries WHERE workspace = ?\"),\n hasMeta: prepare(\"SELECT 1 FROM chap_meta WHERE workspace = ?\"),\n hasTable: prepare(\"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?\"),\n hasName: prepare(\"SELECT 1 FROM sqlite_master WHERE name = ?\"),\n putEntry: prepare(\"INSERT INTO chap_entries (workspace, seq, data) VALUES (?, ?, ?)\"),\n putObject: prepare(\n \"INSERT INTO chap_objects (workspace, collection, id, data) VALUES (?, ?, ?, ?) \" +\n \"ON CONFLICT(workspace, collection, id) DO UPDATE SET data = excluded.data\"),\n dropObject: prepare(\"DELETE FROM chap_objects WHERE workspace = ? AND collection = ? AND id = ?\"),\n putMeta: prepare(\n \"INSERT INTO chap_meta (workspace, data, version, updated_at) VALUES (?, ?, ?, ?) \" +\n \"ON CONFLICT(workspace) DO UPDATE SET data = excluded.data, \" +\n \"version = excluded.version, updated_at = excluded.updated_at\"),\n dropMeta: prepare(\"DELETE FROM chap_meta WHERE workspace = ?\"),\n dropEntries: prepare(\"DELETE FROM chap_entries WHERE workspace = ?\"),\n dropObjects: prepare(\"DELETE FROM chap_objects WHERE workspace = ?\"),\n metas: prepare(\"SELECT workspace, data, version, updated_at FROM chap_meta\"),\n entries: prepare(\"SELECT workspace, seq, data FROM chap_entries ORDER BY workspace, seq\"),\n objects: prepare(\"SELECT workspace, collection, id, data FROM chap_objects ORDER BY rowid\"),\n };\n this.txWrite = this.db.transaction((change: StoreChange) => this.write(change));\n this.txDelete = this.db.transaction((id: string) => this.drop(id));\n this.open();\n }\n\n /**\n * In one transaction, so processes opening the file at once agree: move the\n * snapshots a release before 0.3.2 wrote into the tables above, then put the\n * view in the old table's place.\n */\n private open(): void {\n let kept = \"\";\n const unreadable: string[] = [];\n this.db.transaction(() => {\n if (this.stmt.hasTable.get(LEGACY_TABLE)) {\n const rows = this.db.prepare(\n `SELECT id, data, version, updated_at FROM ${LEGACY_TABLE}`,\n ).all() as Array<{ id: string; data: string; version: number; updated_at: string }>;\n for (const r of rows) {\n // A workspace already in the new tables was written later; it stays.\n if (this.stmt.hasMeta.get(r.id)) continue;\n let data: unknown;\n try { data = JSON.parse(r.data); } catch { unreadable.push(r.id); continue; }\n const change = changeFromRecord({ id: r.id, data, version: r.version, updated_at: r.updated_at });\n this.write({ ...change, version: change.entries.length });\n }\n kept = `${LEGACY_TABLE}_v1`;\n for (let n = 2; this.stmt.hasName.get(kept); n++) kept = `${LEGACY_TABLE}_v1_${n}`;\n // With legacy_alter_table on, a view or trigger of the application's\n // own that reads the old table keeps reading it by name, so it reads\n // the view below and stays current.\n const legacy = this.db.pragma(\"legacy_alter_table\", { simple: true });\n this.db.pragma(\"legacy_alter_table = ON\");\n try {\n this.db.exec(`ALTER TABLE ${LEGACY_TABLE} RENAME TO ${kept}`);\n } finally {\n this.db.pragma(`legacy_alter_table = ${legacy ? \"ON\" : \"OFF\"}`);\n }\n // The old index moved with the table. Without it, a release before\n // 0.3.2 that opens this file tries to index the view and stops with\n // an error, which is better than starting with nothing loaded.\n this.db.exec(\"DROP INDEX IF EXISTS idx_chap_workspaces_updated_at\");\n }\n if (!this.stmt.hasName.get(LEGACY_TABLE)) this.db.exec(LEGACY_VIEW);\n }).immediate();\n for (const id of unreadable) {\n console.warn(`chap SqliteStore: workspace ${id} could not be read during the upgrade; ` +\n `its row is kept, unchanged, in ${kept}`);\n }\n }\n\n private write(change: StoreChange): void {\n const id = change.id;\n if (change.replace) {\n this.drop(id);\n } else {\n const last = (this.stmt.held.get(id) as { last: number | null } | undefined)?.last;\n const held = last === null || last === undefined ? 0 : last + 1;\n if (held !== change.base) throw new StoreConflictError(id, held, change.base);\n }\n const base = change.replace ? 0 : change.base;\n for (let i = 0; i < change.entries.length; i++) {\n this.stmt.putEntry.run(id, base + i, JSON.stringify(change.entries[i]));\n }\n for (const [collection, items] of Object.entries(change.objects)) {\n for (const [key, value] of Object.entries(items)) {\n if (value === null || value === undefined) this.stmt.dropObject.run(id, collection, key);\n else this.stmt.putObject.run(id, collection, key, JSON.stringify(value));\n }\n }\n this.stmt.putMeta.run(id, JSON.stringify(change.meta), change.version, change.updated_at);\n }\n\n private drop(id: string): void {\n this.stmt.dropMeta.run(id);\n this.stmt.dropEntries.run(id);\n this.stmt.dropObjects.run(id);\n }\n\n load(): WorkspaceRecord[] {\n // One read transaction, so a write by another process in the middle of\n // the read cannot leave the log and the objects out of step.\n return this.db.transaction(() => this.readAll())();\n }\n\n private readAll(): WorkspaceRecord[] {\n // A workspace with any unreadable row is skipped whole, with a warning,\n // so the others still load: restoring part of a workspace would put a\n // log beside objects it does not explain.\n const broken = new Map<string, string>();\n const entries = new Map<string, unknown[]>();\n for (const r of this.stmt.entries.all() as Array<{ workspace: string; seq: number; data: string }>) {\n if (broken.has(r.workspace)) continue;\n let list = entries.get(r.workspace);\n if (!list) entries.set(r.workspace, (list = []));\n if (r.seq !== list.length) { broken.set(r.workspace, `audit entry ${list.length} is missing`); continue; }\n try { list.push(JSON.parse(r.data)); }\n catch { broken.set(r.workspace, `audit entry ${r.seq} is unreadable`); }\n }\n const objects = new Map<string, Array<[string, string, unknown]>>();\n for (const r of this.stmt.objects.all() as Array<{ workspace: string; collection: string; id: string; data: string }>) {\n if (broken.has(r.workspace)) continue;\n let list = objects.get(r.workspace);\n if (!list) objects.set(r.workspace, (list = []));\n try { list.push([r.collection, r.id, JSON.parse(r.data)]); }\n catch { broken.set(r.workspace, `${r.collection} ${r.id} is unreadable`); }\n }\n const out: WorkspaceRecord[] = [];\n for (const r of this.stmt.metas.all() as Array<{ workspace: string; data: string; version: number; updated_at: string }>) {\n let meta: Record<string, unknown> | undefined;\n if (!broken.has(r.workspace)) {\n try { meta = JSON.parse(r.data) as Record<string, unknown>; }\n catch { broken.set(r.workspace, \"its own fields are unreadable\"); }\n }\n if (!meta || typeof meta !== \"object\") {\n console.warn(`chap SqliteStore: workspace ${r.workspace} was not loaded: ` +\n `${broken.get(r.workspace) ?? \"its own fields are unreadable\"}`);\n continue;\n }\n const data = assembleSnapshot(meta, entries.get(r.workspace) ?? [], objects.get(r.workspace) ?? []);\n data.id = r.workspace;\n out.push({ id: r.workspace, data, version: r.version, updated_at: r.updated_at });\n }\n return out;\n }\n\n save(record: WorkspaceRecord): void {\n this.txWrite.immediate(changeFromRecord(record));\n }\n\n apply(change: StoreChange): void {\n this.txWrite.immediate(change);\n }\n\n delete(id: string): void {\n this.txDelete.immediate(id);\n }\n\n close(): void {\n this.db.close();\n }\n}\n","/**\n * Pluggable persistence for the Coordinator.\n *\n * A Store keeps workspace state between process restarts. The Coordinator\n * calls `store.load()` once at start, and writes after every call that adds\n * to a workspace's audit log.\n *\n * How the Coordinator writes depends on what the store implements:\n *\n * - `apply(change)`: the Coordinator sends what the call changed: the new\n * audit entries, the objects it touched and the workspace's own fields.\n * The cost of a write stays flat as the log grows; it grows with the\n * objects a call changes, such as a task with a long history. MemoryStore\n * and SqliteStore implement it.\n * - `save(record)` alone: the Coordinator sends the whole workspace on every\n * write, as releases before 0.3.2 did, so the cost of a write grows with\n * the log. Stores written for earlier releases keep working this way.\n *\n * MemoryStore is the default and keeps nothing across processes. SqliteStore\n * keeps state in a SQLite file through better-sqlite3. Third-party stores\n * (Postgres, Redis and so on) implement this interface.\n *\n * Concurrency model\n * -----------------\n *\n * A Coordinator is a **single-writer** component: dispatch is serial,\n * workspace state is held in memory, and each chain link is computed from the\n * head the instance holds. Run one Coordinator per workspace.\n *\n * Every change names the number of audit entries the Coordinator expects the\n * store to hold already (`base`). MemoryStore and SqliteStore refuse a change\n * whose base differs from what they hold: they throw a StoreConflictError and\n * write nothing. A second Coordinator writing the same workspace is refused\n * and reported, and the entries already stored stay as they are. The refused\n * instance's memory no longer matches the store; restart it to reload.\n *\n * `save` makes no such check: it replaces what the store holds for the\n * workspace. A store that implements only `save` therefore still loses\n * entries silently when two instances write one workspace.\n *\n * To run more than one instance, partition workspaces so that no workspace is\n * ever written by two, or hold an external lock. See SPECIFICATION.md 10.3.\n *\n * Production deployments that need rich queries over the log can project it\n * into their own analytics store with the `onAudit` listener.\n */\n\n/** The workspace maps a store keeps object by object. */\nexport const COLLECTIONS = [\n \"members\", \"tasks\", \"overrides\", \"whispers\",\n \"deliberations\", \"handoffs\", \"snapshots\", \"route_decisions\",\n] as const;\n\n/** task.create idempotency keys, kept as one more collection of key to task id. */\nexport const IDEMPOTENCY_KEYS = \"idempotency_keys\";\n\nexport interface WorkspaceRecord {\n /** Workspace identifier. */\n id: string;\n /** JSON-serialisable payload in the shape `Coordinator.snapshot()` produces. */\n data: unknown;\n /**\n * For `save`, a counter the Coordinator increments on each write. For a\n * record a store loads after `apply`, the number of audit entries. Recorded\n * for diagnostics; `save` does not check it.\n */\n version: number;\n /** Wall-clock timestamp of the last write, ISO-8601. */\n updated_at: string;\n}\n\n/** One write to one workspace: what one or more calls changed. */\nexport interface StoreChange {\n /** Workspace identifier. */\n id: string;\n /**\n * Audit entries the store must already hold for this workspace. A store\n * that holds a different number refuses the change with a\n * StoreConflictError and writes nothing. Not checked when `replace` is set.\n */\n base: number;\n /** Audit entries the store holds after the change: `base + entries.length`. */\n version: number;\n /** Wall-clock timestamp of the write, ISO-8601. */\n updated_at: string;\n /** The workspace's own fields: everything except its collections and log. */\n meta: Record<string, unknown>;\n /** New audit entries in order. The first has seq `base`. */\n entries: unknown[];\n /**\n * Objects written, by collection and then by key. A null value removes the\n * object. The collections are those in COLLECTIONS and IDEMPOTENCY_KEYS.\n */\n objects: Record<string, Record<string, unknown>>;\n /**\n * Replace everything the store holds for this workspace with this change,\n * which then carries the whole workspace from seq 0.\n */\n replace?: boolean;\n}\n\n/**\n * A change whose base does not match the audit entries the store holds.\n * Usually a second Coordinator writing the same workspace.\n */\nexport class StoreConflictError extends Error {\n readonly workspace: string;\n /** Audit entries the store holds. */\n readonly held: number;\n /** Audit entries the change expected. */\n readonly base: number;\n\n constructor(workspace: string, held: number, base: number) {\n super(\n `The store holds ${held} audit entries for workspace ${workspace} and ` +\n `this write expected ${base}, so nothing was written. Another ` +\n `coordinator may be writing to this workspace, the store may have been ` +\n `changed outside this coordinator, or this coordinator could not load ` +\n `the workspace when it started. Restart this coordinator to reload.`,\n );\n this.name = \"StoreConflictError\";\n this.workspace = workspace;\n this.held = held;\n this.base = base;\n }\n\n /**\n * Matched by name, so `instanceof` holds across the package's entry points,\n * each of which is bundled with its own copy of this class.\n */\n static [Symbol.hasInstance](value: unknown): boolean {\n return value instanceof Error && value.name === \"StoreConflictError\";\n }\n}\n\nexport interface Store {\n /** Load all workspace records. Called once when the Coordinator starts. */\n load(): Promise<WorkspaceRecord[]> | WorkspaceRecord[];\n\n /**\n * Replace what the store holds for one workspace. The Coordinator calls it\n * after every write when the store has no `apply`.\n */\n save(record: WorkspaceRecord): Promise<void> | void;\n\n /**\n * Write one change. Optional; when present the Coordinator calls it in\n * place of `save`. It must be atomic: the whole change is written, or\n * nothing is. The change refers to the Coordinator's live objects, so\n * serialise or copy everything before returning, including in an async\n * store. Refuse a change whose `base` differs from the entries held, unless\n * `replace` is set, by throwing (or rejecting with) a StoreConflictError.\n */\n apply?(change: StoreChange): Promise<void> | void;\n\n /** Remove one workspace. */\n delete(id: string): Promise<void> | void;\n\n /** Release resources. Idempotent. */\n close?(): Promise<void> | void;\n}\n\nfunction isCollection(name: string): boolean {\n return (COLLECTIONS as readonly string[]).includes(name);\n}\n\n/** The key an object is stored under in its collection. */\nexport function objectKey(collection: string, item: unknown): string | undefined {\n if (item === null || typeof item !== \"object\") return undefined;\n const o = item as Record<string, unknown>;\n const key = collection === \"members\" ? (o.uri ?? o.id) : (o.id ?? o.uri);\n return typeof key === \"string\" ? key : undefined;\n}\n\n/**\n * A collection as a snapshot holds it, keyed. The TypeScript snapshot holds\n * arrays and the Python snapshot holds objects keyed by id; both are read.\n */\nfunction keyed(collection: string, value: unknown): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n if (Array.isArray(value)) {\n for (const item of value) {\n const key = objectKey(collection, item);\n if (key !== undefined) out[key] = item;\n }\n } else if (value !== null && typeof value === \"object\") {\n Object.assign(out, value);\n }\n return out;\n}\n\n/** A whole-workspace snapshot as a change that replaces what the store holds. */\nexport function changeFromRecord(record: WorkspaceRecord): StoreChange {\n const data = (record.data !== null && typeof record.data === \"object\"\n ? record.data : {}) as Record<string, unknown>;\n const meta: Record<string, unknown> = {};\n const objects: Record<string, Record<string, unknown>> = {};\n let entries: unknown[] = [];\n for (const [name, value] of Object.entries(data)) {\n if (name === \"audit\") entries = Array.isArray(value) ? value : [];\n else if (name === IDEMPOTENCY_KEYS || isCollection(name)) objects[name] = keyed(name, value);\n else meta[name] = value;\n }\n if (meta.id === undefined) meta.id = record.id;\n return {\n id: record.id, base: 0, version: record.version, updated_at: record.updated_at,\n meta, entries, objects, replace: true,\n };\n}\n\n/**\n * Rows read back from a store, assembled into the snapshot shape\n * `Coordinator.restore()` reads: arrays for the collections, an object for\n * the idempotency keys, and the log in seq order.\n */\nexport function assembleSnapshot(\n meta: Record<string, unknown>,\n entries: unknown[],\n objects: Iterable<[collection: string, key: string, value: unknown]>,\n): Record<string, unknown> {\n const data: Record<string, unknown> = { ...meta };\n for (const name of COLLECTIONS) data[name] = [];\n const keys: Record<string, unknown> = {};\n data[IDEMPOTENCY_KEYS] = keys;\n for (const [collection, key, value] of objects) {\n if (collection === IDEMPOTENCY_KEYS) {\n keys[key] = value;\n } else {\n const list = (Array.isArray(data[collection]) ? data[collection] : (data[collection] = [])) as unknown[];\n list.push(value);\n }\n }\n data.audit = entries;\n return data;\n}\n\ninterface Held {\n meta: string;\n version: number;\n updated_at: string;\n entries: string[];\n objects: Map<string, Map<string, string>>;\n}\n\n/**\n * In-memory store. The default: it keeps nothing across processes. Like a\n * database it keeps each row as JSON text, so a Coordinator restarted on it\n * sees what was written and nothing changed later.\n */\nexport class MemoryStore implements Store {\n private held = new Map<string, Held>();\n\n load(): WorkspaceRecord[] {\n const out: WorkspaceRecord[] = [];\n for (const [id, h] of this.held) {\n const objects: Array<[string, string, unknown]> = [];\n for (const [collection, items] of h.objects) {\n for (const [key, value] of items) objects.push([collection, key, JSON.parse(value)]);\n }\n out.push({\n id,\n data: assembleSnapshot(JSON.parse(h.meta), h.entries.map(e => JSON.parse(e)), objects),\n version: h.version,\n updated_at: h.updated_at,\n });\n }\n return out;\n }\n\n save(record: WorkspaceRecord): void {\n this.apply(changeFromRecord(record));\n }\n\n apply(change: StoreChange): void {\n // Serialised before anything is held, so a change that cannot be written\n // leaves the store as it was.\n const meta = JSON.stringify(change.meta);\n const entries = change.entries.map(e => JSON.stringify(e));\n const objects = Object.entries(change.objects).map(([collection, items]) =>\n [collection, Object.entries(items).map(([key, value]) =>\n [key, value === null || value === undefined ? null : JSON.stringify(value)] as const)] as const);\n let h = this.held.get(change.id);\n if (!change.replace) {\n const holds = h ? h.entries.length : 0;\n if (holds !== change.base) throw new StoreConflictError(change.id, holds, change.base);\n }\n if (!h || change.replace) {\n h = { meta: \"{}\", version: 0, updated_at: \"\", entries: [], objects: new Map() };\n }\n for (const entry of entries) h.entries.push(entry);\n for (const [collection, items] of objects) {\n let held = h.objects.get(collection);\n if (!held) h.objects.set(collection, (held = new Map()));\n for (const [key, value] of items) {\n if (value === null) held.delete(key);\n else held.set(key, value);\n }\n }\n h.meta = meta;\n h.version = change.version;\n h.updated_at = change.updated_at;\n this.held.set(change.id, h);\n }\n\n delete(id: string): void {\n this.held.delete(id);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/storage/sqlite.ts","../../src/storage/store.ts"],"sourcesContent":["/**\n * SQLite-backed store, through better-sqlite3 where it is installed and\n * through Node's built-in node:sqlite (Node 22.13 and later) where it is not.\n *\n * Schema, shared with the Python SqliteStore. Both write these tables, and\n * the same audit entries for the same calls. Each writes the objects in its\n * own shape and restores either shape, so each restores a file the other\n * wrote, and chap-analytics reads a file from either:\n *\n * CREATE TABLE chap_meta (\n * workspace TEXT PRIMARY KEY,\n * data TEXT NOT NULL, -- the workspace's own fields, JSON\n * version INTEGER NOT NULL, -- audit entries held\n * updated_at TEXT NOT NULL -- ISO-8601\n * );\n * CREATE TABLE chap_entries (\n * workspace TEXT NOT NULL,\n * seq INTEGER NOT NULL,\n * data TEXT NOT NULL, -- one audit entry, JSON\n * PRIMARY KEY (workspace, seq)\n * );\n * CREATE TABLE chap_objects (\n * workspace TEXT NOT NULL,\n * collection TEXT NOT NULL, -- members, tasks, ..., idempotency_keys\n * id TEXT NOT NULL,\n * data TEXT NOT NULL, -- one object, JSON\n * PRIMARY KEY (workspace, collection, id)\n * );\n *\n * A write adds the call's entries, updates the objects the call changed and\n * the workspace's own fields, in one transaction, so its cost does not grow\n * with the log. A write whose first entry is not the next seq the file holds\n * is refused with a StoreConflictError; see the concurrency model in\n * ./store.ts. Audit entries are only ever inserted, except when a workspace\n * is written whole: after `Coordinator.restore()`, and for a workspace an\n * application put in place itself.\n *\n * Files written by releases before 0.3.2 hold one JSON snapshot per workspace\n * in a table named chap_workspaces. Opening such a file moves each workspace\n * into the tables above, in one transaction, and renames the old table to\n * chap_workspaces_v1, which is kept as it was.\n *\n * A view named chap_workspaces takes the old table's place, with the same\n * columns, so a script that read the old table reads the current state; it\n * assembles each workspace when read, with collections keyed by id. Releases\n * before 0.3.2 refuse to open the file, since they cannot write to it.\n *\n * Usage:\n * import { Coordinator } from \"@brightbeamai/chap-coordinator\";\n * import { SqliteStore } from \"@brightbeamai/chap-coordinator/storage/sqlite\";\n *\n * const coord = new Coordinator({\n * store: new SqliteStore(\"./chap.db\"),\n * });\n *\n * Pass `\":memory:\"` for an in-memory SQLite instance (faster than the\n * MemoryStore for tests that need transactional semantics).\n *\n * better-sqlite3 is declared as an optionalDependency. Where its binary\n * cannot be built (rare on Linux/macOS, common on locked-down CI), the store\n * opens the file through node:sqlite, which Node 22.13 and later ship\n * without a flag; both\n * write the same file. With neither, constructing a SqliteStore throws with\n * a clear message, and applications can fall back to MemoryStore.\n */\n\nimport { createRequire } from \"node:module\";\nimport {\n COLLECTIONS, IDEMPOTENCY_KEYS, StoreConflictError, assembleSnapshot, changeFromRecord,\n type Store, type StoreChange, type WorkspaceRecord,\n} from \"./store.js\";\n\n// better-sqlite3 has no published types in its own package, but the\n// `@types/better-sqlite3` package provides them. We require a structural\n// subset so the file type-checks even if the typings are absent.\ninterface SqliteDatabase {\n exec(sql: string): unknown;\n prepare(sql: string): SqliteStatement;\n pragma(name: string, options?: { simple?: boolean }): unknown;\n transaction<A extends unknown[], R>(fn: (...args: A) => R): SqliteTransaction<A, R>;\n close(): void;\n}\ninterface SqliteStatement {\n run(...params: unknown[]): { changes: number; lastInsertRowid: number | bigint };\n get(...params: unknown[]): unknown;\n all(...params: unknown[]): unknown[];\n}\ninterface SqliteTransaction<A extends unknown[], R = void> {\n (...args: A): R;\n immediate(...args: A): R;\n}\n\nexport interface SqliteStoreOptions {\n /**\n * Enable WAL journal mode for better concurrent-read performance.\n * Default true. Set false for `:memory:` databases or when the\n * filesystem does not support WAL (some network filesystems).\n */\n wal?: boolean;\n /**\n * The SQLite binding: better-sqlite3, Node's node:sqlite, or, by default,\n * better-sqlite3 where it is installed and node:sqlite where it is not.\n */\n driver?: \"better-sqlite3\" | \"node:sqlite\";\n}\n\n/** node:sqlite's DatabaseSync, the part of it this store uses. */\ninterface NodeSqliteDatabase {\n exec(sql: string): void;\n prepare(sql: string): SqliteStatement;\n close(): void;\n}\n\n/**\n * A node:sqlite database with the better-sqlite3 calls this store makes:\n * `pragma` and `transaction` are written over `prepare` and `exec`.\n */\nfunction fromNodeSqlite(db: NodeSqliteDatabase): SqliteDatabase {\n const transaction = <A extends unknown[], R>(fn: (...args: A) => R): SqliteTransaction<A, R> => {\n const run = (begin: string) => (...args: A): R => {\n db.exec(begin);\n try {\n const result = fn(...args);\n db.exec(\"COMMIT\");\n return result;\n } catch (err) {\n db.exec(\"ROLLBACK\");\n throw err;\n }\n };\n return Object.assign(run(\"BEGIN\"), { immediate: run(\"BEGIN IMMEDIATE\") });\n };\n return {\n exec: (sql) => db.exec(sql),\n prepare: (sql) => db.prepare(sql),\n pragma: (name, options) => {\n const row = db.prepare(`PRAGMA ${name}`).get() as Record<string, unknown> | undefined;\n return options?.simple ? (row ? Object.values(row)[0] : undefined) : row;\n },\n transaction,\n close: () => db.close(),\n };\n}\n\n/** Open a SQLite file through the binding the options name, or the first installed. */\nfunction openDatabase(path: string, driver: SqliteStoreOptions[\"driver\"]): SqliteDatabase {\n // A binding that loads opens the file, and an error opening it is the\n // file's, reported as it is; only a binding that does not load is passed by.\n const failures: string[] = [];\n const load = <T>(name: string): T | undefined => {\n try {\n // Resolved at construction so the dependency stays optional: importing\n // this module costs nothing if you never build a SqliteStore. An ES\n // module has no require function of its own, so one is made from\n // import.meta.url, and the CommonJS build, which has no\n // import.meta.url, makes it from __filename.\n return createRequire(typeof __filename === \"string\" ? __filename : import.meta.url)(name) as T;\n } catch (err) {\n failures.push(`${name}: ${err instanceof Error ? err.message.split(\"\\n\")[0] : String(err)}`);\n return undefined;\n }\n };\n if (driver !== \"node:sqlite\") {\n const Database = load<new (path: string) => SqliteDatabase>(\"better-sqlite3\");\n if (Database) return new Database(path);\n }\n if (driver !== \"better-sqlite3\") {\n const sqlite = load<{ DatabaseSync: new (path: string) => NodeSqliteDatabase }>(\"node:sqlite\");\n if (sqlite) return fromNodeSqlite(new sqlite.DatabaseSync(path));\n }\n throw new Error(\n \"SqliteStore needs the `better-sqlite3` package, or Node 22.13 or later for node:sqlite. \" +\n \"Install it with `npm install better-sqlite3` \" +\n `or use MemoryStore for non-persistent workloads (${failures.join(\"; \")})`,\n );\n}\n\nconst SCHEMA = `\n CREATE TABLE IF NOT EXISTS chap_meta (\n workspace TEXT PRIMARY KEY,\n data TEXT NOT NULL,\n version INTEGER NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS chap_entries (\n workspace TEXT NOT NULL,\n seq INTEGER NOT NULL,\n data TEXT NOT NULL,\n PRIMARY KEY (workspace, seq)\n );\n CREATE TABLE IF NOT EXISTS chap_objects (\n workspace TEXT NOT NULL,\n collection TEXT NOT NULL,\n id TEXT NOT NULL,\n data TEXT NOT NULL,\n PRIMARY KEY (workspace, collection, id)\n );\n`;\n\n/** The table that releases before 0.3.2 wrote, one snapshot per workspace. */\nconst LEGACY_TABLE = \"chap_workspaces\";\n\n/** A read-only view in the old table's place, for scripts that read it. */\nconst LEGACY_VIEW = `CREATE VIEW IF NOT EXISTS ${LEGACY_TABLE} AS SELECT m.workspace AS id, json_set(m.data, ` +\n [\n \"'$.audit', json((SELECT json_group_array(json(e.data)) FROM \" +\n \"(SELECT data FROM chap_entries WHERE workspace = m.workspace ORDER BY seq) AS e))\",\n ...[...COLLECTIONS, IDEMPOTENCY_KEYS].map(c =>\n `'$.${c}', json((SELECT json_group_object(id, json(data)) FROM chap_objects ` +\n `WHERE workspace = m.workspace AND collection = '${c}'))`),\n ].join(\", \") +\n \") AS data, m.version AS version, m.updated_at AS updated_at FROM chap_meta AS m\";\n\n/** The view's definition as SQLite records it, which leaves out IF NOT EXISTS. */\nconst LEGACY_VIEW_RECORDED = LEGACY_VIEW.replace(\"CREATE VIEW IF NOT EXISTS \", \"CREATE VIEW \");\n\nexport class SqliteStore implements Store {\n readonly db: SqliteDatabase;\n private readonly stmt: Record<string, SqliteStatement>;\n private readonly txWrite: SqliteTransaction<[StoreChange]>;\n private readonly txDelete: SqliteTransaction<[string]>;\n\n constructor(path: string, options: SqliteStoreOptions = {}) {\n this.db = openDatabase(path, options.driver);\n // Wait up to five seconds for another process's lock, as better-sqlite3\n // and Python's sqlite3 do by default; node:sqlite waits for none. It is\n // set first, so the WAL switch below waits too when two processes open\n // a new file together.\n this.db.pragma(\"busy_timeout = 5000\");\n if (path !== \":memory:\" && options.wal !== false) {\n this.db.pragma(\"journal_mode = WAL\");\n }\n this.db.exec(SCHEMA);\n\n const prepare = (sql: string) => this.db.prepare(sql);\n this.stmt = {\n held: prepare(\"SELECT MAX(seq) AS last FROM chap_entries WHERE workspace = ?\"),\n hasMeta: prepare(\"SELECT 1 FROM chap_meta WHERE workspace = ?\"),\n hasTable: prepare(\"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?\"),\n hasName: prepare(\"SELECT 1 FROM sqlite_master WHERE name = ?\"),\n putEntry: prepare(\"INSERT INTO chap_entries (workspace, seq, data) VALUES (?, ?, ?)\"),\n putObject: prepare(\n \"INSERT INTO chap_objects (workspace, collection, id, data) VALUES (?, ?, ?, ?) \" +\n \"ON CONFLICT(workspace, collection, id) DO UPDATE SET data = excluded.data\"),\n dropObject: prepare(\"DELETE FROM chap_objects WHERE workspace = ? AND collection = ? AND id = ?\"),\n putMeta: prepare(\n \"INSERT INTO chap_meta (workspace, data, version, updated_at) VALUES (?, ?, ?, ?) \" +\n \"ON CONFLICT(workspace) DO UPDATE SET data = excluded.data, \" +\n \"version = excluded.version, updated_at = excluded.updated_at\"),\n dropMeta: prepare(\"DELETE FROM chap_meta WHERE workspace = ?\"),\n dropEntries: prepare(\"DELETE FROM chap_entries WHERE workspace = ?\"),\n dropObjects: prepare(\"DELETE FROM chap_objects WHERE workspace = ?\"),\n metas: prepare(\"SELECT workspace, data, version, updated_at FROM chap_meta\"),\n entries: prepare(\"SELECT workspace, seq, data FROM chap_entries ORDER BY workspace, seq\"),\n objects: prepare(\"SELECT workspace, collection, id, data FROM chap_objects ORDER BY rowid\"),\n metaOf: prepare(\"SELECT workspace, data, version, updated_at FROM chap_meta WHERE workspace = ?\"),\n entriesOf: prepare(\"SELECT workspace, seq, data FROM chap_entries WHERE workspace = ? ORDER BY seq\"),\n objectsOf: prepare(\"SELECT workspace, collection, id, data FROM chap_objects WHERE workspace = ? ORDER BY rowid\"),\n };\n this.txWrite = this.db.transaction((change: StoreChange) => this.write(change));\n this.txDelete = this.db.transaction((id: string) => this.drop(id));\n this.open();\n }\n\n /**\n * In one transaction, so processes opening the file at once agree: move the\n * snapshots a release before 0.3.2 wrote into the tables above, then put the\n * view in the old table's place.\n */\n private open(): void {\n let kept = \"\";\n const unreadable: string[] = [];\n this.db.transaction(() => {\n if (this.stmt.hasTable.get(LEGACY_TABLE)) {\n const rows = this.db.prepare(\n `SELECT id, data, version, updated_at FROM ${LEGACY_TABLE}`,\n ).all() as Array<{ id: string; data: string; version: number; updated_at: string }>;\n for (const r of rows) {\n // A workspace already in the new tables was written later; it stays.\n if (this.stmt.hasMeta.get(r.id)) continue;\n let data: unknown;\n try { data = JSON.parse(r.data); } catch { unreadable.push(r.id); continue; }\n const change = changeFromRecord({ id: r.id, data, version: r.version, updated_at: r.updated_at });\n this.write({ ...change, version: change.entries.length });\n }\n kept = `${LEGACY_TABLE}_v1`;\n for (let n = 2; this.stmt.hasName.get(kept); n++) kept = `${LEGACY_TABLE}_v1_${n}`;\n // With legacy_alter_table on, a view or trigger of the application's\n // own that reads the old table keeps reading it by name, so it reads\n // the view below and stays current.\n const legacy = this.db.pragma(\"legacy_alter_table\", { simple: true });\n this.db.pragma(\"legacy_alter_table = ON\");\n try {\n this.db.exec(`ALTER TABLE ${LEGACY_TABLE} RENAME TO ${kept}`);\n } finally {\n this.db.pragma(`legacy_alter_table = ${legacy ? \"ON\" : \"OFF\"}`);\n }\n // The old index moved with the table. Without it, a release before\n // 0.3.2 that opens this file tries to index the view and stops with\n // an error, which is better than starting with nothing loaded.\n this.db.exec(\"DROP INDEX IF EXISTS idx_chap_workspaces_updated_at\");\n }\n // The view assembles every collection. One a release with fewer\n // collections made is replaced, so a script reading it sees them all.\n const view = this.db.prepare(\"SELECT type, sql FROM sqlite_master WHERE name = ?\")\n .get(LEGACY_TABLE) as { type: string; sql: string } | undefined;\n if (view?.type === \"view\" && view.sql !== LEGACY_VIEW_RECORDED) this.db.exec(`DROP VIEW ${LEGACY_TABLE}`);\n if (!this.stmt.hasName.get(LEGACY_TABLE)) this.db.exec(LEGACY_VIEW);\n }).immediate();\n for (const id of unreadable) {\n console.warn(`chap SqliteStore: workspace ${id} could not be read during the upgrade; ` +\n `its row is kept, unchanged, in ${kept}`);\n }\n }\n\n private write(change: StoreChange): void {\n const id = change.id;\n if (change.replace) {\n this.drop(id);\n } else {\n const last = (this.stmt.held.get(id) as { last: number | null } | undefined)?.last;\n const held = last === null || last === undefined ? 0 : last + 1;\n if (held !== change.base) throw new StoreConflictError(id, held, change.base);\n }\n const base = change.replace ? 0 : change.base;\n for (let i = 0; i < change.entries.length; i++) {\n this.stmt.putEntry.run(id, base + i, JSON.stringify(change.entries[i]));\n }\n for (const [collection, items] of Object.entries(change.objects)) {\n for (const [key, value] of Object.entries(items)) {\n if (value === null || value === undefined) this.stmt.dropObject.run(id, collection, key);\n else this.stmt.putObject.run(id, collection, key, JSON.stringify(value));\n }\n }\n this.stmt.putMeta.run(id, JSON.stringify(change.meta), change.version, change.updated_at);\n }\n\n private drop(id: string): void {\n this.stmt.dropMeta.run(id);\n this.stmt.dropEntries.run(id);\n this.stmt.dropObjects.run(id);\n }\n\n load(): WorkspaceRecord[] {\n // One read transaction, so a write by another process in the middle of\n // the read cannot leave the log and the objects out of step.\n return this.db.transaction(() => this.read(\n this.stmt.metas.all(), this.stmt.entries.all(), this.stmt.objects.all()))();\n }\n\n loadOne(id: string): WorkspaceRecord | undefined {\n return this.db.transaction(() => this.read(\n this.stmt.metaOf.all(id), this.stmt.entriesOf.all(id), this.stmt.objectsOf.all(id)))()[0];\n }\n\n version(id: string): number | undefined {\n if (!this.stmt.hasMeta.get(id)) return undefined;\n const last = (this.stmt.held.get(id) as { last: number | null } | undefined)?.last;\n return last === null || last === undefined ? 0 : last + 1;\n }\n\n private read(metas: unknown[], entryRows: unknown[], objectRows: unknown[]): WorkspaceRecord[] {\n // A workspace with any unreadable row is skipped whole, with a warning,\n // so the others still load: restoring part of a workspace would put a\n // log beside objects it does not explain.\n const broken = new Map<string, string>();\n const entries = new Map<string, unknown[]>();\n for (const r of entryRows as Array<{ workspace: string; seq: number; data: string }>) {\n if (broken.has(r.workspace)) continue;\n let list = entries.get(r.workspace);\n if (!list) entries.set(r.workspace, (list = []));\n if (r.seq !== list.length) { broken.set(r.workspace, `audit entry ${list.length} is missing`); continue; }\n try { list.push(JSON.parse(r.data)); }\n catch { broken.set(r.workspace, `audit entry ${r.seq} is unreadable`); }\n }\n const objects = new Map<string, Array<[string, string, unknown]>>();\n for (const r of objectRows as Array<{ workspace: string; collection: string; id: string; data: string }>) {\n if (broken.has(r.workspace)) continue;\n let list = objects.get(r.workspace);\n if (!list) objects.set(r.workspace, (list = []));\n try { list.push([r.collection, r.id, JSON.parse(r.data)]); }\n catch { broken.set(r.workspace, `${r.collection} ${r.id} is unreadable`); }\n }\n const out: WorkspaceRecord[] = [];\n for (const r of metas as Array<{ workspace: string; data: string; version: number; updated_at: string }>) {\n let meta: Record<string, unknown> | undefined;\n if (!broken.has(r.workspace)) {\n try { meta = JSON.parse(r.data) as Record<string, unknown>; }\n catch { broken.set(r.workspace, \"its own fields are unreadable\"); }\n }\n if (!meta || typeof meta !== \"object\") {\n console.warn(`chap SqliteStore: workspace ${r.workspace} was not loaded: ` +\n `${broken.get(r.workspace) ?? \"its own fields are unreadable\"}`);\n continue;\n }\n const data = assembleSnapshot(meta, entries.get(r.workspace) ?? [], objects.get(r.workspace) ?? []);\n data.id = r.workspace;\n out.push({ id: r.workspace, data, version: r.version, updated_at: r.updated_at });\n }\n return out;\n }\n\n save(record: WorkspaceRecord): void {\n this.txWrite.immediate(changeFromRecord(record));\n }\n\n apply(change: StoreChange): void {\n this.txWrite.immediate(change);\n }\n\n delete(id: string): void {\n this.txDelete.immediate(id);\n }\n\n close(): void {\n this.db.close();\n }\n}\n","/**\n * Pluggable persistence for the Coordinator.\n *\n * A Store keeps workspace state between process restarts. The Coordinator\n * calls `store.load()` once at start, and writes after every call that adds\n * to a workspace's audit log.\n *\n * How the Coordinator writes depends on what the store implements:\n *\n * - `apply(change)`: the Coordinator sends what the call changed: the new\n * audit entries, the objects it touched and the workspace's own fields.\n * The cost of a write stays flat as the log grows; it grows with the\n * objects a call changes, such as a task with a long history. MemoryStore\n * and SqliteStore implement it.\n * - `save(record)` alone: the Coordinator sends the whole workspace on every\n * write, as releases before 0.3.2 did, so the cost of a write grows with\n * the log. Stores written for earlier releases keep working this way.\n *\n * MemoryStore is the default and keeps nothing across processes. SqliteStore\n * keeps state in a SQLite file through better-sqlite3. Third-party stores\n * (Postgres, Redis and so on) implement this interface.\n *\n * Concurrency model\n * -----------------\n *\n * A Coordinator is a **single-writer** component: dispatch is serial,\n * workspace state is held in memory, and each chain link is computed from the\n * head the instance holds. Run one Coordinator per workspace.\n *\n * Every change names the number of audit entries the Coordinator expects the\n * store to hold already (`base`). MemoryStore and SqliteStore refuse a change\n * whose base differs from what they hold: they throw a StoreConflictError and\n * write nothing. A second Coordinator writing the same workspace is refused,\n * and the entries already stored stay as they are.\n *\n * A store that also implements `version` and `loadOne` lets a Coordinator\n * keep up with another writer: before each call it compares the entries the\n * store holds for the call's workspace with the entries it last wrote, and\n * reloads the workspace when they differ. A synchronous write that fails or\n * conflicts answers its call with -32603 and `data: {reason: \"store\"}`; the\n * Coordinator then reloads the workspace, and refuses calls on it the same\n * way until a reload succeeds. An asynchronous write that fails is reported,\n * and the next write carries its change again.\n *\n * `save` makes no such check: it replaces what the store holds for the\n * workspace. A store that implements only `save` therefore still loses\n * entries silently when two instances write one workspace.\n *\n * To run more than one instance on a store without `version`, partition\n * workspaces so that no workspace is ever written by two, or hold an\n * external lock. See SPECIFICATION.md 10.3.\n *\n * Production deployments that need rich queries over the log can project it\n * into their own analytics store with the `onAudit` listener.\n */\n\n/** The workspace maps a store keeps object by object. */\nexport const COLLECTIONS = [\n \"members\", \"tasks\", \"overrides\", \"whispers\",\n \"deliberations\", \"handoffs\", \"snapshots\", \"route_decisions\", \"invitations\",\n] as const;\n\n/** task.create idempotency keys, kept as one more collection of key to task id. */\nexport const IDEMPOTENCY_KEYS = \"idempotency_keys\";\n\nexport interface WorkspaceRecord {\n /** Workspace identifier. */\n id: string;\n /** JSON-serialisable payload in the shape `Coordinator.snapshot()` produces. */\n data: unknown;\n /**\n * For `save`, a counter the Coordinator increments on each write. For a\n * record a store loads after `apply`, the number of audit entries. Recorded\n * for diagnostics; `save` does not check it.\n */\n version: number;\n /** Wall-clock timestamp of the last write, ISO-8601. */\n updated_at: string;\n}\n\n/** One write to one workspace: what one or more calls changed. */\nexport interface StoreChange {\n /** Workspace identifier. */\n id: string;\n /**\n * Audit entries the store must already hold for this workspace. A store\n * that holds a different number refuses the change with a\n * StoreConflictError and writes nothing. Not checked when `replace` is set.\n */\n base: number;\n /** Audit entries the store holds after the change: `base + entries.length`. */\n version: number;\n /** Wall-clock timestamp of the write, ISO-8601. */\n updated_at: string;\n /** The workspace's own fields: everything except its collections and log. */\n meta: Record<string, unknown>;\n /** New audit entries in order. The first has seq `base`. */\n entries: unknown[];\n /**\n * Objects written, by collection and then by key. A null value removes the\n * object. The collections are those in COLLECTIONS and IDEMPOTENCY_KEYS.\n */\n objects: Record<string, Record<string, unknown>>;\n /**\n * Replace everything the store holds for this workspace with this change,\n * which then carries the whole workspace from seq 0.\n */\n replace?: boolean;\n}\n\n/**\n * A change whose base does not match the audit entries the store holds.\n * Usually a second Coordinator writing the same workspace.\n */\nexport class StoreConflictError extends Error {\n readonly workspace: string;\n /** Audit entries the store holds. */\n readonly held: number;\n /** Audit entries the change expected. */\n readonly base: number;\n\n constructor(workspace: string, held: number, base: number) {\n super(\n `The store holds ${held} audit entries for workspace ${workspace} and ` +\n `this write expected ${base}, so nothing was written. Another ` +\n `coordinator may be writing to this workspace, or the store may have ` +\n `been changed outside this coordinator. A coordinator reloads the ` +\n `workspace and answers the call as failed, and the call may be sent ` +\n `again.`,\n );\n this.name = \"StoreConflictError\";\n this.workspace = workspace;\n this.held = held;\n this.base = base;\n }\n\n /**\n * Matched by name, so `instanceof` holds across the package's entry points,\n * each of which is bundled with its own copy of this class.\n */\n static [Symbol.hasInstance](value: unknown): boolean {\n return value instanceof Error && value.name === \"StoreConflictError\";\n }\n}\n\nexport interface Store {\n /** Load all workspace records. Called once when the Coordinator starts. */\n load(): Promise<WorkspaceRecord[]> | WorkspaceRecord[];\n\n /**\n * Replace what the store holds for one workspace. The Coordinator calls it\n * after every write when the store has no `apply`.\n */\n save(record: WorkspaceRecord): Promise<void> | void;\n\n /**\n * Write one change. Optional; when present the Coordinator calls it in\n * place of `save`. It must be atomic: the whole change is written, or\n * nothing is. The change refers to the Coordinator's live objects, so\n * serialise or copy everything before returning, including in an async\n * store. Refuse a change whose `base` differs from the entries held, unless\n * `replace` is set, by throwing (or rejecting with) a StoreConflictError.\n */\n apply?(change: StoreChange): Promise<void> | void;\n\n /** Remove one workspace. */\n delete(id: string): Promise<void> | void;\n\n /**\n * The number of audit entries the store holds for one workspace, or\n * undefined when it holds none. Optional; see the concurrency model above.\n */\n version?(id: string): number | undefined;\n\n /** One workspace's record, or undefined when the store holds none. Optional. */\n loadOne?(id: string): WorkspaceRecord | undefined;\n\n /** Release resources. Idempotent. */\n close?(): Promise<void> | void;\n}\n\nfunction isCollection(name: string): boolean {\n return (COLLECTIONS as readonly string[]).includes(name);\n}\n\n/** The key an object is stored under in its collection. */\nexport function objectKey(collection: string, item: unknown): string | undefined {\n if (item === null || typeof item !== \"object\") return undefined;\n const o = item as Record<string, unknown>;\n const key = collection === \"members\" ? (o.uri ?? o.id) : (o.id ?? o.uri);\n return typeof key === \"string\" ? key : undefined;\n}\n\n/**\n * A collection as a snapshot holds it, keyed. The TypeScript snapshot holds\n * arrays and the Python snapshot holds objects keyed by id; both are read.\n */\nfunction keyed(collection: string, value: unknown): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n if (Array.isArray(value)) {\n for (const item of value) {\n const key = objectKey(collection, item);\n if (key !== undefined) out[key] = item;\n }\n } else if (value !== null && typeof value === \"object\") {\n Object.assign(out, value);\n }\n return out;\n}\n\n/** A whole-workspace snapshot as a change that replaces what the store holds. */\nexport function changeFromRecord(record: WorkspaceRecord): StoreChange {\n const data = (record.data !== null && typeof record.data === \"object\"\n ? record.data : {}) as Record<string, unknown>;\n const meta: Record<string, unknown> = {};\n const objects: Record<string, Record<string, unknown>> = {};\n let entries: unknown[] = [];\n for (const [name, value] of Object.entries(data)) {\n if (name === \"audit\") entries = Array.isArray(value) ? value : [];\n else if (name === IDEMPOTENCY_KEYS || isCollection(name)) objects[name] = keyed(name, value);\n else meta[name] = value;\n }\n if (meta.id === undefined) meta.id = record.id;\n return {\n id: record.id, base: 0, version: record.version, updated_at: record.updated_at,\n meta, entries, objects, replace: true,\n };\n}\n\n/**\n * Rows read back from a store, assembled into the snapshot shape\n * `Coordinator.restore()` reads: arrays for the collections, an object for\n * the idempotency keys, and the log in seq order.\n */\nexport function assembleSnapshot(\n meta: Record<string, unknown>,\n entries: unknown[],\n objects: Iterable<[collection: string, key: string, value: unknown]>,\n): Record<string, unknown> {\n const data: Record<string, unknown> = { ...meta };\n for (const name of COLLECTIONS) data[name] = [];\n const keys: Record<string, unknown> = {};\n data[IDEMPOTENCY_KEYS] = keys;\n for (const [collection, key, value] of objects) {\n if (collection === IDEMPOTENCY_KEYS) {\n keys[key] = value;\n } else {\n const list = (Array.isArray(data[collection]) ? data[collection] : (data[collection] = [])) as unknown[];\n list.push(value);\n }\n }\n data.audit = entries;\n return data;\n}\n\ninterface Held {\n meta: string;\n version: number;\n updated_at: string;\n entries: string[];\n objects: Map<string, Map<string, string>>;\n}\n\n/**\n * In-memory store. The default: it keeps nothing across processes. Like a\n * database it keeps each row as JSON text, so a Coordinator restarted on it\n * sees what was written and nothing changed later.\n */\nexport class MemoryStore implements Store {\n private held = new Map<string, Held>();\n\n load(): WorkspaceRecord[] {\n return [...this.held.keys()].map((id) => this.loadOne(id)!);\n }\n\n loadOne(id: string): WorkspaceRecord | undefined {\n const h = this.held.get(id);\n if (!h) return undefined;\n const objects: Array<[string, string, unknown]> = [];\n for (const [collection, items] of h.objects) {\n for (const [key, value] of items) objects.push([collection, key, JSON.parse(value)]);\n }\n return {\n id,\n data: assembleSnapshot(JSON.parse(h.meta), h.entries.map(e => JSON.parse(e)), objects),\n version: h.version,\n updated_at: h.updated_at,\n };\n }\n\n version(id: string): number | undefined {\n return this.held.get(id)?.entries.length;\n }\n\n save(record: WorkspaceRecord): void {\n this.apply(changeFromRecord(record));\n }\n\n apply(change: StoreChange): void {\n // Serialised before anything is held, so a change that cannot be written\n // leaves the store as it was.\n const meta = JSON.stringify(change.meta);\n const entries = change.entries.map(e => JSON.stringify(e));\n const objects = Object.entries(change.objects).map(([collection, items]) =>\n [collection, Object.entries(items).map(([key, value]) =>\n [key, value === null || value === undefined ? null : JSON.stringify(value)] as const)] as const);\n let h = this.held.get(change.id);\n if (!change.replace) {\n const holds = h ? h.entries.length : 0;\n if (holds !== change.base) throw new StoreConflictError(change.id, holds, change.base);\n }\n if (!h || change.replace) {\n h = { meta: \"{}\", version: 0, updated_at: \"\", entries: [], objects: new Map() };\n }\n for (const entry of entries) h.entries.push(entry);\n for (const [collection, items] of objects) {\n let held = h.objects.get(collection);\n if (!held) h.objects.set(collection, (held = new Map()));\n for (const [key, value] of items) {\n if (value === null) held.delete(key);\n else held.set(key, value);\n }\n }\n h.meta = meta;\n h.version = change.version;\n h.updated_at = change.updated_at;\n this.held.set(change.id, h);\n }\n\n delete(id: string): void {\n this.held.delete(id);\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/storage/store.cjs
CHANGED
|
@@ -37,7 +37,8 @@ var COLLECTIONS = [
|
|
|
37
37
|
"deliberations",
|
|
38
38
|
"handoffs",
|
|
39
39
|
"snapshots",
|
|
40
|
-
"route_decisions"
|
|
40
|
+
"route_decisions",
|
|
41
|
+
"invitations"
|
|
41
42
|
];
|
|
42
43
|
var IDEMPOTENCY_KEYS = "idempotency_keys";
|
|
43
44
|
var StoreConflictError = class extends Error {
|
|
@@ -48,7 +49,7 @@ var StoreConflictError = class extends Error {
|
|
|
48
49
|
base;
|
|
49
50
|
constructor(workspace, held, base) {
|
|
50
51
|
super(
|
|
51
|
-
`The store holds ${held} audit entries for workspace ${workspace} and this write expected ${base}, so nothing was written. Another coordinator may be writing to this workspace, the store may have been changed outside this coordinator
|
|
52
|
+
`The store holds ${held} audit entries for workspace ${workspace} and this write expected ${base}, so nothing was written. Another coordinator may be writing to this workspace, or the store may have been changed outside this coordinator. A coordinator reloads the workspace and answers the call as failed, and the call may be sent again.`
|
|
52
53
|
);
|
|
53
54
|
this.name = "StoreConflictError";
|
|
54
55
|
this.workspace = workspace;
|
|
@@ -125,20 +126,24 @@ function assembleSnapshot(meta, entries, objects) {
|
|
|
125
126
|
var MemoryStore = class {
|
|
126
127
|
held = /* @__PURE__ */ new Map();
|
|
127
128
|
load() {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
data: assembleSnapshot(JSON.parse(h.meta), h.entries.map((e) => JSON.parse(e)), objects),
|
|
137
|
-
version: h.version,
|
|
138
|
-
updated_at: h.updated_at
|
|
139
|
-
});
|
|
129
|
+
return [...this.held.keys()].map((id) => this.loadOne(id));
|
|
130
|
+
}
|
|
131
|
+
loadOne(id) {
|
|
132
|
+
const h = this.held.get(id);
|
|
133
|
+
if (!h) return void 0;
|
|
134
|
+
const objects = [];
|
|
135
|
+
for (const [collection, items] of h.objects) {
|
|
136
|
+
for (const [key, value] of items) objects.push([collection, key, JSON.parse(value)]);
|
|
140
137
|
}
|
|
141
|
-
return
|
|
138
|
+
return {
|
|
139
|
+
id,
|
|
140
|
+
data: assembleSnapshot(JSON.parse(h.meta), h.entries.map((e) => JSON.parse(e)), objects),
|
|
141
|
+
version: h.version,
|
|
142
|
+
updated_at: h.updated_at
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
version(id) {
|
|
146
|
+
return this.held.get(id)?.entries.length;
|
|
142
147
|
}
|
|
143
148
|
save(record) {
|
|
144
149
|
this.apply(changeFromRecord(record));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/storage/store.ts"],"sourcesContent":["/**\n * Pluggable persistence for the Coordinator.\n *\n * A Store keeps workspace state between process restarts. The Coordinator\n * calls `store.load()` once at start, and writes after every call that adds\n * to a workspace's audit log.\n *\n * How the Coordinator writes depends on what the store implements:\n *\n * - `apply(change)`: the Coordinator sends what the call changed: the new\n * audit entries, the objects it touched and the workspace's own fields.\n * The cost of a write stays flat as the log grows; it grows with the\n * objects a call changes, such as a task with a long history. MemoryStore\n * and SqliteStore implement it.\n * - `save(record)` alone: the Coordinator sends the whole workspace on every\n * write, as releases before 0.3.2 did, so the cost of a write grows with\n * the log. Stores written for earlier releases keep working this way.\n *\n * MemoryStore is the default and keeps nothing across processes. SqliteStore\n * keeps state in a SQLite file through better-sqlite3. Third-party stores\n * (Postgres, Redis and so on) implement this interface.\n *\n * Concurrency model\n * -----------------\n *\n * A Coordinator is a **single-writer** component: dispatch is serial,\n * workspace state is held in memory, and each chain link is computed from the\n * head the instance holds. Run one Coordinator per workspace.\n *\n * Every change names the number of audit entries the Coordinator expects the\n * store to hold already (`base`). MemoryStore and SqliteStore refuse a change\n * whose base differs from what they hold: they throw a StoreConflictError and\n * write nothing. A second Coordinator writing the same workspace is refused\n * and reported, and the entries already stored stay as they are. The refused\n * instance's memory no longer matches the store; restart it to reload.\n *\n * `save` makes no such check: it replaces what the store holds for the\n * workspace. A store that implements only `save` therefore still loses\n * entries silently when two instances write one workspace.\n *\n * To run more than one instance, partition workspaces so that no workspace is\n * ever written by two, or hold an external lock. See SPECIFICATION.md 10.3.\n *\n * Production deployments that need rich queries over the log can project it\n * into their own analytics store with the `onAudit` listener.\n */\n\n/** The workspace maps a store keeps object by object. */\nexport const COLLECTIONS = [\n \"members\", \"tasks\", \"overrides\", \"whispers\",\n \"deliberations\", \"handoffs\", \"snapshots\", \"route_decisions\",\n] as const;\n\n/** task.create idempotency keys, kept as one more collection of key to task id. */\nexport const IDEMPOTENCY_KEYS = \"idempotency_keys\";\n\nexport interface WorkspaceRecord {\n /** Workspace identifier. */\n id: string;\n /** JSON-serialisable payload in the shape `Coordinator.snapshot()` produces. */\n data: unknown;\n /**\n * For `save`, a counter the Coordinator increments on each write. For a\n * record a store loads after `apply`, the number of audit entries. Recorded\n * for diagnostics; `save` does not check it.\n */\n version: number;\n /** Wall-clock timestamp of the last write, ISO-8601. */\n updated_at: string;\n}\n\n/** One write to one workspace: what one or more calls changed. */\nexport interface StoreChange {\n /** Workspace identifier. */\n id: string;\n /**\n * Audit entries the store must already hold for this workspace. A store\n * that holds a different number refuses the change with a\n * StoreConflictError and writes nothing. Not checked when `replace` is set.\n */\n base: number;\n /** Audit entries the store holds after the change: `base + entries.length`. */\n version: number;\n /** Wall-clock timestamp of the write, ISO-8601. */\n updated_at: string;\n /** The workspace's own fields: everything except its collections and log. */\n meta: Record<string, unknown>;\n /** New audit entries in order. The first has seq `base`. */\n entries: unknown[];\n /**\n * Objects written, by collection and then by key. A null value removes the\n * object. The collections are those in COLLECTIONS and IDEMPOTENCY_KEYS.\n */\n objects: Record<string, Record<string, unknown>>;\n /**\n * Replace everything the store holds for this workspace with this change,\n * which then carries the whole workspace from seq 0.\n */\n replace?: boolean;\n}\n\n/**\n * A change whose base does not match the audit entries the store holds.\n * Usually a second Coordinator writing the same workspace.\n */\nexport class StoreConflictError extends Error {\n readonly workspace: string;\n /** Audit entries the store holds. */\n readonly held: number;\n /** Audit entries the change expected. */\n readonly base: number;\n\n constructor(workspace: string, held: number, base: number) {\n super(\n `The store holds ${held} audit entries for workspace ${workspace} and ` +\n `this write expected ${base}, so nothing was written. Another ` +\n `coordinator may be writing to this workspace, the store may have been ` +\n `changed outside this coordinator, or this coordinator could not load ` +\n `the workspace when it started. Restart this coordinator to reload.`,\n );\n this.name = \"StoreConflictError\";\n this.workspace = workspace;\n this.held = held;\n this.base = base;\n }\n\n /**\n * Matched by name, so `instanceof` holds across the package's entry points,\n * each of which is bundled with its own copy of this class.\n */\n static [Symbol.hasInstance](value: unknown): boolean {\n return value instanceof Error && value.name === \"StoreConflictError\";\n }\n}\n\nexport interface Store {\n /** Load all workspace records. Called once when the Coordinator starts. */\n load(): Promise<WorkspaceRecord[]> | WorkspaceRecord[];\n\n /**\n * Replace what the store holds for one workspace. The Coordinator calls it\n * after every write when the store has no `apply`.\n */\n save(record: WorkspaceRecord): Promise<void> | void;\n\n /**\n * Write one change. Optional; when present the Coordinator calls it in\n * place of `save`. It must be atomic: the whole change is written, or\n * nothing is. The change refers to the Coordinator's live objects, so\n * serialise or copy everything before returning, including in an async\n * store. Refuse a change whose `base` differs from the entries held, unless\n * `replace` is set, by throwing (or rejecting with) a StoreConflictError.\n */\n apply?(change: StoreChange): Promise<void> | void;\n\n /** Remove one workspace. */\n delete(id: string): Promise<void> | void;\n\n /** Release resources. Idempotent. */\n close?(): Promise<void> | void;\n}\n\nfunction isCollection(name: string): boolean {\n return (COLLECTIONS as readonly string[]).includes(name);\n}\n\n/** The key an object is stored under in its collection. */\nexport function objectKey(collection: string, item: unknown): string | undefined {\n if (item === null || typeof item !== \"object\") return undefined;\n const o = item as Record<string, unknown>;\n const key = collection === \"members\" ? (o.uri ?? o.id) : (o.id ?? o.uri);\n return typeof key === \"string\" ? key : undefined;\n}\n\n/**\n * A collection as a snapshot holds it, keyed. The TypeScript snapshot holds\n * arrays and the Python snapshot holds objects keyed by id; both are read.\n */\nfunction keyed(collection: string, value: unknown): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n if (Array.isArray(value)) {\n for (const item of value) {\n const key = objectKey(collection, item);\n if (key !== undefined) out[key] = item;\n }\n } else if (value !== null && typeof value === \"object\") {\n Object.assign(out, value);\n }\n return out;\n}\n\n/** A whole-workspace snapshot as a change that replaces what the store holds. */\nexport function changeFromRecord(record: WorkspaceRecord): StoreChange {\n const data = (record.data !== null && typeof record.data === \"object\"\n ? record.data : {}) as Record<string, unknown>;\n const meta: Record<string, unknown> = {};\n const objects: Record<string, Record<string, unknown>> = {};\n let entries: unknown[] = [];\n for (const [name, value] of Object.entries(data)) {\n if (name === \"audit\") entries = Array.isArray(value) ? value : [];\n else if (name === IDEMPOTENCY_KEYS || isCollection(name)) objects[name] = keyed(name, value);\n else meta[name] = value;\n }\n if (meta.id === undefined) meta.id = record.id;\n return {\n id: record.id, base: 0, version: record.version, updated_at: record.updated_at,\n meta, entries, objects, replace: true,\n };\n}\n\n/**\n * Rows read back from a store, assembled into the snapshot shape\n * `Coordinator.restore()` reads: arrays for the collections, an object for\n * the idempotency keys, and the log in seq order.\n */\nexport function assembleSnapshot(\n meta: Record<string, unknown>,\n entries: unknown[],\n objects: Iterable<[collection: string, key: string, value: unknown]>,\n): Record<string, unknown> {\n const data: Record<string, unknown> = { ...meta };\n for (const name of COLLECTIONS) data[name] = [];\n const keys: Record<string, unknown> = {};\n data[IDEMPOTENCY_KEYS] = keys;\n for (const [collection, key, value] of objects) {\n if (collection === IDEMPOTENCY_KEYS) {\n keys[key] = value;\n } else {\n const list = (Array.isArray(data[collection]) ? data[collection] : (data[collection] = [])) as unknown[];\n list.push(value);\n }\n }\n data.audit = entries;\n return data;\n}\n\ninterface Held {\n meta: string;\n version: number;\n updated_at: string;\n entries: string[];\n objects: Map<string, Map<string, string>>;\n}\n\n/**\n * In-memory store. The default: it keeps nothing across processes. Like a\n * database it keeps each row as JSON text, so a Coordinator restarted on it\n * sees what was written and nothing changed later.\n */\nexport class MemoryStore implements Store {\n private held = new Map<string, Held>();\n\n load(): WorkspaceRecord[] {\n const out: WorkspaceRecord[] = [];\n for (const [id, h] of this.held) {\n const objects: Array<[string, string, unknown]> = [];\n for (const [collection, items] of h.objects) {\n for (const [key, value] of items) objects.push([collection, key, JSON.parse(value)]);\n }\n out.push({\n id,\n data: assembleSnapshot(JSON.parse(h.meta), h.entries.map(e => JSON.parse(e)), objects),\n version: h.version,\n updated_at: h.updated_at,\n });\n }\n return out;\n }\n\n save(record: WorkspaceRecord): void {\n this.apply(changeFromRecord(record));\n }\n\n apply(change: StoreChange): void {\n // Serialised before anything is held, so a change that cannot be written\n // leaves the store as it was.\n const meta = JSON.stringify(change.meta);\n const entries = change.entries.map(e => JSON.stringify(e));\n const objects = Object.entries(change.objects).map(([collection, items]) =>\n [collection, Object.entries(items).map(([key, value]) =>\n [key, value === null || value === undefined ? null : JSON.stringify(value)] as const)] as const);\n let h = this.held.get(change.id);\n if (!change.replace) {\n const holds = h ? h.entries.length : 0;\n if (holds !== change.base) throw new StoreConflictError(change.id, holds, change.base);\n }\n if (!h || change.replace) {\n h = { meta: \"{}\", version: 0, updated_at: \"\", entries: [], objects: new Map() };\n }\n for (const entry of entries) h.entries.push(entry);\n for (const [collection, items] of objects) {\n let held = h.objects.get(collection);\n if (!held) h.objects.set(collection, (held = new Map()));\n for (const [key, value] of items) {\n if (value === null) held.delete(key);\n else held.set(key, value);\n }\n }\n h.meta = meta;\n h.version = change.version;\n h.updated_at = change.updated_at;\n this.held.set(change.id, h);\n }\n\n delete(id: string): void {\n this.held.delete(id);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/storage/store.ts"],"sourcesContent":["/**\n * Pluggable persistence for the Coordinator.\n *\n * A Store keeps workspace state between process restarts. The Coordinator\n * calls `store.load()` once at start, and writes after every call that adds\n * to a workspace's audit log.\n *\n * How the Coordinator writes depends on what the store implements:\n *\n * - `apply(change)`: the Coordinator sends what the call changed: the new\n * audit entries, the objects it touched and the workspace's own fields.\n * The cost of a write stays flat as the log grows; it grows with the\n * objects a call changes, such as a task with a long history. MemoryStore\n * and SqliteStore implement it.\n * - `save(record)` alone: the Coordinator sends the whole workspace on every\n * write, as releases before 0.3.2 did, so the cost of a write grows with\n * the log. Stores written for earlier releases keep working this way.\n *\n * MemoryStore is the default and keeps nothing across processes. SqliteStore\n * keeps state in a SQLite file through better-sqlite3. Third-party stores\n * (Postgres, Redis and so on) implement this interface.\n *\n * Concurrency model\n * -----------------\n *\n * A Coordinator is a **single-writer** component: dispatch is serial,\n * workspace state is held in memory, and each chain link is computed from the\n * head the instance holds. Run one Coordinator per workspace.\n *\n * Every change names the number of audit entries the Coordinator expects the\n * store to hold already (`base`). MemoryStore and SqliteStore refuse a change\n * whose base differs from what they hold: they throw a StoreConflictError and\n * write nothing. A second Coordinator writing the same workspace is refused,\n * and the entries already stored stay as they are.\n *\n * A store that also implements `version` and `loadOne` lets a Coordinator\n * keep up with another writer: before each call it compares the entries the\n * store holds for the call's workspace with the entries it last wrote, and\n * reloads the workspace when they differ. A synchronous write that fails or\n * conflicts answers its call with -32603 and `data: {reason: \"store\"}`; the\n * Coordinator then reloads the workspace, and refuses calls on it the same\n * way until a reload succeeds. An asynchronous write that fails is reported,\n * and the next write carries its change again.\n *\n * `save` makes no such check: it replaces what the store holds for the\n * workspace. A store that implements only `save` therefore still loses\n * entries silently when two instances write one workspace.\n *\n * To run more than one instance on a store without `version`, partition\n * workspaces so that no workspace is ever written by two, or hold an\n * external lock. See SPECIFICATION.md 10.3.\n *\n * Production deployments that need rich queries over the log can project it\n * into their own analytics store with the `onAudit` listener.\n */\n\n/** The workspace maps a store keeps object by object. */\nexport const COLLECTIONS = [\n \"members\", \"tasks\", \"overrides\", \"whispers\",\n \"deliberations\", \"handoffs\", \"snapshots\", \"route_decisions\", \"invitations\",\n] as const;\n\n/** task.create idempotency keys, kept as one more collection of key to task id. */\nexport const IDEMPOTENCY_KEYS = \"idempotency_keys\";\n\nexport interface WorkspaceRecord {\n /** Workspace identifier. */\n id: string;\n /** JSON-serialisable payload in the shape `Coordinator.snapshot()` produces. */\n data: unknown;\n /**\n * For `save`, a counter the Coordinator increments on each write. For a\n * record a store loads after `apply`, the number of audit entries. Recorded\n * for diagnostics; `save` does not check it.\n */\n version: number;\n /** Wall-clock timestamp of the last write, ISO-8601. */\n updated_at: string;\n}\n\n/** One write to one workspace: what one or more calls changed. */\nexport interface StoreChange {\n /** Workspace identifier. */\n id: string;\n /**\n * Audit entries the store must already hold for this workspace. A store\n * that holds a different number refuses the change with a\n * StoreConflictError and writes nothing. Not checked when `replace` is set.\n */\n base: number;\n /** Audit entries the store holds after the change: `base + entries.length`. */\n version: number;\n /** Wall-clock timestamp of the write, ISO-8601. */\n updated_at: string;\n /** The workspace's own fields: everything except its collections and log. */\n meta: Record<string, unknown>;\n /** New audit entries in order. The first has seq `base`. */\n entries: unknown[];\n /**\n * Objects written, by collection and then by key. A null value removes the\n * object. The collections are those in COLLECTIONS and IDEMPOTENCY_KEYS.\n */\n objects: Record<string, Record<string, unknown>>;\n /**\n * Replace everything the store holds for this workspace with this change,\n * which then carries the whole workspace from seq 0.\n */\n replace?: boolean;\n}\n\n/**\n * A change whose base does not match the audit entries the store holds.\n * Usually a second Coordinator writing the same workspace.\n */\nexport class StoreConflictError extends Error {\n readonly workspace: string;\n /** Audit entries the store holds. */\n readonly held: number;\n /** Audit entries the change expected. */\n readonly base: number;\n\n constructor(workspace: string, held: number, base: number) {\n super(\n `The store holds ${held} audit entries for workspace ${workspace} and ` +\n `this write expected ${base}, so nothing was written. Another ` +\n `coordinator may be writing to this workspace, or the store may have ` +\n `been changed outside this coordinator. A coordinator reloads the ` +\n `workspace and answers the call as failed, and the call may be sent ` +\n `again.`,\n );\n this.name = \"StoreConflictError\";\n this.workspace = workspace;\n this.held = held;\n this.base = base;\n }\n\n /**\n * Matched by name, so `instanceof` holds across the package's entry points,\n * each of which is bundled with its own copy of this class.\n */\n static [Symbol.hasInstance](value: unknown): boolean {\n return value instanceof Error && value.name === \"StoreConflictError\";\n }\n}\n\nexport interface Store {\n /** Load all workspace records. Called once when the Coordinator starts. */\n load(): Promise<WorkspaceRecord[]> | WorkspaceRecord[];\n\n /**\n * Replace what the store holds for one workspace. The Coordinator calls it\n * after every write when the store has no `apply`.\n */\n save(record: WorkspaceRecord): Promise<void> | void;\n\n /**\n * Write one change. Optional; when present the Coordinator calls it in\n * place of `save`. It must be atomic: the whole change is written, or\n * nothing is. The change refers to the Coordinator's live objects, so\n * serialise or copy everything before returning, including in an async\n * store. Refuse a change whose `base` differs from the entries held, unless\n * `replace` is set, by throwing (or rejecting with) a StoreConflictError.\n */\n apply?(change: StoreChange): Promise<void> | void;\n\n /** Remove one workspace. */\n delete(id: string): Promise<void> | void;\n\n /**\n * The number of audit entries the store holds for one workspace, or\n * undefined when it holds none. Optional; see the concurrency model above.\n */\n version?(id: string): number | undefined;\n\n /** One workspace's record, or undefined when the store holds none. Optional. */\n loadOne?(id: string): WorkspaceRecord | undefined;\n\n /** Release resources. Idempotent. */\n close?(): Promise<void> | void;\n}\n\nfunction isCollection(name: string): boolean {\n return (COLLECTIONS as readonly string[]).includes(name);\n}\n\n/** The key an object is stored under in its collection. */\nexport function objectKey(collection: string, item: unknown): string | undefined {\n if (item === null || typeof item !== \"object\") return undefined;\n const o = item as Record<string, unknown>;\n const key = collection === \"members\" ? (o.uri ?? o.id) : (o.id ?? o.uri);\n return typeof key === \"string\" ? key : undefined;\n}\n\n/**\n * A collection as a snapshot holds it, keyed. The TypeScript snapshot holds\n * arrays and the Python snapshot holds objects keyed by id; both are read.\n */\nfunction keyed(collection: string, value: unknown): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n if (Array.isArray(value)) {\n for (const item of value) {\n const key = objectKey(collection, item);\n if (key !== undefined) out[key] = item;\n }\n } else if (value !== null && typeof value === \"object\") {\n Object.assign(out, value);\n }\n return out;\n}\n\n/** A whole-workspace snapshot as a change that replaces what the store holds. */\nexport function changeFromRecord(record: WorkspaceRecord): StoreChange {\n const data = (record.data !== null && typeof record.data === \"object\"\n ? record.data : {}) as Record<string, unknown>;\n const meta: Record<string, unknown> = {};\n const objects: Record<string, Record<string, unknown>> = {};\n let entries: unknown[] = [];\n for (const [name, value] of Object.entries(data)) {\n if (name === \"audit\") entries = Array.isArray(value) ? value : [];\n else if (name === IDEMPOTENCY_KEYS || isCollection(name)) objects[name] = keyed(name, value);\n else meta[name] = value;\n }\n if (meta.id === undefined) meta.id = record.id;\n return {\n id: record.id, base: 0, version: record.version, updated_at: record.updated_at,\n meta, entries, objects, replace: true,\n };\n}\n\n/**\n * Rows read back from a store, assembled into the snapshot shape\n * `Coordinator.restore()` reads: arrays for the collections, an object for\n * the idempotency keys, and the log in seq order.\n */\nexport function assembleSnapshot(\n meta: Record<string, unknown>,\n entries: unknown[],\n objects: Iterable<[collection: string, key: string, value: unknown]>,\n): Record<string, unknown> {\n const data: Record<string, unknown> = { ...meta };\n for (const name of COLLECTIONS) data[name] = [];\n const keys: Record<string, unknown> = {};\n data[IDEMPOTENCY_KEYS] = keys;\n for (const [collection, key, value] of objects) {\n if (collection === IDEMPOTENCY_KEYS) {\n keys[key] = value;\n } else {\n const list = (Array.isArray(data[collection]) ? data[collection] : (data[collection] = [])) as unknown[];\n list.push(value);\n }\n }\n data.audit = entries;\n return data;\n}\n\ninterface Held {\n meta: string;\n version: number;\n updated_at: string;\n entries: string[];\n objects: Map<string, Map<string, string>>;\n}\n\n/**\n * In-memory store. The default: it keeps nothing across processes. Like a\n * database it keeps each row as JSON text, so a Coordinator restarted on it\n * sees what was written and nothing changed later.\n */\nexport class MemoryStore implements Store {\n private held = new Map<string, Held>();\n\n load(): WorkspaceRecord[] {\n return [...this.held.keys()].map((id) => this.loadOne(id)!);\n }\n\n loadOne(id: string): WorkspaceRecord | undefined {\n const h = this.held.get(id);\n if (!h) return undefined;\n const objects: Array<[string, string, unknown]> = [];\n for (const [collection, items] of h.objects) {\n for (const [key, value] of items) objects.push([collection, key, JSON.parse(value)]);\n }\n return {\n id,\n data: assembleSnapshot(JSON.parse(h.meta), h.entries.map(e => JSON.parse(e)), objects),\n version: h.version,\n updated_at: h.updated_at,\n };\n }\n\n version(id: string): number | undefined {\n return this.held.get(id)?.entries.length;\n }\n\n save(record: WorkspaceRecord): void {\n this.apply(changeFromRecord(record));\n }\n\n apply(change: StoreChange): void {\n // Serialised before anything is held, so a change that cannot be written\n // leaves the store as it was.\n const meta = JSON.stringify(change.meta);\n const entries = change.entries.map(e => JSON.stringify(e));\n const objects = Object.entries(change.objects).map(([collection, items]) =>\n [collection, Object.entries(items).map(([key, value]) =>\n [key, value === null || value === undefined ? null : JSON.stringify(value)] as const)] as const);\n let h = this.held.get(change.id);\n if (!change.replace) {\n const holds = h ? h.entries.length : 0;\n if (holds !== change.base) throw new StoreConflictError(change.id, holds, change.base);\n }\n if (!h || change.replace) {\n h = { meta: \"{}\", version: 0, updated_at: \"\", entries: [], objects: new Map() };\n }\n for (const entry of entries) h.entries.push(entry);\n for (const [collection, items] of objects) {\n let held = h.objects.get(collection);\n if (!held) h.objects.set(collection, (held = new Map()));\n for (const [key, value] of items) {\n if (value === null) held.delete(key);\n else held.set(key, value);\n }\n }\n h.meta = meta;\n h.version = change.version;\n h.updated_at = change.updated_at;\n this.held.set(change.id, h);\n }\n\n delete(id: string): void {\n this.held.delete(id);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyDO,IAAM,cAAc;AAAA,EACzB;AAAA,EAAW;AAAA,EAAS;AAAA,EAAa;AAAA,EACjC;AAAA,EAAiB;AAAA,EAAY;AAAA,EAAa;AAAA,EAAmB;AAC/D;AAGO,IAAM,mBAAmB;AAmDzB,IAAM,qBAAN,cAAiC,MAAM;AAAA,EACnC;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EAET,YAAY,WAAmB,MAAc,MAAc;AACzD;AAAA,MACE,mBAAmB,IAAI,gCAAgC,SAAS,4BACzC,IAAI;AAAA,IAK7B;AACA,SAAK,OAAO;AACZ,SAAK,YAAY;AACjB,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAQ,OAAO,WAAW,EAAE,OAAyB;AACnD,WAAO,iBAAiB,SAAS,MAAM,SAAS;AAAA,EAClD;AACF;AAsCA,SAAS,aAAa,MAAuB;AAC3C,SAAQ,YAAkC,SAAS,IAAI;AACzD;AAGO,SAAS,UAAU,YAAoB,MAAmC;AAC/E,MAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AACtD,QAAM,IAAI;AACV,QAAM,MAAM,eAAe,YAAa,EAAE,OAAO,EAAE,KAAO,EAAE,MAAM,EAAE;AACpE,SAAO,OAAO,QAAQ,WAAW,MAAM;AACzC;AAMA,SAAS,MAAM,YAAoB,OAAyC;AAC1E,QAAM,MAA+B,CAAC;AACtC,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,eAAW,QAAQ,OAAO;AACxB,YAAM,MAAM,UAAU,YAAY,IAAI;AACtC,UAAI,QAAQ,OAAW,KAAI,GAAG,IAAI;AAAA,IACpC;AAAA,EACF,WAAW,UAAU,QAAQ,OAAO,UAAU,UAAU;AACtD,WAAO,OAAO,KAAK,KAAK;AAAA,EAC1B;AACA,SAAO;AACT;AAGO,SAAS,iBAAiB,QAAsC;AACrE,QAAM,OAAQ,OAAO,SAAS,QAAQ,OAAO,OAAO,SAAS,WACzD,OAAO,OAAO,CAAC;AACnB,QAAM,OAAgC,CAAC;AACvC,QAAM,UAAmD,CAAC;AAC1D,MAAI,UAAqB,CAAC;AAC1B,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAChD,QAAI,SAAS,QAAS,WAAU,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC;AAAA,aACvD,SAAS,oBAAoB,aAAa,IAAI,EAAG,SAAQ,IAAI,IAAI,MAAM,MAAM,KAAK;AAAA,QACtF,MAAK,IAAI,IAAI;AAAA,EACpB;AACA,MAAI,KAAK,OAAO,OAAW,MAAK,KAAK,OAAO;AAC5C,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IAAI,MAAM;AAAA,IAAG,SAAS,OAAO;AAAA,IAAS,YAAY,OAAO;AAAA,IACpE;AAAA,IAAM;AAAA,IAAS;AAAA,IAAS,SAAS;AAAA,EACnC;AACF;AAOO,SAAS,iBACd,MACA,SACA,SACyB;AACzB,QAAM,OAAgC,EAAE,GAAG,KAAK;AAChD,aAAW,QAAQ,YAAa,MAAK,IAAI,IAAI,CAAC;AAC9C,QAAM,OAAgC,CAAC;AACvC,OAAK,gBAAgB,IAAI;AACzB,aAAW,CAAC,YAAY,KAAK,KAAK,KAAK,SAAS;AAC9C,QAAI,eAAe,kBAAkB;AACnC,WAAK,GAAG,IAAI;AAAA,IACd,OAAO;AACL,YAAM,OAAQ,MAAM,QAAQ,KAAK,UAAU,CAAC,IAAI,KAAK,UAAU,IAAK,KAAK,UAAU,IAAI,CAAC;AACxF,WAAK,KAAK,KAAK;AAAA,IACjB;AAAA,EACF;AACA,OAAK,QAAQ;AACb,SAAO;AACT;AAeO,IAAM,cAAN,MAAmC;AAAA,EAChC,OAAO,oBAAI,IAAkB;AAAA,EAErC,OAA0B;AACxB,WAAO,CAAC,GAAG,KAAK,KAAK,KAAK,CAAC,EAAE,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAE;AAAA,EAC5D;AAAA,EAEA,QAAQ,IAAyC;AAC/C,UAAM,IAAI,KAAK,KAAK,IAAI,EAAE;AAC1B,QAAI,CAAC,EAAG,QAAO;AACf,UAAM,UAA4C,CAAC;AACnD,eAAW,CAAC,YAAY,KAAK,KAAK,EAAE,SAAS;AAC3C,iBAAW,CAAC,KAAK,KAAK,KAAK,MAAO,SAAQ,KAAK,CAAC,YAAY,KAAK,KAAK,MAAM,KAAK,CAAC,CAAC;AAAA,IACrF;AACA,WAAO;AAAA,MACL;AAAA,MACA,MAAM,iBAAiB,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,QAAQ,IAAI,OAAK,KAAK,MAAM,CAAC,CAAC,GAAG,OAAO;AAAA,MACrF,SAAS,EAAE;AAAA,MACX,YAAY,EAAE;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,QAAQ,IAAgC;AACtC,WAAO,KAAK,KAAK,IAAI,EAAE,GAAG,QAAQ;AAAA,EACpC;AAAA,EAEA,KAAK,QAA+B;AAClC,SAAK,MAAM,iBAAiB,MAAM,CAAC;AAAA,EACrC;AAAA,EAEA,MAAM,QAA2B;AAG/B,UAAM,OAAO,KAAK,UAAU,OAAO,IAAI;AACvC,UAAM,UAAU,OAAO,QAAQ,IAAI,OAAK,KAAK,UAAU,CAAC,CAAC;AACzD,UAAM,UAAU,OAAO,QAAQ,OAAO,OAAO,EAAE,IAAI,CAAC,CAAC,YAAY,KAAK,MACpE,CAAC,YAAY,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,MACjD,CAAC,KAAK,UAAU,QAAQ,UAAU,SAAY,OAAO,KAAK,UAAU,KAAK,CAAC,CAAU,CAAC,CAAU;AACnG,QAAI,IAAI,KAAK,KAAK,IAAI,OAAO,EAAE;AAC/B,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,QAAQ,IAAI,EAAE,QAAQ,SAAS;AACrC,UAAI,UAAU,OAAO,KAAM,OAAM,IAAI,mBAAmB,OAAO,IAAI,OAAO,OAAO,IAAI;AAAA,IACvF;AACA,QAAI,CAAC,KAAK,OAAO,SAAS;AACxB,UAAI,EAAE,MAAM,MAAM,SAAS,GAAG,YAAY,IAAI,SAAS,CAAC,GAAG,SAAS,oBAAI,IAAI,EAAE;AAAA,IAChF;AACA,eAAW,SAAS,QAAS,GAAE,QAAQ,KAAK,KAAK;AACjD,eAAW,CAAC,YAAY,KAAK,KAAK,SAAS;AACzC,UAAI,OAAO,EAAE,QAAQ,IAAI,UAAU;AACnC,UAAI,CAAC,KAAM,GAAE,QAAQ,IAAI,YAAa,OAAO,oBAAI,IAAI,CAAE;AACvD,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO;AAChC,YAAI,UAAU,KAAM,MAAK,OAAO,GAAG;AAAA,YAC9B,MAAK,IAAI,KAAK,KAAK;AAAA,MAC1B;AAAA,IACF;AACA,MAAE,OAAO;AACT,MAAE,UAAU,OAAO;AACnB,MAAE,aAAa,OAAO;AACtB,SAAK,KAAK,IAAI,OAAO,IAAI,CAAC;AAAA,EAC5B;AAAA,EAEA,OAAO,IAAkB;AACvB,SAAK,KAAK,OAAO,EAAE;AAAA,EACrB;AACF;","names":[]}
|
package/dist/storage/store.d.cts
CHANGED
|
@@ -30,22 +30,31 @@
|
|
|
30
30
|
* Every change names the number of audit entries the Coordinator expects the
|
|
31
31
|
* store to hold already (`base`). MemoryStore and SqliteStore refuse a change
|
|
32
32
|
* whose base differs from what they hold: they throw a StoreConflictError and
|
|
33
|
-
* write nothing. A second Coordinator writing the same workspace is refused
|
|
34
|
-
* and
|
|
35
|
-
*
|
|
33
|
+
* write nothing. A second Coordinator writing the same workspace is refused,
|
|
34
|
+
* and the entries already stored stay as they are.
|
|
35
|
+
*
|
|
36
|
+
* A store that also implements `version` and `loadOne` lets a Coordinator
|
|
37
|
+
* keep up with another writer: before each call it compares the entries the
|
|
38
|
+
* store holds for the call's workspace with the entries it last wrote, and
|
|
39
|
+
* reloads the workspace when they differ. A synchronous write that fails or
|
|
40
|
+
* conflicts answers its call with -32603 and `data: {reason: "store"}`; the
|
|
41
|
+
* Coordinator then reloads the workspace, and refuses calls on it the same
|
|
42
|
+
* way until a reload succeeds. An asynchronous write that fails is reported,
|
|
43
|
+
* and the next write carries its change again.
|
|
36
44
|
*
|
|
37
45
|
* `save` makes no such check: it replaces what the store holds for the
|
|
38
46
|
* workspace. A store that implements only `save` therefore still loses
|
|
39
47
|
* entries silently when two instances write one workspace.
|
|
40
48
|
*
|
|
41
|
-
* To run more than one instance
|
|
42
|
-
* ever written by two, or hold an
|
|
49
|
+
* To run more than one instance on a store without `version`, partition
|
|
50
|
+
* workspaces so that no workspace is ever written by two, or hold an
|
|
51
|
+
* external lock. See SPECIFICATION.md 10.3.
|
|
43
52
|
*
|
|
44
53
|
* Production deployments that need rich queries over the log can project it
|
|
45
54
|
* into their own analytics store with the `onAudit` listener.
|
|
46
55
|
*/
|
|
47
56
|
/** The workspace maps a store keeps object by object. */
|
|
48
|
-
declare const COLLECTIONS: readonly ["members", "tasks", "overrides", "whispers", "deliberations", "handoffs", "snapshots", "route_decisions"];
|
|
57
|
+
declare const COLLECTIONS: readonly ["members", "tasks", "overrides", "whispers", "deliberations", "handoffs", "snapshots", "route_decisions", "invitations"];
|
|
49
58
|
/** task.create idempotency keys, kept as one more collection of key to task id. */
|
|
50
59
|
declare const IDEMPOTENCY_KEYS = "idempotency_keys";
|
|
51
60
|
interface WorkspaceRecord {
|
|
@@ -127,6 +136,13 @@ interface Store {
|
|
|
127
136
|
apply?(change: StoreChange): Promise<void> | void;
|
|
128
137
|
/** Remove one workspace. */
|
|
129
138
|
delete(id: string): Promise<void> | void;
|
|
139
|
+
/**
|
|
140
|
+
* The number of audit entries the store holds for one workspace, or
|
|
141
|
+
* undefined when it holds none. Optional; see the concurrency model above.
|
|
142
|
+
*/
|
|
143
|
+
version?(id: string): number | undefined;
|
|
144
|
+
/** One workspace's record, or undefined when the store holds none. Optional. */
|
|
145
|
+
loadOne?(id: string): WorkspaceRecord | undefined;
|
|
130
146
|
/** Release resources. Idempotent. */
|
|
131
147
|
close?(): Promise<void> | void;
|
|
132
148
|
}
|
|
@@ -148,6 +164,8 @@ declare function assembleSnapshot(meta: Record<string, unknown>, entries: unknow
|
|
|
148
164
|
declare class MemoryStore implements Store {
|
|
149
165
|
private held;
|
|
150
166
|
load(): WorkspaceRecord[];
|
|
167
|
+
loadOne(id: string): WorkspaceRecord | undefined;
|
|
168
|
+
version(id: string): number | undefined;
|
|
151
169
|
save(record: WorkspaceRecord): void;
|
|
152
170
|
apply(change: StoreChange): void;
|
|
153
171
|
delete(id: string): void;
|
package/dist/storage/store.d.ts
CHANGED
|
@@ -30,22 +30,31 @@
|
|
|
30
30
|
* Every change names the number of audit entries the Coordinator expects the
|
|
31
31
|
* store to hold already (`base`). MemoryStore and SqliteStore refuse a change
|
|
32
32
|
* whose base differs from what they hold: they throw a StoreConflictError and
|
|
33
|
-
* write nothing. A second Coordinator writing the same workspace is refused
|
|
34
|
-
* and
|
|
35
|
-
*
|
|
33
|
+
* write nothing. A second Coordinator writing the same workspace is refused,
|
|
34
|
+
* and the entries already stored stay as they are.
|
|
35
|
+
*
|
|
36
|
+
* A store that also implements `version` and `loadOne` lets a Coordinator
|
|
37
|
+
* keep up with another writer: before each call it compares the entries the
|
|
38
|
+
* store holds for the call's workspace with the entries it last wrote, and
|
|
39
|
+
* reloads the workspace when they differ. A synchronous write that fails or
|
|
40
|
+
* conflicts answers its call with -32603 and `data: {reason: "store"}`; the
|
|
41
|
+
* Coordinator then reloads the workspace, and refuses calls on it the same
|
|
42
|
+
* way until a reload succeeds. An asynchronous write that fails is reported,
|
|
43
|
+
* and the next write carries its change again.
|
|
36
44
|
*
|
|
37
45
|
* `save` makes no such check: it replaces what the store holds for the
|
|
38
46
|
* workspace. A store that implements only `save` therefore still loses
|
|
39
47
|
* entries silently when two instances write one workspace.
|
|
40
48
|
*
|
|
41
|
-
* To run more than one instance
|
|
42
|
-
* ever written by two, or hold an
|
|
49
|
+
* To run more than one instance on a store without `version`, partition
|
|
50
|
+
* workspaces so that no workspace is ever written by two, or hold an
|
|
51
|
+
* external lock. See SPECIFICATION.md 10.3.
|
|
43
52
|
*
|
|
44
53
|
* Production deployments that need rich queries over the log can project it
|
|
45
54
|
* into their own analytics store with the `onAudit` listener.
|
|
46
55
|
*/
|
|
47
56
|
/** The workspace maps a store keeps object by object. */
|
|
48
|
-
declare const COLLECTIONS: readonly ["members", "tasks", "overrides", "whispers", "deliberations", "handoffs", "snapshots", "route_decisions"];
|
|
57
|
+
declare const COLLECTIONS: readonly ["members", "tasks", "overrides", "whispers", "deliberations", "handoffs", "snapshots", "route_decisions", "invitations"];
|
|
49
58
|
/** task.create idempotency keys, kept as one more collection of key to task id. */
|
|
50
59
|
declare const IDEMPOTENCY_KEYS = "idempotency_keys";
|
|
51
60
|
interface WorkspaceRecord {
|
|
@@ -127,6 +136,13 @@ interface Store {
|
|
|
127
136
|
apply?(change: StoreChange): Promise<void> | void;
|
|
128
137
|
/** Remove one workspace. */
|
|
129
138
|
delete(id: string): Promise<void> | void;
|
|
139
|
+
/**
|
|
140
|
+
* The number of audit entries the store holds for one workspace, or
|
|
141
|
+
* undefined when it holds none. Optional; see the concurrency model above.
|
|
142
|
+
*/
|
|
143
|
+
version?(id: string): number | undefined;
|
|
144
|
+
/** One workspace's record, or undefined when the store holds none. Optional. */
|
|
145
|
+
loadOne?(id: string): WorkspaceRecord | undefined;
|
|
130
146
|
/** Release resources. Idempotent. */
|
|
131
147
|
close?(): Promise<void> | void;
|
|
132
148
|
}
|
|
@@ -148,6 +164,8 @@ declare function assembleSnapshot(meta: Record<string, unknown>, entries: unknow
|
|
|
148
164
|
declare class MemoryStore implements Store {
|
|
149
165
|
private held;
|
|
150
166
|
load(): WorkspaceRecord[];
|
|
167
|
+
loadOne(id: string): WorkspaceRecord | undefined;
|
|
168
|
+
version(id: string): number | undefined;
|
|
151
169
|
save(record: WorkspaceRecord): void;
|
|
152
170
|
apply(change: StoreChange): void;
|
|
153
171
|
delete(id: string): void;
|
package/dist/storage/store.js
CHANGED
|
@@ -7,7 +7,8 @@ var COLLECTIONS = [
|
|
|
7
7
|
"deliberations",
|
|
8
8
|
"handoffs",
|
|
9
9
|
"snapshots",
|
|
10
|
-
"route_decisions"
|
|
10
|
+
"route_decisions",
|
|
11
|
+
"invitations"
|
|
11
12
|
];
|
|
12
13
|
var IDEMPOTENCY_KEYS = "idempotency_keys";
|
|
13
14
|
var StoreConflictError = class extends Error {
|
|
@@ -18,7 +19,7 @@ var StoreConflictError = class extends Error {
|
|
|
18
19
|
base;
|
|
19
20
|
constructor(workspace, held, base) {
|
|
20
21
|
super(
|
|
21
|
-
`The store holds ${held} audit entries for workspace ${workspace} and this write expected ${base}, so nothing was written. Another coordinator may be writing to this workspace, the store may have been changed outside this coordinator
|
|
22
|
+
`The store holds ${held} audit entries for workspace ${workspace} and this write expected ${base}, so nothing was written. Another coordinator may be writing to this workspace, or the store may have been changed outside this coordinator. A coordinator reloads the workspace and answers the call as failed, and the call may be sent again.`
|
|
22
23
|
);
|
|
23
24
|
this.name = "StoreConflictError";
|
|
24
25
|
this.workspace = workspace;
|
|
@@ -95,20 +96,24 @@ function assembleSnapshot(meta, entries, objects) {
|
|
|
95
96
|
var MemoryStore = class {
|
|
96
97
|
held = /* @__PURE__ */ new Map();
|
|
97
98
|
load() {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
data: assembleSnapshot(JSON.parse(h.meta), h.entries.map((e) => JSON.parse(e)), objects),
|
|
107
|
-
version: h.version,
|
|
108
|
-
updated_at: h.updated_at
|
|
109
|
-
});
|
|
99
|
+
return [...this.held.keys()].map((id) => this.loadOne(id));
|
|
100
|
+
}
|
|
101
|
+
loadOne(id) {
|
|
102
|
+
const h = this.held.get(id);
|
|
103
|
+
if (!h) return void 0;
|
|
104
|
+
const objects = [];
|
|
105
|
+
for (const [collection, items] of h.objects) {
|
|
106
|
+
for (const [key, value] of items) objects.push([collection, key, JSON.parse(value)]);
|
|
110
107
|
}
|
|
111
|
-
return
|
|
108
|
+
return {
|
|
109
|
+
id,
|
|
110
|
+
data: assembleSnapshot(JSON.parse(h.meta), h.entries.map((e) => JSON.parse(e)), objects),
|
|
111
|
+
version: h.version,
|
|
112
|
+
updated_at: h.updated_at
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
version(id) {
|
|
116
|
+
return this.held.get(id)?.entries.length;
|
|
112
117
|
}
|
|
113
118
|
save(record) {
|
|
114
119
|
this.apply(changeFromRecord(record));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/storage/store.ts"],"sourcesContent":["/**\n * Pluggable persistence for the Coordinator.\n *\n * A Store keeps workspace state between process restarts. The Coordinator\n * calls `store.load()` once at start, and writes after every call that adds\n * to a workspace's audit log.\n *\n * How the Coordinator writes depends on what the store implements:\n *\n * - `apply(change)`: the Coordinator sends what the call changed: the new\n * audit entries, the objects it touched and the workspace's own fields.\n * The cost of a write stays flat as the log grows; it grows with the\n * objects a call changes, such as a task with a long history. MemoryStore\n * and SqliteStore implement it.\n * - `save(record)` alone: the Coordinator sends the whole workspace on every\n * write, as releases before 0.3.2 did, so the cost of a write grows with\n * the log. Stores written for earlier releases keep working this way.\n *\n * MemoryStore is the default and keeps nothing across processes. SqliteStore\n * keeps state in a SQLite file through better-sqlite3. Third-party stores\n * (Postgres, Redis and so on) implement this interface.\n *\n * Concurrency model\n * -----------------\n *\n * A Coordinator is a **single-writer** component: dispatch is serial,\n * workspace state is held in memory, and each chain link is computed from the\n * head the instance holds. Run one Coordinator per workspace.\n *\n * Every change names the number of audit entries the Coordinator expects the\n * store to hold already (`base`). MemoryStore and SqliteStore refuse a change\n * whose base differs from what they hold: they throw a StoreConflictError and\n * write nothing. A second Coordinator writing the same workspace is refused\n * and reported, and the entries already stored stay as they are. The refused\n * instance's memory no longer matches the store; restart it to reload.\n *\n * `save` makes no such check: it replaces what the store holds for the\n * workspace. A store that implements only `save` therefore still loses\n * entries silently when two instances write one workspace.\n *\n * To run more than one instance, partition workspaces so that no workspace is\n * ever written by two, or hold an external lock. See SPECIFICATION.md 10.3.\n *\n * Production deployments that need rich queries over the log can project it\n * into their own analytics store with the `onAudit` listener.\n */\n\n/** The workspace maps a store keeps object by object. */\nexport const COLLECTIONS = [\n \"members\", \"tasks\", \"overrides\", \"whispers\",\n \"deliberations\", \"handoffs\", \"snapshots\", \"route_decisions\",\n] as const;\n\n/** task.create idempotency keys, kept as one more collection of key to task id. */\nexport const IDEMPOTENCY_KEYS = \"idempotency_keys\";\n\nexport interface WorkspaceRecord {\n /** Workspace identifier. */\n id: string;\n /** JSON-serialisable payload in the shape `Coordinator.snapshot()` produces. */\n data: unknown;\n /**\n * For `save`, a counter the Coordinator increments on each write. For a\n * record a store loads after `apply`, the number of audit entries. Recorded\n * for diagnostics; `save` does not check it.\n */\n version: number;\n /** Wall-clock timestamp of the last write, ISO-8601. */\n updated_at: string;\n}\n\n/** One write to one workspace: what one or more calls changed. */\nexport interface StoreChange {\n /** Workspace identifier. */\n id: string;\n /**\n * Audit entries the store must already hold for this workspace. A store\n * that holds a different number refuses the change with a\n * StoreConflictError and writes nothing. Not checked when `replace` is set.\n */\n base: number;\n /** Audit entries the store holds after the change: `base + entries.length`. */\n version: number;\n /** Wall-clock timestamp of the write, ISO-8601. */\n updated_at: string;\n /** The workspace's own fields: everything except its collections and log. */\n meta: Record<string, unknown>;\n /** New audit entries in order. The first has seq `base`. */\n entries: unknown[];\n /**\n * Objects written, by collection and then by key. A null value removes the\n * object. The collections are those in COLLECTIONS and IDEMPOTENCY_KEYS.\n */\n objects: Record<string, Record<string, unknown>>;\n /**\n * Replace everything the store holds for this workspace with this change,\n * which then carries the whole workspace from seq 0.\n */\n replace?: boolean;\n}\n\n/**\n * A change whose base does not match the audit entries the store holds.\n * Usually a second Coordinator writing the same workspace.\n */\nexport class StoreConflictError extends Error {\n readonly workspace: string;\n /** Audit entries the store holds. */\n readonly held: number;\n /** Audit entries the change expected. */\n readonly base: number;\n\n constructor(workspace: string, held: number, base: number) {\n super(\n `The store holds ${held} audit entries for workspace ${workspace} and ` +\n `this write expected ${base}, so nothing was written. Another ` +\n `coordinator may be writing to this workspace, the store may have been ` +\n `changed outside this coordinator, or this coordinator could not load ` +\n `the workspace when it started. Restart this coordinator to reload.`,\n );\n this.name = \"StoreConflictError\";\n this.workspace = workspace;\n this.held = held;\n this.base = base;\n }\n\n /**\n * Matched by name, so `instanceof` holds across the package's entry points,\n * each of which is bundled with its own copy of this class.\n */\n static [Symbol.hasInstance](value: unknown): boolean {\n return value instanceof Error && value.name === \"StoreConflictError\";\n }\n}\n\nexport interface Store {\n /** Load all workspace records. Called once when the Coordinator starts. */\n load(): Promise<WorkspaceRecord[]> | WorkspaceRecord[];\n\n /**\n * Replace what the store holds for one workspace. The Coordinator calls it\n * after every write when the store has no `apply`.\n */\n save(record: WorkspaceRecord): Promise<void> | void;\n\n /**\n * Write one change. Optional; when present the Coordinator calls it in\n * place of `save`. It must be atomic: the whole change is written, or\n * nothing is. The change refers to the Coordinator's live objects, so\n * serialise or copy everything before returning, including in an async\n * store. Refuse a change whose `base` differs from the entries held, unless\n * `replace` is set, by throwing (or rejecting with) a StoreConflictError.\n */\n apply?(change: StoreChange): Promise<void> | void;\n\n /** Remove one workspace. */\n delete(id: string): Promise<void> | void;\n\n /** Release resources. Idempotent. */\n close?(): Promise<void> | void;\n}\n\nfunction isCollection(name: string): boolean {\n return (COLLECTIONS as readonly string[]).includes(name);\n}\n\n/** The key an object is stored under in its collection. */\nexport function objectKey(collection: string, item: unknown): string | undefined {\n if (item === null || typeof item !== \"object\") return undefined;\n const o = item as Record<string, unknown>;\n const key = collection === \"members\" ? (o.uri ?? o.id) : (o.id ?? o.uri);\n return typeof key === \"string\" ? key : undefined;\n}\n\n/**\n * A collection as a snapshot holds it, keyed. The TypeScript snapshot holds\n * arrays and the Python snapshot holds objects keyed by id; both are read.\n */\nfunction keyed(collection: string, value: unknown): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n if (Array.isArray(value)) {\n for (const item of value) {\n const key = objectKey(collection, item);\n if (key !== undefined) out[key] = item;\n }\n } else if (value !== null && typeof value === \"object\") {\n Object.assign(out, value);\n }\n return out;\n}\n\n/** A whole-workspace snapshot as a change that replaces what the store holds. */\nexport function changeFromRecord(record: WorkspaceRecord): StoreChange {\n const data = (record.data !== null && typeof record.data === \"object\"\n ? record.data : {}) as Record<string, unknown>;\n const meta: Record<string, unknown> = {};\n const objects: Record<string, Record<string, unknown>> = {};\n let entries: unknown[] = [];\n for (const [name, value] of Object.entries(data)) {\n if (name === \"audit\") entries = Array.isArray(value) ? value : [];\n else if (name === IDEMPOTENCY_KEYS || isCollection(name)) objects[name] = keyed(name, value);\n else meta[name] = value;\n }\n if (meta.id === undefined) meta.id = record.id;\n return {\n id: record.id, base: 0, version: record.version, updated_at: record.updated_at,\n meta, entries, objects, replace: true,\n };\n}\n\n/**\n * Rows read back from a store, assembled into the snapshot shape\n * `Coordinator.restore()` reads: arrays for the collections, an object for\n * the idempotency keys, and the log in seq order.\n */\nexport function assembleSnapshot(\n meta: Record<string, unknown>,\n entries: unknown[],\n objects: Iterable<[collection: string, key: string, value: unknown]>,\n): Record<string, unknown> {\n const data: Record<string, unknown> = { ...meta };\n for (const name of COLLECTIONS) data[name] = [];\n const keys: Record<string, unknown> = {};\n data[IDEMPOTENCY_KEYS] = keys;\n for (const [collection, key, value] of objects) {\n if (collection === IDEMPOTENCY_KEYS) {\n keys[key] = value;\n } else {\n const list = (Array.isArray(data[collection]) ? data[collection] : (data[collection] = [])) as unknown[];\n list.push(value);\n }\n }\n data.audit = entries;\n return data;\n}\n\ninterface Held {\n meta: string;\n version: number;\n updated_at: string;\n entries: string[];\n objects: Map<string, Map<string, string>>;\n}\n\n/**\n * In-memory store. The default: it keeps nothing across processes. Like a\n * database it keeps each row as JSON text, so a Coordinator restarted on it\n * sees what was written and nothing changed later.\n */\nexport class MemoryStore implements Store {\n private held = new Map<string, Held>();\n\n load(): WorkspaceRecord[] {\n const out: WorkspaceRecord[] = [];\n for (const [id, h] of this.held) {\n const objects: Array<[string, string, unknown]> = [];\n for (const [collection, items] of h.objects) {\n for (const [key, value] of items) objects.push([collection, key, JSON.parse(value)]);\n }\n out.push({\n id,\n data: assembleSnapshot(JSON.parse(h.meta), h.entries.map(e => JSON.parse(e)), objects),\n version: h.version,\n updated_at: h.updated_at,\n });\n }\n return out;\n }\n\n save(record: WorkspaceRecord): void {\n this.apply(changeFromRecord(record));\n }\n\n apply(change: StoreChange): void {\n // Serialised before anything is held, so a change that cannot be written\n // leaves the store as it was.\n const meta = JSON.stringify(change.meta);\n const entries = change.entries.map(e => JSON.stringify(e));\n const objects = Object.entries(change.objects).map(([collection, items]) =>\n [collection, Object.entries(items).map(([key, value]) =>\n [key, value === null || value === undefined ? null : JSON.stringify(value)] as const)] as const);\n let h = this.held.get(change.id);\n if (!change.replace) {\n const holds = h ? h.entries.length : 0;\n if (holds !== change.base) throw new StoreConflictError(change.id, holds, change.base);\n }\n if (!h || change.replace) {\n h = { meta: \"{}\", version: 0, updated_at: \"\", entries: [], objects: new Map() };\n }\n for (const entry of entries) h.entries.push(entry);\n for (const [collection, items] of objects) {\n let held = h.objects.get(collection);\n if (!held) h.objects.set(collection, (held = new Map()));\n for (const [key, value] of items) {\n if (value === null) held.delete(key);\n else held.set(key, value);\n }\n }\n h.meta = meta;\n h.version = change.version;\n h.updated_at = change.updated_at;\n this.held.set(change.id, h);\n }\n\n delete(id: string): void {\n this.held.delete(id);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/storage/store.ts"],"sourcesContent":["/**\n * Pluggable persistence for the Coordinator.\n *\n * A Store keeps workspace state between process restarts. The Coordinator\n * calls `store.load()` once at start, and writes after every call that adds\n * to a workspace's audit log.\n *\n * How the Coordinator writes depends on what the store implements:\n *\n * - `apply(change)`: the Coordinator sends what the call changed: the new\n * audit entries, the objects it touched and the workspace's own fields.\n * The cost of a write stays flat as the log grows; it grows with the\n * objects a call changes, such as a task with a long history. MemoryStore\n * and SqliteStore implement it.\n * - `save(record)` alone: the Coordinator sends the whole workspace on every\n * write, as releases before 0.3.2 did, so the cost of a write grows with\n * the log. Stores written for earlier releases keep working this way.\n *\n * MemoryStore is the default and keeps nothing across processes. SqliteStore\n * keeps state in a SQLite file through better-sqlite3. Third-party stores\n * (Postgres, Redis and so on) implement this interface.\n *\n * Concurrency model\n * -----------------\n *\n * A Coordinator is a **single-writer** component: dispatch is serial,\n * workspace state is held in memory, and each chain link is computed from the\n * head the instance holds. Run one Coordinator per workspace.\n *\n * Every change names the number of audit entries the Coordinator expects the\n * store to hold already (`base`). MemoryStore and SqliteStore refuse a change\n * whose base differs from what they hold: they throw a StoreConflictError and\n * write nothing. A second Coordinator writing the same workspace is refused,\n * and the entries already stored stay as they are.\n *\n * A store that also implements `version` and `loadOne` lets a Coordinator\n * keep up with another writer: before each call it compares the entries the\n * store holds for the call's workspace with the entries it last wrote, and\n * reloads the workspace when they differ. A synchronous write that fails or\n * conflicts answers its call with -32603 and `data: {reason: \"store\"}`; the\n * Coordinator then reloads the workspace, and refuses calls on it the same\n * way until a reload succeeds. An asynchronous write that fails is reported,\n * and the next write carries its change again.\n *\n * `save` makes no such check: it replaces what the store holds for the\n * workspace. A store that implements only `save` therefore still loses\n * entries silently when two instances write one workspace.\n *\n * To run more than one instance on a store without `version`, partition\n * workspaces so that no workspace is ever written by two, or hold an\n * external lock. See SPECIFICATION.md 10.3.\n *\n * Production deployments that need rich queries over the log can project it\n * into their own analytics store with the `onAudit` listener.\n */\n\n/** The workspace maps a store keeps object by object. */\nexport const COLLECTIONS = [\n \"members\", \"tasks\", \"overrides\", \"whispers\",\n \"deliberations\", \"handoffs\", \"snapshots\", \"route_decisions\", \"invitations\",\n] as const;\n\n/** task.create idempotency keys, kept as one more collection of key to task id. */\nexport const IDEMPOTENCY_KEYS = \"idempotency_keys\";\n\nexport interface WorkspaceRecord {\n /** Workspace identifier. */\n id: string;\n /** JSON-serialisable payload in the shape `Coordinator.snapshot()` produces. */\n data: unknown;\n /**\n * For `save`, a counter the Coordinator increments on each write. For a\n * record a store loads after `apply`, the number of audit entries. Recorded\n * for diagnostics; `save` does not check it.\n */\n version: number;\n /** Wall-clock timestamp of the last write, ISO-8601. */\n updated_at: string;\n}\n\n/** One write to one workspace: what one or more calls changed. */\nexport interface StoreChange {\n /** Workspace identifier. */\n id: string;\n /**\n * Audit entries the store must already hold for this workspace. A store\n * that holds a different number refuses the change with a\n * StoreConflictError and writes nothing. Not checked when `replace` is set.\n */\n base: number;\n /** Audit entries the store holds after the change: `base + entries.length`. */\n version: number;\n /** Wall-clock timestamp of the write, ISO-8601. */\n updated_at: string;\n /** The workspace's own fields: everything except its collections and log. */\n meta: Record<string, unknown>;\n /** New audit entries in order. The first has seq `base`. */\n entries: unknown[];\n /**\n * Objects written, by collection and then by key. A null value removes the\n * object. The collections are those in COLLECTIONS and IDEMPOTENCY_KEYS.\n */\n objects: Record<string, Record<string, unknown>>;\n /**\n * Replace everything the store holds for this workspace with this change,\n * which then carries the whole workspace from seq 0.\n */\n replace?: boolean;\n}\n\n/**\n * A change whose base does not match the audit entries the store holds.\n * Usually a second Coordinator writing the same workspace.\n */\nexport class StoreConflictError extends Error {\n readonly workspace: string;\n /** Audit entries the store holds. */\n readonly held: number;\n /** Audit entries the change expected. */\n readonly base: number;\n\n constructor(workspace: string, held: number, base: number) {\n super(\n `The store holds ${held} audit entries for workspace ${workspace} and ` +\n `this write expected ${base}, so nothing was written. Another ` +\n `coordinator may be writing to this workspace, or the store may have ` +\n `been changed outside this coordinator. A coordinator reloads the ` +\n `workspace and answers the call as failed, and the call may be sent ` +\n `again.`,\n );\n this.name = \"StoreConflictError\";\n this.workspace = workspace;\n this.held = held;\n this.base = base;\n }\n\n /**\n * Matched by name, so `instanceof` holds across the package's entry points,\n * each of which is bundled with its own copy of this class.\n */\n static [Symbol.hasInstance](value: unknown): boolean {\n return value instanceof Error && value.name === \"StoreConflictError\";\n }\n}\n\nexport interface Store {\n /** Load all workspace records. Called once when the Coordinator starts. */\n load(): Promise<WorkspaceRecord[]> | WorkspaceRecord[];\n\n /**\n * Replace what the store holds for one workspace. The Coordinator calls it\n * after every write when the store has no `apply`.\n */\n save(record: WorkspaceRecord): Promise<void> | void;\n\n /**\n * Write one change. Optional; when present the Coordinator calls it in\n * place of `save`. It must be atomic: the whole change is written, or\n * nothing is. The change refers to the Coordinator's live objects, so\n * serialise or copy everything before returning, including in an async\n * store. Refuse a change whose `base` differs from the entries held, unless\n * `replace` is set, by throwing (or rejecting with) a StoreConflictError.\n */\n apply?(change: StoreChange): Promise<void> | void;\n\n /** Remove one workspace. */\n delete(id: string): Promise<void> | void;\n\n /**\n * The number of audit entries the store holds for one workspace, or\n * undefined when it holds none. Optional; see the concurrency model above.\n */\n version?(id: string): number | undefined;\n\n /** One workspace's record, or undefined when the store holds none. Optional. */\n loadOne?(id: string): WorkspaceRecord | undefined;\n\n /** Release resources. Idempotent. */\n close?(): Promise<void> | void;\n}\n\nfunction isCollection(name: string): boolean {\n return (COLLECTIONS as readonly string[]).includes(name);\n}\n\n/** The key an object is stored under in its collection. */\nexport function objectKey(collection: string, item: unknown): string | undefined {\n if (item === null || typeof item !== \"object\") return undefined;\n const o = item as Record<string, unknown>;\n const key = collection === \"members\" ? (o.uri ?? o.id) : (o.id ?? o.uri);\n return typeof key === \"string\" ? key : undefined;\n}\n\n/**\n * A collection as a snapshot holds it, keyed. The TypeScript snapshot holds\n * arrays and the Python snapshot holds objects keyed by id; both are read.\n */\nfunction keyed(collection: string, value: unknown): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n if (Array.isArray(value)) {\n for (const item of value) {\n const key = objectKey(collection, item);\n if (key !== undefined) out[key] = item;\n }\n } else if (value !== null && typeof value === \"object\") {\n Object.assign(out, value);\n }\n return out;\n}\n\n/** A whole-workspace snapshot as a change that replaces what the store holds. */\nexport function changeFromRecord(record: WorkspaceRecord): StoreChange {\n const data = (record.data !== null && typeof record.data === \"object\"\n ? record.data : {}) as Record<string, unknown>;\n const meta: Record<string, unknown> = {};\n const objects: Record<string, Record<string, unknown>> = {};\n let entries: unknown[] = [];\n for (const [name, value] of Object.entries(data)) {\n if (name === \"audit\") entries = Array.isArray(value) ? value : [];\n else if (name === IDEMPOTENCY_KEYS || isCollection(name)) objects[name] = keyed(name, value);\n else meta[name] = value;\n }\n if (meta.id === undefined) meta.id = record.id;\n return {\n id: record.id, base: 0, version: record.version, updated_at: record.updated_at,\n meta, entries, objects, replace: true,\n };\n}\n\n/**\n * Rows read back from a store, assembled into the snapshot shape\n * `Coordinator.restore()` reads: arrays for the collections, an object for\n * the idempotency keys, and the log in seq order.\n */\nexport function assembleSnapshot(\n meta: Record<string, unknown>,\n entries: unknown[],\n objects: Iterable<[collection: string, key: string, value: unknown]>,\n): Record<string, unknown> {\n const data: Record<string, unknown> = { ...meta };\n for (const name of COLLECTIONS) data[name] = [];\n const keys: Record<string, unknown> = {};\n data[IDEMPOTENCY_KEYS] = keys;\n for (const [collection, key, value] of objects) {\n if (collection === IDEMPOTENCY_KEYS) {\n keys[key] = value;\n } else {\n const list = (Array.isArray(data[collection]) ? data[collection] : (data[collection] = [])) as unknown[];\n list.push(value);\n }\n }\n data.audit = entries;\n return data;\n}\n\ninterface Held {\n meta: string;\n version: number;\n updated_at: string;\n entries: string[];\n objects: Map<string, Map<string, string>>;\n}\n\n/**\n * In-memory store. The default: it keeps nothing across processes. Like a\n * database it keeps each row as JSON text, so a Coordinator restarted on it\n * sees what was written and nothing changed later.\n */\nexport class MemoryStore implements Store {\n private held = new Map<string, Held>();\n\n load(): WorkspaceRecord[] {\n return [...this.held.keys()].map((id) => this.loadOne(id)!);\n }\n\n loadOne(id: string): WorkspaceRecord | undefined {\n const h = this.held.get(id);\n if (!h) return undefined;\n const objects: Array<[string, string, unknown]> = [];\n for (const [collection, items] of h.objects) {\n for (const [key, value] of items) objects.push([collection, key, JSON.parse(value)]);\n }\n return {\n id,\n data: assembleSnapshot(JSON.parse(h.meta), h.entries.map(e => JSON.parse(e)), objects),\n version: h.version,\n updated_at: h.updated_at,\n };\n }\n\n version(id: string): number | undefined {\n return this.held.get(id)?.entries.length;\n }\n\n save(record: WorkspaceRecord): void {\n this.apply(changeFromRecord(record));\n }\n\n apply(change: StoreChange): void {\n // Serialised before anything is held, so a change that cannot be written\n // leaves the store as it was.\n const meta = JSON.stringify(change.meta);\n const entries = change.entries.map(e => JSON.stringify(e));\n const objects = Object.entries(change.objects).map(([collection, items]) =>\n [collection, Object.entries(items).map(([key, value]) =>\n [key, value === null || value === undefined ? null : JSON.stringify(value)] as const)] as const);\n let h = this.held.get(change.id);\n if (!change.replace) {\n const holds = h ? h.entries.length : 0;\n if (holds !== change.base) throw new StoreConflictError(change.id, holds, change.base);\n }\n if (!h || change.replace) {\n h = { meta: \"{}\", version: 0, updated_at: \"\", entries: [], objects: new Map() };\n }\n for (const entry of entries) h.entries.push(entry);\n for (const [collection, items] of objects) {\n let held = h.objects.get(collection);\n if (!held) h.objects.set(collection, (held = new Map()));\n for (const [key, value] of items) {\n if (value === null) held.delete(key);\n else held.set(key, value);\n }\n }\n h.meta = meta;\n h.version = change.version;\n h.updated_at = change.updated_at;\n this.held.set(change.id, h);\n }\n\n delete(id: string): void {\n this.held.delete(id);\n }\n}\n"],"mappings":"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","names":[]}
|