@tearleads/client-sdk 0.1.23 → 0.1.25

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 (109) hide show
  1. package/data/persistence/container-contents/containerContentsPersistenceTypes.d.ts +2 -0
  2. package/data/persistence/container-contents/containerContentsPersistenceTypes.js.map +1 -1
  3. package/data/persistence/container-contents/containerDeletionPersistence.js +3 -0
  4. package/data/persistence/container-contents/containerDeletionPersistence.js.map +1 -1
  5. package/data/persistence/container-contents/documentPurgePlacement.js +15 -0
  6. package/data/persistence/container-contents/documentPurgePlacement.js.map +1 -0
  7. package/data/persistence/documents/documentsPersistence.js +7 -0
  8. package/data/persistence/documents/documentsPersistence.js.map +1 -1
  9. package/data/persistence/documents/internal/documentDeletionPersistence.js +7 -0
  10. package/data/persistence/documents/internal/documentDeletionPersistence.js.map +1 -1
  11. package/data/persistence/documents/orphanBlobPersistence.d.ts +13 -0
  12. package/data/persistence/documents/orphanBlobPersistence.js +2 -0
  13. package/data/persistence/documents/orphanBlobPersistence.js.map +1 -0
  14. package/data/persistence/documents/types.d.ts +4 -1
  15. package/data/persistence/documents/types.js.map +1 -1
  16. package/data/persistence/principalCurrentAcknowledgementPersistence.d.ts +13 -2
  17. package/data/persistence/principalCurrentAcknowledgementPersistence.js +36 -20
  18. package/data/persistence/principalCurrentAcknowledgementPersistence.js.map +1 -1
  19. package/index.d.ts +2 -2
  20. package/index.js +1 -1
  21. package/index.js.map +1 -1
  22. package/internal/api-client/ApiClient.d.ts +6 -2
  23. package/internal/api-client/ApiClient.js +4 -5
  24. package/internal/api-client/ApiClient.js.map +1 -1
  25. package/internal/api-client/apiRequestRuntime.d.ts +1 -2
  26. package/internal/api-client/apiRequestRuntime.js +12 -15
  27. package/internal/api-client/apiRequestRuntime.js.map +1 -1
  28. package/internal/api-client/routes/containers/mutations.js +2 -2
  29. package/internal/api-client/routes/containers/mutations.js.map +1 -1
  30. package/internal/api-client/sessionRefresh.js +14 -6
  31. package/internal/api-client/sessionRefresh.js.map +1 -1
  32. package/internal/api-client/sessionRenewal.js +54 -0
  33. package/internal/api-client/sessionRenewal.js.map +1 -0
  34. package/internal/validators/operation/containerMutations.d.ts +3 -0
  35. package/internal/validators/operation/containerMutations.js +1 -0
  36. package/internal/validators/operation/containerMutations.js.map +1 -1
  37. package/package.json +1 -1
  38. package/stores/documents/documentStore/attachmentHydration.js +2 -2
  39. package/stores/documents/documentStore/attachmentHydration.js.map +1 -1
  40. package/stores/documents/documentStore/attachments.js +7 -5
  41. package/stores/documents/documentStore/attachments.js.map +1 -1
  42. package/stores/documents/documentStore/discard.js +3 -2
  43. package/stores/documents/documentStore/discard.js.map +1 -1
  44. package/stores/documents/documentStore/orphanMaintenance.js +3 -5
  45. package/stores/documents/documentStore/orphanMaintenance.js.map +1 -1
  46. package/stores/documents/documentStore/persistence.js +3 -4
  47. package/stores/documents/documentStore/persistence.js.map +1 -1
  48. package/stores/documents/documentStore/syncDetachedAttachments.js +3 -2
  49. package/stores/documents/documentStore/syncDetachedAttachments.js.map +1 -1
  50. package/stores/documents/documentStore/syncRequest.js +3 -4
  51. package/stores/documents/documentStore/syncRequest.js.map +1 -1
  52. package/workflows/container-contents/container-state/delete.js +2 -0
  53. package/workflows/container-contents/container-state/delete.js.map +1 -1
  54. package/workflows/container-contents/container-state/localPurgeScope.js +49 -0
  55. package/workflows/container-contents/container-state/localPurgeScope.js.map +1 -0
  56. package/workflows/container-contents/container-state/purgeTree.js +42 -7
  57. package/workflows/container-contents/container-state/purgeTree.js.map +1 -1
  58. package/workflows/container-contents/container-state/purgeTreeCollection.js +11 -0
  59. package/workflows/container-contents/container-state/purgeTreeCollection.js.map +1 -1
  60. package/workflows/container-contents/container-state/purgeTreeContainerDeletion.js +4 -1
  61. package/workflows/container-contents/container-state/purgeTreeContainerDeletion.js.map +1 -1
  62. package/workflows/container-contents/container-state/purgeTreeScope.js +158 -0
  63. package/workflows/container-contents/container-state/purgeTreeScope.js.map +1 -0
  64. package/workflows/container-contents/container-state/readLocalPurgeScope.js +18 -0
  65. package/workflows/container-contents/container-state/readLocalPurgeScope.js.map +1 -0
  66. package/workflows/container-contents/container-state/remote.js +1 -0
  67. package/workflows/container-contents/container-state/remote.js.map +1 -1
  68. package/workflows/container-contents/container-state/types.js.map +1 -1
  69. package/workflows/container-contents/documentLinks.d.ts +4 -0
  70. package/workflows/container-contents/documentLinks.js +2 -0
  71. package/workflows/container-contents/documentLinks.js.map +1 -1
  72. package/workflows/container-contents/documentPurge.d.ts +6 -1
  73. package/workflows/container-contents/documentPurge.js +12 -5
  74. package/workflows/container-contents/documentPurge.js.map +1 -1
  75. package/workflows/documents/linkSet.d.ts +1 -0
  76. package/workflows/documents/linkSet.js +2 -0
  77. package/workflows/documents/linkSet.js.map +1 -1
  78. package/workflows/documents/linkSetRemote.d.ts +2 -0
  79. package/workflows/documents/linkSetRemote.js +3 -0
  80. package/workflows/documents/linkSetRemote.js.map +1 -1
  81. package/workflows/documents/orphanBlobReclaimState.js +20 -0
  82. package/workflows/documents/orphanBlobReclaimState.js.map +1 -0
  83. package/workflows/documents/orphanBlobReclaims.d.ts +3 -2
  84. package/workflows/documents/orphanBlobReclaims.js +49 -47
  85. package/workflows/documents/orphanBlobReclaims.js.map +1 -1
  86. package/workflows/documents/purge.d.ts +3 -0
  87. package/workflows/documents/purge.js +7 -0
  88. package/workflows/documents/purge.js.map +1 -1
  89. package/workflows/documents/subtreePurgeScope.js +11 -0
  90. package/workflows/documents/subtreePurgeScope.js.map +1 -0
  91. package/workflows/organizations/acknowledgedPrincipalPredecessor.d.ts +39 -0
  92. package/workflows/organizations/acknowledgedPrincipalPredecessor.js +87 -0
  93. package/workflows/organizations/acknowledgedPrincipalPredecessor.js.map +1 -0
  94. package/workflows/organizations/currentOrganizationMutationRetention.js +13 -2
  95. package/workflows/organizations/currentOrganizationMutationRetention.js.map +1 -1
  96. package/workflows/organizations/index.d.ts +1 -0
  97. package/workflows/organizations/index.js +1 -0
  98. package/workflows/organizations/index.js.map +1 -1
  99. package/workflows/organizations/initialPrincipalCurrentPublication.js +3 -2
  100. package/workflows/organizations/initialPrincipalCurrentPublication.js.map +1 -1
  101. package/workflows/organizations/reconcileAcknowledgedPrincipalCurrent.js +63 -0
  102. package/workflows/organizations/reconcileAcknowledgedPrincipalCurrent.js.map +1 -0
  103. package/workflows/organizations/retainAcknowledgedPrincipalCurrents.d.ts +3 -0
  104. package/workflows/organizations/retainAcknowledgedPrincipalCurrents.js +33 -55
  105. package/workflows/organizations/retainAcknowledgedPrincipalCurrents.js.map +1 -1
  106. package/workflows/organizations/runtimeCurrentGroupMutation.js +13 -2
  107. package/workflows/organizations/runtimeCurrentGroupMutation.js.map +1 -1
  108. package/workflows/organizations/runtimeCurrentOrganizationMutation.js +2 -2
  109. package/workflows/organizations/runtimeCurrentOrganizationMutation.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"discard.js","sourceRoot":"","sources":["discard.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,+BAA+B,EAC/B,0BAA0B,GAC3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAA2B,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAEtE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAClD,KAAyB,EACzB,kBAA0B;IAE1B,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,IAAI,OAAO,CAAC,KAAK,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACvC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,yEAAyE;IACzE,0EAA0E;IAC1E,wEAAwE;IACxE,2EAA2E;IAC3E,kDAAkD;IAClD,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;IACtC,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAC5E,IACE,CAAC,MAAM,EAAE,UAAU;QACnB,MAAM,CAAC,UAAU,KAAK,kBAAkB;QACxC,CAAC,MAAM,CAAC,WAAW,EACnB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,uEAAuE;IACvE,sEAAsE;IACtE,yEAAyE;IACzE,sEAAsE;IACtE,yBAAyB;IACzB,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC;IACjB,KAAK,CAAC,oBAAoB,IAAI,CAAC,CAAC;IAChC,2EAA2E;IAC3E,mEAAmE;IACnE,mEAAmE;IACnE,0EAA0E;IAC1E,2EAA2E;IAC3E,0EAA0E;IAC1E,+DAA+D;IAC/D,MAAM,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAE9C,OAAO,kBAAkB,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;QAC1C,IAAI,KAAkE,CAAC;QACvE,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,+BAA+B,CAAC;gBAC5C,kBAAkB,EAAE,OAAO,CAAC,KAAK,CAAC,kBAAkB;gBACpD,OAAO;gBACP,kBAAkB;gBAClB,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,WAAW,EAAE,KAAK,CAAC,WAAW;aAC/B,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,wEAAwE;YACxE,qEAAqE;YACrE,gEAAgE;YAChE,kBAAkB,CAAC,KAAK,CAAC,CAAC;YAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,CACd,0CAA0C,KAAK,CAAC,OAAO,yCAAyC,CACjG,CAAC;YACF,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;YACrB,qEAAqE;YACrE,mEAAmE;YACnE,kBAAkB,CAAC,KAAK,CAAC,CAAC;YAC1B,OAAO,KAAK,CAAC;QACf,CAAC;QAED,mEAAmE;QACnE,6DAA6D;QAC7D,MAAM,0BAA0B,CAAC,OAAO,CAAC,CAAC;QAC1C,kBAAkB,CAAC,KAAK,CAAC,CAAC;QAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,CACd,iDAAiD,KAAK,CAAC,OAAO,8BAA8B,CAC7F,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import {\n discardPersistedDocumentToShell,\n reclaimDocumentOrphanBlobs,\n} from \"../../../workflows/documents\";\nimport { chainIdentityWrite } from \"./identityWriteChain\";\nimport { type DocumentStoreState, resetDocumentStore } from \"./state\";\n\n/**\n * User-initiated escape hatch for a document whose queued local writes can no\n * longer sync (e.g. an outgoing queue whose update ids conflict forever):\n * convert the locally persisted document to the freshly-discovered-share\n * shell (documentId kept, empty snapshot, queue/history/failure rows\n * dropped) so the store re-hydrates the server copy. The shell matters twice\n * over: priming's candidate scan reads the documents rows themselves, so a\n * fully deleted record would never be restored after a restart, and\n * re-initialization needs the record to hydrate instead of eagerly creating\n * a blank document. discardPersistedDocumentToShell owns the row-level\n * sequence — including refusing local-only, unlinked, and move-pending\n * documents under its serialized mutation — and converts the record in\n * place, so no step leaves the document row absent.\n *\n * The caller restarts initialization after this settles (success or not);\n * the reset store then hydrates from whatever rows the sequence left. That\n * restart must happen OUTSIDE the identity-chained task — initialization\n * chains identity writes of its own, so starting it there would deadlock.\n */\nexport async function discardDocumentStoreLocalState(\n state: DocumentStoreState,\n expectedDocumentId: string,\n): Promise<boolean> {\n const runtime = state.runtime;\n if (runtime.infra.dbStatus !== \"ready\") {\n return false;\n }\n\n // Cheap eligibility probe before disturbing the live store: an obviously\n // ineligible document (local-only, unlinked, or one whose identity is not\n // the one the caller asked to discard) returns without stopping writes.\n // The workflow re-checks under its serialized mutation, so a status change\n // between here and there is still refused safely.\n const execSql = runtime.infra.execSql;\n const record = await state.persistence.loadDocument(execSql, state.localId);\n if (\n !record?.documentId ||\n record.documentId !== expectedDocumentId ||\n !record.containerId\n ) {\n return false;\n }\n\n // Stop local writes before draining: dropping the live doc rejects new\n // mutations at queue time, and the generation bump invalidates queued\n // write tasks that have not started (and in-flight attachment passes and\n // initializations, which re-check the generation inside their writes'\n // serialized mutations).\n state.doc = null;\n state.localWriteGeneration += 1;\n // Drain OUTSIDE the identity chain. Write-chain tasks can themselves enter\n // the identity chain (the stale-heal recovery installs its rebuilt\n // document that way), so awaiting the write chain while holding an\n // identity-chain slot would deadlock: the drained task would queue behind\n // this one and each would wait on the other forever. Out here the drain is\n // a plain await; whatever the drained tasks chain onto the identity chain\n // runs before the teardown task below and is superseded by it.\n await state.writeChain.catch(() => undefined);\n\n return chainIdentityWrite(state, async () => {\n let shell: Awaited<ReturnType<typeof discardPersistedDocumentToShell>>;\n try {\n shell = await discardPersistedDocumentToShell({\n documentProjectors: runtime.infra.documentProjectors,\n execSql,\n expectedDocumentId,\n localId: state.localId,\n persistence: state.persistence,\n });\n } catch {\n // The conversion is in-place, so an interrupted sequence still leaves a\n // loadable record (old or shell). Reset so re-initialization reloads\n // whatever survived instead of running on the dropped live doc.\n resetDocumentStore(state);\n runtime.util.log(\n `Documents: discard failed for document ${state.localId}; store reset to reload persisted state`,\n );\n return false;\n }\n if (!shell.discarded) {\n // Refused under the lock (a move intent, or an identity change since\n // the probe): rows are untouched; reset so the store reloads them.\n resetDocumentStore(state);\n return false;\n }\n\n // The conversion queued the bytes its dropped rows pointed to; the\n // reclaim deletes each only once no other row references it.\n await reclaimDocumentOrphanBlobs(runtime);\n resetDocumentStore(state);\n runtime.util.log(\n `Documents: discarded local edits for document ${state.localId}; re-pulling the server copy`,\n );\n return true;\n });\n}\n"]}
