@tearleads/client-sdk 0.1.24 → 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 (85) 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/internal/documentDeletionPersistence.js +7 -0
  8. package/data/persistence/documents/internal/documentDeletionPersistence.js.map +1 -1
  9. package/data/persistence/documents/types.d.ts +2 -1
  10. package/data/persistence/documents/types.js.map +1 -1
  11. package/data/persistence/principalCurrentAcknowledgementPersistence.d.ts +13 -2
  12. package/data/persistence/principalCurrentAcknowledgementPersistence.js +36 -20
  13. package/data/persistence/principalCurrentAcknowledgementPersistence.js.map +1 -1
  14. package/index.d.ts +2 -2
  15. package/index.js +1 -1
  16. package/index.js.map +1 -1
  17. package/internal/api-client/ApiClient.d.ts +6 -2
  18. package/internal/api-client/ApiClient.js +4 -5
  19. package/internal/api-client/ApiClient.js.map +1 -1
  20. package/internal/api-client/apiRequestRuntime.d.ts +1 -2
  21. package/internal/api-client/apiRequestRuntime.js +12 -15
  22. package/internal/api-client/apiRequestRuntime.js.map +1 -1
  23. package/internal/api-client/routes/containers/mutations.js +2 -2
  24. package/internal/api-client/routes/containers/mutations.js.map +1 -1
  25. package/internal/api-client/sessionRefresh.js +14 -6
  26. package/internal/api-client/sessionRefresh.js.map +1 -1
  27. package/internal/api-client/sessionRenewal.js +54 -0
  28. package/internal/api-client/sessionRenewal.js.map +1 -0
  29. package/internal/validators/operation/containerMutations.d.ts +3 -0
  30. package/internal/validators/operation/containerMutations.js +1 -0
  31. package/internal/validators/operation/containerMutations.js.map +1 -1
  32. package/package.json +1 -1
  33. package/workflows/container-contents/container-state/delete.js +2 -0
  34. package/workflows/container-contents/container-state/delete.js.map +1 -1
  35. package/workflows/container-contents/container-state/localPurgeScope.js +49 -0
  36. package/workflows/container-contents/container-state/localPurgeScope.js.map +1 -0
  37. package/workflows/container-contents/container-state/purgeTree.js +42 -7
  38. package/workflows/container-contents/container-state/purgeTree.js.map +1 -1
  39. package/workflows/container-contents/container-state/purgeTreeCollection.js +11 -0
  40. package/workflows/container-contents/container-state/purgeTreeCollection.js.map +1 -1
  41. package/workflows/container-contents/container-state/purgeTreeContainerDeletion.js +4 -1
  42. package/workflows/container-contents/container-state/purgeTreeContainerDeletion.js.map +1 -1
  43. package/workflows/container-contents/container-state/purgeTreeScope.js +158 -0
  44. package/workflows/container-contents/container-state/purgeTreeScope.js.map +1 -0
  45. package/workflows/container-contents/container-state/readLocalPurgeScope.js +18 -0
  46. package/workflows/container-contents/container-state/readLocalPurgeScope.js.map +1 -0
  47. package/workflows/container-contents/container-state/remote.js +1 -0
  48. package/workflows/container-contents/container-state/remote.js.map +1 -1
  49. package/workflows/container-contents/container-state/types.js.map +1 -1
  50. package/workflows/container-contents/documentLinks.d.ts +4 -0
  51. package/workflows/container-contents/documentLinks.js +2 -0
  52. package/workflows/container-contents/documentLinks.js.map +1 -1
  53. package/workflows/container-contents/documentPurge.d.ts +6 -1
  54. package/workflows/container-contents/documentPurge.js +8 -1
  55. package/workflows/container-contents/documentPurge.js.map +1 -1
  56. package/workflows/documents/linkSet.d.ts +1 -0
  57. package/workflows/documents/linkSet.js +2 -0
  58. package/workflows/documents/linkSet.js.map +1 -1
  59. package/workflows/documents/linkSetRemote.d.ts +2 -0
  60. package/workflows/documents/linkSetRemote.js +3 -0
  61. package/workflows/documents/linkSetRemote.js.map +1 -1
  62. package/workflows/documents/purge.d.ts +3 -0
  63. package/workflows/documents/purge.js +7 -0
  64. package/workflows/documents/purge.js.map +1 -1
  65. package/workflows/documents/subtreePurgeScope.js +11 -0
  66. package/workflows/documents/subtreePurgeScope.js.map +1 -0
  67. package/workflows/organizations/acknowledgedPrincipalPredecessor.d.ts +39 -0
  68. package/workflows/organizations/acknowledgedPrincipalPredecessor.js +87 -0
  69. package/workflows/organizations/acknowledgedPrincipalPredecessor.js.map +1 -0
  70. package/workflows/organizations/currentOrganizationMutationRetention.js +13 -2
  71. package/workflows/organizations/currentOrganizationMutationRetention.js.map +1 -1
  72. package/workflows/organizations/index.d.ts +1 -0
  73. package/workflows/organizations/index.js +1 -0
  74. package/workflows/organizations/index.js.map +1 -1
  75. package/workflows/organizations/initialPrincipalCurrentPublication.js +3 -2
  76. package/workflows/organizations/initialPrincipalCurrentPublication.js.map +1 -1
  77. package/workflows/organizations/reconcileAcknowledgedPrincipalCurrent.js +63 -0
  78. package/workflows/organizations/reconcileAcknowledgedPrincipalCurrent.js.map +1 -0
  79. package/workflows/organizations/retainAcknowledgedPrincipalCurrents.d.ts +3 -0
  80. package/workflows/organizations/retainAcknowledgedPrincipalCurrents.js +33 -55
  81. package/workflows/organizations/retainAcknowledgedPrincipalCurrents.js.map +1 -1
  82. package/workflows/organizations/runtimeCurrentGroupMutation.js +13 -2
  83. package/workflows/organizations/runtimeCurrentGroupMutation.js.map +1 -1
  84. package/workflows/organizations/runtimeCurrentOrganizationMutation.js +2 -2
  85. package/workflows/organizations/runtimeCurrentOrganizationMutation.js.map +1 -1
@@ -153,6 +153,8 @@ export interface ContainerContentsPersistence extends DormantMetadataSweepPersis
153
153
  updatedAt?: string;
154
154
  }) => Promise<void>;
