@tearleads/client-sdk 0.1.113 → 0.1.115

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 (66) hide show
  1. package/data/persistence/container-contents/containerContentsPersistence.js +3 -8
  2. package/data/persistence/container-contents/containerContentsPersistence.js.map +1 -1
  3. package/data/persistence/container-contents/containerHydrationPersistence.js +15 -5
  4. package/data/persistence/container-contents/containerHydrationPersistence.js.map +1 -1
  5. package/data/persistence/container-contents/containerMetadataMutationPersistence.js +8 -2
  6. package/data/persistence/container-contents/containerMetadataMutationPersistence.js.map +1 -1
  7. package/data/persistence/container-contents/containerMetadataRows.js +34 -2
  8. package/data/persistence/container-contents/containerMetadataRows.js.map +1 -1
  9. package/data/persistence/container-contents/containerPersistence.d.ts +231 -0
  10. package/data/persistence/container-contents/containerPersistence.js +20 -16
  11. package/data/persistence/container-contents/containerPersistence.js.map +1 -1
  12. package/data/persistence/container-contents/containerRecoveryPersistence.js +167 -51
  13. package/data/persistence/container-contents/containerRecoveryPersistence.js.map +1 -1
  14. package/data/persistence/container-contents/documentContainerProjectionPersistence.js +56 -19
  15. package/data/persistence/container-contents/documentContainerProjectionPersistence.js.map +1 -1
  16. package/data/persistence/container-contents/documentMoveIntentPersistence.js +29 -5
  17. package/data/persistence/container-contents/documentMoveIntentPersistence.js.map +1 -1
  18. package/data/persistence/container-contents/documentPlacement.js +24 -5
  19. package/data/persistence/container-contents/documentPlacement.js.map +1 -1
  20. package/data/persistence/container-contents/documentPlacementIntentEnqueue.js +1 -1
  21. package/data/persistence/container-contents/documentPlacementIntentEnqueue.js.map +1 -1
  22. package/data/persistence/documents/containerDocumentTombstoneHoldsPersistence.js +1 -1
  23. package/data/persistence/documents/containerDocumentTombstoneHoldsPersistence.js.map +1 -1
  24. package/data/persistence/documents/documentDiscoveryEvidencePersistence.js +43 -35
  25. package/data/persistence/documents/documentDiscoveryEvidencePersistence.js.map +1 -1
  26. package/data/persistence/documents/documentsPersistence.js +4 -58
  27. package/data/persistence/documents/documentsPersistence.js.map +1 -1
  28. package/data/persistence/documents/internal/containerDocumentTombstoneHolds.js +15 -11
  29. package/data/persistence/documents/internal/containerDocumentTombstoneHolds.js.map +1 -1
  30. package/data/persistence/documents/internal/containerDocumentTombstones.js +87 -35
  31. package/data/persistence/documents/internal/containerDocumentTombstones.js.map +1 -1
  32. package/data/persistence/documents/internal/discoveredDocumentPlacement.js +5 -2
  33. package/data/persistence/documents/internal/discoveredDocumentPlacement.js.map +1 -1
  34. package/data/persistence/documents/internal/discoveredDocumentUpsert.js +174 -0
  35. package/data/persistence/documents/internal/discoveredDocumentUpsert.js.map +1 -0
  36. package/data/persistence/documents/internal/documentMutationPersistence.js +15 -15
  37. package/data/persistence/documents/internal/documentMutationPersistence.js.map +1 -1
  38. package/data/persistence/documents/internal/documentRows.js +122 -11
  39. package/data/persistence/documents/internal/documentRows.js.map +1 -1
  40. package/data/persistence/documents/internal/rowQueryPersistence.js +70 -27
  41. package/data/persistence/documents/internal/rowQueryPersistence.js.map +1 -1
  42. package/data/persistence/documents/records/documentHistoryPersistence.js +17 -11
  43. package/data/persistence/documents/records/documentHistoryPersistence.js.map +1 -1
  44. package/data/persistence/documents/records/documentRecordPersistence.d.ts +5 -0
  45. package/data/persistence/documents/records/documentRecordPersistence.js +41 -6
  46. package/data/persistence/documents/records/documentRecordPersistence.js.map +1 -1
  47. package/data/persistence/documents/records/index.d.ts +1 -1
  48. package/data/persistence/documents/records/index.js +1 -1
  49. package/data/persistence/documents/records/index.js.map +1 -1
  50. package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.d.ts +2 -0
  51. package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.js +17 -1
  52. package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.js.map +1 -1
  53. package/package.json +1 -1
  54. package/workflows/container-contents/documentQueries/index.js +21 -6
  55. package/workflows/container-contents/documentQueries/index.js.map +1 -1
  56. package/workflows/container-contents/remoteReset/containerRemoteReset.js +12 -7
  57. package/workflows/container-contents/remoteReset/containerRemoteReset.js.map +1 -1
  58. package/workflows/documents/documentMutationCommit.js +11 -12
  59. package/workflows/documents/documentMutationCommit.js.map +1 -1
  60. package/workflows/documents/documentMutationPreparation.d.ts +1 -0
  61. package/workflows/documents/documentMutationPreparation.js +1 -0
  62. package/workflows/documents/documentMutationPreparation.js.map +1 -1
  63. package/workflows/documents/documentPersistGuards.js +24 -0
  64. package/workflows/documents/documentPersistGuards.js.map +1 -1
  65. package/workflows/documents/remoteReset/documentRemoteReset.js +31 -18
  66. package/workflows/documents/remoteReset/documentRemoteReset.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"containerRecoveryPersistence.js","sourceRoot":"","sources":["containerRecoveryPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { toFingerprint } from \"@tearleads/crypto\";\nimport { and, eq } from \"drizzle-orm\";\nimport {\n documentPlacementIntentTables,\n inContainerIntentLanes,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport { getClientSQLitePersistenceRuntime } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport {\n type ExecSql,\n ensureSqlTables,\n runSerializedSqlMutation,\n} from \"../../sqlite/sqlSchema\";\nimport { sqlContainerContentsPersistence } from \"./containerContentsPersistence\";\nimport { recordContainerHydrationTombstones } from \"./containerHydrationPersistence\";\nimport { repairDocumentsForRemovedContainersInTransaction } from \"./containerStructuralRepair\";\nimport { discardLinkIntentsForRemovedContainers } from \"./documentLinkRemovalRepair\";\nimport { deleteContainerMetadataDocumentRowsInTransaction } from \"./dormantContainerMetadata\";\n\n// The token covers all local work the user is agreeing to discard, including\n// shared link intents. A second pane's edit invalidates the confirmation.\nasync function recoverySnapshot(\n execSql: ExecSql,\n containerId: string,\n organizationId: string,\n) {\n const [dormant] = await execSql(\n `SELECT container_id, organization_id FROM dormant_container_metadata\n WHERE container_id = ? AND organization_id = ?\n AND NOT EXISTS (SELECT 1 FROM containers WHERE id = ?)`,\n [containerId, organizationId, containerId],\n );\n if (!dormant) return null;\n const metadata = [];\n for (const [table, columns] of [\n [\n \"documents\",\n \"document_id, recovery_document_id, recovery_generation, snapshot_end_version, pending_base_version\",\n ],\n [\"document_history_checkpoints\", \"snapshot, end_version_vector\"],\n [\n \"document_history_updates\",\n \"id, update_data, origin, update_id, partial_start_version_vector, partial_end_version_vector, source_version_vector\",\n ],\n ]) {\n metadata.push(\n await execSql(\n `SELECT ${columns} FROM ${table} WHERE app_kind = 'container-metadata' AND local_id = ? ORDER BY rowid`,\n [containerId],\n ),\n );\n }\n const creates = await execSql(\n \"SELECT id, subject_id, target_container_id, intent_type, remote_container_id, remote_metadata_document_id, remote_metadata_access_state_hash FROM local_intents WHERE lane = ? AND subject_id = ? AND sync_status = 'pending' ORDER BY id\",\n [LOCAL_INTENT_LANES.containerCreate, containerId],\n );\n const moves = await execSql(\n \"SELECT id, subject_id, target_container_id, source_container_id, intent_type FROM local_intents WHERE lane = ? AND subject_id = ? ORDER BY id\",\n [LOCAL_INTENT_LANES.containerMove, containerId],\n );\n const placementWhere = `lane = ? AND (target_container_id = ? OR id IN\n (SELECT intent_id FROM document_intent_link_targets WHERE container_id = ?))`;\n const placementBind = [\n LOCAL_INTENT_LANES.documentPlacement,\n containerId,\n containerId,\n ];\n const links = await execSql(\n `SELECT id, local_id, subject_id, target_container_id, source_container_id, replace_linked_containers, intent_type FROM local_intents WHERE ${placementWhere} ORDER BY id`,\n placementBind,\n );\n const targets = await execSql(\n `SELECT intent_id, operation, container_id FROM document_intent_link_targets WHERE intent_id IN\n (SELECT id FROM local_intents WHERE ${placementWhere}) ORDER BY intent_id, container_id, operation`,\n placementBind,\n );\n const revision = await toFingerprint(\n new TextEncoder().encode(\n JSON.stringify([dormant, metadata, creates, moves, links, targets]),\n ),\n );\n return {\n revision,\n pendingUpdateCount: (metadata[2] ?? []).filter(\n (row) => Reflect.get(row, \"update_id\") !== null,\n ).length,\n hasStructuralIntent: creates.length + moves.length + links.length > 0,\n };\n}\n\nexport async function listRetainedContainerMetadata(\n execSql: ExecSql,\n organizationId: string | null,\n) {\n if (!organizationId) return [];\n await sqlContainerContentsPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, documentPlacementIntentTables);\n return runSerializedSqlMutation(execSql, async (locked) =>\n getClientSQLitePersistenceRuntime(locked).transaction(async () => {\n const rows = await locked(\n `SELECT container_id FROM dormant_container_metadata\n WHERE organization_id = ? ORDER BY container_id`,\n [organizationId],\n );\n const result = [];\n for (const row of rows) {\n const containerId = String(Reflect.get(row, \"container_id\"));\n const snapshot = await recoverySnapshot(\n locked,\n containerId,\n organizationId,\n );\n if (!snapshot) continue;\n const record =\n await sqlContainerContentsPersistence.loadContainerMetadataRecord(\n locked,\n containerId,\n );\n result.push({\n containerId,\n organizationId,\n ...snapshot,\n metadataUpdates: record?.metadataUpdates ?? \"\",\n });\n }\n return result;\n }),\n );\n}\n\n/** Explicit local discard, never called by remote discovery. */\nexport async function discardRetainedContainerMetadata(\n execSql: ExecSql,\n input: {\n containerId: string;\n organizationId: string;\n revision: string;\n },\n): Promise<boolean> {\n await sqlContainerContentsPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, documentPlacementIntentTables);\n return runSerializedSqlMutation(execSql, async (locked) =>\n getClientSQLitePersistenceRuntime(locked).transaction(\n async (tx) => {\n const current = await recoverySnapshot(\n locked,\n input.containerId,\n input.organizationId,\n );\n if (!current || current.revision !== input.revision) return false;\n await recordContainerHydrationTombstones({\n tx,\n removals: [\n {\n containerId: input.containerId,\n reason: \"deleted\",\n updatedAt: new Date().toISOString(),\n },\n ],\n });\n await repairDocumentsForRemovedContainersInTransaction({\n tx,\n removals: [\n {\n containerId: input.containerId,\n reason: \"deleted\",\n updatedAt: new Date().toISOString(),\n },\n ],\n });\n await discardLinkIntentsForRemovedContainers({\n tx,\n containerIds: [input.containerId],\n });\n await tx\n .delete(localIntents)\n .where(\n and(\n inContainerIntentLanes(),\n eq(localIntents.subjectId, input.containerId),\n ),\n )\n .run();\n await deleteContainerMetadataDocumentRowsInTransaction(tx, [\n input.containerId,\n ]);\n return true;\n },\n { behavior: \"immediate\" },\n ),\n );\n}\n\nexport async function hasRetainedContainerMetadata(\n execSql: ExecSql,\n organizationId: string | null,\n): Promise<boolean> {\n if (!organizationId) return false;\n await sqlContainerContentsPersistence.ensureSchema(execSql);\n const rows = await execSql(\n `SELECT 1 FROM dormant_container_metadata d\n WHERE d.organization_id = ? AND NOT EXISTS (SELECT 1 FROM containers c WHERE c.id = d.container_id) LIMIT 1`,\n [organizationId],\n );\n return rows.length > 0;\n}\n\n/** Existing local folders whose queued move points at an unavailable parent. */\nexport async function listRecoveryFolderMoveIds(\n execSql: ExecSql,\n organizationId: string | null,\n): Promise<string[]> {\n if (!organizationId) return [];\n await sqlContainerContentsPersistence.ensureSchema(execSql);\n const rows = await execSql(\n `SELECT c.id FROM containers c JOIN local_intents i\n ON i.lane = ? AND i.subject_id = c.id AND i.target_container_id = c.parent_id\n WHERE c.organization_id = ? AND i.sync_status IN ('pending', 'blocked')\n AND NOT EXISTS (SELECT 1 FROM containers p WHERE p.id = c.parent_id)\n ORDER BY c.id`,\n [LOCAL_INTENT_LANES.containerMove, organizationId],\n );\n return rows.map((row) => String(Reflect.get(row, \"id\")));\n}\n"]}