1
+ {"version":3,"file":"discard.js","sourceRoot":"","sources":["discard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,+BAA+B,EAAE,MAAM,8BAA8B,CAAC;AAC/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAA2B,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAEtE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAClD,KAAyB,EACzB,kBAA0B;IAE1B,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,IAAI,OAAO,CAAC,KAAK,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACvC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,yEAAyE;IACzE,0EAA0E;IAC1E,wEAAwE;IACxE,2EAA2E;IAC3E,kDAAkD;IAClD,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;IACtC,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAC5E,IACE,CAAC,MAAM,EAAE,UAAU;QACnB,MAAM,CAAC,UAAU,KAAK,kBAAkB;QACxC,CAAC,MAAM,CAAC,WAAW,EACnB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,uEAAuE;IACvE,sEAAsE;IACtE,yEAAyE;IACzE,sEAAsE;IACtE,yBAAyB;IACzB,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC;IACjB,KAAK,CAAC,oBAAoB,IAAI,CAAC,CAAC;IAChC,2EAA2E;IAC3E,mEAAmE;IACnE,mEAAmE;IACnE,0EAA0E;IAC1E,2EAA2E;IAC3E,0EAA0E;IAC1E,+DAA+D;IAC/D,MAAM,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAE9C,OAAO,kBAAkB,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;QAC1C,IAAI,KAAkE,CAAC;QACvE,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,+BAA+B,CAAC;gBAC5C,kBAAkB,EAAE,OAAO,CAAC,KAAK,CAAC,kBAAkB;gBACpD,OAAO;gBACP,kBAAkB;gBAClB,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,WAAW,EAAE,KAAK,CAAC,WAAW;aAC/B,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,wEAAwE;YACxE,qEAAqE;YACrE,gEAAgE;YAChE,kBAAkB,CAAC,KAAK,CAAC,CAAC;YAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,CACd,0CAA0C,KAAK,CAAC,OAAO,yCAAyC,CACjG,CAAC;YACF,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;YACrB,qEAAqE;YACrE,mEAAmE;YACnE,kBAAkB,CAAC,KAAK,CAAC,CAAC;YAC1B,OAAO,KAAK,CAAC;QACf,CAAC;QAED,mEAAmE;QACnE,6DAA6D;QAC7D,KAAK,4BAA4B,CAAC,KAAK,CAAC,CAAC;QACzC,kBAAkB,CAAC,KAAK,CAAC,CAAC;QAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,CACd,iDAAiD,KAAK,CAAC,OAAO,8BAA8B,CAC7F,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { discardPersistedDocumentToShell } from \"../../../workflows/documents\";\nimport { chainIdentityWrite } from \"./identityWriteChain\";\nimport { runDocumentOrphanMaintenance } from \"./orphanMaintenance\";\nimport { type DocumentStoreState, resetDocumentStore } from \"./state\";\n\n/**\n * User-initiated escape hatch for a document whose queued local writes can no\n * longer sync (e.g. an outgoing queue whose update ids conflict forever):\n * convert the locally persisted document to the freshly-discovered-share\n * shell (documentId kept, empty snapshot, queue/history/failure rows\n * dropped) so the store re-hydrates the server copy. The shell matters twice\n * over: priming's candidate scan reads the documents rows themselves, so a\n * fully deleted record would never be restored after a restart, and\n * re-initialization needs the record to hydrate instead of eagerly creating\n * a blank document. discardPersistedDocumentToShell owns the row-level\n * sequence — including refusing local-only, unlinked, and move-pending\n * documents under its serialized mutation — and converts the record in\n * place, so no step leaves the document row absent.\n *\n * The caller restarts initialization after this settles (success or not);\n * the reset store then hydrates from whatever rows the sequence left. That\n * restart must happen OUTSIDE the identity-chained task — initialization\n * chains identity writes of its own, so starting it there would deadlock.\n */\nexport async function discardDocumentStoreLocalState(\n state: DocumentStoreState,\n expectedDocumentId: string,\n): Promise<boolean> {\n const runtime = state.runtime;\n if (runtime.infra.dbStatus !== \"ready\") {\n return false;\n }\n\n // Cheap eligibility probe before disturbing the live store: an obviously\n // ineligible document (local-only, unlinked, or one whose identity is not\n // the one the caller asked to discard) returns without stopping writes.\n // The workflow re-checks under its serialized mutation, so a status change\n // between here and there is still refused safely.\n const execSql = runtime.infra.execSql;\n const record = await state.persistence.loadDocument(execSql, state.localId);\n if (\n !record?.documentId ||\n record.documentId !== expectedDocumentId ||\n !record.containerId\n ) {\n return false;\n }\n\n // Stop local writes before draining: dropping the live doc rejects new\n // mutations at queue time, and the generation bump invalidates queued\n // write tasks that have not started (and in-flight attachment passes and\n // initializations, which re-check the generation inside their writes'\n // serialized mutations).\n state.doc = null;\n state.localWriteGeneration += 1;\n // Drain OUTSIDE the identity chain. Write-chain tasks can themselves enter\n // the identity chain (the stale-heal recovery installs its rebuilt\n // document that way), so awaiting the write chain while holding an\n // identity-chain slot would deadlock: the drained task would queue behind\n // this one and each would wait on the other forever. Out here the drain is\n // a plain await; whatever the drained tasks chain onto the identity chain\n // runs before the teardown task below and is superseded by it.\n await state.writeChain.catch(() => undefined);\n\n return chainIdentityWrite(state, async () => {\n let shell: Awaited<ReturnType<typeof discardPersistedDocumentToShell>>;\n try {\n shell = await discardPersistedDocumentToShell({\n documentProjectors: runtime.infra.documentProjectors,\n execSql,\n expectedDocumentId,\n localId: state.localId,\n persistence: state.persistence,\n });\n } catch {\n // The conversion is in-place, so an interrupted sequence still leaves a\n // loadable record (old or shell). Reset so re-initialization reloads\n // whatever survived instead of running on the dropped live doc.\n resetDocumentStore(state);\n runtime.util.log(\n `Documents: discard failed for document ${state.localId}; store reset to reload persisted state`,\n );\n return false;\n }\n if (!shell.discarded) {\n // Refused under the lock (a move intent, or an identity change since\n // the probe): rows are untouched; reset so the store reloads them.\n resetDocumentStore(state);\n return false;\n }\n\n // The conversion queued the bytes its dropped rows pointed to; the\n // reclaim deletes each only once no other row references it.\n void runDocumentOrphanMaintenance(state);\n resetDocumentStore(state);\n runtime.util.log(\n `Documents: discarded local edits for document ${state.localId}; re-pulling the server copy`,\n );\n return true;\n });\n}\n"]}
@@ -1,8 +1,6 @@
1
- import { defaultDocumentsPersistence, reclaimDocumentOrphanBlobs, } from "../../../workflows/documents/index.js";
2
- /** Run SQL-only maintenance without making document initialization wait for it. */
1
+ import { reclaimDocumentOrphanBlobs } from "../../../workflows/documents/index.js";
2
+ /** Reclaim through the adapter that owns the attachment references. */
3
3
  export async function runDocumentOrphanMaintenance(state) {
4
- if (state.persistence === defaultDocumentsPersistence) {
5
- await reclaimDocumentOrphanBlobs(state.runtime);
6
- }
4
+ await reclaimDocumentOrphanBlobs(state.runtime, state.persistence);
7
5
  }
8
6
  //# sourceMappingURL=orphanMaintenance.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"orphanMaintenance.js","sourceRoot":"","sources":["orphanMaintenance.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,8BAA8B,CAAC;AAGtC,mFAAmF;AACnF,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,KAAyB;IAEzB,IAAI,KAAK,CAAC,WAAW,KAAK,2BAA2B,EAAE,CAAC;QACtD,MAAM,0BAA0B,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAClD,CAAC;AACH,CAAC","sourcesContent":["import {\n defaultDocumentsPersistence,\n reclaimDocumentOrphanBlobs,\n} from \"../../../workflows/documents\";\nimport type { DocumentStoreState } from \"./state\";\n\n/** Run SQL-only maintenance without making document initialization wait for it. */\nexport async function runDocumentOrphanMaintenance(\n state: DocumentStoreState,\n): Promise<void> {\n if (state.persistence === defaultDocumentsPersistence) {\n await reclaimDocumentOrphanBlobs(state.runtime);\n }\n}\n"]}
