@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
package/dist/client/ApiClient.js
CHANGED
|
@@ -1,20 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* nouns directly to HTTP routes on sync-server.
|
|
2
|
+
* The stateless API client behind `Ablo({ apiKey })`. It carries no schema,
|
|
3
|
+
* object pool, local database, or WebSocket, and maps the public Model, Claim,
|
|
4
|
+
* and Commit nouns directly to HTTP routes on the server. This is the transport
|
|
5
|
+
* used for server-side agents, workers, and serverless code.
|
|
7
6
|
*/
|
|
8
|
-
import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, AbloNotFoundError, claimedError, translateHttpError, } from '../errors.js';
|
|
7
|
+
import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloIdempotencyError, AbloValidationError, AbloNotFoundError, claimedError, translateHttpError, } from '../errors.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { v5 as uuidv5 } from 'uuid';
|
|
9
10
|
import { reconcileFunctionalUpdate, } from './functionalUpdate.js';
|
|
10
11
|
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfCliKeyMismatch, warnIfDatabaseUrlEnvIgnored, warnIfDatabaseUrlDeprecated, } from './auth.js';
|
|
11
12
|
import { registerDataSource } from './registerDataSource.js';
|
|
12
13
|
import { PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER } from '../wire/protocolVersion.js';
|
|
13
|
-
import { toSeconds } from '../utils/duration.js';
|
|
14
|
+
import { toMs, toSeconds } from '../utils/duration.js';
|
|
15
|
+
import { heartbeatCadenceMs, resolveHeartbeatOptions, startClaimHeartbeatLoop, } from './claimHeartbeatLoop.js';
|
|
14
16
|
import { mintSession } from './sessionMint.js';
|
|
17
|
+
import { parseIdentityResolveResponse } from '../auth/schemas.js';
|
|
18
|
+
/**
|
|
19
|
+
* Interpret a heartbeat reply for a lease this handle HOLDS: anything other
|
|
20
|
+
* than `held` means the lease is no longer ours (a holder cannot be `queued`;
|
|
21
|
+
* `lost` rides a 409 that the wire error mapping already surfaces as
|
|
22
|
+
* AbloClaimedError before reaching here). The thrown loss is the definitive
|
|
23
|
+
* signal that stops the auto-heartbeat loop.
|
|
24
|
+
*/
|
|
25
|
+
function heldHeartbeatReply(reply, label) {
|
|
26
|
+
if (reply.status === 'held' && typeof reply.expiresAt === 'number') {
|
|
27
|
+
return {
|
|
28
|
+
expiresAt: reply.expiresAt,
|
|
29
|
+
...(reply.queueDepth !== undefined ? { queueDepth: reply.queueDepth } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
throw new AbloClaimedError(`The lease behind ${label} is no longer held — it expired or was granted onward. Re-acquire the claim and retry; a write attempted under the old lease is rejected by its \`readAt\` guard.`, { code: 'claim_lost' });
|
|
33
|
+
}
|
|
15
34
|
import { assertWriteOptions } from './writeOptionsSchema.js';
|
|
35
|
+
import { createDurableHttpCommitEnvelope, canonicalHttpCommitBody, durableHttpCommitEnvelopeSchema, httpCommitEnvelopeRecordId, isHttpCommitReplayExpired, } from '../transactions/httpCommitEnvelope.js';
|
|
16
36
|
/** Default per-request deadline for the stateless HTTP transport. */
|
|
17
37
|
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
38
|
+
const successfulCommitResponseSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
object: z.literal('commit_receipt'),
|
|
41
|
+
id: z.string().min(1).optional(),
|
|
42
|
+
clientTxId: z.string().min(1),
|
|
43
|
+
serverTxId: z.string().min(1),
|
|
44
|
+
status: z.enum(['queued', 'confirmed']),
|
|
45
|
+
success: z.literal(true),
|
|
46
|
+
lastSyncId: z.number().int().nonnegative().optional(),
|
|
47
|
+
ops: z.number().int().positive(),
|
|
48
|
+
/** Ids of UPDATE/DELETE targets that matched zero rows (loud 0-row writes). */
|
|
49
|
+
missingIds: z.array(z.string().min(1)).optional(),
|
|
50
|
+
})
|
|
51
|
+
.loose();
|
|
52
|
+
function parseSuccessfulCommitResponse(value, idempotencyKey) {
|
|
53
|
+
const parsed = successfulCommitResponseSchema.safeParse(value);
|
|
54
|
+
if (!parsed.success || parsed.data.clientTxId !== idempotencyKey) {
|
|
55
|
+
throw new AbloConnectionError('The commit endpoint returned an invalid success receipt; its outcome remains pending and is safe to retry.', {
|
|
56
|
+
code: 'commit_no_result',
|
|
57
|
+
cause: parsed.success
|
|
58
|
+
? new Error('Commit receipt clientTxId did not match its idempotency key')
|
|
59
|
+
: parsed.error,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return parsed.data;
|
|
63
|
+
}
|
|
18
64
|
const DEFAULT_AGENT_LEASE = '10m';
|
|
19
65
|
export function createProtocolClient(options) {
|
|
20
66
|
const env = readProcessEnv();
|
|
@@ -32,11 +78,12 @@ export function createProtocolClient(options) {
|
|
|
32
78
|
databaseUrl: configuredDatabaseUrl,
|
|
33
79
|
dangerouslyAllowBrowser: options.dangerouslyAllowBrowser,
|
|
34
80
|
});
|
|
35
|
-
// Observability
|
|
36
|
-
// claim
|
|
81
|
+
// Observability hook for the stateless HTTP transport. The WebSocket transport
|
|
82
|
+
// emits claim and conflict events; the HTTP path (server-side agents,
|
|
37
83
|
// `transport: 'http'`) emitted nothing, so a `ClaimLog` handed to a headless
|
|
38
|
-
// agent
|
|
39
|
-
// coordination-conflict rejection.
|
|
84
|
+
// agent evaluation stayed empty. This mirrors the two WebSocket events here:
|
|
85
|
+
// claim acquired and coordination-conflict rejection. A no-op when no provider
|
|
86
|
+
// is configured.
|
|
40
87
|
const observability = options.observability;
|
|
41
88
|
// Shared by the two HTTP write doors (`commits.create` + per-model
|
|
42
89
|
// `mutateModel`): a rejected write whose code is a coordination conflict is
|
|
@@ -47,14 +94,17 @@ export function createProtocolClient(options) {
|
|
|
47
94
|
const recordCoordinationConflict = (error, clientTxId, fallbackRows) => {
|
|
48
95
|
if (!observability)
|
|
49
96
|
return;
|
|
50
|
-
const
|
|
97
|
+
const errorRecord = typeof error === 'object' && error !== null
|
|
98
|
+
? error
|
|
99
|
+
: undefined;
|
|
100
|
+
const code = errorRecord?.code;
|
|
51
101
|
const isConflict = code === 'stale_context' ||
|
|
52
102
|
code === 'claim_conflict' ||
|
|
53
103
|
code === 'entity_claimed' ||
|
|
54
104
|
(typeof code === 'string' && code.startsWith('policy:'));
|
|
55
105
|
if (!isConflict)
|
|
56
106
|
return;
|
|
57
|
-
const rawConflicts =
|
|
107
|
+
const rawConflicts = errorRecord?.conflicts;
|
|
58
108
|
const rows = Array.isArray(rawConflicts) && rawConflicts.length > 0
|
|
59
109
|
? rawConflicts.map((r) => ({
|
|
60
110
|
model: typeof r.model === 'string' ? r.model : 'unknown',
|
|
@@ -73,19 +123,54 @@ export function createProtocolClient(options) {
|
|
|
73
123
|
url,
|
|
74
124
|
bootstrapBaseUrl: options.bootstrapBaseUrl,
|
|
75
125
|
}).replace(/\/+$/, '');
|
|
126
|
+
const commitOutbox = options.commitOutbox;
|
|
127
|
+
const httpOutboxPlaneNamespace = canonicalHttpCommitBody({
|
|
128
|
+
apiBaseUrl,
|
|
129
|
+
defaultQuery: Object.entries(options.defaultQuery ?? {}).sort(([a], [b]) => a.localeCompare(b)),
|
|
130
|
+
});
|
|
131
|
+
let httpOutboxScopeNamespace = null;
|
|
76
132
|
let readyPromise = null;
|
|
133
|
+
let httpCommitLane = Promise.resolve();
|
|
134
|
+
function runInHttpCommitLane(work) {
|
|
135
|
+
const result = httpCommitLane.then(work);
|
|
136
|
+
httpCommitLane = result.then(() => undefined, () => undefined);
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
async function resolveHttpOutboxScope() {
|
|
140
|
+
if (!commitOutbox)
|
|
141
|
+
return null;
|
|
142
|
+
if (httpOutboxScopeNamespace)
|
|
143
|
+
return httpOutboxScopeNamespace;
|
|
144
|
+
let scope = options.commitOutboxScope;
|
|
145
|
+
if (!scope) {
|
|
146
|
+
const rawIdentity = await requestJson('/auth/identity', { method: 'GET' }, true);
|
|
147
|
+
const identity = parseIdentityResolveResponse(rawIdentity);
|
|
148
|
+
scope = {
|
|
149
|
+
organizationId: identity.accountScope,
|
|
150
|
+
participantId: identity.participantId,
|
|
151
|
+
namespace: 'http',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
httpOutboxScopeNamespace = canonicalHttpCommitBody({
|
|
155
|
+
...scope,
|
|
156
|
+
plane: httpOutboxPlaneNamespace,
|
|
157
|
+
});
|
|
158
|
+
return httpOutboxScopeNamespace;
|
|
159
|
+
}
|
|
77
160
|
async function ready() {
|
|
78
161
|
if (readyPromise)
|
|
79
162
|
return readyPromise;
|
|
80
163
|
readyPromise = (async () => {
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
164
|
+
if (configuredDatabaseUrl) {
|
|
165
|
+
await registerDataSource({
|
|
166
|
+
baseUrl: apiBaseUrl,
|
|
167
|
+
apiKey: await resolveApiKeyValue(configuredApiKey),
|
|
168
|
+
databaseUrl: configuredDatabaseUrl,
|
|
169
|
+
...(options.fetch ? { fetchImpl: options.fetch } : {}),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
await resolveHttpOutboxScope();
|
|
173
|
+
await replayHttpCommitOutbox();
|
|
89
174
|
})();
|
|
90
175
|
try {
|
|
91
176
|
await readyPromise;
|
|
@@ -127,8 +212,9 @@ export function createProtocolClient(options) {
|
|
|
127
212
|
return target.toString();
|
|
128
213
|
}
|
|
129
214
|
const requestTimeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
130
|
-
async function requestJson(path, init) {
|
|
131
|
-
|
|
215
|
+
async function requestJson(path, init, skipReady = false) {
|
|
216
|
+
if (!skipReady)
|
|
217
|
+
await ready();
|
|
132
218
|
const { idempotencyKey, ...requestInit } = init;
|
|
133
219
|
const headers = await authHeaders();
|
|
134
220
|
if (idempotencyKey)
|
|
@@ -139,7 +225,7 @@ export function createProtocolClient(options) {
|
|
|
139
225
|
// is combined with the deadline via a shared controller — the portable
|
|
140
226
|
// equivalent of `AbortSignal.any([caller, AbortSignal.timeout(t)])`,
|
|
141
227
|
// which older runtimes (and the jsdom test env) don't implement. The
|
|
142
|
-
// same pattern already guards `query/client.ts` and `
|
|
228
|
+
// same pattern already guards `query/client.ts` and `BootstrapFetcher`.
|
|
143
229
|
const callerSignal = requestInit.signal ?? undefined;
|
|
144
230
|
const controller = new AbortController();
|
|
145
231
|
const onCallerAbort = () => { controller.abort(callerSignal?.reason); };
|
|
@@ -172,6 +258,7 @@ export function createProtocolClient(options) {
|
|
|
172
258
|
bodyText = await res.text();
|
|
173
259
|
}
|
|
174
260
|
catch (error) {
|
|
261
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- assigned asynchronously by the deadline callback
|
|
175
262
|
if (timedOut) {
|
|
176
263
|
// Retryable by contract: `wait_for_timeout` is a registered transient
|
|
177
264
|
// transport code, so `isRetryableCode` steers callers to retry.
|
|
@@ -192,6 +279,169 @@ export function createProtocolClient(options) {
|
|
|
192
279
|
}
|
|
193
280
|
return body;
|
|
194
281
|
}
|
|
282
|
+
function isDefinitiveHttpRejection(error) {
|
|
283
|
+
if (typeof error !== 'object' || error === null)
|
|
284
|
+
return false;
|
|
285
|
+
const candidate = error;
|
|
286
|
+
const status = typeof candidate.httpStatus === 'number'
|
|
287
|
+
? candidate.httpStatus
|
|
288
|
+
: typeof candidate.status === 'number'
|
|
289
|
+
? candidate.status
|
|
290
|
+
: undefined;
|
|
291
|
+
return (status !== undefined &&
|
|
292
|
+
status >= 400 &&
|
|
293
|
+
status < 500 &&
|
|
294
|
+
status !== 408 &&
|
|
295
|
+
status !== 425 &&
|
|
296
|
+
status !== 429);
|
|
297
|
+
}
|
|
298
|
+
async function settleHttpEnvelope(recordId) {
|
|
299
|
+
if (!commitOutbox)
|
|
300
|
+
return;
|
|
301
|
+
try {
|
|
302
|
+
await commitOutbox.remove(recordId);
|
|
303
|
+
}
|
|
304
|
+
catch (cause) {
|
|
305
|
+
// Do not report the remote outcome until local settlement is durable.
|
|
306
|
+
// The retained record can still be replayed inside the safe window.
|
|
307
|
+
throw new AbloConnectionError('The server settled the commit, but its local outbox record could not be cleared.', { code: 'db_not_opened', cause });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function replayHttpCommitOutbox() {
|
|
311
|
+
const replayed = new Map();
|
|
312
|
+
if (!commitOutbox)
|
|
313
|
+
return replayed;
|
|
314
|
+
const scopeNamespace = await resolveHttpOutboxScope();
|
|
315
|
+
if (!scopeNamespace)
|
|
316
|
+
return replayed;
|
|
317
|
+
const rows = await commitOutbox.list();
|
|
318
|
+
const envelopes = [];
|
|
319
|
+
for (const row of rows) {
|
|
320
|
+
if (typeof row !== 'object' ||
|
|
321
|
+
row === null ||
|
|
322
|
+
row.type !== 'http_commit_envelope')
|
|
323
|
+
continue;
|
|
324
|
+
const parsed = durableHttpCommitEnvelopeSchema.safeParse(row);
|
|
325
|
+
if (!parsed.success) {
|
|
326
|
+
throw new AbloValidationError('A saved HTTP write is unreadable; replay stopped before any newer write was sent.', { code: 'write_options_invalid', cause: parsed.error });
|
|
327
|
+
}
|
|
328
|
+
if (parsed.data.scopeNamespace !== scopeNamespace)
|
|
329
|
+
continue;
|
|
330
|
+
if (isHttpCommitReplayExpired(parsed.data)) {
|
|
331
|
+
throw new AbloIdempotencyError('A saved HTTP write is older than the server idempotency window and cannot be replayed safely.', { code: 'idempotency_conflict' });
|
|
332
|
+
}
|
|
333
|
+
envelopes.push(parsed.data);
|
|
334
|
+
}
|
|
335
|
+
envelopes.sort((a, b) => (a.sequence ?? a.sealedAt * 1_000) -
|
|
336
|
+
(b.sequence ?? b.sealedAt * 1_000) ||
|
|
337
|
+
a.id.localeCompare(b.id));
|
|
338
|
+
for (const envelope of envelopes) {
|
|
339
|
+
try {
|
|
340
|
+
const raw = await requestJson(envelope.request.path, {
|
|
341
|
+
method: envelope.request.method,
|
|
342
|
+
idempotencyKey: envelope.idempotencyKey,
|
|
343
|
+
body: envelope.request.body,
|
|
344
|
+
}, true);
|
|
345
|
+
const response = parseSuccessfulCommitResponse(raw, envelope.idempotencyKey);
|
|
346
|
+
await settleHttpEnvelope(envelope.id);
|
|
347
|
+
replayed.set(envelope.idempotencyKey, { envelope, response });
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (isDefinitiveHttpRejection(error)) {
|
|
351
|
+
await settleHttpEnvelope(envelope.id);
|
|
352
|
+
}
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return replayed;
|
|
357
|
+
}
|
|
358
|
+
let lastHttpCommitSequence = 0;
|
|
359
|
+
function nextHttpCommitSequence() {
|
|
360
|
+
const wallSequence = Date.now() * 1_000;
|
|
361
|
+
lastHttpCommitSequence = Math.max(wallSequence, lastHttpCommitSequence + 1);
|
|
362
|
+
return lastHttpCommitSequence;
|
|
363
|
+
}
|
|
364
|
+
async function sealHttpCommit(input) {
|
|
365
|
+
if (!commitOutbox)
|
|
366
|
+
return null;
|
|
367
|
+
const scopeNamespace = await resolveHttpOutboxScope();
|
|
368
|
+
if (!scopeNamespace) {
|
|
369
|
+
throw new AbloValidationError('HTTP commit outbox scope was not resolved', {
|
|
370
|
+
code: 'write_options_invalid',
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const recordId = httpCommitEnvelopeRecordId(input.idempotencyKey, scopeNamespace);
|
|
374
|
+
const legacyRecordId = httpCommitEnvelopeRecordId(input.idempotencyKey);
|
|
375
|
+
const existingRows = await commitOutbox.list();
|
|
376
|
+
const existingRaw = existingRows.find((row) => typeof row === 'object' &&
|
|
377
|
+
row !== null &&
|
|
378
|
+
(row.id === recordId ||
|
|
379
|
+
row.id === legacyRecordId));
|
|
380
|
+
const serializedBody = canonicalHttpCommitBody(input.body);
|
|
381
|
+
if (existingRaw !== undefined) {
|
|
382
|
+
const existing = durableHttpCommitEnvelopeSchema.parse(existingRaw);
|
|
383
|
+
if (isHttpCommitReplayExpired(existing)) {
|
|
384
|
+
throw new AbloIdempotencyError('This saved HTTP write is older than the server idempotency window and cannot be retried safely.', { code: 'idempotency_conflict' });
|
|
385
|
+
}
|
|
386
|
+
if (existing.scopeNamespace !== scopeNamespace ||
|
|
387
|
+
existing.request.method !== input.method ||
|
|
388
|
+
existing.request.path !== input.path ||
|
|
389
|
+
existing.request.body !== serializedBody) {
|
|
390
|
+
throw new AbloIdempotencyError('Idempotency key reused with a different HTTP commit request', { code: 'idempotency_conflict' });
|
|
391
|
+
}
|
|
392
|
+
return existing;
|
|
393
|
+
}
|
|
394
|
+
const envelope = createDurableHttpCommitEnvelope({
|
|
395
|
+
idempotencyKey: input.idempotencyKey,
|
|
396
|
+
request: { method: input.method, path: input.path, body: input.body },
|
|
397
|
+
scopeNamespace,
|
|
398
|
+
sequence: nextHttpCommitSequence(),
|
|
399
|
+
});
|
|
400
|
+
await commitOutbox.seal(envelope, []);
|
|
401
|
+
return envelope;
|
|
402
|
+
}
|
|
403
|
+
async function dispatchHttpCommit(input, beforeSettlement) {
|
|
404
|
+
return runInHttpCommitLane(async () => {
|
|
405
|
+
await ready();
|
|
406
|
+
// `ready()` covers startup. Re-draining here makes every later write wait
|
|
407
|
+
// behind an ambiguous predecessor from this same process.
|
|
408
|
+
const replayed = await replayHttpCommitOutbox();
|
|
409
|
+
const prior = replayed.get(input.idempotencyKey);
|
|
410
|
+
if (prior) {
|
|
411
|
+
const serializedBody = canonicalHttpCommitBody(input.body);
|
|
412
|
+
if (prior.envelope.request.method !== input.method ||
|
|
413
|
+
prior.envelope.request.path !== input.path ||
|
|
414
|
+
prior.envelope.request.body !== serializedBody) {
|
|
415
|
+
throw new AbloIdempotencyError('Idempotency key reused with a different HTTP commit request', { code: 'idempotency_conflict' });
|
|
416
|
+
}
|
|
417
|
+
await beforeSettlement?.(prior.response);
|
|
418
|
+
return prior.response;
|
|
419
|
+
}
|
|
420
|
+
const durableEnvelope = await sealHttpCommit(input);
|
|
421
|
+
const requestBody = durableEnvelope?.request.body ?? canonicalHttpCommitBody(input.body);
|
|
422
|
+
let response;
|
|
423
|
+
try {
|
|
424
|
+
const raw = await requestJson(input.path, {
|
|
425
|
+
method: input.method,
|
|
426
|
+
idempotencyKey: input.idempotencyKey,
|
|
427
|
+
body: requestBody,
|
|
428
|
+
}, true);
|
|
429
|
+
response = parseSuccessfulCommitResponse(raw, input.idempotencyKey);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
if (durableEnvelope && isDefinitiveHttpRejection(error)) {
|
|
433
|
+
await settleHttpEnvelope(durableEnvelope.id);
|
|
434
|
+
}
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
// A model-create readback can participate in settlement: if it fails,
|
|
438
|
+
// retain the exact write so a same-key retry recovers the generated id.
|
|
439
|
+
await beforeSettlement?.(response);
|
|
440
|
+
if (durableEnvelope)
|
|
441
|
+
await settleHttpEnvelope(durableEnvelope.id);
|
|
442
|
+
return response;
|
|
443
|
+
});
|
|
444
|
+
}
|
|
195
445
|
function createClientTxId(idempotencyKey) {
|
|
196
446
|
if (idempotencyKey && idempotencyKey.length > 0)
|
|
197
447
|
return idempotencyKey;
|
|
@@ -204,17 +454,28 @@ export function createProtocolClient(options) {
|
|
|
204
454
|
? `int_${crypto.randomUUID()}`
|
|
205
455
|
: `int_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
206
456
|
}
|
|
207
|
-
function createModelId() {
|
|
457
|
+
function createModelId(modelName, idempotencyKey) {
|
|
458
|
+
if (idempotencyKey) {
|
|
459
|
+
return uuidv5(`${modelName}:${idempotencyKey}`, 'aa4ba6d4-bf0b-5b38-9c45-116f79a6e548');
|
|
460
|
+
}
|
|
208
461
|
return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
209
462
|
? crypto.randomUUID()
|
|
210
463
|
: `id_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
211
464
|
}
|
|
212
|
-
function childClient(authToken) {
|
|
465
|
+
function childClient(authToken, scope) {
|
|
213
466
|
return createProtocolClient({
|
|
214
467
|
...options,
|
|
215
468
|
apiKey: null,
|
|
216
469
|
authToken,
|
|
217
470
|
schema: null,
|
|
471
|
+
...(scope
|
|
472
|
+
? {
|
|
473
|
+
commitOutboxScope: {
|
|
474
|
+
...scope,
|
|
475
|
+
namespace: options.commitOutboxScope?.namespace ?? 'http',
|
|
476
|
+
},
|
|
477
|
+
}
|
|
478
|
+
: { commitOutboxScope: undefined }),
|
|
218
479
|
});
|
|
219
480
|
}
|
|
220
481
|
function normalizeCommitOperation(op, defaults) {
|
|
@@ -341,17 +602,20 @@ export function createProtocolClient(options) {
|
|
|
341
602
|
readAt: commitOptions.readAt ?? claim?.readAt ?? null,
|
|
342
603
|
onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
|
|
343
604
|
});
|
|
605
|
+
const requestBody = {
|
|
606
|
+
clientTxId,
|
|
607
|
+
idempotencyKey: clientTxId,
|
|
608
|
+
claim: normalizeClaimId(commitOptions.claimRef) ?? claim?.id,
|
|
609
|
+
operations,
|
|
610
|
+
reads: commitOptions.reads,
|
|
611
|
+
};
|
|
344
612
|
let body;
|
|
345
613
|
try {
|
|
346
|
-
body = await
|
|
614
|
+
body = await dispatchHttpCommit({
|
|
615
|
+
path: '/v1/commits',
|
|
347
616
|
method: 'POST',
|
|
348
617
|
idempotencyKey: clientTxId,
|
|
349
|
-
body:
|
|
350
|
-
clientTxId,
|
|
351
|
-
idempotencyKey: clientTxId,
|
|
352
|
-
claim: normalizeClaimId(commitOptions.claimRef) ?? claim?.id,
|
|
353
|
-
operations,
|
|
354
|
-
}),
|
|
618
|
+
body: requestBody,
|
|
355
619
|
});
|
|
356
620
|
}
|
|
357
621
|
catch (error) {
|
|
@@ -370,7 +634,7 @@ export function createProtocolClient(options) {
|
|
|
370
634
|
// the rejection body (already thrown).
|
|
371
635
|
const status = body.status === 'queued' ? 'queued' : 'confirmed';
|
|
372
636
|
return {
|
|
373
|
-
id: body.id ?? body.clientTxId
|
|
637
|
+
id: body.id ?? body.clientTxId,
|
|
374
638
|
status,
|
|
375
639
|
lastSyncId: body.lastSyncId,
|
|
376
640
|
...(body.missingIds && body.missingIds.length > 0
|
|
@@ -381,9 +645,9 @@ export function createProtocolClient(options) {
|
|
|
381
645
|
};
|
|
382
646
|
const capabilities = {
|
|
383
647
|
async create(capabilityOptions) {
|
|
384
|
-
const ttlSeconds = capabilityOptions.ttlSeconds ??
|
|
648
|
+
const ttlSeconds = capabilityOptions.ttlSeconds ?? // eslint-disable-line @typescript-eslint/no-deprecated -- compatibility alias
|
|
385
649
|
capabilityOptions.leaseSeconds ??
|
|
386
|
-
toSeconds(capabilityOptions.ttl ?? capabilityOptions.lease ?? DEFAULT_AGENT_LEASE);
|
|
650
|
+
toSeconds(capabilityOptions.ttl ?? capabilityOptions.lease ?? DEFAULT_AGENT_LEASE); // eslint-disable-line @typescript-eslint/no-deprecated -- compatibility alias
|
|
387
651
|
const body = await requestJson('/v1/capabilities', {
|
|
388
652
|
method: 'POST',
|
|
389
653
|
body: JSON.stringify({
|
|
@@ -408,7 +672,10 @@ export function createProtocolClient(options) {
|
|
|
408
672
|
organizationId: body.organizationId,
|
|
409
673
|
scope: body.scope,
|
|
410
674
|
userMeta: body.userMeta,
|
|
411
|
-
client: () => childClient(body.token
|
|
675
|
+
client: () => childClient(body.token, {
|
|
676
|
+
organizationId: body.organizationId,
|
|
677
|
+
participantId: body.scope.participantId,
|
|
678
|
+
}),
|
|
412
679
|
};
|
|
413
680
|
},
|
|
414
681
|
async retrieve(id) {
|
|
@@ -462,7 +729,10 @@ export function createProtocolClient(options) {
|
|
|
462
729
|
id: body.rotatedFrom.capabilityId ?? body.rotatedFrom.id ?? id,
|
|
463
730
|
expiresAt: body.rotatedFrom.expiresAt,
|
|
464
731
|
},
|
|
465
|
-
client: () => childClient(body.token
|
|
732
|
+
client: () => childClient(body.token, {
|
|
733
|
+
organizationId: body.organizationId,
|
|
734
|
+
participantId: body.scope.participantId,
|
|
735
|
+
}),
|
|
466
736
|
};
|
|
467
737
|
},
|
|
468
738
|
mint(options) {
|
|
@@ -482,11 +752,11 @@ export function createProtocolClient(options) {
|
|
|
482
752
|
queue: claimOptions.queue,
|
|
483
753
|
}),
|
|
484
754
|
});
|
|
485
|
-
// The fair-queue grant is
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
755
|
+
// The fair-queue grant is pushed over a WebSocket (`claim_granted`), which
|
|
756
|
+
// this stateless HTTP client doesn't hold. Returning a handle here would be
|
|
757
|
+
// a phantom holder — a lease we can't confirm is ours. So a queued response
|
|
758
|
+
// is surfaced as a typed claimed signal; callers that need to wait in line
|
|
759
|
+
// use the realtime (WebSocket-backed) `ablo.<model>.claim`.
|
|
490
760
|
if (body.status === 'queued') {
|
|
491
761
|
throw new AbloClaimedError(`Target ${claimOptions.target.model}/${claimOptions.target.id} is held; ` +
|
|
492
762
|
`queued at position ${body.position ?? 0}. The HTTP client can't await ` +
|
|
@@ -500,9 +770,24 @@ export function createProtocolClient(options) {
|
|
|
500
770
|
released = true;
|
|
501
771
|
await requestJson(`/v1/claims/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
502
772
|
};
|
|
773
|
+
// The by-id twin of the model-scoped heartbeat — same reply contract.
|
|
774
|
+
const heartbeat = async (beatOptions) => {
|
|
775
|
+
const resolved = resolveHeartbeatOptions(beatOptions);
|
|
776
|
+
const reply = await requestJson(`/v1/claims/${encodeURIComponent(id)}/heartbeat`, {
|
|
777
|
+
method: 'POST',
|
|
778
|
+
body: JSON.stringify({
|
|
779
|
+
...(resolved.ttl !== undefined ? { ttl: resolved.ttl } : {}),
|
|
780
|
+
...(resolved.details !== undefined
|
|
781
|
+
? { details: resolved.details }
|
|
782
|
+
: {}),
|
|
783
|
+
}),
|
|
784
|
+
});
|
|
785
|
+
return heldHeartbeatReply(reply, `claim ${id}`);
|
|
786
|
+
};
|
|
503
787
|
return {
|
|
504
788
|
object: 'claim',
|
|
505
789
|
id,
|
|
790
|
+
heartbeat,
|
|
506
791
|
reason: claimOptions.reason,
|
|
507
792
|
target: {
|
|
508
793
|
type: claimOptions.target.model,
|
|
@@ -523,6 +808,23 @@ export function createProtocolClient(options) {
|
|
|
523
808
|
waitFor(target, options) {
|
|
524
809
|
return waitForNoClaims(target, options);
|
|
525
810
|
},
|
|
811
|
+
async heartbeatAll(options) {
|
|
812
|
+
const reply = await requestJson('/v1/claims/heartbeat', {
|
|
813
|
+
method: 'POST',
|
|
814
|
+
body: JSON.stringify(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
|
|
815
|
+
});
|
|
816
|
+
return (reply.results ?? []).flatMap((entry) => typeof entry.claimId === 'string' && typeof entry.expiresAt === 'number'
|
|
817
|
+
? [
|
|
818
|
+
{
|
|
819
|
+
claimId: entry.claimId,
|
|
820
|
+
expiresAt: entry.expiresAt,
|
|
821
|
+
...(entry.queueDepth !== undefined
|
|
822
|
+
? { queueDepth: entry.queueDepth }
|
|
823
|
+
: {}),
|
|
824
|
+
},
|
|
825
|
+
]
|
|
826
|
+
: []);
|
|
827
|
+
},
|
|
526
828
|
};
|
|
527
829
|
async function listModel(modelName, options) {
|
|
528
830
|
const params = new URLSearchParams();
|
|
@@ -554,11 +856,11 @@ export function createProtocolClient(options) {
|
|
|
554
856
|
const query = await requestJson(`/v1/models/${encodeURIComponent(modelName)}/${encodeURIComponent(params.id)}`, {
|
|
555
857
|
method: 'GET',
|
|
556
858
|
});
|
|
557
|
-
//
|
|
859
|
+
// A miss is `data: undefined`, not a thrown error. The WebSocket client's
|
|
558
860
|
// `retrieve` returns `T | undefined` for a missing row; throwing only here
|
|
559
861
|
// made the obvious read ("does this row exist?") a hard edge that an agent
|
|
560
|
-
// had to wrap in try/catch. Both transports
|
|
561
|
-
// data. Callers branch on `.data` (
|
|
862
|
+
// had to wrap in try/catch. Both transports agree: an absent row means absent
|
|
863
|
+
// data. Callers branch on `.data` (the documented `.data?.x` usage).
|
|
562
864
|
// Normalize a miss to `undefined` (the server may send `null` or omit it).
|
|
563
865
|
const data = (query.data ?? undefined);
|
|
564
866
|
return {
|
|
@@ -568,18 +870,18 @@ export function createProtocolClient(options) {
|
|
|
568
870
|
};
|
|
569
871
|
}
|
|
570
872
|
/**
|
|
571
|
-
*
|
|
572
|
-
* that mirrors `ablo.<model>.create/update/delete`:
|
|
873
|
+
* A single-operation mutation over the model-scoped routes — the canonical
|
|
874
|
+
* surface that mirrors `ablo.<model>.create/update/delete`:
|
|
573
875
|
*
|
|
574
876
|
* POST /v1/models/:model create
|
|
575
877
|
* PATCH /v1/models/:model/:id update
|
|
576
878
|
* DELETE /v1/models/:model/:id delete
|
|
577
879
|
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
880
|
+
* The `commits.create(...)` resource remains the path for atomic
|
|
881
|
+
* multi-operation envelopes; this helper handles the one-operation,
|
|
882
|
+
* one-record case.
|
|
581
883
|
*/
|
|
582
|
-
async function mutateModel(action, modelName, id, data, options) {
|
|
884
|
+
async function mutateModel(action, modelName, id, data, options, beforeSettlement) {
|
|
583
885
|
assertWriteOptions(options && {
|
|
584
886
|
idempotencyKey: options.idempotencyKey,
|
|
585
887
|
readAt: options.readAt,
|
|
@@ -595,11 +897,12 @@ export function createProtocolClient(options) {
|
|
|
595
897
|
const method = action === 'create' ? 'POST' : action === 'update' ? 'PATCH' : 'DELETE';
|
|
596
898
|
// A carried claim handle supplies the stale-guard defaults — one claim
|
|
597
899
|
// vocabulary across the WS proxy, `commits.create`, and these routes.
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
900
|
+
const rawClaim = options?.claim;
|
|
901
|
+
const claimHandle = typeof rawClaim === 'object' &&
|
|
902
|
+
rawClaim !== null &&
|
|
903
|
+
rawClaim.object === 'claim' &&
|
|
904
|
+
typeof rawClaim.id === 'string'
|
|
905
|
+
? rawClaim
|
|
603
906
|
: undefined;
|
|
604
907
|
const readAt = options?.readAt ?? claimHandle?.readAt;
|
|
605
908
|
const requestBody = {
|
|
@@ -614,16 +917,16 @@ export function createProtocolClient(options) {
|
|
|
614
917
|
requestBody.data = data;
|
|
615
918
|
let body;
|
|
616
919
|
try {
|
|
617
|
-
body = await
|
|
920
|
+
body = await dispatchHttpCommit({
|
|
921
|
+
path,
|
|
618
922
|
method,
|
|
619
923
|
idempotencyKey: clientTxId,
|
|
620
|
-
body:
|
|
621
|
-
});
|
|
924
|
+
body: requestBody,
|
|
925
|
+
}, beforeSettlement);
|
|
622
926
|
}
|
|
623
927
|
catch (error) {
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
// collisions here too; this single row IS the fallback target.
|
|
928
|
+
// The per-model write door (`ablo.<model>.update/create/delete`). Capture
|
|
929
|
+
// coordination collisions here too; this single row is the fallback target.
|
|
627
930
|
recordCoordinationConflict(error, clientTxId, [{ model: modelName, id }]);
|
|
628
931
|
throw error;
|
|
629
932
|
}
|
|
@@ -632,7 +935,7 @@ export function createProtocolClient(options) {
|
|
|
632
935
|
// subset; `'rejected'` only appears on a thrown rejection body.
|
|
633
936
|
const status = body.status === 'queued' ? 'queued' : 'confirmed';
|
|
634
937
|
return {
|
|
635
|
-
id: body.serverTxId
|
|
938
|
+
id: body.serverTxId,
|
|
636
939
|
status,
|
|
637
940
|
lastSyncId: body.lastSyncId,
|
|
638
941
|
};
|
|
@@ -669,11 +972,25 @@ export function createProtocolClient(options) {
|
|
|
669
972
|
throw new AbloClaimedError(`Target ${name}/${params.id} is held; queued at position ${body.position ?? 0}. ` +
|
|
670
973
|
`The HTTP client cannot await the grant without a WebSocket.`, { code: 'claim_queued' });
|
|
671
974
|
}
|
|
672
|
-
// `claimId` is the queued
|
|
673
|
-
//
|
|
975
|
+
// `claimId` is the field name the queued response uses; check it alongside
|
|
976
|
+
// the other id shapes the response may carry.
|
|
674
977
|
return body.claim?.id ?? body.id ?? body.claimId ?? createClaimId();
|
|
675
978
|
};
|
|
676
979
|
const releaseClaim = (params) => requestJson(claimPath(isClaimHandle(params) ? params.target.id : params.id), { method: 'DELETE' }).then(() => undefined);
|
|
980
|
+
// One beat on the held lease. A lapsed lease answers `claim_lost`
|
|
981
|
+
// (409), which the wire error mapping surfaces as AbloClaimedError —
|
|
982
|
+
// the definitive signal that stops the auto-heartbeat loop.
|
|
983
|
+
const heartbeatClaim = async (id, claimId, options) => {
|
|
984
|
+
const reply = await requestJson(`${claimPath(id)}/heartbeat`, {
|
|
985
|
+
method: 'POST',
|
|
986
|
+
body: JSON.stringify({
|
|
987
|
+
claimId,
|
|
988
|
+
...(options.ttl !== undefined ? { ttl: options.ttl } : {}),
|
|
989
|
+
...(options.details !== undefined ? { details: options.details } : {}),
|
|
990
|
+
}),
|
|
991
|
+
});
|
|
992
|
+
return heldHeartbeatReply(reply, `claim ${claimId} on ${name}/${id}`);
|
|
993
|
+
};
|
|
677
994
|
async function claimImpl(params) {
|
|
678
995
|
const claimId = await acquireClaim(params);
|
|
679
996
|
observability?.captureClaim({
|
|
@@ -691,7 +1008,33 @@ export function createProtocolClient(options) {
|
|
|
691
1008
|
if (data === undefined) {
|
|
692
1009
|
throw new AbloNotFoundError(`Cannot claim ${name}/${params.id}: it does not exist (or is outside this credential's scope).`, [params.id]);
|
|
693
1010
|
}
|
|
694
|
-
const
|
|
1011
|
+
const heartbeat = async (beatOptions) => {
|
|
1012
|
+
const resolved = resolveHeartbeatOptions(beatOptions);
|
|
1013
|
+
const beat = await heartbeatClaim(params.id, claimId, {
|
|
1014
|
+
ttl: resolved.ttl ?? params.ttl,
|
|
1015
|
+
...(resolved.details !== undefined
|
|
1016
|
+
? { details: resolved.details }
|
|
1017
|
+
: {}),
|
|
1018
|
+
});
|
|
1019
|
+
params.onHeartbeat?.(beat);
|
|
1020
|
+
return beat;
|
|
1021
|
+
};
|
|
1022
|
+
// Opt-in auto-heartbeat — the background-worker cadence. The stateless
|
|
1023
|
+
// HTTP claim defaults to the server's 60s acquire window when no TTL
|
|
1024
|
+
// was requested, so the default cadence lands at 20s beats.
|
|
1025
|
+
const stopHeartbeatLoop = params.heartbeat
|
|
1026
|
+
? startClaimHeartbeatLoop({
|
|
1027
|
+
beat: () => heartbeat(),
|
|
1028
|
+
intervalMs: heartbeatCadenceMs(params.ttl !== undefined ? toMs(params.ttl) : 60_000, params.heartbeat),
|
|
1029
|
+
...(params.onHeartbeatLost
|
|
1030
|
+
? { onLost: params.onHeartbeatLost }
|
|
1031
|
+
: {}),
|
|
1032
|
+
})
|
|
1033
|
+
: undefined;
|
|
1034
|
+
const release = () => {
|
|
1035
|
+
stopHeartbeatLoop?.();
|
|
1036
|
+
return releaseClaim(params);
|
|
1037
|
+
};
|
|
695
1038
|
return {
|
|
696
1039
|
object: 'claim',
|
|
697
1040
|
id: claimId,
|
|
@@ -711,6 +1054,7 @@ export function createProtocolClient(options) {
|
|
|
711
1054
|
revoke: () => {
|
|
712
1055
|
void release().catch(() => { });
|
|
713
1056
|
},
|
|
1057
|
+
heartbeat,
|
|
714
1058
|
[Symbol.asyncDispose]: release,
|
|
715
1059
|
};
|
|
716
1060
|
}
|
|
@@ -729,8 +1073,8 @@ export function createProtocolClient(options) {
|
|
|
729
1073
|
reorder: async (params) => {
|
|
730
1074
|
await requestJson(`${claimPath(params.id)}/reorder`, {
|
|
731
1075
|
method: 'POST',
|
|
732
|
-
// The reorder route's payload is `{ heldBy, claimId }[]` — Claim's id
|
|
733
|
-
//
|
|
1076
|
+
// The reorder route's payload is `{ heldBy, claimId }[]` — a Claim's id
|
|
1077
|
+
// is the claimId.
|
|
734
1078
|
body: JSON.stringify({ order: params.order.map((i) => ({ heldBy: i.heldBy, claimId: i.id })) }),
|
|
735
1079
|
});
|
|
736
1080
|
},
|
|
@@ -742,7 +1086,13 @@ export function createProtocolClient(options) {
|
|
|
742
1086
|
if (isClaimHandle(claimInput)) {
|
|
743
1087
|
return run({ ...input, claimRef: { id: claimInput.id }, claim: undefined });
|
|
744
1088
|
}
|
|
745
|
-
|
|
1089
|
+
// `isClaimHandle` ruled out the handle form above; the generic mismatch
|
|
1090
|
+
// (the union carries `Claim`, the guard narrows `Claim<T>`) keeps the
|
|
1091
|
+
// compiler from subtracting it, so narrow to the inline-options form.
|
|
1092
|
+
const claimId = await acquireClaim({
|
|
1093
|
+
id,
|
|
1094
|
+
...claimInput,
|
|
1095
|
+
});
|
|
746
1096
|
try {
|
|
747
1097
|
return await run({ ...input, claimRef: { id: claimId }, claim: undefined });
|
|
748
1098
|
}
|
|
@@ -790,23 +1140,31 @@ export function createProtocolClient(options) {
|
|
|
790
1140
|
return listModel(name, options);
|
|
791
1141
|
},
|
|
792
1142
|
async create(params) {
|
|
793
|
-
const id = params.id ?? createModelId();
|
|
1143
|
+
const id = params.id ?? createModelId(name, params.idempotencyKey);
|
|
794
1144
|
return withMutationClaim(id, params, async (options) => {
|
|
795
1145
|
await applyClaimedPolicy({ model: name, id }, options);
|
|
796
1146
|
// Confirm the write, then return the row — the obvious expectation of
|
|
797
1147
|
// "create" (the WebSocket client already returns the row). The read-
|
|
798
1148
|
// back is the authoritative server row, so it carries the framework
|
|
799
|
-
// defaults (createdAt
|
|
800
|
-
// an existing id, the
|
|
1149
|
+
// defaults (createdAt, createdBy, …) and, for an idempotent re-create of
|
|
1150
|
+
// an existing id, the existing row rather than the caller's input.
|
|
1151
|
+
let created;
|
|
801
1152
|
await mutateModel('create', name, id, params.data, {
|
|
802
1153
|
...options,
|
|
803
1154
|
wait: options?.wait ?? 'confirmed',
|
|
1155
|
+
}, async () => {
|
|
1156
|
+
const read = await retrieveModel(name, { id });
|
|
1157
|
+
if (read.data === undefined) {
|
|
1158
|
+
throw new AbloNotFoundError(`create ${name}/${id} did not yield a readable row (the write did not confirm).`, [id]);
|
|
1159
|
+
}
|
|
1160
|
+
created = read.data;
|
|
804
1161
|
});
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1162
|
+
if (created === undefined) {
|
|
1163
|
+
throw new AbloConnectionError('Create settlement did not return its row.', {
|
|
1164
|
+
code: 'commit_no_result',
|
|
1165
|
+
});
|
|
808
1166
|
}
|
|
809
|
-
return
|
|
1167
|
+
return created;
|
|
810
1168
|
});
|
|
811
1169
|
},
|
|
812
1170
|
update: updateModel,
|
|
@@ -820,7 +1178,10 @@ export function createProtocolClient(options) {
|
|
|
820
1178
|
}
|
|
821
1179
|
return {
|
|
822
1180
|
ready,
|
|
823
|
-
async
|
|
1181
|
+
waitForFlush: () => runInHttpCommitLane(async () => {
|
|
1182
|
+
await ready();
|
|
1183
|
+
await replayHttpCommitOutbox();
|
|
1184
|
+
}),
|
|
824
1185
|
async dispose() { },
|
|
825
1186
|
async purge() { },
|
|
826
1187
|
capabilities,
|
|
@@ -829,9 +1190,9 @@ export function createProtocolClient(options) {
|
|
|
829
1190
|
model,
|
|
830
1191
|
sessions: {
|
|
831
1192
|
async create(params) {
|
|
832
|
-
// Stateless mint: the configured key
|
|
833
|
-
// (no startup `rk_` exchange runs on this client).
|
|
834
|
-
// URL
|
|
1193
|
+
// Stateless mint: the configured key is the control-plane credential here
|
|
1194
|
+
// (no startup `rk_` exchange runs on this client). It reuses the resolved
|
|
1195
|
+
// base URL and fetch; the shared `mintSession` handles the two server routes.
|
|
835
1196
|
const apiKey = await resolveApiKeyValue(configuredApiKey);
|
|
836
1197
|
if (!apiKey) {
|
|
837
1198
|
throw new AbloAuthenticationError('sessions.create requires a secret (sk_) API key — call it from your backend, not the browser.', { code: 'apikey_missing' });
|