@tearleads/client-sdk 0.1.114 → 0.1.116

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 (80) hide show
  1. package/client/containerContents/documentDiscovery.js +5 -3
  2. package/client/containerContents/documentDiscovery.js.map +1 -1
  3. package/data/persistence/container-contents/containerRecoveryPersistence.js +152 -38
  4. package/data/persistence/container-contents/containerRecoveryPersistence.js.map +1 -1
  5. package/data/persistence/container-contents/documentContainerProjectionPersistence.js +47 -18
  6. package/data/persistence/container-contents/documentContainerProjectionPersistence.js.map +1 -1
  7. package/data/persistence/container-contents/documentPlacement.js +24 -5
  8. package/data/persistence/container-contents/documentPlacement.js.map +1 -1
  9. package/data/persistence/documents/containerDocumentTombstoneHoldsPersistence.js +1 -1
  10. package/data/persistence/documents/containerDocumentTombstoneHoldsPersistence.js.map +1 -1
  11. package/data/persistence/documents/documentDiscoveryEvidencePersistence.js +43 -35
  12. package/data/persistence/documents/documentDiscoveryEvidencePersistence.js.map +1 -1
  13. package/data/persistence/documents/documentsPersistence.js +4 -58
  14. package/data/persistence/documents/documentsPersistence.js.map +1 -1
  15. package/data/persistence/documents/internal/containerDocumentTombstoneHolds.js +15 -11
  16. package/data/persistence/documents/internal/containerDocumentTombstoneHolds.js.map +1 -1
  17. package/data/persistence/documents/internal/containerDocumentTombstones.js +87 -35
  18. package/data/persistence/documents/internal/containerDocumentTombstones.js.map +1 -1
  19. package/data/persistence/documents/internal/discoveredDocumentPlacement.js +5 -2
  20. package/data/persistence/documents/internal/discoveredDocumentPlacement.js.map +1 -1
  21. package/data/persistence/documents/internal/discoveredDocumentUpsert.js +174 -0
  22. package/data/persistence/documents/internal/discoveredDocumentUpsert.js.map +1 -0
  23. package/data/persistence/documents/internal/documentRows.js +96 -1
  24. package/data/persistence/documents/internal/documentRows.js.map +1 -1
  25. package/data/persistence/documents/internal/rowQueryPersistence.js +38 -20
  26. package/data/persistence/documents/internal/rowQueryPersistence.js.map +1 -1
  27. package/data/persistence/documents/records/documentRecordPersistence.d.ts +5 -0
  28. package/data/persistence/documents/records/documentRecordPersistence.js +41 -6
  29. package/data/persistence/documents/records/documentRecordPersistence.js.map +1 -1
  30. package/data/persistence/documents/records/index.d.ts +1 -1
  31. package/data/persistence/documents/records/index.js +1 -1
  32. package/data/persistence/documents/records/index.js.map +1 -1
  33. package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.d.ts +2 -0
  34. package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.js +17 -1
  35. package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.js.map +1 -1
  36. package/internal/api-client/ApiClient.d.ts +118 -0
  37. package/internal/api-client/ApiClient.js +4 -0
  38. package/internal/api-client/ApiClient.js.map +1 -1
  39. package/internal/api-client/index.d.ts +1 -1
  40. package/internal/api-client/index.js.map +1 -1
  41. package/internal/api-client/principalPolicyPages.d.ts +15 -1
  42. package/internal/api-client/principalPolicyPages.js +48 -2
  43. package/internal/api-client/principalPolicyPages.js.map +1 -1
  44. package/internal/api-client/principalPolicyRequests.d.ts +118 -1
  45. package/internal/api-client/principalPolicyRequests.js +4 -1
  46. package/internal/api-client/principalPolicyRequests.js.map +1 -1
  47. package/internal/validators/operation/principalPolicyPageQuery.d.ts +9 -0
  48. package/internal/validators/operation/principalPolicyPageQuery.js +10 -0
  49. package/internal/validators/operation/principalPolicyPageQuery.js.map +1 -1
  50. package/internal/validators/operation/principals.d.ts +37 -4
  51. package/internal/validators/operation/principals.js +5 -3
  52. package/internal/validators/operation/principals.js.map +1 -1
  53. package/internal/validators/response/index.d.ts +1 -1
  54. package/internal/validators/response/index.js +1 -1
  55. package/internal/validators/response/index.js.map +1 -1
  56. package/internal/validators/response/principalPolicyPage.d.ts +497 -1
  57. package/internal/validators/response/principalPolicyPage.js +18 -0
  58. package/internal/validators/response/principalPolicyPage.js.map +1 -1
  59. package/package.json +1 -1
  60. package/workflows/container-contents/documentLinks/index.d.ts +4 -0
  61. package/workflows/container-contents/documentLinks/index.js +10 -3
  62. package/workflows/container-contents/documentLinks/index.js.map +1 -1
  63. package/workflows/container-contents/remoteHydration/remoteContainers/remoteContainerPageHydration.js +7 -2
  64. package/workflows/container-contents/remoteHydration/remoteContainers/remoteContainerPageHydration.js.map +1 -1
  65. package/workflows/container-contents/remoteReset/containerRemoteReset.js +12 -7
  66. package/workflows/container-contents/remoteReset/containerRemoteReset.js.map +1 -1
  67. package/workflows/container-contents/tombstones/documentTombstoneEvidence.js +6 -5
  68. package/workflows/container-contents/tombstones/documentTombstoneEvidence.js.map +1 -1
  69. package/workflows/documents/remoteReset/documentRemoteReset.js +31 -18
  70. package/workflows/documents/remoteReset/documentRemoteReset.js.map +1 -1
  71. package/workflows/principals/principalPolicyHistoryReader.d.ts +6 -1
  72. package/workflows/principals/principalPolicyHistoryReader.js +4 -1
  73. package/workflows/principals/principalPolicyHistoryReader.js.map +1 -1
  74. package/workflows/principals/recoveryOperation/principalRecoveryDirectory.js +108 -59
  75. package/workflows/principals/recoveryOperation/principalRecoveryDirectory.js.map +1 -1
  76. package/workflows/principals/runtimePolicyRecovery.js +26 -14
  77. package/workflows/principals/runtimePolicyRecovery.js.map +1 -1
  78. package/workflows/principals/runtimePolicyWarmer.d.ts +26 -0
  79. package/workflows/principals/runtimePolicyWarmer.js +8 -5
  80. package/workflows/principals/runtimePolicyWarmer.js.map +1 -1
@@ -1,5 +1,6 @@
1
- import { and, asc, eq, inArray, sql } from "drizzle-orm";
1
+ import { and, asc, eq, inArray, or, sql } from "drizzle-orm";
2
2
  import { containerDocumentTombstoneHolds, documentContainerProjection, documentProjection, } from "../../../sqlite/schema/index.js";
3
+ import { remoteResetBatches } from "../../../sqlite/sqliteBindBudget.js";
3
4
  import { filterWritableDocumentPlacements } from "../../container-contents/documentPlacement.js";
4
5
  /** Keep `IN (...)` lists well under SQLite's bound-parameter limit. */
5
6
  const HOLD_SQL_ID_BATCH_SIZE = 400;
@@ -15,11 +16,18 @@ export function containerDocumentPlacementKey(placement) {
15
16
  return `${placement.documentId}\u0000${placement.containerId}`;
16
17
  }
17
18
  const placementKey = containerDocumentPlacementKey;