1
+ {"version":3,"file":"orphanMaintenance.js","sourceRoot":"","sources":["orphanMaintenance.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAG1E,uEAAuE;AACvE,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,KAAyB;IAEzB,MAAM,0BAA0B,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;AACrE,CAAC","sourcesContent":["import { reclaimDocumentOrphanBlobs } from \"../../../workflows/documents\";\nimport type { DocumentStoreState } from \"./state\";\n\n/** Reclaim through the adapter that owns the attachment references. */\nexport async function runDocumentOrphanMaintenance(\n state: DocumentStoreState,\n): Promise<void> {\n await reclaimDocumentOrphanBlobs(state.runtime, state.persistence);\n}\n"]}
@@ -3,9 +3,10 @@ import { normalizeEffectiveAccessLevel } from "../../../data/accessLevel.js";
3
3
  import { DEFAULT_DOCUMENT_KIND } from "../../../data/documents/documentConstants.js";
4
4
  import { projectStoredDocumentState, } from "../../../data/documents/documentKinds.js";
5
5
  import { errorMessage } from "../../../data/errorMessage.js";
6
- import { defaultDocumentsPersistence, enqueuePendingDocumentUpdate, listPendingDocumentUpdates, persistDocumentState, reclaimDocumentOrphanBlobs, runSerializedSqlMutation, } from "../../../workflows/documents/index.js";
6
+ import { enqueuePendingDocumentUpdate, listPendingDocumentUpdates, persistDocumentState, runSerializedSqlMutation, } from "../../../workflows/documents/index.js";
7
7
  import { maybeCompactDocumentHistory } from "./documentHistoryCompaction.js";
8
8
  import { importDurableDocumentHistory, installDurableDocumentReload, } from "./durableDocumentReload.js";
9
+ import { runDocumentOrphanMaintenance } from "./orphanMaintenance.js";
9
10
  import { markDocumentStoreRemoved, setReadySnapshot, } from "./state.js";
10
11
  import { createFreshPeerStoredDocument } from "./storedDocument.js";
11
12
  import { isDocumentStoreSyncGenerationCurrent as isSyncGenerationCurrent, } from "./syncGeneration.js";
@@ -100,9 +101,7 @@ export async function saveDocumentRecord(state, currentDoc, patch = {}, options
100
101
  if (isCurrent()) {
101
102
  markDocumentStoreRemoved(state);
102
103
  }
103
- if (state.persistence === defaultDocumentsPersistence) {
104
- void reclaimDocumentOrphanBlobs(state.runtime);
105
- }
104
+ void runDocumentOrphanMaintenance(state);
106
105
  return null;
107
106
  }
108
107
  const { record: nextRecord, updatedAt } = persistedDocumentState;
@@ -1 +1 @@
1
- {"version":3,"file":"persistence.js","sourceRoot":"","sources":["persistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { encodeVersionVector, exportUpdatesSince } from \"@tearleads/loro\";\nimport { normalizeEffectiveAccessLevel } from \"../../../data/accessLevel\";\nimport { DEFAULT_DOCUMENT_KIND } from \"../../../data/documents/documentConstants\";\nimport {\n type DocumentProjectorRegistry,\n projectStoredDocumentState,\n} from \"../../../data/documents/documentKinds\";\nimport type { DocumentSummary } from \"../../../data/documents/documentSummary\";\nimport { errorMessage } from \"../../../data/errorMessage\";\n\nimport {\n type DocumentRecord,\n defaultDocumentsPersistence,\n type ExecSql,\n enqueuePendingDocumentUpdate,\n listPendingDocumentUpdates,\n type PendingUpdateRecord,\n persistDocumentState,\n reclaimDocumentOrphanBlobs,\n runSerializedSqlMutation,\n} from \"../../../workflows/documents\";\nimport { maybeCompactDocumentHistory } from \"./documentHistoryCompaction\";\nimport {\n importDurableDocumentHistory,\n installDurableDocumentReload,\n} from \"./durableDocumentReload\";\nimport {\n type DocumentState,\n type DocumentStoreState,\n markDocumentStoreRemoved,\n type PersistedDocumentRecord,\n type SaveDocumentRecordOptions,\n setReadySnapshot,\n} from \"./state\";\nimport { createFreshPeerStoredDocument } from \"./storedDocument\";\nimport {\n type DocumentStoreSyncGeneration,\n isDocumentStoreSyncGenerationCurrent as isSyncGenerationCurrent,\n} from \"./syncGeneration\";\n\nexport function documentSummaryFromRecord(\n record: DocumentRecord,\n updatedAt: string,\n documentProjectors: DocumentProjectorRegistry,\n): DocumentSummary {\n return {\n accessStateHash: record.accessStateHash ?? null,\n effectiveAccessLevel: normalizeEffectiveAccessLevel(\n record.effectiveAccessLevel,\n ),\n id: record.id,\n containerId: record.containerId,\n documentKind: record.documentKind ?? DEFAULT_DOCUMENT_KIND,\n documentId: record.documentId,\n title:\n record.title ??\n projectStoredDocumentState(\n {\n documentKind: record.documentKind ?? DEFAULT_DOCUMENT_KIND,\n structuredFields: {},\n text: record.text,\n },\n documentProjectors,\n ).title,\n updatedAt,\n };\n}\n\nfunction toPersistedDocumentRecord(\n persisted: NonNullable<Awaited<ReturnType<typeof persistDocumentState>>>,\n): PersistedDocumentRecord {\n const { record, updatedAt } = persisted;\n return {\n ...(persisted.creationSuperseded\n ? { creationSuperseded: true as const }\n : {}),\n historyRestoreState: persisted.historyRestoreState,\n ...(persisted.pullContinuationSuperseded\n ? { pullContinuationSuperseded: true as const }\n : {}),\n record,\n ...(persisted.syncIdentitySuperseded\n ? { syncIdentitySuperseded: true as const }\n : {}),\n ...(updatedAt !== undefined ? { updatedAt } : {}),\n };\n}\n\n// Nullable without a generation too: an update-persist refuses (returns\n// null) when the durable row was deleted while the persist was queued — the\n// resurrect guard in persistDocumentState — so callers must stop follow-up\n// state changes on null.\nexport async function saveDocumentRecord(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n patch: Partial<DocumentRecord> = {},\n options: SaveDocumentRecordOptions = {},\n expectedGeneration?: DocumentStoreSyncGeneration,\n commitSideEffect?: (transactionExecSql: ExecSql) => Promise<void>,\n): Promise<PersistedDocumentRecord | null> {\n const isCurrent = () =>\n (!expectedGeneration ||\n isSyncGenerationCurrent(state, expectedGeneration)) &&\n options.stillCurrent?.() !== false;\n const createsPlacement = state.record === null;\n const previousDocumentId = state.record?.documentId ?? null;\n const pendingBaseVersion =\n options.pendingBaseVersionOverride === undefined\n ? state.pendingBaseVersion\n : options.pendingBaseVersionOverride;\n const persistenceInput = {\n acceptedPendingUpdateIds: options.acceptedPendingUpdateIds,\n attachmentRemoval: options.attachmentRemoval,\n attachmentStaging: options.attachmentStaging,\n clearSyncFailure: options.clearSyncFailure,\n ...(commitSideEffect ? { commitSideEffect } : {}),\n containerId: state.runtime.state.containerId,\n currentDoc,\n currentRecord: state.record,\n documentProjectors: state.runtime.infra.documentProjectors,\n execSql: state.runtime.infra.execSql,\n expectedSyncState: options.expectedSyncState,\n historyCheckpoint: options.historyCheckpoint,\n historyUpdateOrigin: options.historyUpdateOrigin,\n historyUpdates: options.historyUpdates,\n pendingUpdate: options.pendingUpdate,\n localId: state.localId,\n // Persist the durable outgoing-delta marker with every snapshot write so a\n // restart restores it (see initializeDocumentStore). A device-first\n // deferRemoteSync write leaves it BEHIND the snapshot version on purpose;\n // capturing state.pendingBaseVersion here is what lets the next edit\n // re-derive that deferred op after a restart instead of dropping it.\n patch: { ...patch, pendingBaseVersion },\n persistence: state.persistence,\n };\n const persistedDocumentState =\n expectedGeneration || options.stillCurrent\n ? await persistDocumentState({\n ...persistenceInput,\n // persistDocumentState rechecks after its own pre-save awaits and then\n // immediately claims the executor mutation queue. A reset before that\n // point aborts; a reset afterward cannot let replacement writes overtake.\n canStartDurableMutation: isCurrent,\n })\n : await persistDocumentState(persistenceInput);\n if (!persistedDocumentState) {\n // With a stale generation the teardown that invalidated it owns the\n // state. With a current (or absent) generation, the refusal means the\n // durable row was deleted by ANOTHER subsystem while this persist was\n // queued (the resurrect guard): this store is a zombie — clear it so\n // callers that ignore the result stop advancing state or scheduling\n // sync against a document that no longer exists.\n if (isCurrent()) {\n markDocumentStoreRemoved(state);\n }\n if (state.persistence === defaultDocumentsPersistence) {\n void reclaimDocumentOrphanBlobs(state.runtime);\n }\n return null;\n }\n const { record: nextRecord, updatedAt } = persistedDocumentState;\n if (!isCurrent()) {\n return null;\n }\n\n state.record = persistedDocumentState.record;\n state.pullContinuation = nextRecord.pullContinuation ?? null;\n if (persistedDocumentState.pullContinuationSuperseded) {\n state.pendingBaseVersion =\n nextRecord.pendingBaseVersion ?? nextRecord.snapshotEndVersion;\n }\n if (previousDocumentId !== nextRecord.documentId) {\n state.effects.registerDocumentIdentity(\n state.runtime.state.domainScope,\n nextRecord.id,\n nextRecord.documentId,\n );\n }\n if (updatedAt !== undefined) {\n state.effects.emitPersistedDocument(\n state.runtime.state.domainScope,\n documentSummaryFromRecord(\n nextRecord,\n updatedAt,\n state.runtime.infra.documentProjectors,\n ),\n createsPlacement ||\n Object.hasOwn(patch, \"containerId\") ||\n previousDocumentId !== nextRecord.documentId,\n );\n }\n return toPersistedDocumentRecord(persistedDocumentState);\n}\n\nasync function reloadSupersededDocumentState(\n state: DocumentStoreState,\n isCurrent: () => boolean,\n historyRestoreState: PersistedDocumentRecord[\"historyRestoreState\"],\n previousDocumentId: string | null,\n durableRecord: DocumentRecord,\n): Promise<DocumentState | null> {\n if (historyRestoreState === undefined) {\n throw new Error(\"Superseded document state omitted its durable history\");\n }\n const replacementDoc = await createFreshPeerStoredDocument();\n importDurableDocumentHistory(replacementDoc, historyRestoreState);\n if (!isCurrent()) {\n return null;\n }\n installDurableDocumentReload({\n durableRecord,\n previousDocumentId,\n preserveQueuedWritesWhenIdentityMatches: true,\n registerIdentityChange: false,\n replacementDoc,\n state,\n });\n return replacementDoc;\n}\n\nexport async function persistDocument(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n patch: Partial<DocumentRecord> = {},\n options: SaveDocumentRecordOptions = {},\n expectedGeneration?: DocumentStoreSyncGeneration,\n commitSideEffect?: (transactionExecSql: ExecSql) => Promise<void>,\n): Promise<PersistedDocumentRecord | null> {\n const isCurrent = () =>\n (!expectedGeneration ||\n isSyncGenerationCurrent(state, expectedGeneration)) &&\n options.stillCurrent?.() !== false;\n const previousDocumentId = state.record?.documentId ?? null;\n const persistedRecord = await saveDocumentRecord(\n state,\n currentDoc,\n patch,\n options,\n expectedGeneration,\n commitSideEffect,\n );\n return publishPersistedDocument(\n state,\n currentDoc,\n persistedRecord,\n previousDocumentId,\n options,\n isCurrent,\n );\n}\n\n/** Publish only after the durable claim succeeds and its live context remains. */\nexport async function publishPersistedDocument(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n persistedRecord: PersistedDocumentRecord | null,\n previousDocumentId: string | null,\n options: SaveDocumentRecordOptions,\n isCurrent: () => boolean,\n): Promise<PersistedDocumentRecord | null> {\n if (!persistedRecord) return null;\n if (!isCurrent()) return null;\n\n if (\n persistedRecord.syncIdentitySuperseded ||\n persistedRecord.pullContinuationSuperseded\n ) {\n // Reload the winning durable checkpoint/tail on every CAS loss, including\n // another pane's history or security-context changes. A rejected sync\n // candidate remains isolated from the live document and editor.\n const replacementDoc = await reloadSupersededDocumentState(\n state,\n isCurrent,\n persistedRecord.historyRestoreState,\n previousDocumentId,\n persistedRecord.record,\n );\n if (!replacementDoc) return null;\n return persistedRecord;\n }\n\n setReadySnapshot(\n state,\n currentDoc,\n state.snapshot.syncing,\n options.preserveSnapshotText && !persistedRecord.pullContinuationSuperseded\n ? state.snapshot.text\n : persistedRecord.record.text,\n options.preserveSnapshotStructuredFields &&\n !persistedRecord.pullContinuationSuperseded\n ? state.snapshot.structuredFields\n : undefined,\n );\n // Compaction never fails the persist that triggered it: an unexpected\n // compaction error simply keeps the tail growing until a later pass\n // succeeds, and the content stays durable either way.\n try {\n await maybeCompactDocumentHistory(state, currentDoc, options.stillCurrent);\n } catch (error) {\n state.runtime.util.log(\n `Documents: history compaction skipped: ${errorMessage(error)}`,\n );\n }\n return persistedRecord;\n}\n\nexport async function listPendingUpdates(\n state: DocumentStoreState,\n): Promise<PendingUpdateRecord[]> {\n return listPendingDocumentUpdates({\n execSql: state.runtime.infra.execSql,\n localId: state.localId,\n persistence: state.persistence,\n });\n}\n\n/**\n * Export the outgoing delta for the current edit. The base is `pendingBaseVersion`\n * (the version through which ops are already durably enqueued or synced), NOT the\n * live doc version, so any delta orphaned by a prior failed enqueue is folded back\n * in here. In the success path the marker equals the pre-edit version, so this is\n * identical to exporting \"since the last edit\".\n *\n * Initialization sets `pendingBaseVersion` whenever it sets `doc`, and every\n * caller checks `doc` first, so the marker is non-null here. Assert that rather\n * than falling back to the live version: callers invoke this AFTER mutating the\n * doc, so a live-version fallback would export an empty (post-mutation) delta and\n * silently drop the edit.\n */\nexport function pendingDeltaSinceBase(\n state: DocumentStoreState,\n doc: DocumentState,\n): Uint8Array {\n if (state.pendingBaseVersion === null) {\n throw new Error(\n \"pendingDeltaSinceBase requires an initialized pendingBaseVersion\",\n );\n }\n return exportUpdatesSince(doc, state.pendingBaseVersion);\n}\n\n/**\n * Advance the durable marker to the doc's current version. Call this ONLY after\n * an edit's delta has been durably enqueued and persisted, or after remote\n * updates have been imported (those ops are already on the server).\n */\nexport function advancePendingBaseVersion(\n state: DocumentStoreState,\n doc: DocumentState,\n): void {\n state.pendingBaseVersion = encodeVersionVector(doc);\n}\n\n/**\n * Runs a durable write either ungated — no generation: the caller serializes\n * with teardown via the write-chain drain, and the persistence implementation\n * may legitimately park inside the write WITHOUT holding the global mutation\n * mutex — or inside the serialized mutation with an in-mutex currency check:\n * a teardown (reset/discard) that already ran was ordered strictly before the\n * mutation claim and invalidated the generation, so a stale caller can no\n * longer slip its write in after the teardown's wipe; a write that wins the\n * ordering instead lands before the teardown and is wiped by it. Returns\n * whether the write ran.\n */\nexport async function withGenerationGuardedMutation(\n state: DocumentStoreState,\n expectedGeneration: DocumentStoreSyncGeneration | undefined,\n write: (execSql: ExecSql) => Promise<void>,\n): Promise<boolean> {\n if (!expectedGeneration) {\n await write(state.runtime.infra.execSql);\n return true;\n }\n\n let ran = false;\n await runSerializedSqlMutation(\n state.runtime.infra.execSql,\n async (lockedExecSql) => {\n if (!isSyncGenerationCurrent(state, expectedGeneration)) {\n return;\n }\n await write(lockedExecSql);\n ran = true;\n },\n );\n return ran;\n}\n\nexport async function enqueuePendingUpdate(\n state: DocumentStoreState,\n update: Uint8Array,\n sourceVersionVector?: string | null,\n expectedGeneration?: DocumentStoreSyncGeneration,\n): Promise<boolean> {\n const expectedDocumentId = state.record?.documentId;\n const expectedRecoveryGeneration = state.record?.recoveryGeneration ?? 0;\n if (expectedDocumentId === undefined) {\n return false;\n }\n let enqueued = false;\n const ran = await withGenerationGuardedMutation(\n state,\n expectedGeneration,\n async (execSql) => {\n enqueued = await enqueuePendingDocumentUpdate({\n execSql,\n expectedDocumentId,\n expectedRecoveryGeneration,\n localId: state.localId,\n persistence: state.persistence,\n ...(sourceVersionVector === undefined ? {} : { sourceVersionVector }),\n update,\n });\n },\n );\n return ran && enqueued;\n}\n"]}
1
+ {"version":3,"file":"persistence.js","sourceRoot":"","sources":["persistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { encodeVersionVector, exportUpdatesSince } from \"@tearleads/loro\";\nimport { normalizeEffectiveAccessLevel } from \"../../../data/accessLevel\";\nimport { DEFAULT_DOCUMENT_KIND } from \"../../../data/documents/documentConstants\";\nimport {\n type DocumentProjectorRegistry,\n projectStoredDocumentState,\n} from \"../../../data/documents/documentKinds\";\nimport type { DocumentSummary } from \"../../../data/documents/documentSummary\";\nimport { errorMessage } from \"../../../data/errorMessage\";\nimport {\n type DocumentRecord,\n type ExecSql,\n enqueuePendingDocumentUpdate,\n listPendingDocumentUpdates,\n type PendingUpdateRecord,\n persistDocumentState,\n runSerializedSqlMutation,\n} from \"../../../workflows/documents\";\nimport { maybeCompactDocumentHistory } from \"./documentHistoryCompaction\";\nimport {\n importDurableDocumentHistory,\n installDurableDocumentReload,\n} from \"./durableDocumentReload\";\nimport { runDocumentOrphanMaintenance } from \"./orphanMaintenance\";\nimport {\n type DocumentState,\n type DocumentStoreState,\n markDocumentStoreRemoved,\n type PersistedDocumentRecord,\n type SaveDocumentRecordOptions,\n setReadySnapshot,\n} from \"./state\";\nimport { createFreshPeerStoredDocument } from \"./storedDocument\";\nimport {\n type DocumentStoreSyncGeneration,\n isDocumentStoreSyncGenerationCurrent as isSyncGenerationCurrent,\n} from \"./syncGeneration\";\n\nexport function documentSummaryFromRecord(\n record: DocumentRecord,\n updatedAt: string,\n documentProjectors: DocumentProjectorRegistry,\n): DocumentSummary {\n return {\n accessStateHash: record.accessStateHash ?? null,\n effectiveAccessLevel: normalizeEffectiveAccessLevel(\n record.effectiveAccessLevel,\n ),\n id: record.id,\n containerId: record.containerId,\n documentKind: record.documentKind ?? DEFAULT_DOCUMENT_KIND,\n documentId: record.documentId,\n title:\n record.title ??\n projectStoredDocumentState(\n {\n documentKind: record.documentKind ?? DEFAULT_DOCUMENT_KIND,\n structuredFields: {},\n text: record.text,\n },\n documentProjectors,\n ).title,\n updatedAt,\n };\n}\n\nfunction toPersistedDocumentRecord(\n persisted: NonNullable<Awaited<ReturnType<typeof persistDocumentState>>>,\n): PersistedDocumentRecord {\n const { record, updatedAt } = persisted;\n return {\n ...(persisted.creationSuperseded\n ? { creationSuperseded: true as const }\n : {}),\n historyRestoreState: persisted.historyRestoreState,\n ...(persisted.pullContinuationSuperseded\n ? { pullContinuationSuperseded: true as const }\n : {}),\n record,\n ...(persisted.syncIdentitySuperseded\n ? { syncIdentitySuperseded: true as const }\n : {}),\n ...(updatedAt !== undefined ? { updatedAt } : {}),\n };\n}\n\n// Nullable without a generation too: an update-persist refuses (returns\n// null) when the durable row was deleted while the persist was queued — the\n// resurrect guard in persistDocumentState — so callers must stop follow-up\n// state changes on null.\nexport async function saveDocumentRecord(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n patch: Partial<DocumentRecord> = {},\n options: SaveDocumentRecordOptions = {},\n expectedGeneration?: DocumentStoreSyncGeneration,\n commitSideEffect?: (transactionExecSql: ExecSql) => Promise<void>,\n): Promise<PersistedDocumentRecord | null> {\n const isCurrent = () =>\n (!expectedGeneration ||\n isSyncGenerationCurrent(state, expectedGeneration)) &&\n options.stillCurrent?.() !== false;\n const createsPlacement = state.record === null;\n const previousDocumentId = state.record?.documentId ?? null;\n const pendingBaseVersion =\n options.pendingBaseVersionOverride === undefined\n ? state.pendingBaseVersion\n : options.pendingBaseVersionOverride;\n const persistenceInput = {\n acceptedPendingUpdateIds: options.acceptedPendingUpdateIds,\n attachmentRemoval: options.attachmentRemoval,\n attachmentStaging: options.attachmentStaging,\n clearSyncFailure: options.clearSyncFailure,\n ...(commitSideEffect ? { commitSideEffect } : {}),\n containerId: state.runtime.state.containerId,\n currentDoc,\n currentRecord: state.record,\n documentProjectors: state.runtime.infra.documentProjectors,\n execSql: state.runtime.infra.execSql,\n expectedSyncState: options.expectedSyncState,\n historyCheckpoint: options.historyCheckpoint,\n historyUpdateOrigin: options.historyUpdateOrigin,\n historyUpdates: options.historyUpdates,\n pendingUpdate: options.pendingUpdate,\n localId: state.localId,\n // Persist the durable outgoing-delta marker with every snapshot write so a\n // restart restores it (see initializeDocumentStore). A device-first\n // deferRemoteSync write leaves it BEHIND the snapshot version on purpose;\n // capturing state.pendingBaseVersion here is what lets the next edit\n // re-derive that deferred op after a restart instead of dropping it.\n patch: { ...patch, pendingBaseVersion },\n persistence: state.persistence,\n };\n const persistedDocumentState =\n expectedGeneration || options.stillCurrent\n ? await persistDocumentState({\n ...persistenceInput,\n // persistDocumentState rechecks after its own pre-save awaits and then\n // immediately claims the executor mutation queue. A reset before that\n // point aborts; a reset afterward cannot let replacement writes overtake.\n canStartDurableMutation: isCurrent,\n })\n : await persistDocumentState(persistenceInput);\n if (!persistedDocumentState) {\n // With a stale generation the teardown that invalidated it owns the\n // state. With a current (or absent) generation, the refusal means the\n // durable row was deleted by ANOTHER subsystem while this persist was\n // queued (the resurrect guard): this store is a zombie — clear it so\n // callers that ignore the result stop advancing state or scheduling\n // sync against a document that no longer exists.\n if (isCurrent()) {\n markDocumentStoreRemoved(state);\n }\n void runDocumentOrphanMaintenance(state);\n return null;\n }\n const { record: nextRecord, updatedAt } = persistedDocumentState;\n if (!isCurrent()) {\n return null;\n }\n\n state.record = persistedDocumentState.record;\n state.pullContinuation = nextRecord.pullContinuation ?? null;\n if (persistedDocumentState.pullContinuationSuperseded) {\n state.pendingBaseVersion =\n nextRecord.pendingBaseVersion ?? nextRecord.snapshotEndVersion;\n }\n if (previousDocumentId !== nextRecord.documentId) {\n state.effects.registerDocumentIdentity(\n state.runtime.state.domainScope,\n nextRecord.id,\n nextRecord.documentId,\n );\n }\n if (updatedAt !== undefined) {\n state.effects.emitPersistedDocument(\n state.runtime.state.domainScope,\n documentSummaryFromRecord(\n nextRecord,\n updatedAt,\n state.runtime.infra.documentProjectors,\n ),\n createsPlacement ||\n Object.hasOwn(patch, \"containerId\") ||\n previousDocumentId !== nextRecord.documentId,\n );\n }\n return toPersistedDocumentRecord(persistedDocumentState);\n}\n\nasync function reloadSupersededDocumentState(\n state: DocumentStoreState,\n isCurrent: () => boolean,\n historyRestoreState: PersistedDocumentRecord[\"historyRestoreState\"],\n previousDocumentId: string | null,\n durableRecord: DocumentRecord,\n): Promise<DocumentState | null> {\n if (historyRestoreState === undefined) {\n throw new Error(\"Superseded document state omitted its durable history\");\n }\n const replacementDoc = await createFreshPeerStoredDocument();\n importDurableDocumentHistory(replacementDoc, historyRestoreState);\n if (!isCurrent()) {\n return null;\n }\n installDurableDocumentReload({\n durableRecord,\n previousDocumentId,\n preserveQueuedWritesWhenIdentityMatches: true,\n registerIdentityChange: false,\n replacementDoc,\n state,\n });\n return replacementDoc;\n}\n\nexport async function persistDocument(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n patch: Partial<DocumentRecord> = {},\n options: SaveDocumentRecordOptions = {},\n expectedGeneration?: DocumentStoreSyncGeneration,\n commitSideEffect?: (transactionExecSql: ExecSql) => Promise<void>,\n): Promise<PersistedDocumentRecord | null> {\n const isCurrent = () =>\n (!expectedGeneration ||\n isSyncGenerationCurrent(state, expectedGeneration)) &&\n options.stillCurrent?.() !== false;\n const previousDocumentId = state.record?.documentId ?? null;\n const persistedRecord = await saveDocumentRecord(\n state,\n currentDoc,\n patch,\n options,\n expectedGeneration,\n commitSideEffect,\n );\n return publishPersistedDocument(\n state,\n currentDoc,\n persistedRecord,\n previousDocumentId,\n options,\n isCurrent,\n );\n}\n\n/** Publish only after the durable claim succeeds and its live context remains. */\nexport async function publishPersistedDocument(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n persistedRecord: PersistedDocumentRecord | null,\n previousDocumentId: string | null,\n options: SaveDocumentRecordOptions,\n isCurrent: () => boolean,\n): Promise<PersistedDocumentRecord | null> {\n if (!persistedRecord) return null;\n if (!isCurrent()) return null;\n\n if (\n persistedRecord.syncIdentitySuperseded ||\n persistedRecord.pullContinuationSuperseded\n ) {\n // Reload the winning durable checkpoint/tail on every CAS loss, including\n // another pane's history or security-context changes. A rejected sync\n // candidate remains isolated from the live document and editor.\n const replacementDoc = await reloadSupersededDocumentState(\n state,\n isCurrent,\n persistedRecord.historyRestoreState,\n previousDocumentId,\n persistedRecord.record,\n );\n if (!replacementDoc) return null;\n return persistedRecord;\n }\n\n setReadySnapshot(\n state,\n currentDoc,\n state.snapshot.syncing,\n options.preserveSnapshotText && !persistedRecord.pullContinuationSuperseded\n ? state.snapshot.text\n : persistedRecord.record.text,\n options.preserveSnapshotStructuredFields &&\n !persistedRecord.pullContinuationSuperseded\n ? state.snapshot.structuredFields\n : undefined,\n );\n // Compaction never fails the persist that triggered it: an unexpected\n // compaction error simply keeps the tail growing until a later pass\n // succeeds, and the content stays durable either way.\n try {\n await maybeCompactDocumentHistory(state, currentDoc, options.stillCurrent);\n } catch (error) {\n state.runtime.util.log(\n `Documents: history compaction skipped: ${errorMessage(error)}`,\n );\n }\n return persistedRecord;\n}\n\nexport async function listPendingUpdates(\n state: DocumentStoreState,\n): Promise<PendingUpdateRecord[]> {\n return listPendingDocumentUpdates({\n execSql: state.runtime.infra.execSql,\n localId: state.localId,\n persistence: state.persistence,\n });\n}\n\n/**\n * Export the outgoing delta for the current edit. The base is `pendingBaseVersion`\n * (the version through which ops are already durably enqueued or synced), NOT the\n * live doc version, so any delta orphaned by a prior failed enqueue is folded back\n * in here. In the success path the marker equals the pre-edit version, so this is\n * identical to exporting \"since the last edit\".\n *\n * Initialization sets `pendingBaseVersion` whenever it sets `doc`, and every\n * caller checks `doc` first, so the marker is non-null here. Assert that rather\n * than falling back to the live version: callers invoke this AFTER mutating the\n * doc, so a live-version fallback would export an empty (post-mutation) delta and\n * silently drop the edit.\n */\nexport function pendingDeltaSinceBase(\n state: DocumentStoreState,\n doc: DocumentState,\n): Uint8Array {\n if (state.pendingBaseVersion === null) {\n throw new Error(\n \"pendingDeltaSinceBase requires an initialized pendingBaseVersion\",\n );\n }\n return exportUpdatesSince(doc, state.pendingBaseVersion);\n}\n\n/**\n * Advance the durable marker to the doc's current version. Call this ONLY after\n * an edit's delta has been durably enqueued and persisted, or after remote\n * updates have been imported (those ops are already on the server).\n */\nexport function advancePendingBaseVersion(\n state: DocumentStoreState,\n doc: DocumentState,\n): void {\n state.pendingBaseVersion = encodeVersionVector(doc);\n}\n\n/**\n * Runs a durable write either ungated — no generation: the caller serializes\n * with teardown via the write-chain drain, and the persistence implementation\n * may legitimately park inside the write WITHOUT holding the global mutation\n * mutex — or inside the serialized mutation with an in-mutex currency check:\n * a teardown (reset/discard) that already ran was ordered strictly before the\n * mutation claim and invalidated the generation, so a stale caller can no\n * longer slip its write in after the teardown's wipe; a write that wins the\n * ordering instead lands before the teardown and is wiped by it. Returns\n * whether the write ran.\n */\nexport async function withGenerationGuardedMutation(\n state: DocumentStoreState,\n expectedGeneration: DocumentStoreSyncGeneration | undefined,\n write: (execSql: ExecSql) => Promise<void>,\n): Promise<boolean> {\n if (!expectedGeneration) {\n await write(state.runtime.infra.execSql);\n return true;\n }\n\n let ran = false;\n await runSerializedSqlMutation(\n state.runtime.infra.execSql,\n async (lockedExecSql) => {\n if (!isSyncGenerationCurrent(state, expectedGeneration)) {\n return;\n }\n await write(lockedExecSql);\n ran = true;\n },\n );\n return ran;\n}\n\nexport async function enqueuePendingUpdate(\n state: DocumentStoreState,\n update: Uint8Array,\n sourceVersionVector?: string | null,\n expectedGeneration?: DocumentStoreSyncGeneration,\n): Promise<boolean> {\n const expectedDocumentId = state.record?.documentId;\n const expectedRecoveryGeneration = state.record?.recoveryGeneration ?? 0;\n if (expectedDocumentId === undefined) {\n return false;\n }\n let enqueued = false;\n const ran = await withGenerationGuardedMutation(\n state,\n expectedGeneration,\n async (execSql) => {\n enqueued = await enqueuePendingDocumentUpdate({\n execSql,\n expectedDocumentId,\n expectedRecoveryGeneration,\n localId: state.localId,\n persistence: state.persistence,\n ...(sourceVersionVector === undefined ? {} : { sourceVersionVector }),\n update,\n });\n },\n );\n return ran && enqueued;\n}\n"]}
@@ -1,8 +1,9 @@
1
1
  import { getDocumentAttachments } from "../../../data/documents/documentContent.js";
2
2
  import { detachDocumentAttachment } from "../../../workflows/blobs/detach.js";
3
- import { reclaimDocumentOrphanBlobs, resolveDocumentCreateAuthor, } from "../../../workflows/documents/index.js";
3
+ import { resolveDocumentCreateAuthor, } from "../../../workflows/documents/index.js";
4
4
  import { createRuntimePrincipalPolicyWarmer } from "../../../workflows/principals/runtimePolicyWarmer.js";
5
5
  import { deleteLocalAttachmentRecord } from "./attachmentPersistence.js";
6
+ import { runDocumentOrphanMaintenance } from "./orphanMaintenance.js";
6
7
  import { isDocumentStoreSyncGenerationCurrent, } from "./syncGeneration.js";
7
8
  import { documentSyncContextMatches } from "./syncUpdateImport.js";
8
9
  import { installDocumentWriterProjection } from "./writerProjectionGeneration.js";
@@ -71,7 +72,7 @@ async function cleanupDetachedAttachmentMarker(state, currentDoc, requestRecord,
71
72
  }
72
73
  await deleteLocalAttachmentRecord(state, marker.slotId, marker.storageKey, currentDoc);
73
74
  // Deleting the row queued its bytes for the reference-checked reclaim.
74
- void reclaimDocumentOrphanBlobs(state.runtime);
75
+ void runDocumentOrphanMaintenance(state);
75
76
  return detachedSyncContextIsCurrent(state, generation, requestRecord);
76
77
  }
