@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.
Files changed (168) hide show
  1. package/README.md +46 -4
  2. package/dist/documents.d.ts +66 -0
  3. package/dist/documents.d.ts.map +1 -0
  4. package/dist/documents.js +98 -0
  5. package/dist/documents.js.map +1 -0
  6. package/dist/entries.d.ts +13 -0
  7. package/dist/entries.d.ts.map +1 -0
  8. package/dist/entries.js +18 -0
  9. package/dist/entries.js.map +1 -0
  10. package/dist/env/index.d.ts +131 -0
  11. package/dist/env/index.d.ts.map +1 -0
  12. package/dist/env/index.js +45 -0
  13. package/dist/env/index.js.map +1 -0
  14. package/dist/env/node.d.ts +45 -0
  15. package/dist/env/node.d.ts.map +1 -0
  16. package/dist/env/node.js +908 -0
  17. package/dist/env/node.js.map +1 -0
  18. package/dist/env/utils/adaptive-publisher.d.ts +24 -0
  19. package/dist/env/utils/adaptive-publisher.d.ts.map +1 -0
  20. package/dist/env/utils/adaptive-publisher.js +79 -0
  21. package/dist/env/utils/adaptive-publisher.js.map +1 -0
  22. package/dist/env/utils/output-capture.d.ts +31 -0
  23. package/dist/env/utils/output-capture.d.ts.map +1 -0
  24. package/dist/env/utils/output-capture.js +238 -0
  25. package/dist/env/utils/output-capture.js.map +1 -0
  26. package/dist/env/utils/truncate.d.ts +71 -0
  27. package/dist/env/utils/truncate.d.ts.map +1 -0
  28. package/dist/env/utils/truncate.js +294 -0
  29. package/dist/env/utils/truncate.js.map +1 -0
  30. package/dist/errors.d.ts +15 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +24 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/harness/config.d.ts +20 -0
  35. package/dist/harness/config.d.ts.map +1 -0
  36. package/dist/harness/config.js +18 -0
  37. package/dist/harness/config.js.map +1 -0
  38. package/dist/harness/context.d.ts +27 -0
  39. package/dist/harness/context.d.ts.map +1 -0
  40. package/dist/harness/context.js +109 -0
  41. package/dist/harness/context.js.map +1 -0
  42. package/dist/harness/generation.d.ts +36 -0
  43. package/dist/harness/generation.d.ts.map +1 -0
  44. package/dist/harness/generation.js +266 -0
  45. package/dist/harness/generation.js.map +1 -0
  46. package/dist/harness/harness.d.ts +10 -0
  47. package/dist/harness/harness.d.ts.map +1 -0
  48. package/dist/harness/harness.js +266 -0
  49. package/dist/harness/harness.js.map +1 -0
  50. package/dist/harness/live.d.ts +40 -0
  51. package/dist/harness/live.d.ts.map +1 -0
  52. package/dist/harness/live.js +41 -0
  53. package/dist/harness/live.js.map +1 -0
  54. package/dist/harness/prompt.d.ts +24 -0
  55. package/dist/harness/prompt.d.ts.map +1 -0
  56. package/dist/harness/prompt.js +88 -0
  57. package/dist/harness/prompt.js.map +1 -0
  58. package/dist/harness/registry.d.ts +7 -0
  59. package/dist/harness/registry.d.ts.map +1 -0
  60. package/dist/harness/registry.js +309 -0
  61. package/dist/harness/registry.js.map +1 -0
  62. package/dist/harness/scheduler.d.ts +63 -0
  63. package/dist/harness/scheduler.d.ts.map +1 -0
  64. package/dist/harness/scheduler.js +605 -0
  65. package/dist/harness/scheduler.js.map +1 -0
  66. package/dist/harness/submissions.d.ts +24 -0
  67. package/dist/harness/submissions.d.ts.map +1 -0
  68. package/dist/harness/submissions.js +133 -0
  69. package/dist/harness/submissions.js.map +1 -0
  70. package/dist/harness/types.d.ts +358 -0
  71. package/dist/harness/types.d.ts.map +1 -0
  72. package/dist/harness/types.js +2 -0
  73. package/dist/harness/types.js.map +1 -0
  74. package/dist/harness/util.d.ts +14 -0
  75. package/dist/harness/util.d.ts.map +1 -0
  76. package/dist/harness/util.js +55 -0
  77. package/dist/harness/util.js.map +1 -0
  78. package/dist/ids.d.ts +6 -0
  79. package/dist/ids.d.ts.map +1 -0
  80. package/dist/ids.js +9 -0
  81. package/dist/ids.js.map +1 -0
  82. package/dist/index.d.ts +12 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +10 -0
  85. package/dist/index.js.map +1 -1
  86. package/dist/session/forks.d.ts +10 -0
  87. package/dist/session/forks.d.ts.map +1 -0
  88. package/dist/session/forks.js +42 -0
  89. package/dist/session/forks.js.map +1 -0
  90. package/dist/session/observation.d.ts +28 -0
  91. package/dist/session/observation.d.ts.map +1 -0
  92. package/dist/session/observation.js +280 -0
  93. package/dist/session/observation.js.map +1 -0
  94. package/dist/session/session.d.ts +59 -0
  95. package/dist/session/session.d.ts.map +1 -0
  96. package/dist/session/session.js +380 -0
  97. package/dist/session/session.js.map +1 -0
  98. package/dist/session/transaction.d.ts +105 -0
  99. package/dist/session/transaction.d.ts.map +1 -0
  100. package/dist/session/transaction.js +718 -0
  101. package/dist/session/transaction.js.map +1 -0
  102. package/dist/storage/jsonl/index.d.ts +2 -0
  103. package/dist/storage/jsonl/index.d.ts.map +1 -0
  104. package/dist/storage/jsonl/index.js +2 -0
  105. package/dist/storage/jsonl/index.js.map +1 -0
  106. package/dist/storage/jsonl/node.d.ts +6 -0
  107. package/dist/storage/jsonl/node.d.ts.map +1 -0
  108. package/dist/storage/jsonl/node.js +8 -0
  109. package/dist/storage/jsonl/node.js.map +1 -0
  110. package/dist/storage/jsonl/storage.d.ts +115 -0
  111. package/dist/storage/jsonl/storage.d.ts.map +1 -0
  112. package/dist/storage/jsonl/storage.js +681 -0
  113. package/dist/storage/jsonl/storage.js.map +1 -0
  114. package/dist/storage/memory.d.ts +31 -14
  115. package/dist/storage/memory.d.ts.map +1 -1
  116. package/dist/storage/memory.js +208 -61
  117. package/dist/storage/memory.js.map +1 -1
  118. package/dist/storage/sqlite/database.d.ts +5 -1
  119. package/dist/storage/sqlite/database.d.ts.map +1 -1
  120. package/dist/storage/sqlite/database.js.map +1 -1
  121. package/dist/storage/sqlite/index.d.ts.map +1 -1
  122. package/dist/storage/sqlite/migrations.d.ts +1 -1
  123. package/dist/storage/sqlite/migrations.d.ts.map +1 -1
  124. package/dist/storage/sqlite/migrations.js +8 -1
  125. package/dist/storage/sqlite/migrations.js.map +1 -1
  126. package/dist/storage/sqlite/node.d.ts.map +1 -1
  127. package/dist/storage/sqlite/node.js +2 -2
  128. package/dist/storage/sqlite/node.js.map +1 -1
  129. package/dist/storage/sqlite/storage.d.ts +20 -14
  130. package/dist/storage/sqlite/storage.d.ts.map +1 -1
  131. package/dist/storage/sqlite/storage.js +159 -55
  132. package/dist/storage/sqlite/storage.js.map +1 -1
  133. package/dist/tasks.d.ts +6 -0
  134. package/dist/tasks.d.ts.map +1 -0
  135. package/dist/tasks.js +5 -0
  136. package/dist/tasks.js.map +1 -0
  137. package/dist/testing/assertions.d.ts +5 -0
  138. package/dist/testing/assertions.d.ts.map +1 -0
  139. package/dist/testing/assertions.js +25 -0
  140. package/dist/testing/assertions.js.map +1 -0
  141. package/dist/testing/index.d.ts +6 -0
  142. package/dist/testing/index.d.ts.map +1 -0
  143. package/dist/testing/index.js +5 -0
  144. package/dist/testing/index.js.map +1 -0
  145. package/dist/testing/runner.d.ts +10 -0
  146. package/dist/testing/runner.d.ts.map +1 -0
  147. package/dist/testing/runner.js +14 -0
  148. package/dist/testing/runner.js.map +1 -0
  149. package/dist/testing/storage-benchmark.d.ts +41 -0
  150. package/dist/testing/storage-benchmark.d.ts.map +1 -0
  151. package/dist/testing/storage-benchmark.js +351 -0
  152. package/dist/testing/storage-benchmark.js.map +1 -0
  153. package/dist/testing/storage-conformance.d.ts +4 -0
  154. package/dist/testing/storage-conformance.d.ts.map +1 -0
  155. package/dist/testing/storage-conformance.js +1122 -0
  156. package/dist/testing/storage-conformance.js.map +1 -0
  157. package/dist/testing/types.d.ts +19 -0
  158. package/dist/testing/types.d.ts.map +1 -0
  159. package/dist/testing/types.js +2 -0
  160. package/dist/testing/types.js.map +1 -0
  161. package/dist/types.d.ts +422 -54
  162. package/dist/types.d.ts.map +1 -1
  163. package/dist/types.js.map +1 -1
  164. package/docs/chord-delta-findings.md +1 -1
  165. package/docs/pico-v5-chord-usage.md +47 -60
  166. package/docs/pico-v5-handoff.md +469 -264
  167. package/docs/pico-v5.md +1523 -639
  168. 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 { applyImmutable } from "@earendil-works/chord/delta";
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) => cursor?.after;
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(cursor, limit, _context) {
155
+ async scanConversations(query, limit, cursor, _context) {
146
156
  this.assertOpen();
147
- const after = cursorId(cursor) ?? -1;
148
- const rows = allRows(this.db.prepare("SELECT record FROM conversations WHERE id > ? ORDER BY id LIMIT ?"), after, limit + 1);
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(id, _context) {
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
- return row === undefined ? undefined : { entry: parseJson(row.record), commitSeq: row.commit_seq };
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, cursor, limit, _context) {
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, cursor, limit, _context) {
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
- const row = getRow(this.db.prepare("SELECT record FROM documents WHERE id = ?"), id);
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, cursor, limit, _context) {
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 table = write.type === "document.create" ? "document" : write.type;
340
- const id = write.type === "document.create" ? write.record.id : write.value.id;
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" && write.type !== "document.change" && write.type !== "document.retire") {
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
- return getRow(this.db.prepare(`SELECT id FROM documents
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 (action.content !== undefined) {
499
- if (action.content.kind === "base" && isCurrentOnly(record)) {
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 = action.content.kind === "base" ? encodeJson(action.content.value) : encodeJson(action.content.ops);
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, action.content.kind, action.content.version, encodedContent);
609
+ .run(id, seq, content.kind, content.version, encodedContent);
506
610
  }
507
611
  if (action.retire) {
508
612
  if (action.create === undefined) {