@tearleads/client-sdk 0.1.65 → 0.1.67

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 (40) hide show
  1. package/data/persistence/container-contents/documentPurgePlacement.js +13 -0
  2. package/data/persistence/container-contents/documentPurgePlacement.js.map +1 -1
  3. package/index.d.ts +1 -1
  4. package/index.js.map +1 -1
  5. package/package.json +1 -1
  6. package/stores/container-contents/containerContentsStore.js +3 -2
  7. package/stores/container-contents/containerContentsStore.js.map +1 -1
  8. package/stores/container-contents/index.d.ts +1 -1
  9. package/stores/container-contents/index.js.map +1 -1
  10. package/stores/container-contents/operations/purgeMoveSettlementOperation.js +71 -0
  11. package/stores/container-contents/operations/purgeMoveSettlementOperation.js.map +1 -0
  12. package/stores/container-contents/syncAgent/acceptedMoveRetry.js +40 -0
  13. package/stores/container-contents/syncAgent/acceptedMoveRetry.js.map +1 -0
  14. package/stores/container-contents/syncAgent/syncLaneIteration.js +9 -2
  15. package/stores/container-contents/syncAgent/syncLaneIteration.js.map +1 -1
  16. package/workflows/container-contents/container-state/move/acceptedMoveSettlement.js +59 -0
  17. package/workflows/container-contents/container-state/move/acceptedMoveSettlement.js.map +1 -0
  18. package/workflows/container-contents/container-state/{moveIntentSync.js → move/moveIntentSync.js} +77 -39
  19. package/workflows/container-contents/container-state/move/moveIntentSync.js.map +1 -0
  20. package/workflows/container-contents/container-state/purge/purgeMoveSettlement.js +119 -0
  21. package/workflows/container-contents/container-state/purge/purgeMoveSettlement.js.map +1 -0
  22. package/workflows/container-contents/container-state/purge/purgeProgress.d.ts +8 -0
  23. package/workflows/container-contents/container-state/purge/purgeProgress.js.map +1 -1
  24. package/workflows/container-contents/container-state/remote.js +22 -18
  25. package/workflows/container-contents/container-state/remote.js.map +1 -1
  26. package/workflows/container-contents/container-state/types.js.map +1 -1
  27. package/workflows/container-contents/syncLane.d.ts +2 -0
  28. package/workflows/container-contents/syncLane.js +7 -0
  29. package/workflows/container-contents/syncLane.js.map +1 -1
  30. package/workflows/containers/child/move/index.d.ts +24 -2
  31. package/workflows/containers/child/move/index.js +62 -70
  32. package/workflows/containers/child/move/index.js.map +1 -1
  33. package/workflows/containers/child/move/moveRequest.js +47 -0
  34. package/workflows/containers/child/move/moveRequest.js.map +1 -0
  35. package/workflows/containers/child/mutationSubmit.js +1 -0
  36. package/workflows/containers/child/mutationSubmit.js.map +1 -1
  37. package/workflows/containers/index.d.ts +1 -1
  38. package/workflows/containers/index.js +1 -1
  39. package/workflows/containers/index.js.map +1 -1
  40. package/workflows/container-contents/container-state/moveIntentSync.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"syncLaneIteration.js","sourceRoot":"","sources":["syncLaneIteration.ts"],"names":[],"mappings":"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","sourcesContent":["import { syncPendingContainerCreateIntents } from \"../../../workflows/container-contents/container-state/create/createIntentSync\";\nimport { syncPendingContainerMoveIntents } from \"../../../workflows/container-contents/container-state/moveIntentSync\";\nimport {\n type DocumentMoveIntentSyncHost,\n syncPendingDocumentMoveIntents,\n} from \"../../../workflows/container-contents/documentMove/documentMoveIntentSync\";\nimport { syncContainerMetadataState } from \"../../../workflows/container-contents/metadata\";\nimport type {\n ContainerState,\n RemoteContainerHydrationHost,\n} from \"../../../workflows/container-contents/remoteHydration\";\nimport { createContainerContentsDocumentsRuntime } from \"../../../workflows/container-contents/runtime\";\nimport { openDocumentStore, requestDomainDocumentSync } from \"../../documents\";\nimport { relinkDocumentStoreWithCommitSideEffect } from \"../../documents/documentStore/internalRelink\";\nimport { DocumentRemoteWorkBusyError } from \"../../documents/documentStore/sync/remoteWork\";\nimport { removeMissingSyncedContainerState } from \"../containerState/missingSyncedContainerState\";\nimport { runContainerDocumentWork } from \"../documentWork\";\nimport {\n primeStoreDocuments,\n recoverStoreStaleRoot,\n} from \"../documentWork/documentRecovery\";\nimport {\n bumpMetadataSyncSeq,\n clearMetadataSyncQueueIfUnchanged,\n readMetadataSyncSeq,\n} from \"./metadataSyncSignal\";\nimport type {\n ContainerContentsStoreRuntime,\n ContainerContentsStoreSyncState,\n} from \"./syncAgentTypes\";\n\ntype ContainerContentsStorePrimeDocumentRuntime = ReturnType<\n typeof createContainerContentsDocumentsRuntime\n>;\ntype SyncContainerMetadata = typeof syncContainerMetadataState;\n\nfunction requestContainerContentsStoreSync(\n state: ContainerContentsStoreSyncState,\n) {\n state.syncLane?.requestSync();\n}\n\nfunction createRemoteSyncBlocker(runtime: ContainerContentsStoreRuntime) {\n return (organizationId: string) =>\n runtime.util.isRemoteSyncBlocked?.(organizationId) ?? false;\n}\n\nfunction captureStructuralSyncGeneration(\n state: ContainerContentsStoreSyncState,\n runtime: ContainerContentsStoreRuntime,\n) {\n const activeContainerId = runtime.state.containerId;\n const activeOrganizationId = runtime.auth.organizationId;\n const lifecycleGeneration = state.lifecycleGeneration;\n const structuralGeneration = state.structuralGeneration;\n const domainScope = runtime.state.domainScope;\n const execSql = runtime.infra.execSql;\n const resolveProjectionUserKey = state.resolveProjectionUserKey;\n const syncLane = state.syncLane;\n return () =>\n state.lifecycleGeneration === lifecycleGeneration &&\n state.structuralGeneration === structuralGeneration &&\n state.runtime.auth.organizationId === activeOrganizationId &&\n state.runtime.state.containerId === activeContainerId &&\n state.runtime.state.domainScope === domainScope &&\n state.runtime.infra.execSql === execSql &&\n state.resolveProjectionUserKey === resolveProjectionUserKey &&\n state.syncLane === syncLane &&\n !syncLane?.isDisposed?.();\n}\n\nfunction createContainerContentsStoreDocumentMoveHost(\n state: ContainerContentsStoreSyncState,\n isCurrent: () => boolean,\n): DocumentMoveIntentSyncHost<ContainerContentsStorePrimeDocumentRuntime> {\n const runtime = state.runtime;\n const domainScope = runtime.state.domainScope;\n return {\n documentWorkflowRuntime: (containerId) =>\n createContainerContentsDocumentsRuntime(runtime, containerId),\n openDocumentStore: ({ containerId, documentId, localId }) => {\n const store = openDocumentStore(\n domainScope,\n localId,\n createContainerContentsDocumentsRuntime(runtime, containerId),\n documentId,\n );\n return {\n assertCanRotateContentKey: () =>\n store.assertCanRotateContentKey().catch((error: unknown) => {\n if (error instanceof DocumentRemoteWorkBusyError) {\n void error.whenIdle.then(() => {\n if (isCurrent()) requestContainerContentsStoreSync(state);\n });\n }\n throw error;\n }),\n ensureInitialized: () => store.ensureInitialized(),\n relink: ({ commitSideEffect, ...input }) =>\n commitSideEffect\n ? relinkDocumentStoreWithCommitSideEffect(\n store,\n input,\n commitSideEffect,\n )\n : store.relink(input),\n requestSync: () => store.requestSync(),\n updateRuntime: (nextRuntime) => store.updateRuntime(nextRuntime),\n };\n },\n };\n}\n\nfunction syncPendingStoreDocumentMoves(input: {\n isCreatePending: (containerId: string) => boolean;\n isCurrent: () => boolean;\n isRemoteSyncBlocked: (organizationId: string) => boolean;\n state: ContainerContentsStoreSyncState;\n}) {\n return syncPendingDocumentMoveIntents({\n host: createContainerContentsStoreDocumentMoveHost(\n input.state,\n input.isCurrent,\n ),\n isCreatePending: input.isCreatePending,\n isCurrent: input.isCurrent,\n isRemoteSyncBlocked: input.isRemoteSyncBlocked,\n state: input.state,\n });\n}\n\nasync function syncSingleContainerMetadata(input: {\n host: RemoteContainerHydrationHost;\n isCurrent: () => boolean;\n state: ContainerContentsStoreSyncState;\n containerState: ContainerState;\n encapsulationKeyPair: NonNullable<\n ContainerContentsStoreRuntime[\"crypto\"][\"encapsulationKeyPair\"]\n >;\n syncContainerMetadata?: SyncContainerMetadata | undefined;\n}) {\n const { containerState, encapsulationKeyPair, host, isCurrent, state } =\n input;\n const metadataDocumentId = containerState.record.documentId;\n // Snapshot this signal before the GET so a mid-pass event survives clearing.\n const consumedSeqById = new Map<string, number>();\n if (typeof metadataDocumentId === \"string\") {\n consumedSeqById.set(\n metadataDocumentId,\n readMetadataSyncSeq(state.metadataSyncSignalSeqById, metadataDocumentId),\n );\n }\n const synced = await (\n input.syncContainerMetadata ?? syncContainerMetadataState\n )({\n forceReadSync:\n typeof metadataDocumentId === \"string\" &&\n state.metadataDocumentIdsNeedingSync.has(metadataDocumentId),\n locallyAcceptedUpdateIds: state.locallyAcceptedMetadataUpdateIds,\n isCurrent,\n metadataState: containerState,\n onDurableStateNeedsReload: () => {\n const currentDocumentId = state.containersById.get(\n containerState.container.id,\n )?.record.documentId;\n if (typeof currentDocumentId === \"string\") {\n state.metadataDocumentIdsNeedingSync.add(currentDocumentId);\n bumpMetadataSyncSeq(state.metadataSyncSignalSeqById, currentDocumentId);\n }\n requestContainerContentsStoreSync(state);\n },\n persistence: state.persistence,\n resolveProjectionUserKey: state.resolveProjectionUserKey,\n runtime: state.runtime,\n targetSecretKey: encapsulationKeyPair.secretKey,\n });\n if (!isCurrent() || !synced) {\n return;\n }\n if (\"missing\" in synced) {\n removeMissingSyncedContainerState(\n state,\n containerState,\n host.updateSnapshot,\n );\n return;\n }\n\n for (const id of [metadataDocumentId, synced.record.documentId]) {\n if (typeof id === \"string\") {\n clearMetadataSyncQueueIfUnchanged({\n consumedSeqById,\n id,\n needingSync: state.metadataDocumentIdsNeedingSync,\n seqById: state.metadataSyncSignalSeqById,\n });\n }\n }\n // syncContainerMetadataState settles its detached candidate into this live\n // object while preserving any metadata edit that completed or queued during\n // the remote pass. Do not install its returned detached record again here:\n // doing so would overwrite that preserved live edit.\n host.updateSnapshot();\n\n if (synced.shouldRequestFollowupSync) {\n requestContainerContentsStoreSync(state);\n }\n}\n\n/**\n * A folder whose create has not settled can carry a listed identity that\n * adoption has not verified, or has refused. Folder moves, document moves and\n * metadata edits wait for it; child and document creates wait in their own\n * create paths.\n */\nasync function loadPendingCreatePredicate(\n state: ContainerContentsStoreSyncState,\n): Promise<(containerId: string) => boolean> {\n const pending = await state.persistence.listPendingCreateIntents(\n state.runtime.infra.execSql,\n );\n const pendingIds = new Set(pending.map((intent) => intent.containerId));\n return (containerId) => pendingIds.has(containerId);\n}\n\ninterface ContainerContentsStoreSyncIterationInput {\n host: RemoteContainerHydrationHost;\n reconcileRestoredAccess: (isCurrent: () => boolean) => Promise<void>;\n requestRemoteReconciliation: (parentContainerId: string | null) => void;\n state: ContainerContentsStoreSyncState;\n syncContainerMetadata?: SyncContainerMetadata | undefined;\n}\n\nexport async function runContainerContentsStoreSyncIteration(\n input: ContainerContentsStoreSyncIterationInput,\n) {\n const { host, reconcileRestoredAccess, requestRemoteReconciliation, state } =\n input;\n const runtime = state.runtime;\n const encapsulationKeyPair = runtime.crypto.encapsulationKeyPair;\n if (\n runtime.infra.dbStatus !== \"ready\" ||\n !state.snapshot.ready ||\n !runtime.state.online ||\n !runtime.auth.isAuthenticated ||\n !encapsulationKeyPair\n ) {\n return;\n }\n const domainScope = runtime.state.domainScope;\n const isCurrent = captureStructuralSyncGeneration(state, runtime);\n\n await reconcileRestoredAccess(isCurrent);\n if (!isCurrent()) {\n return;\n }\n const isOrganizationBlocked = createRemoteSyncBlocker(runtime);\n\n const createdContainerCount = await syncPendingContainerCreateIntents({\n host,\n isCurrent,\n isRemoteSyncBlocked: isOrganizationBlocked,\n requestRemoteReconciliation,\n state,\n });\n if (!isCurrent()) {\n return;\n }\n if (createdContainerCount > 0) {\n state.documentStoresNeedPriming = true;\n host.updateSnapshot();\n }\n const isCreatePending = await loadPendingCreatePredicate(state);\n if (!isCurrent()) {\n return;\n }\n\n const movedContainerCount = await syncPendingContainerMoveIntents({\n host,\n isCreatePending,\n isCurrent,\n isRemoteSyncBlocked: isOrganizationBlocked,\n requestRemoteReconciliation,\n state,\n });\n if (!isCurrent()) {\n return;\n }\n if (movedContainerCount > 0) {\n state.documentStoresNeedPriming = true;\n host.updateSnapshot();\n requestDomainDocumentSync(domainScope);\n requestContainerContentsStoreSync(state);\n }\n\n for (const containerState of Array.from(state.containersById.values())) {\n if (isCreatePending(containerState.container.id)) continue;\n await syncSingleContainerMetadata({\n containerState,\n encapsulationKeyPair,\n host,\n isCurrent,\n state,\n syncContainerMetadata: input.syncContainerMetadata,\n });\n if (!isCurrent()) {\n return;\n }\n }\n\n // Root adoption notifies session consumers synchronously. Let container\n // metadata converge first so a recovered system container is never exposed\n // under its placeholder name when the active root changes.\n await runContainerDocumentWork({\n isCurrent,\n onContextChanged: () => {\n if (isCurrent()) {\n requestContainerContentsStoreSync(state);\n }\n },\n onDocumentsMoved: () => {\n if (isCurrent()) {\n requestDomainDocumentSync(domainScope);\n requestContainerContentsStoreSync(state);\n }\n },\n primeDocuments: () => primeStoreDocuments(state, isCurrent),\n recoverStaleRoot: () => recoverStoreStaleRoot(state, isCurrent),\n shouldPrimeDocuments: () => state.documentStoresNeedPriming,\n // Document move intents live in the structural phase because they may\n // target containers created locally in the same session, such as Trash.\n // Root recovery rewrites stale endpoints and returns their intents to\n // pending, so replay follows recovery in this same pass.\n syncPendingDocumentMoves: () =>\n syncPendingStoreDocumentMoves({\n isCreatePending,\n isCurrent,\n isRemoteSyncBlocked: isOrganizationBlocked,\n state,\n }),\n });\n}\n"]}
