@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,349 @@
|
|
|
1
|
+
import { AbloError, AbloNotFoundError } from '@abloatai/transaction/errors';
|
|
2
|
+
import { applyWriteOptions, normalizeModelKey, TX_TYPE_TO_MUTATION_OP } from './commitPayload.js';
|
|
3
|
+
export async function processBatch(ctx) {
|
|
4
|
+
if (ctx.durableReplayBlock)
|
|
5
|
+
return;
|
|
6
|
+
const batchStart = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
7
|
+
if (ctx.isProcessing || ctx.executionQueue.length === 0) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
ctx.setIsProcessing(true);
|
|
11
|
+
// Declare batch outside try so it's accessible in finally for backpressure tracking
|
|
12
|
+
let batch = [];
|
|
13
|
+
await ctx.runtime.observability.startSpanAsync('sync.batch', 'sync.transaction.batch', async () => {
|
|
14
|
+
try {
|
|
15
|
+
// Sort the execution queue by foreign-key priority before selecting a
|
|
16
|
+
// batch, so a parent row is always committed before its children, even
|
|
17
|
+
// across batch boundaries.
|
|
18
|
+
ctx.executionQueue.sort((a, b) => {
|
|
19
|
+
// Ensure derived fields exist (covers restored/persisted transactions)
|
|
20
|
+
ctx.ensureDerivedFields(a);
|
|
21
|
+
ctx.ensureDerivedFields(b);
|
|
22
|
+
if (a.modelName === b.modelName && a.modelId === b.modelId && a.type !== b.type) {
|
|
23
|
+
if (a.type === 'create')
|
|
24
|
+
return -1;
|
|
25
|
+
if (b.type === 'create')
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
return a.priorityScore - b.priorityScore;
|
|
29
|
+
});
|
|
30
|
+
// Take a fresh batch or one complete retry envelope. Retry envelopes
|
|
31
|
+
// retain both their original membership and operation order.
|
|
32
|
+
batch = ctx.takeNextExecutionBatch();
|
|
33
|
+
if (batch.length === 0)
|
|
34
|
+
return;
|
|
35
|
+
const commitIdempotencyKey = ctx.ensureCommitEnvelope(batch);
|
|
36
|
+
// Track executing count for backpressure
|
|
37
|
+
ctx.setExecutingCount(ctx.executingCount + batch.length);
|
|
38
|
+
// Mark all as executing
|
|
39
|
+
for (const tx of batch) {
|
|
40
|
+
const key = `${tx.modelName}:${tx.modelId}`;
|
|
41
|
+
if (tx.type === 'update')
|
|
42
|
+
ctx.inFlightByModel.add(key);
|
|
43
|
+
ctx.store.updateStatus(tx.id, 'executing');
|
|
44
|
+
}
|
|
45
|
+
// Build every operation for one unified commit (a single round trip).
|
|
46
|
+
const batchOps = [];
|
|
47
|
+
for (const tx of batch) {
|
|
48
|
+
// The per-operation `transactionId` carries the local transaction id
|
|
49
|
+
// over the wire so the server can stamp it on the resulting sync
|
|
50
|
+
// delta. The receive path matches it through
|
|
51
|
+
// {@link UnconfirmedWrites.consumeEcho} to avoid applying the
|
|
52
|
+
// client's own optimistic change twice. This is separate from the
|
|
53
|
+
// batch-level idempotency key recorded in `mutation_log`.
|
|
54
|
+
const op = applyWriteOptions({
|
|
55
|
+
type: TX_TYPE_TO_MUTATION_OP[tx.type],
|
|
56
|
+
model: tx.modelKey,
|
|
57
|
+
id: tx.modelId,
|
|
58
|
+
input: tx.type === 'create' || tx.type === 'update' ? tx.data || {} : undefined,
|
|
59
|
+
transactionId: tx.id,
|
|
60
|
+
}, tx);
|
|
61
|
+
batchOps.push({ tx, op });
|
|
62
|
+
}
|
|
63
|
+
// Execute the unified commit for every operation in one round trip.
|
|
64
|
+
if (batchOps.length > 0) {
|
|
65
|
+
let dispatchStarted = false;
|
|
66
|
+
try {
|
|
67
|
+
const durableEnvelope = await ctx.sealDurableCommit({
|
|
68
|
+
idempotencyKey: commitIdempotencyKey,
|
|
69
|
+
origin: 'model_batch',
|
|
70
|
+
operations: batchOps.map(({ op }) => op),
|
|
71
|
+
sourceMutationIds: ctx.sourceMutationIdsFor(batch),
|
|
72
|
+
createdAt: Math.min(...batch.map((transaction) => transaction.createdAt)),
|
|
73
|
+
sealedAt: batch[0]?.commitEnvelope?.sealedAt ?? Date.now(),
|
|
74
|
+
sequence: batch[0]?.commitEnvelope?.sequence,
|
|
75
|
+
});
|
|
76
|
+
const operations = durableEnvelope.operations;
|
|
77
|
+
// Capture lastSyncId from the server response for threshold-based
|
|
78
|
+
// confirmation.
|
|
79
|
+
//
|
|
80
|
+
// The queue owns request identity. A lost acknowledgement may be
|
|
81
|
+
// retried after a backoff or reconnect, so every retry must send
|
|
82
|
+
// the exact key assigned before the first transport attempt.
|
|
83
|
+
ctx.assertEnvelopeInsideReplayWindow(durableEnvelope);
|
|
84
|
+
dispatchStarted = true;
|
|
85
|
+
const result = ctx.parseMutationCommitResult(await ctx.dispatchCommitBounded(operations, {
|
|
86
|
+
idempotencyKey: commitIdempotencyKey,
|
|
87
|
+
}));
|
|
88
|
+
await ctx.persistDurableCommitAcceptance(durableEnvelope, result);
|
|
89
|
+
const lastSyncId = result.lastSyncId;
|
|
90
|
+
const notifications = result.notifications;
|
|
91
|
+
const { holdsEntireBatch, heldTargets, notificationsByTarget, } = ctx.classifyReceiptNotifications(batchOps.map(({ op }) => ({ model: op.model, id: op.id })), notifications ?? []);
|
|
92
|
+
for (const { tx } of batchOps) {
|
|
93
|
+
const txTarget = ctx.receiptTargetKey(tx.modelKey, tx.modelId);
|
|
94
|
+
const txNotifs = holdsEntireBatch
|
|
95
|
+
? notifications
|
|
96
|
+
: notificationsByTarget.get(txTarget);
|
|
97
|
+
if (txNotifs && txNotifs.length > 0) {
|
|
98
|
+
ctx.commitNotifications.set(tx.id, txNotifs);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const missingIds = new Set(result.missingIds ?? []);
|
|
102
|
+
const settlingBatchOps = [];
|
|
103
|
+
for (const entry of batchOps) {
|
|
104
|
+
const { tx } = entry;
|
|
105
|
+
if (missingIds.has(tx.modelId)) {
|
|
106
|
+
await ctx.handleFailure(tx, new AbloNotFoundError(`${tx.modelName}/${tx.modelId} was not found or is outside this credential's scope.`, [tx.modelId]));
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (holdsEntireBatch ||
|
|
110
|
+
heldTargets.has(ctx.receiptTargetKey(tx.modelKey, tx.modelId))) {
|
|
111
|
+
await ctx.rollbackOptimistic(tx, 'conflict_server_wins');
|
|
112
|
+
ctx.store.updateStatus(tx.id, 'completed');
|
|
113
|
+
ctx.emit('transaction:completed', tx);
|
|
114
|
+
ctx.emit(`transaction:completed:${tx.id}`, tx);
|
|
115
|
+
ctx.optimisticUpdates.delete(tx.id);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
settlingBatchOps.push(entry);
|
|
119
|
+
}
|
|
120
|
+
// A forwarded write has been accepted, but the source database
|
|
121
|
+
// remains authoritative. It must not enter any of the confirmed
|
|
122
|
+
// paths below: in particular, a queued DELETE with sync id 0 is
|
|
123
|
+
// not the hosted-path idempotent-delete shortcut, and a later
|
|
124
|
+
// unrelated delta cannot stand in for the source echo.
|
|
125
|
+
if (result.status === 'queued') {
|
|
126
|
+
// Keep the exact envelope crash-durable while any forwarded
|
|
127
|
+
// member is awaiting its WAL echo. If coordination/missing-row
|
|
128
|
+
// handling consumed the whole request, there is no echo left
|
|
129
|
+
// to await and the durable intent is definitive.
|
|
130
|
+
if (settlingBatchOps.length === 0) {
|
|
131
|
+
await ctx.removeDurableCommit(commitIdempotencyKey);
|
|
132
|
+
}
|
|
133
|
+
for (const { tx } of settlingBatchOps) {
|
|
134
|
+
tx.requiresCorrelatedDelta = true;
|
|
135
|
+
tx.syncIdNeededForCompletion = undefined;
|
|
136
|
+
tx.correlationId = result.correlationId;
|
|
137
|
+
const echoSyncId = result.correlationId
|
|
138
|
+
? ctx.recentDeltaCorrelations.get(result.correlationId)
|
|
139
|
+
: undefined;
|
|
140
|
+
if (echoSyncId !== undefined) {
|
|
141
|
+
ctx.store.updateStatus(tx.id, 'completed');
|
|
142
|
+
ctx.emit('transaction:completed', tx);
|
|
143
|
+
ctx.emit(`transaction:completed:${tx.id}`, tx);
|
|
144
|
+
ctx.optimisticUpdates.delete(tx.id);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
ctx.store.updateStatus(tx.id, 'awaiting_delta');
|
|
148
|
+
ctx.scheduleReplicationLagTimeout(tx.id, commitIdempotencyKey, result.correlationId);
|
|
149
|
+
ctx.runtime.logger.debug('tx:awaiting_delta', {
|
|
150
|
+
txId: tx.id.slice(0, 8),
|
|
151
|
+
model: tx.modelName,
|
|
152
|
+
reason: 'queued_forward_waiting_for_correlated_echo',
|
|
153
|
+
});
|
|
154
|
+
ctx.scheduleDeltaConfirmationTimeout(tx, ctx.config.deltaConfirmationTimeout);
|
|
155
|
+
}
|
|
156
|
+
if (settlingBatchOps.length > 0 &&
|
|
157
|
+
settlingBatchOps.every(({ tx }) => tx.status === 'completed')) {
|
|
158
|
+
await ctx.removeDurableCommit(commitIdempotencyKey);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
await ctx.removeDurableCommit(commitIdempotencyKey);
|
|
163
|
+
ctx.noteAck(lastSyncId);
|
|
164
|
+
// A lastSyncId of 0 means the mutation succeeded but the server
|
|
165
|
+
// emitted no sync delta; record that anomaly for observability.
|
|
166
|
+
if (lastSyncId === 0) {
|
|
167
|
+
ctx.runtime.observability.captureCommitZeroSyncId({
|
|
168
|
+
operationCount: operations.length,
|
|
169
|
+
operations: operations.map((op) => `${op.type}:${op.model}:${op.id?.slice(0, 8) ?? '?'}`),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// Mark each remaining transaction with the sync-id threshold
|
|
173
|
+
// that will confirm it: any delta whose id is at least
|
|
174
|
+
// lastSyncId.
|
|
175
|
+
for (const { tx } of settlingBatchOps) {
|
|
176
|
+
tx.syncIdNeededForCompletion = lastSyncId;
|
|
177
|
+
// Safety net: a confirmed zero-sync DELETE is idempotent.
|
|
178
|
+
// This shortcut is intentionally unreachable for queued
|
|
179
|
+
// forwards, which must wait for their correlated echo.
|
|
180
|
+
if (lastSyncId === 0 && tx.type === 'delete') {
|
|
181
|
+
ctx.store.updateStatus(tx.id, 'completed');
|
|
182
|
+
ctx.emit('transaction:completed', tx);
|
|
183
|
+
ctx.emit(`transaction:completed:${tx.id}`, tx);
|
|
184
|
+
ctx.optimisticUpdates.delete(tx.id);
|
|
185
|
+
ctx.runtime.logger.debug('tx:confirm_delete_zero_syncid', {
|
|
186
|
+
txId: tx.id.slice(0, 8),
|
|
187
|
+
model: tx.modelName,
|
|
188
|
+
reason: 'delete_idempotent_no_delta',
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// A real watermark is the established hosted-path
|
|
193
|
+
// confirmation. A zero watermark on a non-delete stays on
|
|
194
|
+
// the anomaly/reconciliation path.
|
|
195
|
+
if (lastSyncId > 0) {
|
|
196
|
+
ctx.store.updateStatus(tx.id, 'completed');
|
|
197
|
+
ctx.emit('transaction:completed', tx);
|
|
198
|
+
ctx.emit(`transaction:completed:${tx.id}`, tx);
|
|
199
|
+
ctx.optimisticUpdates.delete(tx.id);
|
|
200
|
+
ctx.runtime.logger.debug('tx:confirm_ack', {
|
|
201
|
+
txId: tx.id.slice(0, 8),
|
|
202
|
+
model: tx.modelName,
|
|
203
|
+
serverSyncId: lastSyncId,
|
|
204
|
+
lastSeenSyncId: ctx.lastSeenSyncId,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
ctx.store.updateStatus(tx.id, 'awaiting_delta');
|
|
209
|
+
ctx.runtime.logger.debug('tx:awaiting_delta', {
|
|
210
|
+
txId: tx.id.slice(0, 8),
|
|
211
|
+
model: tx.modelName,
|
|
212
|
+
neededSyncId: lastSyncId,
|
|
213
|
+
lastSeenSyncId: ctx.lastSeenSyncId,
|
|
214
|
+
reason: 'zero_sync_id_anomaly',
|
|
215
|
+
});
|
|
216
|
+
ctx.scheduleDeltaConfirmationTimeout(tx, ctx.config.deltaConfirmationTimeout);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const errorMessage = error.message || '';
|
|
223
|
+
if (dispatchStarted && ctx.isDefinitiveRejection(error)) {
|
|
224
|
+
await ctx.removeDurableCommit(commitIdempotencyKey);
|
|
225
|
+
}
|
|
226
|
+
// Surface the raw server rejection for the whole batch so a
|
|
227
|
+
// cascaded failure — for example a foreign-key violation that
|
|
228
|
+
// rolls back a multi-operation transaction — is attributable to a
|
|
229
|
+
// specific cause rather than showing as a generic permanent error
|
|
230
|
+
// on each operation.
|
|
231
|
+
const abloErr = error instanceof AbloError ? error : undefined;
|
|
232
|
+
// SyncWebSocket attaches a `diagnostics` snapshot to its
|
|
233
|
+
// "not connected" / "closed while in flight" rejections.
|
|
234
|
+
// Surface it here so the warn line attributes the drop to
|
|
235
|
+
// a specific cause (handshake reject, heartbeat zombie,
|
|
236
|
+
// session expiry, …) instead of just "AbloConnectionError".
|
|
237
|
+
const readDiagnostics = (e) => {
|
|
238
|
+
let cur = e;
|
|
239
|
+
// Walk up to 3 wrap layers (current err → its cause → its
|
|
240
|
+
// cause's cause) so diagnostics survive AbloConnectionError
|
|
241
|
+
// wrapping in Ablo.commit() and any future wrappers.
|
|
242
|
+
for (let i = 0; i < 3 && cur && typeof cur === 'object'; i++) {
|
|
243
|
+
if ('diagnostics' in cur && cur.diagnostics) {
|
|
244
|
+
return (cur).diagnostics;
|
|
245
|
+
}
|
|
246
|
+
cur = cur.cause;
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
};
|
|
250
|
+
const diagnostics = readDiagnostics(error);
|
|
251
|
+
// Mechanic-level breadcrumb. Every batch rejection — transient
|
|
252
|
+
// (reconnect retries it) or permanent (`handleFailure` logs the
|
|
253
|
+
// authoritative `warn` with the same typed cause) — passes
|
|
254
|
+
// through here. Logging it at `warn` made one rejected write
|
|
255
|
+
// surface three identical dumps; keep it at `debug`.
|
|
256
|
+
ctx.runtime.logger.debug('[MutationQueue] Batch commit rejected', {
|
|
257
|
+
batchSize: batchOps.length,
|
|
258
|
+
models: batchOps.map(({ op }) => `${op.type}:${op.model}`),
|
|
259
|
+
errorType: abloErr?.type ?? error?.name,
|
|
260
|
+
errorCode: abloErr?.code,
|
|
261
|
+
httpStatus: abloErr?.httpStatus,
|
|
262
|
+
requestId: abloErr?.requestId,
|
|
263
|
+
message: errorMessage,
|
|
264
|
+
diagnostics,
|
|
265
|
+
});
|
|
266
|
+
// Handle "no rows in result set" gracefully: the row was already
|
|
267
|
+
// deleted, so for update and delete operations this is effectively
|
|
268
|
+
// success — the intended end state already holds — and the
|
|
269
|
+
// transaction is treated as completed.
|
|
270
|
+
if (errorMessage.includes('no rows in result set')) {
|
|
271
|
+
if (dispatchStarted) {
|
|
272
|
+
await ctx.removeDurableCommit(commitIdempotencyKey);
|
|
273
|
+
}
|
|
274
|
+
ctx.runtime.logger.info('[MutationQueue] Graceful handling: entity already deleted', {
|
|
275
|
+
batchSize: batchOps.length,
|
|
276
|
+
});
|
|
277
|
+
for (const { tx, op } of batchOps) {
|
|
278
|
+
if (op.type === 'UPDATE' || op.type === 'DELETE') {
|
|
279
|
+
// Row already gone: the intended state holds, mark completed.
|
|
280
|
+
ctx.store.updateStatus(tx.id, 'completed');
|
|
281
|
+
ctx.emit('transaction:completed', tx);
|
|
282
|
+
ctx.runtime.logger.debug('[MutationQueue] Orphaned transaction treated as success', {
|
|
283
|
+
txId: tx.id.slice(0, 12),
|
|
284
|
+
model: tx.modelName,
|
|
285
|
+
type: op.type,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// CREATE operations on non-existent parent are real failures
|
|
290
|
+
await ctx.handleFailure(tx, error);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
// Handle other batch failures - mark all as failed
|
|
296
|
+
for (const { tx } of batchOps) {
|
|
297
|
+
await ctx.handleFailure(tx, error);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Handle post-execution merge for updates
|
|
303
|
+
for (const tx of batch) {
|
|
304
|
+
const key = `${tx.modelName}:${tx.modelId}`;
|
|
305
|
+
if (tx.type === 'update') {
|
|
306
|
+
ctx.inFlightByModel.delete(key);
|
|
307
|
+
const pending = ctx.pendingMergeByModel.get(key);
|
|
308
|
+
if (pending && Object.keys(pending.data).length > 0) {
|
|
309
|
+
// Create a single merged follow-up transaction
|
|
310
|
+
const followUp = {
|
|
311
|
+
id: ctx.generateId(),
|
|
312
|
+
type: 'update',
|
|
313
|
+
modelName: tx.modelName,
|
|
314
|
+
modelId: tx.modelId,
|
|
315
|
+
modelKey: tx.modelKey ?? normalizeModelKey(tx.modelName),
|
|
316
|
+
data: pending.data,
|
|
317
|
+
previousData: undefined,
|
|
318
|
+
context: tx.context,
|
|
319
|
+
status: 'pending',
|
|
320
|
+
createdAt: Date.now(),
|
|
321
|
+
attempts: 0,
|
|
322
|
+
priority: 'normal',
|
|
323
|
+
priorityScore: ctx.computePriorityScore('update', tx.modelName),
|
|
324
|
+
sourceMutationIds: pending.sourceMutationIds,
|
|
325
|
+
};
|
|
326
|
+
ctx.pendingMergeByModel.delete(key);
|
|
327
|
+
ctx.store.add(followUp);
|
|
328
|
+
ctx.enqueue(followUp);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
ctx.setIsProcessing(false);
|
|
335
|
+
// Decrement executing count for backpressure tracking
|
|
336
|
+
ctx.setExecutingCount(ctx.executingCount - batch.length);
|
|
337
|
+
// Process next batch if needed
|
|
338
|
+
if (ctx.executionQueue.length > 0 && batch.length > 0) {
|
|
339
|
+
ctx.scheduleProcessing(true);
|
|
340
|
+
}
|
|
341
|
+
const batchEnd = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
342
|
+
ctx.runtime.logger.debug('txn:batch', batchEnd - batchStart, {
|
|
343
|
+
maxBatchSize: ctx.config.maxBatchSize,
|
|
344
|
+
remaining: ctx.executionQueue.length,
|
|
345
|
+
executingCount: ctx.executingCount,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}, { batchSize: ctx.executionQueue.length + (batch?.length ?? 0) });
|
|
349
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The queue's rules for coalescing operations that touch the same row, so their
|
|
3
|
+
* causal order is preserved. {@link MutationQueue} calls into these through a
|
|
4
|
+
* small store-shaped interface:
|
|
5
|
+
*
|
|
6
|
+
* - Create-then-delete cancellation ({@link takeUnsentCreateForModel}):
|
|
7
|
+
* deleting a row whose create never left the client cancels both locally.
|
|
8
|
+
* - The create barrier ({@link findCreateBarrierForDelete}) and deferred-delete
|
|
9
|
+
* parking ({@link deferDeleteUntilCreateSettles} /
|
|
10
|
+
* {@link releaseDeferredDeletesForCreate}): once a create has been sent, a
|
|
11
|
+
* delete must wait for it to settle, or the server could receive the delete
|
|
12
|
+
* before the create.
|
|
13
|
+
* - Update-payload merging ({@link mergeUpdateData}): collapses rapid updates
|
|
14
|
+
* to the same row into one wire operation.
|
|
15
|
+
*
|
|
16
|
+
* The queue keeps the `enqueue` and `delete` methods that orchestrate optimistic
|
|
17
|
+
* state and events; these functions hold only the coalescing rules.
|
|
18
|
+
*/
|
|
19
|
+
import type { MutationInput, QueuedMutation } from './commitPayload.js';
|
|
20
|
+
/** The subset of {@link MutationStore} that the coalescing rules read. */
|
|
21
|
+
export interface MutationStoreLike {
|
|
22
|
+
get(id: string): QueuedMutation | undefined;
|
|
23
|
+
getByStatus(status: QueuedMutation['status']): QueuedMutation[];
|
|
24
|
+
}
|
|
25
|
+
export declare const entityKey: (modelName: string, modelId: string) => string;
|
|
26
|
+
/**
|
|
27
|
+
* Finds and removes a create for the given model and id that has never been
|
|
28
|
+
* sent, searching the staging area, the execution queue, and the store's
|
|
29
|
+
* pending bucket in that order. It splices the match out of whichever queue
|
|
30
|
+
* held it, so the caller can cancel it rather than send a create followed by a
|
|
31
|
+
* delete.
|
|
32
|
+
*/
|
|
33
|
+
export declare function takeUnsentCreateForModel(staged: QueuedMutation[], queued: QueuedMutation[], store: Pick<MutationStoreLike, 'getByStatus'>, modelName: string, modelId: string): QueuedMutation | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Returns the most recent in-flight create for the given model and id that a
|
|
36
|
+
* delete must wait behind, or undefined if there is none. A pending create that
|
|
37
|
+
* has never been attempted is not a barrier, because it can be cancelled
|
|
38
|
+
* instead; once a create has been sent, even a retry-pending one is a barrier.
|
|
39
|
+
*/
|
|
40
|
+
export declare function findCreateBarrierForDelete(store: Pick<MutationStoreLike, 'getByStatus'>, modelName: string, modelId: string): QueuedMutation | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Parks a delete until the create for the same row settles, keyed by the
|
|
43
|
+
* create's model and id. {@link releaseDeferredDeletesForCreate} re-enqueues
|
|
44
|
+
* the parked deletes once that create completes.
|
|
45
|
+
*/
|
|
46
|
+
export declare function deferDeleteUntilCreateSettles(deferredDeletesByCreate: Map<string, QueuedMutation[]>, createTransaction: QueuedMutation, deleteTransaction: QueuedMutation): void;
|
|
47
|
+
/**
|
|
48
|
+
* Re-enqueues the deletes parked behind a create once that create settles,
|
|
49
|
+
* skipping any whose status is no longer pending.
|
|
50
|
+
*/
|
|
51
|
+
export declare function releaseDeferredDeletesForCreate(deferredDeletesByCreate: Map<string, QueuedMutation[]>, store: Pick<MutationStoreLike, 'get'>, enqueue: (transaction: QueuedMutation) => void, createTransaction: QueuedMutation): void;
|
|
52
|
+
/**
|
|
53
|
+
* Merges two update payloads for the same row into one. Later values win, with
|
|
54
|
+
* one exception: a `metadata` field is deep-merged as an object — parsing it
|
|
55
|
+
* first when it arrives as a JSON string — rather than replaced, so partial
|
|
56
|
+
* metadata updates accumulate instead of overwriting each other.
|
|
57
|
+
*/
|
|
58
|
+
export declare function mergeUpdateData(left: MutationInput | undefined, right: MutationInput | undefined, _modelName?: string): MutationInput;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The queue's rules for coalescing operations that touch the same row, so their
|
|
3
|
+
* causal order is preserved. {@link MutationQueue} calls into these through a
|
|
4
|
+
* small store-shaped interface:
|
|
5
|
+
*
|
|
6
|
+
* - Create-then-delete cancellation ({@link takeUnsentCreateForModel}):
|
|
7
|
+
* deleting a row whose create never left the client cancels both locally.
|
|
8
|
+
* - The create barrier ({@link findCreateBarrierForDelete}) and deferred-delete
|
|
9
|
+
* parking ({@link deferDeleteUntilCreateSettles} /
|
|
10
|
+
* {@link releaseDeferredDeletesForCreate}): once a create has been sent, a
|
|
11
|
+
* delete must wait for it to settle, or the server could receive the delete
|
|
12
|
+
* before the create.
|
|
13
|
+
* - Update-payload merging ({@link mergeUpdateData}): collapses rapid updates
|
|
14
|
+
* to the same row into one wire operation.
|
|
15
|
+
*
|
|
16
|
+
* The queue keeps the `enqueue` and `delete` methods that orchestrate optimistic
|
|
17
|
+
* state and events; these functions hold only the coalescing rules.
|
|
18
|
+
*/
|
|
19
|
+
export const entityKey = (modelName, modelId) => `${modelName}:${modelId}`;
|
|
20
|
+
const isTransactionForModel = (transaction, modelName, modelId) => transaction.modelName === modelName && transaction.modelId === modelId;
|
|
21
|
+
/**
|
|
22
|
+
* Finds and removes a create for the given model and id that has never been
|
|
23
|
+
* sent, searching the staging area, the execution queue, and the store's
|
|
24
|
+
* pending bucket in that order. It splices the match out of whichever queue
|
|
25
|
+
* held it, so the caller can cancel it rather than send a create followed by a
|
|
26
|
+
* delete.
|
|
27
|
+
*/
|
|
28
|
+
export function takeUnsentCreateForModel(staged, queued, store, modelName, modelId) {
|
|
29
|
+
const isUnsentCreate = (tx) => tx.type === 'create' &&
|
|
30
|
+
tx.status === 'pending' &&
|
|
31
|
+
tx.attempts === 0 &&
|
|
32
|
+
isTransactionForModel(tx, modelName, modelId);
|
|
33
|
+
const stagedIndex = staged.findIndex(isUnsentCreate);
|
|
34
|
+
if (stagedIndex >= 0) {
|
|
35
|
+
return staged.splice(stagedIndex, 1)[0];
|
|
36
|
+
}
|
|
37
|
+
const queuedIndex = queued.findIndex(isUnsentCreate);
|
|
38
|
+
if (queuedIndex >= 0) {
|
|
39
|
+
return queued.splice(queuedIndex, 1)[0];
|
|
40
|
+
}
|
|
41
|
+
return store.getByStatus('pending').find(isUnsentCreate);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Returns the most recent in-flight create for the given model and id that a
|
|
45
|
+
* delete must wait behind, or undefined if there is none. A pending create that
|
|
46
|
+
* has never been attempted is not a barrier, because it can be cancelled
|
|
47
|
+
* instead; once a create has been sent, even a retry-pending one is a barrier.
|
|
48
|
+
*/
|
|
49
|
+
export function findCreateBarrierForDelete(store, modelName, modelId) {
|
|
50
|
+
const liveCreates = [
|
|
51
|
+
...store.getByStatus('pending'),
|
|
52
|
+
...store.getByStatus('executing'),
|
|
53
|
+
...store.getByStatus('awaiting_delta'),
|
|
54
|
+
].filter((tx) => tx.type === 'create' &&
|
|
55
|
+
isTransactionForModel(tx, modelName, modelId) &&
|
|
56
|
+
// A never-attempted pending create can be cancelled instead. Once the
|
|
57
|
+
// create has been sent, even a retry-pending state is a causal barrier:
|
|
58
|
+
// the server may already have applied it and only the response was lost.
|
|
59
|
+
(tx.status !== 'pending' || tx.attempts > 0));
|
|
60
|
+
return liveCreates.sort((a, b) => b.createdAt - a.createdAt)[0];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parks a delete until the create for the same row settles, keyed by the
|
|
64
|
+
* create's model and id. {@link releaseDeferredDeletesForCreate} re-enqueues
|
|
65
|
+
* the parked deletes once that create completes.
|
|
66
|
+
*/
|
|
67
|
+
export function deferDeleteUntilCreateSettles(deferredDeletesByCreate, createTransaction, deleteTransaction) {
|
|
68
|
+
const key = entityKey(createTransaction.modelName, createTransaction.modelId);
|
|
69
|
+
const deferred = deferredDeletesByCreate.get(key) ?? [];
|
|
70
|
+
deferred.push(deleteTransaction);
|
|
71
|
+
deferredDeletesByCreate.set(key, deferred);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Re-enqueues the deletes parked behind a create once that create settles,
|
|
75
|
+
* skipping any whose status is no longer pending.
|
|
76
|
+
*/
|
|
77
|
+
export function releaseDeferredDeletesForCreate(deferredDeletesByCreate, store, enqueue, createTransaction) {
|
|
78
|
+
const key = entityKey(createTransaction.modelName, createTransaction.modelId);
|
|
79
|
+
const deferred = deferredDeletesByCreate.get(key);
|
|
80
|
+
if (!deferred || deferred.length === 0)
|
|
81
|
+
return;
|
|
82
|
+
deferredDeletesByCreate.delete(key);
|
|
83
|
+
for (const deleteTransaction of deferred) {
|
|
84
|
+
if (store.get(deleteTransaction.id)?.status !== 'pending')
|
|
85
|
+
continue;
|
|
86
|
+
enqueue(deleteTransaction);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Merges two update payloads for the same row into one. Later values win, with
|
|
91
|
+
* one exception: a `metadata` field is deep-merged as an object — parsing it
|
|
92
|
+
* first when it arrives as a JSON string — rather than replaced, so partial
|
|
93
|
+
* metadata updates accumulate instead of overwriting each other.
|
|
94
|
+
*/
|
|
95
|
+
export function mergeUpdateData(left, right, _modelName) {
|
|
96
|
+
const out = { ...(left || {}) };
|
|
97
|
+
const src = right || {};
|
|
98
|
+
for (const key of Object.keys(src)) {
|
|
99
|
+
// Special case: metadata payloads may be JSON strings; merge objects instead of clobbering
|
|
100
|
+
if (key === 'metadata') {
|
|
101
|
+
const l = out.metadata;
|
|
102
|
+
const r = src.metadata;
|
|
103
|
+
// If both sides undefined/null, continue
|
|
104
|
+
if (l == null && r == null) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Normalize to objects
|
|
108
|
+
const toObj = (v) => {
|
|
109
|
+
if (v == null)
|
|
110
|
+
return {};
|
|
111
|
+
if (typeof v === 'string') {
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(v);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (typeof v === 'object')
|
|
120
|
+
return v;
|
|
121
|
+
return {};
|
|
122
|
+
};
|
|
123
|
+
const lobj = toObj(l);
|
|
124
|
+
const robj = toObj(r);
|
|
125
|
+
const merged = { ...lobj, ...robj };
|
|
126
|
+
// Re-stringify to match schema input type
|
|
127
|
+
try {
|
|
128
|
+
out.metadata = JSON.stringify(merged);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Fallback to right-hand side if stringify fails
|
|
132
|
+
out.metadata = typeof r === 'string' ? r : JSON.stringify(robj || {});
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
// Default: shallow overwrite with right-hand value
|
|
137
|
+
out[key] = src[key];
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReadDependency, TrackDependency } from '@abloatai/transaction/coordination/schema';
|
|
2
|
+
import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
|
|
3
|
+
import type { CommitTransaction } from './commitLane.js';
|
|
4
|
+
import type { SealDurableCommitInput } from './commitTransport.js';
|
|
5
|
+
export interface CommitApiContext {
|
|
6
|
+
readonly assertDurableReplayOpen: () => void;
|
|
7
|
+
readonly commitStore: Map<string, CommitTransaction>;
|
|
8
|
+
readonly commitLane: CommitTransaction[];
|
|
9
|
+
readonly replicationLagErrors: Map<string, Error>;
|
|
10
|
+
readonly clearReplicationLagState: (transactionId: string) => void;
|
|
11
|
+
readonly nextCommitSequence: () => number;
|
|
12
|
+
readonly sealDurableCommit: (input: SealDurableCommitInput) => Promise<DurableCommitEnvelope>;
|
|
13
|
+
readonly processCommitLane: () => Promise<void>;
|
|
14
|
+
readonly emitCommitLifecycle: (event: string, payload: object) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function enqueueCommit(ctx: CommitApiContext, clientTxId: string, operations: CommitTransaction['operations'], options?: {
|
|
17
|
+
reads?: ReadDependency[] | null;
|
|
18
|
+
track?: TrackDependency[] | null;
|
|
19
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { AbloIdempotencyError } from '@abloatai/transaction/errors';
|
|
2
|
+
import { stableStringify } from '@abloatai/transaction/utils/json';
|
|
3
|
+
export async function enqueueCommit(ctx, clientTxId, operations, options = {}) {
|
|
4
|
+
ctx.assertDurableReplayOpen();
|
|
5
|
+
const existing = ctx.commitStore.get(clientTxId);
|
|
6
|
+
if (existing) {
|
|
7
|
+
await existing.sealPromise;
|
|
8
|
+
const existingIntent = stableStringify({
|
|
9
|
+
operations: existing.operations,
|
|
10
|
+
reads: existing.reads ?? null,
|
|
11
|
+
track: existing.track ?? null,
|
|
12
|
+
});
|
|
13
|
+
const incomingIntent = stableStringify({
|
|
14
|
+
operations,
|
|
15
|
+
reads: options.reads ?? null,
|
|
16
|
+
track: options.track ?? null,
|
|
17
|
+
});
|
|
18
|
+
if (existingIntent !== incomingIntent) {
|
|
19
|
+
throw new AbloIdempotencyError('Idempotency key reused with a different atomic commit request', { code: 'idempotency_conflict' });
|
|
20
|
+
}
|
|
21
|
+
if (existing.status === 'awaiting_delta' && ctx.replicationLagErrors.has(existing.id)) {
|
|
22
|
+
ctx.clearReplicationLagState(existing.id);
|
|
23
|
+
existing.status = 'pending';
|
|
24
|
+
ctx.commitLane.push(existing);
|
|
25
|
+
}
|
|
26
|
+
if (existing.status === 'pending')
|
|
27
|
+
void ctx.processCommitLane();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
ctx.emitCommitLifecycle('commit:staging', { clientTxId, operations });
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const tx = {
|
|
33
|
+
id: clientTxId,
|
|
34
|
+
kind: 'commit',
|
|
35
|
+
operations: [...operations],
|
|
36
|
+
...(options.reads ? { reads: options.reads } : {}),
|
|
37
|
+
...(options.track ? { track: options.track } : {}),
|
|
38
|
+
status: 'pending',
|
|
39
|
+
createdAt: now,
|
|
40
|
+
attempts: 0,
|
|
41
|
+
sealedAt: now,
|
|
42
|
+
sequence: ctx.nextCommitSequence(),
|
|
43
|
+
};
|
|
44
|
+
ctx.commitStore.set(clientTxId, tx);
|
|
45
|
+
tx.sealPromise = ctx.sealDurableCommit({
|
|
46
|
+
idempotencyKey: tx.id,
|
|
47
|
+
origin: 'atomic_commit',
|
|
48
|
+
operations: tx.operations,
|
|
49
|
+
commitOptions: {
|
|
50
|
+
...(tx.reads ? { reads: tx.reads } : {}),
|
|
51
|
+
...(tx.track ? { track: tx.track } : {}),
|
|
52
|
+
},
|
|
53
|
+
createdAt: tx.createdAt,
|
|
54
|
+
sealedAt: tx.sealedAt,
|
|
55
|
+
sequence: tx.sequence,
|
|
56
|
+
}).then((envelope) => {
|
|
57
|
+
tx.durableEnvelope = envelope;
|
|
58
|
+
tx.operations = envelope.operations.map((operation) => ({ ...operation }));
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
await tx.sealPromise;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
ctx.commitStore.delete(clientTxId);
|
|
65
|
+
ctx.emitCommitLifecycle('commit:seal_failed', { clientTxId });
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
tx.sealPromise = undefined;
|
|
70
|
+
}
|
|
71
|
+
ctx.commitLane.push(tx);
|
|
72
|
+
ctx.emitCommitLifecycle('commit:created', { clientTxId, operations: tx.operations });
|
|
73
|
+
void ctx.processCommitLane();
|
|
74
|
+
}
|