@tearleads/client-sdk 0.1.42 → 0.1.44

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 (224) hide show
  1. package/client/containerDocumentDiscovery.js +71 -29
  2. package/client/containerDocumentDiscovery.js.map +1 -1
  3. package/client/deviceFirst.js +4 -5
  4. package/client/deviceFirst.js.map +1 -1
  5. package/data/blobs/transferPool.js +35 -8
  6. package/data/blobs/transferPool.js.map +1 -1
  7. package/data/containers/shared/projection.js +12 -0
  8. package/data/containers/shared/projection.js.map +1 -1
  9. package/data/documents/blob/shared/types.d.ts +2 -3
  10. package/data/documents/blob/shared/types.js.map +1 -1
  11. package/data/documents/shared/events.js +0 -1
  12. package/data/documents/shared/events.js.map +1 -1
  13. package/data/persistence/container-contents/containerContentsPersistence.js +6 -22
  14. package/data/persistence/container-contents/containerContentsPersistence.js.map +1 -1
  15. package/data/persistence/container-contents/containerMetadataMutationPersistence.js +5 -15
  16. package/data/persistence/container-contents/containerMetadataMutationPersistence.js.map +1 -1
  17. package/data/persistence/container-contents/containerPendingUpdatePersistence.js +6 -15
  18. package/data/persistence/container-contents/containerPendingUpdatePersistence.js.map +1 -1
  19. package/data/persistence/container-contents/containerRecoveryPersistence.js +3 -4
  20. package/data/persistence/container-contents/containerRecoveryPersistence.js.map +1 -1
  21. package/data/persistence/container-contents/containerRemovalWork.js +3 -5
  22. package/data/persistence/container-contents/containerRemovalWork.js.map +1 -1
  23. package/data/persistence/container-contents/dormantContainerMetadata.js +1 -5
  24. package/data/persistence/container-contents/dormantContainerMetadata.js.map +1 -1
  25. package/data/persistence/containers/containerSyncWatermarkPersistence.d.ts +11 -3
  26. package/data/persistence/containers/containerSyncWatermarkPersistence.js +46 -58
  27. package/data/persistence/containers/containerSyncWatermarkPersistence.js.map +1 -1
  28. package/data/persistence/documents/documentsPersistence.js +4 -9
  29. package/data/persistence/documents/documentsPersistence.js.map +1 -1
  30. package/data/persistence/documents/internal/discardDocument.js +1 -2
  31. package/data/persistence/documents/internal/discardDocument.js.map +1 -1
  32. package/data/persistence/documents/internal/documentDeletionPersistence.js +1 -2
  33. package/data/persistence/documents/internal/documentDeletionPersistence.js.map +1 -1
  34. package/data/persistence/documents/internal/documentMutationPersistence.js +17 -25
  35. package/data/persistence/documents/internal/documentMutationPersistence.js.map +1 -1
  36. package/data/persistence/documents/internal/orphanSideRows.js +2 -14
  37. package/data/persistence/documents/internal/orphanSideRows.js.map +1 -1
  38. package/data/persistence/documents/internal/syncQueuePersistence.js +3 -3
  39. package/data/persistence/documents/internal/syncQueuePersistence.js.map +1 -1
  40. package/data/sqlite/documentHistoryPersistence.d.ts +9 -3
  41. package/data/sqlite/documentHistoryPersistence.js +31 -8
  42. package/data/sqlite/documentHistoryPersistence.js.map +1 -1
  43. package/data/sqlite/documentPendingUpdatePersistence.d.ts +58 -11
  44. package/data/sqlite/documentPendingUpdatePersistence.js +137 -63
  45. package/data/sqlite/documentPendingUpdatePersistence.js.map +1 -1
  46. package/data/sqlite/documentPersistence.d.ts +1 -1
  47. package/data/sqlite/documentPersistence.js +1 -1
  48. package/data/sqlite/documentPersistence.js.map +1 -1
  49. package/data/sqlite/documentPersistenceTypes.d.ts +2 -2
  50. package/data/sqlite/documentPersistenceTypes.js.map +1 -1
  51. package/data/sqlite/documentRecordPersistence.js +5 -4
  52. package/data/sqlite/documentRecordPersistence.js.map +1 -1
  53. package/data/sqlite/schema.d.ts +167 -517
  54. package/data/sqlite/schema.js +68 -82
  55. package/data/sqlite/schema.js.map +1 -1
  56. package/internal/api-client/ApiClient.d.ts +18 -41
  57. package/internal/api-client/ApiClient.js +9 -14
  58. package/internal/api-client/ApiClient.js.map +1 -1
  59. package/internal/api-client/apiRequestRuntime.d.ts +2 -0
  60. package/internal/api-client/apiRequestRuntime.js +4 -0
  61. package/internal/api-client/apiRequestRuntime.js.map +1 -1
  62. package/internal/api-client/binaryRequestOperationTransport.d.ts +18 -0
  63. package/internal/api-client/index.d.ts +1 -0
  64. package/internal/api-client/index.js +1 -0
  65. package/internal/api-client/index.js.map +1 -1
  66. package/internal/api-client/operationTransportFactory.d.ts +6 -0
  67. package/internal/api-client/requestInternals.js +7 -7
  68. package/internal/api-client/requestInternals.js.map +1 -1
  69. package/internal/api-client/routes/blobs/get.d.ts +7 -0
  70. package/internal/api-client/routes/blobs/get.js.map +1 -1
  71. package/internal/api-client/routes/blobs/multipart.js +1 -11
  72. package/internal/api-client/routes/blobs/multipart.js.map +1 -1
  73. package/internal/api-client/routes/blobs/multipartUpload.d.ts +14 -0
  74. package/internal/api-client/routes/blobs/multipartUpload.js +94 -43
  75. package/internal/api-client/routes/blobs/multipartUpload.js.map +1 -1
  76. package/internal/api-client/routes/containers/reads.d.ts +13 -7
  77. package/internal/api-client/routes/containers/reads.js +36 -15
  78. package/internal/api-client/routes/containers/reads.js.map +1 -1
  79. package/internal/api-client/types.d.ts +1 -5
  80. package/internal/api-client/types.js.map +1 -1
  81. package/internal/validators/containerReadRefinements.js +15 -5
  82. package/internal/validators/containerReadRefinements.js.map +1 -1
  83. package/internal/validators/containerReadSchemas.d.ts +36 -0
  84. package/internal/validators/containerReadSchemas.js +64 -1
  85. package/internal/validators/containerReadSchemas.js.map +1 -1
  86. package/internal/validators/operation/attachments.d.ts +80 -160
  87. package/internal/validators/operation/auth.d.ts +760 -1664
  88. package/internal/validators/operation/blobDownloads.js +1 -1
  89. package/internal/validators/operation/blobDownloads.js.map +1 -1
  90. package/internal/validators/operation/containerMutations.d.ts +416 -856
  91. package/internal/validators/operation/containerReads.d.ts +118 -182
  92. package/internal/validators/operation/containerReads.js +20 -54
  93. package/internal/validators/operation/containerReads.js.map +1 -1
  94. package/internal/validators/operation/documentMutations.d.ts +120 -264
  95. package/internal/validators/operation/documentSync.d.ts +40 -80
  96. package/internal/validators/operation/index.d.ts +2 -2
  97. package/internal/validators/operation/index.js +2 -2
  98. package/internal/validators/operation/index.js.map +1 -1
  99. package/internal/validators/operation/multipartBlobs.d.ts +70 -190
  100. package/internal/validators/operation/multipartBlobs.js +2 -24
  101. package/internal/validators/operation/multipartBlobs.js.map +1 -1
  102. package/internal/validators/operation/multipartPartUploads.d.ts +5 -5
  103. package/internal/validators/operation/multipartPartUploads.js +6 -20
  104. package/internal/validators/operation/multipartPartUploads.js.map +1 -1
  105. package/internal/validators/operation/organizations.d.ts +882 -1906
  106. package/internal/validators/operation/principals.d.ts +120 -240
  107. package/internal/validators/operation/registry.d.ts +3 -4
  108. package/internal/validators/operation/registry.js +3 -4
  109. package/internal/validators/operation/registry.js.map +1 -1
  110. package/internal/validators/request/blob.d.ts +100 -201
  111. package/internal/validators/request/blob.js +6 -13
  112. package/internal/validators/request/blob.js.map +1 -1
  113. package/internal/validators/request/container.d.ts +136 -272
  114. package/internal/validators/request/container.js +16 -10
  115. package/internal/validators/request/container.js.map +1 -1
  116. package/internal/validators/request/containerDocumentLanes.d.ts +23 -0
  117. package/internal/validators/request/containerDocumentLanes.js +23 -0
  118. package/internal/validators/request/containerDocumentLanes.js.map +1 -0
  119. package/internal/validators/request/containerManifestRef.d.ts +54 -0
  120. package/internal/validators/request/containerManifestRef.js +28 -0
  121. package/internal/validators/request/containerManifestRef.js.map +1 -0
  122. package/internal/validators/request/containerMetadata.d.ts +160 -368
  123. package/internal/validators/request/containerParentLanes.js +5 -35
  124. package/internal/validators/request/containerParentLanes.js.map +1 -1
  125. package/internal/validators/request/document.d.ts +100 -245
  126. package/internal/validators/request/document.js +1 -2
  127. package/internal/validators/request/document.js.map +1 -1
  128. package/internal/validators/request/documentSyncSchema.d.ts +42 -109
  129. package/internal/validators/request/documentSyncSchema.js +8 -25
  130. package/internal/validators/request/documentSyncSchema.js.map +1 -1
  131. package/internal/validators/request/index.d.ts +2 -1
  132. package/internal/validators/request/index.js +2 -1
  133. package/internal/validators/request/index.js.map +1 -1
  134. package/internal/validators/request/organization.d.ts +160 -320
  135. package/internal/validators/request/organizationProvisioning.d.ts +2424 -5312
  136. package/internal/validators/request/principal.d.ts +160 -320
  137. package/internal/validators/request/registration.d.ts +760 -1664
  138. package/internal/validators/response/blob.d.ts +48 -27
  139. package/internal/validators/response/blob.js +3 -10
  140. package/internal/validators/response/blob.js.map +1 -1
  141. package/internal/validators/response/containerDocumentLanes.d.ts +82 -0
  142. package/internal/validators/response/containerDocumentLanes.js +35 -0
  143. package/internal/validators/response/containerDocumentLanes.js.map +1 -0
  144. package/internal/validators/response/containerParentLanes.js +3 -15
  145. package/internal/validators/response/containerParentLanes.js.map +1 -1
  146. package/internal/validators/response/index.d.ts +3 -2
  147. package/internal/validators/response/index.js +2 -2
  148. package/internal/validators/response/index.js.map +1 -1
  149. package/internal/validators/util/blob.d.ts +7 -0
  150. package/internal/validators/util/blob.js +7 -0
  151. package/internal/validators/util/blob.js.map +1 -1
  152. package/internal/validators/util/blobTransferSchemas.d.ts +41 -0
  153. package/internal/validators/util/blobTransferSchemas.js +39 -0
  154. package/internal/validators/util/blobTransferSchemas.js.map +1 -0
  155. package/internal/validators/util/containerLimits.d.ts +7 -0
  156. package/internal/validators/util/containerLimits.js +7 -0
  157. package/internal/validators/util/containerLimits.js.map +1 -1
  158. package/internal/validators/util/index.d.ts +2 -1
  159. package/internal/validators/util/index.js +2 -1
  160. package/internal/validators/util/index.js.map +1 -1
  161. package/package.json +1 -1
  162. package/sync/reconciliation/knownContainerSweep.js +57 -54
  163. package/sync/reconciliation/knownContainerSweep.js.map +1 -1
  164. package/sync/reconciliation/reconcileContainer.js +49 -17
  165. package/sync/reconciliation/reconcileContainer.js.map +1 -1
  166. package/sync/reconciliation/service.js +44 -16
  167. package/sync/reconciliation/service.js.map +1 -1
  168. package/sync/reconciliation/serviceTypes.d.ts +23 -2
  169. package/sync/reconciliation/serviceTypes.js.map +1 -1
  170. package/workflows/blobs/multipartFirstParts.js +82 -0
  171. package/workflows/blobs/multipartFirstParts.js.map +1 -0
  172. package/workflows/blobs/multipartStageResolution.js +83 -0
  173. package/workflows/blobs/multipartStageResolution.js.map +1 -0
  174. package/workflows/blobs/multipartUpload.js +45 -89
  175. package/workflows/blobs/multipartUpload.js.map +1 -1
  176. package/workflows/container-contents/containerDocumentListing.js +92 -38
  177. package/workflows/container-contents/containerDocumentListing.js.map +1 -1
  178. package/workflows/container-contents/containerPersistence.d.ts +8 -2
  179. package/workflows/container-contents/containerPersistence.js +6 -7
  180. package/workflows/container-contents/containerPersistence.js.map +1 -1
  181. package/workflows/container-contents/documentDiscovery.d.ts +3 -3
  182. package/workflows/container-contents/documentDiscovery.js +24 -96
  183. package/workflows/container-contents/documentDiscovery.js.map +1 -1
  184. package/workflows/container-contents/documentDiscoveryApply.js +65 -0
  185. package/workflows/container-contents/documentDiscoveryApply.js.map +1 -0
  186. package/workflows/container-contents/documentDiscoveryLanes.js +53 -0
  187. package/workflows/container-contents/documentDiscoveryLanes.js.map +1 -0
  188. package/workflows/container-contents/documentDiscoveryTypes.d.ts +25 -19
  189. package/workflows/container-contents/documentDiscoveryTypes.js.map +1 -1
  190. package/workflows/container-contents/documentPriming.js +2 -1
  191. package/workflows/container-contents/documentPriming.js.map +1 -1
  192. package/workflows/container-contents/documentQueries/sql.js +4 -4
  193. package/workflows/container-contents/documentQueries/sql.js.map +1 -1
  194. package/workflows/container-contents/pendingWrites/deferredTails.js +6 -3
  195. package/workflows/container-contents/pendingWrites/deferredTails.js.map +1 -1
  196. package/workflows/container-contents/pendingWrites/sources.js +2 -1
  197. package/workflows/container-contents/pendingWrites/sources.js.map +1 -1
  198. package/workflows/container-contents/pendingWrites.js +2 -1
  199. package/workflows/container-contents/pendingWrites.js.map +1 -1
  200. package/workflows/container-contents/remoteHydration/laneFetchMarkers.js +11 -17
  201. package/workflows/container-contents/remoteHydration/laneFetchMarkers.js.map +1 -1
  202. package/workflows/containers/child/create.js +3 -3
  203. package/workflows/containers/child/create.js.map +1 -1
  204. package/workflows/containers/child/move.js +2 -2
  205. package/workflows/containers/child/move.js.map +1 -1
  206. package/workflows/containers/child/mutationRequestCore.js +5 -4
  207. package/workflows/containers/child/mutationRequestCore.js.map +1 -1
  208. package/workflows/containers/child/recitePlan.js +3 -2
  209. package/workflows/containers/child/recitePlan.js.map +1 -1
  210. package/workflows/containers/child/sharePlanCore.js +2 -2
  211. package/workflows/containers/child/sharePlanCore.js.map +1 -1
  212. package/workflows/containers/root/create.js +2 -2
  213. package/workflows/containers/root/create.js.map +1 -1
  214. package/workflows/registration/provisioningBootstrapBuilders.d.ts +20 -46
  215. package/workflows/sync/remoteReset.js +9 -9
  216. package/workflows/sync/remoteReset.js.map +1 -1
  217. package/workflows/sync/remoteResetCursorScope.js +10 -18
  218. package/workflows/sync/remoteResetCursorScope.js.map +1 -1
  219. package/workflows/sync/remoteResetPlans.js +6 -3
  220. package/workflows/sync/remoteResetPlans.js.map +1 -1
  221. package/workflows/sync/remoteResetScope.js +4 -11
  222. package/workflows/sync/remoteResetScope.js.map +1 -1
  223. package/internal/validators/operation/blobTransferSchemas.js +0 -11
  224. package/internal/validators/operation/blobTransferSchemas.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { base64ToBytes } from "../../internal/encoding/index.js";