1
+ {"version":3,"file":"containerRecoveryPersistence.js","sourceRoot":"","sources":["containerRecoveryPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { toFingerprint } from \"@tearleads/crypto\";\nimport { and, eq } from \"drizzle-orm\";\nimport {\n documentPlacementIntentTables,\n inContainerIntentLanes,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport { getClientSQLitePersistenceRuntime } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport {\n type ExecSql,\n ensureSqlTables,\n runSerializedSqlMutation,\n} from \"../../sqlite/sqlSchema\";\nimport { sqlContainerContentsPersistence } from \"./containerContentsPersistence\";\nimport { recordContainerHydrationTombstones } from \"./containerHydrationPersistence\";\nimport { repairDocumentsForRemovedContainersInTransaction } from \"./containerStructuralRepair\";\nimport { discardLinkIntentsForRemovedContainers } from \"./documentLinkRemovalRepair\";\nimport { deleteContainerMetadataDocumentRowsInTransaction } from \"./dormantContainerMetadata\";\n\nconst RECOVERY_METADATA_TABLES = [\n [\n \"documents\",\n \"document_id, recovery_document_id, recovery_generation, snapshot_end_version, pending_base_version\",\n ],\n [\"document_history_checkpoints\", \"snapshot, end_version_vector\"],\n [\n \"document_history_updates\",\n \"id, update_data, origin, update_id, partial_start_version_vector, partial_end_version_vector, source_version_vector\",\n ],\n] as const;\nconst CREATE_COLUMNS =\n \"id, subject_id, target_container_id, intent_type, remote_container_id, remote_metadata_document_id, remote_metadata_access_state_hash\";\nconst MOVE_COLUMNS =\n \"id, subject_id, target_container_id, source_container_id, intent_type\";\nconst LINK_COLUMNS =\n \"id, local_id, subject_id, target_container_id, source_container_id, replace_linked_containers, intent_type\";\nconst TARGET_COLUMNS = \"intent_id, operation, container_id\";\n\ntype RecoveryRows = Awaited<ReturnType<ExecSql>>;\n\n// The token covers all local work the user is agreeing to discard, including\n// shared link intents. A second pane's edit invalidates the confirmation.\nasync function summarizeRecoverySnapshot(input: {\n creates: RecoveryRows;\n dormant: RecoveryRows[number];\n links: RecoveryRows;\n metadata: RecoveryRows[];\n moves: RecoveryRows;\n targets: RecoveryRows;\n}) {\n const { creates, dormant, links, metadata, moves, targets } = input;\n const revision = await toFingerprint(\n new TextEncoder().encode(\n JSON.stringify([dormant, metadata, creates, moves, links, targets]),\n ),\n );\n return {\n revision,\n pendingUpdateCount: (metadata[2] ?? []).filter(\n (row) => Reflect.get(row, \"update_id\") !== null,\n ).length,\n hasStructuralIntent: creates.length + moves.length + links.length > 0,\n };\n}\n\nasync function recoverySnapshot(\n execSql: ExecSql,\n containerId: string,\n organizationId: string,\n) {\n const [dormant] = await execSql(\n `SELECT container_id, organization_id FROM dormant_container_metadata\n WHERE container_id = ? AND organization_id = ?\n AND NOT EXISTS (SELECT 1 FROM containers WHERE id = ?)`,\n [containerId, organizationId, containerId],\n );\n if (!dormant) return null;\n const metadata = [];\n for (const [table, columns] of RECOVERY_METADATA_TABLES) {\n metadata.push(\n await execSql(\n `SELECT ${columns} FROM ${table} WHERE app_kind = 'container-metadata' AND local_id = ? ORDER BY rowid`,\n [containerId],\n ),\n );\n }\n const creates = await execSql(\n `SELECT ${CREATE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id = ? AND sync_status = 'pending' ORDER BY id`,\n [LOCAL_INTENT_LANES.containerCreate, containerId],\n );\n const moves = await execSql(\n `SELECT ${MOVE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id = ? ORDER BY id`,\n [LOCAL_INTENT_LANES.containerMove, containerId],\n );\n const placementWhere = `lane = ? AND (target_container_id = ? OR id IN\n (SELECT intent_id FROM document_intent_link_targets WHERE container_id = ?))`;\n const placementBind = [\n LOCAL_INTENT_LANES.documentPlacement,\n containerId,\n containerId,\n ];\n const links = await execSql(\n `SELECT ${LINK_COLUMNS} FROM local_intents WHERE ${placementWhere} ORDER BY id`,\n placementBind,\n );\n const targets = await execSql(\n `SELECT ${TARGET_COLUMNS} FROM document_intent_link_targets WHERE intent_id IN\n (SELECT id FROM local_intents WHERE ${placementWhere}) ORDER BY intent_id, container_id, operation`,\n placementBind,\n );\n return summarizeRecoverySnapshot({\n creates,\n dormant,\n links,\n metadata,\n moves,\n targets,\n });\n}\n\n/** Rows grouped by one column's value, dropping that column when asked. */\nfunction groupRows(\n rows: RecoveryRows,\n column: string,\n drop: boolean,\n): Map<string, RecoveryRows> {\n const groups = new Map<string, RecoveryRows>();\n for (const row of rows) {\n const key = String(Reflect.get(row, column));\n const kept = drop\n ? Object.fromEntries(\n Object.entries(row).filter(([name]) => name !== column),\n )\n : row;\n const group = groups.get(key);\n if (group) group.push(kept);\n else groups.set(key, [kept]);\n }\n return groups;\n}\n\n/** Keeps SQL `IN` lists, bound twice by the placement reads, under the budget. */\nconst RECOVERY_CONTAINER_BATCH_SIZE = 400;\n\n/** Every row the recovery snapshots of a batch of folders read. */\nasync function readRecoveryBatchRows(execSql: ExecSql, ids: string[]) {\n const list = ids.map(() => \"?\").join(\", \");\n const metadata = [];\n for (const [table, columns] of RECOVERY_METADATA_TABLES) {\n metadata.push(\n groupRows(\n await execSql(\n `SELECT local_id, ${columns} FROM ${table} WHERE app_kind = 'container-metadata' AND local_id IN (${list}) ORDER BY local_id, rowid`,\n ids,\n ),\n \"local_id\",\n true,\n ),\n );\n }\n const creates = groupRows(\n await execSql(\n `SELECT ${CREATE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id IN (${list}) AND sync_status = 'pending' ORDER BY subject_id, id`,\n [LOCAL_INTENT_LANES.containerCreate, ...ids],\n ),\n \"subject_id\",\n false,\n );\n const moves = groupRows(\n await execSql(\n `SELECT ${MOVE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id IN (${list}) ORDER BY subject_id, id`,\n [LOCAL_INTENT_LANES.containerMove, ...ids],\n ),\n \"subject_id\",\n false,\n );\n const placementWhere = `lane = ? AND (target_container_id IN (${list}) OR id IN\n (SELECT intent_id FROM document_intent_link_targets WHERE container_id IN (${list})))`;\n const placementBind = [LOCAL_INTENT_LANES.documentPlacement, ...ids, ...ids];\n // Every placement intent any folder in the batch reaches, and ALL link\n // targets of those intents: a folder's share is then the intents that\n // target it or carry a link target in it, as the per-folder read selects.\n const links = await execSql(\n `SELECT ${LINK_COLUMNS} FROM local_intents WHERE ${placementWhere} ORDER BY id`,\n placementBind,\n );\n const targets = await execSql(\n `SELECT ${TARGET_COLUMNS} FROM document_intent_link_targets WHERE intent_id IN\n (SELECT id FROM local_intents WHERE ${placementWhere}) ORDER BY intent_id, container_id, operation`,\n placementBind,\n );\n return { creates, links, metadata, moves, targets };\n}\n\n/**\n * Each folder's share of a batch's placement rows, indexed in one pass: the\n * intents that target the folder or carry a link target in it (in intent id\n * order), and all link targets of those intents (in the per-folder order).\n */\nfunction indexPlacementRows(\n rows: Awaited<ReturnType<typeof readRecoveryBatchRows>>,\n) {\n const targetsByIntent = groupRows(rows.targets, \"intent_id\", false);\n const linksByFolder = new Map<string, RecoveryRows>();\n for (const link of rows.links) {\n const intentId = String(Reflect.get(link, \"id\"));\n const folders = new Set([\n String(Reflect.get(link, \"target_container_id\")),\n ...(targetsByIntent.get(intentId) ?? []).map((target) =>\n String(Reflect.get(target, \"container_id\")),\n ),\n ]);\n for (const folder of folders) {\n const folderLinks = linksByFolder.get(folder);\n if (folderLinks) folderLinks.push(link);\n else linksByFolder.set(folder, [link]);\n }\n }\n return { linksByFolder, targetsByIntent };\n}\n\n/** One folder's snapshot from its batch's rows, in the per-folder shape. */\nasync function folderRecoverySnapshot(\n rows: Awaited<ReturnType<typeof readRecoveryBatchRows>>,\n placements: ReturnType<typeof indexPlacementRows>,\n containerId: string,\n dormant: RecoveryRows[number],\n) {\n const links = placements.linksByFolder.get(containerId) ?? [];\n // Links are in intent id order and targets in (intent id, container,\n // operation) order, so concatenating per link keeps the per-folder order.\n const targets = links.flatMap(\n (link) =>\n placements.targetsByIntent.get(String(Reflect.get(link, \"id\"))) ?? [],\n );\n const metadata = rows.metadata.map(\n (byFolder) => byFolder.get(containerId) ?? [],\n );\n // The metadata record's updates are its checkpoint snapshot, read above; a\n // folder with no record has none.\n const snapshotRow = (metadata[1] ?? [])[0];\n return {\n containerId,\n metadataUpdates:\n (metadata[0] ?? []).length > 0 && snapshotRow\n ? String(Reflect.get(snapshotRow, \"snapshot\") ?? \"\")\n : \"\",\n snapshot: await summarizeRecoverySnapshot({\n creates: rows.creates.get(containerId) ?? [],\n dormant,\n links,\n metadata,\n moves: rows.moves.get(containerId) ?? [],\n targets,\n }),\n };\n}\n\n/**\n * `recoverySnapshot` for every retained folder of an organization, one read\n * per table for each batch of folders. Each folder's rows are grouped in the\n * per-folder order (`local_id, rowid` for metadata; `subject_id, id` for\n * folder intents), so its revision is byte-identical to `recoverySnapshot`'s,\n * which a later discard compares.\n */\nasync function recoverySnapshots(execSql: ExecSql, organizationId: string) {\n const dormantRows = await execSql(\n `SELECT container_id, organization_id FROM dormant_container_metadata d\n WHERE organization_id = ?\n AND NOT EXISTS (SELECT 1 FROM containers WHERE id = d.container_id)\n ORDER BY container_id`,\n [organizationId],\n );\n const result = [];\n for (\n let offset = 0;\n offset < dormantRows.length;\n offset += RECOVERY_CONTAINER_BATCH_SIZE\n ) {\n const batch = dormantRows.slice(\n offset,\n offset + RECOVERY_CONTAINER_BATCH_SIZE,\n );\n const ids = batch.map((row) => String(Reflect.get(row, \"container_id\")));\n const rows = await readRecoveryBatchRows(execSql, ids);\n const placements = indexPlacementRows(rows);\n for (const [index, dormant] of batch.entries()) {\n result.push(\n await folderRecoverySnapshot(\n rows,\n placements,\n ids[index] ?? \"\",\n dormant,\n ),\n );\n }\n }\n return result;\n}\n\nexport async function listRetainedContainerMetadata(\n execSql: ExecSql,\n organizationId: string | null,\n) {\n if (!organizationId) return [];\n await sqlContainerContentsPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, documentPlacementIntentTables);\n return runSerializedSqlMutation(execSql, async (locked) =>\n getClientSQLitePersistenceRuntime(locked).transaction(async () =>\n (await recoverySnapshots(locked, organizationId)).map(\n ({ containerId, metadataUpdates, snapshot }) => ({\n containerId,\n organizationId,\n ...snapshot,\n metadataUpdates,\n }),\n ),\n ),\n );\n}\n\n/** Explicit local discard, never called by remote discovery. */\nexport async function discardRetainedContainerMetadata(\n execSql: ExecSql,\n input: {\n containerId: string;\n organizationId: string;\n revision: string;\n },\n): Promise<boolean> {\n await sqlContainerContentsPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, documentPlacementIntentTables);\n return runSerializedSqlMutation(execSql, async (locked) =>\n getClientSQLitePersistenceRuntime(locked).transaction(\n async (tx) => {\n const current = await recoverySnapshot(\n locked,\n input.containerId,\n input.organizationId,\n );\n if (!current || current.revision !== input.revision) return false;\n await recordContainerHydrationTombstones({\n tx,\n removals: [\n {\n containerId: input.containerId,\n reason: \"deleted\",\n updatedAt: new Date().toISOString(),\n },\n ],\n });\n await repairDocumentsForRemovedContainersInTransaction({\n tx,\n removals: [\n {\n containerId: input.containerId,\n reason: \"deleted\",\n updatedAt: new Date().toISOString(),\n },\n ],\n });\n await discardLinkIntentsForRemovedContainers({\n tx,\n containerIds: [input.containerId],\n });\n await tx\n .delete(localIntents)\n .where(\n and(\n inContainerIntentLanes(),\n eq(localIntents.subjectId, input.containerId),\n ),\n )\n .run();\n await deleteContainerMetadataDocumentRowsInTransaction(tx, [\n input.containerId,\n ]);\n return true;\n },\n { behavior: \"immediate\" },\n ),\n );\n}\n\n/** Retained metadata of an organization's absent folders; binds the org. */\nexport const RETAINED_CONTAINER_METADATA_SQL = `SELECT 1 FROM dormant_container_metadata d\n WHERE d.organization_id = ? AND NOT EXISTS (SELECT 1 FROM containers c WHERE c.id = d.container_id)`;\n\n/**\n * An organization's local folders whose queued move points at an unavailable\n * parent; binds the move lane, then the organization.\n */\nexport const RECOVERY_FOLDER_MOVES_SQL = `SELECT c.id FROM containers c JOIN local_intents i\n ON i.lane = ? AND i.subject_id = c.id AND i.target_container_id = c.parent_id\n WHERE c.organization_id = ? AND i.sync_status IN ('pending', 'blocked')\n AND NOT EXISTS (SELECT 1 FROM containers p WHERE p.id = c.parent_id)`;\n\n/** Existing local folders whose queued move points at an unavailable parent. */\nexport async function listRecoveryFolderMoveIds(\n execSql: ExecSql,\n organizationId: string | null,\n): Promise<string[]> {\n if (!organizationId) return [];\n await sqlContainerContentsPersistence.ensureSchema(execSql);\n const rows = await execSql(`${RECOVERY_FOLDER_MOVES_SQL} ORDER BY c.id`, [\n LOCAL_INTENT_LANES.containerMove,\n organizationId,\n ]);\n return rows.map((row) => String(Reflect.get(row, \"id\")));\n}\n"]}
@@ -1,10 +1,12 @@
1
- import { and, asc, eq, inArray, notInArray } from "drizzle-orm";
1
+ import { and, asc, eq, inArray, notInArray, or } from "drizzle-orm";
2
2
  import { documentContainerProjection, documentContainerProjectionTables, documentPlacementIntentTables, documentTables, inLocalIntentLane, LOCAL_INTENT_LANES, localIntents, } from "../../sqlite/schema/index.js";