155
155
  deleteContainers: (execSql: ExecSql, removals: ReadonlyArray<ContainerRemoval>, options?: {
156
+ /** Must run inside the deletion transaction before any row is removed. */
157
+ beforeDeleteInTransaction?: ((transactionExecSql: ExecSql) => Promise<boolean>) | undefined;
156
158
  /** Unsigned discovery removes listings but preserves metadata and local intent. */
157
159
  discoveryOnly?: boolean;
158
160
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"containerContentsPersistenceTypes.js","sourceRoot":"","sources":["containerContentsPersistenceTypes.ts"],"names":[],"mappings":"AAYA,MAAM,CAAC,MAAM,4BAA4B,GAAG,kBAAkB,CAAC;AAC/D,MAAM,CAAC,MAAM,0BAA0B,GAAG,gBAAgB,CAAC","sourcesContent":["import type { AccessManifestCheckpoint } from \"@tearleads/crypto\";\nimport type { HeldContainerBinding } from \"../../containers/containerBinding\";\nimport type { DocumentSyncPullContinuation } from \"../../documents/shared/pullContinuation\";\nimport type {\n DocumentRecord,\n PendingUpdateFields,\n PendingUpdateRecord,\n} from \"../../sqlite/documentPersistence\";\nimport type { ExecSql } from \"../../sqlite/sqlSchema\";\nimport type { ContainerRecord } from \"../containers/containerPersistence\";\nimport type { DormantMetadataSweepPersistence } from \"./dormantMetadataSweep\";\n\nexport const CONTAINER_CREATE_INTENT_TYPE = \"container.create\";\nexport const CONTAINER_MOVE_INTENT_TYPE = \"container.move\";\n\nexport type ContainerCreateIntentSyncStatus = \"pending\" | \"synced\";\nexport type ContainerMoveIntentSyncStatus = \"pending\" | \"blocked\";\n\nexport interface ContainerCreateIntentRecord {\n id: string;\n containerId: string;\n parentContainerId: string;\n intentType: typeof CONTAINER_CREATE_INTENT_TYPE;\n syncStatus: ContainerCreateIntentSyncStatus;\n remoteContainerId: string | null;\n remoteMetadataDocumentId: string | null;\n remoteMetadataAccessStateHash: string | null;\n lastError: string | null;\n lastAttemptedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ContainerMoveIntentRecord {\n id: string;\n containerId: string;\n parentContainerId: string;\n previousParentContainerId: string | null;\n intentType: typeof CONTAINER_MOVE_INTENT_TYPE;\n syncStatus: ContainerMoveIntentSyncStatus;\n lastError: string | null;\n lastAttemptedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ContainerCreateIntentInput {\n id?: string;\n parentContainerId: string;\n}\n\nexport interface ContainerMoveIntentInput {\n id?: string;\n parentContainerId: string;\n previousParentContainerId?: string | null | undefined;\n}\n\nexport interface ContainerCreateIntentErrorInput {\n containerId: string;\n expectedIntentId: string;\n expectedUpdatedAt: string;\n message: string;\n stillCurrent?: (() => boolean) | undefined;\n}\n\nexport type ContainerCreateIntentErrorInputRecorder = (\n execSql: ExecSql,\n input: ContainerCreateIntentErrorInput,\n) => Promise<void>;\n\nexport interface ContainerMoveIntentRevisionInput {\n containerId: string;\n expectedIntentId: string;\n expectedUpdatedAt: string;\n stillCurrent: () => boolean;\n}\n\nexport interface ContainerCreateIntentRevisionInput\n extends ContainerMoveIntentRevisionInput {\n remoteContainerId: string;\n remoteMetadataAccessStateHash: string;\n remoteMetadataDocumentId: string;\n /** The remote parent now; a move settlement queues cites it as previous. */\n supersededMovePreviousParentId?: string | undefined;\n /**\n * The parent the create committed under. A desired parent equal to it, or\n * to the current one, owes no move: the user never moved the folder away,\n * and a later remote move by another writer stands.\n */\n createdParentContainerId?: string | undefined;\n /** The parent the settled intent wants. */\n desiredParentContainerId?: string | undefined;\n}\n\nexport interface LocalRootDescendantReparentInput {\n containerId: string;\n parentContainerId: string | null;\n updateCreateIntent?: boolean | undefined;\n}\n\n/**\n * A container's metadata document record plus its durable content: the full\n * Loro updates export of the metadata document, stored in the shared\n * history-checkpoint table under the container-metadata app kind. Metadata\n * documents are tiny registries, so the whole update log is the checkpoint.\n */\nexport interface ContainerMetadataRecord extends DocumentRecord {\n metadataUpdates: string;\n}\n\nexport interface StoredContainerState {\n container: ContainerRecord;\n record: ContainerMetadataRecord | null;\n}\n\nexport interface ContainerRemoval {\n containerId: string;\n reason: \"access_revoked\" | \"deleted\";\n updatedAt: string;\n}\n\nexport interface ContainerHydrationTombstone extends ContainerRemoval {\n generation: number;\n}\n\nexport interface ContainerDeletionGuard {\n containerId: string;\n expectedContainer: ContainerRecord | null;\n}\n\nexport interface SaveContainerOptions {\n createIntent?: ContainerCreateIntentInput;\n stillCurrent?: (() => boolean) | undefined;\n localUpdatedAt?: string;\n moveIntent?: ContainerMoveIntentInput | undefined;\n serverTimestamps?:\n | {\n createdAt?: string | null;\n updatedAt?: string | null;\n }\n | undefined;\n updatedAt?: string;\n}\n\nexport interface SaveContainerWithPendingUpdateOptions\n extends SaveContainerOptions {\n pendingUpdate: PendingUpdateFields;\n}\n\nexport interface ContainerContentsPersistence\n extends DormantMetadataSweepPersistence {\n containerExists: (execSql: ExecSql, containerId: string) => Promise<boolean>;\n /**\n * Create an absent remotely hydrated container only when no newer durable\n * tombstone or concurrent container/dormant-metadata mutation won first.\n */\n commitHydratedContainer: (\n execSql: ExecSql,\n input: {\n container: ContainerRecord;\n expectedDormantRecord: ContainerMetadataRecord | null;\n /** Tombstone observed before the remote request began, or null. */\n expectedHydrationTombstone?:\n | ContainerHydrationTombstone\n | null\n | undefined;\n /** Current placement verified and pinned before recovery insertion. */\n expectedPlacementCheckpoint?: AccessManifestCheckpoint | undefined;\n record: ContainerMetadataRecord;\n remoteUpdatedAt: string;\n saveOptions: {\n localUpdatedAt?: string;\n serverTimestamps?:\n | {\n createdAt?: string | null;\n updatedAt?: string | null;\n }\n | undefined;\n };\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<\n { committed: true; container: ContainerRecord } | { committed: false }\n >;\n /** Capture anti-resurrection fences immediately before a remote fetch. */\n loadContainerHydrationTombstones: (\n execSql: ExecSql,\n ) => Promise<ReadonlyArray<ContainerHydrationTombstone>>;\n deleteContainer: (\n execSql: ExecSql,\n containerId: string,\n options?: {\n reason?: ContainerRemoval[\"reason\"];\n updatedAt?: string;\n },\n ) => Promise<void>;\n deleteContainers: (\n execSql: ExecSql,\n removals: ReadonlyArray<ContainerRemoval>,\n options?: {\n /** Unsigned discovery removes listings but preserves metadata and local intent. */\n discoveryOnly?: boolean;\n /**\n * Request-time states rechecked inside the deletion transaction. An\n * absent or changed row means another pane won and aborts the cascade.\n */\n expectedContainers?: ReadonlyArray<ContainerDeletionGuard>;\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<ReadonlyArray<string>>;\n deletePendingUpdates: (\n execSql: ExecSql,\n containerId: string,\n ) => Promise<void>;\n deletePendingUpdate: (execSql: ExecSql, id: string) => Promise<void>;\n ensureSchema: (execSql: ExecSql) => Promise<void>;\n enqueuePendingUpdate: (\n execSql: ExecSql,\n input: PendingUpdateFields & { containerId: string },\n ) => Promise<string>;\n listPendingCreateIntents: (\n execSql: ExecSql,\n ) => Promise<ContainerCreateIntentRecord[]>;\n // Every move intent that has not yet synced, regardless of syncStatus —\n // a blocked move (destination parent not synced yet) is still unsynced, and\n // synced moves are deleted (see markMoveIntentRevisionSynced), so every surviving\n // row qualifies. Blocked intents replay too: \"blocked\" names the reason the\n // last attempt could not proceed, not a terminal verdict.\n listUnsyncedMoveIntents: (\n execSql: ExecSql,\n ) => Promise<ContainerMoveIntentRecord[]>;\n listContainerIdsWithPendingUpdates: (\n execSql: ExecSql,\n containerIds: ReadonlyArray<string>,\n ) => Promise<string[]>;\n listContainerIdsWithPullContinuations: (\n execSql: ExecSql,\n containerIds: ReadonlyArray<string>,\n ) => Promise<string[]>;\n listPendingUpdates: (\n execSql: ExecSql,\n containerId: string,\n ) => Promise<PendingUpdateRecord[]>;\n rekeyPendingUpdate: (execSql: ExecSql, id: string) => Promise<string | null>;\n /** Revision-CAS error recorder used by asynchronous create replay. */\n recordCreateIntentRevisionError: ContainerCreateIntentErrorInputRecorder;\n recordMoveIntentError: (\n execSql: ExecSql,\n input: {\n blocked?: boolean | undefined;\n containerId: string;\n expectedIntentId?: string | undefined;\n expectedUpdatedAt?: string | undefined;\n message: string;\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<void>;\n reassignContainerDocuments: (\n execSql: ExecSql,\n input: {\n fromContainerId: string;\n stillCurrent?: (() => boolean) | undefined;\n toContainerId: string;\n updatedAt?: string | undefined;\n },\n ) => Promise<void>;\n reconcileLocalRootContainer: (\n execSql: ExecSql,\n input: {\n descendantReparents: ReadonlyArray<LocalRootDescendantReparentInput>;\n localRootContainerId: string;\n remoteOrganizationId: string;\n remoteRootContainerId: string;\n stillCurrent?: (() => boolean) | undefined;\n updatedAt?: string | undefined;\n },\n ) => Promise<void>;\n reconcileLocalSystemContainer: (\n execSql: ExecSql,\n input: {\n localContainerId: string;\n remoteContainerId: string;\n remoteOrganizationId: string;\n stillCurrent?: (() => boolean) | undefined;\n updatedAt?: string | undefined;\n },\n ) => Promise<void>;\n loadContainers: (\n execSql: ExecSql,\n ) => Promise<ReadonlyArray<StoredContainerState>>;\n loadContainerMetadataState: (\n execSql: ExecSql,\n containerId: string,\n ) => Promise<StoredContainerState | null>;\n /**\n * The organization and metadata target this device holds for a container,\n * from its live row or retained dormant metadata; null when never held.\n */\n loadHeldContainerBinding: (\n execSql: ExecSql,\n containerId: string,\n ) => Promise<HeldContainerBinding | null>;\n /** Whether a held folder was re-homed away from this organization. */\n isSupersededContainerBinding: (\n execSql: ExecSql,\n input: { containerId: string; organizationId: string },\n ) => Promise<boolean>;\n /**\n * Move a held folder to the binding its own user re-created it under,\n * resetting the metadata record's remote stream; false if the held binding\n * changed since it was read.\n */\n rebindHeldContainer: (\n execSql: ExecSql,\n input: {\n containerId: string;\n expected: HeldContainerBinding;\n next: { metadataDocumentId: string; organizationId: string };\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<boolean>;\n /**\n * Load a container-metadata record by container id alone, without\n * requiring a containers row — the dormant shape row 4's access_revoked\n * branch leaves behind, re-attached on rehydration.\n */\n loadContainerMetadataRecord: (\n execSql: ExecSql,\n containerId: string,\n ) => Promise<ContainerMetadataRecord | null>;\n /**\n * Atomically replace the exact rejected metadata pull continuation with its\n * durable recovery marker and return the authoritative current record.\n */\n invalidateMetadataPullContinuation: (\n execSql: ExecSql,\n input: {\n accessEpoch: number;\n accessStateHash: string | null;\n continuation: DocumentSyncPullContinuation;\n containerId: string;\n contentKeyBundle: string | null;\n documentId: string;\n documentKekTargets: string | null;\n documentManifestBundle: string | null;\n lastCommitLsn: string | null;\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<ContainerMetadataRecord | null>;\n /**\n * Conditionally commit metadata content, its outgoing row, accepted queue\n * settlement, container fields, and the canonical record in one transaction.\n */\n commitMetadataMutation: (\n execSql: ExecSql,\n input: {\n acceptedPendingUpdateIds: readonly string[];\n clearSyncFailure?: boolean | undefined;\n container: ContainerRecord;\n expectedContainer: ContainerRecord;\n expectedRecord: ContainerMetadataRecord;\n createIntentSettlement?:\n | {\n containerId: string;\n expectedIntentId: string;\n expectedUpdatedAt: string;\n remoteContainerId: string;\n remoteMetadataAccessStateHash: string;\n remoteMetadataDocumentId: string;\n /**\n * When present, an overtaking create revision is adopted as the\n * desired local parent and converted to a move from this remotely\n * committed parent instead of rejecting the remote identity.\n */\n supersededMovePreviousParentId?: string | undefined;\n }\n | undefined;\n moveIntentSettlement?:\n | {\n containerId: string;\n expectedIntentId: string;\n expectedUpdatedAt: string;\n }\n | undefined;\n pendingUpdate?: PendingUpdateFields | undefined;\n preserveDurableStructureWhenPending?: boolean | undefined;\n record: ContainerMetadataRecord;\n saveOptions?:\n | {\n createIntent?: ContainerCreateIntentInput;\n localUpdatedAt?: string;\n moveIntent?: ContainerMoveIntentInput | undefined;\n serverTimestamps?:\n | {\n createdAt?: string | null;\n updatedAt?: string | null;\n }\n | undefined;\n updatedAt?: string;\n }\n | undefined;\n settleAcceptedPendingOnConflict: boolean;\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<\n | {\n committed: true;\n container: ContainerRecord;\n createIntentSettled?: true | undefined;\n moveIntentSettled?: true | undefined;\n }\n | {\n committed: false;\n currentState: StoredContainerState | null;\n staleServerState?: true;\n }\n >;\n settleAcceptedMetadataPendingUpdates: (\n execSql: ExecSql,\n input: {\n containerId: string;\n expectedRecord: ContainerMetadataRecord;\n pendingUpdateIds: readonly string[];\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<StoredContainerState | null>;\n saveContainer: (\n execSql: ExecSql,\n container: ContainerRecord,\n record: ContainerMetadataRecord | null,\n options?: SaveContainerOptions,\n ) => Promise<ContainerRecord>;\n /**\n * Atomically saves a container and enqueues its first metadata update.\n * Host adapters must implement the compound write in their own transaction.\n */\n saveContainerWithPendingUpdate: (\n execSql: ExecSql,\n container: ContainerRecord,\n record: ContainerMetadataRecord,\n options: SaveContainerWithPendingUpdateOptions,\n ) => Promise<ContainerRecord>;\n saveContainerAndDeletePendingUpdates: (\n execSql: ExecSql,\n container: ContainerRecord,\n record: ContainerMetadataRecord,\n pendingUpdateIds: readonly string[],\n ) => Promise<ContainerRecord>;\n /**\n * Revision-CAS settlement used by asynchronous create replay. Invalid\n * adapters are rejected before the remote mutation begins.\n */\n markCreateIntentRevisionSynced: (\n execSql: ExecSql,\n input: ContainerCreateIntentRevisionInput,\n ) => Promise<boolean>;\n /**\n * Explicit revision-CAS settlement. Move replay instead settles inside\n * commitMetadataMutation and requires its atomic acknowledgment.\n */\n markMoveIntentRevisionSynced: (\n execSql: ExecSql,\n input: ContainerMoveIntentRevisionInput,\n ) => Promise<boolean>;\n /**\n * Drop a move revision the server refuses for good and restore the\n * folder's previous parent in the same transaction; false if superseded.\n */\n abandonMoveIntentRevision: (\n execSql: ExecSql,\n input: ContainerMoveIntentRevisionInput & {\n previousParentContainerId: string;\n },\n ) => Promise<boolean>;\n}\n"]}
1
+ {"version":3,"file":"containerContentsPersistenceTypes.js","sourceRoot":"","sources":["containerContentsPersistenceTypes.ts"],"names":[],"mappings":"AAYA,MAAM,CAAC,MAAM,4BAA4B,GAAG,kBAAkB,CAAC;AAC/D,MAAM,CAAC,MAAM,0BAA0B,GAAG,gBAAgB,CAAC","sourcesContent":["import type { AccessManifestCheckpoint } from \"@tearleads/crypto\";\nimport type { HeldContainerBinding } from \"../../containers/containerBinding\";\nimport type { DocumentSyncPullContinuation } from \"../../documents/shared/pullContinuation\";\nimport type {\n DocumentRecord,\n PendingUpdateFields,\n PendingUpdateRecord,\n} from \"../../sqlite/documentPersistence\";\nimport type { ExecSql } from \"../../sqlite/sqlSchema\";\nimport type { ContainerRecord } from \"../containers/containerPersistence\";\nimport type { DormantMetadataSweepPersistence } from \"./dormantMetadataSweep\";\n\nexport const CONTAINER_CREATE_INTENT_TYPE = \"container.create\";\nexport const CONTAINER_MOVE_INTENT_TYPE = \"container.move\";\n\nexport type ContainerCreateIntentSyncStatus = \"pending\" | \"synced\";\nexport type ContainerMoveIntentSyncStatus = \"pending\" | \"blocked\";\n\nexport interface ContainerCreateIntentRecord {\n id: string;\n containerId: string;\n parentContainerId: string;\n intentType: typeof CONTAINER_CREATE_INTENT_TYPE;\n syncStatus: ContainerCreateIntentSyncStatus;\n remoteContainerId: string | null;\n remoteMetadataDocumentId: string | null;\n remoteMetadataAccessStateHash: string | null;\n lastError: string | null;\n lastAttemptedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ContainerMoveIntentRecord {\n id: string;\n containerId: string;\n parentContainerId: string;\n previousParentContainerId: string | null;\n intentType: typeof CONTAINER_MOVE_INTENT_TYPE;\n syncStatus: ContainerMoveIntentSyncStatus;\n lastError: string | null;\n lastAttemptedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ContainerCreateIntentInput {\n id?: string;\n parentContainerId: string;\n}\n\nexport interface ContainerMoveIntentInput {\n id?: string;\n parentContainerId: string;\n previousParentContainerId?: string | null | undefined;\n}\n\nexport interface ContainerCreateIntentErrorInput {\n containerId: string;\n expectedIntentId: string;\n expectedUpdatedAt: string;\n message: string;\n stillCurrent?: (() => boolean) | undefined;\n}\n\nexport type ContainerCreateIntentErrorInputRecorder = (\n execSql: ExecSql,\n input: ContainerCreateIntentErrorInput,\n) => Promise<void>;\n\nexport interface ContainerMoveIntentRevisionInput {\n containerId: string;\n expectedIntentId: string;\n expectedUpdatedAt: string;\n stillCurrent: () => boolean;\n}\n\nexport interface ContainerCreateIntentRevisionInput\n extends ContainerMoveIntentRevisionInput {\n remoteContainerId: string;\n remoteMetadataAccessStateHash: string;\n remoteMetadataDocumentId: string;\n /** The remote parent now; a move settlement queues cites it as previous. */\n supersededMovePreviousParentId?: string | undefined;\n /**\n * The parent the create committed under. A desired parent equal to it, or\n * to the current one, owes no move: the user never moved the folder away,\n * and a later remote move by another writer stands.\n */\n createdParentContainerId?: string | undefined;\n /** The parent the settled intent wants. */\n desiredParentContainerId?: string | undefined;\n}\n\nexport interface LocalRootDescendantReparentInput {\n containerId: string;\n parentContainerId: string | null;\n updateCreateIntent?: boolean | undefined;\n}\n\n/**\n * A container's metadata document record plus its durable content: the full\n * Loro updates export of the metadata document, stored in the shared\n * history-checkpoint table under the container-metadata app kind. Metadata\n * documents are tiny registries, so the whole update log is the checkpoint.\n */\nexport interface ContainerMetadataRecord extends DocumentRecord {\n metadataUpdates: string;\n}\n\nexport interface StoredContainerState {\n container: ContainerRecord;\n record: ContainerMetadataRecord | null;\n}\n\nexport interface ContainerRemoval {\n containerId: string;\n reason: \"access_revoked\" | \"deleted\";\n updatedAt: string;\n}\n\nexport interface ContainerHydrationTombstone extends ContainerRemoval {\n generation: number;\n}\n\nexport interface ContainerDeletionGuard {\n containerId: string;\n expectedContainer: ContainerRecord | null;\n}\n\nexport interface SaveContainerOptions {\n createIntent?: ContainerCreateIntentInput;\n stillCurrent?: (() => boolean) | undefined;\n localUpdatedAt?: string;\n moveIntent?: ContainerMoveIntentInput | undefined;\n serverTimestamps?:\n | {\n createdAt?: string | null;\n updatedAt?: string | null;\n }\n | undefined;\n updatedAt?: string;\n}\n\nexport interface SaveContainerWithPendingUpdateOptions\n extends SaveContainerOptions {\n pendingUpdate: PendingUpdateFields;\n}\n\nexport interface ContainerContentsPersistence\n extends DormantMetadataSweepPersistence {\n containerExists: (execSql: ExecSql, containerId: string) => Promise<boolean>;\n /**\n * Create an absent remotely hydrated container only when no newer durable\n * tombstone or concurrent container/dormant-metadata mutation won first.\n */\n commitHydratedContainer: (\n execSql: ExecSql,\n input: {\n container: ContainerRecord;\n expectedDormantRecord: ContainerMetadataRecord | null;\n /** Tombstone observed before the remote request began, or null. */\n expectedHydrationTombstone?:\n | ContainerHydrationTombstone\n | null\n | undefined;\n /** Current placement verified and pinned before recovery insertion. */\n expectedPlacementCheckpoint?: AccessManifestCheckpoint | undefined;\n record: ContainerMetadataRecord;\n remoteUpdatedAt: string;\n saveOptions: {\n localUpdatedAt?: string;\n serverTimestamps?:\n | {\n createdAt?: string | null;\n updatedAt?: string | null;\n }\n | undefined;\n };\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<\n { committed: true; container: ContainerRecord } | { committed: false }\n >;\n /** Capture anti-resurrection fences immediately before a remote fetch. */\n loadContainerHydrationTombstones: (\n execSql: ExecSql,\n ) => Promise<ReadonlyArray<ContainerHydrationTombstone>>;\n deleteContainer: (\n execSql: ExecSql,\n containerId: string,\n options?: {\n reason?: ContainerRemoval[\"reason\"];\n updatedAt?: string;\n },\n ) => Promise<void>;\n deleteContainers: (\n execSql: ExecSql,\n removals: ReadonlyArray<ContainerRemoval>,\n options?: {\n /** Must run inside the deletion transaction before any row is removed. */\n beforeDeleteInTransaction?:\n | ((transactionExecSql: ExecSql) => Promise<boolean>)\n | undefined;\n /** Unsigned discovery removes listings but preserves metadata and local intent. */\n discoveryOnly?: boolean;\n /**\n * Request-time states rechecked inside the deletion transaction. An\n * absent or changed row means another pane won and aborts the cascade.\n */\n expectedContainers?: ReadonlyArray<ContainerDeletionGuard>;\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<ReadonlyArray<string>>;\n deletePendingUpdates: (\n execSql: ExecSql,\n containerId: string,\n ) => Promise<void>;\n deletePendingUpdate: (execSql: ExecSql, id: string) => Promise<void>;\n ensureSchema: (execSql: ExecSql) => Promise<void>;\n enqueuePendingUpdate: (\n execSql: ExecSql,\n input: PendingUpdateFields & { containerId: string },\n ) => Promise<string>;\n listPendingCreateIntents: (\n execSql: ExecSql,\n ) => Promise<ContainerCreateIntentRecord[]>;\n // Every move intent that has not yet synced, regardless of syncStatus —\n // a blocked move (destination parent not synced yet) is still unsynced, and\n // synced moves are deleted (see markMoveIntentRevisionSynced), so every surviving\n // row qualifies. Blocked intents replay too: \"blocked\" names the reason the\n // last attempt could not proceed, not a terminal verdict.\n listUnsyncedMoveIntents: (\n execSql: ExecSql,\n ) => Promise<ContainerMoveIntentRecord[]>;\n listContainerIdsWithPendingUpdates: (\n execSql: ExecSql,\n containerIds: ReadonlyArray<string>,\n ) => Promise<string[]>;\n listContainerIdsWithPullContinuations: (\n execSql: ExecSql,\n containerIds: ReadonlyArray<string>,\n ) => Promise<string[]>;\n listPendingUpdates: (\n execSql: ExecSql,\n containerId: string,\n ) => Promise<PendingUpdateRecord[]>;\n rekeyPendingUpdate: (execSql: ExecSql, id: string) => Promise<string | null>;\n /** Revision-CAS error recorder used by asynchronous create replay. */\n recordCreateIntentRevisionError: ContainerCreateIntentErrorInputRecorder;\n recordMoveIntentError: (\n execSql: ExecSql,\n input: {\n blocked?: boolean | undefined;\n containerId: string;\n expectedIntentId?: string | undefined;\n expectedUpdatedAt?: string | undefined;\n message: string;\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<void>;\n reassignContainerDocuments: (\n execSql: ExecSql,\n input: {\n fromContainerId: string;\n stillCurrent?: (() => boolean) | undefined;\n toContainerId: string;\n updatedAt?: string | undefined;\n },\n ) => Promise<void>;\n reconcileLocalRootContainer: (\n execSql: ExecSql,\n input: {\n descendantReparents: ReadonlyArray<LocalRootDescendantReparentInput>;\n localRootContainerId: string;\n remoteOrganizationId: string;\n remoteRootContainerId: string;\n stillCurrent?: (() => boolean) | undefined;\n updatedAt?: string | undefined;\n },\n ) => Promise<void>;\n reconcileLocalSystemContainer: (\n execSql: ExecSql,\n input: {\n localContainerId: string;\n remoteContainerId: string;\n remoteOrganizationId: string;\n stillCurrent?: (() => boolean) | undefined;\n updatedAt?: string | undefined;\n },\n ) => Promise<void>;\n loadContainers: (\n execSql: ExecSql,\n ) => Promise<ReadonlyArray<StoredContainerState>>;\n loadContainerMetadataState: (\n execSql: ExecSql,\n containerId: string,\n ) => Promise<StoredContainerState | null>;\n /**\n * The organization and metadata target this device holds for a container,\n * from its live row or retained dormant metadata; null when never held.\n */\n loadHeldContainerBinding: (\n execSql: ExecSql,\n containerId: string,\n ) => Promise<HeldContainerBinding | null>;\n /** Whether a held folder was re-homed away from this organization. */\n isSupersededContainerBinding: (\n execSql: ExecSql,\n input: { containerId: string; organizationId: string },\n ) => Promise<boolean>;\n /**\n * Move a held folder to the binding its own user re-created it under,\n * resetting the metadata record's remote stream; false if the held binding\n * changed since it was read.\n */\n rebindHeldContainer: (\n execSql: ExecSql,\n input: {\n containerId: string;\n expected: HeldContainerBinding;\n next: { metadataDocumentId: string; organizationId: string };\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<boolean>;\n /**\n * Load a container-metadata record by container id alone, without\n * requiring a containers row — the dormant shape row 4's access_revoked\n * branch leaves behind, re-attached on rehydration.\n */\n loadContainerMetadataRecord: (\n execSql: ExecSql,\n containerId: string,\n ) => Promise<ContainerMetadataRecord | null>;\n /**\n * Atomically replace the exact rejected metadata pull continuation with its\n * durable recovery marker and return the authoritative current record.\n */\n invalidateMetadataPullContinuation: (\n execSql: ExecSql,\n input: {\n accessEpoch: number;\n accessStateHash: string | null;\n continuation: DocumentSyncPullContinuation;\n containerId: string;\n contentKeyBundle: string | null;\n documentId: string;\n documentKekTargets: string | null;\n documentManifestBundle: string | null;\n lastCommitLsn: string | null;\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<ContainerMetadataRecord | null>;\n /**\n * Conditionally commit metadata content, its outgoing row, accepted queue\n * settlement, container fields, and the canonical record in one transaction.\n */\n commitMetadataMutation: (\n execSql: ExecSql,\n input: {\n acceptedPendingUpdateIds: readonly string[];\n clearSyncFailure?: boolean | undefined;\n container: ContainerRecord;\n expectedContainer: ContainerRecord;\n expectedRecord: ContainerMetadataRecord;\n createIntentSettlement?:\n | {\n containerId: string;\n expectedIntentId: string;\n expectedUpdatedAt: string;\n remoteContainerId: string;\n remoteMetadataAccessStateHash: string;\n remoteMetadataDocumentId: string;\n /**\n * When present, an overtaking create revision is adopted as the\n * desired local parent and converted to a move from this remotely\n * committed parent instead of rejecting the remote identity.\n */\n supersededMovePreviousParentId?: string | undefined;\n }\n | undefined;\n moveIntentSettlement?:\n | {\n containerId: string;\n expectedIntentId: string;\n expectedUpdatedAt: string;\n }\n | undefined;\n pendingUpdate?: PendingUpdateFields | undefined;\n preserveDurableStructureWhenPending?: boolean | undefined;\n record: ContainerMetadataRecord;\n saveOptions?:\n | {\n createIntent?: ContainerCreateIntentInput;\n localUpdatedAt?: string;\n moveIntent?: ContainerMoveIntentInput | undefined;\n serverTimestamps?:\n | {\n createdAt?: string | null;\n updatedAt?: string | null;\n }\n | undefined;\n updatedAt?: string;\n }\n | undefined;\n settleAcceptedPendingOnConflict: boolean;\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<\n | {\n committed: true;\n container: ContainerRecord;\n createIntentSettled?: true | undefined;\n moveIntentSettled?: true | undefined;\n }\n | {\n committed: false;\n currentState: StoredContainerState | null;\n staleServerState?: true;\n }\n >;\n settleAcceptedMetadataPendingUpdates: (\n execSql: ExecSql,\n input: {\n containerId: string;\n expectedRecord: ContainerMetadataRecord;\n pendingUpdateIds: readonly string[];\n stillCurrent?: (() => boolean) | undefined;\n },\n ) => Promise<StoredContainerState | null>;\n saveContainer: (\n execSql: ExecSql,\n container: ContainerRecord,\n record: ContainerMetadataRecord | null,\n options?: SaveContainerOptions,\n ) => Promise<ContainerRecord>;\n /**\n * Atomically saves a container and enqueues its first metadata update.\n * Host adapters must implement the compound write in their own transaction.\n */\n saveContainerWithPendingUpdate: (\n execSql: ExecSql,\n container: ContainerRecord,\n record: ContainerMetadataRecord,\n options: SaveContainerWithPendingUpdateOptions,\n ) => Promise<ContainerRecord>;\n saveContainerAndDeletePendingUpdates: (\n execSql: ExecSql,\n container: ContainerRecord,\n record: ContainerMetadataRecord,\n pendingUpdateIds: readonly string[],\n ) => Promise<ContainerRecord>;\n /**\n * Revision-CAS settlement used by asynchronous create replay. Invalid\n * adapters are rejected before the remote mutation begins.\n */\n markCreateIntentRevisionSynced: (\n execSql: ExecSql,\n input: ContainerCreateIntentRevisionInput,\n ) => Promise<boolean>;\n /**\n * Explicit revision-CAS settlement. Move replay instead settles inside\n * commitMetadataMutation and requires its atomic acknowledgment.\n */\n markMoveIntentRevisionSynced: (\n execSql: ExecSql,\n input: ContainerMoveIntentRevisionInput,\n ) => Promise<boolean>;\n /**\n * Drop a move revision the server refuses for good and restore the\n * folder's previous parent in the same transaction; false if superseded.\n */\n abandonMoveIntentRevision: (\n execSql: ExecSql,\n input: ContainerMoveIntentRevisionInput & {\n previousParentContainerId: string;\n },\n ) => Promise<boolean>;\n}\n"]}
@@ -138,6 +138,9 @@ export async function deleteStoredContainers(execSql, removals, options) {
138
138
  await sqlContainerSyncWatermarkPersistence.ensureSchema(lockedExecSql);
139
139
  const runtime = getClientSQLitePersistenceRuntime(lockedExecSql);
140
140
  const remove = async (tx) => {
141
+ if (options?.beforeDeleteInTransaction &&
142
+ !(await options.beforeDeleteInTransaction(lockedExecSql)))
143
+ return [];
141
144
  if (!(await canApplyContainerRemovals({
142
145
  expectedContainers: options?.expectedContainers,
143
146
  removals: uniqueRemovals,
@@ -1 +1 @@
1
- {"version":3,"file":"containerDeletionPersistence.js","sourceRoot":"","sources":["containerDeletionPersistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,8BAA8B,EAAE,MAAM,kCAAkC,CAAC;AAClF,OAAO,EACL,sBAAsB,EACtB,2BAA2B,EAC3B,oBAAoB,EACpB,yBAAyB,EACzB,UAAU,EACV,iCAAiC,EACjC,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,iCAAiC,GAClC,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACL,eAAe,EACf,wBAAwB,GACzB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,gCAAgC,EAChC,qBAAqB,GACtB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,sCAAsC,EACtC,oCAAoC,GACrC,MAAM,iDAAiD,CAAC;AAMzD,OAAO,EAAE,kCAAkC,EAAE,MAAM,iCAAiC,CAAC;AACrF,OAAO,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AACrE,OAAO,EAAE,gDAAgD,EAAE,MAAM,6BAA6B,CAAC;AAC/F,OAAO,EACL,mCAAmC,EACnC,qCAAqC,GACtC,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,gDAAgD,EAChD,2CAA2C,GAC5C,MAAM,4BAA4B,CAAC;AAMpC,SAAS,0BAA0B,CACjC,QAAyC;IAEzC,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAA4B,CAAC;IACjE,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,oBAAoB,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QAC9D,IACE,CAAC,OAAO;YACR,CAAC,OAAO,CAAC,MAAM,KAAK,gBAAgB,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC;YACrE,CAAC,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM;gBAChC,OAAO,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,EACxC,CAAC;YACD,oBAAoB,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,oBAAoB,CAAC,MAAM,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,wBAAwB,CAC/B,EAAgC,EAChC,YAAmC;IAEnC,OAAO,EAAE;SACN,MAAM,CAAC;QACN,oBAAoB,EAAE,UAAU,CAAC,oBAAoB;QACrD,EAAE,EAAE,UAAU,CAAC,EAAE;QACjB,kBAAkB,EAAE,UAAU,CAAC,kBAAkB;QACjD,cAAc,EAAE,UAAU,CAAC,cAAc;QACzC,QAAQ,EAAE,UAAU,CAAC,QAAQ;QAC7B,eAAe,EAAE,UAAU,CAAC,eAAe;QAC3C,eAAe,EAAE,UAAU,CAAC,eAAe;QAC3C,UAAU,EAAE,UAAU,CAAC,UAAU;KAClC,CAAC;SACD,IAAI,CAAC,UAAU,CAAC;SAChB,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;AACtD,CAAC;AAMD,SAAS,uBAAuB,CAC9B,GAAoC,EACpC,KAA6B;IAE7B,MAAM,QAAQ,GAAG,KAAK,CAAC,iBAAiB,CAAC;IACzC,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ;QAAE,OAAO,GAAG,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,CAAC;IACrE,OAAO,CACL,GAAG,CAAC,EAAE,KAAK,QAAQ,CAAC,EAAE;QACtB,GAAG,CAAC,cAAc,KAAK,QAAQ,CAAC,cAAc;QAC9C,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,QAAQ;QAClC,GAAG,CAAC,kBAAkB,KAAK,QAAQ,CAAC,kBAAkB;QACtD,GAAG,CAAC,UAAU,KAAK,CAAC,QAAQ,CAAC,UAAU,IAAI,IAAI,CAAC;QAChD,GAAG,CAAC,oBAAoB,KAAK,CAAC,QAAQ,CAAC,oBAAoB,IAAI,MAAM,CAAC;QACtE,GAAG,CAAC,eAAe,KAAK,CAAC,QAAQ,CAAC,eAAe,IAAI,IAAI,CAAC;QAC1D,GAAG,CAAC,eAAe,KAAK,CAAC,QAAQ,CAAC,eAAe,IAAI,IAAI,CAAC,CAC3D,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,yBAAyB,CAAC,KAIxC;IACC,MAAM,WAAW,GAAG,MAAM,wBAAwB,CAChD,KAAK,CAAC,EAAE,EACR,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,CACrD,CAAC;IACF,MAAM,cAAc,GAAG,IAAI,GAAG,CAC5B,WAAW,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAE,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CACzE,CAAC;IACF,MAAM,SAAS,GAAG,IAAI,GAAG,CACvB,KAAK,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAC3E,CAAC;IACF,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,EAAE;QACtC,MAAM,UAAU,GAAG,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QAC3D,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACjD,OAAO,CACL,CAAC,CAAC,KAAK,IAAI,uBAAuB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;YACtD,CAAC,UAAU,EAAE,eAAe,IAAI,IAAI;gBAClC,UAAU,CAAC,eAAe,IAAI,OAAO,CAAC,SAAS,CAAC,CACnD,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,8BAA8B,CAAC,KAI7C;IACC,IAAI,KAAK,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC5C,MAAM,kBAAkB,GAAG,MAAM,KAAK,CAAC,EAAE;SACtC,MAAM,CAAC;QACN,WAAW,EAAE,UAAU,CAAC,EAAE;QAC1B,cAAc,EAAE,UAAU,CAAC,cAAc;KAC1C,CAAC;SACD,IAAI,CAAC,UAAU,CAAC;SAChB,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAC1D,MAAM,2CAA2C,CAC/C,KAAK,CAAC,EAAE,EACR,kBAAkB,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,EAAE,CACvC,SAAS,CAAC,WAAW;QACnB,CAAC,CAAC;YACE;gBACE,WAAW,EAAE,SAAS,CAAC,WAAW;gBAClC,cAAc,EAAE,SAAS,CAAC,cAAc;gBACxC,UAAU,EAAE,KAAK,CAAC,UAAU;aAC7B;SACF;QACH,CAAC,CAAC,EAAE,CACP,CACF,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,sBAAsB,CAAC,KAMrC;IACC,MAAM,YAAY,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1E,MAAM,kCAAkC,CAAC;QACvC,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,EAAE,EAAE,KAAK,CAAC,EAAE;KACb,CAAC,CAAC;IACH,MAAM,8BAA8B,CAAC;QACnC,YAAY,EAAE,KAAK,CAAC,mBAAmB;QACvC,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACpC,EAAE,EAAE,KAAK,CAAC,EAAE;KACb,CAAC,CAAC;IACH,6EAA6E;IAC7E,MAAM,eAAe,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACjD,MAAM,gDAAgD,CAAC;QACrD,QAAQ,EAAE,KAAK,CAAC,aAAa;YAC3B,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gBAC/B,GAAG,OAAO;gBACV,SAAS,EAAE,eAAe;aAC3B,CAAC,CAAC;YACL,CAAC,CAAC,KAAK,CAAC,QAAQ;QAClB,EAAE,EAAE,KAAK,CAAC,EAAE;KACb,CAAC,CAAC;IACH,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;QACxB,MAAM,mCAAmC,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;IAC5E,CAAC;SAAM,CAAC;QACN,MAAM,qCAAqC,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5E,MAAM,KAAK,CAAC,EAAE;aACX,MAAM,CAAC,sBAAsB,CAAC;aAC9B,KAAK,CAAC,OAAO,CAAC,sBAAsB,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;aAChE,GAAG,EAAE,CAAC;QACT,MAAM,KAAK,CAAC,EAAE;aACX,MAAM,CAAC,oBAAoB,CAAC;aAC5B,KAAK,CAAC,OAAO,CAAC,oBAAoB,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;aAC9D,GAAG,EAAE,CAAC;IACX,CAAC;IACD,MAAM,gCAAgC,CAAC,KAAK,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;IAC/D,MAAM,gDAAgD,CACpD,KAAK,CAAC,EAAE,EACR,KAAK,CAAC,iBAAiB,CACxB,CAAC;IACF,MAAM,sCAAsC,CAAC,KAAK,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;IACrE,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,OAAwE,EACxE,QAAyE,EACzE,OAAyE;IAEzE,MAAM,cAAc,GAAG,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IAC5D,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC3C,OAAO,wBAAwB,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE;QAC/D,MAAM,eAAe,CAAC,aAAa,EAAE,iCAAiC,CAAC,CAAC;QACxE,MAAM,eAAe,CAAC,aAAa,EAAE,wBAAwB,CAAC,CAAC;QAC/D,MAAM,qBAAqB,CAAC,aAAa,CAAC,CAAC;QAC3C,MAAM,eAAe,CAAC,aAAa,EAAE,2BAA2B,CAAC,CAAC;QAClE,MAAM,eAAe,CAAC,aAAa,EAAE,yBAAyB,CAAC,CAAC;QAChE,MAAM,8BAA8B,CAAC,aAAa,CAAC,CAAC;QACpD,MAAM,oCAAoC,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;QACvE,MAAM,OAAO,GAAG,iCAAiC,CAAC,aAAa,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,KAAK,EAAE,EAAgC,EAAE,EAAE;YACxD,IACE,CAAC,CAAC,MAAM,yBAAyB,CAAC;gBAChC,kBAAkB,EAAE,OAAO,EAAE,kBAAkB;gBAC/C,QAAQ,EAAE,cAAc;gBACxB,EAAE;aACH,CAAC,CAAC,EACH,CAAC;gBACD,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,MAAM,mBAAmB,GAAG,OAAO,EAAE,aAAa;gBAChD,CAAC,CAAC,MAAM,2BAA2B,CAC/B,EAAE,EACF,cAAc,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,CACrD;gBACH,CAAC,CAAC,EAAE,CAAC;YACP,OAAO,sBAAsB,CAAC;gBAC5B,aAAa,EAAE,OAAO,EAAE,aAAa;gBACrC,iBAAiB,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CACpD,mBAAmB,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC;oBAC/C,CAAC,CAAC,EAAE;oBACJ,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAC1B;gBACD,QAAQ,EAAE,cAAc;gBACxB,mBAAmB;gBACnB,EAAE;aACH,CAAC,CAAC;QACL,CAAC,CAAC;QACF,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,CAAC;YAC3B,OAAO,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC;QAChE,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,kBAAkB,CAC9C,MAAM,EACN,OAAO,CAAC,YAAY,EACpB,EAAE,QAAQ,EAAE,WAAW,EAAE,CAC1B,CAAC;QACF,OAAO,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;IAC9B,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { inArray } from \"drizzle-orm\";\nimport { ensureDocumentProjectionTables } from \"../../sqlite/documentPersistence\";\nimport {\n containerCreateIntents,\n containerCreateIntentTables,\n containerMoveIntents,\n containerMoveIntentTables,\n containers,\n documentContainerProjectionTables,\n documentMoveIntentTables,\n} from \"../../sqlite/schema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../sqlite/sqlitePersistenceRuntime\";\nimport {\n ensureSqlTables,\n runSerializedSqlMutation,\n} from \"../../sqlite/sqlSchema\";\nimport {\n deleteContainerRowsInTransaction,\n ensureContainerTables,\n} from \"../containers/containerPersistence\";\nimport {\n deleteContainerWatermarksInTransaction,\n sqlContainerSyncWatermarkPersistence,\n} from \"../containers/containerSyncWatermarkPersistence\";\nimport type {\n ContainerContentsPersistence,\n ContainerDeletionGuard,\n ContainerRemoval,\n} from \"./containerContentsPersistenceTypes\";\nimport { recordContainerHydrationTombstones } from \"./containerHydrationPersistence\";\nimport { containerIdsWithRemovalWork } from \"./containerRemovalWork\";\nimport { repairDocumentsForRemovedContainersInTransaction } from \"./containerStructuralRepair\";\nimport {\n refreshPlacementIntentsAfterRemoval,\n repairLinkIntentsForRemovedContainers,\n} from \"./documentLinkRemovalRepair\";\nimport {\n deleteContainerMetadataDocumentRowsInTransaction,\n retainDormantContainerMetadataInTransaction,\n} from \"./dormantContainerMetadata\";\n\ntype DeleteContainerOptions = NonNullable<\n Parameters<ContainerContentsPersistence[\"deleteContainers\"]>[2]\n>;\n\nfunction normalizeContainerRemovals(\n removals: ReadonlyArray<ContainerRemoval>,\n): ContainerRemoval[] {\n const removalByContainerId = new Map<string, ContainerRemoval>();\n for (const removal of removals) {\n const current = removalByContainerId.get(removal.containerId);\n if (\n !current ||\n (current.reason === \"access_revoked\" && removal.reason === \"deleted\") ||\n (current.reason === removal.reason &&\n current.updatedAt < removal.updatedAt)\n ) {\n removalByContainerId.set(removal.containerId, removal);\n }\n }\n return Array.from(removalByContainerId.values());\n}\n\nfunction loadGuardedContainerRows(\n tx: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n) {\n return tx\n .select({\n effectiveAccessLevel: containers.effectiveAccessLevel,\n id: containers.id,\n metadataDocumentId: containers.metadataDocumentId,\n organizationId: containers.organizationId,\n parentId: containers.parentId,\n serverCreatedAt: containers.serverCreatedAt,\n serverUpdatedAt: containers.serverUpdatedAt,\n systemSlot: containers.systemSlot,\n })\n .from(containers)\n .where(inArray(containers.id, [...containerIds]));\n}\n\ntype GuardedContainerRow = Awaited<\n ReturnType<typeof loadGuardedContainerRows>\n>[number];\n\nfunction sameGuardedContainerRow(\n row: GuardedContainerRow | undefined,\n guard: ContainerDeletionGuard,\n): boolean {\n const expected = guard.expectedContainer;\n if (!row || !expected) return row === undefined && expected === null;\n return (\n row.id === expected.id &&\n row.organizationId === expected.organizationId &&\n row.parentId === expected.parentId &&\n row.metadataDocumentId === expected.metadataDocumentId &&\n row.systemSlot === (expected.systemSlot ?? null) &&\n row.effectiveAccessLevel === (expected.effectiveAccessLevel ?? \"read\") &&\n row.serverCreatedAt === (expected.serverCreatedAt ?? null) &&\n row.serverUpdatedAt === (expected.serverUpdatedAt ?? null)\n );\n}\n\nasync function canApplyContainerRemovals(input: {\n expectedContainers: DeleteContainerOptions[\"expectedContainers\"];\n removals: ReadonlyArray<ContainerRemoval>;\n tx: ClientSQLiteTransactionScope;\n}): Promise<boolean> {\n const currentRows = await loadGuardedContainerRows(\n input.tx,\n input.removals.map((removal) => removal.containerId),\n );\n const currentRowById = new Map(\n currentRows.flatMap((row) => (row.id ? ([[row.id, row]] as const) : [])),\n );\n const guardById = new Map(\n input.expectedContainers?.map((guard) => [guard.containerId, guard]) ?? [],\n );\n return input.removals.every((removal) => {\n const currentRow = currentRowById.get(removal.containerId);\n const guard = guardById.get(removal.containerId);\n return (\n (!guard || sameGuardedContainerRow(currentRow, guard)) &&\n (currentRow?.serverUpdatedAt == null ||\n currentRow.serverUpdatedAt <= removal.updatedAt)\n );\n });\n}\n\nasync function retainRemovedContainerMetadata(input: {\n containerIds: ReadonlyArray<string>;\n retainedAt: string;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n if (input.containerIds.length === 0) return;\n const retainedContainers = await input.tx\n .select({\n containerId: containers.id,\n organizationId: containers.organizationId,\n })\n .from(containers)\n .where(inArray(containers.id, [...input.containerIds]));\n await retainDormantContainerMetadataInTransaction(\n input.tx,\n retainedContainers.flatMap((container) =>\n container.containerId\n ? [\n {\n containerId: container.containerId,\n organizationId: container.organizationId,\n retainedAt: input.retainedAt,\n },\n ]\n : [],\n ),\n );\n}\n\nasync function applyContainerRemovals(input: {\n discoveryOnly: DeleteContainerOptions[\"discoveryOnly\"];\n metadataDeleteIds: ReadonlyArray<string>;\n removals: ReadonlyArray<ContainerRemoval>;\n retainedMetadataIds: ReadonlyArray<string>;\n tx: ClientSQLiteTransactionScope;\n}): Promise<ReadonlyArray<string>> {\n const containerIds = input.removals.map((removal) => removal.containerId);\n await recordContainerHydrationTombstones({\n removals: input.removals,\n tx: input.tx,\n });\n await retainRemovedContainerMetadata({\n containerIds: input.retainedMetadataIds,\n retainedAt: new Date().toISOString(),\n tx: input.tx,\n });\n // Listing clocks are untrusted; they cannot advance local projection clocks.\n const localObservedAt = new Date().toISOString();\n await repairDocumentsForRemovedContainersInTransaction({\n removals: input.discoveryOnly\n ? input.removals.map((removal) => ({\n ...removal,\n updatedAt: localObservedAt,\n }))\n : input.removals,\n tx: input.tx,\n });\n if (input.discoveryOnly) {\n await refreshPlacementIntentsAfterRemoval({ containerIds, tx: input.tx });\n } else {\n await repairLinkIntentsForRemovedContainers({ containerIds, tx: input.tx });\n await input.tx\n .delete(containerCreateIntents)\n .where(inArray(containerCreateIntents.containerId, containerIds))\n .run();\n await input.tx\n .delete(containerMoveIntents)\n .where(inArray(containerMoveIntents.containerId, containerIds))\n .run();\n }\n await deleteContainerRowsInTransaction(input.tx, containerIds);\n await deleteContainerMetadataDocumentRowsInTransaction(\n input.tx,\n input.metadataDeleteIds,\n );\n await deleteContainerWatermarksInTransaction(input.tx, containerIds);\n return containerIds;\n}\n\nexport async function deleteStoredContainers(\n execSql: Parameters<ContainerContentsPersistence[\"deleteContainers\"]>[0],\n removals: Parameters<ContainerContentsPersistence[\"deleteContainers\"]>[1],\n options?: Parameters<ContainerContentsPersistence[\"deleteContainers\"]>[2],\n): Promise<ReadonlyArray<string>> {\n const uniqueRemovals = normalizeContainerRemovals(removals);\n if (uniqueRemovals.length === 0) return [];\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n await ensureSqlTables(lockedExecSql, documentContainerProjectionTables);\n await ensureSqlTables(lockedExecSql, documentMoveIntentTables);\n await ensureContainerTables(lockedExecSql);\n await ensureSqlTables(lockedExecSql, containerCreateIntentTables);\n await ensureSqlTables(lockedExecSql, containerMoveIntentTables);\n await ensureDocumentProjectionTables(lockedExecSql);\n await sqlContainerSyncWatermarkPersistence.ensureSchema(lockedExecSql);\n const runtime = getClientSQLitePersistenceRuntime(lockedExecSql);\n const remove = async (tx: ClientSQLiteTransactionScope) => {\n if (\n !(await canApplyContainerRemovals({\n expectedContainers: options?.expectedContainers,\n removals: uniqueRemovals,\n tx,\n }))\n ) {\n return [];\n }\n const retainedMetadataIds = options?.discoveryOnly\n ? await containerIdsWithRemovalWork(\n tx,\n uniqueRemovals.map((removal) => removal.containerId),\n )\n : [];\n return applyContainerRemovals({\n discoveryOnly: options?.discoveryOnly,\n metadataDeleteIds: uniqueRemovals.flatMap((removal) =>\n retainedMetadataIds.includes(removal.containerId)\n ? []\n : [removal.containerId],\n ),\n removals: uniqueRemovals,\n retainedMetadataIds,\n tx,\n });\n };\n if (!options?.stillCurrent) {\n return runtime.transaction(remove, { behavior: \"immediate\" });\n }\n const outcome = await runtime.guardedTransaction(\n remove,\n options.stillCurrent,\n { behavior: \"immediate\" },\n );\n return outcome.result ?? [];\n });\n}\n"]}
1
+ {"version":3,"file":"containerDeletionPersistence.js","sourceRoot":"","sources":["containerDeletionPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { inArray } from \"drizzle-orm\";\nimport { ensureDocumentProjectionTables } from \"../../sqlite/documentPersistence\";\nimport {\n containerCreateIntents,\n containerCreateIntentTables,\n containerMoveIntents,\n containerMoveIntentTables,\n containers,\n documentContainerProjectionTables,\n documentMoveIntentTables,\n} from \"../../sqlite/schema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../sqlite/sqlitePersistenceRuntime\";\nimport {\n ensureSqlTables,\n runSerializedSqlMutation,\n} from \"../../sqlite/sqlSchema\";\nimport {\n deleteContainerRowsInTransaction,\n ensureContainerTables,\n} from \"../containers/containerPersistence\";\nimport {\n deleteContainerWatermarksInTransaction,\n sqlContainerSyncWatermarkPersistence,\n} from \"../containers/containerSyncWatermarkPersistence\";\nimport type {\n ContainerContentsPersistence,\n ContainerDeletionGuard,\n ContainerRemoval,\n} from \"./containerContentsPersistenceTypes\";\nimport { recordContainerHydrationTombstones } from \"./containerHydrationPersistence\";\nimport { containerIdsWithRemovalWork } from \"./containerRemovalWork\";\nimport { repairDocumentsForRemovedContainersInTransaction } from \"./containerStructuralRepair\";\nimport {\n refreshPlacementIntentsAfterRemoval,\n repairLinkIntentsForRemovedContainers,\n} from \"./documentLinkRemovalRepair\";\nimport {\n deleteContainerMetadataDocumentRowsInTransaction,\n retainDormantContainerMetadataInTransaction,\n} from \"./dormantContainerMetadata\";\n\ntype DeleteContainerOptions = NonNullable<\n Parameters<ContainerContentsPersistence[\"deleteContainers\"]>[2]\n>;\n\nfunction normalizeContainerRemovals(\n removals: ReadonlyArray<ContainerRemoval>,\n): ContainerRemoval[] {\n const removalByContainerId = new Map<string, ContainerRemoval>();\n for (const removal of removals) {\n const current = removalByContainerId.get(removal.containerId);\n if (\n !current ||\n (current.reason === \"access_revoked\" && removal.reason === \"deleted\") ||\n (current.reason === removal.reason &&\n current.updatedAt < removal.updatedAt)\n ) {\n removalByContainerId.set(removal.containerId, removal);\n }\n }\n return Array.from(removalByContainerId.values());\n}\n\nfunction loadGuardedContainerRows(\n tx: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n) {\n return tx\n .select({\n effectiveAccessLevel: containers.effectiveAccessLevel,\n id: containers.id,\n metadataDocumentId: containers.metadataDocumentId,\n organizationId: containers.organizationId,\n parentId: containers.parentId,\n serverCreatedAt: containers.serverCreatedAt,\n serverUpdatedAt: containers.serverUpdatedAt,\n systemSlot: containers.systemSlot,\n })\n .from(containers)\n .where(inArray(containers.id, [...containerIds]));\n}\n\ntype GuardedContainerRow = Awaited<\n ReturnType<typeof loadGuardedContainerRows>\n>[number];\n\nfunction sameGuardedContainerRow(\n row: GuardedContainerRow | undefined,\n guard: ContainerDeletionGuard,\n): boolean {\n const expected = guard.expectedContainer;\n if (!row || !expected) return row === undefined && expected === null;\n return (\n row.id === expected.id &&\n row.organizationId === expected.organizationId &&\n row.parentId === expected.parentId &&\n row.metadataDocumentId === expected.metadataDocumentId &&\n row.systemSlot === (expected.systemSlot ?? null) &&\n row.effectiveAccessLevel === (expected.effectiveAccessLevel ?? \"read\") &&\n row.serverCreatedAt === (expected.serverCreatedAt ?? null) &&\n row.serverUpdatedAt === (expected.serverUpdatedAt ?? null)\n );\n}\n\nasync function canApplyContainerRemovals(input: {\n expectedContainers: DeleteContainerOptions[\"expectedContainers\"];\n removals: ReadonlyArray<ContainerRemoval>;\n tx: ClientSQLiteTransactionScope;\n}): Promise<boolean> {\n const currentRows = await loadGuardedContainerRows(\n input.tx,\n input.removals.map((removal) => removal.containerId),\n );\n const currentRowById = new Map(\n currentRows.flatMap((row) => (row.id ? ([[row.id, row]] as const) : [])),\n );\n const guardById = new Map(\n input.expectedContainers?.map((guard) => [guard.containerId, guard]) ?? [],\n );\n return input.removals.every((removal) => {\n const currentRow = currentRowById.get(removal.containerId);\n const guard = guardById.get(removal.containerId);\n return (\n (!guard || sameGuardedContainerRow(currentRow, guard)) &&\n (currentRow?.serverUpdatedAt == null ||\n currentRow.serverUpdatedAt <= removal.updatedAt)\n );\n });\n}\n\nasync function retainRemovedContainerMetadata(input: {\n containerIds: ReadonlyArray<string>;\n retainedAt: string;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n if (input.containerIds.length === 0) return;\n const retainedContainers = await input.tx\n .select({\n containerId: containers.id,\n organizationId: containers.organizationId,\n })\n .from(containers)\n .where(inArray(containers.id, [...input.containerIds]));\n await retainDormantContainerMetadataInTransaction(\n input.tx,\n retainedContainers.flatMap((container) =>\n container.containerId\n ? [\n {\n containerId: container.containerId,\n organizationId: container.organizationId,\n retainedAt: input.retainedAt,\n },\n ]\n : [],\n ),\n );\n}\n\nasync function applyContainerRemovals(input: {\n discoveryOnly: DeleteContainerOptions[\"discoveryOnly\"];\n metadataDeleteIds: ReadonlyArray<string>;\n removals: ReadonlyArray<ContainerRemoval>;\n retainedMetadataIds: ReadonlyArray<string>;\n tx: ClientSQLiteTransactionScope;\n}): Promise<ReadonlyArray<string>> {\n const containerIds = input.removals.map((removal) => removal.containerId);\n await recordContainerHydrationTombstones({\n removals: input.removals,\n tx: input.tx,\n });\n await retainRemovedContainerMetadata({\n containerIds: input.retainedMetadataIds,\n retainedAt: new Date().toISOString(),\n tx: input.tx,\n });\n // Listing clocks are untrusted; they cannot advance local projection clocks.\n const localObservedAt = new Date().toISOString();\n await repairDocumentsForRemovedContainersInTransaction({\n removals: input.discoveryOnly\n ? input.removals.map((removal) => ({\n ...removal,\n updatedAt: localObservedAt,\n }))\n : input.removals,\n tx: input.tx,\n });\n if (input.discoveryOnly) {\n await refreshPlacementIntentsAfterRemoval({ containerIds, tx: input.tx });\n } else {\n await repairLinkIntentsForRemovedContainers({ containerIds, tx: input.tx });\n await input.tx\n .delete(containerCreateIntents)\n .where(inArray(containerCreateIntents.containerId, containerIds))\n .run();\n await input.tx\n .delete(containerMoveIntents)\n .where(inArray(containerMoveIntents.containerId, containerIds))\n .run();\n }\n await deleteContainerRowsInTransaction(input.tx, containerIds);\n await deleteContainerMetadataDocumentRowsInTransaction(\n input.tx,\n input.metadataDeleteIds,\n );\n await deleteContainerWatermarksInTransaction(input.tx, containerIds);\n return containerIds;\n}\n\nexport async function deleteStoredContainers(\n execSql: Parameters<ContainerContentsPersistence[\"deleteContainers\"]>[0],\n removals: Parameters<ContainerContentsPersistence[\"deleteContainers\"]>[1],\n options?: Parameters<ContainerContentsPersistence[\"deleteContainers\"]>[2],\n): Promise<ReadonlyArray<string>> {\n const uniqueRemovals = normalizeContainerRemovals(removals);\n if (uniqueRemovals.length === 0) return [];\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n await ensureSqlTables(lockedExecSql, documentContainerProjectionTables);\n await ensureSqlTables(lockedExecSql, documentMoveIntentTables);\n await ensureContainerTables(lockedExecSql);\n await ensureSqlTables(lockedExecSql, containerCreateIntentTables);\n await ensureSqlTables(lockedExecSql, containerMoveIntentTables);\n await ensureDocumentProjectionTables(lockedExecSql);\n await sqlContainerSyncWatermarkPersistence.ensureSchema(lockedExecSql);\n const runtime = getClientSQLitePersistenceRuntime(lockedExecSql);\n const remove = async (tx: ClientSQLiteTransactionScope) => {\n if (\n options?.beforeDeleteInTransaction &&\n !(await options.beforeDeleteInTransaction(lockedExecSql))\n )\n return [];\n if (\n !(await canApplyContainerRemovals({\n expectedContainers: options?.expectedContainers,\n removals: uniqueRemovals,\n tx,\n }))\n ) {\n return [];\n }\n const retainedMetadataIds = options?.discoveryOnly\n ? await containerIdsWithRemovalWork(\n tx,\n uniqueRemovals.map((removal) => removal.containerId),\n )\n : [];\n return applyContainerRemovals({\n discoveryOnly: options?.discoveryOnly,\n metadataDeleteIds: uniqueRemovals.flatMap((removal) =>\n retainedMetadataIds.includes(removal.containerId)\n ? []\n : [removal.containerId],\n ),\n removals: uniqueRemovals,\n retainedMetadataIds,\n tx,\n });\n };\n if (!options?.stillCurrent) {\n return runtime.transaction(remove, { behavior: \"immediate\" });\n }\n const outcome = await runtime.guardedTransaction(\n remove,\n options.stillCurrent,\n { behavior: \"immediate\" },\n );\n return outcome.result ?? [];\n });\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { documentMoveIntents, documentMoveIntentTables, } from "../../sqlite/schema.js";
3
+ import { getClientSQLitePersistenceRuntime } from "../../sqlite/sqlitePersistenceRuntime.js";
4
+ import { ensureSqlTables } from "../../sqlite/sqlSchema.js";
5
+ /** Even denied or unavailable placement intent belongs to the local user. */
6
+ export async function hasUnsettledDocumentPlacement(execSql, localId) {
7
+ await ensureSqlTables(execSql, documentMoveIntentTables);
8
+ const [intent] = await getClientSQLitePersistenceRuntime(execSql)
9
+ .db.select({ id: documentMoveIntents.id })
10
+ .from(documentMoveIntents)
11
+ .where(eq(documentMoveIntents.localId, localId))
12
+ .limit(1);
13
+ return intent !== undefined;
14
+ }
15
+ //# sourceMappingURL=documentPurgePlacement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"documentPurgePlacement.js","sourceRoot":"","sources":["documentPurgePlacement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACjC,OAAO,EACL,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,iCAAiC,EAAE,MAAM,uCAAuC,CAAC;AAC1F,OAAO,EAAgB,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEvE,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,6BAA6B,CACjD,OAAgB,EAChB,OAAe;IAEf,MAAM,eAAe,CAAC,OAAO,EAAE,wBAAwB,CAAC,CAAC;IACzD,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,iCAAiC,CAAC,OAAO,CAAC;SAC9D,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,mBAAmB,CAAC,EAAE,EAAE,CAAC;SACzC,IAAI,CAAC,mBAAmB,CAAC;SACzB,KAAK,CAAC,EAAE,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;SAC/C,KAAK,CAAC,CAAC,CAAC,CAAC;IACZ,OAAO,MAAM,KAAK,SAAS,CAAC;AAC9B,CAAC","sourcesContent":["import { eq } from \"drizzle-orm\";\nimport {\n documentMoveIntents,\n documentMoveIntentTables,\n} from \"../../sqlite/schema\";\nimport { getClientSQLitePersistenceRuntime } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../sqlite/sqlSchema\";\n\n/** Even denied or unavailable placement intent belongs to the local user. */\nexport async function hasUnsettledDocumentPlacement(\n execSql: ExecSql,\n localId: string,\n): Promise<boolean> {\n await ensureSqlTables(execSql, documentMoveIntentTables);\n const [intent] = await getClientSQLitePersistenceRuntime(execSql)\n .db.select({ id: documentMoveIntents.id })\n .from(documentMoveIntents)\n .where(eq(documentMoveIntents.localId, localId))\n .limit(1);\n return intent !== undefined;\n}\n"]}
@@ -101,6 +101,13 @@ export async function deleteStoredDocumentIfMatches(execSql, expectedRecord, del
101
101
  !sameCanonicalDocumentSecurityIdentity(existingDocument, expectedRecord)) {
102
102
  return false;
103
103
  }
104
+ const [placement] = await tx
105
+ .select({ containerId: documentProjection.containerId })
106
+ .from(documentProjection)
107
+ .where(eq(documentProjection.localId, expectedRecord.id))
108
+ .limit(1);
109
+ if ((placement?.containerId ?? null) !== expectedRecord.containerId)
110
+ return false;
104
111
  if (existingDocument.documentId &&
105
112
  (await hasAnotherDocumentAlias({
106
113
  documentId: existingDocument.documentId,
@@ -1 +1 @@
1
- {"version":3,"file":"documentDeletionPersistence.js","sourceRoot":"","sources":["documentDeletionPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq, inArray, ne } from \"drizzle-orm\";\nimport { deleteDocumentHistory } from \"../../../sqlite/documentHistoryPersistence\";\nimport {\n clearDocumentSyncFailure,\n deleteDocumentPendingUpdates,\n deleteDocumentRecord,\n loadDocumentRecord,\n} from \"../../../sqlite/documentPersistence\";\nimport { documentIntentLinkTargets } from \"../../../sqlite/documentPlacementIntentSchema\";\nimport {\n documentAttachmentBlobProjection,\n documentContainerProjection,\n documentMoveIntents,\n documentMoveIntentTables,\n documentPendingAttachments,\n documentProjection,\n documentProjectionText,\n documents,\n} from \"../../../sqlite/schema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport {\n type ExecSql,\n ensureSqlTables,\n runSerializedSqlMutation,\n} from \"../../../sqlite/sqlSchema\";\nimport type { StoredDocumentRecord } from \"../types\";\nimport { DOCUMENTS_APP_KIND } from \"./constants\";\nimport { deleteContainerDocumentTombstoneHoldsForDocuments } from \"./containerDocumentTombstoneHolds\";\nimport { sameCanonicalDocumentSecurityIdentity } from \"./documentRecordIdentity\";\nimport { getDocumentScope } from \"./documentRows\";\nimport { queueDocumentAttachmentBlobReclaims } from \"./orphanSideRows\";\n\nasync function deleteStoredDocumentRows(input: {\n existingDocumentId: string | null;\n localId: string;\n lockedExecSql: ExecSql;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n const { existingDocumentId, localId, lockedExecSql, tx } = input;\n await queueDocumentAttachmentBlobReclaims({\n localWhere: eq(documentAttachmentBlobProjection.localId, localId),\n pendingWhere: eq(documentPendingAttachments.localId, localId),\n tx,\n });\n await tx\n .delete(documentProjection)\n .where(eq(documentProjection.localId, localId))\n .run();\n await tx\n .delete(documentProjectionText)\n .where(eq(documentProjectionText.localId, localId))\n .run();\n await tx\n .delete(documentPendingAttachments)\n .where(eq(documentPendingAttachments.localId, localId))\n .run();\n await tx\n .delete(documentAttachmentBlobProjection)\n .where(eq(documentAttachmentBlobProjection.localId, localId))\n .run();\n if (existingDocumentId) {\n await tx\n .delete(documentContainerProjection)\n .where(eq(documentContainerProjection.documentId, existingDocumentId))\n .run();\n await deleteContainerDocumentTombstoneHoldsForDocuments(tx, [\n existingDocumentId,\n ]);\n await tx\n .delete(documentIntentLinkTargets)\n .where(\n inArray(\n documentIntentLinkTargets.intentId,\n tx\n .select({ id: documentMoveIntents.id })\n .from(documentMoveIntents)\n .where(eq(documentMoveIntents.documentId, existingDocumentId)),\n ),\n )\n .run();\n await tx\n .delete(documentMoveIntents)\n .where(eq(documentMoveIntents.documentId, existingDocumentId))\n .run();\n }\n const scope = getDocumentScope(localId);\n await deleteDocumentPendingUpdates(lockedExecSql, scope);\n await deleteDocumentHistory(lockedExecSql, scope);\n await clearDocumentSyncFailure(lockedExecSql, scope);\n await deleteDocumentRecord(lockedExecSql, scope);\n}\n\nasync function ensureDocumentDeletionTables(execSql: ExecSql): Promise<void> {\n // Move intents live in the container-contents schema, which document-only\n // callers may not have initialized yet.\n await ensureSqlTables(execSql, documentMoveIntentTables);\n}\n\nasync function hasAnotherDocumentAlias(input: {\n documentId: string;\n localId: string;\n tx: ClientSQLiteTransactionScope;\n}): Promise<boolean> {\n const rows = await input.tx\n .select({ localId: documents.localId })\n .from(documents)\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n eq(documents.documentId, input.documentId),\n ne(documents.localId, input.localId),\n ),\n )\n .limit(1);\n return rows.length > 0;\n}\n\nexport async function deleteStoredDocument(\n execSql: ExecSql,\n localId: string,\n): Promise<void> {\n await runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n await ensureDocumentDeletionTables(lockedExecSql);\n await getClientSQLitePersistenceRuntime(lockedExecSql).transaction(\n async (tx) => {\n const existingDocument = await loadDocumentRecord(lockedExecSql, {\n appKind: DOCUMENTS_APP_KIND,\n localId,\n });\n await deleteStoredDocumentRows({\n existingDocumentId: existingDocument?.documentId ?? null,\n localId,\n lockedExecSql,\n tx,\n });\n },\n { behavior: \"immediate\" },\n );\n });\n}\n\nexport async function deleteStoredDocumentIfMatches(\n execSql: ExecSql,\n expectedRecord: StoredDocumentRecord,\n deleteClientProjection: (transactionExecSql: ExecSql) => Promise<void>,\n): Promise<boolean> {\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n await ensureDocumentDeletionTables(lockedExecSql);\n return getClientSQLitePersistenceRuntime(lockedExecSql).transaction(\n async (tx) => {\n const existingDocument = await loadDocumentRecord(lockedExecSql, {\n appKind: DOCUMENTS_APP_KIND,\n localId: expectedRecord.id,\n });\n if (\n !existingDocument ||\n !sameCanonicalDocumentSecurityIdentity(\n existingDocument,\n expectedRecord,\n )\n ) {\n return false;\n }\n if (\n existingDocument.documentId &&\n (await hasAnotherDocumentAlias({\n documentId: existingDocument.documentId,\n localId: expectedRecord.id,\n tx,\n }))\n ) {\n return false;\n }\n await deleteStoredDocumentRows({\n existingDocumentId: existingDocument.documentId ?? null,\n localId: expectedRecord.id,\n lockedExecSql,\n tx,\n });\n await deleteClientProjection(lockedExecSql);\n return true;\n },\n { behavior: \"immediate\" },\n );\n });\n}\n\nexport async function deleteStoredDocumentSideRowsIfAbsent(\n execSql: ExecSql,\n localId: string,\n expectedDocumentId: string | null,\n deleteClientProjection: (transactionExecSql: ExecSql) => Promise<void>,\n): Promise<boolean> {\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n await ensureDocumentDeletionTables(lockedExecSql);\n return getClientSQLitePersistenceRuntime(lockedExecSql).transaction(\n async (tx) => {\n const existingDocument = await loadDocumentRecord(lockedExecSql, {\n appKind: DOCUMENTS_APP_KIND,\n localId,\n });\n if (existingDocument) return false;\n if (\n expectedDocumentId &&\n (await hasAnotherDocumentAlias({\n documentId: expectedDocumentId,\n localId,\n tx,\n }))\n ) {\n return false;\n }\n\n await deleteStoredDocumentRows({\n existingDocumentId: expectedDocumentId,\n localId,\n lockedExecSql,\n tx,\n });\n await deleteClientProjection(lockedExecSql);\n return true;\n },\n { behavior: \"immediate\" },\n );\n });\n}\n"]}
1
+ {"version":3,"file":"documentDeletionPersistence.js","sourceRoot":"","sources":["documentDeletionPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq, inArray, ne } from \"drizzle-orm\";\nimport { deleteDocumentHistory } from \"../../../sqlite/documentHistoryPersistence\";\nimport {\n clearDocumentSyncFailure,\n deleteDocumentPendingUpdates,\n deleteDocumentRecord,\n loadDocumentRecord,\n} from \"../../../sqlite/documentPersistence\";\nimport { documentIntentLinkTargets } from \"../../../sqlite/documentPlacementIntentSchema\";\nimport {\n documentAttachmentBlobProjection,\n documentContainerProjection,\n documentMoveIntents,\n documentMoveIntentTables,\n documentPendingAttachments,\n documentProjection,\n documentProjectionText,\n documents,\n} from \"../../../sqlite/schema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport {\n type ExecSql,\n ensureSqlTables,\n runSerializedSqlMutation,\n} from \"../../../sqlite/sqlSchema\";\nimport type { StoredDocumentRecord } from \"../types\";\nimport { DOCUMENTS_APP_KIND } from \"./constants\";\nimport { deleteContainerDocumentTombstoneHoldsForDocuments } from \"./containerDocumentTombstoneHolds\";\nimport { sameCanonicalDocumentSecurityIdentity } from \"./documentRecordIdentity\";\nimport { getDocumentScope } from \"./documentRows\";\nimport { queueDocumentAttachmentBlobReclaims } from \"./orphanSideRows\";\n\nasync function deleteStoredDocumentRows(input: {\n existingDocumentId: string | null;\n localId: string;\n lockedExecSql: ExecSql;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n const { existingDocumentId, localId, lockedExecSql, tx } = input;\n await queueDocumentAttachmentBlobReclaims({\n localWhere: eq(documentAttachmentBlobProjection.localId, localId),\n pendingWhere: eq(documentPendingAttachments.localId, localId),\n tx,\n });\n await tx\n .delete(documentProjection)\n .where(eq(documentProjection.localId, localId))\n .run();\n await tx\n .delete(documentProjectionText)\n .where(eq(documentProjectionText.localId, localId))\n .run();\n await tx\n .delete(documentPendingAttachments)\n .where(eq(documentPendingAttachments.localId, localId))\n .run();\n await tx\n .delete(documentAttachmentBlobProjection)\n .where(eq(documentAttachmentBlobProjection.localId, localId))\n .run();\n if (existingDocumentId) {\n await tx\n .delete(documentContainerProjection)\n .where(eq(documentContainerProjection.documentId, existingDocumentId))\n .run();\n await deleteContainerDocumentTombstoneHoldsForDocuments(tx, [\n existingDocumentId,\n ]);\n await tx\n .delete(documentIntentLinkTargets)\n .where(\n inArray(\n documentIntentLinkTargets.intentId,\n tx\n .select({ id: documentMoveIntents.id })\n .from(documentMoveIntents)\n .where(eq(documentMoveIntents.documentId, existingDocumentId)),\n ),\n )\n .run();\n await tx\n .delete(documentMoveIntents)\n .where(eq(documentMoveIntents.documentId, existingDocumentId))\n .run();\n }\n const scope = getDocumentScope(localId);\n await deleteDocumentPendingUpdates(lockedExecSql, scope);\n await deleteDocumentHistory(lockedExecSql, scope);\n await clearDocumentSyncFailure(lockedExecSql, scope);\n await deleteDocumentRecord(lockedExecSql, scope);\n}\n\nasync function ensureDocumentDeletionTables(execSql: ExecSql): Promise<void> {\n // Move intents live in the container-contents schema, which document-only\n // callers may not have initialized yet.\n await ensureSqlTables(execSql, documentMoveIntentTables);\n}\n\nasync function hasAnotherDocumentAlias(input: {\n documentId: string;\n localId: string;\n tx: ClientSQLiteTransactionScope;\n}): Promise<boolean> {\n const rows = await input.tx\n .select({ localId: documents.localId })\n .from(documents)\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n eq(documents.documentId, input.documentId),\n ne(documents.localId, input.localId),\n ),\n )\n .limit(1);\n return rows.length > 0;\n}\n\nexport async function deleteStoredDocument(\n execSql: ExecSql,\n localId: string,\n): Promise<void> {\n await runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n await ensureDocumentDeletionTables(lockedExecSql);\n await getClientSQLitePersistenceRuntime(lockedExecSql).transaction(\n async (tx) => {\n const existingDocument = await loadDocumentRecord(lockedExecSql, {\n appKind: DOCUMENTS_APP_KIND,\n localId,\n });\n await deleteStoredDocumentRows({\n existingDocumentId: existingDocument?.documentId ?? null,\n localId,\n lockedExecSql,\n tx,\n });\n },\n { behavior: \"immediate\" },\n );\n });\n}\n\nexport async function deleteStoredDocumentIfMatches(\n execSql: ExecSql,\n expectedRecord: StoredDocumentRecord,\n deleteClientProjection: (transactionExecSql: ExecSql) => Promise<void>,\n): Promise<boolean> {\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n await ensureDocumentDeletionTables(lockedExecSql);\n return getClientSQLitePersistenceRuntime(lockedExecSql).transaction(\n async (tx) => {\n const existingDocument = await loadDocumentRecord(lockedExecSql, {\n appKind: DOCUMENTS_APP_KIND,\n localId: expectedRecord.id,\n });\n if (\n !existingDocument ||\n !sameCanonicalDocumentSecurityIdentity(\n existingDocument,\n expectedRecord,\n )\n ) {\n return false;\n }\n const [placement] = await tx\n .select({ containerId: documentProjection.containerId })\n .from(documentProjection)\n .where(eq(documentProjection.localId, expectedRecord.id))\n .limit(1);\n if ((placement?.containerId ?? null) !== expectedRecord.containerId)\n return false;\n if (\n existingDocument.documentId &&\n (await hasAnotherDocumentAlias({\n documentId: existingDocument.documentId,\n localId: expectedRecord.id,\n tx,\n }))\n ) {\n return false;\n }\n await deleteStoredDocumentRows({\n existingDocumentId: existingDocument.documentId ?? null,\n localId: expectedRecord.id,\n lockedExecSql,\n tx,\n });\n await deleteClientProjection(lockedExecSql);\n return true;\n },\n { behavior: \"immediate\" },\n );\n });\n}\n\nexport async function deleteStoredDocumentSideRowsIfAbsent(\n execSql: ExecSql,\n localId: string,\n expectedDocumentId: string | null,\n deleteClientProjection: (transactionExecSql: ExecSql) => Promise<void>,\n): Promise<boolean> {\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n await ensureDocumentDeletionTables(lockedExecSql);\n return getClientSQLitePersistenceRuntime(lockedExecSql).transaction(\n async (tx) => {\n const existingDocument = await loadDocumentRecord(lockedExecSql, {\n appKind: DOCUMENTS_APP_KIND,\n localId,\n });\n if (existingDocument) return false;\n if (\n expectedDocumentId &&\n (await hasAnotherDocumentAlias({\n documentId: expectedDocumentId,\n localId,\n tx,\n }))\n ) {\n return false;\n }\n\n await deleteStoredDocumentRows({\n existingDocumentId: expectedDocumentId,\n localId,\n lockedExecSql,\n tx,\n });\n await deleteClientProjection(lockedExecSql);\n return true;\n },\n { behavior: \"immediate\" },\n );\n });\n}\n"]}
@@ -262,7 +262,8 @@ export interface DocumentsPersistence {
262
262
  deleteDocument: (execSql: ExecSql, localId: string) => Promise<void>;
263
263
  /**
264
264
  * Delete the canonical row and every side row only when its security identity
265
- * still matches the captured record. The comparison, deletes, and client
265
+ * and container placement still match the captured record. The comparison,
266
+ * deletes, and client
266
267
  * projection callback must share one write transaction.
267
268
  */
268
269
  deleteDocumentIfMatches: (execSql: ExecSql, expectedRecord: StoredDocumentRecord, deleteClientProjection: (transactionExecSql: ExecSql) => Promise<void>) => Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n DocumentProjectorRegistryInput,\n StoredDocumentKind,\n} from \"../../documents/documentKinds\";\nimport type {\n DiscoveredDocumentInput,\n DocumentSummary,\n} from \"../../documents/documentSummary\";\nimport type { DocumentSyncPullContinuation } from \"../../documents/shared/pullContinuation\";\nimport type {\n DocumentRecord as BaseDocumentRecord,\n PendingUpdateFields,\n PendingUpdateRecord,\n} from \"../../sqlite/documentPersistence\";\nimport type { ExecSql } from \"../../sqlite/sqlSchema\";\nimport type { DocumentOrphanBlobPersistence } from \"./orphanBlobPersistence\";\n\nexport type { PendingUpdateRecord } from \"../../sqlite/documentPersistence\";\n\nexport interface StoredDocumentRecord extends BaseDocumentRecord {\n containerId: string | null;\n documentKind?: StoredDocumentKind;\n text: string;\n title?: string;\n}\n\nexport type DiscardDocumentToShellResult =\n | { discarded: false }\n | {\n discarded: true;\n documentKind: StoredDocumentKind;\n };\n\nexport interface PendingUpdateInsert extends PendingUpdateFields {\n localId: string;\n}\n\n/**\n * The identity a pending attachment upload reuses across attempts so a retry\n * (next sync pass or after a restart) reproduces byte-identical encrypted bytes\n * and resumes the same multipart stage instead of orphaning it. `contentKey` and\n * the chunk `nonceSeed` are base64. `partSize`/`stageId` are filled in once the\n * multipart stage has been opened and remain null before that.\n */\nexport interface PendingAttachmentUploadIdentity {\n blobId: string;\n contentKey: string;\n contentKeyEpoch: number;\n nonceSeed: string;\n partSize: number | null;\n plaintextSha256: string;\n stageId: string | null;\n}\n\nexport interface PendingAttachmentRecord {\n byteLength: number;\n contentSha256: string;\n localId: string;\n mimeType: string | null;\n name: string;\n slotId: string;\n storageKey: string;\n upload?: PendingAttachmentUploadIdentity | null;\n}\n\nexport interface LocalAttachmentRecord {\n blobId: string | null;\n byteLength: number;\n // Plaintext digest of the bytes at `storageKey`. Compared with the digest\n // the document content records for the slot: a held copy that differs is a\n // validly signed served binding the document has not recorded (yet).\n contentSha256: string;\n // Set once the slot is unlinked from a synced document and cleared when the\n // slot is written again. The record survives the unlink so the next sync can\n // still detach the remote binding; `detachedAt` is what keeps the local read\n // models from reporting the slot as a live blob reference in the meantime.\n detachedAt: string | null;\n localId: string;\n mimeType: string | null;\n slotId: string;\n storageKey: string;\n}\n\nexport interface AttachmentStagingRows {\n localAttachments: ReadonlyArray<LocalAttachmentRecord>;\n pendingAttachments: ReadonlyArray<PendingAttachmentRecord>;\n}\n\nexport interface AttachmentRemovalRows {\n mode: \"delete\" | \"detach\";\n slotId: string;\n storageKey: string;\n}\n\nexport interface DocumentHistoryRestoreState {\n snapshot: string;\n tailUpdates: readonly {\n origin: \"local\" | \"remote\";\n updateData: string;\n }[];\n}\n\nexport interface RelinkPersistedDocumentInput {\n accessEpoch: number;\n accessStateHash?: string | null;\n containerId: string;\n documentId: string | null;\n localId: string;\n}\n\nexport interface ContainerDocumentTombstoneInput {\n containerId: string;\n documentId: string;\n /** Epoch of the verified head; apply refuses a head below local state. */\n accessEpoch: number;\n /** Verified head link set (omits `containerId`); apply needs signed evidence. */\n linkedContainerIds: ReadonlyArray<string>;\n updatedAt: string;\n}\n\nexport type DocumentSummarySortDirection = \"asc\" | \"desc\";\nexport type DocumentSummarySortKey = \"kind\" | \"title\" | \"updated\";\n\nexport interface DocumentSummarySort {\n readonly direction: DocumentSummarySortDirection;\n readonly key: DocumentSummarySortKey;\n}\n\nexport interface ListDocumentSummariesInput {\n documentKind?: StoredDocumentKind | undefined;\n limit?: number | undefined;\n offset?: number | undefined;\n sort?: DocumentSummarySort | undefined;\n}\n\nexport interface DocumentSummaryList {\n readonly rows: ReadonlyArray<DocumentSummary>;\n readonly totalCount: number;\n}\n\nexport interface DocumentsPersistence {\n readonly orphanBlobs: DocumentOrphanBlobPersistence;\n /**\n * The adapter guarantees that recovery checkpoint replacement, selection of\n * covered local history, rejection of unproven ordinary pending rows, and\n * quarantine of every queued checkpoint artifact share the guarded\n * `commitDocumentMutation` transaction, which also advances the canonical\n * recovery generation. An unrelated history tail not covered by the rebuild\n * must abort that transaction.\n */\n readonly supportsAtomicRecoveryHistoryPruning: boolean;\n /**\n * Atomically create the canonical row, standard projections, and birth\n * checkpoint. Returns null when another initializer already owns localId.\n */\n createDocumentWithHistoryCheckpoint: (\n execSql: ExecSql,\n document: StoredDocumentRecord,\n historyCheckpoint: {\n endVersionVector: string;\n snapshot: string;\n },\n options:\n | {\n pendingUpdate?: PendingUpdateFields;\n /** Recheck volatile caller ownership inside the create transaction. */\n stillCurrent?: (() => boolean) | undefined;\n updatedAt?: string;\n }\n | undefined,\n saveClientProjection: (\n transactionExecSql: ExecSql,\n updatedAt: string,\n ) => Promise<void>,\n ) => Promise<string | null>;\n /**\n * Conditionally commit one already-prepared mutation against the exact\n * durable record it was derived from. The identity/progress comparison,\n * attachment and history writes, accepted-queue settlement,\n * canonical/projection save, and client projection callback must share one\n * adapter transaction.\n */\n commitDocumentMutation: (\n execSql: ExecSql,\n input: {\n acceptedPendingUpdateIds: readonly string[];\n attachmentRemoval?: AttachmentRemovalRows | undefined;\n attachmentStaging?: AttachmentStagingRows | undefined;\n clearSyncFailure?: boolean | undefined;\n document: StoredDocumentRecord;\n expectedRecord: StoredDocumentRecord;\n historyCheckpoint?:\n | {\n coveredTailIds: readonly string[];\n endVersionVector: string;\n pruneCoveredLocalState: boolean;\n snapshot: string;\n }\n | undefined;\n historyUpdateOrigin?: \"local\" | \"remote\" | undefined;\n historyUpdates?: readonly string[] | undefined;\n pendingUpdate?: PendingUpdateFields | undefined;\n settleAcceptedPendingOnConflict: boolean;\n /**\n * Recheck volatile caller ownership inside the adapter transaction,\n * before any durable side effect. A false result aborts the commit.\n */\n stillCurrent?: (() => boolean) | undefined;\n updatedAt?: string | undefined;\n },\n saveClientProjection: (\n transactionExecSql: ExecSql,\n updatedAt: string,\n ) => Promise<void>,\n ) => Promise<\n | { committed: true; updatedAt: string }\n | { committed: false; currentRecord: StoredDocumentRecord | null }\n >;\n /** Settle acknowledged rows only while the response's security identity survives. */\n settleAcceptedPendingUpdates: (\n execSql: ExecSql,\n input: {\n expectedRecord: StoredDocumentRecord;\n pendingUpdateIds: readonly string[];\n },\n ) => Promise<StoredDocumentRecord | null>;\n ensureSchema: (execSql: ExecSql) => Promise<void>;\n listDocuments: (execSql: ExecSql) => Promise<DocumentSummary[]>;\n listDocumentSummaries: (\n execSql: ExecSql,\n input?: ListDocumentSummariesInput,\n ) => Promise<DocumentSummaryList>;\n listDocumentsByContainerIdsOrDocumentIds: (\n execSql: ExecSql,\n input: {\n containerIds: ReadonlyArray<string>;\n documentIds: ReadonlyArray<string>;\n },\n ) => Promise<DocumentSummary[]>;\n findDocumentLocalIdsByContainerId: (\n execSql: ExecSql,\n containerId: string,\n ) => Promise<string[]>;\n /**\n * Select one local row for a remote document identity. Prefer any row with\n * queued updates or a deferred-sync frontier, then break duplicates by\n * updatedAt and localId descending.\n */\n findLocalIdByDocumentId: (\n execSql: ExecSql,\n documentId: string,\n ) => Promise<string | null>;\n /** Canonical-row probe; false authorizes destructive orphan teardown. */\n hasDocument: (execSql: ExecSql, localId: string) => Promise<boolean>;\n /** Observe whether the canonical row still names the expected remote stream. */\n documentIdentityMatches: (\n execSql: ExecSql,\n localId: string,\n expectedDocumentId: string | null,\n expectedRecoveryGeneration: number,\n ) => Promise<boolean>;\n loadDocument: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<StoredDocumentRecord | null>;\n /** Read the canonical record and its history from one database snapshot. */\n loadDocumentWithHistoryRestoreState: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<{\n document: StoredDocumentRecord | null;\n historyRestoreState: DocumentHistoryRestoreState | null;\n }>;\n /** Read every document-store startup row from one database snapshot. */\n loadDocumentStoreState: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<{\n document: StoredDocumentRecord | null;\n historyRestoreState: DocumentHistoryRestoreState | null;\n localAttachments: LocalAttachmentRecord[];\n pendingAttachments: PendingAttachmentRecord[];\n }>;\n /**\n * Atomically replace the exact rejected pull continuation with the durable\n * recovery marker when every supplied sync-identity field still matches.\n * Return the authoritative current record after the compare-and-set, or\n * null when the canonical row no longer exists. A CAS loss returns the\n * winning record so the live store can adopt its newer progress.\n */\n invalidatePullContinuation: (\n execSql: ExecSql,\n input: {\n accessEpoch: number;\n accessStateHash: string | null;\n continuation: DocumentSyncPullContinuation;\n contentKeyBundle: string | null;\n documentId: string;\n documentKekTargets: string | null;\n documentManifestBundle: string | null;\n lastCommitLsn: string | null;\n localId: string;\n },\n ) => Promise<{\n historyRestoreState: DocumentHistoryRestoreState | null;\n record: StoredDocumentRecord;\n } | null>;\n // Read the authoritative container placement for a locally persisted document\n // straight from its projection row. Returns `undefined` when no projection row\n // exists yet (a first create/discovery persist), so a caller can distinguish\n // that from a row that legitimately carries a null container (a document\n // unlinked from every container). Container placement is owned by the\n // link/tombstone/discovery layer, so a content-metadata persist reads this to\n // avoid republishing a stale in-memory container.\n loadDocumentContainer: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<{ containerId: string | null } | undefined>;\n saveDocument: (\n execSql: ExecSql,\n document: StoredDocumentRecord,\n options?: {\n updatedAt?: string;\n },\n ) => Promise<string>;\n /**\n * Durable full-history support (checkpoint + append-only tail) — the ONLY\n * persisted content source. REQUIRED: a persistence without these could\n * report successful writes whose documents reopen empty.\n */\n appendHistoryUpdates: (\n execSql: ExecSql,\n input: {\n localId: string;\n origin: \"local\" | \"remote\";\n updates: readonly string[];\n },\n ) => Promise<void>;\n loadHistoryRestoreState: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<DocumentHistoryRestoreState | null>;\n readHistoryTailSize: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<{\n byteLength: number;\n hasCheckpoint: boolean;\n rowCount: number;\n }>;\n listHistoryTailEntries: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<{ id: string; updateData: string }[]>;\n replaceHistoryCheckpoint: (\n execSql: ExecSql,\n input: {\n coveredTailIds: readonly string[];\n endVersionVector: string;\n force?: boolean;\n localId: string;\n snapshot: string;\n stillCurrent?: () => boolean;\n },\n ) => Promise<void>;\n saveDocumentAndDeletePendingUpdates: (\n execSql: ExecSql,\n document: StoredDocumentRecord,\n pendingUpdateIds: readonly string[],\n options?: {\n updatedAt?: string;\n },\n ) => Promise<string>;\n deleteDocument: (execSql: ExecSql, localId: string) => Promise<void>;\n /**\n * Delete the canonical row and every side row only when its security identity\n * still matches the captured record. The comparison, deletes, and client\n * projection callback must share one write transaction.\n */\n deleteDocumentIfMatches: (\n execSql: ExecSql,\n expectedRecord: StoredDocumentRecord,\n deleteClientProjection: (transactionExecSql: ExecSql) => Promise<void>,\n ) => Promise<boolean>;\n /**\n * Delete orphaned side rows only if the canonical document is still absent.\n * The absence check, row cleanup, and client projection callback must share\n * one write transaction so a concurrent create is preserved.\n */\n deleteDocumentSideRowsIfAbsent: (\n execSql: ExecSql,\n localId: string,\n expectedDocumentId: string | null,\n deleteClientProjection: (transactionExecSql: ExecSql) => Promise<void>,\n ) => Promise<boolean>;\n upsertDiscoveredDocument: (\n execSql: ExecSql,\n input: DiscoveredDocumentInput,\n ) => Promise<DocumentSummary>;\n relinkPersistedDocument: (\n execSql: ExecSql,\n input: RelinkPersistedDocumentInput,\n ) => Promise<DocumentSummary | null>;\n listPendingUpdates: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<PendingUpdateRecord[]>;\n rekeyPendingUpdate: (execSql: ExecSql, id: string) => Promise<string | null>;\n listPendingAttachments: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<PendingAttachmentRecord[]>;\n listLocalAttachments: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<LocalAttachmentRecord[]>;\n /**\n * Durably append the outgoing row and matching local-history tail entry.\n * When expectations are supplied, compare the document identity and recovery\n * generation with the canonical row inside that same mutation and return\n * false on absence or mismatch. Adapters must not insert either row.\n */\n enqueuePendingUpdate: (\n execSql: ExecSql,\n pendingUpdate: PendingUpdateInsert,\n options?: {\n expectedDocumentId: string | null;\n expectedRecoveryGeneration: number;\n },\n ) => Promise<boolean>;\n /** Replace a hydrated copy only if its observed durable slot and live document still match. */\n saveHydratedAttachment: (\n execSql: ExecSql,\n input: {\n attachment: LocalAttachmentRecord;\n expectedStorageKey: string | null;\n expectedSnapshotEndVersion: string | null;\n stillCurrent: () => boolean;\n },\n ) => Promise<boolean>;\n saveLocalAttachment: (\n execSql: ExecSql,\n attachment: LocalAttachmentRecord,\n ) => Promise<void>;\n deleteLocalAttachment: (\n execSql: ExecSql,\n localId: string,\n slotId: string,\n storageKey: string,\n ) => Promise<void>;\n markLocalAttachmentDetached: (\n execSql: ExecSql,\n localId: string,\n slotId: string,\n storageKey: string,\n ) => Promise<void>;\n savePendingAttachment: (\n execSql: ExecSql,\n attachment: PendingAttachmentRecord,\n ) => Promise<void>;\n deletePendingUpdate: (execSql: ExecSql, id: string) => Promise<void>;\n deletePendingUpdates: (execSql: ExecSql, localId: string) => Promise<void>;\n deletePendingAttachment: (\n execSql: ExecSql,\n localId: string,\n slotId: string,\n storageKey: string,\n ) => Promise<void>;\n deletePendingAttachments: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<void>;\n /**\n * Atomically convert a stuck document's local state to the\n * freshly-discovered-share shell (see the SQL implementation's doc\n * comment). The projector registry clears the document-kind client\n * projection in the same transaction — those tables live with the app's\n * projector definitions, so the CALLER'S registry is required (the default\n * registry knows no app kinds, and a discard that silently kept a\n * contact's or card's projected fields would leak the discarded values).\n * The method itself is optional: implementations without the full document\n * schema (container metadata, simple doubles) simply do not offer the\n * discard escape hatch.\n */\n /**\n * `expectedDocumentId` is revalidated against the loaded record inside the\n * serialized mutation: a stale caller (or a relink that raced the request)\n * must never discard a different identity's edits.\n */\n discardDocumentToShell?: (\n execSql: ExecSql,\n localId: string,\n expectedDocumentId: string,\n documentProjectors: DocumentProjectorRegistryInput,\n ) => Promise<DiscardDocumentToShellResult>;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n DocumentProjectorRegistryInput,\n StoredDocumentKind,\n} from \"../../documents/documentKinds\";\nimport type {\n DiscoveredDocumentInput,\n DocumentSummary,\n} from \"../../documents/documentSummary\";\nimport type { DocumentSyncPullContinuation } from \"../../documents/shared/pullContinuation\";\nimport type {\n DocumentRecord as BaseDocumentRecord,\n PendingUpdateFields,\n PendingUpdateRecord,\n} from \"../../sqlite/documentPersistence\";\nimport type { ExecSql } from \"../../sqlite/sqlSchema\";\nimport type { DocumentOrphanBlobPersistence } from \"./orphanBlobPersistence\";\n\nexport type { PendingUpdateRecord } from \"../../sqlite/documentPersistence\";\n\nexport interface StoredDocumentRecord extends BaseDocumentRecord {\n containerId: string | null;\n documentKind?: StoredDocumentKind;\n text: string;\n title?: string;\n}\n\nexport type DiscardDocumentToShellResult =\n | { discarded: false }\n | {\n discarded: true;\n documentKind: StoredDocumentKind;\n };\n\nexport interface PendingUpdateInsert extends PendingUpdateFields {\n localId: string;\n}\n\n/**\n * The identity a pending attachment upload reuses across attempts so a retry\n * (next sync pass or after a restart) reproduces byte-identical encrypted bytes\n * and resumes the same multipart stage instead of orphaning it. `contentKey` and\n * the chunk `nonceSeed` are base64. `partSize`/`stageId` are filled in once the\n * multipart stage has been opened and remain null before that.\n */\nexport interface PendingAttachmentUploadIdentity {\n blobId: string;\n contentKey: string;\n contentKeyEpoch: number;\n nonceSeed: string;\n partSize: number | null;\n plaintextSha256: string;\n stageId: string | null;\n}\n\nexport interface PendingAttachmentRecord {\n byteLength: number;\n contentSha256: string;\n localId: string;\n mimeType: string | null;\n name: string;\n slotId: string;\n storageKey: string;\n upload?: PendingAttachmentUploadIdentity | null;\n}\n\nexport interface LocalAttachmentRecord {\n blobId: string | null;\n byteLength: number;\n // Plaintext digest of the bytes at `storageKey`. Compared with the digest\n // the document content records for the slot: a held copy that differs is a\n // validly signed served binding the document has not recorded (yet).\n contentSha256: string;\n // Set once the slot is unlinked from a synced document and cleared when the\n // slot is written again. The record survives the unlink so the next sync can\n // still detach the remote binding; `detachedAt` is what keeps the local read\n // models from reporting the slot as a live blob reference in the meantime.\n detachedAt: string | null;\n localId: string;\n mimeType: string | null;\n slotId: string;\n storageKey: string;\n}\n\nexport interface AttachmentStagingRows {\n localAttachments: ReadonlyArray<LocalAttachmentRecord>;\n pendingAttachments: ReadonlyArray<PendingAttachmentRecord>;\n}\n\nexport interface AttachmentRemovalRows {\n mode: \"delete\" | \"detach\";\n slotId: string;\n storageKey: string;\n}\n\nexport interface DocumentHistoryRestoreState {\n snapshot: string;\n tailUpdates: readonly {\n origin: \"local\" | \"remote\";\n updateData: string;\n }[];\n}\n\nexport interface RelinkPersistedDocumentInput {\n accessEpoch: number;\n accessStateHash?: string | null;\n containerId: string;\n documentId: string | null;\n localId: string;\n}\n\nexport interface ContainerDocumentTombstoneInput {\n containerId: string;\n documentId: string;\n /** Epoch of the verified head; apply refuses a head below local state. */\n accessEpoch: number;\n /** Verified head link set (omits `containerId`); apply needs signed evidence. */\n linkedContainerIds: ReadonlyArray<string>;\n updatedAt: string;\n}\n\nexport type DocumentSummarySortDirection = \"asc\" | \"desc\";\nexport type DocumentSummarySortKey = \"kind\" | \"title\" | \"updated\";\n\nexport interface DocumentSummarySort {\n readonly direction: DocumentSummarySortDirection;\n readonly key: DocumentSummarySortKey;\n}\n\nexport interface ListDocumentSummariesInput {\n documentKind?: StoredDocumentKind | undefined;\n limit?: number | undefined;\n offset?: number | undefined;\n sort?: DocumentSummarySort | undefined;\n}\n\nexport interface DocumentSummaryList {\n readonly rows: ReadonlyArray<DocumentSummary>;\n readonly totalCount: number;\n}\n\nexport interface DocumentsPersistence {\n readonly orphanBlobs: DocumentOrphanBlobPersistence;\n /**\n * The adapter guarantees that recovery checkpoint replacement, selection of\n * covered local history, rejection of unproven ordinary pending rows, and\n * quarantine of every queued checkpoint artifact share the guarded\n * `commitDocumentMutation` transaction, which also advances the canonical\n * recovery generation. An unrelated history tail not covered by the rebuild\n * must abort that transaction.\n */\n readonly supportsAtomicRecoveryHistoryPruning: boolean;\n /**\n * Atomically create the canonical row, standard projections, and birth\n * checkpoint. Returns null when another initializer already owns localId.\n */\n createDocumentWithHistoryCheckpoint: (\n execSql: ExecSql,\n document: StoredDocumentRecord,\n historyCheckpoint: {\n endVersionVector: string;\n snapshot: string;\n },\n options:\n | {\n pendingUpdate?: PendingUpdateFields;\n /** Recheck volatile caller ownership inside the create transaction. */\n stillCurrent?: (() => boolean) | undefined;\n updatedAt?: string;\n }\n | undefined,\n saveClientProjection: (\n transactionExecSql: ExecSql,\n updatedAt: string,\n ) => Promise<void>,\n ) => Promise<string | null>;\n /**\n * Conditionally commit one already-prepared mutation against the exact\n * durable record it was derived from. The identity/progress comparison,\n * attachment and history writes, accepted-queue settlement,\n * canonical/projection save, and client projection callback must share one\n * adapter transaction.\n */\n commitDocumentMutation: (\n execSql: ExecSql,\n input: {\n acceptedPendingUpdateIds: readonly string[];\n attachmentRemoval?: AttachmentRemovalRows | undefined;\n attachmentStaging?: AttachmentStagingRows | undefined;\n clearSyncFailure?: boolean | undefined;\n document: StoredDocumentRecord;\n expectedRecord: StoredDocumentRecord;\n historyCheckpoint?:\n | {\n coveredTailIds: readonly string[];\n endVersionVector: string;\n pruneCoveredLocalState: boolean;\n snapshot: string;\n }\n | undefined;\n historyUpdateOrigin?: \"local\" | \"remote\" | undefined;\n historyUpdates?: readonly string[] | undefined;\n pendingUpdate?: PendingUpdateFields | undefined;\n settleAcceptedPendingOnConflict: boolean;\n /**\n * Recheck volatile caller ownership inside the adapter transaction,\n * before any durable side effect. A false result aborts the commit.\n */\n stillCurrent?: (() => boolean) | undefined;\n updatedAt?: string | undefined;\n },\n saveClientProjection: (\n transactionExecSql: ExecSql,\n updatedAt: string,\n ) => Promise<void>,\n ) => Promise<\n | { committed: true; updatedAt: string }\n | { committed: false; currentRecord: StoredDocumentRecord | null }\n >;\n /** Settle acknowledged rows only while the response's security identity survives. */\n settleAcceptedPendingUpdates: (\n execSql: ExecSql,\n input: {\n expectedRecord: StoredDocumentRecord;\n pendingUpdateIds: readonly string[];\n },\n ) => Promise<StoredDocumentRecord | null>;\n ensureSchema: (execSql: ExecSql) => Promise<void>;\n listDocuments: (execSql: ExecSql) => Promise<DocumentSummary[]>;\n listDocumentSummaries: (\n execSql: ExecSql,\n input?: ListDocumentSummariesInput,\n ) => Promise<DocumentSummaryList>;\n listDocumentsByContainerIdsOrDocumentIds: (\n execSql: ExecSql,\n input: {\n containerIds: ReadonlyArray<string>;\n documentIds: ReadonlyArray<string>;\n },\n ) => Promise<DocumentSummary[]>;\n findDocumentLocalIdsByContainerId: (\n execSql: ExecSql,\n containerId: string,\n ) => Promise<string[]>;\n /**\n * Select one local row for a remote document identity. Prefer any row with\n * queued updates or a deferred-sync frontier, then break duplicates by\n * updatedAt and localId descending.\n */\n findLocalIdByDocumentId: (\n execSql: ExecSql,\n documentId: string,\n ) => Promise<string | null>;\n /** Canonical-row probe; false authorizes destructive orphan teardown. */\n hasDocument: (execSql: ExecSql, localId: string) => Promise<boolean>;\n /** Observe whether the canonical row still names the expected remote stream. */\n documentIdentityMatches: (\n execSql: ExecSql,\n localId: string,\n expectedDocumentId: string | null,\n expectedRecoveryGeneration: number,\n ) => Promise<boolean>;\n loadDocument: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<StoredDocumentRecord | null>;\n /** Read the canonical record and its history from one database snapshot. */\n loadDocumentWithHistoryRestoreState: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<{\n document: StoredDocumentRecord | null;\n historyRestoreState: DocumentHistoryRestoreState | null;\n }>;\n /** Read every document-store startup row from one database snapshot. */\n loadDocumentStoreState: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<{\n document: StoredDocumentRecord | null;\n historyRestoreState: DocumentHistoryRestoreState | null;\n localAttachments: LocalAttachmentRecord[];\n pendingAttachments: PendingAttachmentRecord[];\n }>;\n /**\n * Atomically replace the exact rejected pull continuation with the durable\n * recovery marker when every supplied sync-identity field still matches.\n * Return the authoritative current record after the compare-and-set, or\n * null when the canonical row no longer exists. A CAS loss returns the\n * winning record so the live store can adopt its newer progress.\n */\n invalidatePullContinuation: (\n execSql: ExecSql,\n input: {\n accessEpoch: number;\n accessStateHash: string | null;\n continuation: DocumentSyncPullContinuation;\n contentKeyBundle: string | null;\n documentId: string;\n documentKekTargets: string | null;\n documentManifestBundle: string | null;\n lastCommitLsn: string | null;\n localId: string;\n },\n ) => Promise<{\n historyRestoreState: DocumentHistoryRestoreState | null;\n record: StoredDocumentRecord;\n } | null>;\n // Read the authoritative container placement for a locally persisted document\n // straight from its projection row. Returns `undefined` when no projection row\n // exists yet (a first create/discovery persist), so a caller can distinguish\n // that from a row that legitimately carries a null container (a document\n // unlinked from every container). Container placement is owned by the\n // link/tombstone/discovery layer, so a content-metadata persist reads this to\n // avoid republishing a stale in-memory container.\n loadDocumentContainer: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<{ containerId: string | null } | undefined>;\n saveDocument: (\n execSql: ExecSql,\n document: StoredDocumentRecord,\n options?: {\n updatedAt?: string;\n },\n ) => Promise<string>;\n /**\n * Durable full-history support (checkpoint + append-only tail) — the ONLY\n * persisted content source. REQUIRED: a persistence without these could\n * report successful writes whose documents reopen empty.\n */\n appendHistoryUpdates: (\n execSql: ExecSql,\n input: {\n localId: string;\n origin: \"local\" | \"remote\";\n updates: readonly string[];\n },\n ) => Promise<void>;\n loadHistoryRestoreState: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<DocumentHistoryRestoreState | null>;\n readHistoryTailSize: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<{\n byteLength: number;\n hasCheckpoint: boolean;\n rowCount: number;\n }>;\n listHistoryTailEntries: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<{ id: string; updateData: string }[]>;\n replaceHistoryCheckpoint: (\n execSql: ExecSql,\n input: {\n coveredTailIds: readonly string[];\n endVersionVector: string;\n force?: boolean;\n localId: string;\n snapshot: string;\n stillCurrent?: () => boolean;\n },\n ) => Promise<void>;\n saveDocumentAndDeletePendingUpdates: (\n execSql: ExecSql,\n document: StoredDocumentRecord,\n pendingUpdateIds: readonly string[],\n options?: {\n updatedAt?: string;\n },\n ) => Promise<string>;\n deleteDocument: (execSql: ExecSql, localId: string) => Promise<void>;\n /**\n * Delete the canonical row and every side row only when its security identity\n * and container placement still match the captured record. The comparison,\n * deletes, and client\n * projection callback must share one write transaction.\n */\n deleteDocumentIfMatches: (\n execSql: ExecSql,\n expectedRecord: StoredDocumentRecord,\n deleteClientProjection: (transactionExecSql: ExecSql) => Promise<void>,\n ) => Promise<boolean>;\n /**\n * Delete orphaned side rows only if the canonical document is still absent.\n * The absence check, row cleanup, and client projection callback must share\n * one write transaction so a concurrent create is preserved.\n */\n deleteDocumentSideRowsIfAbsent: (\n execSql: ExecSql,\n localId: string,\n expectedDocumentId: string | null,\n deleteClientProjection: (transactionExecSql: ExecSql) => Promise<void>,\n ) => Promise<boolean>;\n upsertDiscoveredDocument: (\n execSql: ExecSql,\n input: DiscoveredDocumentInput,\n ) => Promise<DocumentSummary>;\n relinkPersistedDocument: (\n execSql: ExecSql,\n input: RelinkPersistedDocumentInput,\n ) => Promise<DocumentSummary | null>;\n listPendingUpdates: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<PendingUpdateRecord[]>;\n rekeyPendingUpdate: (execSql: ExecSql, id: string) => Promise<string | null>;\n listPendingAttachments: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<PendingAttachmentRecord[]>;\n listLocalAttachments: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<LocalAttachmentRecord[]>;\n /**\n * Durably append the outgoing row and matching local-history tail entry.\n * When expectations are supplied, compare the document identity and recovery\n * generation with the canonical row inside that same mutation and return\n * false on absence or mismatch. Adapters must not insert either row.\n */\n enqueuePendingUpdate: (\n execSql: ExecSql,\n pendingUpdate: PendingUpdateInsert,\n options?: {\n expectedDocumentId: string | null;\n expectedRecoveryGeneration: number;\n },\n ) => Promise<boolean>;\n /** Replace a hydrated copy only if its observed durable slot and live document still match. */\n saveHydratedAttachment: (\n execSql: ExecSql,\n input: {\n attachment: LocalAttachmentRecord;\n expectedStorageKey: string | null;\n expectedSnapshotEndVersion: string | null;\n stillCurrent: () => boolean;\n },\n ) => Promise<boolean>;\n saveLocalAttachment: (\n execSql: ExecSql,\n attachment: LocalAttachmentRecord,\n ) => Promise<void>;\n deleteLocalAttachment: (\n execSql: ExecSql,\n localId: string,\n slotId: string,\n storageKey: string,\n ) => Promise<void>;\n markLocalAttachmentDetached: (\n execSql: ExecSql,\n localId: string,\n slotId: string,\n storageKey: string,\n ) => Promise<void>;\n savePendingAttachment: (\n execSql: ExecSql,\n attachment: PendingAttachmentRecord,\n ) => Promise<void>;\n deletePendingUpdate: (execSql: ExecSql, id: string) => Promise<void>;\n deletePendingUpdates: (execSql: ExecSql, localId: string) => Promise<void>;\n deletePendingAttachment: (\n execSql: ExecSql,\n localId: string,\n slotId: string,\n storageKey: string,\n ) => Promise<void>;\n deletePendingAttachments: (\n execSql: ExecSql,\n localId: string,\n ) => Promise<void>;\n /**\n * Atomically convert a stuck document's local state to the\n * freshly-discovered-share shell (see the SQL implementation's doc\n * comment). The projector registry clears the document-kind client\n * projection in the same transaction — those tables live with the app's\n * projector definitions, so the CALLER'S registry is required (the default\n * registry knows no app kinds, and a discard that silently kept a\n * contact's or card's projected fields would leak the discarded values).\n * The method itself is optional: implementations without the full document\n * schema (container metadata, simple doubles) simply do not offer the\n * discard escape hatch.\n */\n /**\n * `expectedDocumentId` is revalidated against the loaded record inside the\n * serialized mutation: a stale caller (or a relink that raced the request)\n * must never discard a different identity's edits.\n */\n discardDocumentToShell?: (\n execSql: ExecSql,\n localId: string,\n expectedDocumentId: string,\n documentProjectors: DocumentProjectorRegistryInput,\n ) => Promise<DiscardDocumentToShellResult>;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { type VerifiedPrincipalPolicyCurrent } from "../../internal/crypto/index.js";
1
+ import { type PrincipalPolicyCheckpoint, type VerifiedPrincipalPolicyCurrent } from "../../internal/crypto/index.js";
2
2
  import { type ExecSql } from "../sqlite/sqlSchema.js";
3
3
  import { type PrincipalHistoryEvidencePage } from "./principalHistoryEvidencePersistence.js";
4
4
  import type { PrincipalHistoryPrefix } from "./principalHistoryPrefixPersistence.js";
@@ -13,6 +13,17 @@ export interface AcknowledgedPrincipalCurrentPublication {
13
13
  readonly predecessorIndexRootHash: string | null;
14
14
  readonly evidence: PrincipalHistoryEvidencePage;
15
15
  }
16
+ export interface ReconciledPrincipalCurrentPublication extends AcknowledgedPrincipalCurrentPublication {
17
+ readonly preservePrefix: boolean;
18
+ readonly observedPrefixProgress: string | null;
19
+ readonly observedCheckpoint: PrincipalPolicyCheckpoint | null;
20
+ readonly retainedPrefix: PrincipalHistoryPrefix;
21
+ readonly checkpointPolicy: VerifiedPrincipalPolicyCurrent;
22
+ }
23
+ /** A local writer won the CAS; retry the same authenticated receipt without HTTP. */
24
+ export declare class PrincipalAcknowledgementChangedError extends Error {
25
+ constructor();
26
+ }
16
27
  export interface AcknowledgedPrincipalCurrentRetirement {
17
28
  readonly principalId: string;
18
29
  readonly principalType: VerifiedPrincipalPolicyCurrent["principalType"];
@@ -22,7 +33,7 @@ export interface AcknowledgedPrincipalCurrentRetirement {
22
33
  export declare function persistAcknowledgedPrincipalCurrents(input: {
23
34
  readonly execSql: ExecSql;
24
35
  readonly organizationId: string;
25
- readonly entries: readonly AcknowledgedPrincipalCurrentPublication[];
36
+ readonly entries: readonly ReconciledPrincipalCurrentPublication[];
26
37
  readonly retirements?: readonly AcknowledgedPrincipalCurrentRetirement[] | undefined;
27
38
  readonly stillCurrent: () => boolean;
28
39
  }): Promise<void>;
@@ -13,6 +13,13 @@ import { reclaimPrincipalHistoryNodes } from "./principalHistoryNodeRetention.js
13
13
  import { retainPrincipalHistoryRoot } from "./principalHistoryRootOwnership.js";
14
14
  import { reclaimCompletedPrincipalHistoryStages, recordPrincipalHistoryStageScope, } from "./principalHistoryStageRetention.js";
15
15
  import { archivePrincipalHistoryKeyEnvelopes } from "./principalKeyEnvelopeArchivePersistence.js";
16
+ /** A local writer won the CAS; retry the same authenticated receipt without HTTP. */
17
+ export class PrincipalAcknowledgementChangedError extends Error {
18
+ constructor() {
19
+ super("Authenticated principal progress changed during acknowledgement; retry retention");
20
+ this.name = "PrincipalAcknowledgementChangedError";
21
+ }
22
+ }
16
23
  function assertPublicationScope(entries, organizationId) {
17
24
  const seen = new Set();
18
25
  for (const entry of entries) {
@@ -43,9 +50,12 @@ function assertPublicationScope(entries, organizationId) {
43
50
  }
44
51
  async function validatePublication(tx, entry) {
45
52
  const checkpoint = await loadStoredPrincipalPolicyCheckpoint(tx, entry.policy);
53
+ if (checkpoint?.version !== entry.observedCheckpoint?.version ||
54
+ checkpoint?.stateHash !== entry.observedCheckpoint?.stateHash)
55
+ throw new PrincipalAcknowledgementChangedError();
46
56
  verifyPrincipalPolicyCheckpoint({
47
- chain: entry.policy.retainedHistory,
48
- currentState: entry.policy.state,
57
+ chain: entry.checkpointPolicy.retainedHistory,
58
+ currentState: entry.checkpointPolicy.state,
49
59
  localCheckpoint: checkpoint,
50
60
  });
51
61
  if ((!checkpoint && entry.policy.version !== 1) ||
@@ -56,8 +66,8 @@ async function validatePublication(tx, entry) {
56
66
  .from(principalHistoryPrefixes)
57
67
  .where(eq(principalHistoryPrefixes.scopeId, entry.prefix.scopeId))
58
68
  .limit(1);
59
- if ((prefix?.progress ?? null) !== entry.previousPrefixProgress)
60
- throw new KeyingVerificationError("stale_predecessor", "Authenticated principal prefix changed before acknowledgement");
69
+ if ((prefix?.progress ?? null) !== entry.observedPrefixProgress)
70
+ throw new PrincipalAcknowledgementChangedError();
61
71
  }
62
72
  async function archiveAcknowledgedStageKeys(tx, entry) {
63
73
  for (const stage of [entry.predecessorStage, entry.stage]) {
@@ -134,27 +144,33 @@ export async function persistAcknowledgedPrincipalCurrents(input) {
134
144
  })
135
145
  .run();
136
146
  await archiveAcknowledgedStageKeys(tx, entry);
137
- await tx
138
- .insert(principalHistoryPrefixes)
139
- .values(entry.prefix)
140
- .onConflictDoUpdate({
141
- target: principalHistoryPrefixes.scopeId,
142
- set: entry.prefix,
143
- })
144
- .run();
145
- await reclaimCompletedPrincipalHistoryStages(tx, entry.prefix);
146
- for (const id of [
147
- `stage:${entry.stage.id}`,
148
- `prefix:${entry.prefix.scopeId}`,
149
- ])
147
+ if (!entry.preservePrefix)
148
+ await tx
149
+ .insert(principalHistoryPrefixes)
150
+ .values(entry.prefix)
151
+ .onConflictDoUpdate({
152
+ target: principalHistoryPrefixes.scopeId,
153
+ set: entry.prefix,
154
+ })
155
+ .run();
156
+ await retainPrincipalHistoryRoot(tx, {
157
+ id: `stage:${entry.stage.id}`,
158
+ scopeId: entry.prefix.scopeId,
159
+ organizationId,
160
+ rootHash: entry.evidence.indexRootHash,
161
+ });
162
+ if (!entry.preservePrefix)
150
163
  await retainPrincipalHistoryRoot(tx, {
151
- id,
164
+ id: `prefix:${entry.prefix.scopeId}`,
152
165
  scopeId: entry.prefix.scopeId,
153
166
  organizationId,
154
167
  rootHash: entry.evidence.indexRootHash,
155
168
  });
156
- await reclaimPrincipalHistoryNodes(tx, entry.prefix);
157
- await upsertPrincipalPolicyCheckpointInTransaction(tx, entry.policy.checkpoint, new Date().toISOString(), organizationId);
169
+ await reclaimCompletedPrincipalHistoryStages(tx, entry.retainedPrefix);
170
+ await reclaimPrincipalHistoryNodes(tx, entry.retainedPrefix);
171
+ if (!entry.observedCheckpoint ||
172
+ entry.observedCheckpoint.version < entry.policy.version)
173
+ await upsertPrincipalPolicyCheckpointInTransaction(tx, entry.policy.checkpoint, new Date().toISOString(), organizationId);
158
174
  }
159
175
  }, stillCurrent, { behavior: "immediate" });
160
176
  assertProjectionVerificationCurrent(() => stored.committed);