2
2
  import { createDocument, exportFullHistoryIdentity, importSnapshot, satisfiesVersionVector, } from "../../internal/loro/index.js";
3
- import { and, eq, inArray, sql } from "drizzle-orm";
3
+ import { and, eq, inArray, isNull, ne, sql } from "drizzle-orm";
4
4
  import { documentHistoryCheckpoints, documentHistoryUpdates, documentTables, } from "./schema.js";
5
5
  import { getClientSQLitePersistenceRuntime } from "./sqlitePersistenceRuntime.js";
6
6
  import { ensureSqlTables } from "./sqlSchema.js";
@@ -13,6 +13,21 @@ import { ensureSqlTables } from "./sqlSchema.js";
13
13
  */
14
14
  export const DOCUMENT_HISTORY_COMPACTION_MAX_ROWS = 200;
15
15
  export const DOCUMENT_HISTORY_COMPACTION_MAX_BYTES = 512 * 1024;
16
+ /**
17
+ * Rows of the update log that are history tail: every row except an "outbox"
18
+ * row, which only waits to be sent (see `documentHistoryUpdates`).
19
+ */
20
+ function tailInScope(scope) {
21
+ return and(eq(documentHistoryUpdates.appKind, scope.appKind), eq(documentHistoryUpdates.localId, scope.localId), ne(documentHistoryUpdates.origin, "outbox"));
22
+ }
23
+ /**
24
+ * Tail rows compaction may fold into a checkpoint and delete: a queued row is
25
+ * the only durable copy of an unsent update, so it waits for settlement even
26
+ * when a checkpoint already covers its operations.
27
+ */
28
+ function compactableTailInScope(scope) {
29
+ return and(tailInScope(scope), isNull(documentHistoryUpdates.updateId));
30
+ }
16
31
  export async function appendDocumentHistoryUpdates(execSql, scope, updates, origin) {
17
32
  if (updates.length === 0) {
18
33
  return;
@@ -59,7 +74,7 @@ export async function loadDocumentHistoryRestoreState(execSql, scope) {
59
74
  updateData: documentHistoryUpdates.updateData,
60
75
  })
61
76
  .from(documentHistoryUpdates)
62
- .where(and(eq(documentHistoryUpdates.appKind, scope.appKind), eq(documentHistoryUpdates.localId, scope.localId)))
77
+ .where(tailInScope(scope))
63
78
  // Insertion order: same-millisecond appends make createdAt tie and a
64
79
  // random-uuid tiebreak would replay out of order. importUpdates tolerates
65
80
  // out-of-order batches, but replay should still be deterministic.
@@ -83,6 +98,11 @@ export async function loadDocumentHistoryRestoreState(execSql, scope) {
83
98
  })),