3
3
  import { documentIntentLinkTargets } from "../../sqlite/schema/documentPlacementIntentSchema.js";
4
+ import { REMOTE_RESET_SQL_BATCH_SIZE, remoteResetBatches, SQLITE_BOUND_VARIABLE_BUDGET, sqliteInsertBatchSize, } from "../../sqlite/sqliteBindBudget.js";
4
5
  import { getClientSQLitePersistenceRuntime, } from "../../sqlite/sqlitePersistenceRuntime.js";
5
6
  import { ensureSqlTables } from "../../sqlite/sqlSchema.js";
6
- import { deleteContainerDocumentTombstoneHoldRowsForLinks, deleteContainerDocumentTombstoneHoldsForContainers, deleteContainerDocumentTombstoneHoldsForDocuments, } from "../documents/containerDocumentTombstoneHoldsPersistence.js";
7
+ import { deleteContainerDocumentTombstoneHoldRowsForLinks, deleteContainerDocumentTombstoneHoldsForContainers, deleteContainerDocumentTombstoneHoldsForDocuments, placementPairsMatch, } from "../documents/containerDocumentTombstoneHoldsPersistence.js";
7
8
  import { filterWritableDocumentPlacements, } from "./documentPlacement.js";
9
+ const LINK_INSERT_BATCH_SIZE = sqliteInsertBatchSize(documentContainerProjection);
8
10
  /** Link rows and their tombstone holds leave together on a remote reset. */
9
11
  export async function deleteDocumentPlacementRowsByContainerIds(tx, containerIds) {
10
12
  await tx
@@ -20,23 +22,52 @@ export async function deleteDocumentPlacementRowsByDocumentIds(tx, documentIds)
20
22
  .run();
21
23
  await deleteContainerDocumentTombstoneHoldsForDocuments(tx, documentIds);
22
24
  }
23
- /** Removes one placement a verified tombstone retired. */
24
- export async function deleteDocumentPlacementRowInTransaction(tx, placement) {
25
- await tx
26
- .delete(documentContainerProjection)
27
- .where(and(eq(documentContainerProjection.documentId, placement.documentId), eq(documentContainerProjection.containerId, placement.containerId)))
28
- .run();
25
+ /** Removes the listed (document, container) placements, 50 pairs per statement. */
26
+ export async function deleteDocumentPlacementRowsInTransaction(tx, placements) {
27
+ for (const batch of remoteResetBatches(placements)) {
28
+ await tx
29
+ .delete(documentContainerProjection)
30
+ .where(placementPairsMatch(batch, documentContainerProjection.documentId, documentContainerProjection.containerId))
31
+ .run();
32
+ }
29
33
  }
30
- /** Removes a document's placements outside the containers its head links. */
31
- export async function deleteUnlinkedDocumentPlacementRowsInTransaction(tx, documentId, linkedContainerIds) {
32
- await tx
33
- .delete(documentContainerProjection)
34
- .where(linkedContainerIds.size === 0
35
- ? eq(documentContainerProjection.documentId, documentId)
36
- : and(eq(documentContainerProjection.documentId, documentId), notInArray(documentContainerProjection.containerId, [
34
+ /**
35
+ * Removes each document's placements outside the containers its head links,
36
+ * batching documents so one statement stays inside SQLite's bind budget.
37
+ */
38
+ export async function deleteUnlinkedDocumentPlacementRowsInTransaction(tx, linkedContainerIdsByDocumentId) {
39
+ let batch = [];
40
+ let binds = 0;
41
+ const flush = async () => {
42
+ if (batch.length === 0)
43
+ return;
44
+ await tx
45
+ .delete(documentContainerProjection)
46
+ .where(or(...batch))
47
+ .run();
48
+ batch = [];
49
+ binds = 0;
50
+ };
51
+ for (const [documentId, linked] of linkedContainerIdsByDocumentId) {
52
+ // The document id plus one bind per linked container.
53
+ const documentBinds = 1 + linked.size;
54
+ if (batch.length >= REMOTE_RESET_SQL_BATCH_SIZE ||
55
+ binds + documentBinds > SQLITE_BOUND_VARIABLE_BUDGET) {
56
+ await flush();
57
+ }
58
+ batch.push(unlinkedDocumentPlacementRows(documentId, linked));
59
+ binds += documentBinds;
60
+ }
61
+ await flush();
62
+ }
63
+ /** A document's placements outside the containers its head links. */
64
+ function unlinkedDocumentPlacementRows(documentId, linkedContainerIds) {
65
+ const ofDocument = eq(documentContainerProjection.documentId, documentId);
66
+ return linkedContainerIds.size === 0
67
+ ? ofDocument
68
+ : (and(ofDocument, notInArray(documentContainerProjection.containerId, [
37
69
  ...linkedContainerIds,
38
- ])))
39
- .run();
70
+ ])) ?? ofDocument);
40
71
  }
41
72
  /** Removes a deleted document's queued placement intents and link targets. */
42
73
  export async function deleteDocumentPlacementIntentsInTransaction(tx, documentId) {
@@ -142,8 +173,14 @@ export const sqlDocumentContainerProjectionPersistence = {
142
173
  containerId,
143
174
  updatedAt,
144
175
  })));
145
- if (rows.length > 0)
146
- await tx.insert(documentContainerProjection).values(rows).run();
176
+ // A lane page of multi-link documents can carry more link rows than
177
+ // one statement may bind.
178
+ for (let index = 0; index < rows.length; index += LINK_INSERT_BATCH_SIZE) {
179
+ await tx
180
+ .insert(documentContainerProjection)
181
+ .values(rows.slice(index, index + LINK_INSERT_BATCH_SIZE))
182
+ .run();
183
+ }
147
184
  };
