@earendil-works/pi-durable 0.87.1 → 0.99.0
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 +46 -4
- package/dist/documents.d.ts +66 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +98 -0
- package/dist/documents.js.map +1 -0
- package/dist/entries.d.ts +13 -0
- package/dist/entries.d.ts.map +1 -0
- package/dist/entries.js +18 -0
- package/dist/entries.js.map +1 -0
- package/dist/env/index.d.ts +131 -0
- package/dist/env/index.d.ts.map +1 -0
- package/dist/env/index.js +45 -0
- package/dist/env/index.js.map +1 -0
- package/dist/env/node.d.ts +45 -0
- package/dist/env/node.d.ts.map +1 -0
- package/dist/env/node.js +908 -0
- package/dist/env/node.js.map +1 -0
- package/dist/env/utils/adaptive-publisher.d.ts +24 -0
- package/dist/env/utils/adaptive-publisher.d.ts.map +1 -0
- package/dist/env/utils/adaptive-publisher.js +79 -0
- package/dist/env/utils/adaptive-publisher.js.map +1 -0
- package/dist/env/utils/output-capture.d.ts +31 -0
- package/dist/env/utils/output-capture.d.ts.map +1 -0
- package/dist/env/utils/output-capture.js +238 -0
- package/dist/env/utils/output-capture.js.map +1 -0
- package/dist/env/utils/truncate.d.ts +71 -0
- package/dist/env/utils/truncate.d.ts.map +1 -0
- package/dist/env/utils/truncate.js +294 -0
- package/dist/env/utils/truncate.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/harness/config.d.ts +20 -0
- package/dist/harness/config.d.ts.map +1 -0
- package/dist/harness/config.js +18 -0
- package/dist/harness/config.js.map +1 -0
- package/dist/harness/context.d.ts +27 -0
- package/dist/harness/context.d.ts.map +1 -0
- package/dist/harness/context.js +109 -0
- package/dist/harness/context.js.map +1 -0
- package/dist/harness/generation.d.ts +36 -0
- package/dist/harness/generation.d.ts.map +1 -0
- package/dist/harness/generation.js +266 -0
- package/dist/harness/generation.js.map +1 -0
- package/dist/harness/harness.d.ts +10 -0
- package/dist/harness/harness.d.ts.map +1 -0
- package/dist/harness/harness.js +266 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/live.d.ts +40 -0
- package/dist/harness/live.d.ts.map +1 -0
- package/dist/harness/live.js +41 -0
- package/dist/harness/live.js.map +1 -0
- package/dist/harness/prompt.d.ts +24 -0
- package/dist/harness/prompt.d.ts.map +1 -0
- package/dist/harness/prompt.js +88 -0
- package/dist/harness/prompt.js.map +1 -0
- package/dist/harness/registry.d.ts +7 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +309 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/scheduler.d.ts +63 -0
- package/dist/harness/scheduler.d.ts.map +1 -0
- package/dist/harness/scheduler.js +605 -0
- package/dist/harness/scheduler.js.map +1 -0
- package/dist/harness/submissions.d.ts +24 -0
- package/dist/harness/submissions.d.ts.map +1 -0
- package/dist/harness/submissions.js +133 -0
- package/dist/harness/submissions.js.map +1 -0
- package/dist/harness/types.d.ts +358 -0
- package/dist/harness/types.d.ts.map +1 -0
- package/dist/harness/types.js +2 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/util.d.ts +14 -0
- package/dist/harness/util.d.ts.map +1 -0
- package/dist/harness/util.js +55 -0
- package/dist/harness/util.js.map +1 -0
- package/dist/ids.d.ts +6 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +9 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/session/forks.d.ts +10 -0
- package/dist/session/forks.d.ts.map +1 -0
- package/dist/session/forks.js +42 -0
- package/dist/session/forks.js.map +1 -0
- package/dist/session/observation.d.ts +28 -0
- package/dist/session/observation.d.ts.map +1 -0
- package/dist/session/observation.js +280 -0
- package/dist/session/observation.js.map +1 -0
- package/dist/session/session.d.ts +59 -0
- package/dist/session/session.d.ts.map +1 -0
- package/dist/session/session.js +380 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/transaction.d.ts +105 -0
- package/dist/session/transaction.d.ts.map +1 -0
- package/dist/session/transaction.js +718 -0
- package/dist/session/transaction.js.map +1 -0
- package/dist/storage/jsonl/index.d.ts +2 -0
- package/dist/storage/jsonl/index.d.ts.map +1 -0
- package/dist/storage/jsonl/index.js +2 -0
- package/dist/storage/jsonl/index.js.map +1 -0
- package/dist/storage/jsonl/node.d.ts +6 -0
- package/dist/storage/jsonl/node.d.ts.map +1 -0
- package/dist/storage/jsonl/node.js +8 -0
- package/dist/storage/jsonl/node.js.map +1 -0
- package/dist/storage/jsonl/storage.d.ts +115 -0
- package/dist/storage/jsonl/storage.d.ts.map +1 -0
- package/dist/storage/jsonl/storage.js +681 -0
- package/dist/storage/jsonl/storage.js.map +1 -0
- package/dist/storage/memory.d.ts +31 -14
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +208 -61
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/sqlite/database.d.ts +5 -1
- package/dist/storage/sqlite/database.d.ts.map +1 -1
- package/dist/storage/sqlite/database.js.map +1 -1
- package/dist/storage/sqlite/index.d.ts.map +1 -1
- package/dist/storage/sqlite/migrations.d.ts +1 -1
- package/dist/storage/sqlite/migrations.d.ts.map +1 -1
- package/dist/storage/sqlite/migrations.js +8 -1
- package/dist/storage/sqlite/migrations.js.map +1 -1
- package/dist/storage/sqlite/node.d.ts.map +1 -1
- package/dist/storage/sqlite/node.js +2 -2
- package/dist/storage/sqlite/node.js.map +1 -1
- package/dist/storage/sqlite/storage.d.ts +20 -14
- package/dist/storage/sqlite/storage.d.ts.map +1 -1
- package/dist/storage/sqlite/storage.js +159 -55
- package/dist/storage/sqlite/storage.js.map +1 -1
- package/dist/tasks.d.ts +6 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +5 -0
- package/dist/tasks.js.map +1 -0
- package/dist/testing/assertions.d.ts +5 -0
- package/dist/testing/assertions.d.ts.map +1 -0
- package/dist/testing/assertions.js +25 -0
- package/dist/testing/assertions.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/runner.d.ts +10 -0
- package/dist/testing/runner.d.ts.map +1 -0
- package/dist/testing/runner.js +14 -0
- package/dist/testing/runner.js.map +1 -0
- package/dist/testing/storage-benchmark.d.ts +41 -0
- package/dist/testing/storage-benchmark.d.ts.map +1 -0
- package/dist/testing/storage-benchmark.js +351 -0
- package/dist/testing/storage-benchmark.js.map +1 -0
- package/dist/testing/storage-conformance.d.ts +4 -0
- package/dist/testing/storage-conformance.d.ts.map +1 -0
- package/dist/testing/storage-conformance.js +1122 -0
- package/dist/testing/storage-conformance.js.map +1 -0
- package/dist/testing/types.d.ts +19 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/types.d.ts +422 -54
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/chord-delta-findings.md +1 -1
- package/docs/pico-v5-chord-usage.md +47 -60
- package/docs/pico-v5-handoff.md +469 -264
- package/docs/pico-v5.md +1523 -639
- package/package.json +30 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/storage/sqlite/storage.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Context, JsonValue } from \"@earendil-works/chord\";\nimport { applyImmutable, type Op } from \"@earendil-works/chord/delta\";\nimport type {\n\tConversationRecord,\n\tCursor,\n\tDocumentAddress,\n\tDocumentContent,\n\tDocumentCreate,\n\tDocumentPoint,\n\tDocumentQuery,\n\tDocumentRecord,\n\tEntryQuery,\n\tEntryRecord,\n\tId,\n\tJsonObject,\n\tPage,\n\tSeq,\n\tStorage,\n\tStorageWrite,\n\tStoredDocument,\n\tSubmissionRecord,\n\tTaskQuery,\n\tTaskRecord,\n} from \"../../types.ts\";\nimport type { SqliteDatabase, SqliteStatement, SqliteValue } from \"./database.ts\";\nimport { applySqliteMigrations } from \"./migrations.ts\";\n\ntype StoredTask = TaskRecord<JsonValue, JsonValue, JsonValue>;\ntype TableName = \"conversation\" | \"entry\" | \"task\" | \"submission\" | \"document\";\ntype RecordIdRow = { readonly record_type: TableName };\ntype JsonRow = { readonly record: string };\ntype EntryJsonRow = { readonly record: string; readonly commit_seq: number };\ntype RevisionRow = {\n\treadonly seq: number;\n\treadonly kind: DocumentContent[\"kind\"];\n\treadonly version: number;\n\treadonly content: string;\n};\ntype IdRow = { readonly id: number };\ntype MetadataRow = { readonly next_id: string; readonly next_seq: number };\ntype DocumentAction = {\n\tcreate?: DocumentCreate;\n\tcontent?: DocumentContent;\n\tretire: boolean;\n};\ntype ScopeColumns = {\n\treadonly scopeKind: DocumentRecord[\"scope\"][\"kind\"];\n\treadonly ownerId: number;\n};\n\nconst parseJson = <T>(value: string): T => JSON.parse(value) as T;\nconst encodeJson = (value: unknown): string => JSON.stringify(value) as string;\n// Some SQLite bindings replace lone UTF-16 surrogates. JSON encoding keeps indexed identities lossless.\nconst encodeIndexedString = (value: string): string => JSON.stringify(value);\n\nconst getRow = <T extends object>(statement: SqliteStatement, ...params: SqliteValue[]): T | undefined =>\n\tstatement.get<T>(...params);\n\nconst allRows = <T extends object>(statement: SqliteStatement, ...params: SqliteValue[]): T[] =>\n\tstatement.all<T>(...params);\n\nconst cursorId = (cursor: Cursor | undefined): Id | undefined => cursor?.after as Id | undefined;\n\nconst page = <T extends { readonly id: Id }>(values: readonly T[], limit: number): Page<T, Cursor> => {\n\tconst items = values.slice(0, limit);\n\tif (values.length <= limit) return { items };\n\treturn { items, next: { after: items.at(-1)!.id } };\n};\n\nconst scopeColumns = (scope: DocumentRecord[\"scope\"]): ScopeColumns => {\n\tswitch (scope.kind) {\n\t\tcase \"session\":\n\t\t\treturn { scopeKind: \"session\", ownerId: 0 };\n\t\tcase \"conversation\":\n\t\t\treturn { scopeKind: \"conversation\", ownerId: scope.conversationId };\n\t\tcase \"task\":\n\t\t\treturn { scopeKind: \"task\", ownerId: scope.taskId };\n\t}\n};\n\nconst addressParts = (address: DocumentAddress | DocumentCreate | DocumentRecord) => {\n\tconst scope = scopeColumns(address.scope);\n\treturn {\n\t\tkind: encodeIndexedString(address.kind),\n\t\t...scope,\n\t\tfamily: address.key === undefined ? 0 : 1,\n\t\tkeyValue: encodeIndexedString(address.key ?? \"\"),\n\t};\n};\n\nconst addressKey = (address: DocumentAddress | DocumentCreate | DocumentRecord): string => {\n\tconst parts = addressParts(address);\n\treturn JSON.stringify([parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue]);\n};\n\nconst isAliveAt = (record: DocumentRecord, at: DocumentPoint): boolean => {\n\tif (at === \"current\") return record.retiredAt === undefined;\n\treturn record.createdAt <= at && (record.retiredAt === undefined || at < record.retiredAt);\n};\n\nconst isCurrentOnly = (record: DocumentRecord): boolean =>\n\trecord.scope.kind !== \"conversation\" || record.history === \"latest\";\n\nconst writeId = (write: StorageWrite): Id | undefined => {\n\tswitch (write.type) {\n\t\tcase \"conversation\":\n\t\tcase \"entry\":\n\t\tcase \"task\":\n\t\tcase \"submission\":\n\t\t\treturn write.value.id;\n\t\tcase \"document.create\":\n\t\t\treturn write.record.id;\n\t\tcase \"document.change\":\n\t\tcase \"document.retire\":\n\t\t\treturn undefined;\n\t}\n};\n\nclass StatementCachingDatabase implements SqliteDatabase {\n\tprivate readonly database: SqliteDatabase;\n\tprivate readonly statements = new Map<string, SqliteStatement>();\n\n\tconstructor(database: SqliteDatabase) {\n\t\tthis.database = database;\n\t}\n\n\texec(sql: string): void {\n\t\tthis.database.exec(sql);\n\t}\n\n\tprepare(sql: string): SqliteStatement {\n\t\tlet statement = this.statements.get(sql);\n\t\tif (statement === undefined) {\n\t\t\tstatement = this.database.prepare(sql);\n\t\t\tthis.statements.set(sql, statement);\n\t\t}\n\t\treturn statement;\n\t}\n\n\ttransaction<T>(callback: () => T): T | Promise<T> {\n\t\treturn this.database.transaction(callback);\n\t}\n\n\tclose(): void | Promise<void> {\n\t\tthis.statements.clear();\n\t\treturn this.database.close();\n\t}\n}\n\n/** Portable SQLite implementation of the Pico storage contract. */\nexport class SqliteStorage implements Storage {\n\tprivate readonly db: SqliteDatabase;\n\tprivate nextId: Id;\n\tprivate closed = false;\n\n\tprivate constructor(db: SqliteDatabase, nextId: Id) {\n\t\tthis.db = new StatementCachingDatabase(db);\n\t\tthis.nextId = nextId;\n\t}\n\n\t/** Initialize storage over an owned SQLite database facade. */\n\tstatic async open(db: SqliteDatabase): Promise<SqliteStorage> {\n\t\ttry {\n\t\t\tawait applySqliteMigrations(db);\n\t\t\tconst metadata = getRow<MetadataRow>(\n\t\t\t\tdb.prepare(\"SELECT next_id, next_seq FROM durable_metadata WHERE singleton = 1\"),\n\t\t\t);\n\t\t\tif (metadata === undefined) throw new Error(\"Durable SQLite metadata is missing\");\n\t\t\treturn new SqliteStorage(db, Number(metadata.next_id));\n\t\t} catch (error) {\n\t\t\ttry {\n\t\t\t\tawait db.close();\n\t\t\t} catch {\n\t\t\t\t// Preserve the initialization failure.\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync commit(writes: readonly StorageWrite[], _context: Context): Promise<Seq> {\n\t\tthis.assertOpen();\n\t\tconst documentActions = this.prepareDocumentActions(writes);\n\t\tconst candidateNextId = this.candidateNextId(writes);\n\t\tconst seq = await this.db.transaction(() => {\n\t\t\tconst metadata = getRow<MetadataRow>(\n\t\t\t\tthis.db.prepare(\"SELECT next_id, next_seq FROM durable_metadata WHERE singleton = 1\"),\n\t\t\t);\n\t\t\tif (metadata === undefined) throw new Error(\"Durable SQLite metadata is missing\");\n\t\t\tconst committedSeq = metadata.next_seq;\n\t\t\tthis.checkGlobalIds(writes);\n\t\t\tthis.checkDocumentActions(documentActions);\n\t\t\tfor (const write of writes) this.applyTableWrite(write, committedSeq);\n\t\t\tthis.applyDocumentActions(documentActions, committedSeq);\n\t\t\tthis.db\n\t\t\t\t.prepare(\"UPDATE durable_metadata SET next_id = ?, next_seq = ? WHERE singleton = 1\")\n\t\t\t\t.run(String(Math.max(Number(metadata.next_id), candidateNextId)), committedSeq + 1);\n\t\t\treturn committedSeq;\n\t\t});\n\t\tthis.nextId = Math.max(this.nextId, candidateNextId);\n\t\treturn seq;\n\t}\n\n\tasync mintId(): Promise<Id> {\n\t\tthis.assertOpen();\n\t\tif (!Number.isSafeInteger(this.nextId)) throw new Error(\"ID space is exhausted\");\n\t\treturn this.nextId++;\n\t}\n\n\tasync conversation(id: Id, _context: Context): Promise<ConversationRecord | undefined> {\n\t\tthis.assertOpen();\n\t\tconst row = getRow<JsonRow>(this.db.prepare(\"SELECT record FROM conversations WHERE id = ?\"), id);\n\t\treturn row === undefined ? undefined : parseJson<ConversationRecord>(row.record);\n\t}\n\n\tasync scanConversations(\n\t\tcursor: Cursor | undefined,\n\t\tlimit: number,\n\t\t_context: Context,\n\t): Promise<Page<ConversationRecord, Cursor>> {\n\t\tthis.assertOpen();\n\t\tconst after = cursorId(cursor) ?? -1;\n\t\tconst rows = allRows<JsonRow>(\n\t\t\tthis.db.prepare(\"SELECT record FROM conversations WHERE id > ? ORDER BY id LIMIT ?\"),\n\t\t\tafter,\n\t\t\tlimit + 1,\n\t\t);\n\t\treturn page(\n\t\t\trows.map((row) => parseJson<ConversationRecord>(row.record)),\n\t\t\tlimit,\n\t\t);\n\t}\n\n\tasync entry(\n\t\tid: Id,\n\t\t_context: Context,\n\t): Promise<{ readonly entry: EntryRecord; readonly commitSeq: Seq } | undefined> {\n\t\tthis.assertOpen();\n\t\tconst row = getRow<EntryJsonRow>(this.db.prepare(\"SELECT record, commit_seq FROM entries WHERE id = ?\"), id);\n\t\treturn row === undefined ? undefined : { entry: parseJson<EntryRecord>(row.record), commitSeq: row.commit_seq };\n\t}\n\n\tasync findLatestHeadMarker(\n\t\tconversationId: Id,\n\t\tatOrBeforeEntryId: Id | undefined,\n\t\t_context: Context,\n\t): Promise<(EntryRecord & { readonly head: Id }) | undefined> {\n\t\tthis.assertOpen();\n\t\tlet conversation = this.readConversation(conversationId);\n\t\tif (conversation === undefined) throw new Error(`Unknown conversation: ${conversationId}`);\n\t\tlet upper = atOrBeforeEntryId;\n\t\twhile (true) {\n\t\t\tconst row =\n\t\t\t\tupper === undefined\n\t\t\t\t\t? getRow<JsonRow>(\n\t\t\t\t\t\t\tthis.db.prepare(\n\t\t\t\t\t\t\t\t\"SELECT record FROM entries WHERE conversation_id = ? AND head IS NOT NULL ORDER BY id DESC LIMIT 1\",\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tconversation.id,\n\t\t\t\t\t\t)\n\t\t\t\t\t: getRow<JsonRow>(\n\t\t\t\t\t\t\tthis.db.prepare(\n\t\t\t\t\t\t\t\t\"SELECT record FROM entries WHERE conversation_id = ? AND head IS NOT NULL AND id <= ? ORDER BY id DESC LIMIT 1\",\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tconversation.id,\n\t\t\t\t\t\t\tupper,\n\t\t\t\t\t\t);\n\t\t\tif (row !== undefined) return parseJson<EntryRecord & { readonly head: Id }>(row.record);\n\t\t\tif (conversation.parent === undefined) return undefined;\n\t\t\tupper = upper === undefined ? conversation.parent.at : Math.min(upper, conversation.parent.at);\n\t\t\tconversation = this.readConversation(conversation.parent.conversationId)!;\n\t\t}\n\t}\n\n\tasync scanEntries(\n\t\tquery: EntryQuery,\n\t\tcursor: Cursor | undefined,\n\t\tlimit: number,\n\t\t_context: Context,\n\t): Promise<Page<EntryRecord, Cursor>> {\n\t\tthis.assertOpen();\n\t\tlet conversation = this.readConversation(query.conversationId);\n\t\tif (conversation === undefined) throw new Error(`Unknown conversation: ${query.conversationId}`);\n\t\tconst after = cursorId(cursor);\n\t\tlet upper = query.maxEntryId;\n\t\tif (after !== undefined) upper = Math.min(upper ?? Number.MAX_SAFE_INTEGER, after - 1);\n\t\tconst values: EntryRecord[] = [];\n\t\twhile (true) {\n\t\t\tconst clauses = [\"conversation_id = ?\"];\n\t\t\tconst params: SqliteValue[] = [conversation.id];\n\t\t\tif (query.minEntryId !== undefined) {\n\t\t\t\tclauses.push(\"id >= ?\");\n\t\t\t\tparams.push(query.minEntryId);\n\t\t\t}\n\t\t\tif (upper !== undefined) {\n\t\t\t\tclauses.push(\"id <= ?\");\n\t\t\t\tparams.push(upper);\n\t\t\t}\n\t\t\tparams.push(limit + 1 - values.length);\n\t\t\tconst rows = allRows<JsonRow>(\n\t\t\t\tthis.db.prepare(`SELECT record FROM entries WHERE ${clauses.join(\" AND \")} ORDER BY id DESC LIMIT ?`),\n\t\t\t\t...params,\n\t\t\t);\n\t\t\tvalues.push(...rows.map((row) => parseJson<EntryRecord>(row.record)));\n\t\t\tif (values.length > limit || conversation.parent === undefined) break;\n\t\t\tupper = upper === undefined ? conversation.parent.at : Math.min(upper, conversation.parent.at);\n\t\t\tif (query.minEntryId !== undefined && upper < query.minEntryId) break;\n\t\t\tconversation = this.readConversation(conversation.parent.conversationId)!;\n\t\t}\n\t\treturn page(values, limit);\n\t}\n\n\tasync task(id: Id, _context: Context): Promise<StoredTask | undefined> {\n\t\tthis.assertOpen();\n\t\tconst row = getRow<JsonRow>(this.db.prepare(\"SELECT record FROM tasks WHERE id = ?\"), id);\n\t\treturn row === undefined ? undefined : parseJson<StoredTask>(row.record);\n\t}\n\n\tasync scanTasks(\n\t\tquery: TaskQuery,\n\t\tcursor: Cursor | undefined,\n\t\tlimit: number,\n\t\t_context: Context,\n\t): Promise<Page<StoredTask, Cursor>> {\n\t\tthis.assertOpen();\n\t\tconst clauses = [\"id > ?\"];\n\t\tconst params: SqliteValue[] = [cursorId(cursor) ?? -1];\n\t\tif (query.conversationId !== undefined) {\n\t\t\tclauses.push(\"conversation_id = ?\");\n\t\t\tparams.push(query.conversationId);\n\t\t}\n\t\tif (query.kind !== undefined) {\n\t\t\tclauses.push(\"kind = ?\");\n\t\t\tparams.push(encodeIndexedString(query.kind));\n\t\t}\n\t\tif (query.status !== undefined) {\n\t\t\tclauses.push(\"status = ?\");\n\t\t\tparams.push(query.status);\n\t\t}\n\t\tif (query.abortRequested !== undefined) {\n\t\t\tclauses.push(\"abort_requested = ?\");\n\t\t\tparams.push(query.abortRequested ? 1 : 0);\n\t\t}\n\t\tif (query.background !== undefined) {\n\t\t\tclauses.push(\"background = ?\");\n\t\t\tparams.push(query.background ? 1 : 0);\n\t\t}\n\t\tparams.push(limit + 1);\n\t\tconst rows = allRows<JsonRow>(\n\t\t\tthis.db.prepare(`SELECT record FROM tasks WHERE ${clauses.join(\" AND \")} ORDER BY id LIMIT ?`),\n\t\t\t...params,\n\t\t);\n\t\treturn page(\n\t\t\trows.map((row) => parseJson<StoredTask>(row.record)),\n\t\t\tlimit,\n\t\t);\n\t}\n\n\tasync submission(id: Id, _context: Context): Promise<SubmissionRecord | undefined> {\n\t\tthis.assertOpen();\n\t\tconst row = getRow<JsonRow>(this.db.prepare(\"SELECT record FROM submissions WHERE id = ?\"), id);\n\t\treturn row === undefined ? undefined : parseJson<SubmissionRecord>(row.record);\n\t}\n\n\tasync submissionByRequest(\n\t\tconversationId: Id,\n\t\trequestId: string,\n\t\t_context: Context,\n\t): Promise<SubmissionRecord | undefined> {\n\t\tthis.assertOpen();\n\t\tconst row = getRow<JsonRow>(\n\t\t\tthis.db.prepare(\"SELECT record FROM submissions WHERE conversation_id = ? AND request_id = ?\"),\n\t\t\tconversationId,\n\t\t\tencodeIndexedString(requestId),\n\t\t);\n\t\treturn row === undefined ? undefined : parseJson<SubmissionRecord>(row.record);\n\t}\n\n\tasync findDocument(\n\t\taddress: DocumentAddress,\n\t\tat: DocumentPoint,\n\t\t_context: Context,\n\t): Promise<DocumentRecord | undefined> {\n\t\tthis.assertOpen();\n\t\tconst parts = addressParts(address);\n\t\tconst statement =\n\t\t\tat === \"current\"\n\t\t\t\t? this.db.prepare(`SELECT record FROM documents\n\t\t\t\t\tWHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ?\n\t\t\t\t\tAND retired_at IS NULL ORDER BY created_at DESC LIMIT 1`)\n\t\t\t\t: this.db.prepare(`SELECT record FROM documents\n\t\t\t\t\tWHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ?\n\t\t\t\t\tAND created_at <= ? AND (retired_at IS NULL OR retired_at > ?)\n\t\t\t\t\tORDER BY created_at DESC LIMIT 1`);\n\t\tconst params: SqliteValue[] = [parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue];\n\t\tif (at !== \"current\") params.push(at, at);\n\t\tconst row = getRow<JsonRow>(statement, ...params);\n\t\treturn row === undefined ? undefined : parseJson<DocumentRecord>(row.record);\n\t}\n\n\tasync document(id: Id, at: DocumentPoint, _context: Context): Promise<StoredDocument | undefined> {\n\t\tthis.assertOpen();\n\t\tconst row = getRow<JsonRow>(this.db.prepare(\"SELECT record FROM documents WHERE id = ?\"), id);\n\t\tif (row === undefined) return undefined;\n\t\tconst record = parseJson<DocumentRecord>(row.record);\n\t\tif (at !== \"current\" && isCurrentOnly(record)) {\n\t\t\tthrow new Error(`Document ${id} does not retain historical content`);\n\t\t}\n\t\tif (!isAliveAt(record, at)) return undefined;\n\t\tconst upper = at === \"current\" ? Number.MAX_SAFE_INTEGER : at;\n\t\tconst base = getRow<RevisionRow>(\n\t\t\tthis.db.prepare(`SELECT seq, kind, version, content FROM document_revisions\n\t\t\t\tWHERE document_id = ? AND kind = 'base' AND seq <= ? ORDER BY seq DESC LIMIT 1`),\n\t\t\tid,\n\t\t\tupper,\n\t\t);\n\t\tif (base === undefined) throw new Error(`Document ${id} is missing a required base`);\n\t\tlet value = parseJson<JsonObject>(base.content);\n\t\tconst tail = allRows<RevisionRow>(\n\t\t\tthis.db.prepare(`SELECT seq, kind, version, content FROM document_revisions\n\t\t\t\tWHERE document_id = ? AND seq > ? AND seq <= ? ORDER BY seq`),\n\t\t\tid,\n\t\t\tbase.seq,\n\t\t\tupper,\n\t\t);\n\t\tfor (const revision of tail) {\n\t\t\tif (revision.kind !== \"delta\" || revision.version !== base.version) {\n\t\t\t\tthrow new Error(`Document ${id} crosses a stored version boundary without a base`);\n\t\t\t}\n\t\t\tvalue = applyImmutable(value, parseJson<readonly Op[]>(revision.content)) as JsonObject;\n\t\t}\n\t\treturn { record, version: base.version, value };\n\t}\n\n\tasync scanDocuments(\n\t\tquery: DocumentQuery,\n\t\tcursor: Cursor | undefined,\n\t\tlimit: number,\n\t\t_context: Context,\n\t): Promise<Page<DocumentRecord, Cursor>> {\n\t\tthis.assertOpen();\n\t\tconst scope = scopeColumns(query.scope);\n\t\tconst clauses = [\"scope_kind = ?\", \"owner_id = ?\", \"id > ?\"];\n\t\tconst params: SqliteValue[] = [scope.scopeKind, scope.ownerId, cursorId(cursor) ?? -1];\n\t\tif (query.kind !== undefined) {\n\t\t\tclauses.push(\"kind = ?\");\n\t\t\tparams.push(encodeIndexedString(query.kind));\n\t\t}\n\t\tif (query.at === \"current\") {\n\t\t\tclauses.push(\"retired_at IS NULL\");\n\t\t} else {\n\t\t\tclauses.push(\"created_at <= ?\", \"(retired_at IS NULL OR retired_at > ?)\");\n\t\t\tparams.push(query.at, query.at);\n\t\t}\n\t\tparams.push(limit + 1);\n\t\tconst rows = allRows<JsonRow>(\n\t\t\tthis.db.prepare(`SELECT record FROM documents WHERE ${clauses.join(\" AND \")} ORDER BY id LIMIT ?`),\n\t\t\t...params,\n\t\t);\n\t\treturn page(\n\t\t\trows.map((row) => parseJson<DocumentRecord>(row.record)),\n\t\t\tlimit,\n\t\t);\n\t}\n\n\tasync close(_context: Context): Promise<void> {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tawait this.db.close();\n\t}\n\n\tprivate readConversation(id: Id): ConversationRecord | undefined {\n\t\tconst row = getRow<JsonRow>(this.db.prepare(\"SELECT record FROM conversations WHERE id = ?\"), id);\n\t\treturn row === undefined ? undefined : parseJson<ConversationRecord>(row.record);\n\t}\n\n\tprivate candidateNextId(writes: readonly StorageWrite[]): Id {\n\t\tlet nextId = this.nextId;\n\t\tfor (const write of writes) {\n\t\t\tconst id = writeId(write);\n\t\t\tif (id !== undefined) nextId = Math.max(nextId, id + 1);\n\t\t}\n\t\treturn nextId;\n\t}\n\n\tprivate checkGlobalIds(writes: readonly StorageWrite[]): void {\n\t\tconst claimed = new Map<Id, TableName>();\n\t\tconst lookup = this.db.prepare(\"SELECT record_type FROM record_ids WHERE id = ?\");\n\t\tfor (const write of writes) {\n\t\t\tif (write.type === \"document.change\" || write.type === \"document.retire\") continue;\n\t\t\tconst table: TableName = write.type === \"document.create\" ? \"document\" : write.type;\n\t\t\tconst id = write.type === \"document.create\" ? write.record.id : write.value.id;\n\t\t\tconst existing = getRow<RecordIdRow>(lookup, id)?.record_type;\n\t\t\tconst earlier = claimed.get(id);\n\t\t\tif (table === \"conversation\" || table === \"entry\" || table === \"document\") {\n\t\t\t\tif (existing !== undefined) throw new Error(`ID ${id} already belongs to ${existing}`);\n\t\t\t\tif (earlier !== undefined) throw new Error(`ID ${id} is written more than once`);\n\t\t\t} else {\n\t\t\t\tif (existing !== undefined && existing !== table)\n\t\t\t\t\tthrow new Error(`ID ${id} already belongs to ${existing}`);\n\t\t\t\tif (earlier !== undefined && earlier !== table) throw new Error(`ID ${id} is written as two record types`);\n\t\t\t}\n\t\t\tclaimed.set(id, table);\n\t\t}\n\t}\n\n\tprivate prepareDocumentActions(writes: readonly StorageWrite[]): Map<Id, DocumentAction> {\n\t\tconst actions = new Map<Id, DocumentAction>();\n\t\tfor (const write of writes) {\n\t\t\tif (write.type !== \"document.create\" && write.type !== \"document.change\" && write.type !== \"document.retire\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst id = write.type === \"document.create\" ? write.record.id : write.id;\n\t\t\tlet action = actions.get(id);\n\t\t\tif (action === undefined) {\n\t\t\t\taction = { retire: false };\n\t\t\t\tactions.set(id, action);\n\t\t\t}\n\t\t\tswitch (write.type) {\n\t\t\t\tcase \"document.create\":\n\t\t\t\t\tif (action.create !== undefined || action.content !== undefined) {\n\t\t\t\t\t\tthrow new Error(`Document ${id} has more than one content command`);\n\t\t\t\t\t}\n\t\t\t\t\taction.create = write.record;\n\t\t\t\t\taction.content = write.content;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"document.change\":\n\t\t\t\t\tif (action.content !== undefined) throw new Error(`Document ${id} has more than one content command`);\n\t\t\t\t\taction.content = write.content;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"document.retire\":\n\t\t\t\t\tif (action.retire) throw new Error(`Document ${id} is retired more than once`);\n\t\t\t\t\taction.retire = true;\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn actions;\n\t}\n\n\tprivate checkDocumentActions(actions: ReadonlyMap<Id, DocumentAction>): void {\n\t\tconst liveCounts = new Map<string, number>();\n\t\tfor (const [id, action] of actions) {\n\t\t\tconst row = getRow<JsonRow>(this.db.prepare(\"SELECT record FROM documents WHERE id = ?\"), id);\n\t\t\tconst existing = row === undefined ? undefined : parseJson<DocumentRecord>(row.record);\n\t\t\tif (action.create === undefined && existing === undefined) throw new Error(`Unknown document: ${id}`);\n\t\t\tif (action.create !== undefined && existing !== undefined) throw new Error(`Document ${id} already exists`);\n\t\t\tif (existing?.retiredAt !== undefined) throw new Error(`Document ${id} is retired`);\n\t\t\tif (action.create !== undefined && action.content?.kind !== \"base\") {\n\t\t\t\tthrow new Error(`Document ${id} creation requires a base`);\n\t\t\t}\n\t\t\tif (action.content?.kind === \"delta\") {\n\t\t\t\tconst previous = getRow<{ readonly version: number }>(\n\t\t\t\t\tthis.db.prepare(\n\t\t\t\t\t\t\"SELECT version FROM document_revisions WHERE document_id = ? ORDER BY seq DESC LIMIT 1\",\n\t\t\t\t\t),\n\t\t\t\t\tid,\n\t\t\t\t);\n\t\t\t\tif (previous === undefined) throw new Error(`Document ${id} delta has no base`);\n\t\t\t\tif (previous.version !== action.content.version) {\n\t\t\t\t\tthrow new Error(`Document ${id} version transition requires a base`);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst record = action.create ?? existing!;\n\t\t\tconst key = addressKey(record);\n\t\t\tlet live = liveCounts.get(key);\n\t\t\tif (live === undefined) live = this.currentDocumentId(record) === undefined ? 0 : 1;\n\t\t\tif (action.retire && existing !== undefined) live--;\n\t\t\tif (action.create !== undefined && !action.retire) live++;\n\t\t\tliveCounts.set(key, live);\n\t\t}\n\t\tfor (const live of liveCounts.values()) {\n\t\t\tif (live > 1) throw new Error(\"Document address already has a current incarnation\");\n\t\t}\n\t}\n\n\tprivate currentDocumentId(address: DocumentAddress | DocumentCreate | DocumentRecord): Id | undefined {\n\t\tconst parts = addressParts(address);\n\t\treturn getRow<IdRow>(\n\t\t\tthis.db.prepare(`SELECT id FROM documents\n\t\t\t\tWHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ? AND retired_at IS NULL\n\t\t\t\tLIMIT 1`),\n\t\t\tparts.kind,\n\t\t\tparts.scopeKind,\n\t\t\tparts.ownerId,\n\t\t\tparts.family,\n\t\t\tparts.keyValue,\n\t\t)?.id;\n\t}\n\n\tprivate applyTableWrite(write: StorageWrite, seq: Seq): void {\n\t\tswitch (write.type) {\n\t\t\tcase \"conversation\":\n\t\t\t\tthis.claimId(write.value.id, \"conversation\");\n\t\t\t\tthis.db\n\t\t\t\t\t.prepare(\"INSERT INTO conversations (id, record) VALUES (?, ?)\")\n\t\t\t\t\t.run(write.value.id, encodeJson(write.value));\n\t\t\t\tbreak;\n\t\t\tcase \"entry\":\n\t\t\t\tthis.claimId(write.value.id, \"entry\");\n\t\t\t\tthis.db\n\t\t\t\t\t.prepare(\"INSERT INTO entries (id, conversation_id, head, commit_seq, record) VALUES (?, ?, ?, ?, ?)\")\n\t\t\t\t\t.run(write.value.id, write.value.conversationId, write.value.head ?? null, seq, encodeJson(write.value));\n\t\t\t\tbreak;\n\t\t\tcase \"task\":\n\t\t\t\tthis.claimId(write.value.id, \"task\");\n\t\t\t\tthis.db\n\t\t\t\t\t.prepare(`INSERT INTO tasks (id, conversation_id, kind, status, abort_requested, background, record)\n\t\t\t\t\t\tVALUES (?, ?, ?, ?, ?, ?, ?)\n\t\t\t\t\t\tON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id, kind = excluded.kind,\n\t\t\t\t\t\tstatus = excluded.status, abort_requested = excluded.abort_requested,\n\t\t\t\t\t\tbackground = excluded.background, record = excluded.record`)\n\t\t\t\t\t.run(\n\t\t\t\t\t\twrite.value.id,\n\t\t\t\t\t\twrite.value.conversationId,\n\t\t\t\t\t\tencodeIndexedString(write.value.kind),\n\t\t\t\t\t\twrite.value.state.status,\n\t\t\t\t\t\twrite.value.abortRequested ? 1 : 0,\n\t\t\t\t\t\twrite.value.background ? 1 : 0,\n\t\t\t\t\t\tencodeJson(write.value),\n\t\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"submission\":\n\t\t\t\tthis.claimId(write.value.id, \"submission\");\n\t\t\t\tthis.db\n\t\t\t\t\t.prepare(`INSERT INTO submissions (id, conversation_id, request_id, record) VALUES (?, ?, ?, ?)\n\t\t\t\t\t\tON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id,\n\t\t\t\t\t\trequest_id = excluded.request_id, record = excluded.record`)\n\t\t\t\t\t.run(\n\t\t\t\t\t\twrite.value.id,\n\t\t\t\t\t\twrite.value.conversationId,\n\t\t\t\t\t\twrite.value.requestId === undefined ? null : encodeIndexedString(write.value.requestId),\n\t\t\t\t\t\tencodeJson(write.value),\n\t\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"document.create\":\n\t\t\tcase \"document.change\":\n\t\t\tcase \"document.retire\":\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate claimId(id: Id, table: TableName): void {\n\t\tthis.db.prepare(\"INSERT OR IGNORE INTO record_ids (id, record_type) VALUES (?, ?)\").run(id, table);\n\t}\n\n\tprivate applyDocumentActions(actions: ReadonlyMap<Id, DocumentAction>, seq: Seq): void {\n\t\tfor (const [id, action] of actions) {\n\t\t\tlet record: DocumentRecord;\n\t\t\tif (action.create !== undefined) {\n\t\t\t\trecord = {\n\t\t\t\t\t...action.create,\n\t\t\t\t\tcreatedAt: seq,\n\t\t\t\t\t...(action.retire ? { retiredAt: seq } : {}),\n\t\t\t\t};\n\t\t\t\tconst parts = addressParts(record);\n\t\t\t\tthis.claimId(id, \"document\");\n\t\t\t\tthis.db\n\t\t\t\t\t.prepare(`INSERT INTO documents\n\t\t\t\t\t\t(id, kind, family, key_value, scope_kind, owner_id, created_at, retired_at, record)\n\t\t\t\t\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)\n\t\t\t\t\t.run(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tparts.kind,\n\t\t\t\t\t\tparts.family,\n\t\t\t\t\t\tparts.keyValue,\n\t\t\t\t\t\tparts.scopeKind,\n\t\t\t\t\t\tparts.ownerId,\n\t\t\t\t\t\tseq,\n\t\t\t\t\t\taction.retire ? seq : null,\n\t\t\t\t\t\tencodeJson(record),\n\t\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tconst row = getRow<JsonRow>(this.db.prepare(\"SELECT record FROM documents WHERE id = ?\"), id)!;\n\t\t\t\trecord = parseJson<DocumentRecord>(row.record);\n\t\t\t}\n\n\t\t\tif (action.content !== undefined) {\n\t\t\t\tif (action.content.kind === \"base\" && isCurrentOnly(record)) {\n\t\t\t\t\tthis.db.prepare(\"DELETE FROM document_revisions WHERE document_id = ?\").run(id);\n\t\t\t\t}\n\t\t\t\tconst encodedContent =\n\t\t\t\t\taction.content.kind === \"base\" ? encodeJson(action.content.value) : encodeJson(action.content.ops);\n\t\t\t\tthis.db\n\t\t\t\t\t.prepare(\n\t\t\t\t\t\t\"INSERT INTO document_revisions (document_id, seq, kind, version, content) VALUES (?, ?, ?, ?, ?)\",\n\t\t\t\t\t)\n\t\t\t\t\t.run(id, seq, action.content.kind, action.content.version, encodedContent);\n\t\t\t}\n\n\t\t\tif (action.retire) {\n\t\t\t\tif (action.create === undefined) {\n\t\t\t\t\trecord = { ...record, retiredAt: seq };\n\t\t\t\t\tthis.db\n\t\t\t\t\t\t.prepare(\"UPDATE documents SET retired_at = ?, record = ? WHERE id = ?\")\n\t\t\t\t\t\t.run(seq, encodeJson(record), id);\n\t\t\t\t}\n\t\t\t\tif (isCurrentOnly(record)) this.db.prepare(\"DELETE FROM document_revisions WHERE document_id = ?\").run(id);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate assertOpen(): void {\n\t\tif (this.closed) throw new Error(\"SqliteStorage is closed\");\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/storage/sqlite/storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { apply } from "@earendil-works/chord/delta";
|
|
2
|
+
import { StorageRejected } from "../../errors.js";
|
|
3
|
+
import { idFromNumber, seqFromNumber } from "../../ids.js";
|
|
2
4
|
import { applySqliteMigrations } from "./migrations.js";
|
|
3
5
|
const parseJson = (value) => JSON.parse(value);
|
|
4
6
|
const encodeJson = (value) => JSON.stringify(value);
|
|
@@ -6,7 +8,14 @@ const encodeJson = (value) => JSON.stringify(value);
|
|
|
6
8
|
const encodeIndexedString = (value) => JSON.stringify(value);
|
|
7
9
|
const getRow = (statement, ...params) => statement.get(...params);
|
|
8
10
|
const allRows = (statement, ...params) => statement.all(...params);
|
|
9
|
-
const cursorId = (cursor) =>
|
|
11
|
+
const cursorId = (cursor) => {
|
|
12
|
+
const after = cursor?.after;
|
|
13
|
+
if (after === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (typeof after !== "number" || !Number.isSafeInteger(after))
|
|
16
|
+
throw new TypeError("Invalid storage cursor");
|
|
17
|
+
return idFromNumber(after);
|
|
18
|
+
};
|
|
10
19
|
const page = (values, limit) => {
|
|
11
20
|
const items = values.slice(0, limit);
|
|
12
21
|
if (values.length <= limit)
|
|
@@ -50,6 +59,7 @@ const writeId = (write) => {
|
|
|
50
59
|
case "submission":
|
|
51
60
|
return write.value.id;
|
|
52
61
|
case "document.create":
|
|
62
|
+
case "document.copy":
|
|
53
63
|
return write.record.id;
|
|
54
64
|
case "document.change":
|
|
55
65
|
case "document.retire":
|
|
@@ -117,7 +127,7 @@ export class SqliteStorage {
|
|
|
117
127
|
const metadata = getRow(this.db.prepare("SELECT next_id, next_seq FROM durable_metadata WHERE singleton = 1"));
|
|
118
128
|
if (metadata === undefined)
|
|
119
129
|
throw new Error("Durable SQLite metadata is missing");
|
|
120
|
-
const committedSeq = metadata.next_seq;
|
|
130
|
+
const committedSeq = seqFromNumber(metadata.next_seq);
|
|
121
131
|
this.checkGlobalIds(writes);
|
|
122
132
|
this.checkDocumentActions(documentActions);
|
|
123
133
|
for (const write of writes)
|
|
@@ -135,23 +145,61 @@ export class SqliteStorage {
|
|
|
135
145
|
this.assertOpen();
|
|
136
146
|
if (!Number.isSafeInteger(this.nextId))
|
|
137
147
|
throw new Error("ID space is exhausted");
|
|
138
|
-
return this.nextId
|
|
148
|
+
return idFromNumber(this.nextId++);
|
|
139
149
|
}
|
|
140
150
|
async conversation(id, _context) {
|
|
141
151
|
this.assertOpen();
|
|
142
152
|
const row = getRow(this.db.prepare("SELECT record FROM conversations WHERE id = ?"), id);
|
|
143
153
|
return row === undefined ? undefined : parseJson(row.record);
|
|
144
154
|
}
|
|
145
|
-
async scanConversations(
|
|
155
|
+
async scanConversations(query, limit, cursor, _context) {
|
|
146
156
|
this.assertOpen();
|
|
147
|
-
const
|
|
148
|
-
const
|
|
157
|
+
const clauses = ["id > ?"];
|
|
158
|
+
const params = [cursorId(cursor) ?? -1];
|
|
159
|
+
if (query.ownerConversationId !== undefined) {
|
|
160
|
+
clauses.push("owner_conversation_id = ?");
|
|
161
|
+
params.push(query.ownerConversationId);
|
|
162
|
+
}
|
|
163
|
+
if (query.ownerTaskId !== undefined) {
|
|
164
|
+
clauses.push("owner_task_id = ?");
|
|
165
|
+
params.push(query.ownerTaskId);
|
|
166
|
+
}
|
|
167
|
+
params.push(limit + 1);
|
|
168
|
+
const rows = allRows(this.db.prepare(`SELECT record FROM conversations WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`), ...params);
|
|
149
169
|
return page(rows.map((row) => parseJson(row.record)), limit);
|
|
150
170
|
}
|
|
151
|
-
async entry(
|
|
171
|
+
async entry(idOrConversationId, idOrContext, context) {
|
|
152
172
|
this.assertOpen();
|
|
173
|
+
const id = context === undefined
|
|
174
|
+
? idFromNumber(idOrConversationId)
|
|
175
|
+
: typeof idOrContext === "number"
|
|
176
|
+
? idFromNumber(idOrContext)
|
|
177
|
+
: undefined;
|
|
178
|
+
if (id === undefined)
|
|
179
|
+
throw new TypeError("Storage.entry() requires an entry ID");
|
|
180
|
+
let conversation;
|
|
181
|
+
if (context !== undefined) {
|
|
182
|
+
const conversationId = idFromNumber(idOrConversationId);
|
|
183
|
+
conversation = this.readConversation(conversationId);
|
|
184
|
+
if (conversation === undefined)
|
|
185
|
+
throw new Error(`Unknown conversation: ${conversationId}`);
|
|
186
|
+
}
|
|
153
187
|
const row = getRow(this.db.prepare("SELECT record, commit_seq FROM entries WHERE id = ?"), id);
|
|
154
|
-
|
|
188
|
+
if (row === undefined)
|
|
189
|
+
return undefined;
|
|
190
|
+
const entry = parseJson(row.record);
|
|
191
|
+
if (conversation !== undefined) {
|
|
192
|
+
let upperEntryId = Number.POSITIVE_INFINITY;
|
|
193
|
+
while (conversation.id !== entry.conversationId) {
|
|
194
|
+
if (conversation.parent === undefined)
|
|
195
|
+
return undefined;
|
|
196
|
+
upperEntryId = Math.min(upperEntryId, conversation.parent.at);
|
|
197
|
+
conversation = this.readConversation(conversation.parent.conversationId);
|
|
198
|
+
}
|
|
199
|
+
if (entry.id > upperEntryId)
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
return { entry, commitSeq: seqFromNumber(row.commit_seq) };
|
|
155
203
|
}
|
|
156
204
|
async findLatestHeadMarker(conversationId, atOrBeforeEntryId, _context) {
|
|
157
205
|
this.assertOpen();
|
|
@@ -171,7 +219,7 @@ export class SqliteStorage {
|
|
|
171
219
|
conversation = this.readConversation(conversation.parent.conversationId);
|
|
172
220
|
}
|
|
173
221
|
}
|
|
174
|
-
async scanEntries(query,
|
|
222
|
+
async scanEntries(query, limit, cursor, _context) {
|
|
175
223
|
this.assertOpen();
|
|
176
224
|
let conversation = this.readConversation(query.conversationId);
|
|
177
225
|
if (conversation === undefined)
|
|
@@ -209,7 +257,7 @@ export class SqliteStorage {
|
|
|
209
257
|
const row = getRow(this.db.prepare("SELECT record FROM tasks WHERE id = ?"), id);
|
|
210
258
|
return row === undefined ? undefined : parseJson(row.record);
|
|
211
259
|
}
|
|
212
|
-
async scanTasks(query,
|
|
260
|
+
async scanTasks(query, limit, cursor, _context) {
|
|
213
261
|
this.assertOpen();
|
|
214
262
|
const clauses = ["id > ?"];
|
|
215
263
|
const params = [cursorId(cursor) ?? -1];
|
|
@@ -242,6 +290,22 @@ export class SqliteStorage {
|
|
|
242
290
|
const row = getRow(this.db.prepare("SELECT record FROM submissions WHERE id = ?"), id);
|
|
243
291
|
return row === undefined ? undefined : parseJson(row.record);
|
|
244
292
|
}
|
|
293
|
+
async scanSubmissions(query, limit, cursor, _context) {
|
|
294
|
+
this.assertOpen();
|
|
295
|
+
const clauses = ["id > ?"];
|
|
296
|
+
const params = [cursorId(cursor) ?? -1];
|
|
297
|
+
if (query.conversationId !== undefined) {
|
|
298
|
+
clauses.push("conversation_id = ?");
|
|
299
|
+
params.push(query.conversationId);
|
|
300
|
+
}
|
|
301
|
+
if (query.status !== undefined) {
|
|
302
|
+
clauses.push("status = ?");
|
|
303
|
+
params.push(query.status);
|
|
304
|
+
}
|
|
305
|
+
params.push(limit + 1);
|
|
306
|
+
const rows = allRows(this.db.prepare(`SELECT record FROM submissions WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`), ...params);
|
|
307
|
+
return page(rows.map((row) => parseJson(row.record)), limit);
|
|
308
|
+
}
|
|
245
309
|
async submissionByRequest(conversationId, requestId, _context) {
|
|
246
310
|
this.assertOpen();
|
|
247
311
|
const row = getRow(this.db.prepare("SELECT record FROM submissions WHERE conversation_id = ? AND request_id = ?"), conversationId, encodeIndexedString(requestId));
|
|
@@ -266,32 +330,9 @@ export class SqliteStorage {
|
|
|
266
330
|
}
|
|
267
331
|
async document(id, at, _context) {
|
|
268
332
|
this.assertOpen();
|
|
269
|
-
|
|
270
|
-
if (row === undefined)
|
|
271
|
-
return undefined;
|
|
272
|
-
const record = parseJson(row.record);
|
|
273
|
-
if (at !== "current" && isCurrentOnly(record)) {
|
|
274
|
-
throw new Error(`Document ${id} does not retain historical content`);
|
|
275
|
-
}
|
|
276
|
-
if (!isAliveAt(record, at))
|
|
277
|
-
return undefined;
|
|
278
|
-
const upper = at === "current" ? Number.MAX_SAFE_INTEGER : at;
|
|
279
|
-
const base = getRow(this.db.prepare(`SELECT seq, kind, version, content FROM document_revisions
|
|
280
|
-
WHERE document_id = ? AND kind = 'base' AND seq <= ? ORDER BY seq DESC LIMIT 1`), id, upper);
|
|
281
|
-
if (base === undefined)
|
|
282
|
-
throw new Error(`Document ${id} is missing a required base`);
|
|
283
|
-
let value = parseJson(base.content);
|
|
284
|
-
const tail = allRows(this.db.prepare(`SELECT seq, kind, version, content FROM document_revisions
|
|
285
|
-
WHERE document_id = ? AND seq > ? AND seq <= ? ORDER BY seq`), id, base.seq, upper);
|
|
286
|
-
for (const revision of tail) {
|
|
287
|
-
if (revision.kind !== "delta" || revision.version !== base.version) {
|
|
288
|
-
throw new Error(`Document ${id} crosses a stored version boundary without a base`);
|
|
289
|
-
}
|
|
290
|
-
value = applyImmutable(value, parseJson(revision.content));
|
|
291
|
-
}
|
|
292
|
-
return { record, version: base.version, value };
|
|
333
|
+
return this.materializeDocument(id, at);
|
|
293
334
|
}
|
|
294
|
-
async scanDocuments(query,
|
|
335
|
+
async scanDocuments(query, limit, cursor, _context) {
|
|
295
336
|
this.assertOpen();
|
|
296
337
|
const scope = scopeColumns(query.scope);
|
|
297
338
|
const clauses = ["scope_kind = ?", "owner_id = ?", "id > ?"];
|
|
@@ -321,6 +362,32 @@ export class SqliteStorage {
|
|
|
321
362
|
const row = getRow(this.db.prepare("SELECT record FROM conversations WHERE id = ?"), id);
|
|
322
363
|
return row === undefined ? undefined : parseJson(row.record);
|
|
323
364
|
}
|
|
365
|
+
materializeDocument(id, at) {
|
|
366
|
+
const row = getRow(this.db.prepare("SELECT record FROM documents WHERE id = ?"), id);
|
|
367
|
+
if (row === undefined)
|
|
368
|
+
return undefined;
|
|
369
|
+
const record = parseJson(row.record);
|
|
370
|
+
if (at !== "current" && isCurrentOnly(record)) {
|
|
371
|
+
throw new Error(`Document ${id} does not retain historical content`);
|
|
372
|
+
}
|
|
373
|
+
if (!isAliveAt(record, at))
|
|
374
|
+
return undefined;
|
|
375
|
+
const upper = at === "current" ? Number.MAX_SAFE_INTEGER : at;
|
|
376
|
+
const base = getRow(this.db.prepare(`SELECT seq, kind, version, content FROM document_revisions
|
|
377
|
+
WHERE document_id = ? AND kind = 'base' AND seq <= ? ORDER BY seq DESC LIMIT 1`), id, upper);
|
|
378
|
+
if (base === undefined)
|
|
379
|
+
throw new Error(`Document ${id} is missing a required base`);
|
|
380
|
+
let value = parseJson(base.content);
|
|
381
|
+
const tail = allRows(this.db.prepare(`SELECT seq, kind, version, content FROM document_revisions
|
|
382
|
+
WHERE document_id = ? AND seq > ? AND seq <= ? ORDER BY seq`), id, base.seq, upper);
|
|
383
|
+
for (const revision of tail) {
|
|
384
|
+
if (revision.kind !== "delta" || revision.version !== base.version) {
|
|
385
|
+
throw new Error(`Document ${id} crosses a stored version boundary without a base`);
|
|
386
|
+
}
|
|
387
|
+
value = apply(value, parseJson(revision.content));
|
|
388
|
+
}
|
|
389
|
+
return { record, version: base.version, value, deltasSinceBase: tail.length };
|
|
390
|
+
}
|
|
324
391
|
candidateNextId(writes) {
|
|
325
392
|
let nextId = this.nextId;
|
|
326
393
|
for (const write of writes) {
|
|
@@ -336,8 +403,9 @@ export class SqliteStorage {
|
|
|
336
403
|
for (const write of writes) {
|
|
337
404
|
if (write.type === "document.change" || write.type === "document.retire")
|
|
338
405
|
continue;
|
|
339
|
-
const
|
|
340
|
-
const
|
|
406
|
+
const document = write.type === "document.create" || write.type === "document.copy";
|
|
407
|
+
const table = document ? "document" : write.type;
|
|
408
|
+
const id = document ? write.record.id : write.value.id;
|
|
341
409
|
const existing = getRow(lookup, id)?.record_type;
|
|
342
410
|
const earlier = claimed.get(id);
|
|
343
411
|
if (table === "conversation" || table === "entry" || table === "document") {
|
|
@@ -358,10 +426,13 @@ export class SqliteStorage {
|
|
|
358
426
|
prepareDocumentActions(writes) {
|
|
359
427
|
const actions = new Map();
|
|
360
428
|
for (const write of writes) {
|
|
361
|
-
if (write.type !== "document.create" &&
|
|
429
|
+
if (write.type !== "document.create" &&
|
|
430
|
+
write.type !== "document.copy" &&
|
|
431
|
+
write.type !== "document.change" &&
|
|
432
|
+
write.type !== "document.retire") {
|
|
362
433
|
continue;
|
|
363
434
|
}
|
|
364
|
-
const id = write.type === "document.create" ? write.record.id : write.id;
|
|
435
|
+
const id = write.type === "document.create" || write.type === "document.copy" ? write.record.id : write.id;
|
|
365
436
|
let action = actions.get(id);
|
|
366
437
|
if (action === undefined) {
|
|
367
438
|
action = { retire: false };
|
|
@@ -369,15 +440,23 @@ export class SqliteStorage {
|
|
|
369
440
|
}
|
|
370
441
|
switch (write.type) {
|
|
371
442
|
case "document.create":
|
|
372
|
-
if (action.create !== undefined || action.content !== undefined) {
|
|
443
|
+
if (action.create !== undefined || action.content !== undefined || action.copy !== undefined) {
|
|
373
444
|
throw new Error(`Document ${id} has more than one content command`);
|
|
374
445
|
}
|
|
375
446
|
action.create = write.record;
|
|
376
447
|
action.content = write.content;
|
|
377
448
|
break;
|
|
449
|
+
case "document.copy":
|
|
450
|
+
if (action.create !== undefined || action.content !== undefined || action.copy !== undefined) {
|
|
451
|
+
throw new Error(`Document ${id} has more than one content command`);
|
|
452
|
+
}
|
|
453
|
+
action.create = write.record;
|
|
454
|
+
action.copy = write.source;
|
|
455
|
+
break;
|
|
378
456
|
case "document.change":
|
|
379
|
-
if (action.content !== undefined)
|
|
457
|
+
if (action.content !== undefined || action.copy !== undefined) {
|
|
380
458
|
throw new Error(`Document ${id} has more than one content command`);
|
|
459
|
+
}
|
|
381
460
|
action.content = write.content;
|
|
382
461
|
break;
|
|
383
462
|
case "document.retire":
|
|
@@ -392,6 +471,9 @@ export class SqliteStorage {
|
|
|
392
471
|
checkDocumentActions(actions) {
|
|
393
472
|
const liveCounts = new Map();
|
|
394
473
|
for (const [id, action] of actions) {
|
|
474
|
+
if (action.copy !== undefined && actions.has(action.copy.id)) {
|
|
475
|
+
throw new StorageRejected(`Document copy ${id} source is changed in the copy batch`);
|
|
476
|
+
}
|
|
395
477
|
const row = getRow(this.db.prepare("SELECT record FROM documents WHERE id = ?"), id);
|
|
396
478
|
const existing = row === undefined ? undefined : parseJson(row.record);
|
|
397
479
|
if (action.create === undefined && existing === undefined)
|
|
@@ -400,9 +482,6 @@ export class SqliteStorage {
|
|
|
400
482
|
throw new Error(`Document ${id} already exists`);
|
|
401
483
|
if (existing?.retiredAt !== undefined)
|
|
402
484
|
throw new Error(`Document ${id} is retired`);
|
|
403
|
-
if (action.create !== undefined && action.content?.kind !== "base") {
|
|
404
|
-
throw new Error(`Document ${id} creation requires a base`);
|
|
405
|
-
}
|
|
406
485
|
if (action.content?.kind === "delta") {
|
|
407
486
|
const previous = getRow(this.db.prepare("SELECT version FROM document_revisions WHERE document_id = ? ORDER BY seq DESC LIMIT 1"), id);
|
|
408
487
|
if (previous === undefined)
|
|
@@ -429,17 +508,18 @@ export class SqliteStorage {
|
|
|
429
508
|
}
|
|
430
509
|
currentDocumentId(address) {
|
|
431
510
|
const parts = addressParts(address);
|
|
432
|
-
|
|
511
|
+
const id = getRow(this.db.prepare(`SELECT id FROM documents
|
|
433
512
|
WHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ? AND retired_at IS NULL
|
|
434
513
|
LIMIT 1`), parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue)?.id;
|
|
514
|
+
return id === undefined ? undefined : idFromNumber(id);
|
|
435
515
|
}
|
|
436
516
|
applyTableWrite(write, seq) {
|
|
437
517
|
switch (write.type) {
|
|
438
518
|
case "conversation":
|
|
439
519
|
this.claimId(write.value.id, "conversation");
|
|
440
520
|
this.db
|
|
441
|
-
.prepare("INSERT INTO conversations (id, record) VALUES (?, ?)")
|
|
442
|
-
.run(write.value.id, encodeJson(write.value));
|
|
521
|
+
.prepare("INSERT INTO conversations (id, owner_conversation_id, owner_task_id, record) VALUES (?, ?, ?, ?)")
|
|
522
|
+
.run(write.value.id, write.value.owner?.conversationId ?? null, write.value.owner?.taskId ?? null, encodeJson(write.value));
|
|
443
523
|
break;
|
|
444
524
|
case "entry":
|
|
445
525
|
this.claimId(write.value.id, "entry");
|
|
@@ -460,12 +540,13 @@ export class SqliteStorage {
|
|
|
460
540
|
case "submission":
|
|
461
541
|
this.claimId(write.value.id, "submission");
|
|
462
542
|
this.db
|
|
463
|
-
.prepare(`INSERT INTO submissions (id, conversation_id, request_id, record) VALUES (?, ?, ?, ?)
|
|
543
|
+
.prepare(`INSERT INTO submissions (id, conversation_id, request_id, status, record) VALUES (?, ?, ?, ?, ?)
|
|
464
544
|
ON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id,
|
|
465
|
-
request_id = excluded.request_id, record = excluded.record`)
|
|
466
|
-
.run(write.value.id, write.value.conversationId, write.value.requestId === undefined ? null : encodeIndexedString(write.value.requestId), encodeJson(write.value));
|
|
545
|
+
request_id = excluded.request_id, status = excluded.status, record = excluded.record`)
|
|
546
|
+
.run(write.value.id, write.value.conversationId, write.value.requestId === undefined ? null : encodeIndexedString(write.value.requestId), write.value.status, encodeJson(write.value));
|
|
467
547
|
break;
|
|
468
548
|
case "document.create":
|
|
549
|
+
case "document.copy":
|
|
469
550
|
case "document.change":
|
|
470
551
|
case "document.retire":
|
|
471
552
|
break;
|
|
@@ -476,6 +557,29 @@ export class SqliteStorage {
|
|
|
476
557
|
}
|
|
477
558
|
applyDocumentActions(actions, seq) {
|
|
478
559
|
for (const [id, action] of actions) {
|
|
560
|
+
let content = action.content;
|
|
561
|
+
if (action.copy !== undefined) {
|
|
562
|
+
try {
|
|
563
|
+
const stored = this.materializeDocument(action.copy.id, action.copy.at);
|
|
564
|
+
if (stored === undefined)
|
|
565
|
+
throw new Error(`Fork source document ${action.copy.id} cannot be read`);
|
|
566
|
+
const create = action.create;
|
|
567
|
+
if (stored.record.scope.kind !== "conversation" ||
|
|
568
|
+
create.scope.kind !== "conversation" ||
|
|
569
|
+
stored.record.kind !== create.kind ||
|
|
570
|
+
stored.record.key !== create.key ||
|
|
571
|
+
stored.record.history !== create.history ||
|
|
572
|
+
stored.record.fork !== create.fork) {
|
|
573
|
+
throw new Error(`Fork source document ${action.copy.id} does not match the copied record`);
|
|
574
|
+
}
|
|
575
|
+
content = { kind: "base", version: stored.version, value: stored.value };
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
if (error instanceof StorageRejected)
|
|
579
|
+
throw error;
|
|
580
|
+
throw new StorageRejected(`Document copy ${id} was rejected`, { cause: error });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
479
583
|
let record;
|
|
480
584
|
if (action.create !== undefined) {
|
|
481
585
|
record = {
|
|
@@ -495,14 +599,14 @@ export class SqliteStorage {
|
|
|
495
599
|
const row = getRow(this.db.prepare("SELECT record FROM documents WHERE id = ?"), id);
|
|
496
600
|
record = parseJson(row.record);
|
|
497
601
|
}
|
|
498
|
-
if (
|
|
499
|
-
if (
|
|
602
|
+
if (content !== undefined) {
|
|
603
|
+
if (content.kind === "base" && isCurrentOnly(record)) {
|
|
500
604
|
this.db.prepare("DELETE FROM document_revisions WHERE document_id = ?").run(id);
|
|
501
605
|
}
|
|
502
|
-
const encodedContent =
|
|
606
|
+
const encodedContent = content.kind === "base" ? encodeJson(content.value) : encodeJson(content.ops);
|
|
503
607
|
this.db
|
|
504
608
|
.prepare("INSERT INTO document_revisions (document_id, seq, kind, version, content) VALUES (?, ?, ?, ?, ?)")
|
|
505
|
-
.run(id, seq,
|
|
609
|
+
.run(id, seq, content.kind, content.version, encodedContent);
|
|
506
610
|
}
|
|
507
611
|
if (action.retire) {
|
|
508
612
|
if (action.create === undefined) {
|