84
99
  };
85
100
  }
101
+ /**
102
+ * Size of the tail compaction can shrink. Queued rows are excluded: they stay
103
+ * until settlement, so counting them would re-export full history on every
104
+ * persist while offline edits wait to sync.
105
+ */
86
106
  export async function readDocumentHistoryTailSize(execSql, scope) {
87
107
  await ensureSqlTables(execSql, documentTables);
88
108
  const { db } = getClientSQLitePersistenceRuntime(execSql);
@@ -92,7 +112,7 @@ export async function readDocumentHistoryTailSize(execSql, scope) {
92
112
  rowCount: sql `count(*)`,
93
113
  })
94
114
  .from(documentHistoryUpdates)
95
- .where(and(eq(documentHistoryUpdates.appKind, scope.appKind), eq(documentHistoryUpdates.localId, scope.localId)));
115
+ .where(compactableTailInScope(scope));
96
116
  const [checkpoint] = await db
97
117
  .select({ localId: documentHistoryCheckpoints.localId })
98
118
  .from(documentHistoryCheckpoints)
@@ -105,9 +125,10 @@ export async function readDocumentHistoryTailSize(execSql, scope) {
105
125
  };
106
126
  }
107
127
  /**
108
- * The current tail rows WITH their payloads, so a compactor can prove each
109
- * row's ops are covered by the snapshot it is about to install (another
110
- * pane's append may carry ops this pane's document has not merged yet).
128
+ * The current compactable tail rows WITH their payloads, so a compactor can
129
+ * prove each row's ops are covered by the snapshot it is about to install
130
+ * (another pane's append may carry ops this pane's document has not merged
131
+ * yet). Queued rows are never candidates.
111
132
  */
112
133
  export async function listDocumentHistoryTailEntries(execSql, scope) {
113
134
  await ensureSqlTables(execSql, documentTables);
@@ -118,7 +139,7 @@ export async function listDocumentHistoryTailEntries(execSql, scope) {
118
139
  updateData: documentHistoryUpdates.updateData,
119
140
  })
120
141
  .from(documentHistoryUpdates)
121
- .where(and(eq(documentHistoryUpdates.appKind, scope.appKind), eq(documentHistoryUpdates.localId, scope.localId)));
142
+ .where(compactableTailInScope(scope));
122
143
  return rows.flatMap((row) => row.id === null ? [] : [{ id: row.id, updateData: row.updateData }]);
123
144
  }
124
145
  const HISTORY_ROW_CHUNK = 400;
@@ -162,9 +183,11 @@ export async function replaceDocumentHistoryCheckpoint(execSql, scope, input) {
162
183
  continue;
163
184
  }
164
185
  for (let index = 0; index < input.coveredTailIds.length; index += HISTORY_ROW_CHUNK) {
186
+ // Never a queued row, even if a caller names one: its operations may
187
+ // be in the checkpoint, but it is still the unsent update itself.
165
188
  await tx
166
189
  .delete(documentHistoryUpdates)
167
- .where(and(eq(documentHistoryUpdates.appKind, scope.appKind), eq(documentHistoryUpdates.localId, scope.localId), inArray(documentHistoryUpdates.id, input.coveredTailIds.slice(index, index + HISTORY_ROW_CHUNK))));
190
+ .where(and(compactableTailInScope(scope), inArray(documentHistoryUpdates.id, input.coveredTailIds.slice(index, index + HISTORY_ROW_CHUNK))));
168
191
  }
169
192
  return true;
170
193
  }