77
78
  async function syncDetachedAttachmentBinding(input) {
@@ -1 +1 @@
1
- {"version":3,"file":"syncDetachedAttachments.js","sourceRoot":"","sources":["syncDetachedAttachments.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDocumentAttachments } from \"../../../data/documents/documentContent\";\nimport { detachDocumentAttachment } from \"../../../workflows/blobs/detach\";\nimport {\n type DocumentRecord,\n reclaimDocumentOrphanBlobs,\n resolveDocumentCreateAuthor,\n} from \"../../../workflows/documents\";\nimport { createRuntimePrincipalPolicyWarmer } from \"../../../workflows/principals/runtimePolicyWarmer\";\nimport { deleteLocalAttachmentRecord } from \"./attachmentPersistence\";\nimport type {\n DocumentAttachmentBinding,\n DocumentState,\n DocumentStoreState,\n} from \"./state\";\nimport {\n type DocumentStoreSyncGeneration,\n isDocumentStoreSyncGenerationCurrent,\n} from \"./syncGeneration\";\nimport { documentSyncContextMatches } from \"./syncUpdateImport\";\nimport { installDocumentWriterProjection } from \"./writerProjectionGeneration\";\n\ninterface DetachedAttachmentMarker {\n slotId: string;\n storageKey: string;\n}\n\nexport async function syncDetachedAttachmentBindings(\n state: DocumentStoreState,\n nextRecord: DocumentRecord,\n generation: DocumentStoreSyncGeneration,\n): Promise<void> {\n const currentDoc = generation.currentDoc;\n if (\n !currentDoc ||\n !detachedSyncContextIsCurrent(state, generation, nextRecord)\n ) {\n return;\n }\n\n const detachedMarkers = listDetachedAttachmentMarkers(state, currentDoc);\n if (detachedMarkers.length === 0) {\n return;\n }\n\n if (!nextRecord.documentId) {\n await cleanupDetachedAttachmentMarkers(\n state,\n currentDoc,\n nextRecord,\n detachedMarkers,\n generation,\n );\n return;\n }\n\n const remoteDocumentId = nextRecord.documentId;\n const remoteBindings =\n await state.runtime.apiClient.listDocumentAttachments(remoteDocumentId);\n if (\n !remoteBindings ||\n !detachedSyncContextIsCurrent(state, generation, nextRecord)\n ) {\n return;\n }\n\n const activeBindingBySlotId = new Map(\n remoteBindings.map((binding) => [binding.slotId, binding]),\n );\n\n for (const marker of detachedMarkers) {\n if (!detachedSyncContextIsCurrent(state, generation, nextRecord)) {\n return;\n }\n const activeBinding = activeBindingBySlotId.get(marker.slotId);\n if (activeBinding) {\n const detached = await syncDetachedAttachmentBinding({\n binding: activeBinding,\n generation,\n remoteDocumentId,\n requestRecord: nextRecord,\n state,\n });\n if (!detached) {\n return;\n }\n }\n\n const cleaned = await cleanupDetachedAttachmentMarker(\n state,\n currentDoc,\n nextRecord,\n marker,\n generation,\n );\n if (!cleaned) {\n return;\n }\n }\n}\n\nfunction detachedSyncContextIsCurrent(\n state: DocumentStoreState,\n generation: DocumentStoreSyncGeneration,\n requestRecord: DocumentRecord,\n): boolean {\n return (\n isDocumentStoreSyncGenerationCurrent(state, generation) &&\n documentSyncContextMatches(\n state.record,\n requestRecord,\n requestRecord.documentId,\n )\n );\n}\n\nfunction listDetachedAttachmentMarkers(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n): DetachedAttachmentMarker[] {\n const currentSlotIds = new Set(\n getDocumentAttachments(currentDoc).map((attachment) => attachment.slotId),\n );\n\n return Object.entries(state.attachmentStorageKeyBySlotId).flatMap(\n ([slotId, storageKey]) =>\n currentSlotIds.has(slotId) ? [] : [{ slotId, storageKey }],\n );\n}\n\nasync function cleanupDetachedAttachmentMarkers(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n requestRecord: DocumentRecord,\n markers: readonly DetachedAttachmentMarker[],\n generation: DocumentStoreSyncGeneration,\n): Promise<void> {\n for (const marker of markers) {\n if (\n !(await cleanupDetachedAttachmentMarker(\n state,\n currentDoc,\n requestRecord,\n marker,\n generation,\n ))\n ) {\n return;\n }\n }\n}\n\nasync function cleanupDetachedAttachmentMarker(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n requestRecord: DocumentRecord,\n marker: DetachedAttachmentMarker,\n generation: DocumentStoreSyncGeneration,\n): Promise<boolean> {\n if (!detachedSyncContextIsCurrent(state, generation, requestRecord)) {\n return false;\n }\n await deleteLocalAttachmentRecord(\n state,\n marker.slotId,\n marker.storageKey,\n currentDoc,\n );\n // Deleting the row queued its bytes for the reference-checked reclaim.\n void reclaimDocumentOrphanBlobs(state.runtime);\n return detachedSyncContextIsCurrent(state, generation, requestRecord);\n}\n\nasync function syncDetachedAttachmentBinding(input: {\n binding: DocumentAttachmentBinding;\n generation: DocumentStoreSyncGeneration;\n remoteDocumentId: string;\n requestRecord: DocumentRecord;\n state: DocumentStoreState;\n}): Promise<boolean> {\n const { binding, generation, requestRecord, state } = input;\n const contextIsCurrent = () =>\n detachedSyncContextIsCurrent(state, generation, requestRecord);\n if (!contextIsCurrent()) return false;\n\n const author = resolveDocumentCreateAuthor(state.runtime);\n if (!author) {\n state.runtime.util.log(\n \"Documents: skipped attachment detach because the writer context is unavailable.\",\n );\n return false;\n }\n\n const writerProjection =\n state.writerProjection?.documentId === input.remoteDocumentId\n ? state.writerProjection\n : null;\n const writerProjectionGeneration = state.writerProjectionGeneration;\n let remoteSyncBlocked = false;\n const baseDetachInput = {\n apiClient: state.runtime.apiClient,\n author,\n bindingId: binding.bindingId,\n blobId: binding.blobId,\n documentId: input.remoteDocumentId,\n execSql: generation.execSql,\n isRemoteSyncBlocked: (organizationId: string) => {\n remoteSyncBlocked =\n state.runtime.util.isRemoteSyncBlocked?.(organizationId) ?? false;\n return remoteSyncBlocked;\n },\n resolveProjectionUserKey: generation.resolveProjectionUserKey,\n slotId: binding.slotId,\n stillCurrent: contextIsCurrent,\n warmReferencedPrincipalPolicies: createRuntimePrincipalPolicyWarmer(\n state.runtime,\n ),\n };\n let detached = await detachDocumentAttachment({\n ...baseDetachInput,\n writerProjection: writerProjection ?? undefined,\n });\n if (!contextIsCurrent()) return false;\n if (!detached && writerProjection && !remoteSyncBlocked) {\n state.writerProjection = null;\n detached = await detachDocumentAttachment(baseDetachInput);\n if (!contextIsCurrent()) return false;\n }\n if (!detached) {\n return false;\n }\n\n installDocumentWriterProjection(\n state,\n detached.writerProjection,\n writerProjectionGeneration,\n );\n state.runtime.util.log(\n `Detached attachment ${binding.slotId} from document ${input.remoteDocumentId}.`,\n );\n return true;\n}\n"]}
1
+ {"version":3,"file":"syncDetachedAttachments.js","sourceRoot":"","sources":["syncDetachedAttachments.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDocumentAttachments } from \"../../../data/documents/documentContent\";\nimport { detachDocumentAttachment } from \"../../../workflows/blobs/detach\";\nimport {\n type DocumentRecord,\n resolveDocumentCreateAuthor,\n} from \"../../../workflows/documents\";\nimport { createRuntimePrincipalPolicyWarmer } from \"../../../workflows/principals/runtimePolicyWarmer\";\nimport { deleteLocalAttachmentRecord } from \"./attachmentPersistence\";\nimport { runDocumentOrphanMaintenance } from \"./orphanMaintenance\";\nimport type {\n DocumentAttachmentBinding,\n DocumentState,\n DocumentStoreState,\n} from \"./state\";\nimport {\n type DocumentStoreSyncGeneration,\n isDocumentStoreSyncGenerationCurrent,\n} from \"./syncGeneration\";\nimport { documentSyncContextMatches } from \"./syncUpdateImport\";\nimport { installDocumentWriterProjection } from \"./writerProjectionGeneration\";\n\ninterface DetachedAttachmentMarker {\n slotId: string;\n storageKey: string;\n}\n\nexport async function syncDetachedAttachmentBindings(\n state: DocumentStoreState,\n nextRecord: DocumentRecord,\n generation: DocumentStoreSyncGeneration,\n): Promise<void> {\n const currentDoc = generation.currentDoc;\n if (\n !currentDoc ||\n !detachedSyncContextIsCurrent(state, generation, nextRecord)\n ) {\n return;\n }\n\n const detachedMarkers = listDetachedAttachmentMarkers(state, currentDoc);\n if (detachedMarkers.length === 0) {\n return;\n }\n\n if (!nextRecord.documentId) {\n await cleanupDetachedAttachmentMarkers(\n state,\n currentDoc,\n nextRecord,\n detachedMarkers,\n generation,\n );\n return;\n }\n\n const remoteDocumentId = nextRecord.documentId;\n const remoteBindings =\n await state.runtime.apiClient.listDocumentAttachments(remoteDocumentId);\n if (\n !remoteBindings ||\n !detachedSyncContextIsCurrent(state, generation, nextRecord)\n ) {\n return;\n }\n\n const activeBindingBySlotId = new Map(\n remoteBindings.map((binding) => [binding.slotId, binding]),\n );\n\n for (const marker of detachedMarkers) {\n if (!detachedSyncContextIsCurrent(state, generation, nextRecord)) {\n return;\n }\n const activeBinding = activeBindingBySlotId.get(marker.slotId);\n if (activeBinding) {\n const detached = await syncDetachedAttachmentBinding({\n binding: activeBinding,\n generation,\n remoteDocumentId,\n requestRecord: nextRecord,\n state,\n });\n if (!detached) {\n return;\n }\n }\n\n const cleaned = await cleanupDetachedAttachmentMarker(\n state,\n currentDoc,\n nextRecord,\n marker,\n generation,\n );\n if (!cleaned) {\n return;\n }\n }\n}\n\nfunction detachedSyncContextIsCurrent(\n state: DocumentStoreState,\n generation: DocumentStoreSyncGeneration,\n requestRecord: DocumentRecord,\n): boolean {\n return (\n isDocumentStoreSyncGenerationCurrent(state, generation) &&\n documentSyncContextMatches(\n state.record,\n requestRecord,\n requestRecord.documentId,\n )\n );\n}\n\nfunction listDetachedAttachmentMarkers(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n): DetachedAttachmentMarker[] {\n const currentSlotIds = new Set(\n getDocumentAttachments(currentDoc).map((attachment) => attachment.slotId),\n );\n\n return Object.entries(state.attachmentStorageKeyBySlotId).flatMap(\n ([slotId, storageKey]) =>\n currentSlotIds.has(slotId) ? [] : [{ slotId, storageKey }],\n );\n}\n\nasync function cleanupDetachedAttachmentMarkers(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n requestRecord: DocumentRecord,\n markers: readonly DetachedAttachmentMarker[],\n generation: DocumentStoreSyncGeneration,\n): Promise<void> {\n for (const marker of markers) {\n if (\n !(await cleanupDetachedAttachmentMarker(\n state,\n currentDoc,\n requestRecord,\n marker,\n generation,\n ))\n ) {\n return;\n }\n }\n}\n\nasync function cleanupDetachedAttachmentMarker(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n requestRecord: DocumentRecord,\n marker: DetachedAttachmentMarker,\n generation: DocumentStoreSyncGeneration,\n): Promise<boolean> {\n if (!detachedSyncContextIsCurrent(state, generation, requestRecord)) {\n return false;\n }\n await deleteLocalAttachmentRecord(\n state,\n marker.slotId,\n marker.storageKey,\n currentDoc,\n );\n // Deleting the row queued its bytes for the reference-checked reclaim.\n void runDocumentOrphanMaintenance(state);\n return detachedSyncContextIsCurrent(state, generation, requestRecord);\n}\n\nasync function syncDetachedAttachmentBinding(input: {\n binding: DocumentAttachmentBinding;\n generation: DocumentStoreSyncGeneration;\n remoteDocumentId: string;\n requestRecord: DocumentRecord;\n state: DocumentStoreState;\n}): Promise<boolean> {\n const { binding, generation, requestRecord, state } = input;\n const contextIsCurrent = () =>\n detachedSyncContextIsCurrent(state, generation, requestRecord);\n if (!contextIsCurrent()) return false;\n\n const author = resolveDocumentCreateAuthor(state.runtime);\n if (!author) {\n state.runtime.util.log(\n \"Documents: skipped attachment detach because the writer context is unavailable.\",\n );\n return false;\n }\n\n const writerProjection =\n state.writerProjection?.documentId === input.remoteDocumentId\n ? state.writerProjection\n : null;\n const writerProjectionGeneration = state.writerProjectionGeneration;\n let remoteSyncBlocked = false;\n const baseDetachInput = {\n apiClient: state.runtime.apiClient,\n author,\n bindingId: binding.bindingId,\n blobId: binding.blobId,\n documentId: input.remoteDocumentId,\n execSql: generation.execSql,\n isRemoteSyncBlocked: (organizationId: string) => {\n remoteSyncBlocked =\n state.runtime.util.isRemoteSyncBlocked?.(organizationId) ?? false;\n return remoteSyncBlocked;\n },\n resolveProjectionUserKey: generation.resolveProjectionUserKey,\n slotId: binding.slotId,\n stillCurrent: contextIsCurrent,\n warmReferencedPrincipalPolicies: createRuntimePrincipalPolicyWarmer(\n state.runtime,\n ),\n };\n let detached = await detachDocumentAttachment({\n ...baseDetachInput,\n writerProjection: writerProjection ?? undefined,\n });\n if (!contextIsCurrent()) return false;\n if (!detached && writerProjection && !remoteSyncBlocked) {\n state.writerProjection = null;\n detached = await detachDocumentAttachment(baseDetachInput);\n if (!contextIsCurrent()) return false;\n }\n if (!detached) {\n return false;\n }\n\n installDocumentWriterProjection(\n state,\n detached.writerProjection,\n writerProjectionGeneration,\n );\n state.runtime.util.log(\n `Detached attachment ${binding.slotId} from document ${input.remoteDocumentId}.`,\n );\n return true;\n}\n"]}
@@ -1,9 +1,10 @@
1
1
  import { encodeVersionVector, exportFullHistorySnapshot, } from "../../../internal/loro/index.js";
2
- import { createDocumentWriterPublicKeyResolver, defaultDocumentsPersistence, deletePersistedDocument, reclaimDocumentOrphanBlobs, resolveDocumentCreateAuthor, syncRemoteDocument, validateDocumentSyncUpdateImports, } from "../../../workflows/documents/index.js";
2
+ import { createDocumentWriterPublicKeyResolver, deletePersistedDocument, resolveDocumentCreateAuthor, syncRemoteDocument, validateDocumentSyncUpdateImports, } from "../../../workflows/documents/index.js";
3
3
  import { createRuntimePrincipalPolicyWarmer } from "../../../workflows/principals/runtimePolicyWarmer.js";
4
4
  import { requestDocumentStoreSync } from "../registry.js";
5
5
  import { parkForAncestorRepair } from "./ancestorRepairPark.js";
6
6
  import { chainIdentityWrite } from "./identityWriteChain.js";
7
+ import { runDocumentOrphanMaintenance } from "./orphanMaintenance.js";
7
8
  import { invalidateDocumentStorePullContinuation } from "./pullContinuationInvalidation.js";
8
9
  import { markDocumentStoreRemoved, } from "./state.js";
9
10
  import { isDocumentStoreSyncGenerationCurrent, } from "./syncGeneration.js";
@@ -46,9 +47,7 @@ export async function deleteUpstreamDeletedDocument(state, generation, requestRe
46
47
  if (!deletionStarted || !removedInMutation) {
47
48
  return;
48
49
  }
49
- if (state.persistence === defaultDocumentsPersistence) {
50
- void reclaimDocumentOrphanBlobs(state.runtime);
51
- }
50
+ void runDocumentOrphanMaintenance(state);
52
51
  state.runtime.util.log(`Documents: removed local document ${state.localId} after remote deletion.`);
53
52
  });