148
185
  if (options?.stillCurrent) {
149
186
  await runtime.guardedTransaction(replace, options.stillCurrent);
@@ -1 +1 @@
1
- {"version":3,"file":"documentContainerProjectionPersistence.js","sourceRoot":"","sources":["documentContainerProjectionPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, eq, inArray, notInArray } from \"drizzle-orm\";\nimport {\n documentContainerProjection,\n documentContainerProjectionTables,\n documentPlacementIntentTables,\n documentTables,\n inLocalIntentLane,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport { documentIntentLinkTargets } from \"../../sqlite/schema/documentPlacementIntentSchema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../sqlite/sqlSchema\";\nimport {\n deleteContainerDocumentTombstoneHoldRowsForLinks,\n deleteContainerDocumentTombstoneHoldsForContainers,\n deleteContainerDocumentTombstoneHoldsForDocuments,\n} from \"../documents/containerDocumentTombstoneHoldsPersistence\";\n\nimport {\n type DocumentPlacementInput,\n type DocumentPlacementWriteOptions,\n filterWritableDocumentPlacements,\n} from \"./documentPlacement\";\n\ninterface DocumentContainerProjectionPersistence {\n ensureSchema: (execSql: ExecSql) => Promise<void>;\n listLinkedContainerIds: (\n execSql: ExecSql,\n documentId: string,\n ) => Promise<ReadonlyArray<string>>;\n listLinkedContainerIdsByDocumentIds: (\n execSql: ExecSql,\n documentIds: ReadonlyArray<string>,\n ) => Promise<ReadonlyMap<string, ReadonlyArray<string>>>;\n listDocumentIdsByContainerIds: (\n execSql: ExecSql,\n containerIds: ReadonlyArray<string>,\n ) => Promise<ReadonlyArray<string>>;\n replaceDocumentLinks: (\n execSql: ExecSql,\n documentId: string,\n containerIds: ReadonlyArray<string>,\n options?: DocumentPlacementWriteOptions,\n ) => Promise<void>;\n replaceDocumentLinksBatch: (\n execSql: ExecSql,\n inputs: ReadonlyArray<DocumentPlacementInput>,\n options?: DocumentPlacementWriteOptions,\n ) => Promise<void>;\n}\n\n/** Link rows and their tombstone holds leave together on a remote reset. */\nexport async function deleteDocumentPlacementRowsByContainerIds(\n tx: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<void> {\n await tx\n .delete(documentContainerProjection)\n .where(inArray(documentContainerProjection.containerId, containerIds))\n .run();\n await deleteContainerDocumentTombstoneHoldsForContainers(tx, containerIds);\n}\n\nexport async function deleteDocumentPlacementRowsByDocumentIds(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<void> {\n await tx\n .delete(documentContainerProjection)\n .where(inArray(documentContainerProjection.documentId, documentIds))\n .run();\n await deleteContainerDocumentTombstoneHoldsForDocuments(tx, documentIds);\n}\n\n/** Removes one placement a verified tombstone retired. */\nexport async function deleteDocumentPlacementRowInTransaction(\n tx: ClientSQLiteTransactionScope,\n placement: { readonly containerId: string; readonly documentId: string },\n): Promise<void> {\n await tx\n .delete(documentContainerProjection)\n .where(\n and(\n eq(documentContainerProjection.documentId, placement.documentId),\n eq(documentContainerProjection.containerId, placement.containerId),\n ),\n )\n .run();\n}\n\n/** Removes a document's placements outside the containers its head links. */\nexport async function deleteUnlinkedDocumentPlacementRowsInTransaction(\n tx: ClientSQLiteTransactionScope,\n documentId: string,\n linkedContainerIds: ReadonlySet<string>,\n): Promise<void> {\n await tx\n .delete(documentContainerProjection)\n .where(\n linkedContainerIds.size === 0\n ? eq(documentContainerProjection.documentId, documentId)\n : and(\n eq(documentContainerProjection.documentId, documentId),\n notInArray(documentContainerProjection.containerId, [\n ...linkedContainerIds,\n ]),\n ),\n )\n .run();\n}\n\n/** Removes a deleted document's queued placement intents and link targets. */\nexport async function deleteDocumentPlacementIntentsInTransaction(\n tx: ClientSQLiteTransactionScope,\n documentId: string,\n): Promise<void> {\n const placementIntent = and(\n inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement),\n eq(localIntents.subjectId, documentId),\n );\n await tx\n .delete(documentIntentLinkTargets)\n .where(\n inArray(\n documentIntentLinkTargets.intentId,\n tx\n .select({ id: localIntents.id })\n .from(localIntents)\n .where(placementIntent),\n ),\n )\n .run();\n await tx.delete(localIntents).where(placementIntent).run();\n}\n\nexport const sqlDocumentContainerProjectionPersistence: DocumentContainerProjectionPersistence =\n {\n async ensureSchema(execSql) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n },\n async listLinkedContainerIds(execSql, documentId) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({ containerId: documentContainerProjection.containerId })\n .from(documentContainerProjection)\n .where(eq(documentContainerProjection.documentId, documentId))\n .orderBy(asc(documentContainerProjection.containerId));\n\n return rows.map((row) => row.containerId);\n },\n async listLinkedContainerIdsByDocumentIds(execSql, documentIds) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n const uniqueDocumentIds = Array.from(new Set(documentIds));\n if (uniqueDocumentIds.length === 0) {\n return new Map();\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({\n documentId: documentContainerProjection.documentId,\n containerId: documentContainerProjection.containerId,\n })\n .from(documentContainerProjection)\n .where(\n inArray(documentContainerProjection.documentId, uniqueDocumentIds),\n )\n .orderBy(\n asc(documentContainerProjection.documentId),\n asc(documentContainerProjection.containerId),\n );\n\n const linkedContainerIdsByDocumentId = new Map<string, string[]>();\n for (const documentId of uniqueDocumentIds) {\n linkedContainerIdsByDocumentId.set(documentId, []);\n }\n\n for (const row of rows) {\n const linkedContainerIds =\n linkedContainerIdsByDocumentId.get(row.documentId) ?? [];\n linkedContainerIds.push(row.containerId);\n linkedContainerIdsByDocumentId.set(row.documentId, linkedContainerIds);\n }\n\n return linkedContainerIdsByDocumentId;\n },\n async listDocumentIdsByContainerIds(execSql, containerIds) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n const uniqueContainerIds = Array.from(new Set(containerIds));\n if (uniqueContainerIds.length === 0) {\n return [];\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .selectDistinct({ documentId: documentContainerProjection.documentId })\n .from(documentContainerProjection)\n .where(\n inArray(documentContainerProjection.containerId, uniqueContainerIds),\n )\n .orderBy(asc(documentContainerProjection.documentId));\n\n return rows.map((row) => row.documentId);\n },\n async replaceDocumentLinks(execSql, documentId, containerIds, options) {\n await sqlDocumentContainerProjectionPersistence.replaceDocumentLinksBatch(\n execSql,\n [{ documentId, containerIds }],\n options,\n );\n },\n async replaceDocumentLinksBatch(execSql, inputs, options) {\n await ensureSqlTables(execSql, [\n ...documentContainerProjectionTables,\n ...documentPlacementIntentTables,\n ...documentTables,\n ]);\n const latest = new Map(inputs.map((input) => [input.documentId, input]));\n if (latest.size === 0) return;\n const runtime = getClientSQLitePersistenceRuntime(execSql);\n const replace = async (tx: ClientSQLiteTransactionScope) => {\n const writable = await filterWritableDocumentPlacements(\n tx,\n [...latest.values()],\n options,\n );\n if (writable.length === 0) return;\n // A placement the writer re-asserts is no longer tombstoned: a\n // listing that links it again, or a local move that owns it, releases\n // the hold so the document is not hidden until the next retry.\n if (options?.moveIntentId) {\n await deleteContainerDocumentTombstoneHoldsForDocuments(\n tx,\n writable.map((input) => input.documentId),\n );\n } else {\n await deleteContainerDocumentTombstoneHoldRowsForLinks(tx, writable);\n }\n await tx\n .delete(documentContainerProjection)\n .where(\n inArray(\n documentContainerProjection.documentId,\n writable.map((input) => input.documentId),\n ),\n )\n .run();\n const updatedAt = new Date().toISOString();\n const rows = writable.flatMap((input) =>\n Array.from(new Set(input.containerIds))\n .sort()\n .map((containerId) => ({\n documentId: input.documentId,\n containerId,\n updatedAt,\n })),\n );\n if (rows.length > 0)\n await tx.insert(documentContainerProjection).values(rows).run();\n };\n if (options?.stillCurrent) {\n await runtime.guardedTransaction(replace, options.stillCurrent);\n return;\n }\n await runtime.transaction(replace);\n },\n };\n"]}
1
+ {"version":3,"file":"documentContainerProjectionPersistence.js","sourceRoot":"","sources":["documentContainerProjectionPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, eq, inArray, notInArray, or, type SQL } from \"drizzle-orm\";\nimport {\n documentContainerProjection,\n documentContainerProjectionTables,\n documentPlacementIntentTables,\n documentTables,\n inLocalIntentLane,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport { documentIntentLinkTargets } from \"../../sqlite/schema/documentPlacementIntentSchema\";\nimport {\n REMOTE_RESET_SQL_BATCH_SIZE,\n remoteResetBatches,\n SQLITE_BOUND_VARIABLE_BUDGET,\n sqliteInsertBatchSize,\n} from \"../../sqlite/sqliteBindBudget\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../sqlite/sqlSchema\";\nimport {\n deleteContainerDocumentTombstoneHoldRowsForLinks,\n deleteContainerDocumentTombstoneHoldsForContainers,\n deleteContainerDocumentTombstoneHoldsForDocuments,\n placementPairsMatch,\n} from \"../documents/containerDocumentTombstoneHoldsPersistence\";\n\nimport {\n type DocumentPlacementInput,\n type DocumentPlacementWriteOptions,\n filterWritableDocumentPlacements,\n} from \"./documentPlacement\";\n\nconst LINK_INSERT_BATCH_SIZE = sqliteInsertBatchSize(\n documentContainerProjection,\n);\n\ninterface DocumentContainerProjectionPersistence {\n ensureSchema: (execSql: ExecSql) => Promise<void>;\n listLinkedContainerIds: (\n execSql: ExecSql,\n documentId: string,\n ) => Promise<ReadonlyArray<string>>;\n listLinkedContainerIdsByDocumentIds: (\n execSql: ExecSql,\n documentIds: ReadonlyArray<string>,\n ) => Promise<ReadonlyMap<string, ReadonlyArray<string>>>;\n listDocumentIdsByContainerIds: (\n execSql: ExecSql,\n containerIds: ReadonlyArray<string>,\n ) => Promise<ReadonlyArray<string>>;\n replaceDocumentLinks: (\n execSql: ExecSql,\n documentId: string,\n containerIds: ReadonlyArray<string>,\n options?: DocumentPlacementWriteOptions,\n ) => Promise<void>;\n replaceDocumentLinksBatch: (\n execSql: ExecSql,\n inputs: ReadonlyArray<DocumentPlacementInput>,\n options?: DocumentPlacementWriteOptions,\n ) => Promise<void>;\n}\n\n/** Link rows and their tombstone holds leave together on a remote reset. */\nexport async function deleteDocumentPlacementRowsByContainerIds(\n tx: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<void> {\n await tx\n .delete(documentContainerProjection)\n .where(inArray(documentContainerProjection.containerId, containerIds))\n .run();\n await deleteContainerDocumentTombstoneHoldsForContainers(tx, containerIds);\n}\n\nexport async function deleteDocumentPlacementRowsByDocumentIds(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<void> {\n await tx\n .delete(documentContainerProjection)\n .where(inArray(documentContainerProjection.documentId, documentIds))\n .run();\n await deleteContainerDocumentTombstoneHoldsForDocuments(tx, documentIds);\n}\n\n/** Removes the listed (document, container) placements, 50 pairs per statement. */\nexport async function deleteDocumentPlacementRowsInTransaction(\n tx: ClientSQLiteTransactionScope,\n placements: ReadonlyArray<{\n readonly containerId: string;\n readonly documentId: string;\n }>,\n): Promise<void> {\n for (const batch of remoteResetBatches(placements)) {\n await tx\n .delete(documentContainerProjection)\n .where(\n placementPairsMatch(\n batch,\n documentContainerProjection.documentId,\n documentContainerProjection.containerId,\n ),\n )\n .run();\n }\n}\n\n/**\n * Removes each document's placements outside the containers its head links,\n * batching documents so one statement stays inside SQLite's bind budget.\n */\nexport async function deleteUnlinkedDocumentPlacementRowsInTransaction(\n tx: ClientSQLiteTransactionScope,\n linkedContainerIdsByDocumentId: ReadonlyMap<string, ReadonlySet<string>>,\n): Promise<void> {\n let batch: SQL[] = [];\n let binds = 0;\n const flush = async () => {\n if (batch.length === 0) return;\n await tx\n .delete(documentContainerProjection)\n .where(or(...batch))\n .run();\n batch = [];\n binds = 0;\n };\n for (const [documentId, linked] of linkedContainerIdsByDocumentId) {\n // The document id plus one bind per linked container.\n const documentBinds = 1 + linked.size;\n if (\n batch.length >= REMOTE_RESET_SQL_BATCH_SIZE ||\n binds + documentBinds > SQLITE_BOUND_VARIABLE_BUDGET\n ) {\n await flush();\n }\n batch.push(unlinkedDocumentPlacementRows(documentId, linked));\n binds += documentBinds;\n }\n await flush();\n}\n\n/** A document's placements outside the containers its head links. */\nfunction unlinkedDocumentPlacementRows(\n documentId: string,\n linkedContainerIds: ReadonlySet<string>,\n): SQL {\n const ofDocument = eq(documentContainerProjection.documentId, documentId);\n return linkedContainerIds.size === 0\n ? ofDocument\n : (and(\n ofDocument,\n notInArray(documentContainerProjection.containerId, [\n ...linkedContainerIds,\n ]),\n ) ?? ofDocument);\n}\n\n/** Removes a deleted document's queued placement intents and link targets. */\nexport async function deleteDocumentPlacementIntentsInTransaction(\n tx: ClientSQLiteTransactionScope,\n documentId: string,\n): Promise<void> {\n const placementIntent = and(\n inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement),\n eq(localIntents.subjectId, documentId),\n );\n await tx\n .delete(documentIntentLinkTargets)\n .where(\n inArray(\n documentIntentLinkTargets.intentId,\n tx\n .select({ id: localIntents.id })\n .from(localIntents)\n .where(placementIntent),\n ),\n )\n .run();\n await tx.delete(localIntents).where(placementIntent).run();\n}\n\nexport const sqlDocumentContainerProjectionPersistence: DocumentContainerProjectionPersistence =\n {\n async ensureSchema(execSql) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n },\n async listLinkedContainerIds(execSql, documentId) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({ containerId: documentContainerProjection.containerId })\n .from(documentContainerProjection)\n .where(eq(documentContainerProjection.documentId, documentId))\n .orderBy(asc(documentContainerProjection.containerId));\n\n return rows.map((row) => row.containerId);\n },\n async listLinkedContainerIdsByDocumentIds(execSql, documentIds) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n const uniqueDocumentIds = Array.from(new Set(documentIds));\n if (uniqueDocumentIds.length === 0) {\n return new Map();\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({\n documentId: documentContainerProjection.documentId,\n containerId: documentContainerProjection.containerId,\n })\n .from(documentContainerProjection)\n .where(\n inArray(documentContainerProjection.documentId, uniqueDocumentIds),\n )\n .orderBy(\n asc(documentContainerProjection.documentId),\n asc(documentContainerProjection.containerId),\n );\n\n const linkedContainerIdsByDocumentId = new Map<string, string[]>();\n for (const documentId of uniqueDocumentIds) {\n linkedContainerIdsByDocumentId.set(documentId, []);\n }\n\n for (const row of rows) {\n const linkedContainerIds =\n linkedContainerIdsByDocumentId.get(row.documentId) ?? [];\n linkedContainerIds.push(row.containerId);\n linkedContainerIdsByDocumentId.set(row.documentId, linkedContainerIds);\n }\n\n return linkedContainerIdsByDocumentId;\n },\n async listDocumentIdsByContainerIds(execSql, containerIds) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n const uniqueContainerIds = Array.from(new Set(containerIds));\n if (uniqueContainerIds.length === 0) {\n return [];\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .selectDistinct({ documentId: documentContainerProjection.documentId })\n .from(documentContainerProjection)\n .where(\n inArray(documentContainerProjection.containerId, uniqueContainerIds),\n )\n .orderBy(asc(documentContainerProjection.documentId));\n\n return rows.map((row) => row.documentId);\n },\n async replaceDocumentLinks(execSql, documentId, containerIds, options) {\n await sqlDocumentContainerProjectionPersistence.replaceDocumentLinksBatch(\n execSql,\n [{ documentId, containerIds }],\n options,\n );\n },\n async replaceDocumentLinksBatch(execSql, inputs, options) {\n await ensureSqlTables(execSql, [\n ...documentContainerProjectionTables,\n ...documentPlacementIntentTables,\n ...documentTables,\n ]);\n const latest = new Map(inputs.map((input) => [input.documentId, input]));\n if (latest.size === 0) return;\n const runtime = getClientSQLitePersistenceRuntime(execSql);\n const replace = async (tx: ClientSQLiteTransactionScope) => {\n const writable = await filterWritableDocumentPlacements(\n tx,\n [...latest.values()],\n options,\n );\n if (writable.length === 0) return;\n // A placement the writer re-asserts is no longer tombstoned: a\n // listing that links it again, or a local move that owns it, releases\n // the hold so the document is not hidden until the next retry.\n if (options?.moveIntentId) {\n await deleteContainerDocumentTombstoneHoldsForDocuments(\n tx,\n writable.map((input) => input.documentId),\n );\n } else {\n await deleteContainerDocumentTombstoneHoldRowsForLinks(tx, writable);\n }\n await tx\n .delete(documentContainerProjection)\n .where(\n inArray(\n documentContainerProjection.documentId,\n writable.map((input) => input.documentId),\n ),\n )\n .run();\n const updatedAt = new Date().toISOString();\n const rows = writable.flatMap((input) =>\n Array.from(new Set(input.containerIds))\n .sort()\n .map((containerId) => ({\n documentId: input.documentId,\n containerId,\n updatedAt,\n })),\n );\n // A lane page of multi-link documents can carry more link rows than\n // one statement may bind.\n for (\n let index = 0;\n index < rows.length;\n index += LINK_INSERT_BATCH_SIZE\n ) {\n await tx\n .insert(documentContainerProjection)\n .values(rows.slice(index, index + LINK_INSERT_BATCH_SIZE))\n .run();\n }\n };\n if (options?.stillCurrent) {\n await runtime.guardedTransaction(replace, options.stillCurrent);\n return;\n }\n await runtime.transaction(replace);\n },\n };\n"]}
@@ -1,10 +1,33 @@
1
1
  import { and, asc, eq, inArray } from "drizzle-orm";
2
2
  import { containerTables, documentPlacementIntentTables, documentProjectionTables, inLocalIntentLane, LOCAL_INTENT_LANES, localIntents, } from "../../sqlite/schema/index.js";
3
3
  import { documentIntentLinkTargets } from "../../sqlite/schema/documentPlacementIntentSchema.js";
4
- import { getClientSQLitePersistenceRuntime } from "../../sqlite/sqlitePersistenceRuntime.js";
4
+ import { sqliteRowsPerStatement } from "../../sqlite/sqliteBindBudget.js";
5
+ import { getClientSQLitePersistenceRuntime, } from "../../sqlite/sqlitePersistenceRuntime.js";
5
6
  import { ensureSqlTables, runSerializedSqlMutation, } from "../../sqlite/sqlSchema.js";
6
- import { enqueueDocumentLinkIntent, enqueueDocumentMoveIntent, enqueueDocumentUnlinkIntent, loadDocumentIntentLinkTargets, } from "./documentPlacementIntentEnqueue.js";
7
+ import { enqueueDocumentLinkIntent, enqueueDocumentMoveIntent, enqueueDocumentUnlinkIntent, } from "./documentPlacementIntentEnqueue.js";
7
8
  export const DOCUMENT_MOVE_INTENT_TYPE = "document.move";
9
+ const INTENT_IDS_PER_STATEMENT = sqliteRowsPerStatement(1);
10
+ /** Every listed intent's link targets, one keyed read per bind budget. */
11
+ async function loadLinkTargetsByIntent(tx, intentIds) {
12
+ const targetsByIntent = new Map();
13
+ const uniqueIds = [...new Set(intentIds)];
14
+ for (let index = 0; index < uniqueIds.length; index += INTENT_IDS_PER_STATEMENT) {
15
+ const rows = await tx
16
+ .select({
17
+ containerId: documentIntentLinkTargets.containerId,
18
+ intentId: documentIntentLinkTargets.intentId,
19
+ operation: documentIntentLinkTargets.operation,
20
+ })
21
+ .from(documentIntentLinkTargets)
22
+ .where(inArray(documentIntentLinkTargets.intentId, uniqueIds.slice(index, index + INTENT_IDS_PER_STATEMENT)));
23
+ for (const { intentId, ...target } of rows) {
24
+ const targets = targetsByIntent.get(intentId) ?? [];
25
+ targets.push(target);
26
+ targetsByIntent.set(intentId, targets);
27
+ }
28
+ }
29
+ return targetsByIntent;
30
+ }
8
31
  const placementLane = inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement);