19
+ /** Matches the listed (document, container) placements by primary key. */
20
+ export function placementPairsMatch(placements, documentIdColumn, containerIdColumn) {
21
+ return or(...placements.map((placement) => and(eq(documentIdColumn, placement.documentId), eq(containerIdColumn, placement.containerId))));
22
+ }
23
+ function uniquePlacements(placements) {
24
+ return Array.from(new Map(placements.map((placement) => [placementKey(placement), placement])).values());
25
+ }
18
26
  export async function deleteContainerDocumentTombstoneHoldRows(tx, placements) {
19
- for (const placement of placements) {
27
+ for (const batch of remoteResetBatches(uniquePlacements(placements))) {
20
28
  await tx
21
29
  .delete(containerDocumentTombstoneHolds)
22
- .where(and(eq(containerDocumentTombstoneHolds.documentId, placement.documentId), eq(containerDocumentTombstoneHolds.containerId, placement.containerId)))
30
+ .where(placementPairsMatch(batch, containerDocumentTombstoneHolds.documentId, containerDocumentTombstoneHolds.containerId))
23
31
  .run();
24
32
  }
25
33
  }
@@ -126,14 +134,10 @@ export async function holdContainerDocumentTombstonesInTransaction(tx, tombstone
126
134
  }
127
135
  /** Release the holds on placements a listing has re-asserted as linked. */
128
136
  export async function deleteContainerDocumentTombstoneHoldRowsForLinks(tx, links) {
129
- for (const link of links) {
130
- for (const batch of batches(link.containerIds)) {
131
- await tx
132
- .delete(containerDocumentTombstoneHolds)
133
- .where(and(eq(containerDocumentTombstoneHolds.documentId, link.documentId), inArray(containerDocumentTombstoneHolds.containerId, batch)))
134
- .run();
135
- }
136
- }
137
+ await deleteContainerDocumentTombstoneHoldRows(tx, links.flatMap((link) => link.containerIds.map((containerId) => ({
138
+ containerId,
139
+ documentId: link.documentId,
140
+ }))));
137
141
  }
138
142
  const holdRowSelection = {
139
143
  attempts: containerDocumentTombstoneHolds.attempts,
@@ -1 +1 @@
1
- {"version":3,"file":"containerDocumentTombstoneHolds.js","sourceRoot":"","sources":["containerDocumentTombstoneHolds.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, eq, inArray, sql } from \"drizzle-orm\";\nimport {\n containerDocumentTombstoneHolds,\n documentContainerProjection,\n documentProjection,\n} from \"../../../sqlite/schema\";\nimport type { ClientSQLiteTransactionScope } from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport { filterWritableDocumentPlacements } from \"../../container-contents/documentPlacement\";\nimport type { ContainerDocumentTombstoneInput } from \"../types\";\n\nexport interface ContainerDocumentPlacementKey {\n readonly containerId: string;\n readonly documentId: string;\n}\n\nexport type HeldContainerDocumentTombstone = Omit<\n ContainerDocumentTombstoneInput,\n \"accessEpoch\" | \"linkedContainerIds\"\n> & {\n /** Held without a verification attempt; the backoff does not advance. */\n readonly deferred?: boolean | undefined;\n /** A verified head still links it; retry the tombstone while keeping it visible. */\n readonly refuted?: boolean | undefined;\n};\n\n/** Keep `IN (...)` lists well under SQLite's bound-parameter limit. */\nconst HOLD_SQL_ID_BATCH_SIZE = 400;\n\nfunction batches<T>(values: ReadonlyArray<T>): T[][] {\n const unique = Array.from(new Set(values));\n const result: T[][] = [];\n for (let index = 0; index < unique.length; index += HOLD_SQL_ID_BATCH_SIZE) {\n result.push(unique.slice(index, index + HOLD_SQL_ID_BATCH_SIZE));\n }\n return result;\n}\n\nexport function containerDocumentPlacementKey(\n placement: ContainerDocumentPlacementKey,\n): string {\n return `${placement.documentId}\\u0000${placement.containerId}`;\n}\nconst placementKey = containerDocumentPlacementKey;\n\nexport async function deleteContainerDocumentTombstoneHoldRows(\n tx: ClientSQLiteTransactionScope,\n placements: ReadonlyArray<ContainerDocumentPlacementKey>,\n): Promise<void> {\n for (const placement of placements) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(\n and(\n eq(containerDocumentTombstoneHolds.documentId, placement.documentId),\n eq(\n containerDocumentTombstoneHolds.containerId,\n placement.containerId,\n ),\n ),\n )\n .run();\n }\n}\n\n/**\n * A local move takes ownership of a document's placement: the intent, not a\n * server listing, now says where it lives, so every hold that was hiding one\n * of its placements is released with the link rewrite.\n */\nexport async function deleteContainerDocumentTombstoneHoldsForDocuments(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<void> {\n for (const batch of batches(documentIds)) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(inArray(containerDocumentTombstoneHolds.documentId, batch))\n .run();\n }\n}\n\n/** Holds on containers whose link rows are being removed locally. */\nexport async function deleteContainerDocumentTombstoneHoldsForContainers(\n tx: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<void> {\n for (const batch of batches(containerIds)) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(inArray(containerDocumentTombstoneHolds.containerId, batch))\n .run();\n }\n}\n\n/**\n * The placements among `placements` that exist locally: a link row, or a\n * document projection row whose primary container is the named container.\n * A tombstone for any other placement has nothing to remove or hide, so it\n * is not worth fetching and verifying the document head for.\n */\nexport async function listKnownContainerDocumentPlacementsInTransaction(\n tx: ClientSQLiteTransactionScope,\n placements: ReadonlyArray<ContainerDocumentPlacementKey>,\n): Promise<ContainerDocumentPlacementKey[]> {\n const known = new Set<string>();\n for (const batch of batches(placements.map((entry) => entry.documentId))) {\n const [linkRows, projectionRows] = await Promise.all([\n tx\n .select({\n containerId: documentContainerProjection.containerId,\n documentId: documentContainerProjection.documentId,\n })\n .from(documentContainerProjection)\n .where(inArray(documentContainerProjection.documentId, batch)),\n tx\n .select({\n containerId: documentProjection.containerId,\n documentId: documentProjection.documentId,\n })\n .from(documentProjection)\n .where(inArray(documentProjection.documentId, batch)),\n ]);\n for (const row of [...linkRows, ...projectionRows]) {\n if (row.documentId !== null && row.containerId !== null) {\n known.add(\n placementKey({\n containerId: row.containerId,\n documentId: row.documentId,\n }),\n );\n }\n }\n }\n return placements.filter((placement) => known.has(placementKey(placement)));\n}\n\n/**\n * Hold listing tombstones whose signed evidence is unavailable. A placement a\n * pending move intent owns is never held: the intent, not the listing, decides\n * where the document lives until it settles (`ProtectPendingTombstones`).\n */\nexport async function holdContainerDocumentTombstonesInTransaction(\n tx: ClientSQLiteTransactionScope,\n tombstones: ReadonlyArray<HeldContainerDocumentTombstone>,\n now: string,\n): Promise<void> {\n const writable = new Set(\n (\n await filterWritableDocumentPlacements(\n tx,\n tombstones.map((tombstone) => ({\n documentId: tombstone.documentId,\n containerIds: [],\n })),\n )\n ).map((input) => input.documentId),\n );\n for (const tombstone of tombstones) {\n if (!writable.has(tombstone.documentId)) continue;\n const row = {\n containerId: tombstone.containerId,\n documentId: tombstone.documentId,\n tombstonedAt: tombstone.updatedAt,\n hidden: !tombstone.refuted,\n updatedAt: now,\n };\n // A deferred hold was not attempted: it keeps its attempt count and its\n // last-attempt time, so it stays due and is not starved by the cap.\n await tx\n .insert(containerDocumentTombstoneHolds)\n .values({ ...row, attempts: tombstone.deferred ? 0 : 1 })\n .onConflictDoUpdate({\n target: [\n containerDocumentTombstoneHolds.documentId,\n containerDocumentTombstoneHolds.containerId,\n ],\n set: tombstone.deferred\n ? { tombstonedAt: row.tombstonedAt }\n : {\n ...row,\n hidden: tombstone.refuted\n ? false\n : containerDocumentTombstoneHolds.hidden,\n attempts: sql`${containerDocumentTombstoneHolds.attempts} + 1`,\n },\n })\n .run();\n }\n}\n\n/** Release the holds on placements a listing has re-asserted as linked. */\nexport async function deleteContainerDocumentTombstoneHoldRowsForLinks(\n tx: ClientSQLiteTransactionScope,\n links: ReadonlyArray<{\n readonly containerIds: ReadonlyArray<string>;\n readonly documentId: string;\n }>,\n): Promise<void> {\n for (const link of links) {\n for (const batch of batches(link.containerIds)) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(\n and(\n eq(containerDocumentTombstoneHolds.documentId, link.documentId),\n inArray(containerDocumentTombstoneHolds.containerId, batch),\n ),\n )\n .run();\n }\n }\n}\n\nexport interface ContainerDocumentTombstoneHoldRow\n extends ContainerDocumentPlacementKey {\n readonly attempts: number;\n readonly hidden: boolean;\n readonly tombstonedAt: string;\n readonly updatedAt: string;\n}\n\nconst holdRowSelection = {\n attempts: containerDocumentTombstoneHolds.attempts,\n hidden: containerDocumentTombstoneHolds.hidden,\n containerId: containerDocumentTombstoneHolds.containerId,\n documentId: containerDocumentTombstoneHolds.documentId,\n tombstonedAt: containerDocumentTombstoneHolds.tombstonedAt,\n updatedAt: containerDocumentTombstoneHolds.updatedAt,\n};\n\n/** Every hold on the given containers, including visible refuted retries. */\nexport async function listContainerDocumentTombstoneHoldsInTransaction(\n handle: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<ContainerDocumentTombstoneHoldRow[]> {\n const holds: ContainerDocumentTombstoneHoldRow[] = [];\n for (const batch of batches(containerIds)) {\n holds.push(\n ...(await handle\n .select(holdRowSelection)\n .from(containerDocumentTombstoneHolds)\n .where(inArray(containerDocumentTombstoneHolds.containerId, batch))\n .orderBy(\n asc(containerDocumentTombstoneHolds.containerId),\n asc(containerDocumentTombstoneHolds.documentId),\n )),\n );\n }\n return holds;\n}\n\n/** Every hidden hold on the given documents, in any container. */\nexport async function listContainerDocumentTombstoneHoldsForDocumentsInTransaction(\n handle: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<ContainerDocumentTombstoneHoldRow[]> {\n const holds: ContainerDocumentTombstoneHoldRow[] = [];\n for (const batch of batches(documentIds)) {\n holds.push(\n ...(await handle\n .select(holdRowSelection)\n .from(containerDocumentTombstoneHolds)\n .where(\n and(\n inArray(containerDocumentTombstoneHolds.documentId, batch),\n eq(containerDocumentTombstoneHolds.hidden, true),\n ),\n )\n .orderBy(\n asc(containerDocumentTombstoneHolds.documentId),\n asc(containerDocumentTombstoneHolds.containerId),\n )),\n );\n }\n return holds;\n}\n\n/**\n * Split the holds on the given containers into those whose placement is gone\n * (the document was purged, reset, or relinked locally: nothing left to hide,\n * so the hold is dropped) and those still hiding a placement.\n */\nexport async function partitionContainerDocumentTombstoneHolds(\n handle: ClientSQLiteTransactionScope,\n holds: ReadonlyArray<ContainerDocumentTombstoneHoldRow>,\n): Promise<{\n live: ContainerDocumentTombstoneHoldRow[];\n orphaned: ContainerDocumentTombstoneHoldRow[];\n}> {\n const known = new Set(\n (\n await listKnownContainerDocumentPlacementsInTransaction(handle, holds)\n ).map(placementKey),\n );\n return {\n live: holds.filter((hold) => known.has(placementKey(hold))),\n orphaned: holds.filter((hold) => !known.has(placementKey(hold))),\n };\n}\n"]}
1
+ {"version":3,"file":"containerDocumentTombstoneHolds.js","sourceRoot":"","sources":["containerDocumentTombstoneHolds.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAY,GAAG,EAAE,MAAM,aAAa,CAAC;AAEvE,OAAO,EACL,+BAA+B,EAC/B,2BAA2B,EAC3B,kBAAkB,GACnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE,OAAO,EAAE,gCAAgC,EAAE,MAAM,4CAA4C,CAAC;AAkB9F,uEAAuE;AACvE,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAEnC,SAAS,OAAO,CAAI,MAAwB;IAC1C,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAU,EAAE,CAAC;IACzB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,IAAI,sBAAsB,EAAE,CAAC;QAC3E,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,sBAAsB,CAAC,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,6BAA6B,CAC3C,SAAwC;IAExC,OAAO,GAAG,SAAS,CAAC,UAAU,SAAS,SAAS,CAAC,WAAW,EAAE,CAAC;AACjE,CAAC;AACD,MAAM,YAAY,GAAG,6BAA6B,CAAC;AAEnD,0EAA0E;AAC1E,MAAM,UAAU,mBAAmB,CACjC,UAAwD,EACxD,gBAA8B,EAC9B,iBAA+B;IAE/B,OAAO,EAAE,CACP,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAC9B,GAAG,CACD,EAAE,CAAC,gBAAgB,EAAE,SAAS,CAAC,UAAU,CAAC,EAC1C,EAAE,CAAC,iBAAiB,EAAE,SAAS,CAAC,WAAW,CAAC,CAC7C,CACF,CACF,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CACvB,UAAwD;IAExD,OAAO,KAAK,CAAC,IAAI,CACf,IAAI,GAAG,CACL,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC,CACpE,CAAC,MAAM,EAAE,CACX,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wCAAwC,CAC5D,EAAgC,EAChC,UAAwD;IAExD,KAAK,MAAM,KAAK,IAAI,kBAAkB,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC;QACrE,MAAM,EAAE;aACL,MAAM,CAAC,+BAA+B,CAAC;aACvC,KAAK,CACJ,mBAAmB,CACjB,KAAK,EACL,+BAA+B,CAAC,UAAU,EAC1C,+BAA+B,CAAC,WAAW,CAC5C,CACF;aACA,GAAG,EAAE,CAAC;IACX,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iDAAiD,CACrE,EAAgC,EAChC,WAAkC;IAElC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QACzC,MAAM,EAAE;aACL,MAAM,CAAC,+BAA+B,CAAC;aACvC,KAAK,CAAC,OAAO,CAAC,+BAA+B,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;aACjE,GAAG,EAAE,CAAC;IACX,CAAC;AACH,CAAC;AAED,qEAAqE;AACrE,MAAM,CAAC,KAAK,UAAU,kDAAkD,CACtE,EAAgC,EAChC,YAAmC;IAEnC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;QAC1C,MAAM,EAAE;aACL,MAAM,CAAC,+BAA+B,CAAC;aACvC,KAAK,CAAC,OAAO,CAAC,+BAA+B,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;aAClE,GAAG,EAAE,CAAC;IACX,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iDAAiD,CACrE,EAAgC,EAChC,UAAwD;IAExD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC;QACzE,MAAM,CAAC,QAAQ,EAAE,cAAc,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACnD,EAAE;iBACC,MAAM,CAAC;gBACN,WAAW,EAAE,2BAA2B,CAAC,WAAW;gBACpD,UAAU,EAAE,2BAA2B,CAAC,UAAU;aACnD,CAAC;iBACD,IAAI,CAAC,2BAA2B,CAAC;iBACjC,KAAK,CAAC,OAAO,CAAC,2BAA2B,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;YAChE,EAAE;iBACC,MAAM,CAAC;gBACN,WAAW,EAAE,kBAAkB,CAAC,WAAW;gBAC3C,UAAU,EAAE,kBAAkB,CAAC,UAAU;aAC1C,CAAC;iBACD,IAAI,CAAC,kBAAkB,CAAC;iBACxB,KAAK,CAAC,OAAO,CAAC,kBAAkB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;SACxD,CAAC,CAAC;QACH,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,QAAQ,EAAE,GAAG,cAAc,CAAC,EAAE,CAAC;YACnD,IAAI,GAAG,CAAC,UAAU,KAAK,IAAI,IAAI,GAAG,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;gBACxD,KAAK,CAAC,GAAG,CACP,YAAY,CAAC;oBACX,WAAW,EAAE,GAAG,CAAC,WAAW;oBAC5B,UAAU,EAAE,GAAG,CAAC,UAAU;iBAC3B,CAAC,CACH,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;AAC9E,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,4CAA4C,CAChE,EAAgC,EAChC,UAAyD,EACzD,GAAW;IAEX,MAAM,QAAQ,GAAG,IAAI,GAAG,CACtB,CACE,MAAM,gCAAgC,CACpC,EAAE,EACF,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAC7B,UAAU,EAAE,SAAS,CAAC,UAAU;QAChC,YAAY,EAAE,EAAE;KACjB,CAAC,CAAC,CACJ,CACF,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CACnC,CAAC;IACF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC;YAAE,SAAS;QAClD,MAAM,GAAG,GAAG;YACV,WAAW,EAAE,SAAS,CAAC,WAAW;YAClC,UAAU,EAAE,SAAS,CAAC,UAAU;YAChC,YAAY,EAAE,SAAS,CAAC,SAAS;YACjC,MAAM,EAAE,CAAC,SAAS,CAAC,OAAO;YAC1B,SAAS,EAAE,GAAG;SACf,CAAC;QACF,wEAAwE;QACxE,oEAAoE;QACpE,MAAM,EAAE;aACL,MAAM,CAAC,+BAA+B,CAAC;aACvC,MAAM,CAAC,EAAE,GAAG,GAAG,EAAE,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;aACxD,kBAAkB,CAAC;YAClB,MAAM,EAAE;gBACN,+BAA+B,CAAC,UAAU;gBAC1C,+BAA+B,CAAC,WAAW;aAC5C;YACD,GAAG,EAAE,SAAS,CAAC,QAAQ;gBACrB,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,CAAC,YAAY,EAAE;gBACpC,CAAC,CAAC;oBACE,GAAG,GAAG;oBACN,MAAM,EAAE,SAAS,CAAC,OAAO;wBACvB,CAAC,CAAC,KAAK;wBACP,CAAC,CAAC,+BAA+B,CAAC,MAAM;oBAC1C,QAAQ,EAAE,GAAG,CAAA,GAAG,+BAA+B,CAAC,QAAQ,MAAM;iBAC/D;SACN,CAAC;aACD,GAAG,EAAE,CAAC;IACX,CAAC;AACH,CAAC;AAED,2EAA2E;AAC3E,MAAM,CAAC,KAAK,UAAU,gDAAgD,CACpE,EAAgC,EAChC,KAGE;IAEF,MAAM,wCAAwC,CAC5C,EAAE,EACF,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CACrB,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QACtC,WAAW;QACX,UAAU,EAAE,IAAI,CAAC,UAAU;KAC5B,CAAC,CAAC,CACJ,CACF,CAAC;AACJ,CAAC;AAUD,MAAM,gBAAgB,GAAG;IACvB,QAAQ,EAAE,+BAA+B,CAAC,QAAQ;IAClD,MAAM,EAAE,+BAA+B,CAAC,MAAM;IAC9C,WAAW,EAAE,+BAA+B,CAAC,WAAW;IACxD,UAAU,EAAE,+BAA+B,CAAC,UAAU;IACtD,YAAY,EAAE,+BAA+B,CAAC,YAAY;IAC1D,SAAS,EAAE,+BAA+B,CAAC,SAAS;CACrD,CAAC;AAEF,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,gDAAgD,CACpE,MAAoC,EACpC,YAAmC;IAEnC,MAAM,KAAK,GAAwC,EAAE,CAAC;IACtD,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;QAC1C,KAAK,CAAC,IAAI,CACR,GAAG,CAAC,MAAM,MAAM;aACb,MAAM,CAAC,gBAAgB,CAAC;aACxB,IAAI,CAAC,+BAA+B,CAAC;aACrC,KAAK,CAAC,OAAO,CAAC,+BAA+B,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;aAClE,OAAO,CACN,GAAG,CAAC,+BAA+B,CAAC,WAAW,CAAC,EAChD,GAAG,CAAC,+BAA+B,CAAC,UAAU,CAAC,CAChD,CAAC,CACL,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,kEAAkE;AAClE,MAAM,CAAC,KAAK,UAAU,4DAA4D,CAChF,MAAoC,EACpC,WAAkC;IAElC,MAAM,KAAK,GAAwC,EAAE,CAAC;IACtD,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QACzC,KAAK,CAAC,IAAI,CACR,GAAG,CAAC,MAAM,MAAM;aACb,MAAM,CAAC,gBAAgB,CAAC;aACxB,IAAI,CAAC,+BAA+B,CAAC;aACrC,KAAK,CACJ,GAAG,CACD,OAAO,CAAC,+BAA+B,CAAC,UAAU,EAAE,KAAK,CAAC,EAC1D,EAAE,CAAC,+BAA+B,CAAC,MAAM,EAAE,IAAI,CAAC,CACjD,CACF;aACA,OAAO,CACN,GAAG,CAAC,+BAA+B,CAAC,UAAU,CAAC,EAC/C,GAAG,CAAC,+BAA+B,CAAC,WAAW,CAAC,CACjD,CAAC,CACL,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,wCAAwC,CAC5D,MAAoC,EACpC,KAAuD;IAKvD,MAAM,KAAK,GAAG,IAAI,GAAG,CACnB,CACE,MAAM,iDAAiD,CAAC,MAAM,EAAE,KAAK,CAAC,CACvE,CAAC,GAAG,CAAC,YAAY,CAAC,CACpB,CAAC;IACF,OAAO;QACL,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3D,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;KACjE,CAAC;AACJ,CAAC","sourcesContent":["import { and, asc, eq, inArray, or, type SQL, sql } from \"drizzle-orm\";\nimport type { SQLiteColumn } from \"drizzle-orm/sqlite-core\";\nimport {\n containerDocumentTombstoneHolds,\n documentContainerProjection,\n documentProjection,\n} from \"../../../sqlite/schema\";\nimport { remoteResetBatches } from \"../../../sqlite/sqliteBindBudget\";\nimport type { ClientSQLiteTransactionScope } from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport { filterWritableDocumentPlacements } from \"../../container-contents/documentPlacement\";\nimport type { ContainerDocumentTombstoneInput } from \"../types\";\n\nexport interface ContainerDocumentPlacementKey {\n readonly containerId: string;\n readonly documentId: string;\n}\n\nexport type HeldContainerDocumentTombstone = Omit<\n ContainerDocumentTombstoneInput,\n \"accessEpoch\" | \"linkedContainerIds\"\n> & {\n /** Held without a verification attempt; the backoff does not advance. */\n readonly deferred?: boolean | undefined;\n /** A verified head still links it; retry the tombstone while keeping it visible. */\n readonly refuted?: boolean | undefined;\n};\n\n/** Keep `IN (...)` lists well under SQLite's bound-parameter limit. */\nconst HOLD_SQL_ID_BATCH_SIZE = 400;\n\nfunction batches<T>(values: ReadonlyArray<T>): T[][] {\n const unique = Array.from(new Set(values));\n const result: T[][] = [];\n for (let index = 0; index < unique.length; index += HOLD_SQL_ID_BATCH_SIZE) {\n result.push(unique.slice(index, index + HOLD_SQL_ID_BATCH_SIZE));\n }\n return result;\n}\n\nexport function containerDocumentPlacementKey(\n placement: ContainerDocumentPlacementKey,\n): string {\n return `${placement.documentId}\\u0000${placement.containerId}`;\n}\nconst placementKey = containerDocumentPlacementKey;\n\n/** Matches the listed (document, container) placements by primary key. */\nexport function placementPairsMatch(\n placements: ReadonlyArray<ContainerDocumentPlacementKey>,\n documentIdColumn: SQLiteColumn,\n containerIdColumn: SQLiteColumn,\n): SQL | undefined {\n return or(\n ...placements.map((placement) =>\n and(\n eq(documentIdColumn, placement.documentId),\n eq(containerIdColumn, placement.containerId),\n ),\n ),\n );\n}\n\nfunction uniquePlacements(\n placements: ReadonlyArray<ContainerDocumentPlacementKey>,\n): ContainerDocumentPlacementKey[] {\n return Array.from(\n new Map(\n placements.map((placement) => [placementKey(placement), placement]),\n ).values(),\n );\n}\n\nexport async function deleteContainerDocumentTombstoneHoldRows(\n tx: ClientSQLiteTransactionScope,\n placements: ReadonlyArray<ContainerDocumentPlacementKey>,\n): Promise<void> {\n for (const batch of remoteResetBatches(uniquePlacements(placements))) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(\n placementPairsMatch(\n batch,\n containerDocumentTombstoneHolds.documentId,\n containerDocumentTombstoneHolds.containerId,\n ),\n )\n .run();\n }\n}\n\n/**\n * A local move takes ownership of a document's placement: the intent, not a\n * server listing, now says where it lives, so every hold that was hiding one\n * of its placements is released with the link rewrite.\n */\nexport async function deleteContainerDocumentTombstoneHoldsForDocuments(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<void> {\n for (const batch of batches(documentIds)) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(inArray(containerDocumentTombstoneHolds.documentId, batch))\n .run();\n }\n}\n\n/** Holds on containers whose link rows are being removed locally. */\nexport async function deleteContainerDocumentTombstoneHoldsForContainers(\n tx: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<void> {\n for (const batch of batches(containerIds)) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(inArray(containerDocumentTombstoneHolds.containerId, batch))\n .run();\n }\n}\n\n/**\n * The placements among `placements` that exist locally: a link row, or a\n * document projection row whose primary container is the named container.\n * A tombstone for any other placement has nothing to remove or hide, so it\n * is not worth fetching and verifying the document head for.\n */\nexport async function listKnownContainerDocumentPlacementsInTransaction(\n tx: ClientSQLiteTransactionScope,\n placements: ReadonlyArray<ContainerDocumentPlacementKey>,\n): Promise<ContainerDocumentPlacementKey[]> {\n const known = new Set<string>();\n for (const batch of batches(placements.map((entry) => entry.documentId))) {\n const [linkRows, projectionRows] = await Promise.all([\n tx\n .select({\n containerId: documentContainerProjection.containerId,\n documentId: documentContainerProjection.documentId,\n })\n .from(documentContainerProjection)\n .where(inArray(documentContainerProjection.documentId, batch)),\n tx\n .select({\n containerId: documentProjection.containerId,\n documentId: documentProjection.documentId,\n })\n .from(documentProjection)\n .where(inArray(documentProjection.documentId, batch)),\n ]);\n for (const row of [...linkRows, ...projectionRows]) {\n if (row.documentId !== null && row.containerId !== null) {\n known.add(\n placementKey({\n containerId: row.containerId,\n documentId: row.documentId,\n }),\n );\n }\n }\n }\n return placements.filter((placement) => known.has(placementKey(placement)));\n}\n\n/**\n * Hold listing tombstones whose signed evidence is unavailable. A placement a\n * pending move intent owns is never held: the intent, not the listing, decides\n * where the document lives until it settles (`ProtectPendingTombstones`).\n */\nexport async function holdContainerDocumentTombstonesInTransaction(\n tx: ClientSQLiteTransactionScope,\n tombstones: ReadonlyArray<HeldContainerDocumentTombstone>,\n now: string,\n): Promise<void> {\n const writable = new Set(\n (\n await filterWritableDocumentPlacements(\n tx,\n tombstones.map((tombstone) => ({\n documentId: tombstone.documentId,\n containerIds: [],\n })),\n )\n ).map((input) => input.documentId),\n );\n for (const tombstone of tombstones) {\n if (!writable.has(tombstone.documentId)) continue;\n const row = {\n containerId: tombstone.containerId,\n documentId: tombstone.documentId,\n tombstonedAt: tombstone.updatedAt,\n hidden: !tombstone.refuted,\n updatedAt: now,\n };\n // A deferred hold was not attempted: it keeps its attempt count and its\n // last-attempt time, so it stays due and is not starved by the cap.\n await tx\n .insert(containerDocumentTombstoneHolds)\n .values({ ...row, attempts: tombstone.deferred ? 0 : 1 })\n .onConflictDoUpdate({\n target: [\n containerDocumentTombstoneHolds.documentId,\n containerDocumentTombstoneHolds.containerId,\n ],\n set: tombstone.deferred\n ? { tombstonedAt: row.tombstonedAt }\n : {\n ...row,\n hidden: tombstone.refuted\n ? false\n : containerDocumentTombstoneHolds.hidden,\n attempts: sql`${containerDocumentTombstoneHolds.attempts} + 1`,\n },\n })\n .run();\n }\n}\n\n/** Release the holds on placements a listing has re-asserted as linked. */\nexport async function deleteContainerDocumentTombstoneHoldRowsForLinks(\n tx: ClientSQLiteTransactionScope,\n links: ReadonlyArray<{\n readonly containerIds: ReadonlyArray<string>;\n readonly documentId: string;\n }>,\n): Promise<void> {\n await deleteContainerDocumentTombstoneHoldRows(\n tx,\n links.flatMap((link) =>\n link.containerIds.map((containerId) => ({\n containerId,\n documentId: link.documentId,\n })),\n ),\n );\n}\n\nexport interface ContainerDocumentTombstoneHoldRow\n extends ContainerDocumentPlacementKey {\n readonly attempts: number;\n readonly hidden: boolean;\n readonly tombstonedAt: string;\n readonly updatedAt: string;\n}\n\nconst holdRowSelection = {\n attempts: containerDocumentTombstoneHolds.attempts,\n hidden: containerDocumentTombstoneHolds.hidden,\n containerId: containerDocumentTombstoneHolds.containerId,\n documentId: containerDocumentTombstoneHolds.documentId,\n tombstonedAt: containerDocumentTombstoneHolds.tombstonedAt,\n updatedAt: containerDocumentTombstoneHolds.updatedAt,\n};\n\n/** Every hold on the given containers, including visible refuted retries. */\nexport async function listContainerDocumentTombstoneHoldsInTransaction(\n handle: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<ContainerDocumentTombstoneHoldRow[]> {\n const holds: ContainerDocumentTombstoneHoldRow[] = [];\n for (const batch of batches(containerIds)) {\n holds.push(\n ...(await handle\n .select(holdRowSelection)\n .from(containerDocumentTombstoneHolds)\n .where(inArray(containerDocumentTombstoneHolds.containerId, batch))\n .orderBy(\n asc(containerDocumentTombstoneHolds.containerId),\n asc(containerDocumentTombstoneHolds.documentId),\n )),\n );\n }\n return holds;\n}\n\n/** Every hidden hold on the given documents, in any container. */\nexport async function listContainerDocumentTombstoneHoldsForDocumentsInTransaction(\n handle: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<ContainerDocumentTombstoneHoldRow[]> {\n const holds: ContainerDocumentTombstoneHoldRow[] = [];\n for (const batch of batches(documentIds)) {\n holds.push(\n ...(await handle\n .select(holdRowSelection)\n .from(containerDocumentTombstoneHolds)\n .where(\n and(\n inArray(containerDocumentTombstoneHolds.documentId, batch),\n eq(containerDocumentTombstoneHolds.hidden, true),\n ),\n )\n .orderBy(\n asc(containerDocumentTombstoneHolds.documentId),\n asc(containerDocumentTombstoneHolds.containerId),\n )),\n );\n }\n return holds;\n}\n\n/**\n * Split the holds on the given containers into those whose placement is gone\n * (the document was purged, reset, or relinked locally: nothing left to hide,\n * so the hold is dropped) and those still hiding a placement.\n */\nexport async function partitionContainerDocumentTombstoneHolds(\n handle: ClientSQLiteTransactionScope,\n holds: ReadonlyArray<ContainerDocumentTombstoneHoldRow>,\n): Promise<{\n live: ContainerDocumentTombstoneHoldRow[];\n orphaned: ContainerDocumentTombstoneHoldRow[];\n}> {\n const known = new Set(\n (\n await listKnownContainerDocumentPlacementsInTransaction(handle, holds)\n ).map(placementKey),\n );\n return {\n live: holds.filter((hold) => known.has(placementKey(hold))),\n orphaned: holds.filter((hold) => !known.has(placementKey(hold))),\n };\n}\n"]}
@@ -1,8 +1,9 @@
1
1
  import { and, asc, desc, eq, inArray } from "drizzle-orm";
2
2
  import { DOCUMENTS_APP_KIND } from "../../../documents/documentConstants.js";
3
3
  import { documentContainerProjection, documentProjection, documents, } from "../../../sqlite/schema/index.js";
4
+ import { sqliteRowsPerStatement } from "../../../sqlite/sqliteBindBudget.js";
4
5
  import { getClientSQLitePersistenceRuntime, } from "../../../sqlite/sqlitePersistenceRuntime.js";
5
- import { deleteDocumentPlacementRowInTransaction, deleteUnlinkedDocumentPlacementRowsInTransaction, } from "../../container-contents/documentContainerProjectionPersistence.js";
6
+ import { deleteDocumentPlacementRowsInTransaction, deleteUnlinkedDocumentPlacementRowsInTransaction, } from "../../container-contents/documentContainerProjectionPersistence.js";
6
7
  import { filterWritableDocumentPlacements } from "../../container-contents/documentPlacement.js";
7
8
  import { getLatestTimestamp } from "../../latestTimestamp.js";
8
9
  import { deleteContainerDocumentTombstoneHoldRows } from "./containerDocumentTombstoneHolds.js";
@@ -37,12 +38,8 @@ function buildContainerDocumentTombstoneState(uniqueTombstones) {
37
38
  * signed link set never named. Rows the head links are kept as they are.
38
39
  */
39
40
  async function deleteContainerDocumentTombstoneRows(tx, uniqueTombstones, verifiedLinkedContainerIdsByDocumentId) {
40
- for (const tombstone of uniqueTombstones) {
41
- await deleteDocumentPlacementRowInTransaction(tx, tombstone);
42
- }
43
- for (const [documentId, linked] of verifiedLinkedContainerIdsByDocumentId) {
44
- await deleteUnlinkedDocumentPlacementRowsInTransaction(tx, documentId, linked);
45
- }
41
+ await deleteDocumentPlacementRowsInTransaction(tx, uniqueTombstones);
42
+ await deleteUnlinkedDocumentPlacementRowsInTransaction(tx, verifiedLinkedContainerIdsByDocumentId);
46
43
  }
47
44
  /**
48
45
  * The primary container after a removal: the first remaining local row the
@@ -55,8 +52,75 @@ async function deleteContainerDocumentTombstoneRows(tx, uniqueTombstones, verifi
55
52
  function selectNextContainerId(remainingContainerIds, verifiedLinkedContainerIds) {
56
53
  return (remainingContainerIds.find((containerId) => verifiedLinkedContainerIds.has(containerId)) ?? null);
57
54
  }
55
+ function pushGrouped(map, key, value) {
56
+ const values = map.get(key);
57
+ if (values)
58
+ values.push(value);
59
+ else
60
+ map.set(key, [value]);
61
+ }
62
+ /**
63
+ * Every row the repair reads, for all tombstoned documents at once. The
64
+ * deletes above have already run, and the repair below writes only
65
+ * projection rows of the document it repairs, so reading before any repair
66
+ * sees what each per-document read saw.
67
+ */
68
+ async function loadTombstoneRepairReads(tx, documentIds) {
69
+ const localIdsByDocumentId = new Map();
70
+ const projectionByLocalId = new Map();
71
+ const remainingLinksByDocumentId = new Map();
72
+ // The app kind binds one value; the rest carry ids.
73
+ const chunkSize = sqliteRowsPerStatement(1, 1);
74
+ for (let offset = 0; offset < documentIds.length; offset += chunkSize) {
75
+ const batch = documentIds.slice(offset, offset + chunkSize);
76
+ const localRows = await tx
77
+ .select({ documentId: documents.documentId, localId: documents.localId })
78
+ .from(documents)
79
+ .where(and(eq(documents.appKind, DOCUMENTS_APP_KIND), inArray(documents.documentId, batch)));
80
+ for (const row of localRows) {
81
+ if (row.documentId !== null) {
82
+ pushGrouped(localIdsByDocumentId, row.documentId, row.localId);
83
+ }
84
+ }
85
+ const linkRows = await tx
86
+ .select({
87
+ containerId: documentContainerProjection.containerId,
88
+ documentId: documentContainerProjection.documentId,
89
+ })
90
+ .from(documentContainerProjection)
91
+ .where(inArray(documentContainerProjection.documentId, batch))
92
+ .orderBy(asc(documentContainerProjection.documentId), asc(documentContainerProjection.containerId));
93
+ for (const row of linkRows) {
94
+ pushGrouped(remainingLinksByDocumentId, row.documentId, row.containerId);
95
+ }
96
+ }
97
+ const localIds = [...localIdsByDocumentId.values()].flat();
98
+ for (let offset = 0; offset < localIds.length; offset += chunkSize) {
99
+ const projectionRows = await tx
100
+ .select({
101
+ localId: documentProjection.localId,
102
+ containerId: documentProjection.containerId,
103
+ updatedAt: documentProjection.updatedAt,
104
+ })
105
+ .from(documentProjection)
106
+ .where(inArray(documentProjection.localId, localIds.slice(offset, offset + chunkSize)));
107
+ for (const row of projectionRows) {
108
+ if (row.localId !== null) {
109
+ projectionByLocalId.set(row.localId, {
110
+ containerId: row.containerId,
111
+ updatedAt: row.updatedAt,
112
+ });
113
+ }
114
+ }
115
+ }
116
+ return {
117
+ localIdsByDocumentId,
118
+ projectionByLocalId,
119
+ remainingLinksByDocumentId,
120
+ };
121
+ }
58
122
  async function updateSelectedContainersForDocumentTombstones(input) {
59
- const { documentId, removedContainerIds, tombstoneUpdatedAt, tx } = input;
123
+ const { documentId, reads, removedContainerIds, tombstoneUpdatedAt, tx } = input;
60
124
  // A server document can own more than one local projection row: identity
61
125
  // recovery legitimately rematerializes a second projection for the same
62
126
  // documentId before semantic convergence completes. The link delete above is
@@ -65,39 +129,23 @@ async function updateSelectedContainersForDocumentTombstones(input) {
65
129
  // at a removed container must be repaired here — resolving a single localId
66
130
  // left a duplicate row stranded at the source, keeping a moved document
67
131
  // visible in the container it was unlinked from.
68
- const localIds = (await tx
69
- .select({ localId: documents.localId })
70
- .from(documents)
71
- .where(and(eq(documents.appKind, DOCUMENTS_APP_KIND), eq(documents.documentId, documentId)))).map((row) => row.localId);
72
- if (localIds.length === 0) {
73
- return [];
74
- }
75
- const projectionRows = await tx
76
- .select({
77
- localId: documentProjection.localId,
78
- containerId: documentProjection.containerId,
79
- updatedAt: documentProjection.updatedAt,
80
- })
81
- .from(documentProjection)
82
- .where(inArray(documentProjection.localId, localIds));
132
+ const localIds = reads.localIdsByDocumentId.get(documentId) ?? [];
83
133
  // Every projection row whose primary container is tombstoned OR outside the
84
134
  // verified link set is repointed: a primary at a listing-seeded row would
85
135
  // otherwise keep showing the document in a container the head never named.
86
- const rowsAtRemovedContainer = projectionRows.flatMap((row) => row.localId !== null &&
87
- row.containerId !== null &&
88
- (removedContainerIds.has(row.containerId) ||
89
- !input.verifiedLinkedContainerIds.has(row.containerId))
90
- ? [{ localId: row.localId, updatedAt: row.updatedAt ?? undefined }]
91
- : []);
136
+ const rowsAtRemovedContainer = localIds.flatMap((localId) => {
137
+ const row = reads.projectionByLocalId.get(localId);
138
+ return row !== undefined &&
139
+ row.containerId !== null &&
140
+ (removedContainerIds.has(row.containerId) ||
141
+ !input.verifiedLinkedContainerIds.has(row.containerId))
142
+ ? [{ localId, updatedAt: row.updatedAt ?? undefined }]
143
+ : [];
144
+ });
92
145
  if (rowsAtRemovedContainer.length === 0) {
93
146
  return [];
94
147
  }
95
- const remainingLinkRows = await tx
96
- .select({ containerId: documentContainerProjection.containerId })
97
- .from(documentContainerProjection)
98
- .where(eq(documentContainerProjection.documentId, documentId))
99
- .orderBy(asc(documentContainerProjection.containerId));
100
- const nextContainerId = selectNextContainerId(remainingLinkRows.map((row) => row.containerId), input.verifiedLinkedContainerIds);
148
+ const nextContainerId = selectNextContainerId(reads.remainingLinksByDocumentId.get(documentId) ?? [], input.verifiedLinkedContainerIds);
101
149
  const changedLocalIds = [];
102
150
  for (const row of rowsAtRemovedContainer) {
103
151
  await tx
@@ -132,10 +180,14 @@ export async function applyContainerDocumentTombstonesWithExec(execSql, tombston
132
180
  const { removedContainerIdsByDocumentId, tombstoneUpdatedAtByDocumentId, verifiedLinkedContainerIdsByDocumentId, } = buildContainerDocumentTombstoneState(applicable);
133
181
  await deleteContainerDocumentTombstoneRows(tx, applicable, verifiedLinkedContainerIdsByDocumentId);
134
182
  await deleteContainerDocumentTombstoneHoldRows(tx, applicable);
183
+ const reads = await loadTombstoneRepairReads(tx, [
184
+ ...removedContainerIdsByDocumentId.keys(),
185
+ ]);
135
186
  const changedLocalIds = [];
136
187
  for (const [documentId, removedContainerIds,] of removedContainerIdsByDocumentId) {
137
188
  changedLocalIds.push(...(await updateSelectedContainersForDocumentTombstones({
138
189
  documentId,
190
+ reads,
139
191
  removedContainerIds,
140
192
  tombstoneUpdatedAt: tombstoneUpdatedAtByDocumentId.get(documentId),
141
193
  tx,
@@ -1 +1 @@
1
- {"version":3,"file":"containerDocumentTombstones.js","sourceRoot":"","sources":["containerDocumentTombstones.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, desc, eq, inArray } from \"drizzle-orm\";\nimport { DOCUMENTS_APP_KIND } from \"../../../documents/documentConstants\";\nimport {\n documentContainerProjection,\n documentProjection,\n documents,\n} from \"../../../sqlite/schema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport type { ExecSql } from \"../../../sqlite/sqlSchema\";\nimport {\n deleteDocumentPlacementRowInTransaction,\n deleteUnlinkedDocumentPlacementRowsInTransaction,\n} from \"../../container-contents/documentContainerProjectionPersistence\";\nimport { filterWritableDocumentPlacements } from \"../../container-contents/documentPlacement\";\nimport { getLatestTimestamp } from \"../../latestTimestamp\";\nimport type { ContainerDocumentTombstoneInput } from \"../types\";\nimport { deleteContainerDocumentTombstoneHoldRows } from \"./containerDocumentTombstoneHolds\";\nimport {\n documentSummaryJoin,\n documentSummarySelection,\n mapDocumentSummary,\n} from \"./documentProjectionRows\";\n\nfunction dedupeContainerDocumentTombstones(\n tombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n): ContainerDocumentTombstoneInput[] {\n return Array.from(\n new Map(\n tombstones.map((tombstone) => [\n `${tombstone.documentId}\\u0000${tombstone.containerId}`,\n tombstone,\n ]),\n ).values(),\n );\n}\n\nfunction buildContainerDocumentTombstoneState(\n uniqueTombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n): {\n removedContainerIdsByDocumentId: Map<string, Set<string>>;\n tombstoneUpdatedAtByDocumentId: Map<string, string>;\n verifiedLinkedContainerIdsByDocumentId: Map<string, ReadonlySet<string>>;\n} {\n const removedContainerIdsByDocumentId = new Map<string, Set<string>>();\n const tombstoneUpdatedAtByDocumentId = new Map<string, string>();\n const verifiedLinkedContainerIdsByDocumentId = new Map<\n string,\n ReadonlySet<string>\n >();\n\n for (const tombstone of uniqueTombstones) {\n verifiedLinkedContainerIdsByDocumentId.set(\n tombstone.documentId,\n new Set(tombstone.linkedContainerIds),\n );\n const removedContainerIds =\n removedContainerIdsByDocumentId.get(tombstone.documentId) ?? new Set();\n removedContainerIds.add(tombstone.containerId);\n removedContainerIdsByDocumentId.set(\n tombstone.documentId,\n removedContainerIds,\n );\n tombstoneUpdatedAtByDocumentId.set(\n tombstone.documentId,\n getLatestTimestamp(\n tombstoneUpdatedAtByDocumentId.get(tombstone.documentId),\n tombstone.updatedAt,\n ),\n );\n }\n\n return {\n removedContainerIdsByDocumentId,\n tombstoneUpdatedAtByDocumentId,\n verifiedLinkedContainerIdsByDocumentId,\n };\n}\n\n/**\n * Remove the tombstoned placement and, with the head now verified, every\n * remaining local link row the head does not link: those are listing-seeded\n * rows, and leaving one would keep showing the document in a container the\n * signed link set never named. Rows the head links are kept as they are.\n */\nasync function deleteContainerDocumentTombstoneRows(\n tx: ClientSQLiteTransactionScope,\n uniqueTombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n verifiedLinkedContainerIdsByDocumentId: ReadonlyMap<\n string,\n ReadonlySet<string>\n >,\n): Promise<void> {\n for (const tombstone of uniqueTombstones) {\n await deleteDocumentPlacementRowInTransaction(tx, tombstone);\n }\n for (const [documentId, linked] of verifiedLinkedContainerIdsByDocumentId) {\n await deleteUnlinkedDocumentPlacementRowsInTransaction(\n tx,\n documentId,\n linked,\n );\n }\n}\n\n/**\n * The primary container after a removal: the first remaining local row the\n * verified head links. A remaining row the head does not link is a\n * listing-seeded row and never becomes the primary, so a listing cannot\n * re-home the document. With no such row the document is unplaced (`null`)\n * and reachable through orphan recovery, rather than pointed at a head\n * container this device may not be able to read.\n */\nfunction selectNextContainerId(\n remainingContainerIds: ReadonlyArray<string>,\n verifiedLinkedContainerIds: ReadonlySet<string>,\n): string | null {\n return (\n remainingContainerIds.find((containerId) =>\n verifiedLinkedContainerIds.has(containerId),\n ) ?? null\n );\n}\n\nasync function updateSelectedContainersForDocumentTombstones(input: {\n documentId: string;\n removedContainerIds: ReadonlySet<string>;\n tombstoneUpdatedAt: string | undefined;\n tx: ClientSQLiteTransactionScope;\n verifiedLinkedContainerIds: ReadonlySet<string>;\n}): Promise<string[]> {\n const { documentId, removedContainerIds, tombstoneUpdatedAt, tx } = input;\n // A server document can own more than one local projection row: identity\n // recovery legitimately rematerializes a second projection for the same\n // documentId before semantic convergence completes. The link delete above is\n // keyed by (documentId, containerId) and the container item view surfaces a\n // document by its primary container_id, so EVERY projection row still pointing\n // at a removed container must be repaired here — resolving a single localId\n // left a duplicate row stranded at the source, keeping a moved document\n // visible in the container it was unlinked from.\n const localIds = (\n await tx\n .select({ localId: documents.localId })\n .from(documents)\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n eq(documents.documentId, documentId),\n ),\n )\n ).map((row) => row.localId);\n if (localIds.length === 0) {\n return [];\n }\n\n const projectionRows = await tx\n .select({\n localId: documentProjection.localId,\n containerId: documentProjection.containerId,\n updatedAt: documentProjection.updatedAt,\n })\n .from(documentProjection)\n .where(inArray(documentProjection.localId, localIds));\n // Every projection row whose primary container is tombstoned OR outside the\n // verified link set is repointed: a primary at a listing-seeded row would\n // otherwise keep showing the document in a container the head never named.\n const rowsAtRemovedContainer = projectionRows.flatMap((row) =>\n row.localId !== null &&\n row.containerId !== null &&\n (removedContainerIds.has(row.containerId) ||\n !input.verifiedLinkedContainerIds.has(row.containerId))\n ? [{ localId: row.localId, updatedAt: row.updatedAt ?? undefined }]\n : [],\n );\n if (rowsAtRemovedContainer.length === 0) {\n return [];\n }\n\n const remainingLinkRows = await tx\n .select({ containerId: documentContainerProjection.containerId })\n .from(documentContainerProjection)\n .where(eq(documentContainerProjection.documentId, documentId))\n .orderBy(asc(documentContainerProjection.containerId));\n const nextContainerId = selectNextContainerId(\n remainingLinkRows.map((row) => row.containerId),\n input.verifiedLinkedContainerIds,\n );\n\n const changedLocalIds: string[] = [];\n for (const row of rowsAtRemovedContainer) {\n await tx\n .update(documentProjection)\n .set({\n containerId: nextContainerId,\n updatedAt: getLatestTimestamp(row.updatedAt, tombstoneUpdatedAt),\n })\n .where(eq(documentProjection.localId, row.localId))\n .run();\n changedLocalIds.push(row.localId);\n }\n\n return changedLocalIds;\n}\n\nexport async function applyContainerDocumentTombstonesWithExec(\n execSql: ExecSql,\n tombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n) {\n const uniqueTombstones = dedupeContainerDocumentTombstones(tombstones);\n if (uniqueTombstones.length === 0) {\n return [];\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n\n return db.transaction(async (tx) => {\n // Besides the pending-intent guard, the verified head must not be older\n // than local document state: a lagging or replayed head must never\n // delete rows a newer listing or settled move wrote (`CheckEpoch`).\n const writable = await filterWritableDocumentPlacements(\n tx,\n uniqueTombstones.map((tombstone) => ({\n accessEpoch: tombstone.accessEpoch,\n documentId: tombstone.documentId,\n containerIds: [],\n })),\n );\n const writableIds = new Set(writable.map((input) => input.documentId));\n const applicable = uniqueTombstones.filter((tombstone) =>\n writableIds.has(tombstone.documentId),\n );\n const {\n removedContainerIdsByDocumentId,\n tombstoneUpdatedAtByDocumentId,\n verifiedLinkedContainerIdsByDocumentId,\n } = buildContainerDocumentTombstoneState(applicable);\n await deleteContainerDocumentTombstoneRows(\n tx,\n applicable,\n verifiedLinkedContainerIdsByDocumentId,\n );\n await deleteContainerDocumentTombstoneHoldRows(tx, applicable);\n\n const changedLocalIds: string[] = [];\n for (const [\n documentId,\n removedContainerIds,\n ] of removedContainerIdsByDocumentId) {\n changedLocalIds.push(\n ...(await updateSelectedContainersForDocumentTombstones({\n documentId,\n removedContainerIds,\n tombstoneUpdatedAt: tombstoneUpdatedAtByDocumentId.get(documentId),\n tx,\n verifiedLinkedContainerIds:\n verifiedLinkedContainerIdsByDocumentId.get(documentId) ?? new Set(),\n })),\n );\n }\n\n if (changedLocalIds.length === 0) {\n return [];\n }\n\n const rows = await tx\n .select(documentSummarySelection)\n .from(documentProjection)\n .leftJoin(documents, documentSummaryJoin)\n .where(inArray(documentProjection.localId, changedLocalIds))\n .orderBy(\n desc(documentProjection.updatedAt),\n desc(documentProjection.localId),\n );\n\n return rows.map(mapDocumentSummary);\n });\n}\n"]}
1
+ {"version":3,"file":"containerDocumentTombstones.js","sourceRoot":"","sources":["containerDocumentTombstones.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, desc, eq, inArray } from \"drizzle-orm\";\nimport { DOCUMENTS_APP_KIND } from \"../../../documents/documentConstants\";\nimport {\n documentContainerProjection,\n documentProjection,\n documents,\n} from \"../../../sqlite/schema\";\nimport { sqliteRowsPerStatement } from \"../../../sqlite/sqliteBindBudget\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport type { ExecSql } from \"../../../sqlite/sqlSchema\";\nimport {\n deleteDocumentPlacementRowsInTransaction,\n deleteUnlinkedDocumentPlacementRowsInTransaction,\n} from \"../../container-contents/documentContainerProjectionPersistence\";\nimport { filterWritableDocumentPlacements } from \"../../container-contents/documentPlacement\";\nimport { getLatestTimestamp } from \"../../latestTimestamp\";\nimport type { ContainerDocumentTombstoneInput } from \"../types\";\nimport { deleteContainerDocumentTombstoneHoldRows } from \"./containerDocumentTombstoneHolds\";\nimport {\n documentSummaryJoin,\n documentSummarySelection,\n mapDocumentSummary,\n} from \"./documentProjectionRows\";\n\nfunction dedupeContainerDocumentTombstones(\n tombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n): ContainerDocumentTombstoneInput[] {\n return Array.from(\n new Map(\n tombstones.map((tombstone) => [\n `${tombstone.documentId}\\u0000${tombstone.containerId}`,\n tombstone,\n ]),\n ).values(),\n );\n}\n\nfunction buildContainerDocumentTombstoneState(\n uniqueTombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n): {\n removedContainerIdsByDocumentId: Map<string, Set<string>>;\n tombstoneUpdatedAtByDocumentId: Map<string, string>;\n verifiedLinkedContainerIdsByDocumentId: Map<string, ReadonlySet<string>>;\n} {\n const removedContainerIdsByDocumentId = new Map<string, Set<string>>();\n const tombstoneUpdatedAtByDocumentId = new Map<string, string>();\n const verifiedLinkedContainerIdsByDocumentId = new Map<\n string,\n ReadonlySet<string>\n >();\n\n for (const tombstone of uniqueTombstones) {\n verifiedLinkedContainerIdsByDocumentId.set(\n tombstone.documentId,\n new Set(tombstone.linkedContainerIds),\n );\n const removedContainerIds =\n removedContainerIdsByDocumentId.get(tombstone.documentId) ?? new Set();\n removedContainerIds.add(tombstone.containerId);\n removedContainerIdsByDocumentId.set(\n tombstone.documentId,\n removedContainerIds,\n );\n tombstoneUpdatedAtByDocumentId.set(\n tombstone.documentId,\n getLatestTimestamp(\n tombstoneUpdatedAtByDocumentId.get(tombstone.documentId),\n tombstone.updatedAt,\n ),\n );\n }\n\n return {\n removedContainerIdsByDocumentId,\n tombstoneUpdatedAtByDocumentId,\n verifiedLinkedContainerIdsByDocumentId,\n };\n}\n\n/**\n * Remove the tombstoned placement and, with the head now verified, every\n * remaining local link row the head does not link: those are listing-seeded\n * rows, and leaving one would keep showing the document in a container the\n * signed link set never named. Rows the head links are kept as they are.\n */\nasync function deleteContainerDocumentTombstoneRows(\n tx: ClientSQLiteTransactionScope,\n uniqueTombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n verifiedLinkedContainerIdsByDocumentId: ReadonlyMap<\n string,\n ReadonlySet<string>\n >,\n): Promise<void> {\n await deleteDocumentPlacementRowsInTransaction(tx, uniqueTombstones);\n await deleteUnlinkedDocumentPlacementRowsInTransaction(\n tx,\n verifiedLinkedContainerIdsByDocumentId,\n );\n}\n\n/**\n * The primary container after a removal: the first remaining local row the\n * verified head links. A remaining row the head does not link is a\n * listing-seeded row and never becomes the primary, so a listing cannot\n * re-home the document. With no such row the document is unplaced (`null`)\n * and reachable through orphan recovery, rather than pointed at a head\n * container this device may not be able to read.\n */\nfunction selectNextContainerId(\n remainingContainerIds: ReadonlyArray<string>,\n verifiedLinkedContainerIds: ReadonlySet<string>,\n): string | null {\n return (\n remainingContainerIds.find((containerId) =>\n verifiedLinkedContainerIds.has(containerId),\n ) ?? null\n );\n}\n\ninterface TombstoneRepairReads {\n /** Local ids per document id. */\n readonly localIdsByDocumentId: ReadonlyMap<string, string[]>;\n readonly projectionByLocalId: ReadonlyMap<\n string,\n { containerId: string | null; updatedAt: string | null }\n >;\n /** Remaining link containers per document, in container id order. */\n readonly remainingLinksByDocumentId: ReadonlyMap<string, string[]>;\n}\n\nfunction pushGrouped(\n map: Map<string, string[]>,\n key: string,\n value: string,\n): void {\n const values = map.get(key);\n if (values) values.push(value);\n else map.set(key, [value]);\n}\n\n/**\n * Every row the repair reads, for all tombstoned documents at once. The\n * deletes above have already run, and the repair below writes only\n * projection rows of the document it repairs, so reading before any repair\n * sees what each per-document read saw.\n */\nasync function loadTombstoneRepairReads(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<TombstoneRepairReads> {\n const localIdsByDocumentId = new Map<string, string[]>();\n const projectionByLocalId = new Map<\n string,\n { containerId: string | null; updatedAt: string | null }\n >();\n const remainingLinksByDocumentId = new Map<string, string[]>();\n // The app kind binds one value; the rest carry ids.\n const chunkSize = sqliteRowsPerStatement(1, 1);\n for (let offset = 0; offset < documentIds.length; offset += chunkSize) {\n const batch = documentIds.slice(offset, offset + chunkSize);\n const localRows = await tx\n .select({ documentId: documents.documentId, localId: documents.localId })\n .from(documents)\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n inArray(documents.documentId, batch),\n ),\n );\n for (const row of localRows) {\n if (row.documentId !== null) {\n pushGrouped(localIdsByDocumentId, row.documentId, row.localId);\n }\n }\n const linkRows = await tx\n .select({\n containerId: documentContainerProjection.containerId,\n documentId: documentContainerProjection.documentId,\n })\n .from(documentContainerProjection)\n .where(inArray(documentContainerProjection.documentId, batch))\n .orderBy(\n asc(documentContainerProjection.documentId),\n asc(documentContainerProjection.containerId),\n );\n for (const row of linkRows) {\n pushGrouped(remainingLinksByDocumentId, row.documentId, row.containerId);\n }\n }\n const localIds = [...localIdsByDocumentId.values()].flat();\n for (let offset = 0; offset < localIds.length; offset += chunkSize) {\n const projectionRows = await tx\n .select({\n localId: documentProjection.localId,\n containerId: documentProjection.containerId,\n updatedAt: documentProjection.updatedAt,\n })\n .from(documentProjection)\n .where(\n inArray(\n documentProjection.localId,\n localIds.slice(offset, offset + chunkSize),\n ),\n );\n for (const row of projectionRows) {\n if (row.localId !== null) {\n projectionByLocalId.set(row.localId, {\n containerId: row.containerId,\n updatedAt: row.updatedAt,\n });\n }\n }\n }\n return {\n localIdsByDocumentId,\n projectionByLocalId,\n remainingLinksByDocumentId,\n };\n}\n\nasync function updateSelectedContainersForDocumentTombstones(input: {\n documentId: string;\n reads: TombstoneRepairReads;\n removedContainerIds: ReadonlySet<string>;\n tombstoneUpdatedAt: string | undefined;\n tx: ClientSQLiteTransactionScope;\n verifiedLinkedContainerIds: ReadonlySet<string>;\n}): Promise<string[]> {\n const { documentId, reads, removedContainerIds, tombstoneUpdatedAt, tx } =\n input;\n // A server document can own more than one local projection row: identity\n // recovery legitimately rematerializes a second projection for the same\n // documentId before semantic convergence completes. The link delete above is\n // keyed by (documentId, containerId) and the container item view surfaces a\n // document by its primary container_id, so EVERY projection row still pointing\n // at a removed container must be repaired here — resolving a single localId\n // left a duplicate row stranded at the source, keeping a moved document\n // visible in the container it was unlinked from.\n const localIds = reads.localIdsByDocumentId.get(documentId) ?? [];\n // Every projection row whose primary container is tombstoned OR outside the\n // verified link set is repointed: a primary at a listing-seeded row would\n // otherwise keep showing the document in a container the head never named.\n const rowsAtRemovedContainer = localIds.flatMap((localId) => {\n const row = reads.projectionByLocalId.get(localId);\n return row !== undefined &&\n row.containerId !== null &&\n (removedContainerIds.has(row.containerId) ||\n !input.verifiedLinkedContainerIds.has(row.containerId))\n ? [{ localId, updatedAt: row.updatedAt ?? undefined }]\n : [];\n });\n if (rowsAtRemovedContainer.length === 0) {\n return [];\n }\n\n const nextContainerId = selectNextContainerId(\n reads.remainingLinksByDocumentId.get(documentId) ?? [],\n input.verifiedLinkedContainerIds,\n );\n\n const changedLocalIds: string[] = [];\n for (const row of rowsAtRemovedContainer) {\n await tx\n .update(documentProjection)\n .set({\n containerId: nextContainerId,\n updatedAt: getLatestTimestamp(row.updatedAt, tombstoneUpdatedAt),\n })\n .where(eq(documentProjection.localId, row.localId))\n .run();\n changedLocalIds.push(row.localId);\n }\n\n return changedLocalIds;\n}\n\nexport async function applyContainerDocumentTombstonesWithExec(\n execSql: ExecSql,\n tombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n) {\n const uniqueTombstones = dedupeContainerDocumentTombstones(tombstones);\n if (uniqueTombstones.length === 0) {\n return [];\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n\n return db.transaction(async (tx) => {\n // Besides the pending-intent guard, the verified head must not be older\n // than local document state: a lagging or replayed head must never\n // delete rows a newer listing or settled move wrote (`CheckEpoch`).\n const writable = await filterWritableDocumentPlacements(\n tx,\n uniqueTombstones.map((tombstone) => ({\n accessEpoch: tombstone.accessEpoch,\n documentId: tombstone.documentId,\n containerIds: [],\n })),\n );\n const writableIds = new Set(writable.map((input) => input.documentId));\n const applicable = uniqueTombstones.filter((tombstone) =>\n writableIds.has(tombstone.documentId),\n );\n const {\n removedContainerIdsByDocumentId,\n tombstoneUpdatedAtByDocumentId,\n verifiedLinkedContainerIdsByDocumentId,\n } = buildContainerDocumentTombstoneState(applicable);\n await deleteContainerDocumentTombstoneRows(\n tx,\n applicable,\n verifiedLinkedContainerIdsByDocumentId,\n );\n await deleteContainerDocumentTombstoneHoldRows(tx, applicable);\n\n const reads = await loadTombstoneRepairReads(tx, [\n ...removedContainerIdsByDocumentId.keys(),\n ]);\n const changedLocalIds: string[] = [];\n for (const [\n documentId,\n removedContainerIds,\n ] of removedContainerIdsByDocumentId) {\n changedLocalIds.push(\n ...(await updateSelectedContainersForDocumentTombstones({\n documentId,\n reads,\n removedContainerIds,\n tombstoneUpdatedAt: tombstoneUpdatedAtByDocumentId.get(documentId),\n tx,\n verifiedLinkedContainerIds:\n verifiedLinkedContainerIdsByDocumentId.get(documentId) ?? new Set(),\n })),\n );\n }\n\n if (changedLocalIds.length === 0) {\n return [];\n }\n\n const rows = await tx\n .select(documentSummarySelection)\n .from(documentProjection)\n .leftJoin(documents, documentSummaryJoin)\n .where(inArray(documentProjection.localId, changedLocalIds))\n .orderBy(\n desc(documentProjection.updatedAt),\n desc(documentProjection.localId),\n );\n\n return rows.map(mapDocumentSummary);\n });\n}\n"]}
@@ -1,8 +1,11 @@
1
1
  import { DEFAULT_DOCUMENT_ACCESS_EPOCH } from "../../../documents/documentConstants.js";
2
- import { loadDocumentMovePlacement } from "../../container-contents/documentPlacement.js";
2
+ import { loadDocumentMovePlacement, } from "../../container-contents/documentPlacement.js";
3
3
  import { resolvePersistedAccessStateHash } from "./documentRuntimeState.js";
4
4
  export async function resolveDiscoveredDocumentPlacement(tx, existing, input) {
5
- const intent = await loadDocumentMovePlacement(tx, input.documentId);
5
+ return discoveredDocumentPlacement(existing, input, await loadDocumentMovePlacement(tx, input.documentId));
6
+ }
7
+ /** The placement discovery writes, given the document's pending move intent. */
8
+ export function discoveredDocumentPlacement(existing, input, intent) {
6
9
  const stale = existing !== null && existing.accessEpoch > input.accessEpoch;
7
10
  const accessEpoch = Math.max(existing?.accessEpoch ?? DEFAULT_DOCUMENT_ACCESS_EPOCH, input.accessEpoch);
8
11
  const pendingContainerId = intent?.intentType === "document.link"
@@ -1 +1 @@
1
- {"version":3,"file":"discoveredDocumentPlacement.js","sourceRoot":"","sources":["discoveredDocumentPlacement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,sCAAsC,CAAC;AAGrF,OAAO,EAAE,yBAAyB,EAAE,MAAM,4CAA4C,CAAC;AAEvF,OAAO,EAAE,+BAA+B,EAAE,MAAM,wBAAwB,CAAC;AAEzE,MAAM,CAAC,KAAK,UAAU,kCAAkC,CACtD,EAAgC,EAChC,QAAqC,EACrC,KAA8B;IAE9B,MAAM,MAAM,GAAG,MAAM,yBAAyB,CAAC,EAAE,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACrE,MAAM,KAAK,GAAG,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC;IAC5E,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAC1B,QAAQ,EAAE,WAAW,IAAI,6BAA6B,EACtD,KAAK,CAAC,WAAW,CAClB,CAAC;IACF,MAAM,kBAAkB,GACtB,MAAM,EAAE,UAAU,KAAK,eAAe;QACpC,CAAC,CAAC,CAAC,QAAQ,EAAE,WAAW,IAAI,MAAM,CAAC,iBAAiB,CAAC;QACrD,CAAC,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAChC,MAAM,WAAW,GACf,kBAAkB;QAClB,CAAC,KAAK;YACJ,CAAC,CAAC,QAAQ,CAAC,WAAW;YACtB,CAAC,CAAC,QAAQ,EAAE,WAAW;gBACnB,KAAK,CAAC,kBAAkB,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC;gBACzD,CAAC,CAAC,QAAQ,CAAC,WAAW;gBACtB,CAAC,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,KAAK,CAAC,WAAW,CAAC;oBAChE,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAAC;oBAC3B,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAC5B,OAAO;QACL,WAAW;QACX,WAAW;QACX,eAAe,EAAE,+BAA+B,CAAC,QAAQ,EAAE;YACzD,WAAW;YACX,eAAe,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,eAAe;YACzE,UAAU,EAAE,KAAK,CAAC,UAAU;SAC7B,CAAC;KACH,CAAC;AACJ,CAAC","sourcesContent":["import { DEFAULT_DOCUMENT_ACCESS_EPOCH } from \"../../../documents/documentConstants\";\nimport type { DiscoveredDocumentInput } from \"../../../documents/documentSummary\";\nimport type { ClientSQLiteTransactionScope } from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport { loadDocumentMovePlacement } from \"../../container-contents/documentPlacement\";\nimport type { StoredDocumentRecord } from \"../types\";\nimport { resolvePersistedAccessStateHash } from \"./documentRuntimeState\";\n\nexport async function resolveDiscoveredDocumentPlacement(\n tx: ClientSQLiteTransactionScope,\n existing: StoredDocumentRecord | null,\n input: DiscoveredDocumentInput,\n) {\n const intent = await loadDocumentMovePlacement(tx, input.documentId);\n const stale = existing !== null && existing.accessEpoch > input.accessEpoch;\n const accessEpoch = Math.max(\n existing?.accessEpoch ?? DEFAULT_DOCUMENT_ACCESS_EPOCH,\n input.accessEpoch,\n );\n const pendingContainerId =\n intent?.intentType === \"document.link\"\n ? (existing?.containerId ?? intent.targetContainerId)\n : intent?.targetContainerId;\n const containerId =\n pendingContainerId ??\n (stale\n ? existing.containerId\n : existing?.containerId &&\n input.linkedContainerIds.includes(existing.containerId)\n ? existing.containerId\n : (input.linkedContainerIds.find((id) => id === input.containerId) ??\n input.linkedContainerIds[0] ??\n input.containerId));\n return {\n accessEpoch,\n containerId,\n accessStateHash: resolvePersistedAccessStateHash(existing, {\n accessEpoch,\n accessStateHash: stale ? existing.accessStateHash : input.accessStateHash,\n documentId: input.documentId,\n }),\n };\n}\n"]}
1
+ {"version":3,"file":"discoveredDocumentPlacement.js","sourceRoot":"","sources":["discoveredDocumentPlacement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,sCAAsC,CAAC;AAGrF,OAAO,EAEL,yBAAyB,GAC1B,MAAM,4CAA4C,CAAC;AAEpD,OAAO,EAAE,+BAA+B,EAAE,MAAM,wBAAwB,CAAC;AAEzE,MAAM,CAAC,KAAK,UAAU,kCAAkC,CACtD,EAAgC,EAChC,QAAqC,EACrC,KAA8B;IAE9B,OAAO,2BAA2B,CAChC,QAAQ,EACR,KAAK,EACL,MAAM,yBAAyB,CAAC,EAAE,EAAE,KAAK,CAAC,UAAU,CAAC,CACtD,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,2BAA2B,CACzC,QAAqC,EACrC,KAA8B,EAC9B,MAAyC;IAEzC,MAAM,KAAK,GAAG,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC;IAC5E,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAC1B,QAAQ,EAAE,WAAW,IAAI,6BAA6B,EACtD,KAAK,CAAC,WAAW,CAClB,CAAC;IACF,MAAM,kBAAkB,GACtB,MAAM,EAAE,UAAU,KAAK,eAAe;QACpC,CAAC,CAAC,CAAC,QAAQ,EAAE,WAAW,IAAI,MAAM,CAAC,iBAAiB,CAAC;QACrD,CAAC,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAChC,MAAM,WAAW,GACf,kBAAkB;QAClB,CAAC,KAAK;YACJ,CAAC,CAAC,QAAQ,CAAC,WAAW;YACtB,CAAC,CAAC,QAAQ,EAAE,WAAW;gBACnB,KAAK,CAAC,kBAAkB,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC;gBACzD,CAAC,CAAC,QAAQ,CAAC,WAAW;gBACtB,CAAC,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,KAAK,CAAC,WAAW,CAAC;oBAChE,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAAC;oBAC3B,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAC5B,OAAO;QACL,WAAW;QACX,WAAW;QACX,eAAe,EAAE,+BAA+B,CAAC,QAAQ,EAAE;YACzD,WAAW;YACX,eAAe,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,eAAe;YACzE,UAAU,EAAE,KAAK,CAAC,UAAU;SAC7B,CAAC;KACH,CAAC;AACJ,CAAC","sourcesContent":["import { DEFAULT_DOCUMENT_ACCESS_EPOCH } from \"../../../documents/documentConstants\";\nimport type { DiscoveredDocumentInput } from \"../../../documents/documentSummary\";\nimport type { ClientSQLiteTransactionScope } from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport {\n type DocumentMovePlacement,\n loadDocumentMovePlacement,\n} from \"../../container-contents/documentPlacement\";\nimport type { StoredDocumentRecord } from \"../types\";\nimport { resolvePersistedAccessStateHash } from \"./documentRuntimeState\";\n\nexport async function resolveDiscoveredDocumentPlacement(\n tx: ClientSQLiteTransactionScope,\n existing: StoredDocumentRecord | null,\n input: DiscoveredDocumentInput,\n) {\n return discoveredDocumentPlacement(\n existing,\n input,\n await loadDocumentMovePlacement(tx, input.documentId),\n );\n}\n\n/** The placement discovery writes, given the document's pending move intent. */\nexport function discoveredDocumentPlacement(\n existing: StoredDocumentRecord | null,\n input: DiscoveredDocumentInput,\n intent: DocumentMovePlacement | undefined,\n) {\n const stale = existing !== null && existing.accessEpoch > input.accessEpoch;\n const accessEpoch = Math.max(\n existing?.accessEpoch ?? DEFAULT_DOCUMENT_ACCESS_EPOCH,\n input.accessEpoch,\n );\n const pendingContainerId =\n intent?.intentType === \"document.link\"\n ? (existing?.containerId ?? intent.targetContainerId)\n : intent?.targetContainerId;\n const containerId =\n pendingContainerId ??\n (stale\n ? existing.containerId\n : existing?.containerId &&\n input.linkedContainerIds.includes(existing.containerId)\n ? existing.containerId\n : (input.linkedContainerIds.find((id) => id === input.containerId) ??\n input.linkedContainerIds[0] ??\n input.containerId));\n return {\n accessEpoch,\n containerId,\n accessStateHash: resolvePersistedAccessStateHash(existing, {\n accessEpoch,\n accessStateHash: stale ? existing.accessStateHash : input.accessStateHash,\n documentId: input.documentId,\n }),\n };\n}\n"]}
@@ -0,0 +1,174 @@
1
+ import { KeyingVerificationError } from "../../../../internal/crypto/index.js";
2
+ import { DEFAULT_DOCUMENT_KIND, DOCUMENTS_APP_KIND, } from "../../../documents/documentConstants.js";
3
+ import { loadDocumentMovePlacements, } from "../../container-contents/documentPlacement.js";
4
+ import { hasDocumentPurgeCheckpointInTransaction, listPurgedDocumentIdsInTransaction, } from "../../keyingCheckpoints/documentPurgeCheckpointPersistence.js";
5
+ import { findLocalIdByDocumentId, findLocalIdsByDocumentIds } from "../records/index.js";
6
+ import { discoveredDocumentPlacement, resolveDiscoveredDocumentPlacement, } from "./discoveredDocumentPlacement.js";
7
+ import { toDocumentSummary } from "./documentProjectionRows.js";
8
+ import { documentSaveTimestampFrom, resolveDocumentSaveTimestamp, saveDocumentRows, saveDocumentRowsBatch, } from "./documentRows.js";
9
+ import { resolvePersistedDocumentRuntimeState } from "./documentRuntimeState.js";
10
+ import { loadPendingCreateSummary } from "./pendingCreateAdoption.js";
11
+ import { loadStoredDocumentState, loadStoredDocumentStates, } from "./rowQueryPersistence.js";
12
+ const DISCOVERED_DOCUMENT_PLACEHOLDER_TITLE = "Syncing document...";
13
+ function purgedDocumentError() {
14
+ return new KeyingVerificationError("rollback", "Document discovery targets a permanently purged document");
15
+ }
16
+ function fenceSkippedError() {
17
+ return new Error("Document recovery generation changed during discovery");
18
+ }
19
+ function nextDiscoveredDocument(localId, input, existingDocument, placement) {
20
+ return {
21
+ id: localId,
22
+ documentId: input.documentId,
23
+ documentKind: existingDocument?.documentKind ?? DEFAULT_DOCUMENT_KIND,
24
+ text: existingDocument?.text ?? "",
25
+ title: existingDocument?.title ?? DISCOVERED_DOCUMENT_PLACEHOLDER_TITLE,
26
+ snapshotEndVersion: existingDocument?.snapshotEndVersion ?? "",
27
+ recoveryGeneration: existingDocument?.recoveryGeneration ?? 0,
28
+ ...placement,
29
+ effectiveAccessLevel: input.effectiveAccessLevel ??
30
+ existingDocument?.effectiveAccessLevel ??
31
+ null,
32
+ ...resolvePersistedDocumentRuntimeState(existingDocument, {
33
+ accessEpoch: placement.accessEpoch,
34
+ documentId: input.documentId,
35
+ }),
36
+ };
37
+ }
38
+ /** One discovered document, read and written on its own (the per-row path). */
39
+ async function upsertDiscoveredDocumentWithExec(execSql, input, pendingCreates, tx) {
40
+ if (await hasDocumentPurgeCheckpointInTransaction(tx, input.documentId)) {
41
+ throw purgedDocumentError();
42
+ }
43
+ const existingLocalId = await findLocalIdByDocumentId(execSql, DOCUMENTS_APP_KIND, input.documentId);
44
+ const localId = existingLocalId ?? input.documentId;
45
+ const pendingLocalId = pendingCreates.get(input.documentId);
46
+ if (!existingLocalId && pendingLocalId) {
47
+ const pending = await loadPendingCreateSummary(execSql, pendingLocalId);
48
+ if (!pending)
49
+ throw new Error("Pending create projection is unavailable");
50
+ return pending;
51
+ }
52
+ const existingDocument = (await loadStoredDocumentState(execSql, localId))?.record ?? null;
53
+ const placement = await resolveDiscoveredDocumentPlacement(tx, existingDocument, input);
54
+ const nextDocument = nextDiscoveredDocument(localId, input, existingDocument, placement);
55
+ const updatedAt = await resolveDocumentSaveTimestamp({
56
+ document: nextDocument,
57
+ options: existingDocument ? undefined : { updatedAt: input.createdAt },
58
+ tx,
59
+ });
60
+ if (!(await saveDocumentRows({ document: nextDocument, tx, updatedAt }))) {
61
+ throw fenceSkippedError();
62
+ }
63
+ return toDocumentSummary(nextDocument, updatedAt);
64
+ }
65
+ async function upsertDiscoveredDocumentsPerRow(execSql, inputs, pendingCreates, tx) {
66
+ const summaries = [];
67
+ for (const input of inputs) {
68
+ summaries.push(await upsertDiscoveredDocumentWithExec(execSql, input, pendingCreates, tx));
69
+ }
70
+ return summaries;
71
+ }
72
+ /**
73
+ * Resolves every input to its pending-create summary or its target local id,
74
+ * in input order, so the first purged or unavailable input throws exactly as
75
+ * the per-row walk would. Returns null when one input's write could change
76
+ * another input's reads, which only the per-row walk handles.
77
+ */
78
+ async function resolveDiscoveryTargets(execSql, inputs, pendingCreates, tx) {
79
+ const documentIds = inputs.map((input) => input.documentId);
80
+ const purged = await listPurgedDocumentIdsInTransaction(tx, documentIds);
81
+ const existingLocalIds = await findLocalIdsByDocumentIds(execSql, DOCUMENTS_APP_KIND, documentIds);
82
+ const targets = [];
83
+ const pendingLocalIds = new Set();
84
+ for (const input of inputs) {
85
+ if (purged.has(input.documentId))
86
+ throw purgedDocumentError();
87
+ const existingLocalId = existingLocalIds.get(input.documentId);
88
+ const pendingLocalId = pendingCreates.get(input.documentId);
89
+ if (!existingLocalId && pendingLocalId) {
90
+ const pending = await loadPendingCreateSummary(execSql, pendingLocalId);
91
+ if (!pending)
92
+ throw new Error("Pending create projection is unavailable");
93
+ pendingLocalIds.add(pendingLocalId);
94
+ targets.push({ kind: "pending", summary: pending });
95
+ continue;
96
+ }
97
+ targets.push({
98
+ input,
99
+ kind: "write",
100
+ localId: existingLocalId ?? input.documentId,
101
+ });
102
+ }
103
+ const writeLocalIds = targets.flatMap((target) => target.kind === "write" ? [target.localId] : []);
104
+ const independent = new Set(writeLocalIds).size === writeLocalIds.length &&
105
+ !writeLocalIds.some((localId) => pendingLocalIds.has(localId));
106
+ return independent ? targets : null;
107
+ }
108
+ /**
109
+ * Whether writing each target leaves every other target's reads unchanged: a
110
+ * stored row at a target local id that carries a different document id would
111
+ * be re-pointed by its write, which another input's lookup could have read.
112
+ */
113
+ function targetsReadDisjointRows(targets, states) {
114
+ return targets.every((target) => target.kind !== "write" ||
115
+ (states.get(target.localId)?.record.documentId ??
116
+ target.input.documentId) === target.input.documentId);
117
+ }
118
+ function plannedDiscoveryWrite(target, state, intent) {
119
+ const existingDocument = state?.record ?? null;
120
+ const document = nextDiscoveredDocument(target.localId, target.input, existingDocument, discoveredDocumentPlacement(existingDocument, target.input, intent));
121
+ // The joined state read is the row and projection the per-row walk reads
122
+ // again for its timestamp; nothing earlier in this transaction writes it.
123
+ const updatedAt = state
124
+ ? documentSaveTimestampFrom({
125
+ projection: state.projection,
126
+ snapshotEndVersion: state.record.snapshotEndVersion,
127
+ }, document)
128
+ : target.input.createdAt;
129
+ return { document, updatedAt };
130
+ }
131
+ /**
132
+ * Discovery as set statements: one read per table for every input, then
133
+ * multi-row upserts. Inputs whose rows could interact (a repeated document
134
+ * id, a shared local id, or a row another input would re-point) keep the
135
+ * per-row walk, which reads each input after the previous one's writes.
136
+ */
137
+ export async function upsertDiscoveredDocumentsInTransaction(execSql, inputs, pendingCreates, tx) {
138
+ const documentIds = inputs.map((input) => input.documentId);
139
+ if (new Set(documentIds).size !== documentIds.length) {
140
+ return upsertDiscoveredDocumentsPerRow(execSql, inputs, pendingCreates, tx);
141
+ }
142
+ const targets = await resolveDiscoveryTargets(execSql, inputs, pendingCreates, tx);
143
+ if (!targets) {
144
+ return upsertDiscoveredDocumentsPerRow(execSql, inputs, pendingCreates, tx);
145
+ }
146
+ const writes = targets.flatMap((target) => target.kind === "write" ? [target] : []);
147
+ if (writes.length === 0) {
148
+ return targets.flatMap((target) => target.kind === "pending" ? [target.summary] : []);
149
+ }
150
+ const states = await loadStoredDocumentStates(execSql, writes.map((target) => target.localId));
151
+ if (!targetsReadDisjointRows(targets, states)) {
152
+ return upsertDiscoveredDocumentsPerRow(execSql, inputs, pendingCreates, tx);
153
+ }
154
+ const intents = await loadDocumentMovePlacements(tx, writes.map((target) => target.input.documentId));
155
+ const planned = new Map(writes.map((target) => [
156
+ target.localId,
157
+ plannedDiscoveryWrite(target, states.get(target.localId), intents.get(target.input.documentId)),
158
+ ]));
159
+ const saved = await saveDocumentRowsBatch({
160
+ entries: [...planned.values()],
161
+ tx,
162
+ });
163
+ if (saved.size !== planned.size)
164
+ throw fenceSkippedError();
165
+ return targets.map((target) => {
166
+ if (target.kind === "pending")
167
+ return target.summary;
168
+ const write = planned.get(target.localId);
169
+ if (!write)
170
+ throw new Error("Discovered document write is missing");
171
+ return toDocumentSummary(write.document, write.updatedAt);
172
+ });
173
+ }
174
+ //# sourceMappingURL=discoveredDocumentUpsert.js.map