54
53
  }
@@ -1 +1 @@
1
- {"version":3,"file":"syncRequest.js","sourceRoot":"","sources":["syncRequest.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n encodeVersionVector,\n exportFullHistorySnapshot,\n} from \"@tearleads/loro\";\nimport {\n createDocumentWriterPublicKeyResolver,\n type DocumentRecord,\n defaultDocumentsPersistence,\n deletePersistedDocument,\n type ExecSql,\n type PendingUpdateRecord,\n reclaimDocumentOrphanBlobs,\n resolveDocumentCreateAuthor,\n syncRemoteDocument,\n validateDocumentSyncUpdateImports,\n} from \"../../../workflows/documents\";\nimport { createRuntimePrincipalPolicyWarmer } from \"../../../workflows/principals/runtimePolicyWarmer\";\nimport { requestDocumentStoreSync } from \"../registry\";\nimport { parkForAncestorRepair } from \"./ancestorRepairPark\";\nimport { chainIdentityWrite } from \"./identityWriteChain\";\nimport { invalidateDocumentStorePullContinuation } from \"./pullContinuationInvalidation\";\nimport {\n type DocumentState,\n type DocumentStoreState,\n type DocumentSyncAttempt,\n type EncapsulationKeyPair,\n markDocumentStoreRemoved,\n} from \"./state\";\nimport {\n type DocumentStoreSyncGeneration,\n isDocumentStoreSyncGenerationCurrent,\n} from \"./syncGeneration\";\nimport {\n documentIncomingUpdateIsolationFailureHandler,\n documentRevalidationFailureHandler,\n documentTerminalSubmitFailureHandler,\n} from \"./syncShared\";\nimport { documentSyncContextMatches } from \"./syncUpdateImport\";\n\n/**\n * Destroying queued local edits here is settled policy, not an oversight\n * (docs/sync-edge-cases.md row 1): deletion is a privacy operation, so an\n * authoritative remote delete removes the document and its unsynced edits\n * everywhere with no preservation copy. Quarantine/export were rejected —\n * they retain or re-upload content the user deliberately destroyed.\n */\nexport async function deleteUpstreamDeletedDocument(\n state: DocumentStoreState,\n generation: DocumentStoreSyncGeneration,\n requestRecord: DocumentRecord,\n remoteDocumentId: string,\n commitPurgeProof?: (transactionExecSql: ExecSql) => Promise<void>,\n) {\n await chainIdentityWrite(state, async () => {\n if (\n !isDocumentStoreSyncGenerationCurrent(state, generation) ||\n !documentSyncContextMatches(state.record, requestRecord, remoteDocumentId)\n ) {\n return;\n }\n\n const canDeleteCapturedDocument = () =>\n isDocumentStoreSyncGenerationCurrent(state, generation) &&\n documentSyncContextMatches(state.record, requestRecord, remoteDocumentId);\n let removedInMutation = false;\n const deletionStarted = await deletePersistedDocument({\n beforeDeleteInTransaction: commitPurgeProof,\n canStartDurableMutation: canDeleteCapturedDocument,\n documentProjectors: state.runtime.infra.documentProjectors,\n execSql: generation.execSql,\n expectedRecord: requestRecord,\n localId: requestRecord.id,\n // Invalidate the store generation INSIDE the deletion mutation: a\n // terminal failure handler queued behind it then observes the stale\n // generation and cannot resurrect a failure row the deletes removed.\n onDeletedInMutation: () => {\n if (canDeleteCapturedDocument()) {\n markDocumentStoreRemoved(state);\n removedInMutation = true;\n }\n },\n persistence: state.persistence,\n });\n if (!deletionStarted || !removedInMutation) {\n return;\n }\n if (state.persistence === defaultDocumentsPersistence) {\n void reclaimDocumentOrphanBlobs(state.runtime);\n }\n state.runtime.util.log(\n `Documents: removed local document ${state.localId} after remote deletion.`,\n );\n });\n}\n\nfunction createDocumentSyncAttempt(input: {\n currentRecord: DocumentRecord;\n outgoingUpdateCount: number;\n requestedPullContinuation: DocumentStoreState[\"pullContinuation\"];\n synced: DocumentSyncAttempt[\"synced\"];\n writerProjectionGeneration: number;\n}): DocumentSyncAttempt {\n return {\n consumedPullContinuation:\n input.synced.plan.request.pullCursor === undefined\n ? null\n : input.requestedPullContinuation,\n outgoingUpdateCount: input.outgoingUpdateCount,\n requestRecord: input.currentRecord,\n synced: input.synced,\n writerProjectionGeneration: input.writerProjectionGeneration,\n };\n}\n\ninterface RequestRemoteDocumentSyncInput {\n allowRecoveryBaseline?: boolean | undefined;\n currentDoc: DocumentState;\n currentRecord: DocumentRecord;\n encapsulationKeyPair: EncapsulationKeyPair;\n generation: DocumentStoreSyncGeneration;\n onOutgoingUpdatesMaterialized?:\n | ((updateIds: readonly string[]) => void)\n | undefined;\n pendingUpdates: PendingUpdateRecord[];\n queuedUpdateCount?: number | undefined;\n state: DocumentStoreState;\n unavailableWriterLogMessage: string;\n}\n\nfunction runRemoteDocumentSync(\n input: RequestRemoteDocumentSyncInput,\n author: NonNullable<ReturnType<typeof resolveDocumentCreateAuthor>>,\n documentId: string,\n requestedPullContinuation: DocumentStoreState[\"pullContinuation\"],\n) {\n const { currentDoc, currentRecord, generation, pendingUpdates, state } =\n input;\n const runtime = state.runtime;\n const writerProjectionGenerationAtStart = state.writerProjectionGeneration;\n state.awaitingAncestorRepair = false;\n return syncRemoteDocument({\n apiClient: runtime.apiClient,\n author,\n // Heal a stale content-key bundle from this full-history snapshot. Rotation\n // settlement disables this: it may submit only the ordinary local stream.\n ...(input.allowRecoveryBaseline === false\n ? {}\n : {\n buildRotationSnapshot: async () =>\n exportFullHistorySnapshot(currentDoc),\n }),\n documentId,\n execSql: runtime.infra.execSql,\n isRemoteSyncBlocked: runtime.util.isRemoteSyncBlocked,\n localVersionVector: encodeVersionVector(currentDoc),\n minLsn: currentRecord.lastCommitLsn ?? undefined,\n onRemoteDocumentDeleted: ({ commitPurgeProof, documentId }) =>\n deleteUpstreamDeletedDocument(\n state,\n generation,\n currentRecord,\n documentId,\n commitPurgeProof,\n ),\n onSyncAbandoned: (reason) => {\n if (!isDocumentStoreSyncGenerationCurrent(state, generation)) return;\n const hintLandedMidPass = parkForAncestorRepair(\n state,\n reason,\n writerProjectionGenerationAtStart,\n );\n if (hintLandedMidPass) requestDocumentStoreSync(state);\n },\n onSyncTrace: (line) => runtime.util.log(`Documents: ${line}`),\n onReadOnlyProjectionFailure: documentRevalidationFailureHandler(\n state,\n generation,\n ),\n onIncomingUpdateIsolationFailure:\n documentIncomingUpdateIsolationFailureHandler(state, generation),\n onOutgoingUpdatesMaterialized: input.onOutgoingUpdatesMaterialized,\n onPullContinuationInvalidated: (continuation) =>\n invalidateDocumentStorePullContinuation({\n continuation,\n currentRecord,\n generation,\n state,\n }),\n onTerminalSubmitFailure: documentTerminalSubmitFailureHandler(\n state,\n generation,\n ),\n pendingUpdates,\n persistedState: currentRecord,\n pullContinuation: requestedPullContinuation ?? undefined,\n rekeyPendingUpdate: state.persistence.rekeyPendingUpdate,\n resolveProjectionUserKey: state.resolveProjectionUserKey,\n resolveWriterPublicKey: createDocumentWriterPublicKeyResolver({\n logPrefix: \"Documents\",\n runtime,\n writerKeyLabel: \"writer key\",\n }),\n stillCurrent: () => isDocumentStoreSyncGenerationCurrent(state, generation),\n targetSecretKey: input.encapsulationKeyPair.secretKey,\n validateIncomingUpdates: (result) =>\n validateDocumentSyncUpdateImports({\n currentDocument: currentDoc,\n decryptedUpdates: result.decryptedUpdates,\n responseUpdates: result.response.updates,\n }),\n warmReferencedPrincipalPolicies:\n createRuntimePrincipalPolicyWarmer(runtime),\n writerProjection:\n state.writerProjection?.documentId === currentRecord.documentId\n ? state.writerProjection\n : undefined,\n });\n}\n\nexport async function requestRemoteDocumentSync(\n input: RequestRemoteDocumentSyncInput,\n): Promise<DocumentSyncAttempt | null> {\n const {\n currentRecord,\n generation,\n pendingUpdates,\n state,\n unavailableWriterLogMessage,\n } = input;\n if (!isDocumentStoreSyncGenerationCurrent(state, generation)) return null;\n // Captured before the network round trip: a hint that lands meanwhile\n // must keep the response's projection out of the cache.\n const writerProjectionGeneration = state.writerProjectionGeneration;\n\n const runtime = state.runtime;\n if (!currentRecord.documentId) return null;\n\n const requestedPullContinuation = state.pullContinuation;\n\n const author = resolveDocumentCreateAuthor(runtime);\n if (!author) {\n runtime.util.log(unavailableWriterLogMessage);\n return null;\n }\n\n const synced = await runRemoteDocumentSync(\n input,\n author,\n currentRecord.documentId,\n requestedPullContinuation,\n );\n if (!isDocumentStoreSyncGenerationCurrent(state, generation)) return null;\n if (!synced) return null;\n\n const outgoingUpdateCount = input.queuedUpdateCount ?? pendingUpdates.length;\n return createDocumentSyncAttempt({\n currentRecord,\n outgoingUpdateCount,\n requestedPullContinuation,\n synced,\n writerProjectionGeneration,\n });\n}\n"]}
1
+ {"version":3,"file":"syncRequest.js","sourceRoot":"","sources":["syncRequest.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,qCAAqC,EAErC,uBAAuB,EAGvB,2BAA2B,EAC3B,kBAAkB,EAClB,iCAAiC,GAClC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,kCAAkC,EAAE,MAAM,mDAAmD,CAAC;AACvG,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,uCAAuC,EAAE,MAAM,gCAAgC,CAAC;AACzF,OAAO,EAKL,wBAAwB,GACzB,MAAM,SAAS,CAAC;AACjB,OAAO,EAEL,oCAAoC,GACrC,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,6CAA6C,EAC7C,kCAAkC,EAClC,oCAAoC,GACrC,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAEhE;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B,CACjD,KAAyB,EACzB,UAAuC,EACvC,aAA6B,EAC7B,gBAAwB,EACxB,gBAAiE;IAEjE,MAAM,kBAAkB,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;QACzC,IACE,CAAC,oCAAoC,CAAC,KAAK,EAAE,UAAU,CAAC;YACxD,CAAC,0BAA0B,CAAC,KAAK,CAAC,MAAM,EAAE,aAAa,EAAE,gBAAgB,CAAC,EAC1E,CAAC;YACD,OAAO;QACT,CAAC;QAED,MAAM,yBAAyB,GAAG,GAAG,EAAE,CACrC,oCAAoC,CAAC,KAAK,EAAE,UAAU,CAAC;YACvD,0BAA0B,CAAC,KAAK,CAAC,MAAM,EAAE,aAAa,EAAE,gBAAgB,CAAC,CAAC;QAC5E,IAAI,iBAAiB,GAAG,KAAK,CAAC;QAC9B,MAAM,eAAe,GAAG,MAAM,uBAAuB,CAAC;YACpD,yBAAyB,EAAE,gBAAgB;YAC3C,uBAAuB,EAAE,yBAAyB;YAClD,kBAAkB,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,kBAAkB;YAC1D,OAAO,EAAE,UAAU,CAAC,OAAO;YAC3B,cAAc,EAAE,aAAa;YAC7B,OAAO,EAAE,aAAa,CAAC,EAAE;YACzB,kEAAkE;YAClE,oEAAoE;YACpE,qEAAqE;YACrE,mBAAmB,EAAE,GAAG,EAAE;gBACxB,IAAI,yBAAyB,EAAE,EAAE,CAAC;oBAChC,wBAAwB,CAAC,KAAK,CAAC,CAAC;oBAChC,iBAAiB,GAAG,IAAI,CAAC;gBAC3B,CAAC;YACH,CAAC;YACD,WAAW,EAAE,KAAK,CAAC,WAAW;SAC/B,CAAC,CAAC;QACH,IAAI,CAAC,eAAe,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC3C,OAAO;QACT,CAAC;QACD,KAAK,4BAA4B,CAAC,KAAK,CAAC,CAAC;QACzC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CACpB,qCAAqC,KAAK,CAAC,OAAO,yBAAyB,CAC5E,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,yBAAyB,CAAC,KAMlC;IACC,OAAO;QACL,wBAAwB,EACtB,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS;YAChD,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,KAAK,CAAC,yBAAyB;QACrC,mBAAmB,EAAE,KAAK,CAAC,mBAAmB;QAC9C,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,0BAA0B,EAAE,KAAK,CAAC,0BAA0B;KAC7D,CAAC;AACJ,CAAC;AAiBD,SAAS,qBAAqB,CAC5B,KAAqC,EACrC,MAAmE,EACnE,UAAkB,EAClB,yBAAiE;IAEjE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,KAAK,EAAE,GACpE,KAAK,CAAC;IACR,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,MAAM,iCAAiC,GAAG,KAAK,CAAC,0BAA0B,CAAC;IAC3E,KAAK,CAAC,sBAAsB,GAAG,KAAK,CAAC;IACrC,OAAO,kBAAkB,CAAC;QACxB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,MAAM;QACN,4EAA4E;QAC5E,0EAA0E;QAC1E,GAAG,CAAC,KAAK,CAAC,qBAAqB,KAAK,KAAK;YACvC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC;gBACE,qBAAqB,EAAE,KAAK,IAAI,EAAE,CAChC,yBAAyB,CAAC,UAAU,CAAC;aACxC,CAAC;QACN,UAAU;QACV,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO;QAC9B,mBAAmB,EAAE,OAAO,CAAC,IAAI,CAAC,mBAAmB;QACrD,kBAAkB,EAAE,mBAAmB,CAAC,UAAU,CAAC;QACnD,MAAM,EAAE,aAAa,CAAC,aAAa,IAAI,SAAS;QAChD,uBAAuB,EAAE,CAAC,EAAE,gBAAgB,EAAE,UAAU,EAAE,EAAE,EAAE,CAC5D,6BAA6B,CAC3B,KAAK,EACL,UAAU,EACV,aAAa,EACb,UAAU,EACV,gBAAgB,CACjB;QACH,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE;YAC1B,IAAI,CAAC,oCAAoC,CAAC,KAAK,EAAE,UAAU,CAAC;gBAAE,OAAO;YACrE,MAAM,iBAAiB,GAAG,qBAAqB,CAC7C,KAAK,EACL,MAAM,EACN,iCAAiC,CAClC,CAAC;YACF,IAAI,iBAAiB;gBAAE,wBAAwB,CAAC,KAAK,CAAC,CAAC;QACzD,CAAC;QACD,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC;QAC7D,2BAA2B,EAAE,kCAAkC,CAC7D,KAAK,EACL,UAAU,CACX;QACD,gCAAgC,EAC9B,6CAA6C,CAAC,KAAK,EAAE,UAAU,CAAC;QAClE,6BAA6B,EAAE,KAAK,CAAC,6BAA6B;QAClE,6BAA6B,EAAE,CAAC,YAAY,EAAE,EAAE,CAC9C,uCAAuC,CAAC;YACtC,YAAY;YACZ,aAAa;YACb,UAAU;YACV,KAAK;SACN,CAAC;QACJ,uBAAuB,EAAE,oCAAoC,CAC3D,KAAK,EACL,UAAU,CACX;QACD,cAAc;QACd,cAAc,EAAE,aAAa;QAC7B,gBAAgB,EAAE,yBAAyB,IAAI,SAAS;QACxD,kBAAkB,EAAE,KAAK,CAAC,WAAW,CAAC,kBAAkB;QACxD,wBAAwB,EAAE,KAAK,CAAC,wBAAwB;QACxD,sBAAsB,EAAE,qCAAqC,CAAC;YAC5D,SAAS,EAAE,WAAW;YACtB,OAAO;YACP,cAAc,EAAE,YAAY;SAC7B,CAAC;QACF,YAAY,EAAE,GAAG,EAAE,CAAC,oCAAoC,CAAC,KAAK,EAAE,UAAU,CAAC;QAC3E,eAAe,EAAE,KAAK,CAAC,oBAAoB,CAAC,SAAS;QACrD,uBAAuB,EAAE,CAAC,MAAM,EAAE,EAAE,CAClC,iCAAiC,CAAC;YAChC,eAAe,EAAE,UAAU;YAC3B,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;YACzC,eAAe,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO;SACzC,CAAC;QACJ,+BAA+B,EAC7B,kCAAkC,CAAC,OAAO,CAAC;QAC7C,gBAAgB,EACd,KAAK,CAAC,gBAAgB,EAAE,UAAU,KAAK,aAAa,CAAC,UAAU;YAC7D,CAAC,CAAC,KAAK,CAAC,gBAAgB;YACxB,CAAC,CAAC,SAAS;KAChB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,KAAqC;IAErC,MAAM,EACJ,aAAa,EACb,UAAU,EACV,cAAc,EACd,KAAK,EACL,2BAA2B,GAC5B,GAAG,KAAK,CAAC;IACV,IAAI,CAAC,oCAAoC,CAAC,KAAK,EAAE,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1E,sEAAsE;IACtE,wDAAwD;IACxD,MAAM,0BAA0B,GAAG,KAAK,CAAC,0BAA0B,CAAC;IAEpE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,IAAI,CAAC,aAAa,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAE3C,MAAM,yBAAyB,GAAG,KAAK,CAAC,gBAAgB,CAAC;IAEzD,MAAM,MAAM,GAAG,2BAA2B,CAAC,OAAO,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,qBAAqB,CACxC,KAAK,EACL,MAAM,EACN,aAAa,CAAC,UAAU,EACxB,yBAAyB,CAC1B,CAAC;IACF,IAAI,CAAC,oCAAoC,CAAC,KAAK,EAAE,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1E,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,mBAAmB,GAAG,KAAK,CAAC,iBAAiB,IAAI,cAAc,CAAC,MAAM,CAAC;IAC7E,OAAO,yBAAyB,CAAC;QAC/B,aAAa;QACb,mBAAmB;QACnB,yBAAyB;QACzB,MAAM;QACN,0BAA0B;KAC3B,CAAC,CAAC;AACL,CAAC","sourcesContent":["import {\n encodeVersionVector,\n exportFullHistorySnapshot,\n} from \"@tearleads/loro\";\nimport {\n createDocumentWriterPublicKeyResolver,\n type DocumentRecord,\n deletePersistedDocument,\n type ExecSql,\n type PendingUpdateRecord,\n resolveDocumentCreateAuthor,\n syncRemoteDocument,\n validateDocumentSyncUpdateImports,\n} from \"../../../workflows/documents\";\nimport { createRuntimePrincipalPolicyWarmer } from \"../../../workflows/principals/runtimePolicyWarmer\";\nimport { requestDocumentStoreSync } from \"../registry\";\nimport { parkForAncestorRepair } from \"./ancestorRepairPark\";\nimport { chainIdentityWrite } from \"./identityWriteChain\";\nimport { runDocumentOrphanMaintenance } from \"./orphanMaintenance\";\nimport { invalidateDocumentStorePullContinuation } from \"./pullContinuationInvalidation\";\nimport {\n type DocumentState,\n type DocumentStoreState,\n type DocumentSyncAttempt,\n type EncapsulationKeyPair,\n markDocumentStoreRemoved,\n} from \"./state\";\nimport {\n type DocumentStoreSyncGeneration,\n isDocumentStoreSyncGenerationCurrent,\n} from \"./syncGeneration\";\nimport {\n documentIncomingUpdateIsolationFailureHandler,\n documentRevalidationFailureHandler,\n documentTerminalSubmitFailureHandler,\n} from \"./syncShared\";\nimport { documentSyncContextMatches } from \"./syncUpdateImport\";\n\n/**\n * Destroying queued local edits here is settled policy, not an oversight\n * (docs/sync-edge-cases.md row 1): deletion is a privacy operation, so an\n * authoritative remote delete removes the document and its unsynced edits\n * everywhere with no preservation copy. Quarantine/export were rejected —\n * they retain or re-upload content the user deliberately destroyed.\n */\nexport async function deleteUpstreamDeletedDocument(\n state: DocumentStoreState,\n generation: DocumentStoreSyncGeneration,\n requestRecord: DocumentRecord,\n remoteDocumentId: string,\n commitPurgeProof?: (transactionExecSql: ExecSql) => Promise<void>,\n) {\n await chainIdentityWrite(state, async () => {\n if (\n !isDocumentStoreSyncGenerationCurrent(state, generation) ||\n !documentSyncContextMatches(state.record, requestRecord, remoteDocumentId)\n ) {\n return;\n }\n\n const canDeleteCapturedDocument = () =>\n isDocumentStoreSyncGenerationCurrent(state, generation) &&\n documentSyncContextMatches(state.record, requestRecord, remoteDocumentId);\n let removedInMutation = false;\n const deletionStarted = await deletePersistedDocument({\n beforeDeleteInTransaction: commitPurgeProof,\n canStartDurableMutation: canDeleteCapturedDocument,\n documentProjectors: state.runtime.infra.documentProjectors,\n execSql: generation.execSql,\n expectedRecord: requestRecord,\n localId: requestRecord.id,\n // Invalidate the store generation INSIDE the deletion mutation: a\n // terminal failure handler queued behind it then observes the stale\n // generation and cannot resurrect a failure row the deletes removed.\n onDeletedInMutation: () => {\n if (canDeleteCapturedDocument()) {\n markDocumentStoreRemoved(state);\n removedInMutation = true;\n }\n },\n persistence: state.persistence,\n });\n if (!deletionStarted || !removedInMutation) {\n return;\n }\n void runDocumentOrphanMaintenance(state);\n state.runtime.util.log(\n `Documents: removed local document ${state.localId} after remote deletion.`,\n );\n });\n}\n\nfunction createDocumentSyncAttempt(input: {\n currentRecord: DocumentRecord;\n outgoingUpdateCount: number;\n requestedPullContinuation: DocumentStoreState[\"pullContinuation\"];\n synced: DocumentSyncAttempt[\"synced\"];\n writerProjectionGeneration: number;\n}): DocumentSyncAttempt {\n return {\n consumedPullContinuation:\n input.synced.plan.request.pullCursor === undefined\n ? null\n : input.requestedPullContinuation,\n outgoingUpdateCount: input.outgoingUpdateCount,\n requestRecord: input.currentRecord,\n synced: input.synced,\n writerProjectionGeneration: input.writerProjectionGeneration,\n };\n}\n\ninterface RequestRemoteDocumentSyncInput {\n allowRecoveryBaseline?: boolean | undefined;\n currentDoc: DocumentState;\n currentRecord: DocumentRecord;\n encapsulationKeyPair: EncapsulationKeyPair;\n generation: DocumentStoreSyncGeneration;\n onOutgoingUpdatesMaterialized?:\n | ((updateIds: readonly string[]) => void)\n | undefined;\n pendingUpdates: PendingUpdateRecord[];\n queuedUpdateCount?: number | undefined;\n state: DocumentStoreState;\n unavailableWriterLogMessage: string;\n}\n\nfunction runRemoteDocumentSync(\n input: RequestRemoteDocumentSyncInput,\n author: NonNullable<ReturnType<typeof resolveDocumentCreateAuthor>>,\n documentId: string,\n requestedPullContinuation: DocumentStoreState[\"pullContinuation\"],\n) {\n const { currentDoc, currentRecord, generation, pendingUpdates, state } =\n input;\n const runtime = state.runtime;\n const writerProjectionGenerationAtStart = state.writerProjectionGeneration;\n state.awaitingAncestorRepair = false;\n return syncRemoteDocument({\n apiClient: runtime.apiClient,\n author,\n // Heal a stale content-key bundle from this full-history snapshot. Rotation\n // settlement disables this: it may submit only the ordinary local stream.\n ...(input.allowRecoveryBaseline === false\n ? {}\n : {\n buildRotationSnapshot: async () =>\n exportFullHistorySnapshot(currentDoc),\n }),\n documentId,\n execSql: runtime.infra.execSql,\n isRemoteSyncBlocked: runtime.util.isRemoteSyncBlocked,\n localVersionVector: encodeVersionVector(currentDoc),\n minLsn: currentRecord.lastCommitLsn ?? undefined,\n onRemoteDocumentDeleted: ({ commitPurgeProof, documentId }) =>\n deleteUpstreamDeletedDocument(\n state,\n generation,\n currentRecord,\n documentId,\n commitPurgeProof,\n ),\n onSyncAbandoned: (reason) => {\n if (!isDocumentStoreSyncGenerationCurrent(state, generation)) return;\n const hintLandedMidPass = parkForAncestorRepair(\n state,\n reason,\n writerProjectionGenerationAtStart,\n );\n if (hintLandedMidPass) requestDocumentStoreSync(state);\n },\n onSyncTrace: (line) => runtime.util.log(`Documents: ${line}`),\n onReadOnlyProjectionFailure: documentRevalidationFailureHandler(\n state,\n generation,\n ),\n onIncomingUpdateIsolationFailure:\n documentIncomingUpdateIsolationFailureHandler(state, generation),\n onOutgoingUpdatesMaterialized: input.onOutgoingUpdatesMaterialized,\n onPullContinuationInvalidated: (continuation) =>\n invalidateDocumentStorePullContinuation({\n continuation,\n currentRecord,\n generation,\n state,\n }),\n onTerminalSubmitFailure: documentTerminalSubmitFailureHandler(\n state,\n generation,\n ),\n pendingUpdates,\n persistedState: currentRecord,\n pullContinuation: requestedPullContinuation ?? undefined,\n rekeyPendingUpdate: state.persistence.rekeyPendingUpdate,\n resolveProjectionUserKey: state.resolveProjectionUserKey,\n resolveWriterPublicKey: createDocumentWriterPublicKeyResolver({\n logPrefix: \"Documents\",\n runtime,\n writerKeyLabel: \"writer key\",\n }),\n stillCurrent: () => isDocumentStoreSyncGenerationCurrent(state, generation),\n targetSecretKey: input.encapsulationKeyPair.secretKey,\n validateIncomingUpdates: (result) =>\n validateDocumentSyncUpdateImports({\n currentDocument: currentDoc,\n decryptedUpdates: result.decryptedUpdates,\n responseUpdates: result.response.updates,\n }),\n warmReferencedPrincipalPolicies:\n createRuntimePrincipalPolicyWarmer(runtime),\n writerProjection:\n state.writerProjection?.documentId === currentRecord.documentId\n ? state.writerProjection\n : undefined,\n });\n}\n\nexport async function requestRemoteDocumentSync(\n input: RequestRemoteDocumentSyncInput,\n): Promise<DocumentSyncAttempt | null> {\n const {\n currentRecord,\n generation,\n pendingUpdates,\n state,\n unavailableWriterLogMessage,\n } = input;\n if (!isDocumentStoreSyncGenerationCurrent(state, generation)) return null;\n // Captured before the network round trip: a hint that lands meanwhile\n // must keep the response's projection out of the cache.\n const writerProjectionGeneration = state.writerProjectionGeneration;\n\n const runtime = state.runtime;\n if (!currentRecord.documentId) return null;\n\n const requestedPullContinuation = state.pullContinuation;\n\n const author = resolveDocumentCreateAuthor(runtime);\n if (!author) {\n runtime.util.log(unavailableWriterLogMessage);\n return null;\n }\n\n const synced = await runRemoteDocumentSync(\n input,\n author,\n currentRecord.documentId,\n requestedPullContinuation,\n );\n if (!isDocumentStoreSyncGenerationCurrent(state, generation)) return null;\n if (!synced) return null;\n\n const outgoingUpdateCount = input.queuedUpdateCount ?? pendingUpdates.length;\n return createDocumentSyncAttempt({\n currentRecord,\n outgoingUpdateCount,\n requestedPullContinuation,\n synced,\n writerProjectionGeneration,\n });\n}\n"]}
@@ -5,6 +5,7 @@ export async function deleteContainerState(input) {
5
5
  if (isRemoteContainer) {
6
6
  const deletedRemoteContainer = await deleteRemoteContainer({
7
7
  containerId: input.containerState.container.id,
8
+ requiredAncestorId: input.requiredAncestorId,
8
9
  organizationId: input.containerState.container.organizationId,
9
10
  runtime: input.runtime,
10
11
  });
@@ -22,6 +23,7 @@ export async function deleteContainerState(input) {
22
23
  updatedAt: deletedAt ?? new Date().toISOString(),
23
24
  },
24
25
  ], {
26
+ beforeDeleteInTransaction: input.beforeDeleteInTransaction,
25
27
  expectedContainers: [
26
28
  { containerId, expectedContainer: input.containerState.container },
27
29
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"delete.js","sourceRoot":"","sources":["delete.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AASjD,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,KAK1C;IACC,MAAM,iBAAiB,GAAG,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC1E,IAAI,SAA6B,CAAC;IAElC,IAAI,iBAAiB,EAAE,CAAC;QACtB,MAAM,sBAAsB,GAAG,MAAM,qBAAqB,CAAC;YACzD,WAAW,EAAE,KAAK,CAAC,cAAc,CAAC,SAAS,CAAC,EAAE;YAC9C,cAAc,EAAE,KAAK,CAAC,cAAc,CAAC,SAAS,CAAC,cAAc;YAC7D,OAAO,EAAE,KAAK,CAAC,OAAO;SACvB,CAAC,CAAC;QACH,IAAI,CAAC,sBAAsB,EAAE,CAAC;YAC5B,OAAO,eAAe,CAAC;QACzB,CAAC;QACD,SAAS,GAAG,sBAAsB,CAAC,SAAS,CAAC;IAC/C,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;IAC5C,MAAM,WAAW,GAAG,KAAK,CAAC,cAAc,CAAC,SAAS,CAAC,EAAE,CAAC;IACtD,MAAM,mBAAmB,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,gBAAgB,CAClE,OAAO,EACP;QACE;YACE,WAAW;YACX,MAAM,EAAE,SAAS;YACjB,SAAS,EAAE,SAAS,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACjD;KACF,EACD;QACE,kBAAkB,EAAE;YAClB,EAAE,WAAW,EAAE,iBAAiB,EAAE,KAAK,CAAC,cAAc,CAAC,SAAS,EAAE;SACnE;QACD,YAAY,EAAE,KAAK,CAAC,YAAY;KACjC,CACF,CAAC;IACF,IAAI,mBAAmB,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,SAAS,CAAC;IAChE,OAAO,iBAAiB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,gBAAgB,CAAC;AACjE,CAAC","sourcesContent":["import type { ContainerContentsPersistence } from \"../containerPersistence\";\nimport type { ContainerState } from \"../remoteHydration\";\nimport { deleteRemoteContainer } from \"./remote\";\nimport type { ContainerWorkflowRuntime } from \"./types\";\n\ntype DeleteContainerStateResult =\n | \"deleted\"\n | \"local-conflict\"\n | \"remote-deleted\"\n | \"remote-failed\";\n\nexport async function deleteContainerState(input: {\n containerState: ContainerState;\n persistence: ContainerContentsPersistence;\n runtime: ContainerWorkflowRuntime;\n stillCurrent?: (() => boolean) | undefined;\n}): Promise<DeleteContainerStateResult> {\n const isRemoteContainer = Boolean(input.containerState.record.documentId);\n let deletedAt: string | undefined;\n\n if (isRemoteContainer) {\n const deletedRemoteContainer = await deleteRemoteContainer({\n containerId: input.containerState.container.id,\n organizationId: input.containerState.container.organizationId,\n runtime: input.runtime,\n });\n if (!deletedRemoteContainer) {\n return \"remote-failed\";\n }\n deletedAt = deletedRemoteContainer.deletedAt;\n }\n const execSql = input.runtime.infra.execSql;\n const containerId = input.containerState.container.id;\n const deletedContainerIds = await input.persistence.deleteContainers(\n execSql,\n [\n {\n containerId,\n reason: \"deleted\",\n updatedAt: deletedAt ?? new Date().toISOString(),\n },\n ],\n {\n expectedContainers: [\n { containerId, expectedContainer: input.containerState.container },\n ],\n stillCurrent: input.stillCurrent,\n },\n );\n if (deletedContainerIds.includes(containerId)) return \"deleted\";\n return isRemoteContainer ? \"remote-deleted\" : \"local-conflict\";\n}\n"]}
1
+ {"version":3,"file":"delete.js","sourceRoot":"","sources":["delete.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AASjD,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,KAS1C;IACC,MAAM,iBAAiB,GAAG,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC1E,IAAI,SAA6B,CAAC;IAElC,IAAI,iBAAiB,EAAE,CAAC;QACtB,MAAM,sBAAsB,GAAG,MAAM,qBAAqB,CAAC;YACzD,WAAW,EAAE,KAAK,CAAC,cAAc,CAAC,SAAS,CAAC,EAAE;YAC9C,kBAAkB,EAAE,KAAK,CAAC,kBAAkB;YAC5C,cAAc,EAAE,KAAK,CAAC,cAAc,CAAC,SAAS,CAAC,cAAc;YAC7D,OAAO,EAAE,KAAK,CAAC,OAAO;SACvB,CAAC,CAAC;QACH,IAAI,CAAC,sBAAsB,EAAE,CAAC;YAC5B,OAAO,eAAe,CAAC;QACzB,CAAC;QACD,SAAS,GAAG,sBAAsB,CAAC,SAAS,CAAC;IAC/C,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;IAC5C,MAAM,WAAW,GAAG,KAAK,CAAC,cAAc,CAAC,SAAS,CAAC,EAAE,CAAC;IACtD,MAAM,mBAAmB,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,gBAAgB,CAClE,OAAO,EACP;QACE;YACE,WAAW;YACX,MAAM,EAAE,SAAS;YACjB,SAAS,EAAE,SAAS,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACjD;KACF,EACD;QACE,yBAAyB,EAAE,KAAK,CAAC,yBAAyB;QAC1D,kBAAkB,EAAE;YAClB,EAAE,WAAW,EAAE,iBAAiB,EAAE,KAAK,CAAC,cAAc,CAAC,SAAS,EAAE;SACnE;QACD,YAAY,EAAE,KAAK,CAAC,YAAY;KACjC,CACF,CAAC;IACF,IAAI,mBAAmB,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,SAAS,CAAC;IAChE,OAAO,iBAAiB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,gBAAgB,CAAC;AACjE,CAAC","sourcesContent":["import type { ContainerContentsPersistence } from \"../containerPersistence\";\nimport type { ContainerState } from \"../remoteHydration\";\nimport { deleteRemoteContainer } from \"./remote\";\nimport type { ContainerWorkflowRuntime } from \"./types\";\n\ntype DeleteContainerStateResult =\n | \"deleted\"\n | \"local-conflict\"\n | \"remote-deleted\"\n | \"remote-failed\";\n\nexport async function deleteContainerState(input: {\n beforeDeleteInTransaction?: NonNullable<\n Parameters<ContainerContentsPersistence[\"deleteContainers\"]>[2]\n >[\"beforeDeleteInTransaction\"];\n containerState: ContainerState;\n persistence: ContainerContentsPersistence;\n requiredAncestorId?: string | undefined;\n runtime: ContainerWorkflowRuntime;\n stillCurrent?: (() => boolean) | undefined;\n}): Promise<DeleteContainerStateResult> {\n const isRemoteContainer = Boolean(input.containerState.record.documentId);\n let deletedAt: string | undefined;\n\n if (isRemoteContainer) {\n const deletedRemoteContainer = await deleteRemoteContainer({\n containerId: input.containerState.container.id,\n requiredAncestorId: input.requiredAncestorId,\n organizationId: input.containerState.container.organizationId,\n runtime: input.runtime,\n });\n if (!deletedRemoteContainer) {\n return \"remote-failed\";\n }\n deletedAt = deletedRemoteContainer.deletedAt;\n }\n const execSql = input.runtime.infra.execSql;\n const containerId = input.containerState.container.id;\n const deletedContainerIds = await input.persistence.deleteContainers(\n execSql,\n [\n {\n containerId,\n reason: \"deleted\",\n updatedAt: deletedAt ?? new Date().toISOString(),\n },\n ],\n {\n beforeDeleteInTransaction: input.beforeDeleteInTransaction,\n expectedContainers: [\n { containerId, expectedContainer: input.containerState.container },\n ],\n stillCurrent: input.stillCurrent,\n },\n );\n if (deletedContainerIds.includes(containerId)) return \"deleted\";\n return isRemoteContainer ? \"remote-deleted\" : \"local-conflict\";\n}\n"]}
@@ -0,0 +1,49 @@
1
+ import { hasUnsettledDocumentPlacement } from "../../../data/persistence/container-contents/documentPurgePlacement.js";
2
+ function guardedContainerIds(input) {
3
+ const guardedIds = new Set();
4
+ const seen = new Set();
5
+ let currentId = input.containerId;
6
+ while (true) {
7
+ if (!currentId || seen.has(currentId))
8
+ throw changedScope();
9
+ seen.add(currentId);
10
+ guardedIds.add(currentId);
11
+ for (const id of input.verifiedAncestorsById.get(currentId) ?? [])
12
+ guardedIds.add(id);
13
+ const expected = input.containersById.get(currentId);
14
+ if (!expected)
15
+ throw changedScope();
16
+ if (currentId === input.rootContainerId)
17
+ break;
18
+ currentId = expected.container.parentId;
19
+ }
20
+ return guardedIds;
21
+ }
22
+ /** Called inside the document deletion transaction; performs no remote I/O. */
23
+ export async function assertLocalPurgeScope(input) {
24
+ if (input.localId !== undefined &&
25
+ (await hasUnsettledDocumentPlacement(input.execSql, input.localId)))
26
+ throw changedScope();
27
+ const guardedIds = guardedContainerIds(input);
28
+ // Include every known signed ancestor as well as the local selection chain.
29
+ // A dishonest listing can give these chains different intermediate nodes.
30
+ for (const id of guardedIds) {
31
+ const expected = input.containersById.get(id);
32
+ if (!expected)
33
+ continue;
34
+ const current = await input.persistence.loadContainerMetadataState(input.execSql, id);
35
+ if (!current?.record ||
36
+ current.container.parentId !== expected.container.parentId ||
37
+ current.container.organizationId !== expected.container.organizationId ||
38
+ current.record.documentId !== expected.record.documentId)
39
+ throw changedScope();
40
+ }
41
+ const pending = await input.persistence.listUnsyncedMoveIntents(input.execSql);
42
+ if (pending.some((intent) => guardedIds.has(intent.containerId)) ||
43
+ input.stillCurrent?.() === false)
44
+ throw changedScope();
45
+ }
46
+ function changedScope() {
47
+ return new Error("Local subtree purge scope changed before deletion committed");
48
+ }
49
+ //# sourceMappingURL=localPurgeScope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localPurgeScope.js","sourceRoot":"","sources":["localPurgeScope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,qEAAqE,CAAC;AAgBpH,SAAS,mBAAmB,CAAC,KAA2B;IACtD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,IAAI,SAAS,GAAG,KAAK,CAAC,WAAW,CAAC;IAClC,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,MAAM,YAAY,EAAE,CAAC;QAC5D,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACpB,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC1B,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,qBAAqB,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE;YAC/D,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrB,MAAM,QAAQ,GAAG,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrD,IAAI,CAAC,QAAQ;YAAE,MAAM,YAAY,EAAE,CAAC;QACpC,IAAI,SAAS,KAAK,KAAK,CAAC,eAAe;YAAE,MAAM;QAC/C,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC;IAC1C,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,KAA2B;IAE3B,IACE,KAAK,CAAC,OAAO,KAAK,SAAS;QAC3B,CAAC,MAAM,6BAA6B,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAEnE,MAAM,YAAY,EAAE,CAAC;IACvB,MAAM,UAAU,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAC9C,4EAA4E;IAC5E,0EAA0E;IAC1E,KAAK,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;QAC5B,MAAM,QAAQ,GAAG,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9C,IAAI,CAAC,QAAQ;YAAE,SAAS;QACxB,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,0BAA0B,CAChE,KAAK,CAAC,OAAO,EACb,EAAE,CACH,CAAC;QACF,IACE,CAAC,OAAO,EAAE,MAAM;YAChB,OAAO,CAAC,SAAS,CAAC,QAAQ,KAAK,QAAQ,CAAC,SAAS,CAAC,QAAQ;YAC1D,OAAO,CAAC,SAAS,CAAC,cAAc,KAAK,QAAQ,CAAC,SAAS,CAAC,cAAc;YACtE,OAAO,CAAC,MAAM,CAAC,UAAU,KAAK,QAAQ,CAAC,MAAM,CAAC,UAAU;YAExD,MAAM,YAAY,EAAE,CAAC;IACzB,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,uBAAuB,CAC7D,KAAK,CAAC,OAAO,CACd,CAAC;IACF,IACE,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QAC5D,KAAK,CAAC,YAAY,EAAE,EAAE,KAAK,KAAK;QAEhC,MAAM,YAAY,EAAE,CAAC;AACzB,CAAC;AAED,SAAS,YAAY;IACnB,OAAO,IAAI,KAAK,CACd,6DAA6D,CAC9D,CAAC;AACJ,CAAC","sourcesContent":["import { hasUnsettledDocumentPlacement } from \"../../../data/persistence/container-contents/documentPurgePlacement\";\nimport type { ExecSql } from \"../../../data/sqlite/sqlSchema\";\nimport type { ContainerContentsPersistence } from \"../containerPersistence\";\nimport type { ContainerState } from \"../remoteHydration\";\n\ninterface LocalPurgeScopeInput {\n containerId: string | null | undefined;\n containersById: ReadonlyMap<string, ContainerState>;\n execSql: ExecSql;\n localId?: string | undefined;\n persistence: ContainerContentsPersistence;\n rootContainerId: string;\n stillCurrent?: (() => boolean) | undefined;\n verifiedAncestorsById: ReadonlyMap<string, readonly string[]>;\n}\n\nfunction guardedContainerIds(input: LocalPurgeScopeInput): ReadonlySet<string> {\n const guardedIds = new Set<string>();\n const seen = new Set<string>();\n let currentId = input.containerId;\n while (true) {\n if (!currentId || seen.has(currentId)) throw changedScope();\n seen.add(currentId);\n guardedIds.add(currentId);\n for (const id of input.verifiedAncestorsById.get(currentId) ?? [])\n guardedIds.add(id);\n const expected = input.containersById.get(currentId);\n if (!expected) throw changedScope();\n if (currentId === input.rootContainerId) break;\n currentId = expected.container.parentId;\n }\n return guardedIds;\n}\n\n/** Called inside the document deletion transaction; performs no remote I/O. */\nexport async function assertLocalPurgeScope(\n input: LocalPurgeScopeInput,\n): Promise<void> {\n if (\n input.localId !== undefined &&\n (await hasUnsettledDocumentPlacement(input.execSql, input.localId))\n )\n throw changedScope();\n const guardedIds = guardedContainerIds(input);\n // Include every known signed ancestor as well as the local selection chain.\n // A dishonest listing can give these chains different intermediate nodes.\n for (const id of guardedIds) {\n const expected = input.containersById.get(id);\n if (!expected) continue;\n const current = await input.persistence.loadContainerMetadataState(\n input.execSql,\n id,\n );\n if (\n !current?.record ||\n current.container.parentId !== expected.container.parentId ||\n current.container.organizationId !== expected.container.organizationId ||\n current.record.documentId !== expected.record.documentId\n )\n throw changedScope();\n }\n const pending = await input.persistence.listUnsyncedMoveIntents(\n input.execSql,\n );\n if (\n pending.some((intent) => guardedIds.has(intent.containerId)) ||\n input.stillCurrent?.() === false\n )\n throw changedScope();\n}\n\nfunction changedScope(): Error {\n return new Error(\n \"Local subtree purge scope changed before deletion committed\",\n );\n}\n"]}
@@ -3,8 +3,9 @@ import { sqlDocumentContainerProjectionPersistence } from "../../../data/persist
3
3
  import { listDocumentsByContainerIdsOrDocumentIds } from "../../../data/persistence/documents/documentsPersistence.js";
4
4
  import { unlinkRemoteContainerDocument } from "../documentLinks.js";
5
5
  import { purgeLocalContainerDocument, purgeRemoteContainerDocument, } from "../documentPurge.js";
6
- import { collectSubtreeLeafFirst } from "./purgeTreeCollection.js";
6
+ import { collectSubtreeLeafFirst, snapshotContainerStates, } from "./purgeTreeCollection.js";
7
7
  import { deleteSubtreeContainers } from "./purgeTreeContainerDeletion.js";
8
+ import { createSubtreePurgeScope } from "./purgeTreeScope.js";
8
9
  export { collectSubtreeLeafFirst } from "./purgeTreeCollection.js";
9
10
  // Destroy local-only documents locally. Destroy synced documents only when all
10
11
  // links are in the subtree; preserve documents with an external link. The purge
@@ -84,7 +85,10 @@ async function unlinkSubtreeDocument(input) {
84
85
  }
85
86
  for (const containerId of input.containerIds) {
86
87
  const result = await unlinkRemoteContainerDocument({
88
+ beforeSubmit: input.beforeSubmit,
87
89
  documentId,
90
+ expectedSubtreeRootId: input.expectedSubtreeRootId,
91
+ isCurrent: input.stillCurrent,
88
92
  noteId: input.document.id,
89
93
  resolveProjectionUserKey: input.resolveProjectionUserKey,
90
94
  rotationSnapshot,
@@ -97,28 +101,51 @@ async function unlinkSubtreeDocument(input) {
97
101
  }
98
102
  return true;
99
103
  }
100
- function resolveSubtreeDocumentOperations(input) {
101
- return (input.documentOperations ?? {
104
+ function resolveSubtreeDocumentOperations(input, scope) {
105
+ const operations = input.documentOperations ?? {
102
106
  purgeLocal: async (document) => (await purgeLocalContainerDocument({
107
+ beforeDeleteInTransaction: (execSql) => scope.assertLocalScopeInTransaction(execSql, document.containerId, document.id),
103
108
  noteId: document.id,
109
+ expectedContainerId: document.containerId,
104
110
  runtime: input.runtime,
105
111
  })) !== null,
106
112
  purgeRemote: async (document) => Boolean(document.documentId &&
107
113
  (await purgeRemoteContainerDocument({
114
+ beforeSubmit: () => scope.allowsDocument(document),
108
115
  documentId: document.documentId,
116
+ expectedSubtreeRootId: input.rootContainerId,
117
+ stillCurrent: input.stillCurrent,
109
118
  documentKind: document.documentKind ?? DEFAULT_DOCUMENT_KIND,
110
119
  noteId: document.id,
111
120
  resolveProjectionUserKey: input.resolveProjectionUserKey,
112
121
  runtime: input.runtime,
113
122
  }))),
114
123
  unlink: (document, containerIds) => unlinkSubtreeDocument({
124
+ beforeSubmit: () => scope.allowsDocument(document),
115
125
  containerIds,
116
126
  document,
127
+ expectedSubtreeRootId: input.rootContainerId,
128
+ stillCurrent: input.stillCurrent,
117
129
  prepareDocumentRotationSnapshot: input.prepareDocumentRotationSnapshot,
118
130
  resolveProjectionUserKey: input.resolveProjectionUserKey,
119
131
  runtime: input.runtime,
120
132
  }),
121
- });
133
+ };
134
+ return {
135
+ purgeLocal: async (document) => (await scope.allowsDocument(document)) &&
136
+ (await operations.purgeLocal(document)),
137
+ purgeRemote: async (document) => (await scope.allowsDocument(document)) &&
138
+ (await operations.purgeRemote(document)),
139
+ unlink: async (document, containerIds) => {
140
+ if (!(await scope.allowsDocument(document)))
141
+ return false;
142
+ for (const containerId of containerIds) {
143
+ if (!(await scope.allowsContainer(containerId)))
144
+ return false;
145
+ }
146
+ return operations.unlink(document, containerIds);
147
+ },
148
+ };
122
149
  }
123
150
  function purgeWasCancelled(input) {
124
151
  return input.signal?.aborted === true || input.stillCurrent?.() === false;
@@ -169,10 +196,14 @@ async function teardownSubtreeDocuments(input) {
169
196
  // reports whatever prefix was actually destroyed alongside completed/failed
170
197
  // counts and whether a cancellation cut it short. Returns null only when the
171
198
  // target container is absent from the snapshot.
172
- export async function purgeContainerTree(input) {
173
- if (purgeWasCancelled(input)) {
199
+ export async function purgeContainerTree(request) {
200
+ if (purgeWasCancelled(request)) {
174
201
  return null;
175
202
  }
203
+ const input = {
204
+ ...request,
205
+ containersById: snapshotContainerStates(request.containersById),
206
+ };
176
207
  const subtreeStates = collectSubtreeLeafFirst(input.containersById, input.rootContainerId);
177
208
  if (subtreeStates.length === 0) {
178
209
  return null;
@@ -216,8 +247,9 @@ export async function purgeContainerTree(input) {
216
247
  // Emit the initial 0/total so a determinate bar renders immediately, before the
217
248
  // first (potentially slow) remote call.
218
249
  emitProgress();
250
+ const scope = createSubtreePurgeScope(input);
219
251
  const teardown = await teardownSubtreeDocuments({
220
- documentOperations: resolveSubtreeDocumentOperations(input),
252
+ documentOperations: resolveSubtreeDocumentOperations(input, scope),
221
253
  plan,
222
254
  reportStep,
223
255
  signal: input.signal,
@@ -228,6 +260,9 @@ export async function purgeContainerTree(input) {
228
260
  let aborted = teardown.aborted;
229
261
  if (!teardown.aborted) {
230
262
  const deletion = await deleteSubtreeContainers({
263
+ allowsContainer: scope.allowsContainer,
264
+ allowsLocalScopeInTransaction: scope.allowsLocalScopeInTransaction,
265
+ rootContainerId: input.rootContainerId,
231
266
  persistence: input.persistence,
232
267
  reportStep,
233
268
  runtime: input.runtime,