9
32
  export const DOCUMENT_LINK_INTENT_TYPE = "document.link";
10
33
  const DOCUMENT_PLACEMENT_INTENT_TYPES = [
@@ -99,8 +122,9 @@ export const sqlDocumentMoveIntentPersistence = {
99
122
  // (#2278 #4). Only the tombstone cascade or a re-enqueue revives them.
100
123
  .where(and(placementLane, inArray(localIntents.syncStatus, ["pending", "blocked"]), inArray(localIntents.intentType, DOCUMENT_PLACEMENT_INTENT_TYPES)))
101
124
  .orderBy(asc(localIntents.createdAt));
102
- return Promise.all(rows.map(async (row) => {
103
- const targets = await loadDocumentIntentLinkTargets(lockedExecSql, row.id ?? "");
125
+ const targetsByIntent = await loadLinkTargetsByIntent(tx, rows.map((row) => row.id ?? ""));
126
+ return rows.map((row) => {
127
+ const targets = targetsByIntent.get(row.id ?? "") ?? [];
104
128
  const added = targets
105
129
  .filter((target) => target.operation === "link")
106
130
  .map((target) => target.containerId)
@@ -114,7 +138,7 @@ export const sqlDocumentMoveIntentPersistence = {
114
138
  ...(added.length ? { additionalLinkContainerIds: added } : {}),
115
139
  ...(removed.length ? { removedLinkContainerIds: removed } : {}),
116
140
  };
117
- }));
141
+ });
118
142
  });
119
143
  });
120
144
  },