@@ -1 +1 @@
1
- {"version":3,"file":"documentHistoryPersistence.js","sourceRoot":"","sources":["documentHistoryPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { base64ToBytes } from \"@tearleads/encoding\";\nimport {\n createDocument,\n exportFullHistoryIdentity,\n importSnapshot,\n satisfiesVersionVector,\n} from \"@tearleads/loro\";\nimport { and, eq, inArray, sql } from \"drizzle-orm\";\nimport type { DocumentScope } from \"./documentPersistenceTypes\";\nimport {\n documentHistoryCheckpoints,\n documentHistoryUpdates,\n documentTables,\n} from \"./schema\";\nimport { getClientSQLitePersistenceRuntime } from \"./sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"./sqlSchema\";\n\n/**\n * Compaction thresholds: once the tail carries this many rows or bytes, the\n * next persist of a full-history-capable document exports a fresh checkpoint\n * and clears the tail. Small enough to keep restore replay short, large\n * enough that a full-history export (O(history)) happens once per burst of\n * edits instead of per keystroke.\n */\nexport const DOCUMENT_HISTORY_COMPACTION_MAX_ROWS = 200;\nexport const DOCUMENT_HISTORY_COMPACTION_MAX_BYTES = 512 * 1024;\n\n/**\n * Row provenance: \"local\" for updates authored on this device, \"remote\" for\n * decrypted pulled updates. Restores advance the outgoing-delta marker across\n * remote rows (already server-side); local rows never advance it.\n */\ntype DocumentHistoryUpdateOrigin = \"local\" | \"remote\";\n\ninterface DocumentHistoryTailUpdate {\n readonly origin: DocumentHistoryUpdateOrigin;\n readonly updateData: string;\n}\n\ninterface DocumentHistoryRestoreState {\n readonly snapshot: string;\n readonly tailUpdates: readonly DocumentHistoryTailUpdate[];\n}\n\nexport async function appendDocumentHistoryUpdates(\n execSql: ExecSql,\n scope: DocumentScope,\n updates: readonly string[],\n origin: DocumentHistoryUpdateOrigin,\n): Promise<void> {\n if (updates.length === 0) {\n return;\n }\n await ensureSqlTables(execSql, documentTables);\n const createdAt = new Date().toISOString();\n await getClientSQLitePersistenceRuntime(execSql).runMutation(async (db) => {\n // Chunked: a catch-up pull can carry thousands of updates, and a single\n // multi-row insert would exceed SQLite's bound-variable limit.\n for (let index = 0; index < updates.length; index += HISTORY_ROW_CHUNK) {\n await db.insert(documentHistoryUpdates).values(\n updates.slice(index, index + HISTORY_ROW_CHUNK).map((updateData) => ({\n id: crypto.randomUUID(),\n appKind: scope.appKind,\n localId: scope.localId,\n updateData,\n origin,\n createdAt,\n })),\n );\n }\n });\n}\n\n/**\n * The checkpoint + ordered tail for a scope, or null when NEITHER exists (a\n * discovered shell that has not hydrated content yet; the caller starts the\n * document empty and the remote pull supplies the content). A tail WITHOUT a\n * checkpoint — a crash between the first enqueued edit's tail append and the\n * birth-checkpoint seed — restores as tail-only (`snapshot: \"\"`): those tail\n * rows can be the only durable copy of a local edit, so they must never be\n * silently ignored.\n */\nexport async function loadDocumentHistoryRestoreState(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<DocumentHistoryRestoreState | null> {\n await ensureSqlTables(execSql, documentTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n // Read the TAIL before the (full) checkpoint: if another pane compacts\n // between the two reads, this order yields old-tail + new-checkpoint — a\n // safe superset (the new checkpoint subsumes the old tail, and replay is\n // idempotent by op identity). The reverse order could yield old-checkpoint\n // + already-emptied tail, a torn read that silently drops the compacted\n // ops for the whole session.\n const tail = await db\n .select({\n id: documentHistoryUpdates.id,\n origin: documentHistoryUpdates.origin,\n updateData: documentHistoryUpdates.updateData,\n })\n .from(documentHistoryUpdates)\n .where(\n and(\n eq(documentHistoryUpdates.appKind, scope.appKind),\n eq(documentHistoryUpdates.localId, scope.localId),\n ),\n )\n // Insertion order: same-millisecond appends make createdAt tie and a\n // random-uuid tiebreak would replay out of order. importUpdates tolerates\n // out-of-order batches, but replay should still be deterministic.\n .orderBy(sql`rowid`);\n\n const [checkpoint] = await db\n .select({ snapshot: documentHistoryCheckpoints.snapshot })\n .from(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n )\n .limit(1);\n if (!checkpoint && tail.length === 0) {\n return null;\n }\n\n return {\n snapshot: checkpoint?.snapshot ?? \"\",\n tailUpdates: tail.map((row) => ({\n // Fail toward re-sending: an unrecognized origin never advances the\n // marker, which is the recoverable direction (a redundant re-submit\n // is idempotent; a skipped local op would be orphaned from sync).\n origin:\n row.origin === \"remote\" ? (\"remote\" as const) : (\"local\" as const),\n updateData: row.updateData,\n })),\n };\n}\n\nexport async function readDocumentHistoryTailSize(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<{ byteLength: number; hasCheckpoint: boolean; rowCount: number }> {\n await ensureSqlTables(execSql, documentTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const [totals] = await db\n .select({\n byteLength: sql<number>`coalesce(sum(length(${documentHistoryUpdates.updateData})), 0)`,\n rowCount: sql<number>`count(*)`,\n })\n .from(documentHistoryUpdates)\n .where(\n and(\n eq(documentHistoryUpdates.appKind, scope.appKind),\n eq(documentHistoryUpdates.localId, scope.localId),\n ),\n );\n const [checkpoint] = await db\n .select({ localId: documentHistoryCheckpoints.localId })\n .from(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n )\n .limit(1);\n return {\n byteLength: Number(totals?.byteLength ?? 0),\n hasCheckpoint: checkpoint !== undefined,\n rowCount: Number(totals?.rowCount ?? 0),\n };\n}\n\ninterface DocumentHistoryTailEntry {\n readonly id: string;\n readonly updateData: string;\n}\n\n/**\n * The current tail rows WITH their payloads, so a compactor can prove each\n * row's ops are covered by the snapshot it is about to install (another\n * pane's append may carry ops this pane's document has not merged yet).\n */\nexport async function listDocumentHistoryTailEntries(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<DocumentHistoryTailEntry[]> {\n await ensureSqlTables(execSql, documentTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({\n id: documentHistoryUpdates.id,\n updateData: documentHistoryUpdates.updateData,\n })\n .from(documentHistoryUpdates)\n .where(\n and(\n eq(documentHistoryUpdates.appKind, scope.appKind),\n eq(documentHistoryUpdates.localId, scope.localId),\n ),\n );\n return rows.flatMap((row) =>\n row.id === null ? [] : [{ id: row.id, updateData: row.updateData }],\n );\n}\n\nconst HISTORY_ROW_CHUNK = 400;\n\n/**\n * Replace the scope's checkpoint with a fresh full-history snapshot and\n * delete ONLY the tail rows the caller captured before exporting it. Every\n * append path writes its tail row after the ops are already in the live\n * document, so any row captured before the export is provably covered by\n * the snapshot — while a row appended concurrently (between export and this\n * delete) survives for the next compaction instead of being lost.\n */\nexport async function replaceDocumentHistoryCheckpoint(\n execSql: ExecSql,\n scope: DocumentScope,\n input: {\n coveredTailIds: readonly string[];\n endVersionVector: string;\n /**\n * Skip the monotonic version gate. Only for document CREATION, where any\n * existing checkpoint under this localId is the orphan of a failed prior\n * attempt (checkpoint written, record row never committed) — a retry's\n * fresh document need not dominate that orphan to replace it.\n */\n force?: boolean;\n snapshot: string;\n /**\n * Re-checked after acquiring the serialized mutation slot (like the\n * record-save path): a store reset/recreation that happened while this\n * call waited in the queue must abort the write, or a stale compactor\n * could install the OLD document's checkpoint over the new identity's.\n */\n stillCurrent?: () => boolean;\n },\n): Promise<boolean> {\n await ensureSqlTables(execSql, documentTables);\n // Sequential (non-transactional) so callers already inside a transaction\n // can nest this. Crash between the statements leaves the new checkpoint\n // plus covered tail rows — a safe superset, since tail replay is\n // idempotent by op identity. The replacement itself is a compare-and-swap\n // on the stored revision, so the version gate cannot be bypassed by a\n // concurrent compactor between the read and the write: whoever loses the\n // CAS re-reads and re-evaluates the gate against the winner's checkpoint.\n return getClientSQLitePersistenceRuntime(execSql).runMutation(async (tx) => {\n if (input.stillCurrent?.() === false) {\n return false;\n }\n for (let attempt = 0; attempt < CHECKPOINT_CAS_ATTEMPTS; attempt += 1) {\n let replaced: boolean;\n try {\n replaced = await tryReplaceCheckpointOnce(tx, scope, input);\n } catch (error) {\n if (error instanceof CheckpointGateRejected) {\n // A fresher checkpoint dominates this candidate: no write, and\n // the caller's covered rows must stay for the covering pane.\n return false;\n }\n throw error;\n }\n if (!replaced) {\n // CAS lost to a concurrent writer — re-read and re-evaluate the\n // gate against the winner's checkpoint.\n continue;\n }\n for (\n let index = 0;\n index < input.coveredTailIds.length;\n index += HISTORY_ROW_CHUNK\n ) {\n await tx\n .delete(documentHistoryUpdates)\n .where(\n and(\n eq(documentHistoryUpdates.appKind, scope.appKind),\n eq(documentHistoryUpdates.localId, scope.localId),\n inArray(\n documentHistoryUpdates.id,\n input.coveredTailIds.slice(index, index + HISTORY_ROW_CHUNK),\n ),\n ),\n );\n }\n return true;\n }\n // Exhaustion must not read as durable success: creation and rebuild\n // callers persist record rows assuming the checkpoint landed.\n throw new Error(\n \"Document history checkpoint replacement lost every CAS attempt\",\n );\n });\n}\n\nconst CHECKPOINT_CAS_ATTEMPTS = 16;\n\ntype MutationDb = Parameters<\n Parameters<\n ReturnType<typeof getClientSQLitePersistenceRuntime>[\"runMutation\"]\n >[0]\n>[0];\n\n/**\n * One compare-and-swap attempt. Returns true when this caller's checkpoint\n * became the stored one, false when a concurrent writer won the CAS (the\n * caller re-reads and retries), and throws CheckpointGateRejected when a\n * fresher stored checkpoint dominates the candidate.\n */\nasync function tryReplaceCheckpointOnce(\n tx: MutationDb,\n scope: DocumentScope,\n input: {\n endVersionVector: string;\n force?: boolean;\n snapshot: string;\n },\n): Promise<boolean> {\n const updatedAt = new Date().toISOString();\n const revision = crypto.randomUUID();\n const [stored] = await tx\n .select({\n endVersionVector: documentHistoryCheckpoints.endVersionVector,\n revision: documentHistoryCheckpoints.revision,\n snapshot: documentHistoryCheckpoints.snapshot,\n })\n .from(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n )\n .limit(1);\n\n if (!stored) {\n const inserted = await tx\n .insert(documentHistoryCheckpoints)\n .values({\n appKind: scope.appKind,\n localId: scope.localId,\n snapshot: input.snapshot,\n endVersionVector: input.endVersionVector,\n revision,\n updatedAt,\n })\n .onConflictDoNothing({\n target: [\n documentHistoryCheckpoints.appKind,\n documentHistoryCheckpoints.localId,\n ],\n })\n .returning({ localId: documentHistoryCheckpoints.localId });\n // Conflict means a concurrent writer inserted first — retry the gate.\n return inserted.length > 0;\n }\n\n // A stale compactor (a pane whose document has not merged another pane's\n // ops) must never replace a checkpoint it does not cover: the covered\n // pane may already have deleted its tail rows, so regressing the\n // checkpoint would leave those ops with no durable copy at all. Vector\n // dominance proves counter coverage; the retained-prefix identity also\n // proves the candidate carries the exact stored operations at those\n // counters rather than a same-frontier fork.\n if (input.force !== true) {\n if (\n !satisfiesVersionVector(input.endVersionVector, stored.endVersionVector)\n ) {\n throw new CheckpointGateRejected();\n }\n if (\n input.snapshot !== stored.snapshot &&\n !(await candidateContainsStoredCheckpointHistory({\n candidateSnapshot: input.snapshot,\n storedEndVersion: stored.endVersionVector,\n storedSnapshot: stored.snapshot,\n }))\n ) {\n throw new CheckpointGateRejected();\n }\n }\n\n const replaced = await tx\n .update(documentHistoryCheckpoints)\n .set({\n snapshot: input.snapshot,\n endVersionVector: input.endVersionVector,\n revision,\n updatedAt,\n })\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n eq(documentHistoryCheckpoints.revision, stored.revision),\n ),\n )\n .returning({ localId: documentHistoryCheckpoints.localId });\n return replaced.length > 0;\n}\n\nasync function candidateContainsStoredCheckpointHistory(input: {\n candidateSnapshot: string;\n storedEndVersion: string;\n storedSnapshot: string;\n}): Promise<boolean> {\n const [candidateDocument, storedDocument] = await Promise.all([\n createDocument(\"checkpoint-candidate-history-gate\"),\n createDocument(\"checkpoint-stored-history-gate\"),\n ]);\n try {\n importSnapshot(candidateDocument, base64ToBytes(input.candidateSnapshot));\n importSnapshot(storedDocument, base64ToBytes(input.storedSnapshot));\n return (\n exportFullHistoryIdentity(candidateDocument, input.storedEndVersion) ===\n exportFullHistoryIdentity(storedDocument)\n );\n } finally {\n candidateDocument.free();\n storedDocument.free();\n }\n}\n\nclass CheckpointGateRejected extends Error {\n constructor() {\n super(\"checkpoint gate rejected\");\n this.name = \"CheckpointGateRejected\";\n }\n}\n\nexport async function deleteDocumentHistory(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<void> {\n await ensureSqlTables(execSql, documentTables);\n await getClientSQLitePersistenceRuntime(execSql).runMutation(async (tx) => {\n await tx\n .delete(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n );\n await tx\n .delete(documentHistoryUpdates)\n .where(\n and(\n eq(documentHistoryUpdates.appKind, scope.appKind),\n eq(documentHistoryUpdates.localId, scope.localId),\n ),\n );\n });\n}\n"]}
1
+ {"version":3,"file":"documentHistoryPersistence.js","sourceRoot":"","sources":["documentHistoryPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { base64ToBytes } from \"@tearleads/encoding\";\nimport {\n createDocument,\n exportFullHistoryIdentity,\n importSnapshot,\n satisfiesVersionVector,\n} from \"@tearleads/loro\";\nimport { and, eq, inArray, isNull, ne, sql } from \"drizzle-orm\";\nimport type { DocumentScope } from \"./documentPersistenceTypes\";\nimport {\n documentHistoryCheckpoints,\n documentHistoryUpdates,\n documentTables,\n} from \"./schema\";\nimport { getClientSQLitePersistenceRuntime } from \"./sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"./sqlSchema\";\n\n/**\n * Compaction thresholds: once the tail carries this many rows or bytes, the\n * next persist of a full-history-capable document exports a fresh checkpoint\n * and clears the tail. Small enough to keep restore replay short, large\n * enough that a full-history export (O(history)) happens once per burst of\n * edits instead of per keystroke.\n */\nexport const DOCUMENT_HISTORY_COMPACTION_MAX_ROWS = 200;\nexport const DOCUMENT_HISTORY_COMPACTION_MAX_BYTES = 512 * 1024;\n\n/**\n * Row provenance: \"local\" for updates authored on this device, \"remote\" for\n * decrypted pulled updates. Restores advance the outgoing-delta marker across\n * remote rows (already server-side); local rows never advance it.\n */\ntype DocumentHistoryUpdateOrigin = \"local\" | \"remote\";\n\n/**\n * Rows of the update log that are history tail: every row except an \"outbox\"\n * row, which only waits to be sent (see `documentHistoryUpdates`).\n */\nfunction tailInScope(scope: DocumentScope) {\n return and(\n eq(documentHistoryUpdates.appKind, scope.appKind),\n eq(documentHistoryUpdates.localId, scope.localId),\n ne(documentHistoryUpdates.origin, \"outbox\"),\n );\n}\n\n/**\n * Tail rows compaction may fold into a checkpoint and delete: a queued row is\n * the only durable copy of an unsent update, so it waits for settlement even\n * when a checkpoint already covers its operations.\n */\nfunction compactableTailInScope(scope: DocumentScope) {\n return and(tailInScope(scope), isNull(documentHistoryUpdates.updateId));\n}\n\ninterface DocumentHistoryTailUpdate {\n readonly origin: DocumentHistoryUpdateOrigin;\n readonly updateData: string;\n}\n\ninterface DocumentHistoryRestoreState {\n readonly snapshot: string;\n readonly tailUpdates: readonly DocumentHistoryTailUpdate[];\n}\n\nexport async function appendDocumentHistoryUpdates(\n execSql: ExecSql,\n scope: DocumentScope,\n updates: readonly string[],\n origin: DocumentHistoryUpdateOrigin,\n): Promise<void> {\n if (updates.length === 0) {\n return;\n }\n await ensureSqlTables(execSql, documentTables);\n const createdAt = new Date().toISOString();\n await getClientSQLitePersistenceRuntime(execSql).runMutation(async (db) => {\n // Chunked: a catch-up pull can carry thousands of updates, and a single\n // multi-row insert would exceed SQLite's bound-variable limit.\n for (let index = 0; index < updates.length; index += HISTORY_ROW_CHUNK) {\n await db.insert(documentHistoryUpdates).values(\n updates.slice(index, index + HISTORY_ROW_CHUNK).map((updateData) => ({\n id: crypto.randomUUID(),\n appKind: scope.appKind,\n localId: scope.localId,\n updateData,\n origin,\n createdAt,\n })),\n );\n }\n });\n}\n\n/**\n * The checkpoint + ordered tail for a scope, or null when NEITHER exists (a\n * discovered shell that has not hydrated content yet; the caller starts the\n * document empty and the remote pull supplies the content). A tail WITHOUT a\n * checkpoint — a crash between the first enqueued edit's tail append and the\n * birth-checkpoint seed — restores as tail-only (`snapshot: \"\"`): those tail\n * rows can be the only durable copy of a local edit, so they must never be\n * silently ignored.\n */\nexport async function loadDocumentHistoryRestoreState(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<DocumentHistoryRestoreState | null> {\n await ensureSqlTables(execSql, documentTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n // Read the TAIL before the (full) checkpoint: if another pane compacts\n // between the two reads, this order yields old-tail + new-checkpoint — a\n // safe superset (the new checkpoint subsumes the old tail, and replay is\n // idempotent by op identity). The reverse order could yield old-checkpoint\n // + already-emptied tail, a torn read that silently drops the compacted\n // ops for the whole session.\n const tail = await db\n .select({\n id: documentHistoryUpdates.id,\n origin: documentHistoryUpdates.origin,\n updateData: documentHistoryUpdates.updateData,\n })\n .from(documentHistoryUpdates)\n .where(tailInScope(scope))\n // Insertion order: same-millisecond appends make createdAt tie and a\n // random-uuid tiebreak would replay out of order. importUpdates tolerates\n // out-of-order batches, but replay should still be deterministic.\n .orderBy(sql`rowid`);\n\n const [checkpoint] = await db\n .select({ snapshot: documentHistoryCheckpoints.snapshot })\n .from(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n )\n .limit(1);\n if (!checkpoint && tail.length === 0) {\n return null;\n }\n\n return {\n snapshot: checkpoint?.snapshot ?? \"\",\n tailUpdates: tail.map((row) => ({\n // Fail toward re-sending: an unrecognized origin never advances the\n // marker, which is the recoverable direction (a redundant re-submit\n // is idempotent; a skipped local op would be orphaned from sync).\n origin:\n row.origin === \"remote\" ? (\"remote\" as const) : (\"local\" as const),\n updateData: row.updateData,\n })),\n };\n}\n\n/**\n * Size of the tail compaction can shrink. Queued rows are excluded: they stay\n * until settlement, so counting them would re-export full history on every\n * persist while offline edits wait to sync.\n */\nexport async function readDocumentHistoryTailSize(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<{ byteLength: number; hasCheckpoint: boolean; rowCount: number }> {\n await ensureSqlTables(execSql, documentTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const [totals] = await db\n .select({\n byteLength: sql<number>`coalesce(sum(length(${documentHistoryUpdates.updateData})), 0)`,\n rowCount: sql<number>`count(*)`,\n })\n .from(documentHistoryUpdates)\n .where(compactableTailInScope(scope));\n const [checkpoint] = await db\n .select({ localId: documentHistoryCheckpoints.localId })\n .from(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n )\n .limit(1);\n return {\n byteLength: Number(totals?.byteLength ?? 0),\n hasCheckpoint: checkpoint !== undefined,\n rowCount: Number(totals?.rowCount ?? 0),\n };\n}\n\ninterface DocumentHistoryTailEntry {\n readonly id: string;\n readonly updateData: string;\n}\n\n/**\n * The current compactable tail rows WITH their payloads, so a compactor can\n * prove each row's ops are covered by the snapshot it is about to install\n * (another pane's append may carry ops this pane's document has not merged\n * yet). Queued rows are never candidates.\n */\nexport async function listDocumentHistoryTailEntries(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<DocumentHistoryTailEntry[]> {\n await ensureSqlTables(execSql, documentTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({\n id: documentHistoryUpdates.id,\n updateData: documentHistoryUpdates.updateData,\n })\n .from(documentHistoryUpdates)\n .where(compactableTailInScope(scope));\n return rows.flatMap((row) =>\n row.id === null ? [] : [{ id: row.id, updateData: row.updateData }],\n );\n}\n\nconst HISTORY_ROW_CHUNK = 400;\n\n/**\n * Replace the scope's checkpoint with a fresh full-history snapshot and\n * delete ONLY the tail rows the caller captured before exporting it. Every\n * append path writes its tail row after the ops are already in the live\n * document, so any row captured before the export is provably covered by\n * the snapshot — while a row appended concurrently (between export and this\n * delete) survives for the next compaction instead of being lost.\n */\nexport async function replaceDocumentHistoryCheckpoint(\n execSql: ExecSql,\n scope: DocumentScope,\n input: {\n coveredTailIds: readonly string[];\n endVersionVector: string;\n /**\n * Skip the monotonic version gate. Only for document CREATION, where any\n * existing checkpoint under this localId is the orphan of a failed prior\n * attempt (checkpoint written, record row never committed) — a retry's\n * fresh document need not dominate that orphan to replace it.\n */\n force?: boolean;\n snapshot: string;\n /**\n * Re-checked after acquiring the serialized mutation slot (like the\n * record-save path): a store reset/recreation that happened while this\n * call waited in the queue must abort the write, or a stale compactor\n * could install the OLD document's checkpoint over the new identity's.\n */\n stillCurrent?: () => boolean;\n },\n): Promise<boolean> {\n await ensureSqlTables(execSql, documentTables);\n // Sequential (non-transactional) so callers already inside a transaction\n // can nest this. Crash between the statements leaves the new checkpoint\n // plus covered tail rows — a safe superset, since tail replay is\n // idempotent by op identity. The replacement itself is a compare-and-swap\n // on the stored revision, so the version gate cannot be bypassed by a\n // concurrent compactor between the read and the write: whoever loses the\n // CAS re-reads and re-evaluates the gate against the winner's checkpoint.\n return getClientSQLitePersistenceRuntime(execSql).runMutation(async (tx) => {\n if (input.stillCurrent?.() === false) {\n return false;\n }\n for (let attempt = 0; attempt < CHECKPOINT_CAS_ATTEMPTS; attempt += 1) {\n let replaced: boolean;\n try {\n replaced = await tryReplaceCheckpointOnce(tx, scope, input);\n } catch (error) {\n if (error instanceof CheckpointGateRejected) {\n // A fresher checkpoint dominates this candidate: no write, and\n // the caller's covered rows must stay for the covering pane.\n return false;\n }\n throw error;\n }\n if (!replaced) {\n // CAS lost to a concurrent writer — re-read and re-evaluate the\n // gate against the winner's checkpoint.\n continue;\n }\n for (\n let index = 0;\n index < input.coveredTailIds.length;\n index += HISTORY_ROW_CHUNK\n ) {\n // Never a queued row, even if a caller names one: its operations may\n // be in the checkpoint, but it is still the unsent update itself.\n await tx\n .delete(documentHistoryUpdates)\n .where(\n and(\n compactableTailInScope(scope),\n inArray(\n documentHistoryUpdates.id,\n input.coveredTailIds.slice(index, index + HISTORY_ROW_CHUNK),\n ),\n ),\n );\n }\n return true;\n }\n // Exhaustion must not read as durable success: creation and rebuild\n // callers persist record rows assuming the checkpoint landed.\n throw new Error(\n \"Document history checkpoint replacement lost every CAS attempt\",\n );\n });\n}\n\nconst CHECKPOINT_CAS_ATTEMPTS = 16;\n\ntype MutationDb = Parameters<\n Parameters<\n ReturnType<typeof getClientSQLitePersistenceRuntime>[\"runMutation\"]\n >[0]\n>[0];\n\n/**\n * One compare-and-swap attempt. Returns true when this caller's checkpoint\n * became the stored one, false when a concurrent writer won the CAS (the\n * caller re-reads and retries), and throws CheckpointGateRejected when a\n * fresher stored checkpoint dominates the candidate.\n */\nasync function tryReplaceCheckpointOnce(\n tx: MutationDb,\n scope: DocumentScope,\n input: {\n endVersionVector: string;\n force?: boolean;\n snapshot: string;\n },\n): Promise<boolean> {\n const updatedAt = new Date().toISOString();\n const revision = crypto.randomUUID();\n const [stored] = await tx\n .select({\n endVersionVector: documentHistoryCheckpoints.endVersionVector,\n revision: documentHistoryCheckpoints.revision,\n snapshot: documentHistoryCheckpoints.snapshot,\n })\n .from(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n )\n .limit(1);\n\n if (!stored) {\n const inserted = await tx\n .insert(documentHistoryCheckpoints)\n .values({\n appKind: scope.appKind,\n localId: scope.localId,\n snapshot: input.snapshot,\n endVersionVector: input.endVersionVector,\n revision,\n updatedAt,\n })\n .onConflictDoNothing({\n target: [\n documentHistoryCheckpoints.appKind,\n documentHistoryCheckpoints.localId,\n ],\n })\n .returning({ localId: documentHistoryCheckpoints.localId });\n // Conflict means a concurrent writer inserted first — retry the gate.\n return inserted.length > 0;\n }\n\n // A stale compactor (a pane whose document has not merged another pane's\n // ops) must never replace a checkpoint it does not cover: the covered\n // pane may already have deleted its tail rows, so regressing the\n // checkpoint would leave those ops with no durable copy at all. Vector\n // dominance proves counter coverage; the retained-prefix identity also\n // proves the candidate carries the exact stored operations at those\n // counters rather than a same-frontier fork.\n if (input.force !== true) {\n if (\n !satisfiesVersionVector(input.endVersionVector, stored.endVersionVector)\n ) {\n throw new CheckpointGateRejected();\n }\n if (\n input.snapshot !== stored.snapshot &&\n !(await candidateContainsStoredCheckpointHistory({\n candidateSnapshot: input.snapshot,\n storedEndVersion: stored.endVersionVector,\n storedSnapshot: stored.snapshot,\n }))\n ) {\n throw new CheckpointGateRejected();\n }\n }\n\n const replaced = await tx\n .update(documentHistoryCheckpoints)\n .set({\n snapshot: input.snapshot,\n endVersionVector: input.endVersionVector,\n revision,\n updatedAt,\n })\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n eq(documentHistoryCheckpoints.revision, stored.revision),\n ),\n )\n .returning({ localId: documentHistoryCheckpoints.localId });\n return replaced.length > 0;\n}\n\nasync function candidateContainsStoredCheckpointHistory(input: {\n candidateSnapshot: string;\n storedEndVersion: string;\n storedSnapshot: string;\n}): Promise<boolean> {\n const [candidateDocument, storedDocument] = await Promise.all([\n createDocument(\"checkpoint-candidate-history-gate\"),\n createDocument(\"checkpoint-stored-history-gate\"),\n ]);\n try {\n importSnapshot(candidateDocument, base64ToBytes(input.candidateSnapshot));\n importSnapshot(storedDocument, base64ToBytes(input.storedSnapshot));\n return (\n exportFullHistoryIdentity(candidateDocument, input.storedEndVersion) ===\n exportFullHistoryIdentity(storedDocument)\n );\n } finally {\n candidateDocument.free();\n storedDocument.free();\n }\n}\n\nclass CheckpointGateRejected extends Error {\n constructor() {\n super(\"checkpoint gate rejected\");\n this.name = \"CheckpointGateRejected\";\n }\n}\n\nexport async function deleteDocumentHistory(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<void> {\n await ensureSqlTables(execSql, documentTables);\n await getClientSQLitePersistenceRuntime(execSql).runMutation(async (tx) => {\n await tx\n .delete(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n );\n await tx\n .delete(documentHistoryUpdates)\n .where(\n and(\n eq(documentHistoryUpdates.appKind, scope.appKind),\n eq(documentHistoryUpdates.localId, scope.localId),\n ),\n );\n });\n}\n"]}
@@ -1,14 +1,48 @@
1
1
  import type { DocumentScope, PendingUpdateFields, PendingUpdateRecord } from "./documentPersistenceTypes.js";
