@abloatai/humans 0.37.0
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.
- package/README.md +52 -0
- package/dist/Ablo.d.ts +208 -0
- package/dist/Ablo.js +120 -0
- package/dist/client.d.ts +317 -0
- package/dist/client.js +13 -0
- package/dist/core.d.ts +35 -0
- package/dist/core.js +48 -0
- package/dist/humans.d.ts +28 -0
- package/dist/humans.js +34 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/local/BaseSyncedStore.d.ts +807 -0
- package/dist/local/BaseSyncedStore.js +1516 -0
- package/dist/local/Database.d.ts +322 -0
- package/dist/local/Database.js +1589 -0
- package/dist/local/InstanceCache.d.ts +255 -0
- package/dist/local/InstanceCache.js +1263 -0
- package/dist/local/LazyReferenceCollection.d.ts +177 -0
- package/dist/local/LazyReferenceCollection.js +461 -0
- package/dist/local/Model.d.ts +475 -0
- package/dist/local/Model.js +950 -0
- package/dist/local/ModelRegistry.d.ts +225 -0
- package/dist/local/ModelRegistry.js +539 -0
- package/dist/local/NetworkMonitor.d.ts +28 -0
- package/dist/local/NetworkMonitor.js +79 -0
- package/dist/local/RuntimeContext.d.ts +52 -0
- package/dist/local/RuntimeContext.js +80 -0
- package/dist/local/SyncClient.d.ts +516 -0
- package/dist/local/SyncClient.js +1754 -0
- package/dist/local/adapters/alwaysOnline.d.ts +14 -0
- package/dist/local/adapters/alwaysOnline.js +17 -0
- package/dist/local/adapters/inMemoryStorage.d.ts +37 -0
- package/dist/local/adapters/inMemoryStorage.js +122 -0
- package/dist/local/client/clientPrelude.d.ts +52 -0
- package/dist/local/client/clientPrelude.js +60 -0
- package/dist/local/client/consoleLogger.d.ts +35 -0
- package/dist/local/client/consoleLogger.js +44 -0
- package/dist/local/client/createInternalComponents.d.ts +50 -0
- package/dist/local/client/createInternalComponents.js +98 -0
- package/dist/local/client/createModelProxy.d.ts +248 -0
- package/dist/local/client/createModelProxy.js +880 -0
- package/dist/local/client/modelRegistration.d.ts +10 -0
- package/dist/local/client/modelRegistration.js +316 -0
- package/dist/local/client/options.d.ts +440 -0
- package/dist/local/client/options.js +7 -0
- package/dist/local/client/reactiveEngine.d.ts +53 -0
- package/dist/local/client/reactiveEngine.js +705 -0
- package/dist/local/client/resourceTypes.d.ts +12 -0
- package/dist/local/client/resourceTypes.js +10 -0
- package/dist/local/client/schemaConfig.d.ts +44 -0
- package/dist/local/client/schemaConfig.js +185 -0
- package/dist/local/client/storeCluster.d.ts +46 -0
- package/dist/local/client/storeCluster.js +133 -0
- package/dist/local/client/storeLifecycle.d.ts +61 -0
- package/dist/local/client/storeLifecycle.js +236 -0
- package/dist/local/client/validateAbloOptions.d.ts +42 -0
- package/dist/local/client/validateAbloOptions.js +43 -0
- package/dist/local/client/wsMutationExecutor.d.ts +27 -0
- package/dist/local/client/wsMutationExecutor.js +72 -0
- package/dist/local/context.d.ts +42 -0
- package/dist/local/context.js +81 -0
- package/dist/local/coordination/ClaimLog.d.ts +26 -0
- package/dist/local/coordination/ClaimLog.js +32 -0
- package/dist/local/interfaces/index.d.ts +311 -0
- package/dist/local/interfaces/index.js +9 -0
- package/dist/local/localModelContract.d.ts +14 -0
- package/dist/local/localModelContract.js +1 -0
- package/dist/local/logPosition.d.ts +31 -0
- package/dist/local/logPosition.js +53 -0
- package/dist/local/mutationPersistence.d.ts +6 -0
- package/dist/local/mutationPersistence.js +1 -0
- package/dist/local/mutators/RecordingMutation.d.ts +36 -0
- package/dist/local/mutators/RecordingMutation.js +182 -0
- package/dist/local/mutators/Transaction.d.ts +40 -0
- package/dist/local/mutators/Transaction.js +58 -0
- package/dist/local/mutators/UndoManager.d.ts +258 -0
- package/dist/local/mutators/UndoManager.js +665 -0
- package/dist/local/mutators/defineMutators.d.ts +60 -0
- package/dist/local/mutators/defineMutators.js +18 -0
- package/dist/local/mutators/inverseOp.d.ts +126 -0
- package/dist/local/mutators/inverseOp.js +71 -0
- package/dist/local/mutators/mutateActions.d.ts +45 -0
- package/dist/local/mutators/mutateActions.js +105 -0
- package/dist/local/mutators/readerActions.d.ts +33 -0
- package/dist/local/mutators/readerActions.js +57 -0
- package/dist/local/mutators/undoApply.d.ts +51 -0
- package/dist/local/mutators/undoApply.js +117 -0
- package/dist/local/persistence.d.ts +7 -0
- package/dist/local/persistence.js +9 -0
- package/dist/local/query/QueryProcessor.d.ts +75 -0
- package/dist/local/query/QueryProcessor.js +255 -0
- package/dist/local/query/client.d.ts +64 -0
- package/dist/local/query/client.js +138 -0
- package/dist/local/query/types.d.ts +85 -0
- package/dist/local/query/types.js +16 -0
- package/dist/local/schema/serialize.d.ts +1 -0
- package/dist/local/schema/serialize.js +1 -0
- package/dist/local/store/queryApi.d.ts +13 -0
- package/dist/local/store/queryApi.js +35 -0
- package/dist/local/storeContract.d.ts +145 -0
- package/dist/local/storeContract.js +12 -0
- package/dist/local/stores/DatabaseManager.d.ts +112 -0
- package/dist/local/stores/DatabaseManager.js +400 -0
- package/dist/local/stores/ObjectStore.d.ts +115 -0
- package/dist/local/stores/ObjectStore.js +393 -0
- package/dist/local/stores/ObjectStoreContract.d.ts +38 -0
- package/dist/local/stores/ObjectStoreContract.js +1 -0
- package/dist/local/stores/StoreManager.d.ts +114 -0
- package/dist/local/stores/StoreManager.js +304 -0
- package/dist/local/stores/SyncActionStore.d.ts +99 -0
- package/dist/local/stores/SyncActionStore.js +506 -0
- package/dist/local/stores/openIDBWithTimeout.d.ts +65 -0
- package/dist/local/stores/openIDBWithTimeout.js +153 -0
- package/dist/local/stores/persistenceCleanup.d.ts +7 -0
- package/dist/local/stores/persistenceCleanup.js +26 -0
- package/dist/local/stores/persistenceIdentity.d.ts +27 -0
- package/dist/local/stores/persistenceIdentity.js +38 -0
- package/dist/local/stores/syncAction.d.ts +26 -0
- package/dist/local/stores/syncAction.js +16 -0
- package/dist/local/stores/v1PersistenceDeletion.d.ts +8 -0
- package/dist/local/stores/v1PersistenceDeletion.js +16 -0
- package/dist/local/sync/BootstrapFetcher.d.ts +284 -0
- package/dist/local/sync/BootstrapFetcher.js +964 -0
- package/dist/local/sync/ConnectionManager.d.ts +8 -0
- package/dist/local/sync/ConnectionManager.js +8 -0
- package/dist/local/sync/OnDemandLoader.d.ts +231 -0
- package/dist/local/sync/OnDemandLoader.js +743 -0
- package/dist/local/sync/SubscriptionManager.d.ts +159 -0
- package/dist/local/sync/SubscriptionManager.js +243 -0
- package/dist/local/sync/SyncWebSocket.d.ts +173 -0
- package/dist/local/sync/SyncWebSocket.js +438 -0
- package/dist/local/sync/bootstrapApply.d.ts +73 -0
- package/dist/local/sync/bootstrapApply.js +73 -0
- package/dist/local/sync/commitFrames.d.ts +8 -0
- package/dist/local/sync/commitFrames.js +8 -0
- package/dist/local/sync/connectionManagerLifecycle.d.ts +23 -0
- package/dist/local/sync/connectionManagerLifecycle.js +126 -0
- package/dist/local/sync/contextPorts.d.ts +18 -0
- package/dist/local/sync/contextPorts.js +31 -0
- package/dist/local/sync/createClaimStream.d.ts +64 -0
- package/dist/local/sync/createClaimStream.js +475 -0
- package/dist/local/sync/createSnapshot.d.ts +29 -0
- package/dist/local/sync/createSnapshot.js +116 -0
- package/dist/local/sync/credentialLifecycle.d.ts +7 -0
- package/dist/local/sync/credentialLifecycle.js +7 -0
- package/dist/local/sync/deltaPipeline.d.ts +116 -0
- package/dist/local/sync/deltaPipeline.js +357 -0
- package/dist/local/sync/groupChange.d.ts +116 -0
- package/dist/local/sync/groupChange.js +244 -0
- package/dist/local/sync/initialize.d.ts +27 -0
- package/dist/local/sync/initialize.js +137 -0
- package/dist/local/sync/participants.d.ts +132 -0
- package/dist/local/sync/participants.js +342 -0
- package/dist/local/sync/persistedPrefix.d.ts +12 -0
- package/dist/local/sync/persistedPrefix.js +22 -0
- package/dist/local/sync/reconnect.d.ts +23 -0
- package/dist/local/sync/reconnect.js +55 -0
- package/dist/local/sync/schemaDrift.d.ts +55 -0
- package/dist/local/sync/schemaDrift.js +53 -0
- package/dist/local/sync/schemas.d.ts +71 -0
- package/dist/local/sync/schemas.js +94 -0
- package/dist/local/sync/socketEventWiring.d.ts +31 -0
- package/dist/local/sync/socketEventWiring.js +130 -0
- package/dist/local/sync/syncCursor.d.ts +40 -0
- package/dist/local/sync/syncCursor.js +55 -0
- package/dist/local/sync/syncPlan.d.ts +54 -0
- package/dist/local/sync/syncPlan.js +50 -0
- package/dist/local/sync/terminalSessionLifecycle.d.ts +20 -0
- package/dist/local/sync/terminalSessionLifecycle.js +50 -0
- package/dist/local/sync/wsFrameHandlers.d.ts +8 -0
- package/dist/local/sync/wsFrameHandlers.js +8 -0
- package/dist/local/transactions/databaseCommitOutbox.d.ts +15 -0
- package/dist/local/transactions/databaseCommitOutbox.js +16 -0
- package/dist/local/transactions/localMutation.d.ts +10 -0
- package/dist/local/transactions/localMutation.js +37 -0
- package/dist/local/transactions/mutations/MutationQueue.d.ts +511 -0
- package/dist/local/transactions/mutations/MutationQueue.js +1498 -0
- package/dist/local/transactions/mutations/MutationStore.d.ts +20 -0
- package/dist/local/transactions/mutations/MutationStore.js +53 -0
- package/dist/local/transactions/mutations/UnconfirmedWrites.d.ts +82 -0
- package/dist/local/transactions/mutations/UnconfirmedWrites.js +104 -0
- package/dist/local/transactions/mutations/batchProcessing.d.ts +64 -0
- package/dist/local/transactions/mutations/batchProcessing.js +349 -0
- package/dist/local/transactions/mutations/coalesceRules.d.ts +58 -0
- package/dist/local/transactions/mutations/coalesceRules.js +140 -0
- package/dist/local/transactions/mutations/commitApi.d.ts +19 -0
- package/dist/local/transactions/mutations/commitApi.js +74 -0
- package/dist/local/transactions/mutations/commitLane.d.ts +70 -0
- package/dist/local/transactions/mutations/commitLane.js +140 -0
- package/dist/local/transactions/mutations/commitLatency.d.ts +52 -0
- package/dist/local/transactions/mutations/commitLatency.js +130 -0
- package/dist/local/transactions/mutations/commitPayload.d.ts +165 -0
- package/dist/local/transactions/mutations/commitPayload.js +152 -0
- package/dist/local/transactions/mutations/commitTransport.d.ts +36 -0
- package/dist/local/transactions/mutations/commitTransport.js +104 -0
- package/dist/local/transactions/mutations/deltaConfirmation.d.ts +63 -0
- package/dist/local/transactions/mutations/deltaConfirmation.js +235 -0
- package/dist/local/transactions/mutations/durableCommitRestore.d.ts +17 -0
- package/dist/local/transactions/mutations/durableCommitRestore.js +95 -0
- package/dist/local/transactions/mutations/durableWriteStore.d.ts +14 -0
- package/dist/local/transactions/mutations/durableWriteStore.js +12 -0
- package/dist/local/transactions/mutations/executionSelection.d.ts +6 -0
- package/dist/local/transactions/mutations/executionSelection.js +42 -0
- package/dist/local/transactions/mutations/failureHandling.d.ts +16 -0
- package/dist/local/transactions/mutations/failureHandling.js +130 -0
- package/dist/local/transactions/mutations/failurePolicy.d.ts +13 -0
- package/dist/local/transactions/mutations/failurePolicy.js +48 -0
- package/dist/local/transactions/mutations/localMutation.d.ts +58 -0
- package/dist/local/transactions/mutations/localMutation.js +75 -0
- package/dist/local/transactions/mutations/modelOperations.d.ts +44 -0
- package/dist/local/transactions/mutations/modelOperations.js +144 -0
- package/dist/local/transactions/mutations/mutationPersistence.d.ts +25 -0
- package/dist/local/transactions/mutations/mutationPersistence.js +188 -0
- package/dist/local/transactions/mutations/pendingDrain.d.ts +33 -0
- package/dist/local/transactions/mutations/pendingDrain.js +112 -0
- package/dist/local/transactions/mutations/processingScheduler.d.ts +14 -0
- package/dist/local/transactions/mutations/processingScheduler.js +24 -0
- package/dist/local/transactions/mutations/queueCoalescing.d.ts +13 -0
- package/dist/local/transactions/mutations/queueCoalescing.js +35 -0
- package/dist/local/transactions/mutations/replayValidation.d.ts +187 -0
- package/dist/local/transactions/mutations/replayValidation.js +164 -0
- package/dist/local/transactions/reconnectDrain.d.ts +11 -0
- package/dist/local/transactions/reconnectDrain.js +13 -0
- package/dist/local/utils/mobxSetup.d.ts +53 -0
- package/dist/local/utils/mobxSetup.js +330 -0
- package/dist/local/views/QueryView.d.ts +79 -0
- package/dist/local/views/QueryView.js +218 -0
- package/dist/local/views/ViewRegistry.d.ts +20 -0
- package/dist/local/views/ViewRegistry.js +57 -0
- package/dist/local/views/incrementalView.d.ts +45 -0
- package/dist/local/views/incrementalView.js +69 -0
- package/dist/plugin.d.ts +285 -0
- package/dist/plugin.js +106 -0
- package/dist/presenceStream.d.ts +69 -0
- package/dist/presenceStream.js +200 -0
- package/dist/react/AbloProvider.d.ts +242 -0
- package/dist/react/AbloProvider.js +456 -0
- package/dist/react/ClientSideSuspense.d.ts +36 -0
- package/dist/react/ClientSideSuspense.js +17 -0
- package/dist/react/DefaultFallback.d.ts +24 -0
- package/dist/react/DefaultFallback.js +43 -0
- package/dist/react/context.d.ts +55 -0
- package/dist/react/context.js +29 -0
- package/dist/react/createAbloReact.d.ts +50 -0
- package/dist/react/createAbloReact.js +48 -0
- package/dist/react/internalContext.d.ts +33 -0
- package/dist/react/internalContext.js +3 -0
- package/dist/react/useAblo.d.ts +96 -0
- package/dist/react/useAblo.js +120 -0
- package/dist/react/useCurrentUserId.d.ts +2 -0
- package/dist/react/useCurrentUserId.js +12 -0
- package/dist/react/useErrorListener.d.ts +2 -0
- package/dist/react/useErrorListener.js +14 -0
- package/dist/react/useMutationFailureListener.d.ts +8 -0
- package/dist/react/useMutationFailureListener.js +19 -0
- package/dist/react/useMutators.d.ts +56 -0
- package/dist/react/useMutators.js +84 -0
- package/dist/react/useSyncStatus.d.ts +19 -0
- package/dist/react/useSyncStatus.js +37 -0
- package/dist/react/useUndoScope.d.ts +34 -0
- package/dist/react/useUndoScope.js +73 -0
- package/dist/react.d.ts +18 -0
- package/dist/react.js +14 -0
- package/dist/reactRuntime.d.ts +4 -0
- package/dist/reactRuntime.js +2 -0
- package/dist/surface.d.ts +36 -0
- package/dist/surface.js +77 -0
- package/dist/useReactive.d.ts +6 -0
- package/dist/useReactive.js +43 -0
- package/package.json +119 -0
- package/src/Ablo.ts +456 -0
- package/src/client.ts +374 -0
- package/src/core.ts +104 -0
- package/src/humans.ts +61 -0
- package/src/index.ts +40 -0
- package/src/local/BaseSyncedStore.ts +1991 -0
- package/src/local/Database.ts +2052 -0
- package/src/local/InstanceCache.ts +1503 -0
- package/src/local/LazyReferenceCollection.ts +563 -0
- package/src/local/Model.ts +1124 -0
- package/src/local/ModelRegistry.ts +762 -0
- package/src/local/NetworkMonitor.ts +88 -0
- package/src/local/RuntimeContext.ts +141 -0
- package/src/local/SyncClient.ts +2131 -0
- package/src/local/adapters/alwaysOnline.ts +20 -0
- package/src/local/adapters/inMemoryStorage.ts +141 -0
- package/src/local/client/clientPrelude.ts +112 -0
- package/src/local/client/consoleLogger.ts +60 -0
- package/src/local/client/createInternalComponents.ts +163 -0
- package/src/local/client/createModelProxy.ts +1390 -0
- package/src/local/client/modelRegistration.ts +350 -0
- package/src/local/client/options.ts +526 -0
- package/src/local/client/reactiveEngine.ts +935 -0
- package/src/local/client/resourceTypes.ts +37 -0
- package/src/local/client/schemaConfig.ts +194 -0
- package/src/local/client/storeCluster.ts +172 -0
- package/src/local/client/storeLifecycle.ts +343 -0
- package/src/local/client/validateAbloOptions.ts +95 -0
- package/src/local/client/wsMutationExecutor.ts +110 -0
- package/src/local/context.ts +96 -0
- package/src/local/coordination/ClaimLog.ts +39 -0
- package/src/local/interfaces/index.ts +468 -0
- package/src/local/localModelContract.ts +15 -0
- package/src/local/logPosition.ts +84 -0
- package/src/local/mutationPersistence.ts +7 -0
- package/src/local/mutators/RecordingMutation.ts +222 -0
- package/src/local/mutators/Transaction.ts +97 -0
- package/src/local/mutators/UndoManager.ts +741 -0
- package/src/local/mutators/defineMutators.ts +76 -0
- package/src/local/mutators/inverseOp.ts +83 -0
- package/src/local/mutators/mutateActions.ts +167 -0
- package/src/local/mutators/readerActions.ts +99 -0
- package/src/local/mutators/undoApply.ts +141 -0
- package/src/local/persistence.ts +16 -0
- package/src/local/query/QueryProcessor.ts +347 -0
- package/src/local/query/client.ts +197 -0
- package/src/local/query/types.ts +102 -0
- package/src/local/schema/serialize.ts +1 -0
- package/src/local/store/queryApi.ts +56 -0
- package/src/local/storeContract.ts +146 -0
- package/src/local/stores/DatabaseManager.ts +507 -0
- package/src/local/stores/ObjectStore.ts +449 -0
- package/src/local/stores/ObjectStoreContract.ts +48 -0
- package/src/local/stores/StoreManager.ts +388 -0
- package/src/local/stores/SyncActionStore.ts +579 -0
- package/src/local/stores/openIDBWithTimeout.ts +195 -0
- package/src/local/stores/persistenceCleanup.ts +43 -0
- package/src/local/stores/persistenceIdentity.ts +83 -0
- package/src/local/stores/syncAction.ts +21 -0
- package/src/local/stores/v1PersistenceDeletion.ts +21 -0
- package/src/local/sync/BootstrapFetcher.ts +1224 -0
- package/src/local/sync/ConnectionManager.ts +15 -0
- package/src/local/sync/OnDemandLoader.ts +927 -0
- package/src/local/sync/SubscriptionManager.ts +300 -0
- package/src/local/sync/SyncWebSocket.ts +584 -0
- package/src/local/sync/bootstrapApply.ts +130 -0
- package/src/local/sync/commitFrames.ts +16 -0
- package/src/local/sync/connectionManagerLifecycle.ts +158 -0
- package/src/local/sync/contextPorts.ts +37 -0
- package/src/local/sync/createClaimStream.ts +668 -0
- package/src/local/sync/createSnapshot.ts +160 -0
- package/src/local/sync/credentialLifecycle.ts +18 -0
- package/src/local/sync/deltaPipeline.ts +473 -0
- package/src/local/sync/groupChange.ts +343 -0
- package/src/local/sync/initialize.ts +205 -0
- package/src/local/sync/participants.ts +564 -0
- package/src/local/sync/persistedPrefix.ts +27 -0
- package/src/local/sync/reconnect.ts +88 -0
- package/src/local/sync/schemaDrift.ts +86 -0
- package/src/local/sync/schemas.ts +118 -0
- package/src/local/sync/socketEventWiring.ts +196 -0
- package/src/local/sync/syncCursor.ts +62 -0
- package/src/local/sync/syncPlan.ts +89 -0
- package/src/local/sync/terminalSessionLifecycle.ts +66 -0
- package/src/local/sync/wsFrameHandlers.ts +20 -0
- package/src/local/transactions/databaseCommitOutbox.ts +33 -0
- package/src/local/transactions/localMutation.ts +58 -0
- package/src/local/transactions/mutations/MutationQueue.ts +1998 -0
- package/src/local/transactions/mutations/MutationStore.ts +65 -0
- package/src/local/transactions/mutations/UnconfirmedWrites.ts +133 -0
- package/src/local/transactions/mutations/batchProcessing.ts +469 -0
- package/src/local/transactions/mutations/coalesceRules.ts +192 -0
- package/src/local/transactions/mutations/commitApi.ts +97 -0
- package/src/local/transactions/mutations/commitLane.ts +191 -0
- package/src/local/transactions/mutations/commitLatency.ts +164 -0
- package/src/local/transactions/mutations/commitPayload.ts +281 -0
- package/src/local/transactions/mutations/commitTransport.ts +174 -0
- package/src/local/transactions/mutations/deltaConfirmation.ts +298 -0
- package/src/local/transactions/mutations/durableCommitRestore.ts +128 -0
- package/src/local/transactions/mutations/durableWriteStore.ts +21 -0
- package/src/local/transactions/mutations/executionSelection.ts +42 -0
- package/src/local/transactions/mutations/failureHandling.ts +154 -0
- package/src/local/transactions/mutations/failurePolicy.ts +61 -0
- package/src/local/transactions/mutations/localMutation.ts +135 -0
- package/src/local/transactions/mutations/modelOperations.ts +210 -0
- package/src/local/transactions/mutations/mutationPersistence.ts +231 -0
- package/src/local/transactions/mutations/pendingDrain.ts +160 -0
- package/src/local/transactions/mutations/processingScheduler.ts +35 -0
- package/src/local/transactions/mutations/queueCoalescing.ts +45 -0
- package/src/local/transactions/mutations/replayValidation.ts +192 -0
- package/src/local/transactions/reconnectDrain.ts +24 -0
- package/src/local/utils/mobxSetup.ts +388 -0
- package/src/local/views/QueryView.ts +311 -0
- package/src/local/views/ViewRegistry.ts +61 -0
- package/src/local/views/incrementalView.ts +92 -0
- package/src/plugin.ts +396 -0
- package/src/presenceStream.ts +279 -0
- package/src/react/AbloProvider.tsx +744 -0
- package/src/react/ClientSideSuspense.tsx +57 -0
- package/src/react/DefaultFallback.tsx +60 -0
- package/src/react/context.ts +89 -0
- package/src/react/createAbloReact.ts +116 -0
- package/src/react/internalContext.ts +38 -0
- package/src/react/useAblo.ts +280 -0
- package/src/react/useCurrentUserId.ts +17 -0
- package/src/react/useErrorListener.ts +22 -0
- package/src/react/useMutationFailureListener.ts +34 -0
- package/src/react/useMutators.ts +184 -0
- package/src/react/useSyncStatus.ts +42 -0
- package/src/react/useUndoScope.ts +143 -0
- package/src/react.ts +69 -0
- package/src/reactRuntime.ts +10 -0
- package/src/surface.ts +106 -0
- package/src/useReactive.ts +51 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks whether the confirming delta for a write has arrived. It holds the
|
|
3
|
+
* acknowledgement watermark (through the shared {@link LogPosition}), the
|
|
4
|
+
* per-transaction confirmation timeouts, and the retry-with-backoff and
|
|
5
|
+
* reconciliation policy for transactions in the `awaiting_delta` status. It
|
|
6
|
+
* reaches back to {@link MutationQueue} only through the small
|
|
7
|
+
* {@link DeltaConfirmationContext} interface, not the queue class itself, so it
|
|
8
|
+
* has no cyclic dependency and can be tested on its own.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { globalRuntime } from '../../context.js';
|
|
12
|
+
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
13
|
+
import type { LogPositionPort } from '../../logPosition.js';
|
|
14
|
+
import type { QueuedMutation } from './commitPayload.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The subset of {@link MutationQueue} that the confirmation tracker needs:
|
|
18
|
+
* store lookups and status changes, removing optimistic entries once a write
|
|
19
|
+
* confirms, the queue's event emitter (`transaction:completed`,
|
|
20
|
+
* `reconciliation:needed`, and so on), a connection check (so timeouts
|
|
21
|
+
* re-schedule instead of escalating while offline), and the shared client
|
|
22
|
+
* position (`noteAck` advances the acknowledgement cursor; diagnostics read the
|
|
23
|
+
* applied cursor).
|
|
24
|
+
*/
|
|
25
|
+
export interface DeltaConfirmationContext {
|
|
26
|
+
store: {
|
|
27
|
+
get(id: string): QueuedMutation | undefined;
|
|
28
|
+
getByStatus(status: QueuedMutation['status']): QueuedMutation[];
|
|
29
|
+
updateStatus(id: string, status: QueuedMutation['status']): void;
|
|
30
|
+
};
|
|
31
|
+
optimisticUpdates: { delete(id: string): boolean };
|
|
32
|
+
emit(event: string, payload?: unknown): void;
|
|
33
|
+
isConnected(): boolean;
|
|
34
|
+
position: LogPositionPort;
|
|
35
|
+
/** The owning client's runtime. Defaults to the module-global bridge. */
|
|
36
|
+
runtime?: RuntimeContext;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class DeltaConfirmationTracker {
|
|
40
|
+
// Retry configuration for delta confirmation, using exponential backoff.
|
|
41
|
+
// Maximum retries before requesting a full reconciliation.
|
|
42
|
+
private static readonly DELTA_MAX_RETRIES = 5;
|
|
43
|
+
// Upper bound on the backoff timeout.
|
|
44
|
+
private static readonly DELTA_MAX_TIMEOUT_MS = 120_000;
|
|
45
|
+
|
|
46
|
+
// Pending confirmation timeouts for transactions awaiting their delta. On
|
|
47
|
+
// timeout the tracker retries with backoff rather than rolling back.
|
|
48
|
+
private deltaConfirmationTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
49
|
+
|
|
50
|
+
// Track retry attempts per transaction for exponential backoff
|
|
51
|
+
private deltaConfirmationRetries = new Map<string, number>();
|
|
52
|
+
|
|
53
|
+
private readonly runtime: RuntimeContext;
|
|
54
|
+
|
|
55
|
+
constructor(private readonly ctx: DeltaConfirmationContext) {
|
|
56
|
+
this.runtime = ctx.runtime ?? globalRuntime;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Applied-cursor alias, kept so the read sites below stay legible. */
|
|
60
|
+
private get lastSeenSyncId(): number {
|
|
61
|
+
return this.ctx.position.applied;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private matchesSourceEcho(
|
|
65
|
+
tx: QueuedMutation,
|
|
66
|
+
correlationId: string | undefined,
|
|
67
|
+
): boolean {
|
|
68
|
+
return (
|
|
69
|
+
correlationId !== undefined &&
|
|
70
|
+
tx.correlationId !== undefined &&
|
|
71
|
+
correlationId === tx.correlationId
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
noteAck(lastSyncId: number | undefined): void {
|
|
76
|
+
this.ctx.position.noteAck(lastSyncId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Confirms every awaiting transaction whose sync-id threshold this delta
|
|
81
|
+
* meets or exceeds.
|
|
82
|
+
* @param syncId - The sync id of the received delta.
|
|
83
|
+
*/
|
|
84
|
+
onDeltaReceived(syncId: number, correlationId?: string): void {
|
|
85
|
+
// The cursor advances where the delta is applied (the store calls
|
|
86
|
+
// position.advanceApplied / advancePersisted); this hook only resolves
|
|
87
|
+
// confirmation thresholds against the incoming id.
|
|
88
|
+
|
|
89
|
+
const awaitingTxs = this.ctx.store.getByStatus('awaiting_delta');
|
|
90
|
+
const executingTxs = this.ctx.store.getByStatus('executing');
|
|
91
|
+
|
|
92
|
+
// Debug: Show state when delta arrives
|
|
93
|
+
if (awaitingTxs.length > 0 || executingTxs.length > 0) {
|
|
94
|
+
this.runtime.logger.debug('tx:delta_received', {
|
|
95
|
+
syncId,
|
|
96
|
+
lastSeenSyncId: this.lastSeenSyncId,
|
|
97
|
+
awaitingCount: awaitingTxs.length,
|
|
98
|
+
executingCount: executingTxs.length,
|
|
99
|
+
awaitingThresholds: awaitingTxs.map((tx) => ({
|
|
100
|
+
txId: tx.id.slice(0, 8),
|
|
101
|
+
model: tx.modelName,
|
|
102
|
+
needed: tx.syncIdNeededForCompletion,
|
|
103
|
+
requiresCorrelatedDelta: tx.requiresCorrelatedDelta === true,
|
|
104
|
+
willConfirm:
|
|
105
|
+
tx.requiresCorrelatedDelta === true
|
|
106
|
+
? this.matchesSourceEcho(tx, correlationId)
|
|
107
|
+
: tx.syncIdNeededForCompletion !== undefined &&
|
|
108
|
+
syncId >= tx.syncIdNeededForCompletion,
|
|
109
|
+
})),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Fast path: no awaiting transactions
|
|
114
|
+
if (awaitingTxs.length === 0) return;
|
|
115
|
+
|
|
116
|
+
let confirmedCount = 0;
|
|
117
|
+
|
|
118
|
+
for (const tx of awaitingTxs) {
|
|
119
|
+
// Queued forward receipts deliberately have no watermark. They confirm
|
|
120
|
+
// only when the authoritative source echoes the receipt's opaque,
|
|
121
|
+
// authenticated-scope batch identity; the legacy anomaly path continues
|
|
122
|
+
// to use its sync-id threshold.
|
|
123
|
+
const confirmedByCorrelation =
|
|
124
|
+
tx.requiresCorrelatedDelta === true &&
|
|
125
|
+
this.matchesSourceEcho(tx, correlationId);
|
|
126
|
+
const confirmedByThreshold =
|
|
127
|
+
tx.requiresCorrelatedDelta !== true &&
|
|
128
|
+
tx.syncIdNeededForCompletion !== undefined &&
|
|
129
|
+
syncId >= tx.syncIdNeededForCompletion;
|
|
130
|
+
if (confirmedByCorrelation || confirmedByThreshold) {
|
|
131
|
+
this.cancelDeltaConfirmationTimeout(tx.id);
|
|
132
|
+
this.ctx.store.updateStatus(tx.id, 'completed');
|
|
133
|
+
this.ctx.emit('transaction:completed', tx);
|
|
134
|
+
this.ctx.emit(`transaction:completed:${tx.id}`, tx);
|
|
135
|
+
this.ctx.optimisticUpdates.delete(tx.id);
|
|
136
|
+
confirmedCount++;
|
|
137
|
+
|
|
138
|
+
this.runtime.logger.debug('tx:confirm_via_delta', {
|
|
139
|
+
txId: tx.id.slice(0, 8),
|
|
140
|
+
model: tx.modelName,
|
|
141
|
+
neededSyncId: tx.syncIdNeededForCompletion,
|
|
142
|
+
receivedSyncId: syncId,
|
|
143
|
+
confirmation: confirmedByCorrelation ? 'source_correlation' : 'sync_id',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Log batch summary only if we confirmed something
|
|
149
|
+
if (confirmedCount > 0) {
|
|
150
|
+
// Leave a breadcrumb when transactions confirm.
|
|
151
|
+
this.runtime.observability.breadcrumb('Transactions confirmed via delta', 'sync.transaction', 'info', {
|
|
152
|
+
count: confirmedCount,
|
|
153
|
+
syncId,
|
|
154
|
+
remainingAwaiting: awaitingTxs.length - confirmedCount,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Schedule the confirmation wait for a transaction. On timeout the tracker
|
|
160
|
+
// retries with exponential backoff and requests reconciliation to catch up on
|
|
161
|
+
// missed deltas, rather than rolling back, which would discard state the
|
|
162
|
+
// server has already confirmed. A rollback happens only on an explicit server
|
|
163
|
+
// rejection, never on a timeout.
|
|
164
|
+
scheduleDeltaConfirmationTimeout(tx: QueuedMutation, timeoutMs: number): void {
|
|
165
|
+
// Cancel any existing timeout for this transaction
|
|
166
|
+
this.cancelDeltaConfirmationTimeout(tx.id);
|
|
167
|
+
|
|
168
|
+
// Deliberately not an async callback: the body is fully synchronous, and
|
|
169
|
+
// `setTimeout(async …)` would turn any throw into an unhandled promise
|
|
170
|
+
// rejection instead of a catchable synchronous error.
|
|
171
|
+
const timeoutHandle = setTimeout(() => {
|
|
172
|
+
const currentTx = this.ctx.store.get(tx.id);
|
|
173
|
+
if (currentTx?.status !== 'awaiting_delta') {
|
|
174
|
+
this.deltaConfirmationRetries.delete(tx.id);
|
|
175
|
+
return; // Already confirmed or failed
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// If disconnected, re-schedule with same timeout (no backoff while offline)
|
|
179
|
+
if (!this.ctx.isConnected()) {
|
|
180
|
+
// Self-healing: re-schedule the confirmation wait while offline, no
|
|
181
|
+
// consumer action needed → debug.
|
|
182
|
+
this.runtime.logger.debug('[MutationQueue] Timeout fired while disconnected - re-scheduling', {
|
|
183
|
+
txId: tx.id.slice(0, 8),
|
|
184
|
+
model: tx.modelName,
|
|
185
|
+
});
|
|
186
|
+
this.deltaConfirmationTimeouts.delete(tx.id);
|
|
187
|
+
this.scheduleDeltaConfirmationTimeout(tx, timeoutMs);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const retryCount = this.deltaConfirmationRetries.get(tx.id) ?? 0;
|
|
192
|
+
|
|
193
|
+
this.runtime.observability.captureReconciliation({
|
|
194
|
+
reason: 'delta_timeout',
|
|
195
|
+
model: tx.modelName,
|
|
196
|
+
modelId: tx.modelId,
|
|
197
|
+
syncIdNeeded: currentTx.syncIdNeededForCompletion,
|
|
198
|
+
lastSeenSyncId: this.lastSeenSyncId,
|
|
199
|
+
retryCount,
|
|
200
|
+
connectionState: this.ctx.isConnected() ? 'connected' : 'disconnected',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (retryCount < DeltaConfirmationTracker.DELTA_MAX_RETRIES) {
|
|
204
|
+
// Retry: request reconciliation and re-schedule with exponential
|
|
205
|
+
// backoff. The server has already committed the mutation; only the
|
|
206
|
+
// delta is outstanding.
|
|
207
|
+
this.deltaConfirmationRetries.set(tx.id, retryCount + 1);
|
|
208
|
+
this.deltaConfirmationTimeouts.delete(tx.id);
|
|
209
|
+
|
|
210
|
+
// Exponential backoff: 30s → 60s → 120s → 120s → 120s (capped)
|
|
211
|
+
const nextTimeout = Math.min(timeoutMs * 2, DeltaConfirmationTracker.DELTA_MAX_TIMEOUT_MS);
|
|
212
|
+
|
|
213
|
+
// Request reconciliation so the client can cycle the connection and
|
|
214
|
+
// catch up on missed deltas from the server.
|
|
215
|
+
this.ctx.emit('reconciliation:needed', {
|
|
216
|
+
reason: 'delta_confirmation_timeout',
|
|
217
|
+
txId: tx.id,
|
|
218
|
+
model: tx.modelName,
|
|
219
|
+
modelId: tx.modelId,
|
|
220
|
+
syncIdNeeded: currentTx.syncIdNeededForCompletion,
|
|
221
|
+
lastSeenSyncId: this.lastSeenSyncId,
|
|
222
|
+
retryCount: retryCount + 1,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Self-healing retry with backoff — the server already committed; we're
|
|
226
|
+
// just waiting on the delta. No consumer action → debug.
|
|
227
|
+
this.runtime.logger.debug('[MutationQueue] Re-scheduling with backoff', {
|
|
228
|
+
txId: tx.id.slice(0, 8),
|
|
229
|
+
model: tx.modelName,
|
|
230
|
+
nextTimeoutMs: nextTimeout,
|
|
231
|
+
retry: retryCount + 1,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
this.scheduleDeltaConfirmationTimeout(tx, nextTimeout);
|
|
235
|
+
} else {
|
|
236
|
+
// Retries exhausted: persist the awaiting state instead of rolling back.
|
|
237
|
+
// The commit succeeded on the server, so the data exists there. Saving
|
|
238
|
+
// the awaiting state lets it survive the page closing; on the next
|
|
239
|
+
// session, reconnecting and catching up on deltas will confirm it.
|
|
240
|
+
this.deltaConfirmationRetries.delete(tx.id);
|
|
241
|
+
this.deltaConfirmationTimeouts.delete(tx.id);
|
|
242
|
+
|
|
243
|
+
this.runtime.observability.captureDeltaRetryExhausted({
|
|
244
|
+
txId: tx.id,
|
|
245
|
+
model: tx.modelName,
|
|
246
|
+
modelId: tx.modelId,
|
|
247
|
+
retryCount: DeltaConfirmationTracker.DELTA_MAX_RETRIES,
|
|
248
|
+
syncIdNeeded: currentTx.syncIdNeededForCompletion,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Emit the persist event; the client performs the write to local storage.
|
|
252
|
+
this.ctx.emit('transaction:persist_awaiting', {
|
|
253
|
+
txId: tx.id,
|
|
254
|
+
model: tx.modelName,
|
|
255
|
+
modelId: tx.modelId,
|
|
256
|
+
operationType: tx.type,
|
|
257
|
+
syncIdNeeded: currentTx.syncIdNeededForCompletion,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Also request one final reconciliation cycle
|
|
261
|
+
this.ctx.emit('reconciliation:needed', {
|
|
262
|
+
reason: 'delta_retries_exhausted',
|
|
263
|
+
txId: tx.id,
|
|
264
|
+
model: tx.modelName,
|
|
265
|
+
modelId: tx.modelId,
|
|
266
|
+
syncIdNeeded: currentTx.syncIdNeededForCompletion,
|
|
267
|
+
lastSeenSyncId: this.lastSeenSyncId,
|
|
268
|
+
retryCount: DeltaConfirmationTracker.DELTA_MAX_RETRIES,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}, timeoutMs);
|
|
272
|
+
|
|
273
|
+
this.deltaConfirmationTimeouts.set(tx.id, timeoutHandle);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Cancel a pending delta confirmation timeout and clean up retry tracking
|
|
277
|
+
private cancelDeltaConfirmationTimeout(id: string): void {
|
|
278
|
+
const timeoutHandle = this.deltaConfirmationTimeouts.get(id);
|
|
279
|
+
if (timeoutHandle) {
|
|
280
|
+
clearTimeout(timeoutHandle);
|
|
281
|
+
this.deltaConfirmationTimeouts.delete(id);
|
|
282
|
+
}
|
|
283
|
+
this.deltaConfirmationRetries.delete(id);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Clears every armed confirmation timer, one per in-flight transaction.
|
|
288
|
+
* {@link MutationQueue.dispose} calls this; without it a disposed queue
|
|
289
|
+
* would keep the process alive and fire callbacks against a cleared store.
|
|
290
|
+
*/
|
|
291
|
+
dispose(): void {
|
|
292
|
+
for (const timeoutHandle of this.deltaConfirmationTimeouts.values()) {
|
|
293
|
+
clearTimeout(timeoutHandle);
|
|
294
|
+
}
|
|
295
|
+
this.deltaConfirmationTimeouts.clear();
|
|
296
|
+
this.deltaConfirmationRetries.clear();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { AbloIdempotencyError, AbloValidationError } from '@abloatai/transaction/errors';
|
|
2
|
+
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
3
|
+
import type { CommitTransaction } from './commitLane.js';
|
|
4
|
+
import type { DurableCommitEnvelope, CommitOutboxScope } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
|
|
5
|
+
import type { DurableWriteStore } from './durableWriteStore.js';
|
|
6
|
+
import { durableCommitEnvelopeSchema } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
|
|
7
|
+
|
|
8
|
+
export interface DurableCommitRestoreContext {
|
|
9
|
+
readonly config: { enablePersistence: boolean };
|
|
10
|
+
readonly commitOutbox: DurableWriteStore | null;
|
|
11
|
+
readonly commitOutboxScope: CommitOutboxScope | null;
|
|
12
|
+
readonly commitStore: Map<string, CommitTransaction>;
|
|
13
|
+
readonly commitLane: CommitTransaction[];
|
|
14
|
+
readonly runtime: RuntimeContext;
|
|
15
|
+
readonly processCommitLane: () => Promise<void>;
|
|
16
|
+
readonly durableReplayWindowMs: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function restoreDurableCommits(ctx: DurableCommitRestoreContext): Promise<Set<string>> {
|
|
20
|
+
if (!ctx.config.enablePersistence) return new Set();
|
|
21
|
+
|
|
22
|
+
const sourceMutationIds = new Set<string>();
|
|
23
|
+
try {
|
|
24
|
+
if (!ctx.commitOutbox) return sourceMutationIds;
|
|
25
|
+
const rows = await ctx.commitOutbox.list();
|
|
26
|
+
const envelopes: DurableCommitEnvelope[] = [];
|
|
27
|
+
for (const row of rows) {
|
|
28
|
+
if (
|
|
29
|
+
typeof row !== 'object' ||
|
|
30
|
+
row === null ||
|
|
31
|
+
(row as { type?: unknown }).type !== 'commit_envelope'
|
|
32
|
+
) continue;
|
|
33
|
+
const parsed = durableCommitEnvelopeSchema.safeParse(row);
|
|
34
|
+
if (parsed.success) {
|
|
35
|
+
envelopes.push(parsed.data);
|
|
36
|
+
} else {
|
|
37
|
+
ctx.runtime.logger.warn('A saved local write is unreadable and was held for review.');
|
|
38
|
+
ctx.runtime.observability.captureMutationFailure({
|
|
39
|
+
context: 'restore-commit-envelope',
|
|
40
|
+
error: parsed.error,
|
|
41
|
+
});
|
|
42
|
+
throw new AbloValidationError(
|
|
43
|
+
'A saved commit envelope is unreadable; replay stopped before newer writes were sent.',
|
|
44
|
+
{ code: 'write_options_invalid', cause: parsed.error },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
envelopes.sort(
|
|
49
|
+
(a, b) =>
|
|
50
|
+
(a.sequence ?? a.sealedAt * 1_000) -
|
|
51
|
+
(b.sequence ?? b.sealedAt * 1_000) ||
|
|
52
|
+
a.id.localeCompare(b.id),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
for (const envelope of envelopes) {
|
|
56
|
+
for (const mutationId of envelope.sourceMutationIds) {
|
|
57
|
+
sourceMutationIds.add(mutationId);
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
envelope.acceptedAt === undefined &&
|
|
61
|
+
Date.now() - envelope.sealedAt >=
|
|
62
|
+
ctx.durableReplayWindowMs
|
|
63
|
+
) {
|
|
64
|
+
ctx.runtime.logger.warn(
|
|
65
|
+
'A saved local write is too old to retry safely and was held for review.',
|
|
66
|
+
);
|
|
67
|
+
ctx.runtime.observability.captureMutationFailure({
|
|
68
|
+
context: 'quarantine-expired-commit-envelope',
|
|
69
|
+
error: `Envelope ${envelope.idempotencyKey} is too old to replay safely`,
|
|
70
|
+
});
|
|
71
|
+
throw new AbloIdempotencyError(
|
|
72
|
+
'A saved commit is older than the server idempotency window and cannot be replayed safely.',
|
|
73
|
+
{ code: 'idempotency_conflict' },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (
|
|
77
|
+
ctx.commitOutboxScope &&
|
|
78
|
+
(
|
|
79
|
+
!envelope.scope || // eslint-disable-line @typescript-eslint/prefer-optional-chain -- missing scope must quarantine
|
|
80
|
+
envelope.scope.organizationId !== ctx.commitOutboxScope.organizationId ||
|
|
81
|
+
envelope.scope.participantId !== ctx.commitOutboxScope.participantId ||
|
|
82
|
+
envelope.scope.namespace !== ctx.commitOutboxScope.namespace
|
|
83
|
+
)
|
|
84
|
+
) {
|
|
85
|
+
ctx.runtime.logger.warn(
|
|
86
|
+
'A saved local write belongs to a different account or server and was held for review.',
|
|
87
|
+
);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ctx.commitStore.has(envelope.idempotencyKey)) continue;
|
|
91
|
+
const transaction: CommitTransaction = {
|
|
92
|
+
id: envelope.idempotencyKey,
|
|
93
|
+
kind: 'commit',
|
|
94
|
+
operations: envelope.operations.map((operation) => ({ ...operation })),
|
|
95
|
+
...(envelope.commitOptions.reads
|
|
96
|
+
? { reads: [...envelope.commitOptions.reads] }
|
|
97
|
+
: {}),
|
|
98
|
+
...(envelope.commitOptions.track
|
|
99
|
+
? { track: [...envelope.commitOptions.track] }
|
|
100
|
+
: {}),
|
|
101
|
+
status: 'pending',
|
|
102
|
+
createdAt: envelope.createdAt,
|
|
103
|
+
sealedAt: envelope.sealedAt,
|
|
104
|
+
sequence: envelope.sequence ?? envelope.sealedAt * 1_000,
|
|
105
|
+
attempts: 0,
|
|
106
|
+
...(envelope.correlationId
|
|
107
|
+
? { correlationId: envelope.correlationId }
|
|
108
|
+
: {}),
|
|
109
|
+
sourceMutationIds: [...envelope.sourceMutationIds],
|
|
110
|
+
durableEnvelope: envelope,
|
|
111
|
+
};
|
|
112
|
+
ctx.commitStore.set(transaction.id, transaction);
|
|
113
|
+
ctx.commitLane.push(transaction);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (ctx.commitLane.length > 0) void ctx.processCommitLane();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
ctx.runtime.logger.debug('[MutationQueue] Failed to restore durable writes', {
|
|
119
|
+
error: error instanceof Error ? error.message : String(error),
|
|
120
|
+
});
|
|
121
|
+
ctx.runtime.observability.captureMutationFailure({
|
|
122
|
+
context: 'restore-commit-envelopes',
|
|
123
|
+
error: error instanceof Error ? error : String(error),
|
|
124
|
+
});
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
return sourceMutationIds;
|
|
128
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The durable-write port moved to the settlement core (ADR 0016): it is a
|
|
3
|
+
* contract over commit envelopes and holds no local rows. Re-exported here so
|
|
4
|
+
* the existing `transactions/mutations/durableWriteStore` import path keeps
|
|
5
|
+
* resolving for the queue, the outbox, and the client options.
|
|
6
|
+
*
|
|
7
|
+
* The port and its config live in the core's `durableWrites` module (a behavior
|
|
8
|
+
* contract, not a persisted shape); the records that cross it are owned by
|
|
9
|
+
* `transactions/settlement/pendingWrite`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
durableWriteStoreSchema,
|
|
14
|
+
durableWritesConfigSchema,
|
|
15
|
+
} from '@abloatai/transaction/durableWrites';
|
|
16
|
+
export type {
|
|
17
|
+
DurableWriteStore,
|
|
18
|
+
DurableWritesConfig,
|
|
19
|
+
} from '@abloatai/transaction/durableWrites';
|
|
20
|
+
export { pendingWriteSchema } from '@abloatai/transaction/transactions/settlement/pendingWrite';
|
|
21
|
+
export type { PendingWrite } from '@abloatai/transaction/transactions/settlement/pendingWrite';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { QueuedMutation } from './commitPayload.js';
|
|
2
|
+
|
|
3
|
+
export function takeNextExecutionBatch(
|
|
4
|
+
executionQueue: QueuedMutation[],
|
|
5
|
+
maxBatchSize: number,
|
|
6
|
+
): { batch: QueuedMutation[]; remaining: QueuedMutation[] } {
|
|
7
|
+
const retryGroups = new Map<string, Map<string, QueuedMutation>>();
|
|
8
|
+
for (const tx of executionQueue) {
|
|
9
|
+
const envelope = tx.commitEnvelope;
|
|
10
|
+
if (!envelope) continue;
|
|
11
|
+
const group = retryGroups.get(envelope.idempotencyKey) ?? new Map<string, QueuedMutation>();
|
|
12
|
+
group.set(tx.id, tx);
|
|
13
|
+
retryGroups.set(envelope.idempotencyKey, group);
|
|
14
|
+
}
|
|
15
|
+
for (const [idempotencyKey, byId] of retryGroups) {
|
|
16
|
+
const members = [...byId.values()];
|
|
17
|
+
const expectedCount = members[0]?.commitEnvelope?.operationCount;
|
|
18
|
+
if (expectedCount === undefined || members.length !== expectedCount) continue;
|
|
19
|
+
const remaining = executionQueue.filter((tx) => tx.commitEnvelope?.idempotencyKey !== idempotencyKey);
|
|
20
|
+
members.sort((a, b) => (a.commitEnvelope?.operationIndex ?? 0) - (b.commitEnvelope?.operationIndex ?? 0));
|
|
21
|
+
return { batch: members, remaining };
|
|
22
|
+
}
|
|
23
|
+
const fresh = executionQueue.filter((tx) => !tx.commitEnvelope);
|
|
24
|
+
const firstFresh = fresh[0];
|
|
25
|
+
if (!firstFresh) return { batch: [], remaining: executionQueue };
|
|
26
|
+
const explicitIndex = fresh.findIndex((tx) => typeof tx.writeOptions?.idempotencyKey === 'string');
|
|
27
|
+
const selected = explicitIndex === 0
|
|
28
|
+
? [firstFresh]
|
|
29
|
+
: fresh.slice(0, Math.min(maxBatchSize, explicitIndex > 0 ? explicitIndex : fresh.length));
|
|
30
|
+
const selectedIds = new Set(selected.map((tx) => tx.id));
|
|
31
|
+
return { batch: selected, remaining: executionQueue.filter((tx) => !selectedIds.has(tx.id)) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function takePendingDrainBatch(pending: QueuedMutation[], maxBatchSize: number): QueuedMutation[] {
|
|
35
|
+
const first = pending[0];
|
|
36
|
+
if (!first) return [];
|
|
37
|
+
const envelope = first.commitEnvelope;
|
|
38
|
+
if (envelope) return pending.filter((tx) => tx.commitEnvelope?.idempotencyKey === envelope.idempotencyKey);
|
|
39
|
+
if (typeof first.writeOptions?.idempotencyKey === 'string') return [first];
|
|
40
|
+
const explicitIndex = pending.findIndex((tx) => typeof tx.writeOptions?.idempotencyKey === 'string');
|
|
41
|
+
return pending.slice(0, Math.min(maxBatchSize, explicitIndex > 0 ? explicitIndex : pending.length));
|
|
42
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
2
|
+
import type { MutationQueueConfig } from './MutationQueue.js';
|
|
3
|
+
import type { QueuedMutation } from './commitPayload.js';
|
|
4
|
+
import type { MutationStore } from './MutationStore.js';
|
|
5
|
+
import { AbloError } from '@abloatai/transaction/errors';
|
|
6
|
+
import { extractStatusCode } from './commitPayload.js';
|
|
7
|
+
|
|
8
|
+
export interface FailureHandlingContext {
|
|
9
|
+
readonly runtime: RuntimeContext;
|
|
10
|
+
readonly config: Pick<MutationQueueConfig, 'enableOptimistic' | 'maxRetries' | 'retryBackoff'>;
|
|
11
|
+
readonly store: MutationStore;
|
|
12
|
+
readonly isPermanentError: (error: Error) => boolean;
|
|
13
|
+
readonly rollbackOptimistic: (transaction: QueuedMutation, reason: string, error?: Error) => Promise<void>;
|
|
14
|
+
readonly enqueue: (transaction: QueuedMutation) => void;
|
|
15
|
+
readonly getLastPermanentErrorSignature: () => string | undefined;
|
|
16
|
+
readonly setLastPermanentErrorSignature: (signature: string) => void;
|
|
17
|
+
readonly emit: (event: string, payload: object) => boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function handleFailure(ctx: FailureHandlingContext, transaction: QueuedMutation, error: Error): Promise<void> {
|
|
21
|
+
transaction.attempts++;
|
|
22
|
+
|
|
23
|
+
// Check whether this is a permanent error that should not be retried.
|
|
24
|
+
if (ctx.isPermanentError(error)) {
|
|
25
|
+
// Logged at warn: a permanent error means the server rejected the write,
|
|
26
|
+
// so the developer should see the reason in the console. The typed
|
|
27
|
+
// AbloError fields (`type`, `code`, `httpStatus`) are included so the
|
|
28
|
+
// cause is visible — for example a foreign-key violation
|
|
29
|
+
// (AbloValidationError) versus expired authentication
|
|
30
|
+
// (AbloAuthenticationError).
|
|
31
|
+
try {
|
|
32
|
+
const abloErr = error instanceof AbloError ? error : undefined;
|
|
33
|
+
const details = {
|
|
34
|
+
txId: transaction.id.slice(0, 8),
|
|
35
|
+
type: transaction.type,
|
|
36
|
+
model: transaction.modelName,
|
|
37
|
+
modelId: transaction.modelId.slice(0, 12),
|
|
38
|
+
errorType: abloErr?.type ?? error?.name,
|
|
39
|
+
errorCode: abloErr?.code,
|
|
40
|
+
httpStatus: abloErr?.httpStatus,
|
|
41
|
+
requestId: abloErr?.requestId,
|
|
42
|
+
message: error?.message,
|
|
43
|
+
inputKeys: transaction.data ? Object.keys(transaction.data) : undefined,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// A `create` whose id already exists is the benign idempotency case:
|
|
47
|
+
// "this row is already there." It's the least alarming permanent
|
|
48
|
+
// error, so it doesn't warrant a `warn` — `info` keeps it visible
|
|
49
|
+
// without crying wolf. Everything else (FK violation, auth expiry,
|
|
50
|
+
// server 500) stays at `warn`.
|
|
51
|
+
const isBenignIdempotent =
|
|
52
|
+
transaction.type === 'create' &&
|
|
53
|
+
(abloErr?.code === 'unique_violation' ||
|
|
54
|
+
abloErr?.type === 'AbloIdempotencyError');
|
|
55
|
+
|
|
56
|
+
// Demote exact repeats (same write rejected for the same reason on
|
|
57
|
+
// each reconnect replay) to `debug` so the loop logs once.
|
|
58
|
+
const sig = `${details.type}:${details.model}:${details.modelId}:${details.errorCode ?? details.errorType}`;
|
|
59
|
+
const isRepeat = sig === ctx.getLastPermanentErrorSignature();
|
|
60
|
+
ctx.setLastPermanentErrorSignature(sig);
|
|
61
|
+
|
|
62
|
+
const logger = ctx.runtime.logger;
|
|
63
|
+
|
|
64
|
+
// Two registers from one call site, split by log level (the default
|
|
65
|
+
// logger is gated at `warn`, so `debug` stays hidden unless
|
|
66
|
+
// ABLO_LOG_LEVEL=debug is set to inspect the engine):
|
|
67
|
+
// - the default-visible line speaks the application developer's
|
|
68
|
+
// language: their verb (such as `update`), their model, the typed
|
|
69
|
+
// error's own message, and the wire `code` for searching. It uses
|
|
70
|
+
// no engine jargon and prints no JSON dump, which would alarm
|
|
71
|
+
// without helping.
|
|
72
|
+
// - the forensic `details` ride a companion `debug` line for anyone
|
|
73
|
+
// debugging the engine internals.
|
|
74
|
+
const revertNote = ctx.config.enableOptimistic
|
|
75
|
+
? ' The local change was reverted.'
|
|
76
|
+
: '';
|
|
77
|
+
const reason = abloErr?.message ? ` — ${abloErr.message}` : '';
|
|
78
|
+
const code = abloErr?.code ? ` (code: ${abloErr.code})` : '';
|
|
79
|
+
// An optimistic write resolves before the server answers, so a later
|
|
80
|
+
// rejection has no caller left to return to and this log is the only
|
|
81
|
+
// place it appears. That reads to an application developer as their own
|
|
82
|
+
// save silently failing — the write showed, then vanished — and sends
|
|
83
|
+
// them into their editor instead of here. Name the subscription that
|
|
84
|
+
// hands them the same typed error, so the application can say what
|
|
85
|
+
// happened rather than only the console.
|
|
86
|
+
const channelNote = ctx.config.enableOptimistic
|
|
87
|
+
? ' To surface this in your app, subscribe with `ablo.onMutationFailure(…)`.'
|
|
88
|
+
: '';
|
|
89
|
+
const headline = `Your ${transaction.type} to "${transaction.modelName}" was not saved${reason}${code}.${revertNote}${channelNote}`;
|
|
90
|
+
|
|
91
|
+
if (isRepeat) {
|
|
92
|
+
// Same write rejected for the same reason on each reconnect replay —
|
|
93
|
+
// log the forensics once, stay quiet after.
|
|
94
|
+
logger.debug('write rejected again (same reason)', details);
|
|
95
|
+
} else if (isBenignIdempotent) {
|
|
96
|
+
// Already-exists on a `create` is expected on replay, not a problem.
|
|
97
|
+
logger.info(`Your ${transaction.type} to "${transaction.modelName}" was skipped — this row already exists.`);
|
|
98
|
+
logger.debug('idempotent skip — details', details);
|
|
99
|
+
} else {
|
|
100
|
+
logger.warn(headline);
|
|
101
|
+
logger.debug('write rejection — details', details);
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
|
|
105
|
+
// Mark as failed immediately and rollback
|
|
106
|
+
ctx.store.updateStatus(transaction.id, 'failed');
|
|
107
|
+
|
|
108
|
+
if (ctx.config.enableOptimistic) {
|
|
109
|
+
await ctx.rollbackOptimistic(transaction, 'permanent_error', error);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
ctx.emit('transaction:failed', { transaction, error, permanent: true });
|
|
113
|
+
// The id-suffixed event is what `waitForConfirmation` (the
|
|
114
|
+
// `wait:'confirmed'` path) listens on — without it a permanently
|
|
115
|
+
// rejected write left the caller's promise hanging forever.
|
|
116
|
+
ctx.emit(`transaction:failed:${transaction.id}`, { error });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (transaction.attempts < ctx.config.maxRetries) {
|
|
121
|
+
// Exponential backoff with full jitter on every transient retry:
|
|
122
|
+
// `sleep = random(0, min(cap, base * 2^attempt))`. Throttling responses
|
|
123
|
+
// (429/503) use a longer base than other transient errors. The re-enqueue
|
|
124
|
+
// is scheduled rather than awaited, so one backing-off transaction cannot
|
|
125
|
+
// stall unrelated commits.
|
|
126
|
+
const { baseMs, capMs } = ctx.config.retryBackoff;
|
|
127
|
+
let base = baseMs;
|
|
128
|
+
try {
|
|
129
|
+
const status = extractStatusCode(error);
|
|
130
|
+
if (status === 429 || status === 503) base = Math.max(baseMs, 1_000);
|
|
131
|
+
} catch {}
|
|
132
|
+
const ceiling = Math.min(capMs, base * Math.pow(2, transaction.attempts - 1));
|
|
133
|
+
const delay = Math.floor(Math.random() * ceiling);
|
|
134
|
+
|
|
135
|
+
ctx.store.updateStatus(transaction.id, 'pending');
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
// The queue may have shut down or the tx may have been settled
|
|
138
|
+
// (e.g. delta-confirmed) while we backed off.
|
|
139
|
+
if (ctx.store.get(transaction.id)?.status !== 'pending') return;
|
|
140
|
+
ctx.enqueue(transaction);
|
|
141
|
+
}, delay);
|
|
142
|
+
} else {
|
|
143
|
+
// Mark as failed and rollback
|
|
144
|
+
ctx.store.updateStatus(transaction.id, 'failed');
|
|
145
|
+
|
|
146
|
+
if (ctx.config.enableOptimistic) {
|
|
147
|
+
await ctx.rollbackOptimistic(transaction, 'max_retries_exhausted', error);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
ctx.emit('transaction:failed', { transaction, error });
|
|
151
|
+
// Settle `waitForConfirmation` waiters (see the permanent branch above).
|
|
152
|
+
ctx.emit(`transaction:failed:${transaction.id}`, { error });
|
|
153
|
+
}
|
|
154
|
+
}
|