@@ -1 +1 @@
1
- {"version":3,"file":"documentMoveIntentPersistence.js","sourceRoot":"","sources":["documentMoveIntentPersistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EACL,eAAe,EACf,6BAA6B,EAC7B,wBAAwB,EACxB,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,GACb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,yBAAyB,EAAE,MAAM,mDAAmD,CAAC;AAC9F,OAAO,EAAE,iCAAiC,EAAE,MAAM,uCAAuC,CAAC;AAC1F,OAAO,EAEL,eAAe,EACf,wBAAwB,GACzB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,yBAAyB,EACzB,yBAAyB,EACzB,2BAA2B,EAC3B,6BAA6B,GAC9B,MAAM,kCAAkC,CAAC;AAE1C,MAAM,CAAC,MAAM,yBAAyB,GAAG,eAAe,CAAC;AACzD,MAAM,aAAa,GAAG,iBAAiB,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;AAC9E,MAAM,CAAC,MAAM,yBAAyB,GAAG,eAAe,CAAC;AACzD,MAAM,+BAA+B,GAAG;IACtC,yBAAyB;IACzB,yBAAyB;CAC1B,CAAC;AAEF,2EAA2E;AAC3E,mEAAmE;AACnE,yEAAyE;AACzE,oEAAoE;AACpE,6EAA6E;AAC7E,4EAA4E;AAC5E,wDAAwD;AACxD,MAAM,8BAA8B,GAAG;;;;EAIrC,CAAC;AAEH,MAAM,oCAAoC,GAAG;;;;;;yDAMY,CAAC;AAuD1D,SAAS,iCAAiC,CACxC,KAAc;IAEd,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,aAAa,EAAE,CAAC;QACzE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,yBAAyB,CAAC,KAIlC;IACC,IAAI,KAAK,CAAC,WAAW;QAAE,OAAO,aAAa,CAAC;IAC5C,IAAI,KAAK,CAAC,MAAM;QAAE,OAAO,QAAQ,CAAC;IAClC,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/C,CAAC;AAED,SAAS,2BAA2B,CAClC,GAAqC;IAErC,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC;QACxB,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,UAAU,EACR,GAAG,CAAC,UAAU,KAAK,yBAAyB;YAC1C,CAAC,CAAC,yBAAyB;YAC3B,CAAC,CAAC,yBAAyB;QAC/B,eAAe,EAAE,GAAG,CAAC,eAAe;QACpC,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,2DAA2D;QAC3D,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE;QAC1B,uBAAuB,EAAE,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC;QAC7D,iBAAiB,EAAE,GAAG,CAAC,iBAAiB;QACxC,UAAU,EAAE,iCAAiC,CAAC,GAAG,CAAC,UAAU,CAAC;QAC7D,iBAAiB,EAAE,GAAG,CAAC,iBAAiB;QACxC,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,SAAS,EAAE,GAAG,CAAC,SAAS;KACzB,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,gCAAgC,GAAG;IAC9C,KAAK,CAAC,YAAY,CAAC,OAAgB;QACjC,MAAM,eAAe,CAAC,OAAO,EAAE,6BAA6B,CAAC,CAAC;IAChE,CAAC;IAED,iBAAiB,EAAE,yBAAyB;IAC5C,iBAAiB,EAAE,yBAAyB;IAC5C,mBAAmB,EAAE,2BAA2B;IAEhD,KAAK,CAAC,sBAAsB,CAC1B,OAAgB;QAEhB,OAAO,wBAAwB,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE;YAC/D,MAAM,eAAe,CAAC,aAAa,EAAE,6BAA6B,CAAC,CAAC;YACpE,OAAO,iCAAiC,CAAC,aAAa,CAAC,CAAC,WAAW,CACjE,KAAK,EAAE,EAAE,EAAE,EAAE;gBACX,MAAM,IAAI,GAAG,MAAM,EAAE;qBAClB,MAAM,CAAC;oBACN,EAAE,EAAE,YAAY,CAAC,EAAE;oBACnB,UAAU,EAAE,YAAY,CAAC,SAAS;oBAClC,UAAU,EAAE,YAAY,CAAC,UAAU;oBACnC,eAAe,EAAE,YAAY,CAAC,eAAe;oBAC7C,SAAS,EAAE,YAAY,CAAC,SAAS;oBACjC,OAAO,EAAE,YAAY,CAAC,OAAO;oBAC7B,uBAAuB,EAAE,YAAY,CAAC,uBAAuB;oBAC7D,iBAAiB,EAAE,YAAY,CAAC,iBAAiB;oBACjD,UAAU,EAAE,YAAY,CAAC,UAAU;oBACnC,iBAAiB,EAAE,YAAY,CAAC,iBAAiB;oBACjD,SAAS,EAAE,YAAY,CAAC,SAAS;oBACjC,SAAS,EAAE,YAAY,CAAC,SAAS;iBAClC,CAAC;qBACD,IAAI,CAAC,YAAY,CAAC;oBACnB,kEAAkE;oBAClE,qEAAqE;oBACrE,uEAAuE;oBACvE,qEAAqE;oBACrE,2DAA2D;oBAC3D,wEAAwE;oBACxE,mEAAmE;oBACnE,uEAAuE;qBACtE,KAAK,CACJ,GAAG,CACD,aAAa,EACb,OAAO,CAAC,YAAY,CAAC,UAAU,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,EACxD,OAAO,CACL,YAAY,CAAC,UAAU,EACvB,+BAA+B,CAChC,CACF,CACF;qBACA,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC;gBAExC,OAAO,OAAO,CAAC,GAAG,CAChB,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;oBACrB,MAAM,OAAO,GAAG,MAAM,6BAA6B,CACjD,aAAa,EACb,GAAG,CAAC,EAAE,IAAI,EAAE,CACb,CAAC;oBACF,MAAM,KAAK,GAAG,OAAO;yBAClB,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,KAAK,MAAM,CAAC;yBAC/C,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC;yBACnC,IAAI,EAAE,CAAC;oBACV,MAAM,OAAO,GAAG,OAAO;yBACpB,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC;yBACjD,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC;yBACnC,IAAI,EAAE,CAAC;oBACV,OAAO;wBACL,GAAG,2BAA2B,CAAC,GAAG,CAAC;wBACnC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,0BAA0B,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;wBAC9D,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,uBAAuB,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;qBAChE,CAAC;gBACJ,CAAC,CAAC,CACH,CAAC;YACJ,CAAC,CACF,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,oBAAoB,CACxB,OAAgB,EAChB,KAIC;QAED,OAAO,wBAAwB,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE;YAC/D,MAAM,OAAO,GAAG,MAAM,aAAa,CACjC;;;;;aAKK,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE;sBACjC,EACd;gBACE,kBAAkB,CAAC,iBAAiB;gBACpC,KAAK,CAAC,UAAU;gBAChB,GAAG,+BAA+B;gBAClC,KAAK,CAAC,iBAAiB;gBACvB,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5D,CACF,CAAC;YACF,KAAK,MAAM,EAAE,EAAE,EAAE,IAAI,OAAO,EAAE,CAAC;gBAC7B,MAAM,iCAAiC,CAAC,aAAa,CAAC;qBACnD,EAAE,CAAC,MAAM,CAAC,yBAAyB,CAAC;qBACpC,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;qBACzD,GAAG,EAAE,CAAC;YACX,CAAC;YACD,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,qBAAqB,CACzB,OAAgB,EAChB,KA2BC;QAED,MAAM,iCAAiC,CAAC,OAAO,CAAC,CAAC,kBAAkB,CACjE,KAAK,EAAE,EAAE,EAAE,EAAE;YACX,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YAC3C,MAAM,EAAE;iBACL,MAAM,CAAC,YAAY,CAAC;iBACpB,GAAG,CAAC;gBACH,eAAe,EAAE,SAAS;gBAC1B,SAAS,EAAE,KAAK,CAAC,OAAO;gBACxB,UAAU,EAAE,yBAAyB,CAAC,KAAK,CAAC;gBAC5C,SAAS;aACV,CAAC;iBACD,KAAK,CACJ,GAAG,CACD,aAAa,EACb,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,CAAC;YAC5C,4DAA4D;YAC5D,8DAA8D;YAC9D,kDAAkD;YAClD,OAAO,CAAC,YAAY,CAAC,UAAU,EAAE;gBAC/B,SAAS;gBACT,SAAS;gBACT,QAAQ;aACT,CAAC,EACF,OAAO,CAAC,YAAY,CAAC,UAAU,EAAE,+BAA+B,CAAC,EACjE,GAAG,CAAC,KAAK,CAAC,gBAAgB;gBACxB,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;gBAC/C,CAAC,CAAC,EAAE,CAAC,EACP,GAAG,CAAC,KAAK,CAAC,iBAAiB;gBACzB,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAC;gBACvD,CAAC,CAAC,EAAE,CAAC,CACR,CACF;iBACA,GAAG,EAAE,CAAC;QACX,CAAC,EACD,KAAK,CAAC,YAAY,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,EAClC,EAAE,QAAQ,EAAE,WAAW,EAAE,CAC1B,CAAC;IACJ,CAAC;IACD;;;OAGG;IACH,KAAK,CAAC,oBAAoB,CACxB,OAAgB,EAChB,KAAmC;QAEnC,MAAM,gCAAgC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QAC7D,MAAM,eAAe,CAAC,OAAO,EAAE;YAC7B,GAAG,wBAAwB;YAC3B,GAAG,eAAe;SACnB,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CACxB;;SAEG,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC,EAAE;;;;WAI/D,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,QAAQ,8BAA8B,WAAW,8BAA8B,WAAW,CAAC,CAAC,CAAC,EAAE;eACnH,EACT;YACE,kBAAkB,CAAC,iBAAiB;YACpC,GAAG,+BAA+B;YAClC,GAAG,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;SACzD,CACF,CAAC;QACF,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IACzB,CAAC;IACD;;;;OAIG;IACH,KAAK,CAAC,sBAAsB,CAC1B,OAAgB,EAChB,KAAqD;QAErD,MAAM,gCAAgC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QAC7D,MAAM,eAAe,CAAC,OAAO,EAAE;YAC7B,GAAG,wBAAwB;YAC3B,GAAG,eAAe;SACnB,CAAC,CAAC;QACH,MAAM,wBAAwB,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE;YAC9D,uEAAuE;YACvE,sEAAsE;YACtE,qEAAqE;YACrE,8DAA8D;YAC9D,MAAM,aAAa,CACjB;;;;;aAKK,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE;aAExC,KAAK,EAAE,cAAc;gBACnB,CAAC,CAAC;;sBAEK,oCAAoC;;6BAE7B,8BAA8B;6BAC9B,8BAA8B;oBACvC;gBACL,CAAC,CAAC,EACN,EAAE,EACL;gBACE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACxB,kBAAkB,CAAC,iBAAiB;gBACpC,GAAG,+BAA+B;gBAClC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1C,GAAG,CAAC,KAAK,EAAE,cAAc;oBACvB,CAAC,CAAC,CAAC,kBAAkB,CAAC,iBAAiB,EAAE,KAAK,CAAC,cAAc,CAAC;oBAC9D,CAAC,CAAC,EAAE,CAAC;aACR,CACF,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;CACF,CAAC","sourcesContent":["import { and, asc, eq, inArray } from \"drizzle-orm\";\nimport {\n containerTables,\n documentPlacementIntentTables,\n documentProjectionTables,\n inLocalIntentLane,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport { documentIntentLinkTargets } from \"../../sqlite/schema/documentPlacementIntentSchema\";\nimport { getClientSQLitePersistenceRuntime } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport {\n type ExecSql,\n ensureSqlTables,\n runSerializedSqlMutation,\n} from \"../../sqlite/sqlSchema\";\nimport {\n enqueueDocumentLinkIntent,\n enqueueDocumentMoveIntent,\n enqueueDocumentUnlinkIntent,\n loadDocumentIntentLinkTargets,\n} from \"./documentPlacementIntentEnqueue\";\n\nexport const DOCUMENT_MOVE_INTENT_TYPE = \"document.move\";\nconst placementLane = inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement);\nexport const DOCUMENT_LINK_INTENT_TYPE = \"document.link\";\nconst DOCUMENT_PLACEMENT_INTENT_TYPES = [\n DOCUMENT_MOVE_INTENT_TYPE,\n DOCUMENT_LINK_INTENT_TYPE,\n];\n\n// Resolves the organization a parked move belongs to, in confidence order:\n// the move's target container, the document's preserved projection\n// attribution, then the container the document currently sits in. Normal\n// saves routinely leave projection attribution empty (deferred-tail\n// reporting resolves through the same container join), so the container rows\n// carry the common case; rows with no resolvable organization stay in every\n// scope (device-first shapes, matching orphan priming).\nconst DENIED_INTENT_ORGANIZATION_SQL = `COALESCE(\n NULLIF(target_container.organization_id, ''),\n NULLIF(projection.organization_id, ''),\n NULLIF(projection_container.organization_id, '')\n)`;\n\nconst DENIED_INTENT_ORGANIZATION_JOINS_SQL = `\n LEFT JOIN containers target_container\n ON target_container.id = intent.target_container_id\n LEFT JOIN document_projection projection\n ON projection.local_id = intent.local_id\n LEFT JOIN containers projection_container\n ON projection_container.id = projection.container_id`;\n\n/**\n * - `pending`: replays on every structural pass.\n * - `blocked`: the last pass could not proceed locally (missing local\n * document / destination); replays, since hydration can heal it.\n * - `denied`: parked on a 403 until the access-restored replay (row 7).\n * - `unavailable`: terminal — after a projection refresh the destination\n * itself is proven deleted (coded 404 on its own projection probe). No\n * mutation 409 is ever terminal: with the destination live it can only\n * name an ancestor or source, and the next pass's fresh paths outrun it.\n * Container ids never return, so no replay can commit the intent as\n * written; it leaves the replay set until the local container tombstone\n * cascade retargets it or the user re-enqueues the move.\n */\nexport type DocumentMoveIntentSyncStatus =\n | \"pending\"\n | \"blocked\"\n | \"denied\"\n | \"unavailable\";\n\nexport interface DocumentMoveIntentRecord {\n additionalLinkContainerIds?: readonly string[] | undefined;\n removedLinkContainerIds?: readonly string[] | undefined;\n id: string;\n documentId: string;\n intentType:\n | typeof DOCUMENT_MOVE_INTENT_TYPE\n | typeof DOCUMENT_LINK_INTENT_TYPE;\n lastAttemptedAt: string | null;\n lastError: string | null;\n localId: string;\n replaceLinkedContainers: boolean;\n sourceContainerId: string | null;\n syncStatus: DocumentMoveIntentSyncStatus;\n targetContainerId: string;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface SelectedDocumentMoveIntentRecord {\n id: string | null;\n documentId: string;\n intentType: string;\n lastAttemptedAt: string | null;\n lastError: string | null;\n localId: string | null;\n replaceLinkedContainers: boolean;\n sourceContainerId: string | null;\n syncStatus: string;\n targetContainerId: string;\n createdAt: string;\n updatedAt: string;\n}\n\nfunction parseDocumentMoveIntentSyncStatus(\n value: unknown,\n): DocumentMoveIntentSyncStatus {\n if (value === \"blocked\" || value === \"denied\" || value === \"unavailable\") {\n return value;\n }\n return \"pending\";\n}\n\nfunction resolveRecordedSyncStatus(input: {\n blocked?: boolean | undefined;\n denied?: boolean | undefined;\n unavailable?: boolean | undefined;\n}): DocumentMoveIntentSyncStatus {\n if (input.unavailable) return \"unavailable\";\n if (input.denied) return \"denied\";\n return input.blocked ? \"blocked\" : \"pending\";\n}\n\nfunction mapDocumentMoveIntentRecord(\n row: SelectedDocumentMoveIntentRecord,\n): DocumentMoveIntentRecord {\n return {\n id: String(row.id ?? \"\"),\n documentId: row.documentId,\n intentType:\n row.intentType === DOCUMENT_LINK_INTENT_TYPE\n ? DOCUMENT_LINK_INTENT_TYPE\n : DOCUMENT_MOVE_INTENT_TYPE,\n lastAttemptedAt: row.lastAttemptedAt,\n lastError: row.lastError,\n // Placement intents always carry their local document row.\n localId: row.localId ?? \"\",\n replaceLinkedContainers: Boolean(row.replaceLinkedContainers),\n sourceContainerId: row.sourceContainerId,\n syncStatus: parseDocumentMoveIntentSyncStatus(row.syncStatus),\n targetContainerId: row.targetContainerId,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport const sqlDocumentMoveIntentPersistence = {\n async ensureSchema(execSql: ExecSql): Promise<void> {\n await ensureSqlTables(execSql, documentPlacementIntentTables);\n },\n\n enqueueMoveIntent: enqueueDocumentMoveIntent,\n enqueueLinkIntent: enqueueDocumentLinkIntent,\n enqueueUnlinkIntent: enqueueDocumentUnlinkIntent,\n\n async listPendingMoveIntents(\n execSql: ExecSql,\n ): Promise<DocumentMoveIntentRecord[]> {\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n await ensureSqlTables(lockedExecSql, documentPlacementIntentTables);\n return getClientSQLitePersistenceRuntime(lockedExecSql).transaction(\n async (tx) => {\n const rows = await tx\n .select({\n id: localIntents.id,\n documentId: localIntents.subjectId,\n intentType: localIntents.intentType,\n lastAttemptedAt: localIntents.lastAttemptedAt,\n lastError: localIntents.lastError,\n localId: localIntents.localId,\n replaceLinkedContainers: localIntents.replaceLinkedContainers,\n sourceContainerId: localIntents.sourceContainerId,\n syncStatus: localIntents.syncStatus,\n targetContainerId: localIntents.targetContainerId,\n createdAt: localIntents.createdAt,\n updatedAt: localIntents.updatedAt,\n })\n .from(localIntents)\n // Blocked intents replay too: \"blocked\" names the reason the last\n // attempt could not proceed (missing local doc / destination), not a\n // terminal verdict. The blocking condition can heal after hydration or\n // recovery, and re-checking is cheap — a still-blocked intent simply\n // re-records its reason without counting as lane progress.\n // Unavailable intents never replay: the server proved a cited container\n // is gone, so every replay would re-issue the same doomed requests\n // (#2278 #4). Only the tombstone cascade or a re-enqueue revives them.\n .where(\n and(\n placementLane,\n inArray(localIntents.syncStatus, [\"pending\", \"blocked\"]),\n inArray(\n localIntents.intentType,\n DOCUMENT_PLACEMENT_INTENT_TYPES,\n ),\n ),\n )\n .orderBy(asc(localIntents.createdAt));\n\n return Promise.all(\n rows.map(async (row) => {\n const targets = await loadDocumentIntentLinkTargets(\n lockedExecSql,\n row.id ?? \"\",\n );\n const added = targets\n .filter((target) => target.operation === \"link\")\n .map((target) => target.containerId)\n .sort();\n const removed = targets\n .filter((target) => target.operation === \"unlink\")\n .map((target) => target.containerId)\n .sort();\n return {\n ...mapDocumentMoveIntentRecord(row),\n ...(added.length ? { additionalLinkContainerIds: added } : {}),\n ...(removed.length ? { removedLinkContainerIds: removed } : {}),\n };\n }),\n );\n },\n );\n });\n },\n\n async markMoveIntentSynced(\n execSql: ExecSql,\n input: {\n documentId: string;\n expectedIntentId?: string | undefined;\n expectedUpdatedAt: string;\n },\n ): Promise<boolean> {\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n const deleted = await lockedExecSql(\n `DELETE FROM local_intents\n WHERE lane = ?\n AND subject_id = ?\n AND intent_type IN (?, ?)\n AND updated_at = ?\n ${input.expectedIntentId ? \"AND id = ?\" : \"\"}\n RETURNING id`,\n [\n LOCAL_INTENT_LANES.documentPlacement,\n input.documentId,\n ...DOCUMENT_PLACEMENT_INTENT_TYPES,\n input.expectedUpdatedAt,\n ...(input.expectedIntentId ? [input.expectedIntentId] : []),\n ],\n );\n for (const { id } of deleted) {\n await getClientSQLitePersistenceRuntime(lockedExecSql)\n .db.delete(documentIntentLinkTargets)\n .where(eq(documentIntentLinkTargets.intentId, String(id)))\n .run();\n }\n return deleted.length > 0;\n });\n },\n\n async recordMoveIntentError(\n execSql: ExecSql,\n input: {\n blocked?: boolean | undefined;\n /**\n * A permission denial (403): the intent parks as `denied` — excluded\n * from routine structural replays — until the org-access-restored\n * signal or a manual retry flips it back to pending (edge-case row 7).\n */\n denied?: boolean | undefined;\n documentId: string;\n /** Exact intent revision read by the pass; preferred over wall-clock CAS. */\n expectedIntentId?: string | undefined;\n /**\n * Optimistic concurrency: when given, the error only records against\n * the intent revision the pass read — a re-enqueued move (new\n * updatedAt) is never parked by a stale in-flight failure.\n */\n expectedUpdatedAt?: string | undefined;\n message: string;\n stillCurrent?: (() => boolean) | undefined;\n /**\n * Server proof that a cited container was deleted (coded 404 or\n * `container_unavailable` 409): the intent parks terminally as\n * `unavailable`, outside every replay, until the tombstone cascade\n * retargets it or a re-enqueue replaces it. Outranks `denied`: a\n * restored permission cannot revive a deleted container.\n */\n unavailable?: boolean | undefined;\n },\n ): Promise<void> {\n await getClientSQLitePersistenceRuntime(execSql).guardedTransaction(\n async (db) => {\n const updatedAt = new Date().toISOString();\n await db\n .update(localIntents)\n .set({\n lastAttemptedAt: updatedAt,\n lastError: input.message,\n syncStatus: resolveRecordedSyncStatus(input),\n updatedAt,\n })\n .where(\n and(\n placementLane,\n eq(localIntents.subjectId, input.documentId),\n // Blocked/denied rows must stay updatable: a retried intent\n // records its fresh outcome, and a transient failure flips it\n // back to pending instead of freezing it forever.\n inArray(localIntents.syncStatus, [\n \"pending\",\n \"blocked\",\n \"denied\",\n ]),\n inArray(localIntents.intentType, DOCUMENT_PLACEMENT_INTENT_TYPES),\n ...(input.expectedIntentId\n ? [eq(localIntents.id, input.expectedIntentId)]\n : []),\n ...(input.expectedUpdatedAt\n ? [eq(localIntents.updatedAt, input.expectedUpdatedAt)]\n : []),\n ),\n )\n .run();\n },\n input.stillCurrent ?? (() => true),\n { behavior: \"immediate\" },\n );\n },\n /**\n * Evidence for the org-access-restored re-arm gate: a parked\n * permission-denied move proves a queued write was refused.\n */\n async hasDeniedMoveIntents(\n execSql: ExecSql,\n input?: { organizationId?: string },\n ): Promise<boolean> {\n await sqlDocumentMoveIntentPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, [\n ...documentProjectionTables,\n ...containerTables,\n ]);\n const rows = await execSql(\n `SELECT intent.subject_id AS document_id\n FROM local_intents intent\n ${input?.organizationId ? DENIED_INTENT_ORGANIZATION_JOINS_SQL : \"\"}\n WHERE intent.lane = ?\n AND intent.sync_status = 'denied'\n AND intent.intent_type IN (?, ?)\n ${input?.organizationId ? `AND (${DENIED_INTENT_ORGANIZATION_SQL} = ? OR ${DENIED_INTENT_ORGANIZATION_SQL} IS NULL)` : \"\"}\n LIMIT 1`,\n [\n LOCAL_INTENT_LANES.documentPlacement,\n ...DOCUMENT_PLACEMENT_INTENT_TYPES,\n ...(input?.organizationId ? [input.organizationId] : []),\n ],\n );\n return rows.length > 0;\n },\n /**\n * Flip parked permission-denied moves back to pending so the re-armed\n * structural pass replays them (access restored, or a manual retry scoped\n * to one document).\n */\n async resetDeniedMoveIntents(\n execSql: ExecSql,\n input?: { localId?: string; organizationId?: string },\n ): Promise<void> {\n await sqlDocumentMoveIntentPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, [\n ...documentProjectionTables,\n ...containerTables,\n ]);\n await runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n // Organization scoping resolves each intent's organization through the\n // container joins above: restoring one organization's access must not\n // un-park moves that another organization still denies. Rows with no\n // resolvable organization are included (device-first shapes).\n await lockedExecSql(\n `UPDATE local_intents\n SET sync_status = 'pending', updated_at = ?\n WHERE lane = ?\n AND sync_status = 'denied'\n AND intent_type IN (?, ?)\n ${input?.localId ? \"AND local_id = ?\" : \"\"}\n ${\n input?.organizationId\n ? `AND local_id IN (\n SELECT intent.local_id FROM local_intents intent\n ${DENIED_INTENT_ORGANIZATION_JOINS_SQL}\n WHERE intent.lane = ?\n AND (${DENIED_INTENT_ORGANIZATION_SQL} = ?\n OR ${DENIED_INTENT_ORGANIZATION_SQL} IS NULL)\n )`\n : \"\"\n }`,\n [\n new Date().toISOString(),\n LOCAL_INTENT_LANES.documentPlacement,\n ...DOCUMENT_PLACEMENT_INTENT_TYPES,\n ...(input?.localId ? [input.localId] : []),\n ...(input?.organizationId\n ? [LOCAL_INTENT_LANES.documentPlacement, input.organizationId]\n : []),\n ],\n );\n });\n },\n};\n"]}
1
+ {"version":3,"file":"documentMoveIntentPersistence.js","sourceRoot":"","sources":["documentMoveIntentPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, eq, inArray } from \"drizzle-orm\";\nimport {\n containerTables,\n documentPlacementIntentTables,\n documentProjectionTables,\n inLocalIntentLane,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport { documentIntentLinkTargets } from \"../../sqlite/schema/documentPlacementIntentSchema\";\nimport { sqliteRowsPerStatement } from \"../../sqlite/sqliteBindBudget\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../sqlite/sqlitePersistenceRuntime\";\nimport {\n type ExecSql,\n ensureSqlTables,\n runSerializedSqlMutation,\n} from \"../../sqlite/sqlSchema\";\nimport {\n enqueueDocumentLinkIntent,\n enqueueDocumentMoveIntent,\n enqueueDocumentUnlinkIntent,\n} from \"./documentPlacementIntentEnqueue\";\n\ntype DocumentIntentLinkTarget = {\n containerId: string;\n operation: \"link\" | \"unlink\";\n};\n\nexport const DOCUMENT_MOVE_INTENT_TYPE = \"document.move\";\nconst INTENT_IDS_PER_STATEMENT = sqliteRowsPerStatement(1);\n\n/** Every listed intent's link targets, one keyed read per bind budget. */\nasync function loadLinkTargetsByIntent(\n tx: ClientSQLiteTransactionScope,\n intentIds: readonly string[],\n): Promise<ReadonlyMap<string, readonly DocumentIntentLinkTarget[]>> {\n const targetsByIntent = new Map<string, DocumentIntentLinkTarget[]>();\n const uniqueIds = [...new Set(intentIds)];\n for (\n let index = 0;\n index < uniqueIds.length;\n index += INTENT_IDS_PER_STATEMENT\n ) {\n const rows = await tx\n .select({\n containerId: documentIntentLinkTargets.containerId,\n intentId: documentIntentLinkTargets.intentId,\n operation: documentIntentLinkTargets.operation,\n })\n .from(documentIntentLinkTargets)\n .where(\n inArray(\n documentIntentLinkTargets.intentId,\n uniqueIds.slice(index, index + INTENT_IDS_PER_STATEMENT),\n ),\n );\n for (const { intentId, ...target } of rows) {\n const targets = targetsByIntent.get(intentId) ?? [];\n targets.push(target);\n targetsByIntent.set(intentId, targets);\n }\n }\n return targetsByIntent;\n}\nconst placementLane = inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement);\nexport const DOCUMENT_LINK_INTENT_TYPE = \"document.link\";\nconst DOCUMENT_PLACEMENT_INTENT_TYPES = [\n DOCUMENT_MOVE_INTENT_TYPE,\n DOCUMENT_LINK_INTENT_TYPE,\n];\n\n// Resolves the organization a parked move belongs to, in confidence order:\n// the move's target container, the document's preserved projection\n// attribution, then the container the document currently sits in. Normal\n// saves routinely leave projection attribution empty (deferred-tail\n// reporting resolves through the same container join), so the container rows\n// carry the common case; rows with no resolvable organization stay in every\n// scope (device-first shapes, matching orphan priming).\nconst DENIED_INTENT_ORGANIZATION_SQL = `COALESCE(\n NULLIF(target_container.organization_id, ''),\n NULLIF(projection.organization_id, ''),\n NULLIF(projection_container.organization_id, '')\n)`;\n\nconst DENIED_INTENT_ORGANIZATION_JOINS_SQL = `\n LEFT JOIN containers target_container\n ON target_container.id = intent.target_container_id\n LEFT JOIN document_projection projection\n ON projection.local_id = intent.local_id\n LEFT JOIN containers projection_container\n ON projection_container.id = projection.container_id`;\n\n/**\n * - `pending`: replays on every structural pass.\n * - `blocked`: the last pass could not proceed locally (missing local\n * document / destination); replays, since hydration can heal it.\n * - `denied`: parked on a 403 until the access-restored replay (row 7).\n * - `unavailable`: terminal — after a projection refresh the destination\n * itself is proven deleted (coded 404 on its own projection probe). No\n * mutation 409 is ever terminal: with the destination live it can only\n * name an ancestor or source, and the next pass's fresh paths outrun it.\n * Container ids never return, so no replay can commit the intent as\n * written; it leaves the replay set until the local container tombstone\n * cascade retargets it or the user re-enqueues the move.\n */\nexport type DocumentMoveIntentSyncStatus =\n | \"pending\"\n | \"blocked\"\n | \"denied\"\n | \"unavailable\";\n\nexport interface DocumentMoveIntentRecord {\n additionalLinkContainerIds?: readonly string[] | undefined;\n removedLinkContainerIds?: readonly string[] | undefined;\n id: string;\n documentId: string;\n intentType:\n | typeof DOCUMENT_MOVE_INTENT_TYPE\n | typeof DOCUMENT_LINK_INTENT_TYPE;\n lastAttemptedAt: string | null;\n lastError: string | null;\n localId: string;\n replaceLinkedContainers: boolean;\n sourceContainerId: string | null;\n syncStatus: DocumentMoveIntentSyncStatus;\n targetContainerId: string;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface SelectedDocumentMoveIntentRecord {\n id: string | null;\n documentId: string;\n intentType: string;\n lastAttemptedAt: string | null;\n lastError: string | null;\n localId: string | null;\n replaceLinkedContainers: boolean;\n sourceContainerId: string | null;\n syncStatus: string;\n targetContainerId: string;\n createdAt: string;\n updatedAt: string;\n}\n\nfunction parseDocumentMoveIntentSyncStatus(\n value: unknown,\n): DocumentMoveIntentSyncStatus {\n if (value === \"blocked\" || value === \"denied\" || value === \"unavailable\") {\n return value;\n }\n return \"pending\";\n}\n\nfunction resolveRecordedSyncStatus(input: {\n blocked?: boolean | undefined;\n denied?: boolean | undefined;\n unavailable?: boolean | undefined;\n}): DocumentMoveIntentSyncStatus {\n if (input.unavailable) return \"unavailable\";\n if (input.denied) return \"denied\";\n return input.blocked ? \"blocked\" : \"pending\";\n}\n\nfunction mapDocumentMoveIntentRecord(\n row: SelectedDocumentMoveIntentRecord,\n): DocumentMoveIntentRecord {\n return {\n id: String(row.id ?? \"\"),\n documentId: row.documentId,\n intentType:\n row.intentType === DOCUMENT_LINK_INTENT_TYPE\n ? DOCUMENT_LINK_INTENT_TYPE\n : DOCUMENT_MOVE_INTENT_TYPE,\n lastAttemptedAt: row.lastAttemptedAt,\n lastError: row.lastError,\n // Placement intents always carry their local document row.\n localId: row.localId ?? \"\",\n replaceLinkedContainers: Boolean(row.replaceLinkedContainers),\n sourceContainerId: row.sourceContainerId,\n syncStatus: parseDocumentMoveIntentSyncStatus(row.syncStatus),\n targetContainerId: row.targetContainerId,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport const sqlDocumentMoveIntentPersistence = {\n async ensureSchema(execSql: ExecSql): Promise<void> {\n await ensureSqlTables(execSql, documentPlacementIntentTables);\n },\n\n enqueueMoveIntent: enqueueDocumentMoveIntent,\n enqueueLinkIntent: enqueueDocumentLinkIntent,\n enqueueUnlinkIntent: enqueueDocumentUnlinkIntent,\n\n async listPendingMoveIntents(\n execSql: ExecSql,\n ): Promise<DocumentMoveIntentRecord[]> {\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n await ensureSqlTables(lockedExecSql, documentPlacementIntentTables);\n return getClientSQLitePersistenceRuntime(lockedExecSql).transaction(\n async (tx) => {\n const rows = await tx\n .select({\n id: localIntents.id,\n documentId: localIntents.subjectId,\n intentType: localIntents.intentType,\n lastAttemptedAt: localIntents.lastAttemptedAt,\n lastError: localIntents.lastError,\n localId: localIntents.localId,\n replaceLinkedContainers: localIntents.replaceLinkedContainers,\n sourceContainerId: localIntents.sourceContainerId,\n syncStatus: localIntents.syncStatus,\n targetContainerId: localIntents.targetContainerId,\n createdAt: localIntents.createdAt,\n updatedAt: localIntents.updatedAt,\n })\n .from(localIntents)\n // Blocked intents replay too: \"blocked\" names the reason the last\n // attempt could not proceed (missing local doc / destination), not a\n // terminal verdict. The blocking condition can heal after hydration or\n // recovery, and re-checking is cheap — a still-blocked intent simply\n // re-records its reason without counting as lane progress.\n // Unavailable intents never replay: the server proved a cited container\n // is gone, so every replay would re-issue the same doomed requests\n // (#2278 #4). Only the tombstone cascade or a re-enqueue revives them.\n .where(\n and(\n placementLane,\n inArray(localIntents.syncStatus, [\"pending\", \"blocked\"]),\n inArray(\n localIntents.intentType,\n DOCUMENT_PLACEMENT_INTENT_TYPES,\n ),\n ),\n )\n .orderBy(asc(localIntents.createdAt));\n\n const targetsByIntent = await loadLinkTargetsByIntent(\n tx,\n rows.map((row) => row.id ?? \"\"),\n );\n return rows.map((row) => {\n const targets = targetsByIntent.get(row.id ?? \"\") ?? [];\n const added = targets\n .filter((target) => target.operation === \"link\")\n .map((target) => target.containerId)\n .sort();\n const removed = targets\n .filter((target) => target.operation === \"unlink\")\n .map((target) => target.containerId)\n .sort();\n return {\n ...mapDocumentMoveIntentRecord(row),\n ...(added.length ? { additionalLinkContainerIds: added } : {}),\n ...(removed.length ? { removedLinkContainerIds: removed } : {}),\n };\n });\n },\n );\n });\n },\n\n async markMoveIntentSynced(\n execSql: ExecSql,\n input: {\n documentId: string;\n expectedIntentId?: string | undefined;\n expectedUpdatedAt: string;\n },\n ): Promise<boolean> {\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n const deleted = await lockedExecSql(\n `DELETE FROM local_intents\n WHERE lane = ?\n AND subject_id = ?\n AND intent_type IN (?, ?)\n AND updated_at = ?\n ${input.expectedIntentId ? \"AND id = ?\" : \"\"}\n RETURNING id`,\n [\n LOCAL_INTENT_LANES.documentPlacement,\n input.documentId,\n ...DOCUMENT_PLACEMENT_INTENT_TYPES,\n input.expectedUpdatedAt,\n ...(input.expectedIntentId ? [input.expectedIntentId] : []),\n ],\n );\n for (const { id } of deleted) {\n await getClientSQLitePersistenceRuntime(lockedExecSql)\n .db.delete(documentIntentLinkTargets)\n .where(eq(documentIntentLinkTargets.intentId, String(id)))\n .run();\n }\n return deleted.length > 0;\n });\n },\n\n async recordMoveIntentError(\n execSql: ExecSql,\n input: {\n blocked?: boolean | undefined;\n /**\n * A permission denial (403): the intent parks as `denied` — excluded\n * from routine structural replays — until the org-access-restored\n * signal or a manual retry flips it back to pending (edge-case row 7).\n */\n denied?: boolean | undefined;\n documentId: string;\n /** Exact intent revision read by the pass; preferred over wall-clock CAS. */\n expectedIntentId?: string | undefined;\n /**\n * Optimistic concurrency: when given, the error only records against\n * the intent revision the pass read — a re-enqueued move (new\n * updatedAt) is never parked by a stale in-flight failure.\n */\n expectedUpdatedAt?: string | undefined;\n message: string;\n stillCurrent?: (() => boolean) | undefined;\n /**\n * Server proof that a cited container was deleted (coded 404 or\n * `container_unavailable` 409): the intent parks terminally as\n * `unavailable`, outside every replay, until the tombstone cascade\n * retargets it or a re-enqueue replaces it. Outranks `denied`: a\n * restored permission cannot revive a deleted container.\n */\n unavailable?: boolean | undefined;\n },\n ): Promise<void> {\n await getClientSQLitePersistenceRuntime(execSql).guardedTransaction(\n async (db) => {\n const updatedAt = new Date().toISOString();\n await db\n .update(localIntents)\n .set({\n lastAttemptedAt: updatedAt,\n lastError: input.message,\n syncStatus: resolveRecordedSyncStatus(input),\n updatedAt,\n })\n .where(\n and(\n placementLane,\n eq(localIntents.subjectId, input.documentId),\n // Blocked/denied rows must stay updatable: a retried intent\n // records its fresh outcome, and a transient failure flips it\n // back to pending instead of freezing it forever.\n inArray(localIntents.syncStatus, [\n \"pending\",\n \"blocked\",\n \"denied\",\n ]),\n inArray(localIntents.intentType, DOCUMENT_PLACEMENT_INTENT_TYPES),\n ...(input.expectedIntentId\n ? [eq(localIntents.id, input.expectedIntentId)]\n : []),\n ...(input.expectedUpdatedAt\n ? [eq(localIntents.updatedAt, input.expectedUpdatedAt)]\n : []),\n ),\n )\n .run();\n },\n input.stillCurrent ?? (() => true),\n { behavior: \"immediate\" },\n );\n },\n /**\n * Evidence for the org-access-restored re-arm gate: a parked\n * permission-denied move proves a queued write was refused.\n */\n async hasDeniedMoveIntents(\n execSql: ExecSql,\n input?: { organizationId?: string },\n ): Promise<boolean> {\n await sqlDocumentMoveIntentPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, [\n ...documentProjectionTables,\n ...containerTables,\n ]);\n const rows = await execSql(\n `SELECT intent.subject_id AS document_id\n FROM local_intents intent\n ${input?.organizationId ? DENIED_INTENT_ORGANIZATION_JOINS_SQL : \"\"}\n WHERE intent.lane = ?\n AND intent.sync_status = 'denied'\n AND intent.intent_type IN (?, ?)\n ${input?.organizationId ? `AND (${DENIED_INTENT_ORGANIZATION_SQL} = ? OR ${DENIED_INTENT_ORGANIZATION_SQL} IS NULL)` : \"\"}\n LIMIT 1`,\n [\n LOCAL_INTENT_LANES.documentPlacement,\n ...DOCUMENT_PLACEMENT_INTENT_TYPES,\n ...(input?.organizationId ? [input.organizationId] : []),\n ],\n );\n return rows.length > 0;\n },\n /**\n * Flip parked permission-denied moves back to pending so the re-armed\n * structural pass replays them (access restored, or a manual retry scoped\n * to one document).\n */\n async resetDeniedMoveIntents(\n execSql: ExecSql,\n input?: { localId?: string; organizationId?: string },\n ): Promise<void> {\n await sqlDocumentMoveIntentPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, [\n ...documentProjectionTables,\n ...containerTables,\n ]);\n await runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n // Organization scoping resolves each intent's organization through the\n // container joins above: restoring one organization's access must not\n // un-park moves that another organization still denies. Rows with no\n // resolvable organization are included (device-first shapes).\n await lockedExecSql(\n `UPDATE local_intents\n SET sync_status = 'pending', updated_at = ?\n WHERE lane = ?\n AND sync_status = 'denied'\n AND intent_type IN (?, ?)\n ${input?.localId ? \"AND local_id = ?\" : \"\"}\n ${\n input?.organizationId\n ? `AND local_id IN (\n SELECT intent.local_id FROM local_intents intent\n ${DENIED_INTENT_ORGANIZATION_JOINS_SQL}\n WHERE intent.lane = ?\n AND (${DENIED_INTENT_ORGANIZATION_SQL} = ?\n OR ${DENIED_INTENT_ORGANIZATION_SQL} IS NULL)\n )`\n : \"\"\n }`,\n [\n new Date().toISOString(),\n LOCAL_INTENT_LANES.documentPlacement,\n ...DOCUMENT_PLACEMENT_INTENT_TYPES,\n ...(input?.localId ? [input.localId] : []),\n ...(input?.organizationId\n ? [LOCAL_INTENT_LANES.documentPlacement, input.organizationId]\n : []),\n ],\n );\n });\n },\n};\n"]}
@@ -1,18 +1,37 @@
1
1
  import { and, eq, inArray } from "drizzle-orm";
2
2
  import { DOCUMENTS_APP_KIND } from "../../documents/documentConstants.js";
3
3
  import { documents, inLocalIntentLane, LOCAL_INTENT_LANES, localIntents, } from "../../sqlite/schema/index.js";
4
+ import { sqliteRowsPerStatement } from "../../sqlite/sqliteBindBudget.js";
5
+ const movePlacementSelection = {
6
+ id: localIntents.id,
7
+ intentType: localIntents.intentType,
8
+ targetContainerId: localIntents.targetContainerId,
9
+ };
4
10
  export async function loadDocumentMovePlacement(tx, documentId) {
5
11
  const [intent] = await tx
6
- .select({
7
- id: localIntents.id,
8
- intentType: localIntents.intentType,
9
- targetContainerId: localIntents.targetContainerId,
10
- })
12
+ .select(movePlacementSelection)
11
13
  .from(localIntents)
12
14
  .where(and(inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement), eq(localIntents.subjectId, documentId)))
13
15
  .limit(1);
14
16
  return intent;
15
17
  }