2
2
  import type { ClientSQLiteTransactionScope } from "./sqlitePersistenceRuntime.js";
3
3
  import type { ExecSql } from "./sqlSchema.js";
4
+ /**
5
+ * Queued updates are the rows of the local update log that carry an
6
+ * `updateId`. A tail row (origin "local") stays in the log after settlement as
7
+ * history until compaction; an "outbox" row has no tail role and settlement
8
+ * deletes it.
9
+ */
10
+ declare const OUTBOX_ORIGIN = "outbox";
11
+ type QueueStatement = Pick<ClientSQLiteTransactionScope, "delete" | "update">;
12
+ type QueueRead = Pick<ClientSQLiteTransactionScope, "selectDistinct">;
13
+ /** The ids among `localIds` (one app kind) with at least one queued update. */
14
+ export declare function listLocalIdsWithQueuedUpdates(db: QueueRead, appKind: string, localIds: readonly string[]): Promise<string[]>;
15
+ export declare function hasQueuedDocumentUpdates(db: QueueRead, scope: DocumentScope): Promise<boolean>;
4
16
  export declare function listDocumentPendingUpdates(execSql: ExecSql, scope: DocumentScope): Promise<PendingUpdateRecord[]>;
17
+ /** One log row for a queued update; `origin` decides its tail role. */
18
+ export declare function queuedUpdateRow(input: {
19
+ createdAt: string;
20
+ origin: "local" | typeof OUTBOX_ORIGIN;
21
+ pendingUpdate: PendingUpdateFields;
22
+ scope: DocumentScope;
23
+ updateId: string;
24
+ }): {
25
+ id: `${string}-${string}-${string}-${string}-${string}`;
26
+ appKind: string;
27
+ localId: string;
28
+ updateData: string;
29
+ origin: "local" | "outbox";
30
+ createdAt: string;
31
+ updateId: string;
32
+ partialStartVersionVector: string;
33
+ partialEndVersionVector: string;
34
+ sourceVersionVector: string | null;
35
+ };
36
+ /**
37
+ * Queue an update that has no history-tail role: container metadata (its
38
+ * content is its checkpoint) and full-state re-uploads after a remote reset.
39
+ */
5
40
  export declare function enqueueDocumentPendingUpdate(execSql: ExecSql, scope: DocumentScope, pendingUpdate: PendingUpdateFields): Promise<string>;