1
+ {"version":3,"file":"syncLaneIteration.js","sourceRoot":"","sources":["syncLaneIteration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iCAAiC,EAAE,MAAM,+EAA+E,CAAC;AAClI,OAAO,EAAE,+BAA+B,EAAE,MAAM,2EAA2E,CAAC;AAC5H,OAAO,EAEL,8BAA8B,GAC/B,MAAM,2EAA2E,CAAC;AACnF,OAAO,EAAE,0BAA0B,EAAE,MAAM,gDAAgD,CAAC;AAK5F,OAAO,EAAE,uCAAuC,EAAE,MAAM,+CAA+C,CAAC;AACxG,OAAO,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,uCAAuC,EAAE,MAAM,8CAA8C,CAAC;AACvG,OAAO,EAAE,2BAA2B,EAAE,MAAM,+CAA+C,CAAC;AAC5F,OAAO,EAAE,iCAAiC,EAAE,MAAM,+CAA+C,CAAC;AAClG,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EACL,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EACL,mBAAmB,EACnB,iCAAiC,EACjC,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAW9B,SAAS,iCAAiC,CACxC,KAAsC;IAEtC,KAAK,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC;AAChC,CAAC;AAED,SAAS,uBAAuB,CAAC,OAAsC;IACrE,OAAO,CAAC,cAAsB,EAAE,EAAE,CAChC,OAAO,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC,cAAc,CAAC,IAAI,KAAK,CAAC;AAChE,CAAC;AAED,SAAS,+BAA+B,CACtC,KAAsC,EACtC,OAAsC;IAEtC,MAAM,iBAAiB,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC;IACpD,MAAM,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC;IACzD,MAAM,mBAAmB,GAAG,KAAK,CAAC,mBAAmB,CAAC;IACtD,MAAM,oBAAoB,GAAG,KAAK,CAAC,oBAAoB,CAAC;IACxD,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC;IAC9C,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;IACtC,MAAM,wBAAwB,GAAG,KAAK,CAAC,wBAAwB,CAAC;IAChE,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;IAChC,OAAO,GAAG,EAAE,CACV,KAAK,CAAC,mBAAmB,KAAK,mBAAmB;QACjD,KAAK,CAAC,oBAAoB,KAAK,oBAAoB;QACnD,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,KAAK,oBAAoB;QAC1D,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,KAAK,iBAAiB;QACrD,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,KAAK,WAAW;QAC/C,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,KAAK,OAAO;QACvC,KAAK,CAAC,wBAAwB,KAAK,wBAAwB;QAC3D,KAAK,CAAC,QAAQ,KAAK,QAAQ;QAC3B,CAAC,QAAQ,EAAE,UAAU,EAAE,EAAE,CAAC;AAC9B,CAAC;AAED,SAAS,4CAA4C,CACnD,KAAsC,EACtC,SAAwB;IAExB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC;IAC9C,OAAO;QACL,uBAAuB,EAAE,CAAC,WAAW,EAAE,EAAE,CACvC,uCAAuC,CAAC,OAAO,EAAE,WAAW,CAAC;QAC/D,iBAAiB,EAAE,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,EAAE;YAC1D,MAAM,KAAK,GAAG,iBAAiB,CAC7B,WAAW,EACX,OAAO,EACP,uCAAuC,CAAC,OAAO,EAAE,WAAW,CAAC,EAC7D,UAAU,CACX,CAAC;YACF,OAAO;gBACL,yBAAyB,EAAE,GAAG,EAAE,CAC9B,KAAK,CAAC,yBAAyB,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;oBACzD,IAAI,KAAK,YAAY,2BAA2B,EAAE,CAAC;wBACjD,KAAK,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE;4BAC5B,IAAI,SAAS,EAAE;gCAAE,iCAAiC,CAAC,KAAK,CAAC,CAAC;wBAC5D,CAAC,CAAC,CAAC;oBACL,CAAC;oBACD,MAAM,KAAK,CAAC;gBACd,CAAC,CAAC;gBACJ,iBAAiB,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,iBAAiB,EAAE;gBAClD,MAAM,EAAE,CAAC,EAAE,gBAAgB,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE,CACzC,gBAAgB;oBACd,CAAC,CAAC,uCAAuC,CACrC,KAAK,EACL,KAAK,EACL,gBAAgB,CACjB;oBACH,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC;gBACzB,WAAW,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE;gBACtC,aAAa,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC;aACjE,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,6BAA6B,CAAC,KAKtC;IACC,OAAO,8BAA8B,CAAC;QACpC,IAAI,EAAE,4CAA4C,CAChD,KAAK,CAAC,KAAK,EACX,KAAK,CAAC,SAAS,CAChB;QACD,eAAe,EAAE,KAAK,CAAC,eAAe;QACtC,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,mBAAmB,EAAE,KAAK,CAAC,mBAAmB;QAC9C,KAAK,EAAE,KAAK,CAAC,KAAK;KACnB,CAAC,CAAC;AACL,CAAC;AAED,SAAS,8BAA8B,CACrC,KAG8C;IAE9C,OAAO,+BAA+B,CAAC;QACrC,GAAG,KAAK;QACR,iBAAiB,EAAE,uBAAuB,CAAC,KAAK,CAAC,KAAK,CAAC;KACxD,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,2BAA2B,CAAC,KAS1C;IACC,MAAM,EAAE,cAAc,EAAE,oBAAoB,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GACpE,KAAK,CAAC;IACR,MAAM,kBAAkB,GAAG,cAAc,CAAC,MAAM,CAAC,UAAU,CAAC;IAC5D,6EAA6E;IAC7E,MAAM,eAAe,GAAG,IAAI,GAAG,EAAkB,CAAC;IAClD,IAAI,OAAO,kBAAkB,KAAK,QAAQ,EAAE,CAAC;QAC3C,eAAe,CAAC,GAAG,CACjB,kBAAkB,EAClB,mBAAmB,CAAC,KAAK,CAAC,yBAAyB,EAAE,kBAAkB,CAAC,CACzE,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CACnB,KAAK,CAAC,qBAAqB,IAAI,0BAA0B,CAC1D,CAAC;QACA,aAAa,EACX,OAAO,kBAAkB,KAAK,QAAQ;YACtC,KAAK,CAAC,8BAA8B,CAAC,GAAG,CAAC,kBAAkB,CAAC;QAC9D,wBAAwB,EAAE,KAAK,CAAC,gCAAgC;QAChE,SAAS;QACT,aAAa,EAAE,cAAc;QAC7B,yBAAyB,EAAE,GAAG,EAAE;YAC9B,MAAM,iBAAiB,GAAG,KAAK,CAAC,cAAc,CAAC,GAAG,CAChD,cAAc,CAAC,SAAS,CAAC,EAAE,CAC5B,EAAE,MAAM,CAAC,UAAU,CAAC;YACrB,IAAI,OAAO,iBAAiB,KAAK,QAAQ,EAAE,CAAC;gBAC1C,KAAK,CAAC,8BAA8B,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;gBAC5D,mBAAmB,CAAC,KAAK,CAAC,yBAAyB,EAAE,iBAAiB,CAAC,CAAC;YAC1E,CAAC;YACD,iCAAiC,CAAC,KAAK,CAAC,CAAC;QAC3C,CAAC;QACD,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,wBAAwB,EAAE,KAAK,CAAC,wBAAwB;QACxD,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,eAAe,EAAE,oBAAoB,CAAC,SAAS;KAChD,CAAC,CAAC;IACH,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;QAC5B,OAAO;IACT,CAAC;IACD,IAAI,SAAS,IAAI,MAAM,EAAE,CAAC;QACxB,iCAAiC,CAC/B,KAAK,EACL,cAAc,EACd,IAAI,CAAC,cAAc,CACpB,CAAC;QACF,OAAO;IACT,CAAC;IAED,KAAK,MAAM,EAAE,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QAChE,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;YAC3B,iCAAiC,CAAC;gBAChC,eAAe;gBACf,EAAE;gBACF,WAAW,EAAE,KAAK,CAAC,8BAA8B;gBACjD,OAAO,EAAE,KAAK,CAAC,yBAAyB;aACzC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,2EAA2E;IAC3E,4EAA4E;IAC5E,2EAA2E;IAC3E,qDAAqD;IACrD,IAAI,CAAC,cAAc,EAAE,CAAC;IAEtB,IAAI,MAAM,CAAC,yBAAyB,EAAE,CAAC;QACrC,iCAAiC,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,0BAA0B,CACvC,KAAsC;IAEtC,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,wBAAwB,CAC9D,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAC5B,CAAC;IACF,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;IACxE,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AACtD,CAAC;AAUD,MAAM,CAAC,KAAK,UAAU,sCAAsC,CAC1D,KAA+C;IAE/C,MAAM,EAAE,IAAI,EAAE,uBAAuB,EAAE,2BAA2B,EAAE,KAAK,EAAE,GACzE,KAAK,CAAC;IACR,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,MAAM,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC;IACjE,IACE,OAAO,CAAC,KAAK,CAAC,QAAQ,KAAK,OAAO;QAClC,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK;QACrB,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM;QACrB,CAAC,OAAO,CAAC,IAAI,CAAC,eAAe;QAC7B,CAAC,oBAAoB,EACrB,CAAC;QACD,OAAO;IACT,CAAC;IACD,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC;IAC9C,MAAM,SAAS,GAAG,+BAA+B,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAElE,MAAM,uBAAuB,CAAC,SAAS,CAAC,CAAC;IACzC,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;QACjB,OAAO;IACT,CAAC;IACD,MAAM,qBAAqB,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAC;IAE/D,MAAM,qBAAqB,GAAG,MAAM,iCAAiC,CAAC;QACpE,IAAI;QACJ,SAAS;QACT,mBAAmB,EAAE,qBAAqB;QAC1C,2BAA2B;QAC3B,KAAK;KACN,CAAC,CAAC;IACH,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;QACjB,OAAO;IACT,CAAC;IACD,IAAI,qBAAqB,GAAG,CAAC,EAAE,CAAC;QAC9B,KAAK,CAAC,yBAAyB,GAAG,IAAI,CAAC;QACvC,IAAI,CAAC,cAAc,EAAE,CAAC;IACxB,CAAC;IACD,MAAM,eAAe,GAAG,MAAM,0BAA0B,CAAC,KAAK,CAAC,CAAC;IAChE,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;QACjB,OAAO;IACT,CAAC;IAED,MAAM,mBAAmB,GAAG,MAAM,8BAA8B,CAAC;QAC/D,IAAI;QACJ,eAAe;QACf,SAAS;QACT,mBAAmB,EAAE,qBAAqB;QAC1C,2BAA2B;QAC3B,KAAK;KACN,CAAC,CAAC;IACH,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;QACjB,OAAO;IACT,CAAC;IACD,IAAI,mBAAmB,GAAG,CAAC,EAAE,CAAC;QAC5B,KAAK,CAAC,yBAAyB,GAAG,IAAI,CAAC;QACvC,IAAI,CAAC,cAAc,EAAE,CAAC;QACtB,yBAAyB,CAAC,WAAW,CAAC,CAAC;QACvC,iCAAiC,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC;IAED,KAAK,MAAM,cAAc,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;QACvE,IAAI,eAAe,CAAC,cAAc,CAAC,SAAS,CAAC,EAAE,CAAC;YAAE,SAAS;QAC3D,MAAM,2BAA2B,CAAC;YAChC,cAAc;YACd,oBAAoB;YACpB,IAAI;YACJ,SAAS;YACT,KAAK;YACL,qBAAqB,EAAE,KAAK,CAAC,qBAAqB;SACnD,CAAC,CAAC;QACH,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;IACH,CAAC;IAED,wEAAwE;IACxE,2EAA2E;IAC3E,2DAA2D;IAC3D,MAAM,wBAAwB,CAAC;QAC7B,SAAS;QACT,gBAAgB,EAAE,GAAG,EAAE;YACrB,IAAI,SAAS,EAAE,EAAE,CAAC;gBAChB,iCAAiC,CAAC,KAAK,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;QACD,gBAAgB,EAAE,GAAG,EAAE;YACrB,IAAI,SAAS,EAAE,EAAE,CAAC;gBAChB,yBAAyB,CAAC,WAAW,CAAC,CAAC;gBACvC,iCAAiC,CAAC,KAAK,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;QACD,cAAc,EAAE,GAAG,EAAE,CAAC,mBAAmB,CAAC,KAAK,EAAE,SAAS,CAAC;QAC3D,gBAAgB,EAAE,GAAG,EAAE,CAAC,qBAAqB,CAAC,KAAK,EAAE,SAAS,CAAC;QAC/D,oBAAoB,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,yBAAyB;QAC3D,sEAAsE;QACtE,wEAAwE;QACxE,sEAAsE;QACtE,yDAAyD;QACzD,wBAAwB,EAAE,GAAG,EAAE,CAC7B,6BAA6B,CAAC;YAC5B,eAAe;YACf,SAAS;YACT,mBAAmB,EAAE,qBAAqB;YAC1C,KAAK;SACN,CAAC;KACL,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { syncPendingContainerCreateIntents } from \"../../../workflows/container-contents/container-state/create/createIntentSync\";\nimport { syncPendingContainerMoveIntents } from \"../../../workflows/container-contents/container-state/move/moveIntentSync\";\nimport {\n type DocumentMoveIntentSyncHost,\n syncPendingDocumentMoveIntents,\n} from \"../../../workflows/container-contents/documentMove/documentMoveIntentSync\";\nimport { syncContainerMetadataState } from \"../../../workflows/container-contents/metadata\";\nimport type {\n ContainerState,\n RemoteContainerHydrationHost,\n} from \"../../../workflows/container-contents/remoteHydration\";\nimport { createContainerContentsDocumentsRuntime } from \"../../../workflows/container-contents/runtime\";\nimport { openDocumentStore, requestDomainDocumentSync } from \"../../documents\";\nimport { relinkDocumentStoreWithCommitSideEffect } from \"../../documents/documentStore/internalRelink\";\nimport { DocumentRemoteWorkBusyError } from \"../../documents/documentStore/sync/remoteWork\";\nimport { removeMissingSyncedContainerState } from \"../containerState/missingSyncedContainerState\";\nimport { runContainerDocumentWork } from \"../documentWork\";\nimport {\n primeStoreDocuments,\n recoverStoreStaleRoot,\n} from \"../documentWork/documentRecovery\";\nimport { createAcceptedMoveRetry } from \"./acceptedMoveRetry\";\nimport {\n bumpMetadataSyncSeq,\n clearMetadataSyncQueueIfUnchanged,\n readMetadataSyncSeq,\n} from \"./metadataSyncSignal\";\nimport type {\n ContainerContentsStoreRuntime,\n ContainerContentsStoreSyncState,\n} from \"./syncAgentTypes\";\n\ntype ContainerContentsStorePrimeDocumentRuntime = ReturnType<\n typeof createContainerContentsDocumentsRuntime\n>;\ntype SyncContainerMetadata = typeof syncContainerMetadataState;\n\nfunction requestContainerContentsStoreSync(\n state: ContainerContentsStoreSyncState,\n) {\n state.syncLane?.requestSync();\n}\n\nfunction createRemoteSyncBlocker(runtime: ContainerContentsStoreRuntime) {\n return (organizationId: string) =>\n runtime.util.isRemoteSyncBlocked?.(organizationId) ?? false;\n}\n\nfunction captureStructuralSyncGeneration(\n state: ContainerContentsStoreSyncState,\n runtime: ContainerContentsStoreRuntime,\n) {\n const activeContainerId = runtime.state.containerId;\n const activeOrganizationId = runtime.auth.organizationId;\n const lifecycleGeneration = state.lifecycleGeneration;\n const structuralGeneration = state.structuralGeneration;\n const domainScope = runtime.state.domainScope;\n const execSql = runtime.infra.execSql;\n const resolveProjectionUserKey = state.resolveProjectionUserKey;\n const syncLane = state.syncLane;\n return () =>\n state.lifecycleGeneration === lifecycleGeneration &&\n state.structuralGeneration === structuralGeneration &&\n state.runtime.auth.organizationId === activeOrganizationId &&\n state.runtime.state.containerId === activeContainerId &&\n state.runtime.state.domainScope === domainScope &&\n state.runtime.infra.execSql === execSql &&\n state.resolveProjectionUserKey === resolveProjectionUserKey &&\n state.syncLane === syncLane &&\n !syncLane?.isDisposed?.();\n}\n\nfunction createContainerContentsStoreDocumentMoveHost(\n state: ContainerContentsStoreSyncState,\n isCurrent: () => boolean,\n): DocumentMoveIntentSyncHost<ContainerContentsStorePrimeDocumentRuntime> {\n const runtime = state.runtime;\n const domainScope = runtime.state.domainScope;\n return {\n documentWorkflowRuntime: (containerId) =>\n createContainerContentsDocumentsRuntime(runtime, containerId),\n openDocumentStore: ({ containerId, documentId, localId }) => {\n const store = openDocumentStore(\n domainScope,\n localId,\n createContainerContentsDocumentsRuntime(runtime, containerId),\n documentId,\n );\n return {\n assertCanRotateContentKey: () =>\n store.assertCanRotateContentKey().catch((error: unknown) => {\n if (error instanceof DocumentRemoteWorkBusyError) {\n void error.whenIdle.then(() => {\n if (isCurrent()) requestContainerContentsStoreSync(state);\n });\n }\n throw error;\n }),\n ensureInitialized: () => store.ensureInitialized(),\n relink: ({ commitSideEffect, ...input }) =>\n commitSideEffect\n ? relinkDocumentStoreWithCommitSideEffect(\n store,\n input,\n commitSideEffect,\n )\n : store.relink(input),\n requestSync: () => store.requestSync(),\n updateRuntime: (nextRuntime) => store.updateRuntime(nextRuntime),\n };\n },\n };\n}\n\nfunction syncPendingStoreDocumentMoves(input: {\n isCreatePending: (containerId: string) => boolean;\n isCurrent: () => boolean;\n isRemoteSyncBlocked: (organizationId: string) => boolean;\n state: ContainerContentsStoreSyncState;\n}) {\n return syncPendingDocumentMoveIntents({\n host: createContainerContentsStoreDocumentMoveHost(\n input.state,\n input.isCurrent,\n ),\n isCreatePending: input.isCreatePending,\n isCurrent: input.isCurrent,\n isRemoteSyncBlocked: input.isRemoteSyncBlocked,\n state: input.state,\n });\n}\n\nfunction syncPendingStoreContainerMoves(\n input: Omit<\n Parameters<typeof syncPendingContainerMoveIntents>[0],\n \"acceptedMoveRetry\" | \"state\"\n > & { state: ContainerContentsStoreSyncState },\n) {\n return syncPendingContainerMoveIntents({\n ...input,\n acceptedMoveRetry: createAcceptedMoveRetry(input.state),\n });\n}\n\nasync function syncSingleContainerMetadata(input: {\n host: RemoteContainerHydrationHost;\n isCurrent: () => boolean;\n state: ContainerContentsStoreSyncState;\n containerState: ContainerState;\n encapsulationKeyPair: NonNullable<\n ContainerContentsStoreRuntime[\"crypto\"][\"encapsulationKeyPair\"]\n >;\n syncContainerMetadata?: SyncContainerMetadata | undefined;\n}) {\n const { containerState, encapsulationKeyPair, host, isCurrent, state } =\n input;\n const metadataDocumentId = containerState.record.documentId;\n // Snapshot this signal before the GET so a mid-pass event survives clearing.\n const consumedSeqById = new Map<string, number>();\n if (typeof metadataDocumentId === \"string\") {\n consumedSeqById.set(\n metadataDocumentId,\n readMetadataSyncSeq(state.metadataSyncSignalSeqById, metadataDocumentId),\n );\n }\n const synced = await (\n input.syncContainerMetadata ?? syncContainerMetadataState\n )({\n forceReadSync:\n typeof metadataDocumentId === \"string\" &&\n state.metadataDocumentIdsNeedingSync.has(metadataDocumentId),\n locallyAcceptedUpdateIds: state.locallyAcceptedMetadataUpdateIds,\n isCurrent,\n metadataState: containerState,\n onDurableStateNeedsReload: () => {\n const currentDocumentId = state.containersById.get(\n containerState.container.id,\n )?.record.documentId;\n if (typeof currentDocumentId === \"string\") {\n state.metadataDocumentIdsNeedingSync.add(currentDocumentId);\n bumpMetadataSyncSeq(state.metadataSyncSignalSeqById, currentDocumentId);\n }\n requestContainerContentsStoreSync(state);\n },\n persistence: state.persistence,\n resolveProjectionUserKey: state.resolveProjectionUserKey,\n runtime: state.runtime,\n targetSecretKey: encapsulationKeyPair.secretKey,\n });\n if (!isCurrent() || !synced) {\n return;\n }\n if (\"missing\" in synced) {\n removeMissingSyncedContainerState(\n state,\n containerState,\n host.updateSnapshot,\n );\n return;\n }\n\n for (const id of [metadataDocumentId, synced.record.documentId]) {\n if (typeof id === \"string\") {\n clearMetadataSyncQueueIfUnchanged({\n consumedSeqById,\n id,\n needingSync: state.metadataDocumentIdsNeedingSync,\n seqById: state.metadataSyncSignalSeqById,\n });\n }\n }\n // syncContainerMetadataState settles its detached candidate into this live\n // object while preserving any metadata edit that completed or queued during\n // the remote pass. Do not install its returned detached record again here:\n // doing so would overwrite that preserved live edit.\n host.updateSnapshot();\n\n if (synced.shouldRequestFollowupSync) {\n requestContainerContentsStoreSync(state);\n }\n}\n\n/**\n * A folder whose create has not settled can carry a listed identity that\n * adoption has not verified, or has refused. Folder moves, document moves and\n * metadata edits wait for it; child and document creates wait in their own\n * create paths.\n */\nasync function loadPendingCreatePredicate(\n state: ContainerContentsStoreSyncState,\n): Promise<(containerId: string) => boolean> {\n const pending = await state.persistence.listPendingCreateIntents(\n state.runtime.infra.execSql,\n );\n const pendingIds = new Set(pending.map((intent) => intent.containerId));\n return (containerId) => pendingIds.has(containerId);\n}\n\ninterface ContainerContentsStoreSyncIterationInput {\n host: RemoteContainerHydrationHost;\n reconcileRestoredAccess: (isCurrent: () => boolean) => Promise<void>;\n requestRemoteReconciliation: (parentContainerId: string | null) => void;\n state: ContainerContentsStoreSyncState;\n syncContainerMetadata?: SyncContainerMetadata | undefined;\n}\n\nexport async function runContainerContentsStoreSyncIteration(\n input: ContainerContentsStoreSyncIterationInput,\n) {\n const { host, reconcileRestoredAccess, requestRemoteReconciliation, state } =\n input;\n const runtime = state.runtime;\n const encapsulationKeyPair = runtime.crypto.encapsulationKeyPair;\n if (\n runtime.infra.dbStatus !== \"ready\" ||\n !state.snapshot.ready ||\n !runtime.state.online ||\n !runtime.auth.isAuthenticated ||\n !encapsulationKeyPair\n ) {\n return;\n }\n const domainScope = runtime.state.domainScope;\n const isCurrent = captureStructuralSyncGeneration(state, runtime);\n\n await reconcileRestoredAccess(isCurrent);\n if (!isCurrent()) {\n return;\n }\n const isOrganizationBlocked = createRemoteSyncBlocker(runtime);\n\n const createdContainerCount = await syncPendingContainerCreateIntents({\n host,\n isCurrent,\n isRemoteSyncBlocked: isOrganizationBlocked,\n requestRemoteReconciliation,\n state,\n });\n if (!isCurrent()) {\n return;\n }\n if (createdContainerCount > 0) {\n state.documentStoresNeedPriming = true;\n host.updateSnapshot();\n }\n const isCreatePending = await loadPendingCreatePredicate(state);\n if (!isCurrent()) {\n return;\n }\n\n const movedContainerCount = await syncPendingStoreContainerMoves({\n host,\n isCreatePending,\n isCurrent,\n isRemoteSyncBlocked: isOrganizationBlocked,\n requestRemoteReconciliation,\n state,\n });\n if (!isCurrent()) {\n return;\n }\n if (movedContainerCount > 0) {\n state.documentStoresNeedPriming = true;\n host.updateSnapshot();\n requestDomainDocumentSync(domainScope);\n requestContainerContentsStoreSync(state);\n }\n\n for (const containerState of Array.from(state.containersById.values())) {\n if (isCreatePending(containerState.container.id)) continue;\n await syncSingleContainerMetadata({\n containerState,\n encapsulationKeyPair,\n host,\n isCurrent,\n state,\n syncContainerMetadata: input.syncContainerMetadata,\n });\n if (!isCurrent()) {\n return;\n }\n }\n\n // Root adoption notifies session consumers synchronously. Let container\n // metadata converge first so a recovered system container is never exposed\n // under its placeholder name when the active root changes.\n await runContainerDocumentWork({\n isCurrent,\n onContextChanged: () => {\n if (isCurrent()) {\n requestContainerContentsStoreSync(state);\n }\n },\n onDocumentsMoved: () => {\n if (isCurrent()) {\n requestDomainDocumentSync(domainScope);\n requestContainerContentsStoreSync(state);\n }\n },\n primeDocuments: () => primeStoreDocuments(state, isCurrent),\n recoverStaleRoot: () => recoverStoreStaleRoot(state, isCurrent),\n shouldPrimeDocuments: () => state.documentStoresNeedPriming,\n // Document move intents live in the structural phase because they may\n // target containers created locally in the same session, such as Trash.\n // Root recovery rewrites stale endpoints and returns their intents to\n // pending, so replay follows recovery in this same pass.\n syncPendingDocumentMoves: () =>\n syncPendingStoreDocumentMoves({\n isCreatePending,\n isCurrent,\n isRemoteSyncBlocked: isOrganizationBlocked,\n state,\n }),\n });\n}\n"]}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The server holds a move this pass did not settle. Keep the intent queued,
3
+ * report it with a fixed message (the error's call site names the path), and
4
+ * ask for a later pass: its replay finds the move applied and settles it
5
+ * without posting again, so no reload or unrelated write is needed.
6
+ */
7
+ export function leaveAcceptedMoveQueued(syncInput, message, error) {
8
+ try {
9
+ const report = syncInput.state.runtime.util.logError;
10
+ if (report)
11
+ void Promise.resolve(report(message, error)).catch(() => { });
12
+ }
13
+ catch {
14
+ // A host logger must not change the replay result.
15
+ }
16
+ syncInput.acceptedMoveRetry.arm(syncInput.intent.id);
17
+ }
18
+ export const STALE_ACCEPTED_MOVE = "Container contents: a stale sync pass dropped an accepted container move";
19
+ /** persistAcceptedMoveIntent already asked to reconcile a stale result. */
20
+ export function unpersistedAcceptedMove(syncInput) {
21
+ if (!syncInput.isCurrent()) {
22
+ leaveAcceptedMoveQueued(syncInput, STALE_ACCEPTED_MOVE, new Error("Stale pass dropped a persisting container move"));
23
+ return "abandoned";
24
+ }
25
+ leaveAcceptedMoveQueued(syncInput, "Container contents: an accepted container move did not persist", new Error("Accepted container move did not persist"));
26
+ return "failed";
27
+ }
28
+ /** The server accepted, but this current pass got no result to settle. */
29
+ export function unacknowledgedAcceptedMove(syncInput) {
30
+ leaveAcceptedMoveQueued(syncInput, "Container contents: an accepted container move was not acknowledged", new Error("Accepted container move was not acknowledged"));
31
+ return "failed";
32
+ }
33
+ /**
34
+ * Settles from the verified head the server already holds. A head carries no
35
+ * server timestamps, so the container keeps the ones this device holds.
36
+ */
37
+ export function appliedRemoteContainer(containerState, applied) {
38
+ const { container } = containerState;
39
+ const createdAt = container.serverCreatedAt ?? container.createdAt;
40
+ const updatedAt = container.serverUpdatedAt ?? container.updatedAt;
41
+ if (!createdAt || !updatedAt) {
42
+ throw new Error("A synced container must carry its server timestamps");
43
+ }
44
+ const { state } = applied;
45
+ return {
46
+ createdAt,
47
+ effectiveAccessLevel: "admin",
48
+ id: state.containerId,
49
+ metadataAccessEpoch: state.epoch,
50
+ metadataAccessStateHash: applied.manifestHash,
51
+ metadataDocumentId: state.metadataDocumentId,
52
+ // Verifying the head already warmed the policies it references.
53
+ metadataReferencedPrincipals: [],
54
+ organizationId: state.organizationId,
55
+ parentId: state.parentContainerId,
56
+ updatedAt,
57
+ };
58
+ }
59
+ //# sourceMappingURL=acceptedMoveSettlement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acceptedMoveSettlement.js","sourceRoot":"","sources":["acceptedMoveSettlement.ts"],"names":[],"mappings":"AASA;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,SAA4B,EAC5B,OAAe,EACf,KAAY;IAEZ,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;QACrD,IAAI,MAAM;YAAE,KAAK,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC3E,CAAC;IAAC,MAAM,CAAC;QACP,mDAAmD;IACrD,CAAC;IACD,SAAS,CAAC,iBAAiB,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,CAAC,MAAM,mBAAmB,GAC9B,0EAA0E,CAAC;AAE7E,2EAA2E;AAC3E,MAAM,UAAU,uBAAuB,CACrC,SAA4B;IAE5B,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,CAAC;QAC3B,uBAAuB,CACrB,SAAS,EACT,mBAAmB,EACnB,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAC5D,CAAC;QACF,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,uBAAuB,CACrB,SAAS,EACT,gEAAgE,EAChE,IAAI,KAAK,CAAC,yCAAyC,CAAC,CACrD,CAAC;IACF,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,0BAA0B,CACxC,SAA4B;IAE5B,uBAAuB,CACrB,SAAS,EACT,qEAAqE,EACrE,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAC1D,CAAC;IACF,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CACpC,cAA8B,EAC9B,OAA6B;IAE7B,MAAM,EAAE,SAAS,EAAE,GAAG,cAAc,CAAC;IACrC,MAAM,SAAS,GAAG,SAAS,CAAC,eAAe,IAAI,SAAS,CAAC,SAAS,CAAC;IACnE,MAAM,SAAS,GAAG,SAAS,CAAC,eAAe,IAAI,SAAS,CAAC,SAAS,CAAC;IACnE,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAC1B,OAAO;QACL,SAAS;QACT,oBAAoB,EAAE,OAAO;QAC7B,EAAE,EAAE,KAAK,CAAC,WAAW;QACrB,mBAAmB,EAAE,KAAK,CAAC,KAAK;QAChC,uBAAuB,EAAE,OAAO,CAAC,YAAY;QAC7C,kBAAkB,EAAE,KAAK,CAAC,kBAAkB;QAC5C,gEAAgE;QAChE,4BAA4B,EAAE,EAAE;QAChC,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,QAAQ,EAAE,KAAK,CAAC,iBAAiB;QACjC,SAAS;KACV,CAAC;AACJ,CAAC","sourcesContent":["import type { AppliedContainerMove } from \"../../../containers\";\nimport type { ContainerState, RemoteContainer } from \"../../remoteHydration\";\nimport type { ContainerMoveIntentSyncInput } from \"../types\";\n\ntype AcceptedMoveInput = Pick<\n ContainerMoveIntentSyncInput,\n \"acceptedMoveRetry\" | \"intent\" | \"isCurrent\" | \"state\"\n>;\n\n/**\n * The server holds a move this pass did not settle. Keep the intent queued,\n * report it with a fixed message (the error's call site names the path), and\n * ask for a later pass: its replay finds the move applied and settles it\n * without posting again, so no reload or unrelated write is needed.\n */\nexport function leaveAcceptedMoveQueued(\n syncInput: AcceptedMoveInput,\n message: string,\n error: Error,\n): void {\n try {\n const report = syncInput.state.runtime.util.logError;\n if (report) void Promise.resolve(report(message, error)).catch(() => {});\n } catch {\n // A host logger must not change the replay result.\n }\n syncInput.acceptedMoveRetry.arm(syncInput.intent.id);\n}\n\nexport const STALE_ACCEPTED_MOVE =\n \"Container contents: a stale sync pass dropped an accepted container move\";\n\n/** persistAcceptedMoveIntent already asked to reconcile a stale result. */\nexport function unpersistedAcceptedMove(\n syncInput: AcceptedMoveInput,\n): \"abandoned\" | \"failed\" {\n if (!syncInput.isCurrent()) {\n leaveAcceptedMoveQueued(\n syncInput,\n STALE_ACCEPTED_MOVE,\n new Error(\"Stale pass dropped a persisting container move\"),\n );\n return \"abandoned\";\n }\n leaveAcceptedMoveQueued(\n syncInput,\n \"Container contents: an accepted container move did not persist\",\n new Error(\"Accepted container move did not persist\"),\n );\n return \"failed\";\n}\n\n/** The server accepted, but this current pass got no result to settle. */\nexport function unacknowledgedAcceptedMove(\n syncInput: AcceptedMoveInput,\n): \"failed\" {\n leaveAcceptedMoveQueued(\n syncInput,\n \"Container contents: an accepted container move was not acknowledged\",\n new Error(\"Accepted container move was not acknowledged\"),\n );\n return \"failed\";\n}\n\n/**\n * Settles from the verified head the server already holds. A head carries no\n * server timestamps, so the container keeps the ones this device holds.\n */\nexport function appliedRemoteContainer(\n containerState: ContainerState,\n applied: AppliedContainerMove,\n): RemoteContainer {\n const { container } = containerState;\n const createdAt = container.serverCreatedAt ?? container.createdAt;\n const updatedAt = container.serverUpdatedAt ?? container.updatedAt;\n if (!createdAt || !updatedAt) {\n throw new Error(\"A synced container must carry its server timestamps\");\n }\n const { state } = applied;\n return {\n createdAt,\n effectiveAccessLevel: \"admin\",\n id: state.containerId,\n metadataAccessEpoch: state.epoch,\n metadataAccessStateHash: applied.manifestHash,\n metadataDocumentId: state.metadataDocumentId,\n // Verifying the head already warmed the policies it references.\n metadataReferencedPrincipals: [],\n organizationId: state.organizationId,\n parentId: state.parentContainerId,\n updatedAt,\n };\n}\n"]}
@@ -1,10 +1,11 @@
1
- import { ContainerPathTooDeepError } from "../../../data/containers/shared/containerPathLimits.js";
2
- import { reportAndRethrowKeyingVerificationError } from "../../../data/keyingProjectionVerification/error.js";
3
- import { errorMessage } from "../../../data/shared/errorMessage.js";
4
- import { createRuntimePrincipalPolicyWarmer } from "../../principals/runtimePolicyWarmer.js";
5
- import { createDetachedContainerMetadataState, installDetachedContainerMetadataState, } from "../metadata/metadataStateIsolation.js";
6
- import { hasRemoteContainerMetadataState } from "../remoteHydration/reconciliation.js";
7
- import { moveRemoteContainer } from "./remote.js";
1
+ import { ContainerPathTooDeepError } from "../../../../data/containers/shared/containerPathLimits.js";
2
+ import { reportAndRethrowKeyingVerificationError } from "../../../../data/keyingProjectionVerification/error.js";
3
+ import { errorMessage } from "../../../../data/shared/errorMessage.js";
4
+ import { createRuntimePrincipalPolicyWarmer } from "../../../principals/runtimePolicyWarmer.js";
5
+ import { createDetachedContainerMetadataState, installDetachedContainerMetadataState, } from "../../metadata/metadataStateIsolation.js";
6
+ import { hasRemoteContainerMetadataState } from "../../remoteHydration/reconciliation.js";
7
+ import { moveRemoteContainer } from "../remote.js";
8
+ import { appliedRemoteContainer, leaveAcceptedMoveQueued, STALE_ACCEPTED_MOVE, unacknowledgedAcceptedMove, unpersistedAcceptedMove, } from "./acceptedMoveSettlement.js";
8
9
  function currentMoveResult(isCurrent, result) {
9
10
  return isCurrent() ? result : "abandoned";
10
11
  }
@@ -163,30 +164,69 @@ async function abandonTooDeepMove(syncInput) {
163
164
  state.runtime.util.log(`Container contents: abandoned queued move of ${intent.containerId}; its path would exceed the maximum depth`);
164
165
  return "failed";
165
166
  }
167
+ /** A replay that threw: refused for depth, tampered, or left unsettled. */
168
+ async function failedMoveReplay(input) {
169
+ const { abandonAppliedMove, error, parentState, remoteMoveApplied, syncInput, } = input;
170
+ const { intent, state } = syncInput;
171
+ if (!syncInput.isCurrent()) {
172
+ return abandonAppliedMove();
173
+ }
174
+ if (error instanceof ContainerPathTooDeepError)
175
+ return abandonTooDeepMove(syncInput);
176
+ // Even a refused acknowledgement leaves the move on the server; its replay
177
+ // verifies the head again before settling.
178
+ if (remoteMoveApplied && error instanceof Error)
179
+ leaveAcceptedMoveQueued(syncInput, "Container contents: an accepted container move failed to settle", error);
180
+ await reportAndRethrowKeyingVerificationError(error, state.runtime.util.reportSecurityIncident, {
181
+ objectId: intent.containerId,
182
+ objectKind: "container",
183
+ operation: "container.move.replay",
184
+ organizationId: parentState.container.organizationId,
185
+ });
186
+ if (!syncInput.isCurrent()) {
187
+ return abandonAppliedMove();
188
+ }
189
+ await recordPendingMoveIntentError({
190
+ containerId: intent.containerId,
191
+ expectedIntentId: intent.id,
192
+ expectedUpdatedAt: intent.updatedAt,
193
+ isCurrent: syncInput.isCurrent,
194
+ message: `Failed to sync container move: ${errorMessage(error)}`,
195
+ state,
196
+ });
197
+ return currentMoveResult(syncInput.isCurrent, "failed");
198
+ }
166
199
  async function movePendingRemoteContainer(input) {
167
- const { parentState, syncInput } = input;
200
+ const { containerState, parentState, syncInput } = input;
168
201
  const { host, intent, state } = syncInput;
169
202
  let remoteMoveApplied = false;
170
203
  let remoteParentId = intent.parentContainerId;
171
204
  const abandonAppliedMove = () => {
172
205
  if (remoteMoveApplied) {
206
+ leaveAcceptedMoveQueued(syncInput, STALE_ACCEPTED_MOVE, new Error("Stale pass dropped an accepted container move"));
173
207
  syncInput.requestRemoteReconciliation(remoteParentId);
174
208
  }
175
209
  return "abandoned";
176
210
  };
177
211
  try {
178
- const moved = await moveRemoteContainer({
212
+ const replayed = await moveRemoteContainer({
179
213
  containerId: intent.containerId,
214
+ onAccepted: () => {
215
+ remoteMoveApplied = true;
216
+ },
180
217
  parentContainerId: intent.parentContainerId,
181
218
  resolveProjectionUserKey: state.resolveProjectionUserKey,
182
219
  runtime: state.runtime,
183
220
  stillCurrent: syncInput.isCurrent,
184
221
  });
185
- if (!moved) {
222
+ if (!replayed) {
186
223
  if (!syncInput.isCurrent()) {
187
- syncInput.requestRemoteReconciliation(intent.parentContainerId);
188
- return "abandoned";
224
+ if (!remoteMoveApplied)
225
+ syncInput.requestRemoteReconciliation(intent.parentContainerId);
226
+ return abandonAppliedMove();
189
227
  }
228
+ if (remoteMoveApplied)
229
+ return unacknowledgedAcceptedMove(syncInput);
190
230
  await recordPendingMoveIntentError({
191
231
  containerId: intent.containerId,
192
232
  expectedIntentId: intent.id,
@@ -198,6 +238,9 @@ async function movePendingRemoteContainer(input) {
198
238
  return currentMoveResult(syncInput.isCurrent, "failed");
199
239
  }
200
240
  remoteMoveApplied = true;
241
+ const moved = "applied" in replayed
242
+ ? appliedRemoteContainer(containerState, replayed.applied)
243
+ : replayed.moved;
201
244
  remoteParentId = moved.parentId;
202
245
  if (!syncInput.isCurrent())
203
246
  return abandonAppliedMove();
@@ -209,36 +252,21 @@ async function movePendingRemoteContainer(input) {
209
252
  requestRemoteReconciliation: syncInput.requestRemoteReconciliation,
210
253
  state,
211
254
  });
212
- if (!persisted) {
213
- return currentMoveResult(syncInput.isCurrent, "failed");
214
- }
215
- state.runtime.util.log(`Container contents: synced queued container move ${intent.containerId}`);
255
+ if (!persisted)
256
+ return unpersistedAcceptedMove(syncInput);
257
+ state.runtime.util.log("applied" in replayed
258
+ ? `Container contents: settled queued container move ${intent.containerId} the server already held`
259
+ : `Container contents: synced queued container move ${intent.containerId}`);
216
260
  return "moved";
217
261
  }
218
262
  catch (error) {
219
- if (!syncInput.isCurrent()) {
220
- return abandonAppliedMove();
221
- }
222
- if (error instanceof ContainerPathTooDeepError)
223
- return abandonTooDeepMove(syncInput);
224
- await reportAndRethrowKeyingVerificationError(error, state.runtime.util.reportSecurityIncident, {
225
- objectId: intent.containerId,
226
- objectKind: "container",
227
- operation: "container.move.replay",
228
- organizationId: parentState.container.organizationId,
229
- });
230
- if (!syncInput.isCurrent()) {
231
- return abandonAppliedMove();
232
- }
233
- await recordPendingMoveIntentError({
234
- containerId: intent.containerId,
235
- expectedIntentId: intent.id,
236
- expectedUpdatedAt: intent.updatedAt,
237
- isCurrent: syncInput.isCurrent,
238
- message: `Failed to sync container move: ${errorMessage(error)}`,
239
- state,
263
+ return failedMoveReplay({
264
+ abandonAppliedMove,
265
+ error,
266
+ parentState,
267
+ remoteMoveApplied,
268
+ syncInput,
240
269
  });
241
- return currentMoveResult(syncInput.isCurrent, "failed");
242
270
  }
243
271
  }
244
272
  async function trySyncPendingContainerMoveIntent(input) {
@@ -306,7 +334,11 @@ async function trySyncPendingContainerMoveIntent(input) {
306
334
  });
307
335
  return currentMoveResult(input.isCurrent, "blocked");
308
336
  }
309
- return movePendingRemoteContainer({ parentState, syncInput: input });
337
+ return movePendingRemoteContainer({
338
+ containerState,
339
+ parentState,
340
+ syncInput: input,
341
+ });
310
342
  }
311
343
  export async function syncPendingContainerMoveIntents(input) {
312
344
  if (!input.isCurrent()) {
@@ -331,6 +363,7 @@ export async function syncPendingContainerMoveIntents(input) {
331
363
  isRemoteSyncBlocked: input.isRemoteSyncBlocked,
332
364
  intent,
333
365
  requestRemoteReconciliation: input.requestRemoteReconciliation,
366
+ acceptedMoveRetry: input.acceptedMoveRetry,
334
367
  state,
335
368
  });
336
369
  if (result === "abandoned") {
@@ -339,6 +372,11 @@ export async function syncPendingContainerMoveIntents(input) {
339
372
  if (result === "moved") {
340
373
  movedCount += 1;
341
374
  }
375
+ else {
376
+ // A replay that failed before reaching the server keeps an earlier
377
+ // acceptance's retries going.
378
+ input.acceptedMoveRetry.resume(intent.id);
379
+ }
342
380
  }
343
381
  return movedCount;
344
382
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moveIntentSync.js","sourceRoot":"","sources":["moveIntentSync.ts"],"names":[],"mappings":"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","sourcesContent":["import { ContainerPathTooDeepError } from \"../../../../data/containers/shared/containerPathLimits\";\nimport { reportAndRethrowKeyingVerificationError } from \"../../../../data/keyingProjectionVerification/error\";\nimport { errorMessage } from \"../../../../data/shared/errorMessage\";\nimport { createRuntimePrincipalPolicyWarmer } from \"../../../principals/runtimePolicyWarmer\";\nimport {\n createDetachedContainerMetadataState,\n installDetachedContainerMetadataState,\n} from \"../../metadata/metadataStateIsolation\";\nimport type { ContainerState, RemoteContainer } from \"../../remoteHydration\";\nimport { hasRemoteContainerMetadataState } from \"../../remoteHydration/reconciliation\";\nimport { moveRemoteContainer } from \"../remote\";\nimport type {\n AcceptedMoveRetry,\n ContainerMoveIntentSyncHost,\n ContainerMoveIntentSyncInput,\n ContainerMoveIntentSyncState,\n} from \"../types\";\nimport {\n appliedRemoteContainer,\n leaveAcceptedMoveQueued,\n STALE_ACCEPTED_MOVE,\n unacknowledgedAcceptedMove,\n unpersistedAcceptedMove,\n} from \"./acceptedMoveSettlement\";\n\ntype MoveIntentSyncResult = \"abandoned\" | \"blocked\" | \"failed\" | \"moved\";\n\nfunction currentMoveResult<\n Result extends Exclude<MoveIntentSyncResult, \"abandoned\">,\n>(isCurrent: () => boolean, result: Result): \"abandoned\" | Result {\n return isCurrent() ? result : \"abandoned\";\n}\n\nasync function recordPendingMoveIntentError(input: {\n blocked?: boolean | undefined;\n containerId: string;\n expectedIntentId: string;\n expectedUpdatedAt: string;\n isCurrent: () => boolean;\n message: string;\n state: ContainerMoveIntentSyncState;\n}): Promise<boolean> {\n if (!input.isCurrent()) {\n return false;\n }\n const execSql = input.state.runtime.infra.execSql;\n await input.state.persistence.recordMoveIntentError(execSql, {\n blocked: input.blocked,\n containerId: input.containerId,\n expectedIntentId: input.expectedIntentId,\n expectedUpdatedAt: input.expectedUpdatedAt,\n message: input.message,\n stillCurrent: input.isCurrent,\n });\n return input.isCurrent();\n}\n\nasync function resolveMoveIntentLocalUpdatedAt(input: {\n containerId: string;\n isCurrent: () => boolean;\n remoteUpdatedAt: string;\n state: ContainerMoveIntentSyncState;\n}): Promise<string | null> {\n if (!input.isCurrent()) {\n return null;\n }\n const previousLocalUpdatedAt =\n input.state.containersById.get(input.containerId)?.container\n .localUpdatedAt ?? null;\n if (\n !previousLocalUpdatedAt ||\n previousLocalUpdatedAt.localeCompare(input.remoteUpdatedAt) <= 0\n ) {\n return input.remoteUpdatedAt;\n }\n\n const execSql = input.state.runtime.infra.execSql;\n const containersWithPendingMetadataUpdates =\n await input.state.persistence.listContainerIdsWithPendingUpdates(execSql, [\n input.containerId,\n ]);\n if (!input.isCurrent()) {\n return null;\n }\n\n return containersWithPendingMetadataUpdates.includes(input.containerId)\n ? previousLocalUpdatedAt\n : input.remoteUpdatedAt;\n}\n\nexport async function persistAcceptedMoveIntent(input: {\n host: ContainerMoveIntentSyncHost;\n isCurrent: () => boolean;\n intent: ContainerMoveIntentSyncInput[\"intent\"];\n moved: RemoteContainer;\n requestRemoteReconciliation: (parentContainerId: string | null) => void;\n state: ContainerMoveIntentSyncState;\n}): Promise<boolean> {\n const { host, intent, moved, state } = input;\n const abandon = () => {\n input.requestRemoteReconciliation(moved.parentId);\n return false;\n };\n if (!input.isCurrent()) return abandon();\n const containerState = state.containersById.get(intent.containerId);\n if (!containerState) {\n return false;\n }\n\n await createRuntimePrincipalPolicyWarmer(state.runtime)({\n organizationId: moved.organizationId,\n references: moved.metadataReferencedPrincipals,\n stillCurrent: input.isCurrent,\n });\n if (!input.isCurrent()) {\n return abandon();\n }\n const localUpdatedAt = await resolveMoveIntentLocalUpdatedAt({\n containerId: intent.containerId,\n isCurrent: input.isCurrent,\n remoteUpdatedAt: moved.updatedAt,\n state,\n });\n if (!input.isCurrent() || localUpdatedAt === null) {\n return abandon();\n }\n const persistenceCandidate =\n await createDetachedContainerMetadataState(containerState);\n if (!input.isCurrent()) {\n return abandon();\n }\n persistenceCandidate.container = {\n ...persistenceCandidate.container,\n createdAt: moved.createdAt,\n serverCreatedAt: moved.createdAt,\n serverUpdatedAt: moved.updatedAt,\n updatedAt: moved.updatedAt,\n };\n const persistenceResult = await host.persistContainerState(\n persistenceCandidate,\n {\n accessEpoch: moved.metadataAccessEpoch,\n accessStateHash: moved.metadataAccessStateHash,\n documentId: moved.metadataDocumentId,\n metadataDocumentId: moved.metadataDocumentId,\n organizationId: moved.organizationId,\n parentId: moved.parentId,\n systemSlot: moved.systemSlot ?? null,\n },\n false,\n {\n localUpdatedAt,\n serverTimestamps: {\n createdAt: moved.createdAt,\n updatedAt: moved.updatedAt,\n },\n },\n {\n expectedStateWhenMissing: containerState,\n isCurrent: input.isCurrent,\n moveIntentSettlement: {\n containerId: intent.containerId,\n expectedIntentId: intent.id,\n expectedUpdatedAt: intent.updatedAt,\n },\n },\n );\n if (!input.isCurrent() || persistenceResult.status !== \"persisted\") {\n return abandon();\n }\n if (persistenceResult.moveIntentSettled !== true) {\n throw new Error(\n \"Container move persistence must settle the intent atomically\",\n );\n }\n if (!input.isCurrent()) return abandon();\n const { record: nextRecord } = persistenceResult;\n installDetachedContainerMetadataState(containerState, persistenceCandidate, {\n candidateRecord: nextRecord,\n preserveConcurrentMetadataEdit: true,\n });\n containerState.container = {\n ...containerState.container,\n metadataDocumentId: moved.metadataDocumentId,\n organizationId: moved.organizationId,\n parentId: moved.parentId,\n };\n return true;\n}\n\n/**\n * A move refused for path length never succeeds; the queue-time check saw only\n * the descendants this device holds. Drop the queued move and restore the\n * previous parent together: the server row never changed, so no listing would\n * ever put the folder back.\n */\nasync function abandonTooDeepMove(\n syncInput: ContainerMoveIntentSyncInput,\n): Promise<MoveIntentSyncResult> {\n const { host, intent, state } = syncInput;\n const previousParentId = intent.previousParentContainerId;\n if (previousParentId === null) {\n throw new Error(\"A queued container move must name its previous parent\");\n }\n const abandoned = await state.persistence.abandonMoveIntentRevision(\n state.runtime.infra.execSql,\n {\n containerId: intent.containerId,\n expectedIntentId: intent.id,\n expectedUpdatedAt: intent.updatedAt,\n previousParentContainerId: previousParentId,\n stillCurrent: syncInput.isCurrent,\n },\n );\n if (!syncInput.isCurrent()) return \"abandoned\";\n // A newer local move superseded this revision; it replays on its own.\n if (!abandoned) return \"failed\";\n const containerState = state.containersById.get(intent.containerId);\n if (containerState) {\n containerState.container = {\n ...containerState.container,\n parentId: previousParentId,\n };\n }\n host.updateSnapshot();\n state.runtime.util.log(\n `Container contents: abandoned queued move of ${intent.containerId}; its path would exceed the maximum depth`,\n );\n return \"failed\";\n}\n\n/** A replay that threw: refused for depth, tampered, or left unsettled. */\nasync function failedMoveReplay(input: {\n abandonAppliedMove: () => \"abandoned\";\n error: unknown;\n parentState: ContainerState;\n remoteMoveApplied: boolean;\n syncInput: ContainerMoveIntentSyncInput;\n}): Promise<MoveIntentSyncResult> {\n const {\n abandonAppliedMove,\n error,\n parentState,\n remoteMoveApplied,\n syncInput,\n } = input;\n const { intent, state } = syncInput;\n if (!syncInput.isCurrent()) {\n return abandonAppliedMove();\n }\n if (error instanceof ContainerPathTooDeepError)\n return abandonTooDeepMove(syncInput);\n // Even a refused acknowledgement leaves the move on the server; its replay\n // verifies the head again before settling.\n if (remoteMoveApplied && error instanceof Error)\n leaveAcceptedMoveQueued(\n syncInput,\n \"Container contents: an accepted container move failed to settle\",\n error,\n );\n await reportAndRethrowKeyingVerificationError(\n error,\n state.runtime.util.reportSecurityIncident,\n {\n objectId: intent.containerId,\n objectKind: \"container\",\n operation: \"container.move.replay\",\n organizationId: parentState.container.organizationId,\n },\n );\n if (!syncInput.isCurrent()) {\n return abandonAppliedMove();\n }\n await recordPendingMoveIntentError({\n containerId: intent.containerId,\n expectedIntentId: intent.id,\n expectedUpdatedAt: intent.updatedAt,\n isCurrent: syncInput.isCurrent,\n message: `Failed to sync container move: ${errorMessage(error)}`,\n state,\n });\n return currentMoveResult(syncInput.isCurrent, \"failed\");\n}\n\nasync function movePendingRemoteContainer(input: {\n containerState: ContainerState;\n parentState: ContainerState;\n syncInput: ContainerMoveIntentSyncInput;\n}): Promise<MoveIntentSyncResult> {\n const { containerState, parentState, syncInput } = input;\n const { host, intent, state } = syncInput;\n let remoteMoveApplied = false;\n let remoteParentId: string | null = intent.parentContainerId;\n const abandonAppliedMove = () => {\n if (remoteMoveApplied) {\n leaveAcceptedMoveQueued(\n syncInput,\n STALE_ACCEPTED_MOVE,\n new Error(\"Stale pass dropped an accepted container move\"),\n );\n syncInput.requestRemoteReconciliation(remoteParentId);\n }\n return \"abandoned\" as const;\n };\n try {\n const replayed = await moveRemoteContainer({\n containerId: intent.containerId,\n onAccepted: () => {\n remoteMoveApplied = true;\n },\n parentContainerId: intent.parentContainerId,\n resolveProjectionUserKey: state.resolveProjectionUserKey,\n runtime: state.runtime,\n stillCurrent: syncInput.isCurrent,\n });\n if (!replayed) {\n if (!syncInput.isCurrent()) {\n if (!remoteMoveApplied)\n syncInput.requestRemoteReconciliation(intent.parentContainerId);\n return abandonAppliedMove();\n }\n if (remoteMoveApplied) return unacknowledgedAcceptedMove(syncInput);\n await recordPendingMoveIntentError({\n containerId: intent.containerId,\n expectedIntentId: intent.id,\n expectedUpdatedAt: intent.updatedAt,\n isCurrent: syncInput.isCurrent,\n message: \"Remote container move was rejected or unavailable\",\n state,\n });\n return currentMoveResult(syncInput.isCurrent, \"failed\");\n }\n remoteMoveApplied = true;\n const moved =\n \"applied\" in replayed\n ? appliedRemoteContainer(containerState, replayed.applied)\n : replayed.moved;\n remoteParentId = moved.parentId;\n if (!syncInput.isCurrent()) return abandonAppliedMove();\n\n const persisted = await persistAcceptedMoveIntent({\n host,\n isCurrent: syncInput.isCurrent,\n intent,\n moved,\n requestRemoteReconciliation: syncInput.requestRemoteReconciliation,\n state,\n });\n if (!persisted) return unpersistedAcceptedMove(syncInput);\n state.runtime.util.log(\n \"applied\" in replayed\n ? `Container contents: settled queued container move ${intent.containerId} the server already held`\n : `Container contents: synced queued container move ${intent.containerId}`,\n );\n return \"moved\";\n } catch (error: unknown) {\n return failedMoveReplay({\n abandonAppliedMove,\n error,\n parentState,\n remoteMoveApplied,\n syncInput,\n });\n }\n}\n\nasync function trySyncPendingContainerMoveIntent(\n input: ContainerMoveIntentSyncInput,\n): Promise<MoveIntentSyncResult> {\n const { intent, state } = input;\n if (!input.isCurrent()) {\n return \"abandoned\";\n }\n const containerState = state.containersById.get(intent.containerId);\n const parentState = state.containersById.get(intent.parentContainerId);\n\n if (!containerState) {\n await recordPendingMoveIntentError({\n blocked: true,\n containerId: intent.containerId,\n expectedIntentId: intent.id,\n expectedUpdatedAt: intent.updatedAt,\n isCurrent: input.isCurrent,\n message: \"Container move intent references a missing local container\",\n state,\n });\n return currentMoveResult(input.isCurrent, \"failed\");\n }\n if (!parentState) {\n await recordPendingMoveIntentError({\n blocked: true,\n containerId: intent.containerId,\n expectedIntentId: intent.id,\n expectedUpdatedAt: intent.updatedAt,\n isCurrent: input.isCurrent,\n message:\n \"Container move intent references a missing destination parent container\",\n state,\n });\n return currentMoveResult(input.isCurrent, \"failed\");\n }\n if (input.isRemoteSyncBlocked(containerState.container.organizationId)) {\n return \"blocked\";\n }\n if (\n !hasRemoteContainerMetadataState(containerState) ||\n input.isCreatePending(intent.containerId)\n ) {\n // Source not synced yet is TRANSIENT, exactly like the destination case\n // below: the source's own create intent syncs first (createIntentSync runs\n // before moveIntentSync in a pass), but if that create fails on this pass\n // the source still lacks remote metadata here. Keep the intent 'pending'\n // (not 'blocked') so the queue reports a retryable failure rather than a\n // missing-dependency block; either way listUnsyncedMoveIntents replays it\n // once the create lands.\n await recordPendingMoveIntentError({\n containerId: intent.containerId,\n expectedIntentId: intent.id,\n expectedUpdatedAt: intent.updatedAt,\n isCurrent: input.isCurrent,\n message: \"Container move source is not synced yet\",\n state,\n });\n return currentMoveResult(input.isCurrent, \"blocked\");\n }\n if (\n !hasRemoteContainerMetadataState(parentState) ||\n input.isCreatePending(intent.parentContainerId)\n ) {\n await recordPendingMoveIntentError({\n containerId: intent.containerId,\n expectedIntentId: intent.id,\n expectedUpdatedAt: intent.updatedAt,\n isCurrent: input.isCurrent,\n message: \"Container move destination parent is not synced yet\",\n state,\n });\n return currentMoveResult(input.isCurrent, \"blocked\");\n }\n\n return movePendingRemoteContainer({\n containerState,\n parentState,\n syncInput: input,\n });\n}\n\nexport async function syncPendingContainerMoveIntents(input: {\n host: ContainerMoveIntentSyncHost;\n isCreatePending: (containerId: string) => boolean;\n isCurrent: () => boolean;\n isRemoteSyncBlocked: (organizationId: string) => boolean;\n requestRemoteReconciliation: (parentContainerId: string | null) => void;\n acceptedMoveRetry: AcceptedMoveRetry;\n state: ContainerMoveIntentSyncState;\n}): Promise<number> {\n if (!input.isCurrent()) {\n return 0;\n }\n const { host } = input;\n const state = { ...input.state };\n const execSql = state.runtime.infra.execSql;\n const pendingIntents =\n await state.persistence.listUnsyncedMoveIntents(execSql);\n if (!input.isCurrent()) {\n return 0;\n }\n let movedCount = 0;\n\n for (const intent of pendingIntents) {\n if (!input.isCurrent()) {\n return movedCount;\n }\n const result = await trySyncPendingContainerMoveIntent({\n host,\n isCurrent: input.isCurrent,\n isCreatePending: input.isCreatePending,\n isRemoteSyncBlocked: input.isRemoteSyncBlocked,\n intent,\n requestRemoteReconciliation: input.requestRemoteReconciliation,\n acceptedMoveRetry: input.acceptedMoveRetry,\n state,\n });\n if (result === \"abandoned\") {\n return movedCount;\n }\n\n if (result === \"moved\") {\n movedCount += 1;\n } else {\n // A replay that failed before reaching the server keeps an earlier\n // acceptance's retries going.\n input.acceptedMoveRetry.resume(intent.id);\n }\n }\n\n return movedCount;\n}\n"]}
@@ -0,0 +1,119 @@
1
+ import { listDocumentPlacementIntentStates } from "../../../../data/persistence/container-contents/documentPurgePlacement.js";
2
+ import { listDocumentsByContainerIdsOrDocumentIds } from "../../../../data/persistence/documents/documentsPersistence.js";
3
+ import { collectSubtreeLeafFirst } from "./purgeTreeCollection.js";
4
+ // A pass replays pending and blocked moves. A denied placement waits for the
5
+ // next store generation and an unavailable one never replays, so waiting on
6
+ // either would only reach the ceiling.
7
+ const REPLAYED_STATUSES = new Set(["pending", "blocked"]);
8
+ const DEFAULT_TIMING = { pollMs: 200, timeoutMs: 30_000 };
9
+ function delay(ms, signal) {
10
+ return new Promise((resolve) => {
11
+ const timer = setTimeout(done, ms);
12
+ function done() {
13
+ clearTimeout(timer);
14
+ signal?.removeEventListener("abort", done);
15
+ resolve();
16
+ }
17
+ signal?.addEventListener("abort", done, { once: true });
18
+ });
19
+ }
20
+ function createScopeReader(input) {
21
+ const containerIds = new Set(collectSubtreeLeafFirst(input.containersById, input.rootContainerId).map((containerState) => containerState.container.id));
22
+ let documentLocalIds = null;
23
+ // Listing the subtree's documents is the costly read, so it runs only once
24
+ // some document placement is queued anywhere on this device.
25
+ const readDocumentLocalIds = () => {
26
+ documentLocalIds ??= listDocumentsByContainerIdsOrDocumentIds(input.execSql, { containerIds: [...containerIds], documentIds: [] }).then((documents) => new Set(documents.map((document) => document.id)));
27
+ return documentLocalIds;
28
+ };
29
+ return async () => {
30
+ const moves = await input.persistence.listUnsyncedMoveIntents(input.execSql);
31
+ const placements = await listDocumentPlacementIntentStates(input.execSql);
32
+ const scoped = moves.filter((move) => containerIds.has(move.containerId));
33
+ if (placements.length === 0)
34
+ return scoped;
35
+ const localIds = await readDocumentLocalIds();
36
+ return [
37
+ ...scoped,
38
+ ...placements.filter((placement) => localIds.has(placement.localId)),
39
+ ];
40
+ };
41
+ }
42
+ const isParked = (intent) => !REPLAYED_STATUSES.has(intent.syncStatus);
43
+ /** Why a wait that began at `startedAt` must stop, or null to keep polling. */
44
+ function stopReason(input) {
45
+ const { intents, lane, startedAt } = input;
46
+ if (intents.some(isParked))
47
+ return {
48
+ error: new Error("Purge scope move was parked"),
49
+ message: "Container contents: a move in the purge scope was parked",
50
+ };
51
+ if (startedAt !== null &&
52
+ intents.some(({ lastAttemptedAt }) => lastAttemptedAt !== null && lastAttemptedAt >= startedAt))
53
+ return {
54
+ error: new Error("Purge scope move failed to sync"),
55
+ message: "Container contents: a move in the purge scope failed to sync",
56
+ };
57
+ if (startedAt !== null && lane && !lane.running && !lane.requested)
58
+ return {
59
+ error: new Error("Purge scope move stayed queued"),
60
+ message: "Container contents: a sync pass left a purge scope move queued",
61
+ };
62
+ if (Date.now() >= input.deadline)
63
+ return {
64
+ error: new Error("Purge scope move wait timed out"),
65
+ message: "Container contents: purge timed out waiting for a move",
66
+ };
67
+ return null;
68
+ }
69
+ /**
70
+ * The purge scope refuses a subtree whose moves the server has not accepted,
71
+ * so ask for a pass that replays them and wait for its verdict. The scope's
72
+ * own checks still run afterwards; this only gives the moves time to land.
73
+ * It ends unsettled once a pass requested here has tried and left one queued.
74
+ */
75
+ export async function settlePurgeScopeMoves(input) {
76
+ const timing = input.timing ?? DEFAULT_TIMING;
77
+ const cancelled = () => input.signal?.aborted === true || !input.stillCurrent();
78
+ const unsettled = (reason) => {
79
+ input.report(reason.message, reason.error);
80
+ input.onMoveSettlement?.("unsettled");
81
+ return "unsettled";
82
+ };
83
+ const readScope = createScopeReader(input);
84
+ let intents = await readScope();
85
+ if (cancelled())
86
+ return "cancelled";
87
+ if (intents.length === 0)
88
+ return "settled";
89
+ if (intents.some(isParked))
90
+ return unsettled({
91
+ error: new Error("Purge scope holds a parked move"),
92
+ message: "Container contents: purge scope holds a parked move",
93
+ });
94
+ input.onMoveSettlement?.("waiting");
95
+ const runCountAtRequest = input.lane.read()?.runCount ?? 0;
96
+ input.lane.request();
97
+ const deadline = Date.now() + timing.timeoutMs;
98
+ let startedAt = null;
99
+ while (true) {
100
+ await delay(timing.pollMs, input.signal);
101
+ if (cancelled())
102
+ return "cancelled";
103
+ // Read the lane first: a row still queued after a pass it shows finished
104
+ // survived that pass, while one read before could have settled since.
105
+ const lane = input.lane.read();
106
+ intents = await readScope();
107
+ if (cancelled())
108
+ return "cancelled";
109
+ if (intents.length === 0)
110
+ return "settled";
111
+ // Only a pass that starts after the request answers it.
112
+ if (lane && lane.runCount > runCountAtRequest)
113
+ startedAt ??= lane.lastStartedAt;
114
+ const reason = stopReason({ deadline, intents, lane, startedAt });
115
+ if (reason)
116
+ return unsettled(reason);
117
+ }
118
+ }
119
+ //# sourceMappingURL=purgeMoveSettlement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"purgeMoveSettlement.js","sourceRoot":"","sources":["purgeMoveSettlement.ts"],"names":[],"mappings":"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","sourcesContent":["import { listDocumentPlacementIntentStates } from \"../../../../data/persistence/container-contents/documentPurgePlacement\";\nimport { listDocumentsByContainerIdsOrDocumentIds } from \"../../../../data/persistence/documents/documentsPersistence\";\nimport type { ExecSql } from \"../../../../data/sqlite/sqlSchema\";\nimport type { ContainerContentsPersistence } from \"../../containerPersistence\";\nimport type { ContainerState } from \"../../remoteHydration\";\nimport type { PurgeMoveSettlement } from \"./purgeProgress\";\nimport { collectSubtreeLeafFirst } from \"./purgeTreeCollection\";\n\n/** The container-contents lane replays queued container and document moves. */\nexport interface PurgeMoveSyncLane {\n readonly read: () => {\n readonly lastStartedAt: string | null;\n readonly requested: boolean;\n readonly runCount: number;\n readonly running: boolean;\n } | null;\n readonly request: () => void;\n}\n\ninterface ScopedIntent {\n readonly lastAttemptedAt: string | null;\n readonly syncStatus: string;\n}\n\ninterface SettlePurgeScopeMovesInput {\n readonly containersById: ReadonlyMap<string, ContainerState>;\n readonly execSql: ExecSql;\n readonly lane: PurgeMoveSyncLane;\n readonly onMoveSettlement?:\n | ((settlement: PurgeMoveSettlement) => void)\n | undefined;\n readonly persistence: Pick<\n ContainerContentsPersistence,\n \"listUnsyncedMoveIntents\"\n >;\n /** Fixed messages only: the error's call site names the outcome. */\n readonly report: (message: string, error: Error) => void;\n readonly rootContainerId: string;\n readonly signal?: AbortSignal | undefined;\n readonly stillCurrent: () => boolean;\n readonly timing?: { pollMs: number; timeoutMs: number } | undefined;\n}\n\n// A pass replays pending and blocked moves. A denied placement waits for the\n// next store generation and an unavailable one never replays, so waiting on\n// either would only reach the ceiling.\nconst REPLAYED_STATUSES = new Set([\"pending\", \"blocked\"]);\nconst DEFAULT_TIMING = { pollMs: 200, timeoutMs: 30_000 };\n\nfunction delay(ms: number, signal: AbortSignal | undefined): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(done, ms);\n function done() {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", done);\n resolve();\n }\n signal?.addEventListener(\"abort\", done, { once: true });\n });\n}\n\nfunction createScopeReader(input: SettlePurgeScopeMovesInput) {\n const containerIds = new Set(\n collectSubtreeLeafFirst(input.containersById, input.rootContainerId).map(\n (containerState) => containerState.container.id,\n ),\n );\n let documentLocalIds: Promise<Set<string>> | null = null;\n // Listing the subtree's documents is the costly read, so it runs only once\n // some document placement is queued anywhere on this device.\n const readDocumentLocalIds = () => {\n documentLocalIds ??= listDocumentsByContainerIdsOrDocumentIds(\n input.execSql,\n { containerIds: [...containerIds], documentIds: [] },\n ).then((documents) => new Set(documents.map((document) => document.id)));\n return documentLocalIds;\n };\n return async (): Promise<ScopedIntent[]> => {\n const moves = await input.persistence.listUnsyncedMoveIntents(\n input.execSql,\n );\n const placements = await listDocumentPlacementIntentStates(input.execSql);\n const scoped: ScopedIntent[] = moves.filter((move) =>\n containerIds.has(move.containerId),\n );\n if (placements.length === 0) return scoped;\n const localIds = await readDocumentLocalIds();\n return [\n ...scoped,\n ...placements.filter((placement) => localIds.has(placement.localId)),\n ];\n };\n}\n\ntype LaneRun = ReturnType<PurgeMoveSyncLane[\"read\"]>;\n\nconst isParked = (intent: ScopedIntent) =>\n !REPLAYED_STATUSES.has(intent.syncStatus);\n\n/** Why a wait that began at `startedAt` must stop, or null to keep polling. */\nfunction stopReason(input: {\n deadline: number;\n intents: readonly ScopedIntent[];\n lane: LaneRun;\n startedAt: string | null;\n}): { error: Error; message: string } | null {\n const { intents, lane, startedAt } = input;\n if (intents.some(isParked))\n return {\n error: new Error(\"Purge scope move was parked\"),\n message: \"Container contents: a move in the purge scope was parked\",\n };\n if (\n startedAt !== null &&\n intents.some(\n ({ lastAttemptedAt }) =>\n lastAttemptedAt !== null && lastAttemptedAt >= startedAt,\n )\n )\n return {\n error: new Error(\"Purge scope move failed to sync\"),\n message: \"Container contents: a move in the purge scope failed to sync\",\n };\n if (startedAt !== null && lane && !lane.running && !lane.requested)\n return {\n error: new Error(\"Purge scope move stayed queued\"),\n message: \"Container contents: a sync pass left a purge scope move queued\",\n };\n if (Date.now() >= input.deadline)\n return {\n error: new Error(\"Purge scope move wait timed out\"),\n message: \"Container contents: purge timed out waiting for a move\",\n };\n return null;\n}\n\n/**\n * The purge scope refuses a subtree whose moves the server has not accepted,\n * so ask for a pass that replays them and wait for its verdict. The scope's\n * own checks still run afterwards; this only gives the moves time to land.\n * It ends unsettled once a pass requested here has tried and left one queued.\n */\nexport async function settlePurgeScopeMoves(\n input: SettlePurgeScopeMovesInput,\n): Promise<\"cancelled\" | \"settled\" | \"unsettled\"> {\n const timing = input.timing ?? DEFAULT_TIMING;\n const cancelled = () =>\n input.signal?.aborted === true || !input.stillCurrent();\n const unsettled = (reason: { error: Error; message: string }) => {\n input.report(reason.message, reason.error);\n input.onMoveSettlement?.(\"unsettled\");\n return \"unsettled\" as const;\n };\n const readScope = createScopeReader(input);\n let intents = await readScope();\n if (cancelled()) return \"cancelled\";\n if (intents.length === 0) return \"settled\";\n if (intents.some(isParked))\n return unsettled({\n error: new Error(\"Purge scope holds a parked move\"),\n message: \"Container contents: purge scope holds a parked move\",\n });\n\n input.onMoveSettlement?.(\"waiting\");\n const runCountAtRequest = input.lane.read()?.runCount ?? 0;\n input.lane.request();\n const deadline = Date.now() + timing.timeoutMs;\n let startedAt: string | null = null;\n while (true) {\n await delay(timing.pollMs, input.signal);\n if (cancelled()) return \"cancelled\";\n // Read the lane first: a row still queued after a pass it shows finished\n // survived that pass, while one read before could have settled since.\n const lane = input.lane.read();\n intents = await readScope();\n if (cancelled()) return \"cancelled\";\n if (intents.length === 0) return \"settled\";\n // Only a pass that starts after the request answers it.\n if (lane && lane.runCount > runCountAtRequest)\n startedAt ??= lane.lastStartedAt;\n const reason = stopReason({ deadline, intents, lane, startedAt });\n if (reason) return unsettled(reason);\n }\n}\n"]}
@@ -3,7 +3,15 @@ export interface PurgeProgress {
3
3
  readonly failedCount: number;
4
4
  readonly totalCount: number;
5
5
  }
6
+ /**
7
+ * A purge first waits for moves this device queued inside its scope: until the
8
+ * server holds them, the scope cannot prove what it would destroy. "waiting"
9
+ * reports the wait; "unsettled" means it ended before every move reached the
10
+ * server, and nothing was deleted.
11
+ */
12
+ export type PurgeMoveSettlement = "waiting" | "unsettled";
6
13
  export interface PurgeOptions {
14
+ onMoveSettlement?: ((settlement: PurgeMoveSettlement) => void) | undefined;
7
15
  onProgress?: ((progress: PurgeProgress) => void) | undefined;
8
16
  signal?: AbortSignal | undefined;
9
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"purgeProgress.js","sourceRoot":"","sources":["purgeProgress.ts"],"names":[],"mappings":"","sourcesContent":["// Progress + cancellation contract shared by the recursive purge engine and its\n// store wrappers (\"Delete Forever\" and \"Empty Trash\"). onProgress reports\n// ABSOLUTE counts (not deltas) so a determinate progress bar can render\n// completed/total directly; a fresh object is emitted each tick so a\n// referential-equality state setter never drops an update. signal cancels at the\n// next unit boundary — between whole documents / whole containers, never\n// mid-remote-call — so a cancelled run always stops on a consistent prefix that\n// re-running can safely resume.\nexport interface PurgeProgress {\n readonly completedCount: number;\n readonly failedCount: number;\n readonly totalCount: number;\n}\n\nexport interface PurgeOptions {\n onProgress?: ((progress: PurgeProgress) => void) | undefined;\n signal?: AbortSignal | undefined;\n}\n"]}
1
+ {"version":3,"file":"purgeProgress.js","sourceRoot":"","sources":["purgeProgress.ts"],"names":[],"mappings":"","sourcesContent":["// Progress + cancellation contract shared by the recursive purge engine and its\n// store wrappers (\"Delete Forever\" and \"Empty Trash\"). onProgress reports\n// ABSOLUTE counts (not deltas) so a determinate progress bar can render\n// completed/total directly; a fresh object is emitted each tick so a\n// referential-equality state setter never drops an update. signal cancels at the\n// next unit boundary — between whole documents / whole containers, never\n// mid-remote-call — so a cancelled run always stops on a consistent prefix that\n// re-running can safely resume.\nexport interface PurgeProgress {\n readonly completedCount: number;\n readonly failedCount: number;\n readonly totalCount: number;\n}\n\n/**\n * A purge first waits for moves this device queued inside its scope: until the\n * server holds them, the scope cannot prove what it would destroy. \"waiting\"\n * reports the wait; \"unsettled\" means it ended before every move reached the\n * server, and nothing was deleted.\n */\nexport type PurgeMoveSettlement = \"waiting\" | \"unsettled\";\n\nexport interface PurgeOptions {\n onMoveSettlement?: ((settlement: PurgeMoveSettlement) => void) | undefined;\n onProgress?: ((progress: PurgeProgress) => void) | undefined;\n signal?: AbortSignal | undefined;\n}\n"]}