18
+ /** `loadDocumentMovePlacement` for many documents, keyed by document id. */
19
+ export async function loadDocumentMovePlacements(tx, documentIds) {
20
+ const placements = new Map();
21
+ const unique = Array.from(new Set(documentIds));
22
+ // The lane predicate binds one value; the rest carry document ids.
23
+ const chunkSize = sqliteRowsPerStatement(1, 1);
24
+ for (let offset = 0; offset < unique.length; offset += chunkSize) {
25
+ const rows = await tx
26
+ .select({ subjectId: localIntents.subjectId, ...movePlacementSelection })
27
+ .from(localIntents)
28
+ .where(and(inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement), inArray(localIntents.subjectId, unique.slice(offset, offset + chunkSize))));
29
+ for (const { subjectId, ...placement } of rows) {
30
+ placements.set(subjectId, placement);
31
+ }
32
+ }
33
+ return placements;
34
+ }
16
35
  /** Check at commit time: neither intermediate replay nor discovery owns a pending move. */
17
36
  export async function filterWritableDocumentPlacements(tx, inputs, options) {
18
37
  const ids = inputs.map((input) => input.documentId);
@@ -1 +1 @@
1
- {"version":3,"file":"documentPlacement.js","sourceRoot":"","sources":["documentPlacement.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq, inArray } from \"drizzle-orm\";\nimport { DOCUMENTS_APP_KIND } from \"../../documents/documentConstants\";\nimport {\n documents,\n inLocalIntentLane,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\n\nexport interface DocumentPlacementInput {\n documentId: string;\n containerIds: ReadonlyArray<string>;\n accessEpoch?: number | undefined;\n}\n\nexport interface DocumentPlacementWriteOptions {\n stillCurrent?: (() => boolean) | undefined;\n moveIntentId?: string | undefined;\n}\n\nexport async function loadDocumentMovePlacement(\n tx: ClientSQLiteTransactionScope,\n documentId: string,\n) {\n const [intent] = await tx\n .select({\n id: localIntents.id,\n intentType: localIntents.intentType,\n targetContainerId: localIntents.targetContainerId,\n })\n .from(localIntents)\n .where(\n and(\n inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement),\n eq(localIntents.subjectId, documentId),\n ),\n )\n .limit(1);\n return intent;\n}\n\n/** Check at commit time: neither intermediate replay nor discovery owns a pending move. */\nexport async function filterWritableDocumentPlacements(\n tx: ClientSQLiteTransactionScope,\n inputs: readonly DocumentPlacementInput[],\n options?: DocumentPlacementWriteOptions,\n): Promise<readonly DocumentPlacementInput[]> {\n const ids = inputs.map((input) => input.documentId);\n const intents = new Map(\n (\n await tx\n .select({\n documentId: localIntents.subjectId,\n id: localIntents.id,\n })\n .from(localIntents)\n .where(\n and(\n inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement),\n inArray(localIntents.subjectId, ids),\n ),\n )\n ).map((row) => [row.documentId, row.id]),\n );\n const versionedIds = inputs\n .filter((input) => input.accessEpoch !== undefined)\n .map((input) => input.documentId);\n const epochs = new Map<string, number>();\n if (versionedIds.length > 0) {\n const rows = await tx\n .select({\n documentId: documents.documentId,\n accessEpoch: documents.accessEpoch,\n })\n .from(documents)\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n inArray(documents.documentId, versionedIds),\n ),\n );\n for (const row of rows)\n if (row.documentId)\n epochs.set(\n row.documentId,\n Math.max(epochs.get(row.documentId) ?? 0, row.accessEpoch),\n );\n }\n return inputs.filter((input) => {\n if (options?.moveIntentId)\n return intents.get(input.documentId) === options.moveIntentId;\n if (intents.has(input.documentId)) return false;\n return (\n input.accessEpoch === undefined ||\n input.accessEpoch >= (epochs.get(input.documentId) ?? 0)\n );\n });\n}\n"]}
1
+ {"version":3,"file":"documentPlacement.js","sourceRoot":"","sources":["documentPlacement.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq, inArray } from \"drizzle-orm\";\nimport { DOCUMENTS_APP_KIND } from \"../../documents/documentConstants\";\nimport {\n documents,\n inLocalIntentLane,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport { sqliteRowsPerStatement } from \"../../sqlite/sqliteBindBudget\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\n\nexport interface DocumentPlacementInput {\n documentId: string;\n containerIds: ReadonlyArray<string>;\n accessEpoch?: number | undefined;\n}\n\nexport interface DocumentPlacementWriteOptions {\n stillCurrent?: (() => boolean) | undefined;\n moveIntentId?: string | undefined;\n}\n\nconst movePlacementSelection = {\n id: localIntents.id,\n intentType: localIntents.intentType,\n targetContainerId: localIntents.targetContainerId,\n};\n\nexport interface DocumentMovePlacement {\n readonly id: string | null;\n readonly intentType: string;\n readonly targetContainerId: string;\n}\n\nexport async function loadDocumentMovePlacement(\n tx: ClientSQLiteTransactionScope,\n documentId: string,\n): Promise<DocumentMovePlacement | undefined> {\n const [intent] = await tx\n .select(movePlacementSelection)\n .from(localIntents)\n .where(\n and(\n inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement),\n eq(localIntents.subjectId, documentId),\n ),\n )\n .limit(1);\n return intent;\n}\n\n/** `loadDocumentMovePlacement` for many documents, keyed by document id. */\nexport async function loadDocumentMovePlacements(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<Map<string, DocumentMovePlacement>> {\n const placements = new Map<string, DocumentMovePlacement>();\n const unique = Array.from(new Set(documentIds));\n // The lane predicate binds one value; the rest carry document ids.\n const chunkSize = sqliteRowsPerStatement(1, 1);\n for (let offset = 0; offset < unique.length; offset += chunkSize) {\n const rows = await tx\n .select({ subjectId: localIntents.subjectId, ...movePlacementSelection })\n .from(localIntents)\n .where(\n and(\n inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement),\n inArray(\n localIntents.subjectId,\n unique.slice(offset, offset + chunkSize),\n ),\n ),\n );\n for (const { subjectId, ...placement } of rows) {\n placements.set(subjectId, placement);\n }\n }\n return placements;\n}\n\n/** Check at commit time: neither intermediate replay nor discovery owns a pending move. */\nexport async function filterWritableDocumentPlacements(\n tx: ClientSQLiteTransactionScope,\n inputs: readonly DocumentPlacementInput[],\n options?: DocumentPlacementWriteOptions,\n): Promise<readonly DocumentPlacementInput[]> {\n const ids = inputs.map((input) => input.documentId);\n const intents = new Map(\n (\n await tx\n .select({\n documentId: localIntents.subjectId,\n id: localIntents.id,\n })\n .from(localIntents)\n .where(\n and(\n inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement),\n inArray(localIntents.subjectId, ids),\n ),\n )\n ).map((row) => [row.documentId, row.id]),\n );\n const versionedIds = inputs\n .filter((input) => input.accessEpoch !== undefined)\n .map((input) => input.documentId);\n const epochs = new Map<string, number>();\n if (versionedIds.length > 0) {\n const rows = await tx\n .select({\n documentId: documents.documentId,\n accessEpoch: documents.accessEpoch,\n })\n .from(documents)\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n inArray(documents.documentId, versionedIds),\n ),\n );\n for (const row of rows)\n if (row.documentId)\n epochs.set(\n row.documentId,\n Math.max(epochs.get(row.documentId) ?? 0, row.accessEpoch),\n );\n }\n return inputs.filter((input) => {\n if (options?.moveIntentId)\n return intents.get(input.documentId) === options.moveIntentId;\n if (intents.has(input.documentId)) return false;\n return (\n input.accessEpoch === undefined ||\n input.accessEpoch >= (epochs.get(input.documentId) ?? 0)\n );\n });\n}\n"]}
@@ -4,7 +4,7 @@ import { documentContainerProjection, documentContainerProjectionTables, documen
4
4
  import { documentIntentLinkTargets } from "../../sqlite/schema/documentPlacementIntentSchema.js";
5
5
  import { getClientSQLitePersistenceRuntime } from "../../sqlite/sqlitePersistenceRuntime.js";
6
6
  import { ensureSqlTables, runSerializedSqlMutation, } from "../../sqlite/sqlSchema.js";
7
- export async function loadDocumentIntentLinkTargets(execSql, intentId) {
7
+ async function loadDocumentIntentLinkTargets(execSql, intentId) {
8
8
  return getClientSQLitePersistenceRuntime(execSql)
9
9
  .db.select({
10
10
  containerId: documentIntentLinkTargets.containerId,