6
41
  /**
7
- * Enqueue an outgoing update AND append it to the durable-history tail in a
8
- * single transaction. Serialization alone is not enough: a crash between
9
- * separate inserts could leave the op restorable from history while missing
10
- * from the outgoing queue, and a null-marker restore would then classify it
11
- * as covered and never send it.
42
+ * Append a locally authored update to the history tail AND queue it, as one
43
+ * row in one transaction. A crash can therefore never leave the op restorable
44
+ * from history while missing from the outgoing queue, where a null-marker
45
+ * restore would classify it as covered and never send it.
12
46
  */
13
47
  export declare function enqueueDocumentPendingUpdateWithHistory(execSql: ExecSql, scope: DocumentScope, pendingUpdate: PendingUpdateFields, options?: {
14
48
  expectedDocumentId: string | null;
@@ -21,7 +55,7 @@ export declare function insertDocumentPendingUpdateWithHistoryInTransaction(inpu
21
55
  tx: ClientSQLiteTransactionScope;
22
56
  }): Promise<string>;
23
57
  /**
24
- * Assign a fresh id to a pending update, keeping every other column. A lost
58
+ * Assign a fresh id to a queued update, keeping every other column. A lost
25
59
  * sync ack leaves the server holding the update under the original id with
26
60
  * ciphertext a rebuilt submission can never reproduce (each rebuild encrypts
27
61
  * with a fresh IV), so the id is permanently poisoned: every resubmission
@@ -32,8 +66,8 @@ export declare function insertDocumentPendingUpdateWithHistoryInTransaction(inpu
32
66
  * Returns null when no row carries the id, so callers never report progress
33
67
  * for a row that lives in some other queue (or nowhere). The UPDATE's own
34
68
  * RETURNING row is the progress signal — a pre-flight SELECT would race a
35
- * second tab sharing the SQLite file, which can settle-delete the row between
36
- * the read and the write and turn "rekeyed" into a spurious re-arm.
69
+ * second tab sharing the SQLite file, which can settle the row between the
70
+ * read and the write and turn "rekeyed" into a spurious re-arm.
37
71
  *
38
72
  * The persisted per-row bound is enforced here as well: a row already at
39
73
  * MAX_PENDING_UPDATE_REKEYS is left untouched and reported as no progress.
@@ -43,7 +77,7 @@ export declare function insertDocumentPendingUpdateWithHistoryInTransaction(inpu
43
77
  */
44
78
  export declare const MAX_PENDING_UPDATE_REKEYS = 5;
45
79
  /**
46
- * Reset the durable re-key budget for every pending update in a scope. The
80
+ * Reset the durable re-key budget for every queued update in a scope. The
47
81
  * cap exists to stop a server that keeps conflicting fresh ids from driving
48
82
  * a network-speed loop — but a budget burned during an unrelated outage
49
83
  * (e.g. yesterday's stale-bundle deadlock 409ing every submit) should not be
@@ -52,5 +86,18 @@ export declare const MAX_PENDING_UPDATE_REKEYS = 5;
52
86
  */
53
87
  export declare function resetDocumentPendingUpdateRekeyBudget(execSql: ExecSql, scope: DocumentScope): Promise<void>;
54
88
  export declare function rekeyDocumentPendingUpdate(execSql: ExecSql, id: string): Promise<string | null>;
55
- export declare function deleteDocumentPendingUpdate(execSql: ExecSql, id: string): Promise<void>;
56
- export declare function deleteDocumentPendingUpdates(execSql: ExecSql, scope: DocumentScope): Promise<void>;
89
+ /**
90
+ * Settle queued updates: an outbox row is deleted, and a tail row stops being
91
+ * queued but stays as history until compaction folds it into a checkpoint.
92
+ */
93
+ export declare function settleQueuedDocumentUpdatesInTransaction(tx: QueueStatement, scope: DocumentScope, updateIds: readonly string[]): Promise<void>;
94
+ /** Settle one queued update by id, whatever its scope. */
95
+ export declare function settleDocumentPendingUpdate(execSql: ExecSql, id: string): Promise<void>;
96
+ /**
97
+ * Stop queueing every update in the given scopes: outbox rows are dropped,
98
+ * and local tail rows keep their content as history (it is already in the
99
+ * document).
100
+ */
101
+ export declare function unqueueDocumentScopesInTransaction(tx: QueueStatement, scopes: readonly DocumentScope[]): Promise<void>;
102
+ export declare function unqueueDocumentPendingUpdates(execSql: ExecSql, scope: DocumentScope): Promise<void>;
103
+ export {};
@@ -1,7 +1,23 @@
1
- import { and, asc, eq, lt, sql } from "drizzle-orm";
2
- import { documentHistoryUpdates, documentPendingUpdates, documents, } from "./schema.js";
1
+ import { and, asc, eq, inArray, isNotNull, lt, or, sql, } from "drizzle-orm";
2
+ import { documentHistoryUpdates, documents } from "./schema.js";
3
3
  import { getClientSQLitePersistenceRuntime } from "./sqlitePersistenceRuntime.js";
4
+ /**
5
+ * Queued updates are the rows of the local update log that carry an
6
+ * `updateId`. A tail row (origin "local") stays in the log after settlement as
7
+ * history until compaction; an "outbox" row has no tail role and settlement
8
+ * deletes it.
9
+ */
10
+ const OUTBOX_ORIGIN = "outbox";
11
+ function queuedInScope(scope) {
12
+ return and(eq(documentHistoryUpdates.appKind, scope.appKind), eq(documentHistoryUpdates.localId, scope.localId), isNotNull(documentHistoryUpdates.updateId));
13
+ }
4
14
  function mapSelectedPendingUpdate(row) {
15
+ // A queued row always carries its submission vectors; a row without them
16
+ // cannot be submitted and must fail closed rather than send empty ranges.
17
+ if (row.partialStartVersionVector === null ||
18
+ row.partialEndVersionVector === null) {
19
+ throw new Error("Queued document update is missing its version vectors");
20
+ }
5
21
  return {
6
22
  id: String(row.id ?? ""),
7
23
  updateData: row.updateData,
@@ -11,50 +27,80 @@ function mapSelectedPendingUpdate(row) {
11
27
  rekeyCount: row.rekeyCount,
12
28
  };
13
29
  }
30
+ /** The ids among `localIds` (one app kind) with at least one queued update. */
31
+ export async function listLocalIdsWithQueuedUpdates(db, appKind, localIds) {
32
+ const uniqueIds = [...new Set(localIds)];
33
+ if (uniqueIds.length === 0)
34
+ return [];
35
+ const rows = await db
36
+ .selectDistinct({ localId: documentHistoryUpdates.localId })
37
+ .from(documentHistoryUpdates)
38
+ .where(and(eq(documentHistoryUpdates.appKind, appKind), inArray(documentHistoryUpdates.localId, uniqueIds), isNotNull(documentHistoryUpdates.updateId)))
39
+ .orderBy(asc(documentHistoryUpdates.localId));
40
+ return rows.map((row) => row.localId);
41
+ }
42
+ export async function hasQueuedDocumentUpdates(db, scope) {
43
+ const ids = await listLocalIdsWithQueuedUpdates(db, scope.appKind, [
44
+ scope.localId,
45
+ ]);
46
+ return ids.length > 0;
47
+ }
14
48
  export async function listDocumentPendingUpdates(execSql, scope) {
15
49
  const { db } = getClientSQLitePersistenceRuntime(execSql);
16
50
  const rows = await db
17
51
  .select({
18
- id: documentPendingUpdates.id,
19
- updateData: documentPendingUpdates.updateData,
20
- partialStartVersionVector: documentPendingUpdates.partialStartVersionVector,
21
- partialEndVersionVector: documentPendingUpdates.partialEndVersionVector,
22
- sourceVersionVector: documentPendingUpdates.sourceVersionVector,
23
- rekeyCount: documentPendingUpdates.rekeyCount,
52
+ id: documentHistoryUpdates.updateId,
53
+ updateData: documentHistoryUpdates.updateData,
54
+ partialStartVersionVector: documentHistoryUpdates.partialStartVersionVector,
55
+ partialEndVersionVector: documentHistoryUpdates.partialEndVersionVector,
56
+ sourceVersionVector: documentHistoryUpdates.sourceVersionVector,
57
+ rekeyCount: documentHistoryUpdates.rekeyCount,
24
58
  })
25
- .from(documentPendingUpdates)
26
- .where(and(eq(documentPendingUpdates.appKind, scope.appKind), eq(documentPendingUpdates.localId, scope.localId)))
27
- .orderBy(asc(documentPendingUpdates.createdAt), asc(sql `rowid`));
59
+ .from(documentHistoryUpdates)
60
+ .where(queuedInScope(scope))
61
+ .orderBy(asc(documentHistoryUpdates.createdAt), asc(sql `rowid`));
28
62
  return rows.map(mapSelectedPendingUpdate);
29
63
  }
30
- function pendingUpdateRow(scope, pendingUpdate, createdAt, id = crypto.randomUUID()) {
64
+ /** One log row for a queued update; `origin` decides its tail role. */
65
+ export function queuedUpdateRow(input) {
31
66
  return {
32
- id,
33
- appKind: scope.appKind,
34
- localId: scope.localId,
35
- updateData: pendingUpdate.updateData,
36
- partialStartVersionVector: pendingUpdate.partialStartVersionVector,
37
- partialEndVersionVector: pendingUpdate.partialEndVersionVector,
38
- sourceVersionVector: pendingUpdate.sourceVersionVector ?? null,
39
- createdAt,
67
+ id: crypto.randomUUID(),
68
+ appKind: input.scope.appKind,
69
+ localId: input.scope.localId,
70
+ updateData: input.pendingUpdate.updateData,
71
+ origin: input.origin,
72
+ createdAt: input.createdAt,
73
+ updateId: input.updateId,
74
+ partialStartVersionVector: input.pendingUpdate.partialStartVersionVector,
75
+ partialEndVersionVector: input.pendingUpdate.partialEndVersionVector,
76
+ sourceVersionVector: input.pendingUpdate.sourceVersionVector ?? null,
40
77
  };
41
78
  }
79
+ /**
80
+ * Queue an update that has no history-tail role: container metadata (its
81
+ * content is its checkpoint) and full-state re-uploads after a remote reset.
82
+ */
42
83
  export async function enqueueDocumentPendingUpdate(execSql, scope, pendingUpdate) {
43
- const id = crypto.randomUUID();
84
+ const updateId = crypto.randomUUID();
44
85
  await getClientSQLitePersistenceRuntime(execSql).runMutation(async (db) => {
45
86
  await db
46
- .insert(documentPendingUpdates)
47
- .values(pendingUpdateRow(scope, pendingUpdate, new Date().toISOString(), id))
87
+ .insert(documentHistoryUpdates)
88
+ .values(queuedUpdateRow({
89
+ createdAt: new Date().toISOString(),
90
+ origin: OUTBOX_ORIGIN,
91
+ pendingUpdate,
92
+ scope,
93
+ updateId,
94
+ }))
48
95
  .run();
49
96
  });
50
- return id;
97
+ return updateId;
51
98
  }
52
99
  /**
53
- * Enqueue an outgoing update AND append it to the durable-history tail in a
54
- * single transaction. Serialization alone is not enough: a crash between
55
- * separate inserts could leave the op restorable from history while missing
56
- * from the outgoing queue, and a null-marker restore would then classify it
57
- * as covered and never send it.
100
+ * Append a locally authored update to the history tail AND queue it, as one
101
+ * row in one transaction. A crash can therefore never leave the op restorable
102
+ * from history while missing from the outgoing queue, where a null-marker
103
+ * restore would classify it as covered and never send it.
58
104
  */
59
105
  export async function enqueueDocumentPendingUpdateWithHistory(execSql, scope, pendingUpdate, options) {
60
106
  const createdAt = new Date().toISOString();
@@ -78,26 +124,21 @@ export async function enqueueDocumentPendingUpdateWithHistory(execSql, scope, pe
78
124
  }, { behavior: "immediate" });
79
125
  }
80
126
  export async function insertDocumentPendingUpdateWithHistoryInTransaction(input) {
81
- const id = crypto.randomUUID();
127
+ const updateId = crypto.randomUUID();
82
128
  await input.tx
83
129
  .insert(documentHistoryUpdates)
84
- .values({
85
- id: crypto.randomUUID(),
86
- appKind: input.scope.appKind,
87
- localId: input.scope.localId,
88
- updateData: input.pendingUpdate.updateData,
89
- origin: "local",
130
+ .values(queuedUpdateRow({
90
131
  createdAt: input.createdAt,
91
- })
92
- .run();
93
- await input.tx
94
- .insert(documentPendingUpdates)
95
- .values(pendingUpdateRow(input.scope, input.pendingUpdate, input.createdAt, id))
132
+ origin: "local",
133
+ pendingUpdate: input.pendingUpdate,
134
+ scope: input.scope,
135
+ updateId,
136
+ }))
96
137
  .run();
97
- return id;
138
+ return updateId;
98
139
  }
99
140
  /**
100
- * Assign a fresh id to a pending update, keeping every other column. A lost
141
+ * Assign a fresh id to a queued update, keeping every other column. A lost
101
142
  * sync ack leaves the server holding the update under the original id with
102
143
  * ciphertext a rebuilt submission can never reproduce (each rebuild encrypts
103
144
  * with a fresh IV), so the id is permanently poisoned: every resubmission
@@ -108,8 +149,8 @@ export async function insertDocumentPendingUpdateWithHistoryInTransaction(input)
108
149
  * Returns null when no row carries the id, so callers never report progress
109
150
  * for a row that lives in some other queue (or nowhere). The UPDATE's own
110
151
  * RETURNING row is the progress signal — a pre-flight SELECT would race a
111
- * second tab sharing the SQLite file, which can settle-delete the row between
112
- * the read and the write and turn "rekeyed" into a spurious re-arm.
152
+ * second tab sharing the SQLite file, which can settle the row between the
153
+ * read and the write and turn "rekeyed" into a spurious re-arm.
113
154
  *
114
155
  * The persisted per-row bound is enforced here as well: a row already at
115
156
  * MAX_PENDING_UPDATE_REKEYS is left untouched and reported as no progress.
@@ -119,7 +160,7 @@ export async function insertDocumentPendingUpdateWithHistoryInTransaction(input)
119
160
  */
120
161
  export const MAX_PENDING_UPDATE_REKEYS = 5;
121
162
  /**
122
- * Reset the durable re-key budget for every pending update in a scope. The
163
+ * Reset the durable re-key budget for every queued update in a scope. The
123
164
  * cap exists to stop a server that keeps conflicting fresh ids from driving
124
165
  * a network-speed loop — but a budget burned during an unrelated outage
125
166
  * (e.g. yesterday's stale-bundle deadlock 409ing every submit) should not be
@@ -129,9 +170,9 @@ export const MAX_PENDING_UPDATE_REKEYS = 5;
129
170
  export async function resetDocumentPendingUpdateRekeyBudget(execSql, scope) {
130
171
  await getClientSQLitePersistenceRuntime(execSql).runMutation(async (db) => {
131
172
  await db
132
- .update(documentPendingUpdates)
173
+ .update(documentHistoryUpdates)
133
174
  .set({ rekeyCount: 0 })
134
- .where(and(eq(documentPendingUpdates.appKind, scope.appKind), eq(documentPendingUpdates.localId, scope.localId)));
175
+ .where(queuedInScope(scope));
135
176
  });
136
177
  }
137
178
  export async function rekeyDocumentPendingUpdate(execSql, id) {
@@ -139,31 +180,64 @@ export async function rekeyDocumentPendingUpdate(execSql, id) {
139
180
  let rekeyed = false;
140
181
  await getClientSQLitePersistenceRuntime(execSql).runMutation(async (db) => {
141
182
  const updated = await db
142
- .update(documentPendingUpdates)
183
+ .update(documentHistoryUpdates)
143
184
  .set({
144
- id: nextId,
145
- rekeyCount: sql `${documentPendingUpdates.rekeyCount} + 1`,
185
+ updateId: nextId,
186
+ rekeyCount: sql `${documentHistoryUpdates.rekeyCount} + 1`,
146
187
  })
147
- .where(and(eq(documentPendingUpdates.id, id), lt(documentPendingUpdates.rekeyCount, MAX_PENDING_UPDATE_REKEYS)))
148
- .returning({ id: documentPendingUpdates.id });
188
+ .where(and(eq(documentHistoryUpdates.updateId, id), lt(documentHistoryUpdates.rekeyCount, MAX_PENDING_UPDATE_REKEYS)))
189
+ .returning({ id: documentHistoryUpdates.updateId });
149
190
  rekeyed = updated.length > 0;
150
191
  });
151
192
  return rekeyed ? nextId : null;
152
193
  }
153
- export async function deleteDocumentPendingUpdate(execSql, id) {
194
+ /**
195
+ * Settle queued updates: an outbox row is deleted, and a tail row stops being
196
+ * queued but stays as history until compaction folds it into a checkpoint.
197
+ */
198
+ export async function settleQueuedDocumentUpdatesInTransaction(tx, scope, updateIds) {
199
+ const uniqueIds = [...new Set(updateIds)];
200
+ if (uniqueIds.length === 0)
201
+ return;
202
+ const selected = and(eq(documentHistoryUpdates.appKind, scope.appKind), eq(documentHistoryUpdates.localId, scope.localId), inArray(documentHistoryUpdates.updateId, uniqueIds));
203
+ await unqueueSelectedUpdates(tx, selected);
204
+ }
205
+ async function unqueueSelectedUpdates(tx, selected) {
206
+ await tx
207
+ .delete(documentHistoryUpdates)
208
+ .where(and(selected, eq(documentHistoryUpdates.origin, OUTBOX_ORIGIN)))
209
+ .run();
210
+ await tx
211
+ .update(documentHistoryUpdates)
212
+ .set({
213
+ updateId: null,
214
+ partialStartVersionVector: null,
215
+ partialEndVersionVector: null,
216
+ sourceVersionVector: null,
217
+ rekeyCount: 0,
218
+ })
219
+ .where(selected)
220
+ .run();
221
+ }
222
+ /** Settle one queued update by id, whatever its scope. */
223
+ export async function settleDocumentPendingUpdate(execSql, id) {
154
224
  await getClientSQLitePersistenceRuntime(execSql).runMutation(async (db) => {
155
- await db
156
- .delete(documentPendingUpdates)
157
- .where(eq(documentPendingUpdates.id, id))
158
- .run();
225
+ await unqueueSelectedUpdates(db, eq(documentHistoryUpdates.updateId, id));
159
226
  });
160
227
  }
161
- export async function deleteDocumentPendingUpdates(execSql, scope) {
228
+ /**
229
+ * Stop queueing every update in the given scopes: outbox rows are dropped,
230
+ * and local tail rows keep their content as history (it is already in the
231
+ * document).
232
+ */
233
+ export async function unqueueDocumentScopesInTransaction(tx, scopes) {
234
+ if (scopes.length === 0)
235
+ return;
236
+ await unqueueSelectedUpdates(tx, or(...scopes.map(queuedInScope)));
237
+ }
238
+ export async function unqueueDocumentPendingUpdates(execSql, scope) {
162
239
  await getClientSQLitePersistenceRuntime(execSql).runMutation(async (db) => {
163
- await db
164
- .delete(documentPendingUpdates)
165
- .where(and(eq(documentPendingUpdates.appKind, scope.appKind), eq(documentPendingUpdates.localId, scope.localId)))
166
- .run();
240
+ await unqueueDocumentScopesInTransaction(db, [scope]);
167
241
  });
168
242
  }
169
243
  //# sourceMappingURL=documentPendingUpdatePersistence.js.map