@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,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An in-memory index of transactions by id, with a secondary index by status.
|
|
3
|
+
* The status index keeps the queue's hot paths — such as `getByStatus('pending')`
|
|
4
|
+
* on every batch and coalesce decision — proportional to the number of
|
|
5
|
+
* transactions in that status rather than the total across all statuses.
|
|
6
|
+
* {@link MutationQueue} owns an instance and routes every status change
|
|
7
|
+
* through {@link updateStatus}, which keeps the two indexes consistent.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { QueuedMutation } from './commitPayload.js';
|
|
11
|
+
|
|
12
|
+
export class MutationStore {
|
|
13
|
+
private transactions = new Map<string, QueuedMutation>();
|
|
14
|
+
private byStatus = new Map<string, Set<string>>();
|
|
15
|
+
|
|
16
|
+
add(transaction: QueuedMutation): void {
|
|
17
|
+
this.transactions.set(transaction.id, transaction);
|
|
18
|
+
|
|
19
|
+
if (!this.byStatus.has(transaction.status)) {
|
|
20
|
+
this.byStatus.set(transaction.status, new Set());
|
|
21
|
+
}
|
|
22
|
+
this.byStatus.get(transaction.status)!.add(transaction.id);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get(id: string): QueuedMutation | undefined {
|
|
26
|
+
return this.transactions.get(id);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
updateStatus(id: string, newStatus: QueuedMutation['status']): void {
|
|
30
|
+
const tx = this.transactions.get(id);
|
|
31
|
+
if (!tx) return;
|
|
32
|
+
|
|
33
|
+
this.byStatus.get(tx.status)?.delete(id);
|
|
34
|
+
tx.status = newStatus;
|
|
35
|
+
|
|
36
|
+
if (!this.byStatus.has(newStatus)) {
|
|
37
|
+
this.byStatus.set(newStatus, new Set());
|
|
38
|
+
}
|
|
39
|
+
this.byStatus.get(newStatus)!.add(id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getByStatus(status: QueuedMutation['status']): QueuedMutation[] {
|
|
43
|
+
const ids = this.byStatus.get(status) || new Set();
|
|
44
|
+
return Array.from(ids)
|
|
45
|
+
.map((id) => this.transactions.get(id)!)
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
remove(id: string): void {
|
|
50
|
+
const tx = this.transactions.get(id);
|
|
51
|
+
if (!tx) return;
|
|
52
|
+
|
|
53
|
+
this.transactions.delete(id);
|
|
54
|
+
this.byStatus.get(tx.status)?.delete(id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
clear(): void {
|
|
58
|
+
this.transactions.clear();
|
|
59
|
+
this.byStatus.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getAll(): QueuedMutation[] {
|
|
63
|
+
return Array.from(this.transactions.values());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks the transactions this client has applied locally but the server has
|
|
3
|
+
* not yet confirmed, so their echoes can be recognized when they return over
|
|
4
|
+
* the sync stream. When a change is made optimistically, the client updates
|
|
5
|
+
* its own state at once; the server later broadcasts the same change as a sync
|
|
6
|
+
* delta stamped with the originating `transactionId`. This tracker lets the
|
|
7
|
+
* receive path recognize that delta as its own echo and skip re-applying it,
|
|
8
|
+
* since the optimistic write already reflects it. Without this discriminator
|
|
9
|
+
* the apply path would re-apply the confirmation on top of state that may have
|
|
10
|
+
* changed since, producing a visible flicker.
|
|
11
|
+
*
|
|
12
|
+
* An entry is added with {@link markPending} and drained either by
|
|
13
|
+
* {@link consumeEcho} when the matching delta arrives, or by
|
|
14
|
+
* {@link drainOnRollback} when the originating transaction is cancelled.
|
|
15
|
+
*
|
|
16
|
+
* The set is bounded by `maxSize` to guard against unbounded growth if
|
|
17
|
+
* transactions are somehow never confirmed and never rolled back. When the
|
|
18
|
+
* bound is reached the oldest id is evicted first, since the backing Map
|
|
19
|
+
* iterates in insertion order. Eviction carries no correctness risk for the
|
|
20
|
+
* client that made the write, whose local state already reflects the change;
|
|
21
|
+
* the only cost is that a later echo of the evicted transaction is applied as
|
|
22
|
+
* if it were a foreign change, at worst a single redundant re-set.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface UnconfirmedWritesOptions {
|
|
26
|
+
/**
|
|
27
|
+
* The maximum number of unconfirmed transaction ids to track. Once this many
|
|
28
|
+
* are held, adding another evicts the oldest. Defaults to 10,000, which
|
|
29
|
+
* leaves ample headroom even for a client with many tabs open during a large
|
|
30
|
+
* bulk edit.
|
|
31
|
+
*/
|
|
32
|
+
maxSize?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UnconfirmedWritesMetrics {
|
|
36
|
+
/** Total ids currently tracked. */
|
|
37
|
+
size: number;
|
|
38
|
+
/** Cumulative ids ever added since construction. */
|
|
39
|
+
totalAdded: number;
|
|
40
|
+
/** Cumulative successful echo matches (delta arrived → drained). */
|
|
41
|
+
hits: number;
|
|
42
|
+
/** Cumulative explicit rollback drains (transaction never made it). */
|
|
43
|
+
rollbacks: number;
|
|
44
|
+
/** Cumulative ids evicted due to maxSize pressure. */
|
|
45
|
+
evictions: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DEFAULT_MAX_SIZE = 10_000;
|
|
49
|
+
|
|
50
|
+
export class UnconfirmedWrites {
|
|
51
|
+
// Map (not Set) for O(1) FIFO eviction via insertion order.
|
|
52
|
+
// Value is unused; Map.keys() iterates in insertion order so
|
|
53
|
+
// `keys().next()` yields the oldest id.
|
|
54
|
+
private readonly ids = new Map<string, true>();
|
|
55
|
+
private readonly maxSize: number;
|
|
56
|
+
|
|
57
|
+
// Metrics — internal counters; exposed via `getMetrics()`. Kept
|
|
58
|
+
// numeric (not BigInt) since cumulative-since-page-load fits well
|
|
59
|
+
// under Number.MAX_SAFE_INTEGER for any realistic session.
|
|
60
|
+
private _totalAdded = 0;
|
|
61
|
+
private _hits = 0;
|
|
62
|
+
private _rollbacks = 0;
|
|
63
|
+
private _evictions = 0;
|
|
64
|
+
|
|
65
|
+
constructor(options: UnconfirmedWritesOptions = {}) {
|
|
66
|
+
this.maxSize = options.maxSize ?? DEFAULT_MAX_SIZE;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Mark a transaction as locally-applied. The next sync delta with a
|
|
71
|
+
* matching `transactionId` will be recognized as the server's
|
|
72
|
+
* confirmation of this same write. Idempotent — repeated calls with
|
|
73
|
+
* the same id are no-ops.
|
|
74
|
+
*/
|
|
75
|
+
markPending(transactionId: string): void {
|
|
76
|
+
if (this.ids.has(transactionId)) return;
|
|
77
|
+
if (this.ids.size >= this.maxSize) {
|
|
78
|
+
const oldest = this.ids.keys().next().value;
|
|
79
|
+
if (oldest !== undefined) {
|
|
80
|
+
this.ids.delete(oldest);
|
|
81
|
+
this._evictions += 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.ids.set(transactionId, true);
|
|
85
|
+
this._totalAdded += 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reports whether an incoming delta is this client's own echo, draining the
|
|
90
|
+
* entry if so. Returns true when the id was being tracked, signalling the
|
|
91
|
+
* caller to skip re-applying the change because the optimistic write already
|
|
92
|
+
* reflects it; returns false for a change that originated elsewhere, which
|
|
93
|
+
* should be applied normally.
|
|
94
|
+
*
|
|
95
|
+
* The check and drain are combined into one call so the receive path cannot
|
|
96
|
+
* accidentally treat two deltas carrying the same id, arriving in one batch,
|
|
97
|
+
* as separate echoes.
|
|
98
|
+
*/
|
|
99
|
+
consumeEcho(transactionId: string | null | undefined): boolean {
|
|
100
|
+
if (!transactionId) return false;
|
|
101
|
+
if (!this.ids.has(transactionId)) return false;
|
|
102
|
+
this.ids.delete(transactionId);
|
|
103
|
+
this._hits += 1;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Drains the entry for a transaction that was rolled back before the server
|
|
109
|
+
* confirmed it. No echo will ever arrive for a cancelled transaction, so
|
|
110
|
+
* without this the pending entry would leak. Rollbacks are counted separately
|
|
111
|
+
* from confirmed echoes, so a rise in rollbacks relative to hits can flag
|
|
112
|
+
* network or server trouble.
|
|
113
|
+
*/
|
|
114
|
+
drainOnRollback(transactionId: string): void {
|
|
115
|
+
if (this.ids.delete(transactionId)) {
|
|
116
|
+
this._rollbacks += 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getMetrics(): Readonly<UnconfirmedWritesMetrics> {
|
|
121
|
+
return {
|
|
122
|
+
size: this.ids.size,
|
|
123
|
+
totalAdded: this._totalAdded,
|
|
124
|
+
hits: this._hits,
|
|
125
|
+
rollbacks: this._rollbacks,
|
|
126
|
+
evictions: this._evictions,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
clear(): void {
|
|
131
|
+
this.ids.clear();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
2
|
+
import type { MutationQueueConfig } from './MutationQueue.js';
|
|
3
|
+
import type { MutationInput, QueuedMutation } from './commitPayload.js';
|
|
4
|
+
import type { MutationStore } from './MutationStore.js';
|
|
5
|
+
import type { OptimisticUpdateEntry } from './localMutation.js';
|
|
6
|
+
import type { StaleNotification } from '@abloatai/transaction/coordination/schema';
|
|
7
|
+
import type { DeltaConfirmationTracker } from './deltaConfirmation.js';
|
|
8
|
+
import type { MutationCommitResult } from '@abloatai/transaction/wire/commit';
|
|
9
|
+
import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
|
|
10
|
+
import { AbloError, AbloNotFoundError } from '@abloatai/transaction/errors';
|
|
11
|
+
import { applyWriteOptions, normalizeModelKey, TX_TYPE_TO_MUTATION_OP, type WriteOperationFields } from './commitPayload.js';
|
|
12
|
+
import type { MutationOperationType } from '@abloatai/transaction/types';
|
|
13
|
+
|
|
14
|
+
export interface BatchProcessingContext {
|
|
15
|
+
readonly runtime: RuntimeContext;
|
|
16
|
+
readonly config: MutationQueueConfig;
|
|
17
|
+
readonly durableReplayBlock: object | null;
|
|
18
|
+
readonly executionQueue: QueuedMutation[];
|
|
19
|
+
readonly isProcessing: boolean;
|
|
20
|
+
readonly setIsProcessing: (value: boolean) => void;
|
|
21
|
+
readonly takeNextExecutionBatch: () => QueuedMutation[];
|
|
22
|
+
readonly ensureDerivedFields: (transaction: QueuedMutation) => void;
|
|
23
|
+
readonly ensureCommitEnvelope: (batch: QueuedMutation[]) => string;
|
|
24
|
+
readonly executingCount: number;
|
|
25
|
+
readonly setExecutingCount: (value: number) => void;
|
|
26
|
+
readonly inFlightByModel: Set<string>;
|
|
27
|
+
readonly pendingMergeByModel: Map<string, { data: MutationInput; sourceMutationIds: string[] }>;
|
|
28
|
+
readonly generateId: () => string;
|
|
29
|
+
readonly computePriorityScore: (type: QueuedMutation['type'], modelName: string) => number;
|
|
30
|
+
readonly store: MutationStore;
|
|
31
|
+
readonly enqueue: (transaction: QueuedMutation) => void;
|
|
32
|
+
readonly optimisticUpdates: Map<string, OptimisticUpdateEntry>;
|
|
33
|
+
readonly commitNotifications: Map<string, StaleNotification[]>;
|
|
34
|
+
readonly commitMissingIds: Map<string, string[]>;
|
|
35
|
+
readonly sourceMutationIdsFor: (batch: readonly QueuedMutation[]) => string[];
|
|
36
|
+
readonly dispatchCommitBounded: (...args: Parameters<import('../../interfaces/index.js').MutationExecutor['commit']>) => ReturnType<import('../../interfaces/index.js').MutationExecutor['commit']>;
|
|
37
|
+
readonly parseMutationCommitResult: (value: Awaited<ReturnType<import('../../interfaces/index.js').MutationExecutor['commit']>>) => MutationCommitResult;
|
|
38
|
+
readonly persistDurableCommitAcceptance: (envelope: DurableCommitEnvelope, result: MutationCommitResult) => Promise<DurableCommitEnvelope>;
|
|
39
|
+
readonly removeDurableCommit: (idempotencyKey: string) => Promise<void>;
|
|
40
|
+
readonly assertEnvelopeInsideReplayWindow: (envelope: DurableCommitEnvelope) => void;
|
|
41
|
+
readonly sealDurableCommit: (input: Parameters<typeof import('./commitTransport.js').sealDurableCommit>[1]) => Promise<DurableCommitEnvelope>;
|
|
42
|
+
readonly noteAck: (syncId: number | undefined) => void;
|
|
43
|
+
readonly classifyReceiptNotifications: (
|
|
44
|
+
operations: readonly { model: string; id: string }[],
|
|
45
|
+
notifications: readonly StaleNotification[],
|
|
46
|
+
) => { holdsEntireBatch: boolean; heldTargets: Set<string>; notificationsByTarget: Map<string, StaleNotification[]> };
|
|
47
|
+
readonly receiptTargetKey: (modelName: string, modelId: string) => string;
|
|
48
|
+
readonly scheduleReplicationLagTimeout: (transactionId: string, clientTxId?: string, correlationId?: string) => void;
|
|
49
|
+
readonly scheduleDeltaConfirmationTimeout: (transaction: QueuedMutation, timeoutMs: number) => void;
|
|
50
|
+
readonly clearReplicationLagState: (transactionId: string) => void;
|
|
51
|
+
readonly completeQueuedCommit: (transaction: import('./commitLane.js').CommitTransaction, syncId: number) => void;
|
|
52
|
+
readonly queuedCommitMatchesCorrelation: (transaction: import('./commitLane.js').CommitTransaction, correlationId: string) => boolean;
|
|
53
|
+
readonly recentDeltaCorrelations: Map<string, number>;
|
|
54
|
+
readonly lastSeenSyncId: number;
|
|
55
|
+
readonly scheduleProcessing: (immediate?: boolean) => void;
|
|
56
|
+
readonly handleFailure: (transaction: QueuedMutation, error: Error) => Promise<void>;
|
|
57
|
+
readonly isDefinitiveRejection: (error: Error) => boolean;
|
|
58
|
+
readonly emitCommitLifecycle: (event: string, payload: object) => void;
|
|
59
|
+
readonly emit: (event: string, payload: object) => boolean;
|
|
60
|
+
readonly rollbackOptimistic: (transaction: QueuedMutation, reason: string, error?: Error) => Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function processBatch(ctx: BatchProcessingContext): Promise<void> { if (ctx.durableReplayBlock) return;
|
|
64
|
+
const batchStart = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
65
|
+
|
|
66
|
+
if (ctx.isProcessing || ctx.executionQueue.length === 0) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ctx.setIsProcessing(true);
|
|
71
|
+
|
|
72
|
+
// Declare batch outside try so it's accessible in finally for backpressure tracking
|
|
73
|
+
let batch: QueuedMutation[] = [];
|
|
74
|
+
|
|
75
|
+
await ctx.runtime.observability.startSpanAsync(
|
|
76
|
+
'sync.batch',
|
|
77
|
+
'sync.transaction.batch',
|
|
78
|
+
async () => {
|
|
79
|
+
try {
|
|
80
|
+
// Sort the execution queue by foreign-key priority before selecting a
|
|
81
|
+
// batch, so a parent row is always committed before its children, even
|
|
82
|
+
// across batch boundaries.
|
|
83
|
+
ctx.executionQueue.sort((a, b) => {
|
|
84
|
+
// Ensure derived fields exist (covers restored/persisted transactions)
|
|
85
|
+
ctx.ensureDerivedFields(a);
|
|
86
|
+
ctx.ensureDerivedFields(b);
|
|
87
|
+
if (a.modelName === b.modelName && a.modelId === b.modelId && a.type !== b.type) {
|
|
88
|
+
if (a.type === 'create') return -1;
|
|
89
|
+
if (b.type === 'create') return 1;
|
|
90
|
+
}
|
|
91
|
+
return a.priorityScore - b.priorityScore;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Take a fresh batch or one complete retry envelope. Retry envelopes
|
|
95
|
+
// retain both their original membership and operation order.
|
|
96
|
+
batch = ctx.takeNextExecutionBatch();
|
|
97
|
+
if (batch.length === 0) return;
|
|
98
|
+
const commitIdempotencyKey = ctx.ensureCommitEnvelope(batch);
|
|
99
|
+
|
|
100
|
+
// Track executing count for backpressure
|
|
101
|
+
ctx.setExecutingCount(ctx.executingCount + batch.length);
|
|
102
|
+
|
|
103
|
+
// Mark all as executing
|
|
104
|
+
for (const tx of batch) {
|
|
105
|
+
const key = `${tx.modelName}:${tx.modelId}`;
|
|
106
|
+
if (tx.type === 'update') ctx.inFlightByModel.add(key);
|
|
107
|
+
ctx.store.updateStatus(tx.id, 'executing');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Build every operation for one unified commit (a single round trip).
|
|
111
|
+
const batchOps: {
|
|
112
|
+
tx: QueuedMutation;
|
|
113
|
+
op: {
|
|
114
|
+
type: MutationOperationType;
|
|
115
|
+
model: string;
|
|
116
|
+
id: string;
|
|
117
|
+
input?: Record<string, unknown>;
|
|
118
|
+
transactionId: string;
|
|
119
|
+
} & WriteOperationFields;
|
|
120
|
+
}[] = [];
|
|
121
|
+
|
|
122
|
+
for (const tx of batch) {
|
|
123
|
+
// The per-operation `transactionId` carries the local transaction id
|
|
124
|
+
// over the wire so the server can stamp it on the resulting sync
|
|
125
|
+
// delta. The receive path matches it through
|
|
126
|
+
// {@link UnconfirmedWrites.consumeEcho} to avoid applying the
|
|
127
|
+
// client's own optimistic change twice. This is separate from the
|
|
128
|
+
// batch-level idempotency key recorded in `mutation_log`.
|
|
129
|
+
const op = applyWriteOptions({
|
|
130
|
+
type: TX_TYPE_TO_MUTATION_OP[tx.type],
|
|
131
|
+
model: tx.modelKey,
|
|
132
|
+
id: tx.modelId,
|
|
133
|
+
input: tx.type === 'create' || tx.type === 'update' ? tx.data || {} : undefined,
|
|
134
|
+
transactionId: tx.id,
|
|
135
|
+
}, tx);
|
|
136
|
+
batchOps.push({ tx, op });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Execute the unified commit for every operation in one round trip.
|
|
140
|
+
if (batchOps.length > 0) {
|
|
141
|
+
let dispatchStarted = false;
|
|
142
|
+
try {
|
|
143
|
+
const durableEnvelope = await ctx.sealDurableCommit({
|
|
144
|
+
idempotencyKey: commitIdempotencyKey,
|
|
145
|
+
origin: 'model_batch',
|
|
146
|
+
operations: batchOps.map(({ op }) => op),
|
|
147
|
+
sourceMutationIds: ctx.sourceMutationIdsFor(batch),
|
|
148
|
+
createdAt: Math.min(...batch.map((transaction) => transaction.createdAt)),
|
|
149
|
+
sealedAt: batch[0]?.commitEnvelope?.sealedAt ?? Date.now(),
|
|
150
|
+
sequence: batch[0]?.commitEnvelope?.sequence,
|
|
151
|
+
});
|
|
152
|
+
const operations = durableEnvelope.operations;
|
|
153
|
+
|
|
154
|
+
// Capture lastSyncId from the server response for threshold-based
|
|
155
|
+
// confirmation.
|
|
156
|
+
//
|
|
157
|
+
// The queue owns request identity. A lost acknowledgement may be
|
|
158
|
+
// retried after a backoff or reconnect, so every retry must send
|
|
159
|
+
// the exact key assigned before the first transport attempt.
|
|
160
|
+
ctx.assertEnvelopeInsideReplayWindow(durableEnvelope);
|
|
161
|
+
dispatchStarted = true;
|
|
162
|
+
const result = ctx.parseMutationCommitResult(
|
|
163
|
+
await ctx.dispatchCommitBounded(operations, {
|
|
164
|
+
idempotencyKey: commitIdempotencyKey,
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
await ctx.persistDurableCommitAcceptance(
|
|
168
|
+
durableEnvelope,
|
|
169
|
+
result,
|
|
170
|
+
);
|
|
171
|
+
const lastSyncId = result.lastSyncId;
|
|
172
|
+
|
|
173
|
+
const notifications = result.notifications;
|
|
174
|
+
const {
|
|
175
|
+
holdsEntireBatch,
|
|
176
|
+
heldTargets,
|
|
177
|
+
notificationsByTarget,
|
|
178
|
+
} = ctx.classifyReceiptNotifications(
|
|
179
|
+
batchOps.map(({ op }) => ({ model: op.model, id: op.id })),
|
|
180
|
+
notifications ?? [],
|
|
181
|
+
);
|
|
182
|
+
for (const { tx } of batchOps) {
|
|
183
|
+
const txTarget = ctx.receiptTargetKey(tx.modelKey, tx.modelId);
|
|
184
|
+
const txNotifs = holdsEntireBatch
|
|
185
|
+
? notifications
|
|
186
|
+
: notificationsByTarget.get(txTarget);
|
|
187
|
+
if (txNotifs && txNotifs.length > 0) {
|
|
188
|
+
ctx.commitNotifications.set(tx.id, txNotifs);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const missingIds = new Set(result.missingIds ?? []);
|
|
192
|
+
const settlingBatchOps: typeof batchOps = [];
|
|
193
|
+
for (const entry of batchOps) {
|
|
194
|
+
const { tx } = entry;
|
|
195
|
+
if (missingIds.has(tx.modelId)) {
|
|
196
|
+
await ctx.handleFailure(
|
|
197
|
+
tx,
|
|
198
|
+
new AbloNotFoundError(
|
|
199
|
+
`${tx.modelName}/${tx.modelId} was not found or is outside this credential's scope.`,
|
|
200
|
+
[tx.modelId],
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (
|
|
206
|
+
holdsEntireBatch ||
|
|
207
|
+
heldTargets.has(ctx.receiptTargetKey(tx.modelKey, tx.modelId))
|
|
208
|
+
) {
|
|
209
|
+
await ctx.rollbackOptimistic(tx, 'conflict_server_wins');
|
|
210
|
+
ctx.store.updateStatus(tx.id, 'completed');
|
|
211
|
+
ctx.emit('transaction:completed', tx);
|
|
212
|
+
ctx.emit(`transaction:completed:${tx.id}`, tx);
|
|
213
|
+
ctx.optimisticUpdates.delete(tx.id);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
settlingBatchOps.push(entry);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// A forwarded write has been accepted, but the source database
|
|
220
|
+
// remains authoritative. It must not enter any of the confirmed
|
|
221
|
+
// paths below: in particular, a queued DELETE with sync id 0 is
|
|
222
|
+
// not the hosted-path idempotent-delete shortcut, and a later
|
|
223
|
+
// unrelated delta cannot stand in for the source echo.
|
|
224
|
+
if (result.status === 'queued') {
|
|
225
|
+
// Keep the exact envelope crash-durable while any forwarded
|
|
226
|
+
// member is awaiting its WAL echo. If coordination/missing-row
|
|
227
|
+
// handling consumed the whole request, there is no echo left
|
|
228
|
+
// to await and the durable intent is definitive.
|
|
229
|
+
if (settlingBatchOps.length === 0) {
|
|
230
|
+
await ctx.removeDurableCommit(commitIdempotencyKey);
|
|
231
|
+
}
|
|
232
|
+
for (const { tx } of settlingBatchOps) {
|
|
233
|
+
tx.requiresCorrelatedDelta = true;
|
|
234
|
+
tx.syncIdNeededForCompletion = undefined;
|
|
235
|
+
tx.correlationId = result.correlationId;
|
|
236
|
+
const echoSyncId = result.correlationId
|
|
237
|
+
? ctx.recentDeltaCorrelations.get(result.correlationId)
|
|
238
|
+
: undefined;
|
|
239
|
+
if (echoSyncId !== undefined) {
|
|
240
|
+
ctx.store.updateStatus(tx.id, 'completed');
|
|
241
|
+
ctx.emit('transaction:completed', tx);
|
|
242
|
+
ctx.emit(`transaction:completed:${tx.id}`, tx);
|
|
243
|
+
ctx.optimisticUpdates.delete(tx.id);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
ctx.store.updateStatus(tx.id, 'awaiting_delta');
|
|
247
|
+
ctx.scheduleReplicationLagTimeout(
|
|
248
|
+
tx.id,
|
|
249
|
+
commitIdempotencyKey,
|
|
250
|
+
result.correlationId,
|
|
251
|
+
);
|
|
252
|
+
ctx.runtime.logger.debug('tx:awaiting_delta', {
|
|
253
|
+
txId: tx.id.slice(0, 8),
|
|
254
|
+
model: tx.modelName,
|
|
255
|
+
reason: 'queued_forward_waiting_for_correlated_echo',
|
|
256
|
+
});
|
|
257
|
+
ctx.scheduleDeltaConfirmationTimeout(
|
|
258
|
+
tx,
|
|
259
|
+
ctx.config.deltaConfirmationTimeout,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (
|
|
263
|
+
settlingBatchOps.length > 0 &&
|
|
264
|
+
settlingBatchOps.every(({ tx }) => tx.status === 'completed')
|
|
265
|
+
) {
|
|
266
|
+
await ctx.removeDurableCommit(commitIdempotencyKey);
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
await ctx.removeDurableCommit(commitIdempotencyKey);
|
|
270
|
+
ctx.noteAck(lastSyncId);
|
|
271
|
+
|
|
272
|
+
// A lastSyncId of 0 means the mutation succeeded but the server
|
|
273
|
+
// emitted no sync delta; record that anomaly for observability.
|
|
274
|
+
if (lastSyncId === 0) {
|
|
275
|
+
ctx.runtime.observability.captureCommitZeroSyncId({
|
|
276
|
+
operationCount: operations.length,
|
|
277
|
+
operations: operations.map(
|
|
278
|
+
(op) => `${op.type}:${op.model}:${op.id?.slice(0, 8) ?? '?'}`
|
|
279
|
+
),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Mark each remaining transaction with the sync-id threshold
|
|
284
|
+
// that will confirm it: any delta whose id is at least
|
|
285
|
+
// lastSyncId.
|
|
286
|
+
for (const { tx } of settlingBatchOps) {
|
|
287
|
+
tx.syncIdNeededForCompletion = lastSyncId;
|
|
288
|
+
|
|
289
|
+
// Safety net: a confirmed zero-sync DELETE is idempotent.
|
|
290
|
+
// This shortcut is intentionally unreachable for queued
|
|
291
|
+
// forwards, which must wait for their correlated echo.
|
|
292
|
+
if (lastSyncId === 0 && tx.type === 'delete') {
|
|
293
|
+
ctx.store.updateStatus(tx.id, 'completed');
|
|
294
|
+
ctx.emit('transaction:completed', tx);
|
|
295
|
+
ctx.emit(`transaction:completed:${tx.id}`, tx);
|
|
296
|
+
ctx.optimisticUpdates.delete(tx.id);
|
|
297
|
+
ctx.runtime.logger.debug('tx:confirm_delete_zero_syncid', {
|
|
298
|
+
txId: tx.id.slice(0, 8),
|
|
299
|
+
model: tx.modelName,
|
|
300
|
+
reason: 'delete_idempotent_no_delta',
|
|
301
|
+
});
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// A real watermark is the established hosted-path
|
|
306
|
+
// confirmation. A zero watermark on a non-delete stays on
|
|
307
|
+
// the anomaly/reconciliation path.
|
|
308
|
+
if (lastSyncId > 0) {
|
|
309
|
+
ctx.store.updateStatus(tx.id, 'completed');
|
|
310
|
+
ctx.emit('transaction:completed', tx);
|
|
311
|
+
ctx.emit(`transaction:completed:${tx.id}`, tx);
|
|
312
|
+
ctx.optimisticUpdates.delete(tx.id);
|
|
313
|
+
ctx.runtime.logger.debug('tx:confirm_ack', {
|
|
314
|
+
txId: tx.id.slice(0, 8),
|
|
315
|
+
model: tx.modelName,
|
|
316
|
+
serverSyncId: lastSyncId,
|
|
317
|
+
lastSeenSyncId: ctx.lastSeenSyncId,
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
ctx.store.updateStatus(tx.id, 'awaiting_delta');
|
|
321
|
+
ctx.runtime.logger.debug('tx:awaiting_delta', {
|
|
322
|
+
txId: tx.id.slice(0, 8),
|
|
323
|
+
model: tx.modelName,
|
|
324
|
+
neededSyncId: lastSyncId,
|
|
325
|
+
lastSeenSyncId: ctx.lastSeenSyncId,
|
|
326
|
+
reason: 'zero_sync_id_anomaly',
|
|
327
|
+
});
|
|
328
|
+
ctx.scheduleDeltaConfirmationTimeout(
|
|
329
|
+
tx,
|
|
330
|
+
ctx.config.deltaConfirmationTimeout,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const errorMessage = (error as Error).message || '';
|
|
337
|
+
if (dispatchStarted && ctx.isDefinitiveRejection(error as Error)) {
|
|
338
|
+
await ctx.removeDurableCommit(commitIdempotencyKey);
|
|
339
|
+
}
|
|
340
|
+
// Surface the raw server rejection for the whole batch so a
|
|
341
|
+
// cascaded failure — for example a foreign-key violation that
|
|
342
|
+
// rolls back a multi-operation transaction — is attributable to a
|
|
343
|
+
// specific cause rather than showing as a generic permanent error
|
|
344
|
+
// on each operation.
|
|
345
|
+
const abloErr = error instanceof AbloError ? error : undefined;
|
|
346
|
+
// SyncWebSocket attaches a `diagnostics` snapshot to its
|
|
347
|
+
// "not connected" / "closed while in flight" rejections.
|
|
348
|
+
// Surface it here so the warn line attributes the drop to
|
|
349
|
+
// a specific cause (handshake reject, heartbeat zombie,
|
|
350
|
+
// session expiry, …) instead of just "AbloConnectionError".
|
|
351
|
+
const readDiagnostics = (e: unknown): unknown => {
|
|
352
|
+
let cur: unknown = e;
|
|
353
|
+
// Walk up to 3 wrap layers (current err → its cause → its
|
|
354
|
+
// cause's cause) so diagnostics survive AbloConnectionError
|
|
355
|
+
// wrapping in Ablo.commit() and any future wrappers.
|
|
356
|
+
for (let i = 0; i < 3 && cur && typeof cur === 'object'; i++) {
|
|
357
|
+
if ('diagnostics' in cur && (cur as { diagnostics?: unknown }).diagnostics) {
|
|
358
|
+
return (cur).diagnostics;
|
|
359
|
+
}
|
|
360
|
+
cur = (cur as { cause?: unknown }).cause;
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
};
|
|
364
|
+
const diagnostics = readDiagnostics(error);
|
|
365
|
+
// Mechanic-level breadcrumb. Every batch rejection — transient
|
|
366
|
+
// (reconnect retries it) or permanent (`handleFailure` logs the
|
|
367
|
+
// authoritative `warn` with the same typed cause) — passes
|
|
368
|
+
// through here. Logging it at `warn` made one rejected write
|
|
369
|
+
// surface three identical dumps; keep it at `debug`.
|
|
370
|
+
ctx.runtime.logger.debug('[MutationQueue] Batch commit rejected', {
|
|
371
|
+
batchSize: batchOps.length,
|
|
372
|
+
models: batchOps.map(({ op }) => `${op.type}:${op.model}`),
|
|
373
|
+
errorType: abloErr?.type ?? (error as Error)?.name,
|
|
374
|
+
errorCode: abloErr?.code,
|
|
375
|
+
httpStatus: abloErr?.httpStatus,
|
|
376
|
+
requestId: abloErr?.requestId,
|
|
377
|
+
message: errorMessage,
|
|
378
|
+
diagnostics,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Handle "no rows in result set" gracefully: the row was already
|
|
382
|
+
// deleted, so for update and delete operations this is effectively
|
|
383
|
+
// success — the intended end state already holds — and the
|
|
384
|
+
// transaction is treated as completed.
|
|
385
|
+
if (errorMessage.includes('no rows in result set')) {
|
|
386
|
+
if (dispatchStarted) {
|
|
387
|
+
await ctx.removeDurableCommit(commitIdempotencyKey);
|
|
388
|
+
}
|
|
389
|
+
ctx.runtime.logger.info('[MutationQueue] Graceful handling: entity already deleted', {
|
|
390
|
+
batchSize: batchOps.length,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
for (const { tx, op } of batchOps) {
|
|
394
|
+
if (op.type === 'UPDATE' || op.type === 'DELETE') {
|
|
395
|
+
// Row already gone: the intended state holds, mark completed.
|
|
396
|
+
ctx.store.updateStatus(tx.id, 'completed');
|
|
397
|
+
ctx.emit('transaction:completed', tx);
|
|
398
|
+
|
|
399
|
+
ctx.runtime.logger.debug('[MutationQueue] Orphaned transaction treated as success', {
|
|
400
|
+
txId: tx.id.slice(0, 12),
|
|
401
|
+
model: tx.modelName,
|
|
402
|
+
type: op.type,
|
|
403
|
+
});
|
|
404
|
+
} else {
|
|
405
|
+
// CREATE operations on non-existent parent are real failures
|
|
406
|
+
await ctx.handleFailure(tx, error as Error);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
// Handle other batch failures - mark all as failed
|
|
411
|
+
for (const { tx } of batchOps) {
|
|
412
|
+
await ctx.handleFailure(tx, error as Error);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Handle post-execution merge for updates
|
|
419
|
+
for (const tx of batch) {
|
|
420
|
+
const key = `${tx.modelName}:${tx.modelId}`;
|
|
421
|
+
if (tx.type === 'update') {
|
|
422
|
+
ctx.inFlightByModel.delete(key);
|
|
423
|
+
const pending = ctx.pendingMergeByModel.get(key);
|
|
424
|
+
if (pending && Object.keys(pending.data).length > 0) {
|
|
425
|
+
// Create a single merged follow-up transaction
|
|
426
|
+
const followUp: QueuedMutation = {
|
|
427
|
+
id: ctx.generateId(),
|
|
428
|
+
type: 'update',
|
|
429
|
+
modelName: tx.modelName,
|
|
430
|
+
modelId: tx.modelId,
|
|
431
|
+
modelKey: tx.modelKey ?? normalizeModelKey(tx.modelName),
|
|
432
|
+
data: pending.data,
|
|
433
|
+
previousData: undefined,
|
|
434
|
+
context: tx.context,
|
|
435
|
+
status: 'pending',
|
|
436
|
+
createdAt: Date.now(),
|
|
437
|
+
attempts: 0,
|
|
438
|
+
priority: 'normal',
|
|
439
|
+
priorityScore: ctx.computePriorityScore('update', tx.modelName),
|
|
440
|
+
sourceMutationIds: pending.sourceMutationIds,
|
|
441
|
+
};
|
|
442
|
+
ctx.pendingMergeByModel.delete(key);
|
|
443
|
+
ctx.store.add(followUp);
|
|
444
|
+
ctx.enqueue(followUp);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} finally {
|
|
449
|
+
ctx.setIsProcessing(false);
|
|
450
|
+
|
|
451
|
+
// Decrement executing count for backpressure tracking
|
|
452
|
+
ctx.setExecutingCount(ctx.executingCount - batch.length);
|
|
453
|
+
|
|
454
|
+
// Process next batch if needed
|
|
455
|
+
if (ctx.executionQueue.length > 0 && batch.length > 0) {
|
|
456
|
+
ctx.scheduleProcessing(true);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const batchEnd = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
460
|
+
ctx.runtime.logger.debug('txn:batch', batchEnd - batchStart, {
|
|
461
|
+
maxBatchSize: ctx.config.maxBatchSize,
|
|
462
|
+
remaining: ctx.executionQueue.length,
|
|
463
|
+
executingCount: ctx.executingCount,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
{ batchSize: ctx.executionQueue.length + (batch?.length ?? 0) }
|
|
468
|
+
);
|
|
469
|
+
}
|