@abloatai/ablo 0.26.0 → 0.28.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/CHANGELOG.md +42 -2
- package/README.md +102 -86
- package/dist/BaseSyncedStore.d.ts +85 -88
- package/dist/BaseSyncedStore.js +134 -151
- package/dist/Database.d.ts +68 -69
- package/dist/Database.js +316 -135
- package/dist/{ObjectPool.d.ts → InstanceCache.d.ts} +10 -13
- package/dist/{ObjectPool.js → InstanceCache.js} +85 -83
- package/dist/LazyReferenceCollection.d.ts +11 -15
- package/dist/LazyReferenceCollection.js +12 -16
- package/dist/Model.d.ts +54 -52
- package/dist/Model.js +78 -62
- package/dist/ModelRegistry.d.ts +21 -19
- package/dist/ModelRegistry.js +23 -27
- package/dist/NetworkMonitor.d.ts +5 -6
- package/dist/NetworkMonitor.js +5 -6
- package/dist/SyncClient.d.ts +122 -118
- package/dist/SyncClient.js +541 -245
- package/dist/adapters/alwaysOnline.d.ts +6 -8
- package/dist/adapters/alwaysOnline.js +6 -8
- package/dist/adapters/inMemoryStorage.d.ts +10 -9
- package/dist/adapters/inMemoryStorage.js +21 -9
- package/dist/agent/Agent.d.ts +27 -32
- package/dist/agent/Agent.js +18 -19
- package/dist/agent/index.d.ts +4 -4
- package/dist/agent/index.js +5 -5
- package/dist/agent/session.d.ts +47 -44
- package/dist/agent/session.js +37 -48
- package/dist/agent/types.d.ts +26 -31
- package/dist/agent/types.js +6 -7
- package/dist/ai-sdk/coordinatedTool.d.ts +108 -0
- package/dist/ai-sdk/{coordinated-tool.js → coordinatedTool.js} +44 -38
- package/dist/ai-sdk/coordinationContext.d.ts +46 -0
- package/dist/ai-sdk/{coordination-context.js → coordinationContext.js} +26 -33
- package/dist/ai-sdk/index.d.ts +25 -22
- package/dist/ai-sdk/index.js +25 -22
- package/dist/ai-sdk/wrap.d.ts +6 -7
- package/dist/ai-sdk/wrap.js +1 -1
- package/dist/auth/credentialPolicy.d.ts +69 -74
- package/dist/auth/credentialPolicy.js +51 -56
- package/dist/auth/credentialSource.d.ts +6 -5
- package/dist/auth/credentialSource.js +9 -10
- package/dist/auth/index.d.ts +59 -58
- package/dist/auth/index.js +31 -37
- package/dist/auth/schemas.d.ts +5 -4
- package/dist/auth/schemas.js +5 -4
- package/dist/batching/index.d.ts +19 -21
- package/dist/batching/index.js +14 -17
- package/dist/cli.cjs +173 -121
- package/dist/client/Ablo.d.ts +97 -74
- package/dist/client/Ablo.js +129 -163
- package/dist/client/ApiClient.d.ts +30 -19
- package/dist/client/ApiClient.js +442 -81
- package/dist/client/auth.d.ts +47 -47
- package/dist/client/auth.js +108 -117
- package/dist/client/claimHeartbeatLoop.d.ts +50 -0
- package/dist/client/claimHeartbeatLoop.js +88 -0
- package/dist/client/consoleLogger.d.ts +5 -6
- package/dist/client/consoleLogger.js +5 -6
- package/dist/client/createInternalComponents.d.ts +16 -17
- package/dist/client/createInternalComponents.js +26 -31
- package/dist/client/createModelProxy.d.ts +130 -120
- package/dist/client/createModelProxy.js +152 -122
- package/dist/client/credentialEndpoint.d.ts +40 -42
- package/dist/client/credentialEndpoint.js +35 -36
- package/dist/client/functionalUpdate.d.ts +29 -27
- package/dist/client/functionalUpdate.js +21 -21
- package/dist/client/hostedEndpoints.d.ts +9 -12
- package/dist/client/hostedEndpoints.js +9 -12
- package/dist/client/httpClient.d.ts +59 -53
- package/dist/client/httpClient.js +29 -31
- package/dist/client/identity.d.ts +15 -20
- package/dist/client/identity.js +47 -58
- package/dist/client/modelRegistration.d.ts +5 -9
- package/dist/client/modelRegistration.js +78 -87
- package/dist/client/options.d.ts +157 -157
- package/dist/client/options.js +3 -7
- package/dist/client/registerDataSource.d.ts +9 -9
- package/dist/client/registerDataSource.js +15 -16
- package/dist/client/resourceTypes.d.ts +64 -75
- package/dist/client/resourceTypes.js +4 -10
- package/dist/client/schemaConfig.d.ts +31 -43
- package/dist/client/schemaConfig.js +38 -50
- package/dist/client/sessionMint.d.ts +16 -12
- package/dist/client/sessionMint.js +26 -31
- package/dist/client/validateAbloOptions.d.ts +12 -14
- package/dist/client/validateAbloOptions.js +8 -9
- package/dist/client/writeOptionsSchema.d.ts +18 -16
- package/dist/client/writeOptionsSchema.js +23 -20
- package/dist/client/wsMutationExecutor.d.ts +16 -20
- package/dist/client/wsMutationExecutor.js +18 -23
- package/dist/commit/contract.d.ts +493 -0
- package/dist/commit/contract.js +187 -0
- package/dist/commit/index.d.ts +6 -0
- package/dist/commit/index.js +5 -0
- package/dist/context.d.ts +6 -4
- package/dist/context.js +6 -4
- package/dist/coordination/index.d.ts +10 -8
- package/dist/coordination/index.js +14 -12
- package/dist/coordination/schema.d.ts +176 -128
- package/dist/coordination/schema.js +197 -133
- package/dist/coordination/trace.d.ts +9 -10
- package/dist/coordination/trace.js +13 -14
- package/dist/core/DatabaseManager.d.ts +5 -7
- package/dist/core/DatabaseManager.js +15 -19
- package/dist/core/QueryProcessor.d.ts +7 -9
- package/dist/core/QueryProcessor.js +22 -28
- package/dist/core/QueryView.d.ts +8 -8
- package/dist/core/QueryView.js +2 -2
- package/dist/core/StoreManager.d.ts +14 -14
- package/dist/core/StoreManager.js +33 -24
- package/dist/core/ViewRegistry.d.ts +5 -5
- package/dist/core/ViewRegistry.js +4 -4
- package/dist/core/index.d.ts +17 -12
- package/dist/core/index.js +32 -26
- package/dist/core/openIDBWithTimeout.d.ts +38 -36
- package/dist/core/openIDBWithTimeout.js +42 -43
- package/dist/core/queryUtils.d.ts +45 -0
- package/dist/core/queryUtils.js +69 -0
- package/dist/core/storeContract.d.ts +63 -61
- package/dist/core/storeContract.js +8 -12
- package/dist/environment.d.ts +28 -0
- package/dist/environment.js +21 -0
- package/dist/errorCodes.d.ts +107 -99
- package/dist/errorCodes.js +137 -134
- package/dist/errors.d.ts +160 -166
- package/dist/errors.js +155 -158
- package/dist/index.d.ts +36 -27
- package/dist/index.js +91 -86
- package/dist/interfaces/index.d.ts +102 -113
- package/dist/interfaces/index.js +5 -4
- package/dist/keys/index.d.ts +27 -29
- package/dist/keys/index.js +41 -40
- package/dist/mutators/RecordingTransaction.d.ts +16 -16
- package/dist/mutators/RecordingTransaction.js +31 -37
- package/dist/mutators/Transaction.d.ts +18 -26
- package/dist/mutators/Transaction.js +14 -20
- package/dist/mutators/UndoManager.d.ts +124 -131
- package/dist/mutators/UndoManager.js +177 -156
- package/dist/mutators/defineMutators.d.ts +23 -34
- package/dist/mutators/defineMutators.js +14 -20
- package/dist/mutators/inverseOp.d.ts +12 -15
- package/dist/mutators/inverseOp.js +12 -15
- package/dist/mutators/mutateActions.d.ts +10 -9
- package/dist/mutators/mutateActions.js +1 -1
- package/dist/mutators/readerActions.d.ts +9 -8
- package/dist/mutators/readerActions.js +2 -2
- package/dist/mutators/undoApply.d.ts +31 -27
- package/dist/mutators/undoApply.js +26 -24
- package/dist/policy/index.d.ts +5 -3
- package/dist/policy/index.js +5 -3
- package/dist/policy/types.d.ts +104 -100
- package/dist/policy/types.js +67 -66
- package/dist/query/client.d.ts +28 -23
- package/dist/query/client.js +45 -43
- package/dist/query/types.d.ts +37 -60
- package/dist/query/types.js +13 -33
- package/dist/react/AbloProvider.d.ts +1 -1
- package/dist/react/AbloProvider.js +2 -2
- package/dist/react/context.d.ts +25 -28
- package/dist/react/context.js +9 -10
- package/dist/react/index.d.ts +41 -42
- package/dist/react/index.js +37 -38
- package/dist/react/internalContext.d.ts +17 -19
- package/dist/react/useAblo.d.ts +28 -25
- package/dist/react/useAblo.js +41 -17
- package/dist/react/useCurrentUserId.d.ts +8 -7
- package/dist/react/useCurrentUserId.js +8 -7
- package/dist/react/useErrorListener.d.ts +7 -7
- package/dist/react/useErrorListener.js +10 -11
- package/dist/react/useMutationFailureListener.d.ts +8 -8
- package/dist/react/useMutationFailureListener.js +8 -8
- package/dist/react/useMutators.d.ts +11 -11
- package/dist/react/useMutators.js +3 -3
- package/dist/react/useReactive.js +2 -2
- package/dist/react/useSyncStatus.d.ts +4 -6
- package/dist/react/useUndoScope.d.ts +7 -9
- package/dist/react/useUndoScope.js +1 -1
- package/dist/schema/coordination.d.ts +21 -25
- package/dist/schema/coordination.js +21 -25
- package/dist/schema/ddl.d.ts +43 -39
- package/dist/schema/ddl.js +75 -68
- package/dist/schema/ddlLock.d.ts +20 -24
- package/dist/schema/ddlLock.js +18 -23
- package/dist/schema/diff.d.ts +99 -61
- package/dist/schema/diff.js +43 -34
- package/dist/schema/field.d.ts +37 -42
- package/dist/schema/field.js +35 -48
- package/dist/schema/generate.d.ts +12 -12
- package/dist/schema/generate.js +12 -12
- package/dist/schema/index.d.ts +3 -3
- package/dist/schema/index.js +21 -23
- package/dist/schema/model.d.ts +118 -143
- package/dist/schema/model.js +22 -33
- package/dist/schema/openapi.d.ts +10 -9
- package/dist/schema/openapi.js +5 -3
- package/dist/schema/queries.d.ts +29 -31
- package/dist/schema/queries.js +23 -25
- package/dist/schema/relation.d.ts +89 -99
- package/dist/schema/relation.js +13 -13
- package/dist/schema/residency.d.ts +16 -13
- package/dist/schema/residency.js +16 -13
- package/dist/schema/roles.d.ts +36 -43
- package/dist/schema/roles.js +31 -37
- package/dist/schema/schema.d.ts +64 -43
- package/dist/schema/schema.js +31 -32
- package/dist/schema/select.d.ts +13 -13
- package/dist/schema/select.js +13 -13
- package/dist/schema/serialize.d.ts +28 -31
- package/dist/schema/serialize.js +27 -31
- package/dist/schema/sugar.d.ts +17 -32
- package/dist/schema/sugar.js +14 -29
- package/dist/schema/{sync-delta-row.d.ts → syncDeltaRow.d.ts} +26 -49
- package/dist/schema/syncDeltaRow.js +89 -0
- package/dist/schema/tenancy.d.ts +44 -46
- package/dist/schema/tenancy.js +46 -48
- package/dist/server/adapter.d.ts +58 -58
- package/dist/server/adapter.js +13 -14
- package/dist/server/commit.d.ts +60 -64
- package/dist/server/index.d.ts +9 -10
- package/dist/server/index.js +1 -1
- package/dist/server/readConfig.d.ts +70 -0
- package/dist/server/readConfig.js +8 -0
- package/dist/server/storageMode.d.ts +23 -0
- package/dist/server/storageMode.js +17 -0
- package/dist/source/adapter.d.ts +30 -25
- package/dist/source/adapter.js +10 -10
- package/dist/source/adapters/drizzle.d.ts +28 -23
- package/dist/source/adapters/drizzle.js +30 -25
- package/dist/source/adapters/kysely.d.ts +27 -25
- package/dist/source/adapters/kysely.js +24 -23
- package/dist/source/adapters/memory.d.ts +8 -7
- package/dist/source/adapters/memory.js +9 -8
- package/dist/source/adapters/prisma.d.ts +13 -12
- package/dist/source/adapters/prisma.js +22 -25
- package/dist/source/conformance.d.ts +18 -11
- package/dist/source/conformance.js +17 -11
- package/dist/source/connector.d.ts +31 -32
- package/dist/source/connector.js +28 -28
- package/dist/source/connectorProtocol.d.ts +160 -0
- package/dist/source/connectorProtocol.js +162 -0
- package/dist/source/contract.d.ts +26 -27
- package/dist/source/contract.js +28 -29
- package/dist/source/factory.d.ts +46 -58
- package/dist/source/factory.js +22 -27
- package/dist/source/index.d.ts +7 -9
- package/dist/source/index.js +12 -14
- package/dist/source/migrations.d.ts +9 -9
- package/dist/source/migrations.js +9 -9
- package/dist/source/next.d.ts +9 -10
- package/dist/source/next.js +6 -7
- package/dist/source/pushQueue.d.ts +69 -47
- package/dist/source/pushQueue.js +32 -28
- package/dist/source/signing.d.ts +46 -17
- package/dist/source/signing.js +28 -11
- package/dist/source/types.d.ts +121 -104
- package/dist/source/types.js +13 -14
- package/dist/stores/ObjectStore.d.ts +24 -12
- package/dist/stores/ObjectStore.js +38 -16
- package/dist/stores/ObjectStoreContract.d.ts +14 -15
- package/dist/stores/SyncActionStore.d.ts +7 -11
- package/dist/stores/SyncActionStore.js +13 -17
- package/dist/surface.d.ts +28 -21
- package/dist/surface.js +29 -20
- package/dist/sync/{BootstrapHelper.d.ts → BootstrapFetcher.d.ts} +36 -42
- package/dist/sync/{BootstrapHelper.js → BootstrapFetcher.js} +76 -76
- package/dist/sync/ConnectionManager.d.ts +39 -50
- package/dist/sync/ConnectionManager.js +55 -66
- package/dist/sync/NetworkProbe.d.ts +24 -29
- package/dist/sync/NetworkProbe.js +63 -69
- package/dist/sync/{HydrationCoordinator.d.ts → OnDemandLoader.d.ts} +42 -41
- package/dist/sync/{HydrationCoordinator.js → OnDemandLoader.js} +59 -54
- package/dist/sync/{AreaOfInterestManager.d.ts → SubscriptionManager.d.ts} +43 -57
- package/dist/sync/{AreaOfInterestManager.js → SubscriptionManager.js} +43 -51
- package/dist/sync/SyncWebSocket.d.ts +141 -166
- package/dist/sync/SyncWebSocket.js +191 -223
- package/dist/sync/awaitClaimGrant.d.ts +18 -18
- package/dist/sync/awaitClaimGrant.js +11 -11
- package/dist/sync/bootstrapApply.d.ts +34 -24
- package/dist/sync/bootstrapApply.js +27 -19
- package/dist/sync/commitFrames.d.ts +21 -20
- package/dist/sync/commitFrames.js +18 -18
- package/dist/sync/createClaimStream.d.ts +23 -22
- package/dist/sync/createClaimStream.js +105 -23
- package/dist/sync/createPresenceStream.d.ts +19 -18
- package/dist/sync/createPresenceStream.js +25 -26
- package/dist/sync/createSnapshot.d.ts +12 -14
- package/dist/sync/createSnapshot.js +20 -26
- package/dist/sync/credentialLifecycle.d.ts +104 -104
- package/dist/sync/credentialLifecycle.js +140 -147
- package/dist/sync/deltaPipeline.d.ts +36 -34
- package/dist/sync/deltaPipeline.js +64 -65
- package/dist/sync/groupChange.d.ts +63 -61
- package/dist/sync/groupChange.js +74 -78
- package/dist/sync/heartbeat.d.ts +34 -33
- package/dist/sync/heartbeat.js +31 -31
- package/dist/sync/participants.d.ts +19 -19
- package/dist/sync/persistedPrefix.d.ts +12 -0
- package/dist/sync/persistedPrefix.js +22 -0
- package/dist/sync/schemas.d.ts +3 -2
- package/dist/sync/schemas.js +14 -10
- package/dist/sync/syncCursor.d.ts +17 -21
- package/dist/sync/syncCursor.js +17 -21
- package/dist/sync/syncPlan.d.ts +28 -36
- package/dist/sync/syncPlan.js +18 -19
- package/dist/sync/syncPosition.d.ts +54 -49
- package/dist/sync/syncPosition.js +57 -52
- package/dist/sync/wsFrameHandlers.d.ts +35 -36
- package/dist/sync/wsFrameHandlers.js +63 -67
- package/dist/testing/fixtures/bootstrap.d.ts +12 -6
- package/dist/testing/fixtures/bootstrap.js +12 -6
- package/dist/testing/fixtures/deltas.d.ts +30 -33
- package/dist/testing/fixtures/deltas.js +30 -33
- package/dist/testing/fixtures/models.d.ts +11 -10
- package/dist/testing/fixtures/models.js +11 -10
- package/dist/testing/helpers/{react-wrapper.d.ts → reactWrapper.d.ts} +13 -10
- package/dist/testing/helpers/{react-wrapper.js → reactWrapper.js} +15 -12
- package/dist/testing/helpers/{sync-engine-harness.d.ts → syncEngineHarness.d.ts} +17 -15
- package/dist/testing/helpers/{sync-engine-harness.js → syncEngineHarness.js} +12 -10
- package/dist/testing/helpers/wait.d.ts +13 -8
- package/dist/testing/helpers/wait.js +13 -8
- package/dist/testing/index.d.ts +5 -3
- package/dist/testing/index.js +3 -2
- package/dist/testing/mocks/FakeDatabase.d.ts +18 -0
- package/dist/testing/mocks/FakeDatabase.js +10 -0
- package/dist/testing/mocks/MockMutationExecutor.d.ts +18 -17
- package/dist/testing/mocks/MockMutationExecutor.js +15 -14
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +8 -8
- package/dist/testing/mocks/MockNetworkMonitor.js +8 -8
- package/dist/testing/mocks/MockSyncContext.d.ts +20 -17
- package/dist/testing/mocks/MockSyncContext.js +15 -13
- package/dist/testing/mocks/MockSyncStore.d.ts +10 -10
- package/dist/testing/mocks/MockSyncStore.js +11 -11
- package/dist/testing/mocks/MockWebSocket.d.ts +28 -23
- package/dist/testing/mocks/MockWebSocket.js +22 -21
- package/dist/transactions/TransactionQueue.d.ts +244 -181
- package/dist/transactions/TransactionQueue.js +929 -423
- package/dist/transactions/TransactionStore.d.ts +6 -4
- package/dist/transactions/TransactionStore.js +6 -4
- package/dist/transactions/UnconfirmedWrites.d.ts +82 -0
- package/dist/transactions/UnconfirmedWrites.js +104 -0
- package/dist/transactions/coalesceRules.d.ts +41 -17
- package/dist/transactions/coalesceRules.js +40 -17
- package/dist/transactions/commitEnvelope.d.ts +132 -0
- package/dist/transactions/commitEnvelope.js +139 -0
- package/dist/transactions/commitOutboxStore.d.ts +32 -0
- package/dist/transactions/commitOutboxStore.js +26 -0
- package/dist/transactions/commitPayload.d.ts +63 -52
- package/dist/transactions/commitPayload.js +54 -57
- package/dist/transactions/deltaConfirmation.d.ts +20 -22
- package/dist/transactions/deltaConfirmation.js +37 -45
- package/dist/transactions/httpCommitEnvelope.d.ts +43 -0
- package/dist/transactions/httpCommitEnvelope.js +179 -0
- package/dist/transactions/optimisticApply.d.ts +49 -0
- package/dist/transactions/optimisticApply.js +65 -0
- package/dist/transactions/replayValidation.d.ts +182 -0
- package/dist/transactions/replayValidation.js +156 -0
- package/dist/types/global.d.ts +46 -41
- package/dist/types/global.js +20 -19
- package/dist/types/index.d.ts +71 -77
- package/dist/types/index.js +22 -22
- package/dist/types/modelData.d.ts +6 -8
- package/dist/types/modelData.js +5 -7
- package/dist/types/participant.d.ts +10 -11
- package/dist/types/participant.js +6 -8
- package/dist/types/streams.d.ts +208 -195
- package/dist/types/streams.js +7 -7
- package/dist/utils/asyncIterator.d.ts +25 -32
- package/dist/utils/asyncIterator.js +25 -32
- package/dist/utils/duration.d.ts +12 -15
- package/dist/utils/duration.js +12 -15
- package/dist/utils/mobxSetup.d.ts +53 -0
- package/dist/utils/{mobx-setup.js → mobxSetup.js} +42 -98
- package/dist/webhooks/events.d.ts +21 -16
- package/dist/webhooks/events.js +10 -8
- package/dist/webhooks/index.d.ts +5 -7
- package/dist/webhooks/index.js +5 -7
- package/dist/wire/bootstrapReason.d.ts +9 -0
- package/dist/wire/bootstrapReason.js +8 -0
- package/dist/{schema/sync-delta-wire.d.ts → wire/delta.d.ts} +58 -41
- package/dist/wire/delta.js +114 -0
- package/dist/wire/errorEnvelope.d.ts +30 -31
- package/dist/wire/errorEnvelope.js +34 -40
- package/dist/wire/frames.d.ts +315 -86
- package/dist/wire/frames.js +47 -33
- package/dist/wire/index.d.ts +18 -14
- package/dist/wire/index.js +32 -27
- package/dist/wire/listEnvelope.d.ts +16 -23
- package/dist/wire/listEnvelope.js +7 -6
- package/dist/wire/protocol.d.ts +25 -32
- package/dist/wire/protocol.js +25 -32
- package/dist/wire/protocolVersion.d.ts +44 -40
- package/dist/wire/protocolVersion.js +44 -40
- package/docs/api.md +10 -10
- package/docs/coordination.md +59 -0
- package/docs/mcp.md +1 -1
- package/package.json +17 -11
- package/dist/ai-sdk/coordinated-tool.d.ts +0 -101
- package/dist/ai-sdk/coordination-context.d.ts +0 -52
- package/dist/core/query-utils.d.ts +0 -34
- package/dist/core/query-utils.js +0 -59
- package/dist/schema/sync-delta-row.js +0 -103
- package/dist/schema/sync-delta-wire.js +0 -102
- package/dist/server/read-config.d.ts +0 -67
- package/dist/server/read-config.js +0 -8
- package/dist/server/storage-mode.d.ts +0 -8
- package/dist/server/storage-mode.js +0 -28
- package/dist/source/connector-protocol.d.ts +0 -159
- package/dist/source/connector-protocol.js +0 -161
- package/dist/transactions/OptimisticEchoTracker.d.ts +0 -82
- package/dist/transactions/OptimisticEchoTracker.js +0 -104
- package/dist/transactions/mutation-error-handler.d.ts +0 -5
- package/dist/transactions/mutation-error-handler.js +0 -39
- package/dist/transactions/optimistic.d.ts +0 -24
- package/dist/transactions/optimistic.js +0 -45
- package/dist/transactions/persistedReplay.d.ts +0 -93
- package/dist/transactions/persistedReplay.js +0 -105
- package/dist/utils/mobx-setup.d.ts +0 -42
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* so the leaf stays cycle-free and testable in isolation.
|
|
2
|
+
* Tracks whether the confirming delta for a write has arrived. It holds the
|
|
3
|
+
* acknowledgement watermark (through the shared {@link SyncPosition}), the
|
|
4
|
+
* per-transaction confirmation timeouts, and the retry-with-backoff and
|
|
5
|
+
* reconciliation policy for transactions in the `awaiting_delta` status. It
|
|
6
|
+
* reaches back to {@link TransactionQueue} only through the small
|
|
7
|
+
* {@link DeltaConfirmationContext} interface, not the queue class itself, so it
|
|
8
|
+
* has no cyclic dependency and can be tested on its own.
|
|
10
9
|
*/
|
|
11
10
|
import { getContext } from '../context.js';
|
|
12
11
|
export class DeltaConfirmationTracker {
|
|
13
12
|
ctx;
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// Retry configuration for delta confirmation, using exponential backoff.
|
|
14
|
+
// Maximum retries before requesting a full reconciliation.
|
|
16
15
|
static DELTA_MAX_RETRIES = 5;
|
|
17
|
-
//
|
|
18
|
-
static DELTA_INITIAL_TIMEOUT_MS = 30_000;
|
|
19
|
-
// Max timeout cap (like Replicache's maxDelayMs of 60s)
|
|
16
|
+
// Upper bound on the backoff timeout.
|
|
20
17
|
static DELTA_MAX_TIMEOUT_MS = 120_000;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
18
|
+
// Pending confirmation timeouts for transactions awaiting their delta. On
|
|
19
|
+
// timeout the tracker retries with backoff rather than rolling back.
|
|
23
20
|
deltaConfirmationTimeouts = new Map();
|
|
24
21
|
// Track retry attempts per transaction for exponential backoff
|
|
25
22
|
deltaConfirmationRetries = new Map();
|
|
@@ -34,12 +31,12 @@ export class DeltaConfirmationTracker {
|
|
|
34
31
|
this.ctx.position.noteAck(lastSyncId);
|
|
35
32
|
}
|
|
36
33
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* @param syncId - The sync
|
|
34
|
+
* Confirms every awaiting transaction whose sync-id threshold this delta
|
|
35
|
+
* meets or exceeds.
|
|
36
|
+
* @param syncId - The sync id of the received delta.
|
|
40
37
|
*/
|
|
41
38
|
onDeltaReceived(syncId) {
|
|
42
|
-
//
|
|
39
|
+
// The cursor advances where the delta is applied (the store calls
|
|
43
40
|
// position.advanceApplied / advancePersisted); this hook only resolves
|
|
44
41
|
// confirmation thresholds against the incoming id.
|
|
45
42
|
const awaitingTxs = this.ctx.store.getByStatus('awaiting_delta');
|
|
@@ -82,7 +79,7 @@ export class DeltaConfirmationTracker {
|
|
|
82
79
|
}
|
|
83
80
|
// Log batch summary only if we confirmed something
|
|
84
81
|
if (confirmedCount > 0) {
|
|
85
|
-
//
|
|
82
|
+
// Leave a breadcrumb when transactions confirm.
|
|
86
83
|
getContext().observability.breadcrumb('Transactions confirmed via delta', 'sync.transaction', 'info', {
|
|
87
84
|
count: confirmedCount,
|
|
88
85
|
syncId,
|
|
@@ -90,15 +87,16 @@ export class DeltaConfirmationTracker {
|
|
|
90
87
|
});
|
|
91
88
|
}
|
|
92
89
|
}
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
90
|
+
// Schedule the confirmation wait for a transaction. On timeout the tracker
|
|
91
|
+
// retries with exponential backoff and requests reconciliation to catch up on
|
|
92
|
+
// missed deltas, rather than rolling back, which would discard state the
|
|
93
|
+
// server has already confirmed. A rollback happens only on an explicit server
|
|
94
|
+
// rejection, never on a timeout.
|
|
97
95
|
scheduleDeltaConfirmationTimeout(tx, timeoutMs) {
|
|
98
96
|
// Cancel any existing timeout for this transaction
|
|
99
97
|
this.cancelDeltaConfirmationTimeout(tx.id);
|
|
100
|
-
//
|
|
101
|
-
//
|
|
98
|
+
// Deliberately not an async callback: the body is fully synchronous, and
|
|
99
|
+
// `setTimeout(async …)` would turn any throw into an unhandled promise
|
|
102
100
|
// rejection instead of a catchable synchronous error.
|
|
103
101
|
const timeoutHandle = setTimeout(() => {
|
|
104
102
|
const currentTx = this.ctx.store.get(tx.id);
|
|
@@ -119,12 +117,6 @@ export class DeltaConfirmationTracker {
|
|
|
119
117
|
return;
|
|
120
118
|
}
|
|
121
119
|
const retryCount = this.deltaConfirmationRetries.get(tx.id) ?? 0;
|
|
122
|
-
const diagnosis = this.lastSeenSyncId === 0
|
|
123
|
-
? 'No deltas received - delta pipeline may be broken'
|
|
124
|
-
: currentTx.syncIdNeededForCompletion &&
|
|
125
|
-
this.lastSeenSyncId < currentTx.syncIdNeededForCompletion
|
|
126
|
-
? 'Delta not yet received - may be lost or delayed'
|
|
127
|
-
: 'Delta should have confirmed - possible race condition';
|
|
128
120
|
getContext().observability.captureReconciliation({
|
|
129
121
|
reason: 'delta_timeout',
|
|
130
122
|
model: tx.modelName,
|
|
@@ -135,14 +127,15 @@ export class DeltaConfirmationTracker {
|
|
|
135
127
|
connectionState: this.ctx.isConnected() ? 'connected' : 'disconnected',
|
|
136
128
|
});
|
|
137
129
|
if (retryCount < DeltaConfirmationTracker.DELTA_MAX_RETRIES) {
|
|
138
|
-
//
|
|
139
|
-
// The server already committed
|
|
130
|
+
// Retry: request reconciliation and re-schedule with exponential
|
|
131
|
+
// backoff. The server has already committed the mutation; only the
|
|
132
|
+
// delta is outstanding.
|
|
140
133
|
this.deltaConfirmationRetries.set(tx.id, retryCount + 1);
|
|
141
134
|
this.deltaConfirmationTimeouts.delete(tx.id);
|
|
142
135
|
// Exponential backoff: 30s → 60s → 120s → 120s → 120s (capped)
|
|
143
136
|
const nextTimeout = Math.min(timeoutMs * 2, DeltaConfirmationTracker.DELTA_MAX_TIMEOUT_MS);
|
|
144
|
-
//
|
|
145
|
-
//
|
|
137
|
+
// Request reconciliation so the client can cycle the connection and
|
|
138
|
+
// catch up on missed deltas from the server.
|
|
146
139
|
this.ctx.emit('reconciliation:needed', {
|
|
147
140
|
reason: 'delta_confirmation_timeout',
|
|
148
141
|
txId: tx.id,
|
|
@@ -163,10 +156,10 @@ export class DeltaConfirmationTracker {
|
|
|
163
156
|
this.scheduleDeltaConfirmationTimeout(tx, nextTimeout);
|
|
164
157
|
}
|
|
165
158
|
else {
|
|
166
|
-
//
|
|
167
|
-
// The
|
|
168
|
-
//
|
|
169
|
-
//
|
|
159
|
+
// Retries exhausted: persist the awaiting state instead of rolling back.
|
|
160
|
+
// The commit succeeded on the server, so the data exists there. Saving
|
|
161
|
+
// the awaiting state lets it survive the page closing; on the next
|
|
162
|
+
// session, reconnecting and catching up on deltas will confirm it.
|
|
170
163
|
this.deltaConfirmationRetries.delete(tx.id);
|
|
171
164
|
this.deltaConfirmationTimeouts.delete(tx.id);
|
|
172
165
|
getContext().observability.captureDeltaRetryExhausted({
|
|
@@ -176,7 +169,7 @@ export class DeltaConfirmationTracker {
|
|
|
176
169
|
retryCount: DeltaConfirmationTracker.DELTA_MAX_RETRIES,
|
|
177
170
|
syncIdNeeded: currentTx.syncIdNeededForCompletion,
|
|
178
171
|
});
|
|
179
|
-
// Emit persist event
|
|
172
|
+
// Emit the persist event; the client performs the write to local storage.
|
|
180
173
|
this.ctx.emit('transaction:persist_awaiting', {
|
|
181
174
|
txId: tx.id,
|
|
182
175
|
model: tx.modelName,
|
|
@@ -208,10 +201,9 @@ export class DeltaConfirmationTracker {
|
|
|
208
201
|
this.deltaConfirmationRetries.delete(id);
|
|
209
202
|
}
|
|
210
203
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* callbacks against an already-cleared store (T1.19).
|
|
204
|
+
* Clears every armed confirmation timer, one per in-flight transaction.
|
|
205
|
+
* {@link TransactionQueue.dispose} calls this; without it a disposed queue
|
|
206
|
+
* would keep the process alive and fire callbacks against a cleared store.
|
|
215
207
|
*/
|
|
216
208
|
dispose() {
|
|
217
209
|
for (const timeoutHandle of this.deltaConfirmationTimeouts.values()) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Crash-durable exact HTTP request used by the stateless agent client. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare const HTTP_COMMIT_ENVELOPE_VERSION: 1;
|
|
4
|
+
export declare const HTTP_COMMIT_ENVELOPE_PREFIX = "http-commit-envelope:";
|
|
5
|
+
/** Stay one hour inside the server's 24-hour idempotency retention window. */
|
|
6
|
+
export declare const HTTP_COMMIT_REPLAY_WINDOW_MS: number;
|
|
7
|
+
/** Apply normal JSON semantics once, then make object-key order canonical. */
|
|
8
|
+
export declare function canonicalHttpCommitBody(value: unknown): string;
|
|
9
|
+
export declare const durableHttpCommitEnvelopeSchema: z.ZodObject<{
|
|
10
|
+
id: z.ZodString;
|
|
11
|
+
type: z.ZodLiteral<"http_commit_envelope">;
|
|
12
|
+
storageVersion: z.ZodLiteral<1>;
|
|
13
|
+
idempotencyKey: z.core.$ZodBranded<z.ZodString, "IdempotencyKey", "out">;
|
|
14
|
+
request: z.ZodObject<{
|
|
15
|
+
method: z.ZodEnum<{
|
|
16
|
+
DELETE: "DELETE";
|
|
17
|
+
POST: "POST";
|
|
18
|
+
PATCH: "PATCH";
|
|
19
|
+
}>;
|
|
20
|
+
path: z.ZodString;
|
|
21
|
+
body: z.ZodString;
|
|
22
|
+
}, z.core.$strict>;
|
|
23
|
+
scopeNamespace: z.ZodString;
|
|
24
|
+
createdAt: z.ZodNumber;
|
|
25
|
+
sealedAt: z.ZodNumber;
|
|
26
|
+
sequence: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
timestamp: z.ZodNumber;
|
|
28
|
+
}, z.core.$strict>;
|
|
29
|
+
export type DurableHttpCommitEnvelope = z.infer<typeof durableHttpCommitEnvelopeSchema>;
|
|
30
|
+
export declare function httpCommitEnvelopeRecordId(idempotencyKey: string, scopeNamespace?: string): string;
|
|
31
|
+
export declare function createDurableHttpCommitEnvelope(input: {
|
|
32
|
+
idempotencyKey: string;
|
|
33
|
+
request: {
|
|
34
|
+
method: 'POST' | 'PATCH' | 'DELETE';
|
|
35
|
+
path: string;
|
|
36
|
+
body: unknown;
|
|
37
|
+
};
|
|
38
|
+
scopeNamespace: string;
|
|
39
|
+
createdAt?: number;
|
|
40
|
+
sealedAt?: number;
|
|
41
|
+
sequence?: number;
|
|
42
|
+
}): DurableHttpCommitEnvelope;
|
|
43
|
+
export declare function isHttpCommitReplayExpired(envelope: Pick<DurableHttpCommitEnvelope, 'sealedAt'>, now?: number): boolean;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/** Crash-durable exact HTTP request used by the stateless agent client. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { v5 as uuidv5 } from 'uuid';
|
|
4
|
+
import { idempotencyKeySchema } from '../commit/contract.js';
|
|
5
|
+
import { stableStringify } from '../utils/json.js';
|
|
6
|
+
export const HTTP_COMMIT_ENVELOPE_VERSION = 1;
|
|
7
|
+
export const HTTP_COMMIT_ENVELOPE_PREFIX = 'http-commit-envelope:';
|
|
8
|
+
/** Stay one hour inside the server's 24-hour idempotency retention window. */
|
|
9
|
+
export const HTTP_COMMIT_REPLAY_WINDOW_MS = 23 * 60 * 60 * 1000;
|
|
10
|
+
const HTTP_COMMIT_SCOPE_ID_NAMESPACE = '043e8f73-86fc-5f62-af46-935d68fca729';
|
|
11
|
+
const commitPathSchema = z.literal('/v1/commits');
|
|
12
|
+
const modelCollectionPathSchema = z.string().regex(/^\/v1\/models\/[^/]+$/);
|
|
13
|
+
const modelEntityPathSchema = z.string().regex(/^\/v1\/models\/[^/]+\/[^/]+$/);
|
|
14
|
+
function hasSafeModelPathSegments(path) {
|
|
15
|
+
if (!path.startsWith('/v1/models/'))
|
|
16
|
+
return true;
|
|
17
|
+
try {
|
|
18
|
+
return path
|
|
19
|
+
.slice('/v1/models/'.length)
|
|
20
|
+
.split('/')
|
|
21
|
+
.every((segment) => {
|
|
22
|
+
const decoded = decodeURIComponent(segment);
|
|
23
|
+
return (decoded.length > 0 &&
|
|
24
|
+
decoded !== '.' &&
|
|
25
|
+
decoded !== '..' &&
|
|
26
|
+
!decoded.includes('/') &&
|
|
27
|
+
!decoded.includes('\\'));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Apply normal JSON semantics once, then make object-key order canonical. */
|
|
35
|
+
export function canonicalHttpCommitBody(value) {
|
|
36
|
+
const serialized = JSON.stringify(value);
|
|
37
|
+
if (serialized === undefined) {
|
|
38
|
+
throw new TypeError('HTTP commit body is not JSON serializable');
|
|
39
|
+
}
|
|
40
|
+
return stableStringify(JSON.parse(serialized));
|
|
41
|
+
}
|
|
42
|
+
export const durableHttpCommitEnvelopeSchema = z
|
|
43
|
+
.strictObject({
|
|
44
|
+
id: z.string().startsWith(HTTP_COMMIT_ENVELOPE_PREFIX),
|
|
45
|
+
type: z.literal('http_commit_envelope'),
|
|
46
|
+
storageVersion: z.literal(HTTP_COMMIT_ENVELOPE_VERSION),
|
|
47
|
+
idempotencyKey: idempotencyKeySchema,
|
|
48
|
+
request: z.strictObject({
|
|
49
|
+
method: z.enum(['POST', 'PATCH', 'DELETE']),
|
|
50
|
+
path: z.string().startsWith('/'),
|
|
51
|
+
body: z.string().refine((body) => {
|
|
52
|
+
try {
|
|
53
|
+
JSON.parse(body);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}, 'HTTP commit body must be valid JSON'),
|
|
60
|
+
}),
|
|
61
|
+
scopeNamespace: z.string().min(1),
|
|
62
|
+
createdAt: z.number().int().nonnegative(),
|
|
63
|
+
sealedAt: z.number().int().nonnegative(),
|
|
64
|
+
/** Monotonic within one client; disambiguates writes sealed in the same ms. */
|
|
65
|
+
sequence: z.number().int().nonnegative().optional(),
|
|
66
|
+
timestamp: z.number().int().nonnegative(),
|
|
67
|
+
})
|
|
68
|
+
.superRefine((envelope, context) => {
|
|
69
|
+
const legacyId = httpCommitEnvelopeRecordId(envelope.idempotencyKey);
|
|
70
|
+
const scopedId = httpCommitEnvelopeRecordId(envelope.idempotencyKey, envelope.scopeNamespace);
|
|
71
|
+
if (envelope.id !== legacyId && envelope.id !== scopedId) {
|
|
72
|
+
context.addIssue({
|
|
73
|
+
code: 'custom',
|
|
74
|
+
path: ['id'],
|
|
75
|
+
message: 'HTTP envelope id must be derived from its idempotency key',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (envelope.sealedAt < envelope.createdAt) {
|
|
79
|
+
context.addIssue({
|
|
80
|
+
code: 'custom',
|
|
81
|
+
path: ['sealedAt'],
|
|
82
|
+
message: 'HTTP envelope cannot be sealed before it is created',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const { method, path } = envelope.request;
|
|
86
|
+
const allowedPath = method === 'POST'
|
|
87
|
+
? commitPathSchema.safeParse(path).success ||
|
|
88
|
+
modelCollectionPathSchema.safeParse(path).success
|
|
89
|
+
: modelEntityPathSchema.safeParse(path).success;
|
|
90
|
+
if (!allowedPath || !hasSafeModelPathSegments(path)) {
|
|
91
|
+
context.addIssue({
|
|
92
|
+
code: 'custom',
|
|
93
|
+
path: ['request', 'path'],
|
|
94
|
+
message: 'HTTP outbox records may target only commit or model-mutation routes',
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
let body;
|
|
99
|
+
try {
|
|
100
|
+
body = JSON.parse(envelope.request.body);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return; // The field-level JSON refinement reports this.
|
|
104
|
+
}
|
|
105
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
106
|
+
context.addIssue({
|
|
107
|
+
code: 'custom',
|
|
108
|
+
path: ['request', 'body'],
|
|
109
|
+
message: 'HTTP commit body must be a JSON object',
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const record = body;
|
|
114
|
+
if (record.idempotencyKey !== envelope.idempotencyKey) {
|
|
115
|
+
context.addIssue({
|
|
116
|
+
code: 'custom',
|
|
117
|
+
path: ['request', 'body', 'idempotencyKey'],
|
|
118
|
+
message: 'HTTP body idempotency key must match its envelope',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (path === '/v1/commits') {
|
|
122
|
+
if (record.clientTxId !== envelope.idempotencyKey ||
|
|
123
|
+
!Array.isArray(record.operations) ||
|
|
124
|
+
record.operations.length === 0) {
|
|
125
|
+
context.addIssue({
|
|
126
|
+
code: 'custom',
|
|
127
|
+
path: ['request', 'body'],
|
|
128
|
+
message: 'Commit-route body must carry the same clientTxId and operations',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (method === 'POST' && typeof record.id !== 'string') {
|
|
133
|
+
context.addIssue({
|
|
134
|
+
code: 'custom',
|
|
135
|
+
path: ['request', 'body', 'id'],
|
|
136
|
+
message: 'Model-create body must carry its entity id',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
else if (method === 'PATCH' &&
|
|
140
|
+
(typeof record.data !== 'object' || record.data === null || Array.isArray(record.data))) {
|
|
141
|
+
context.addIssue({
|
|
142
|
+
code: 'custom',
|
|
143
|
+
path: ['request', 'body', 'data'],
|
|
144
|
+
message: 'Model-update body must carry a JSON object patch',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
export function httpCommitEnvelopeRecordId(idempotencyKey, scopeNamespace) {
|
|
149
|
+
if (!scopeNamespace) {
|
|
150
|
+
return `${HTTP_COMMIT_ENVELOPE_PREFIX}${idempotencyKey}`;
|
|
151
|
+
}
|
|
152
|
+
const scopeId = uuidv5(scopeNamespace, HTTP_COMMIT_SCOPE_ID_NAMESPACE);
|
|
153
|
+
return `${HTTP_COMMIT_ENVELOPE_PREFIX}${scopeId}:${idempotencyKey}`;
|
|
154
|
+
}
|
|
155
|
+
export function createDurableHttpCommitEnvelope(input) {
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
const createdAt = input.createdAt ?? now;
|
|
158
|
+
const sealedAt = input.sealedAt ?? now;
|
|
159
|
+
const body = canonicalHttpCommitBody(input.request.body);
|
|
160
|
+
return durableHttpCommitEnvelopeSchema.parse({
|
|
161
|
+
id: httpCommitEnvelopeRecordId(input.idempotencyKey, input.scopeNamespace),
|
|
162
|
+
type: 'http_commit_envelope',
|
|
163
|
+
storageVersion: HTTP_COMMIT_ENVELOPE_VERSION,
|
|
164
|
+
idempotencyKey: input.idempotencyKey,
|
|
165
|
+
request: {
|
|
166
|
+
method: input.request.method,
|
|
167
|
+
path: input.request.path,
|
|
168
|
+
body,
|
|
169
|
+
},
|
|
170
|
+
scopeNamespace: input.scopeNamespace,
|
|
171
|
+
createdAt,
|
|
172
|
+
sealedAt,
|
|
173
|
+
...(input.sequence !== undefined ? { sequence: input.sequence } : {}),
|
|
174
|
+
timestamp: sealedAt,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
export function isHttpCommitReplayExpired(envelope, now = Date.now()) {
|
|
178
|
+
return now - envelope.sealedAt >= HTTP_COMMIT_REPLAY_WINDOW_MS;
|
|
179
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-first apply and rollback bookkeeping for pending mutations.
|
|
3
|
+
*
|
|
4
|
+
* When a change is written before the server confirms it, these functions
|
|
5
|
+
* record it in a ledger and emit an `optimistic:*` event; separate listeners
|
|
6
|
+
* apply the change to the in-memory data and, on rollback, restore the value
|
|
7
|
+
* it replaced. Each function takes the ledger and a small event emitter rather
|
|
8
|
+
* than any larger object, so the rules have no dependencies of their own and
|
|
9
|
+
* can be tested in isolation.
|
|
10
|
+
*/
|
|
11
|
+
import type { Model } from '../Model.js';
|
|
12
|
+
import type { MutationInput, Transaction } from './commitPayload.js';
|
|
13
|
+
/**
|
|
14
|
+
* One tracked optimistic mutation: the live model plus the value it held
|
|
15
|
+
* before the change, kept so a rollback can restore it.
|
|
16
|
+
*/
|
|
17
|
+
export interface OptimisticUpdateEntry {
|
|
18
|
+
model: Model;
|
|
19
|
+
previousState: MutationInput | null | undefined;
|
|
20
|
+
transaction: Transaction;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The event emitter these functions use to announce each optimistic change and
|
|
24
|
+
* its rollback. You supply the implementation.
|
|
25
|
+
*/
|
|
26
|
+
export interface OptimisticEmitter {
|
|
27
|
+
emit(event: string, payload: unknown): void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Record an optimistic create and announce it. There is no prior value to
|
|
31
|
+
* restore, so the rollback pre-image is `null`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function applyOptimisticCreate(optimisticUpdates: Map<string, OptimisticUpdateEntry>, emitter: OptimisticEmitter, model: Model, transaction: Transaction): void;
|
|
34
|
+
/**
|
|
35
|
+
* Record an optimistic update and announce it, keeping the row's prior value so
|
|
36
|
+
* a rollback can put it back.
|
|
37
|
+
*/
|
|
38
|
+
export declare function applyOptimisticUpdate(optimisticUpdates: Map<string, OptimisticUpdateEntry>, emitter: OptimisticEmitter, model: Model, transaction: Transaction): void;
|
|
39
|
+
/**
|
|
40
|
+
* Record an optimistic delete and announce it, keeping the deleted row so a
|
|
41
|
+
* rollback can restore it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function applyOptimisticDelete(optimisticUpdates: Map<string, OptimisticUpdateEntry>, emitter: OptimisticEmitter, model: Model, transaction: Transaction): void;
|
|
44
|
+
/**
|
|
45
|
+
* Undo an optimistic mutation by its transaction id: emit `optimistic:rollback`
|
|
46
|
+
* with the saved pre-image so listeners can restore the prior value, then drop
|
|
47
|
+
* the ledger entry. Does nothing if the transaction was never tracked.
|
|
48
|
+
*/
|
|
49
|
+
export declare function rollbackOptimistic(optimisticUpdates: Map<string, OptimisticUpdateEntry>, emitter: OptimisticEmitter, transaction: Transaction, reason?: string, error?: Error): Promise<void>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-first apply and rollback bookkeeping for pending mutations.
|
|
3
|
+
*
|
|
4
|
+
* When a change is written before the server confirms it, these functions
|
|
5
|
+
* record it in a ledger and emit an `optimistic:*` event; separate listeners
|
|
6
|
+
* apply the change to the in-memory data and, on rollback, restore the value
|
|
7
|
+
* it replaced. Each function takes the ledger and a small event emitter rather
|
|
8
|
+
* than any larger object, so the rules have no dependencies of their own and
|
|
9
|
+
* can be tested in isolation.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Record an optimistic create and announce it. There is no prior value to
|
|
13
|
+
* restore, so the rollback pre-image is `null`.
|
|
14
|
+
*/
|
|
15
|
+
export function applyOptimisticCreate(optimisticUpdates, emitter, model, transaction) {
|
|
16
|
+
optimisticUpdates.set(transaction.id, {
|
|
17
|
+
model,
|
|
18
|
+
previousState: null,
|
|
19
|
+
transaction,
|
|
20
|
+
});
|
|
21
|
+
emitter.emit('optimistic:create', { model, transaction });
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Record an optimistic update and announce it, keeping the row's prior value so
|
|
25
|
+
* a rollback can put it back.
|
|
26
|
+
*/
|
|
27
|
+
export function applyOptimisticUpdate(optimisticUpdates, emitter, model, transaction) {
|
|
28
|
+
optimisticUpdates.set(transaction.id, {
|
|
29
|
+
model,
|
|
30
|
+
previousState: transaction.previousData,
|
|
31
|
+
transaction,
|
|
32
|
+
});
|
|
33
|
+
emitter.emit('optimistic:update', { model, transaction });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Record an optimistic delete and announce it, keeping the deleted row so a
|
|
37
|
+
* rollback can restore it.
|
|
38
|
+
*/
|
|
39
|
+
export function applyOptimisticDelete(optimisticUpdates, emitter, model, transaction) {
|
|
40
|
+
optimisticUpdates.set(transaction.id, {
|
|
41
|
+
model,
|
|
42
|
+
previousState: transaction.previousData,
|
|
43
|
+
transaction,
|
|
44
|
+
});
|
|
45
|
+
emitter.emit('optimistic:delete', { model, transaction });
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Undo an optimistic mutation by its transaction id: emit `optimistic:rollback`
|
|
49
|
+
* with the saved pre-image so listeners can restore the prior value, then drop
|
|
50
|
+
* the ledger entry. Does nothing if the transaction was never tracked.
|
|
51
|
+
*/
|
|
52
|
+
export function rollbackOptimistic(optimisticUpdates, emitter, transaction, reason, error) {
|
|
53
|
+
const optimistic = optimisticUpdates.get(transaction.id);
|
|
54
|
+
if (!optimistic)
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
emitter.emit('optimistic:rollback', {
|
|
57
|
+
model: optimistic.model,
|
|
58
|
+
previousState: optimistic.previousState,
|
|
59
|
+
transaction,
|
|
60
|
+
reason: reason ?? 'unknown',
|
|
61
|
+
error,
|
|
62
|
+
});
|
|
63
|
+
optimisticUpdates.delete(transaction.id);
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
}
|