@abloatai/ablo 0.25.0 → 0.27.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/AGENTS.md +5 -3
- package/CHANGELOG.md +34 -0
- package/README.md +104 -88
- package/dist/BaseSyncedStore.d.ts +140 -266
- package/dist/BaseSyncedStore.js +338 -739
- package/dist/Database.d.ts +62 -77
- package/dist/Database.js +106 -127
- package/dist/{ObjectPool.d.ts → InstanceCache.d.ts} +10 -13
- package/dist/{ObjectPool.js → InstanceCache.js} +91 -83
- package/dist/LazyReferenceCollection.d.ts +11 -15
- package/dist/LazyReferenceCollection.js +16 -15
- package/dist/Model.d.ts +37 -52
- package/dist/Model.js +52 -69
- package/dist/ModelRegistry.d.ts +46 -25
- package/dist/ModelRegistry.js +32 -30
- package/dist/NetworkMonitor.d.ts +5 -6
- package/dist/NetworkMonitor.js +6 -7
- package/dist/SyncClient.d.ts +119 -109
- package/dist/SyncClient.js +303 -224
- package/dist/SyncEngineContext.d.ts +1 -3
- package/dist/SyncEngineContext.js +1 -2
- package/dist/adapters/alwaysOnline.d.ts +6 -8
- package/dist/adapters/alwaysOnline.js +6 -8
- package/dist/adapters/inMemoryStorage.d.ts +9 -9
- package/dist/adapters/inMemoryStorage.js +9 -9
- package/dist/agent/Agent.d.ts +39 -31
- package/dist/agent/Agent.js +35 -23
- 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} +30 -31
- package/dist/ai-sdk/index.d.ts +25 -22
- package/dist/ai-sdk/index.js +25 -22
- package/dist/ai-sdk/wrap.d.ts +7 -8
- package/dist/ai-sdk/wrap.js +2 -2
- package/dist/auth/credentialPolicy.d.ts +74 -71
- package/dist/auth/credentialPolicy.js +51 -56
- package/dist/auth/credentialSource.d.ts +7 -18
- package/dist/auth/credentialSource.js +10 -18
- package/dist/auth/index.d.ts +59 -58
- package/dist/auth/index.js +34 -40
- 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 +483 -369
- package/dist/client/Ablo.d.ts +107 -836
- package/dist/client/Ablo.js +174 -833
- package/dist/client/ApiClient.d.ts +44 -20
- package/dist/client/ApiClient.js +193 -44
- package/dist/client/auth.d.ts +51 -60
- package/dist/client/auth.js +137 -110
- package/dist/client/claimHeartbeatLoop.d.ts +50 -0
- package/dist/client/claimHeartbeatLoop.js +88 -0
- package/dist/client/consoleLogger.d.ts +35 -0
- package/dist/client/consoleLogger.js +44 -0
- package/dist/client/createInternalComponents.d.ts +14 -17
- package/dist/client/createInternalComponents.js +26 -31
- package/dist/client/createModelProxy.d.ts +130 -120
- package/dist/client/createModelProxy.js +158 -124
- package/dist/client/credentialEndpoint.d.ts +61 -0
- package/dist/client/credentialEndpoint.js +86 -0
- package/dist/client/functionalUpdate.d.ts +29 -27
- package/dist/client/functionalUpdate.js +21 -21
- package/dist/client/hostedEndpoints.d.ts +21 -0
- package/dist/client/hostedEndpoints.js +21 -0
- package/dist/client/httpClient.d.ts +58 -54
- package/dist/client/httpClient.js +29 -31
- package/dist/client/identity.d.ts +15 -20
- package/dist/client/identity.js +49 -59
- package/dist/client/modelRegistration.d.ts +10 -0
- package/dist/client/modelRegistration.js +301 -0
- package/dist/client/options.d.ts +373 -0
- package/dist/client/options.js +6 -0
- package/dist/client/registerDataSource.d.ts +9 -9
- package/dist/client/registerDataSource.js +15 -16
- package/dist/client/resourceTypes.d.ts +333 -0
- package/dist/client/resourceTypes.js +7 -0
- package/dist/client/schemaConfig.d.ts +44 -0
- package/dist/client/schemaConfig.js +176 -0
- package/dist/client/sessionMint.d.ts +17 -13
- package/dist/client/sessionMint.js +26 -31
- package/dist/client/validateAbloOptions.d.ts +12 -14
- package/dist/client/validateAbloOptions.js +9 -10
- package/dist/client/writeOptionsSchema.d.ts +18 -16
- package/dist/client/writeOptionsSchema.js +23 -20
- package/dist/client/wsMutationExecutor.d.ts +28 -0
- package/dist/client/wsMutationExecutor.js +71 -0
- package/dist/context.d.ts +6 -4
- package/dist/context.js +6 -7
- package/dist/coordination/index.d.ts +13 -4
- package/dist/coordination/index.js +29 -4
- package/dist/coordination/schema.d.ts +176 -128
- package/dist/coordination/schema.js +197 -133
- package/dist/coordination/trace.d.ts +9 -11
- package/dist/coordination/trace.js +13 -15
- package/dist/core/DatabaseManager.d.ts +5 -8
- package/dist/core/DatabaseManager.js +38 -40
- package/dist/core/QueryProcessor.d.ts +7 -9
- package/dist/core/QueryProcessor.js +27 -34
- package/dist/core/QueryView.d.ts +17 -5
- package/dist/core/QueryView.js +6 -7
- package/dist/core/StoreManager.d.ts +14 -16
- package/dist/core/StoreManager.js +26 -25
- package/dist/core/ViewRegistry.d.ts +5 -5
- package/dist/core/ViewRegistry.js +4 -4
- package/dist/core/index.d.ts +18 -13
- package/dist/core/index.js +32 -26
- package/dist/core/openIDBWithTimeout.d.ts +38 -36
- package/dist/core/openIDBWithTimeout.js +57 -54
- package/dist/core/queryUtils.d.ts +45 -0
- package/dist/core/queryUtils.js +69 -0
- package/dist/core/storeContract.d.ts +145 -0
- package/dist/core/storeContract.js +12 -0
- package/dist/environment.d.ts +28 -0
- package/dist/environment.js +21 -0
- package/dist/errorCodes.d.ts +118 -101
- package/dist/errorCodes.js +277 -260
- package/dist/errors.d.ts +170 -165
- package/dist/errors.js +161 -151
- package/dist/index.d.ts +30 -27
- package/dist/index.js +90 -82
- package/dist/interfaces/index.d.ts +108 -133
- package/dist/interfaces/index.js +5 -4
- package/dist/keys/index.d.ts +27 -29
- package/dist/keys/index.js +59 -49
- 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 +122 -131
- package/dist/mutators/UndoManager.js +149 -155
- package/dist/mutators/defineMutators.d.ts +24 -37
- 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 +105 -101
- package/dist/policy/types.js +67 -66
- package/dist/query/client.d.ts +32 -16
- package/dist/query/client.js +103 -72
- package/dist/query/types.d.ts +37 -60
- package/dist/query/types.js +13 -33
- package/dist/react/AbloProvider.d.ts +7 -11
- package/dist/react/AbloProvider.js +24 -17
- package/dist/react/context.d.ts +27 -146
- 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 +23 -22
- package/dist/react/useAblo.js +17 -15
- 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 +11 -12
- package/dist/react/useMutationFailureListener.d.ts +8 -8
- package/dist/react/useMutationFailureListener.js +9 -9
- package/dist/react/useMutators.d.ts +11 -11
- package/dist/react/useMutators.js +10 -4
- package/dist/react/useReactive.js +2 -3
- package/dist/react/useSyncStatus.d.ts +4 -6
- package/dist/react/useUndoScope.d.ts +7 -9
- package/dist/react/useUndoScope.js +3 -3
- 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 +35 -0
- package/dist/schema/ddlLock.js +46 -0
- 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 +36 -49
- package/dist/schema/generate.d.ts +12 -12
- package/dist/schema/generate.js +12 -12
- package/dist/schema/index.d.ts +5 -4
- package/dist/schema/index.js +29 -21
- package/dist/schema/model.d.ts +121 -146
- package/dist/schema/model.js +24 -35
- package/dist/schema/openapi.d.ts +10 -9
- package/dist/schema/openapi.js +7 -1
- package/dist/schema/queries.d.ts +30 -32
- package/dist/schema/queries.js +24 -25
- package/dist/schema/relation.d.ts +89 -99
- package/dist/schema/relation.js +13 -13
- package/dist/schema/residency.d.ts +38 -0
- package/dist/schema/residency.js +30 -0
- package/dist/schema/roles.d.ts +45 -27
- package/dist/schema/roles.js +52 -21
- package/dist/schema/schema.d.ts +36 -45
- package/dist/schema/schema.js +42 -39
- package/dist/schema/select.d.ts +13 -13
- package/dist/schema/select.js +13 -13
- package/dist/schema/serialize.d.ts +36 -39
- 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} +27 -50
- 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 +31 -26
- package/dist/source/adapter.js +10 -10
- package/dist/source/adapters/drizzle.d.ts +28 -23
- package/dist/source/adapters/drizzle.js +34 -28
- package/dist/source/adapters/kysely.d.ts +27 -25
- package/dist/source/adapters/kysely.js +28 -26
- package/dist/source/adapters/memory.d.ts +8 -7
- package/dist/source/adapters/memory.js +10 -9
- package/dist/source/adapters/prisma.d.ts +13 -12
- package/dist/source/adapters/prisma.js +27 -29
- package/dist/source/conformance.d.ts +18 -11
- package/dist/source/conformance.js +27 -19
- package/dist/source/connector.d.ts +31 -32
- package/dist/source/connector.js +30 -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 +94 -0
- package/dist/source/factory.js +268 -0
- package/dist/source/index.d.ts +10 -462
- package/dist/source/index.js +17 -421
- package/dist/source/migrations.d.ts +9 -9
- package/dist/source/migrations.js +9 -9
- package/dist/source/next.d.ts +10 -11
- package/dist/source/next.js +7 -8
- package/dist/source/pushQueue.d.ts +70 -48
- package/dist/source/pushQueue.js +36 -29
- package/dist/source/signing.d.ts +88 -0
- package/dist/source/signing.js +159 -0
- package/dist/source/types.d.ts +351 -0
- package/dist/source/types.js +43 -0
- package/dist/stores/ObjectStore.d.ts +11 -12
- package/dist/stores/ObjectStore.js +34 -35
- package/dist/stores/ObjectStoreContract.d.ts +12 -15
- package/dist/stores/SyncActionStore.d.ts +8 -12
- package/dist/stores/SyncActionStore.js +77 -46
- package/dist/surface.d.ts +28 -21
- package/dist/surface.js +28 -20
- package/dist/sync/{BootstrapHelper.d.ts → BootstrapFetcher.d.ts} +37 -45
- package/dist/sync/{BootstrapHelper.js → BootstrapFetcher.js} +101 -80
- package/dist/sync/ConnectionManager.d.ts +47 -50
- package/dist/sync/ConnectionManager.js +74 -70
- package/dist/sync/NetworkProbe.d.ts +27 -31
- package/dist/sync/NetworkProbe.js +67 -72
- package/dist/sync/{HydrationCoordinator.d.ts → OnDemandLoader.d.ts} +49 -36
- package/dist/sync/{HydrationCoordinator.js → OnDemandLoader.js} +79 -54
- package/dist/sync/{AreaOfInterestManager.d.ts → SubscriptionManager.d.ts} +45 -59
- package/dist/sync/{AreaOfInterestManager.js → SubscriptionManager.js} +44 -52
- package/dist/sync/SyncWebSocket.d.ts +175 -250
- package/dist/sync/SyncWebSocket.js +431 -769
- package/dist/sync/awaitClaimGrant.d.ts +18 -18
- package/dist/sync/awaitClaimGrant.js +38 -30
- package/dist/sync/bootstrapApply.d.ts +70 -0
- package/dist/sync/bootstrapApply.js +73 -0
- package/dist/sync/commitFrames.d.ts +44 -0
- package/dist/sync/commitFrames.js +94 -0
- package/dist/sync/createClaimStream.d.ts +23 -22
- package/dist/sync/createClaimStream.js +108 -25
- package/dist/sync/createPresenceStream.d.ts +19 -18
- package/dist/sync/createPresenceStream.js +25 -26
- package/dist/sync/createSnapshot.d.ts +13 -17
- package/dist/sync/createSnapshot.js +20 -26
- package/dist/sync/credentialLifecycle.d.ts +175 -0
- package/dist/sync/credentialLifecycle.js +322 -0
- package/dist/sync/deltaPipeline.d.ts +113 -0
- package/dist/sync/deltaPipeline.js +261 -0
- package/dist/sync/groupChange.d.ts +113 -0
- package/dist/sync/groupChange.js +242 -0
- package/dist/sync/heartbeat.d.ts +63 -0
- package/dist/sync/heartbeat.js +91 -0
- package/dist/sync/participants.d.ts +27 -27
- package/dist/sync/schemas.d.ts +3 -2
- package/dist/sync/schemas.js +14 -10
- package/dist/sync/syncCursor.d.ts +40 -0
- package/dist/sync/syncCursor.js +55 -0
- package/dist/sync/syncPlan.d.ts +54 -0
- package/dist/sync/syncPlan.js +50 -0
- package/dist/sync/syncPosition.d.ts +54 -49
- package/dist/sync/syncPosition.js +57 -52
- package/dist/sync/wsFrameHandlers.d.ts +116 -0
- package/dist/sync/wsFrameHandlers.js +374 -0
- package/dist/testing/fixtures/bootstrap.d.ts +21 -17
- package/dist/testing/fixtures/bootstrap.js +12 -6
- package/dist/testing/fixtures/deltas.d.ts +31 -34
- package/dist/testing/fixtures/deltas.js +30 -33
- package/dist/testing/fixtures/models.d.ts +11 -10
- package/dist/testing/fixtures/models.js +12 -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 -18
- package/dist/testing/helpers/{sync-engine-harness.js → syncEngineHarness.js} +14 -11
- package/dist/testing/helpers/wait.d.ts +13 -8
- package/dist/testing/helpers/wait.js +13 -8
- package/dist/testing/index.d.ts +4 -4
- package/dist/testing/index.js +3 -3
- 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 +21 -34
- package/dist/testing/mocks/MockSyncContext.js +16 -45
- package/dist/testing/mocks/MockSyncStore.d.ts +14 -14
- package/dist/testing/mocks/MockSyncStore.js +11 -11
- package/dist/testing/mocks/MockWebSocket.d.ts +28 -24
- package/dist/testing/mocks/MockWebSocket.js +22 -21
- package/dist/transactions/TransactionQueue.d.ts +190 -221
- package/dist/transactions/TransactionQueue.js +424 -822
- package/dist/transactions/TransactionStore.d.ts +20 -0
- package/dist/transactions/TransactionStore.js +53 -0
- package/dist/transactions/UnconfirmedWrites.d.ts +82 -0
- package/dist/transactions/UnconfirmedWrites.js +104 -0
- package/dist/transactions/coalesceRules.d.ts +58 -0
- package/dist/transactions/coalesceRules.js +140 -0
- package/dist/transactions/commitPayload.d.ts +130 -0
- package/dist/transactions/commitPayload.js +143 -0
- package/dist/transactions/deltaConfirmation.d.ts +58 -0
- package/dist/transactions/deltaConfirmation.js +215 -0
- package/dist/transactions/optimisticApply.d.ts +49 -0
- package/dist/transactions/optimisticApply.js +65 -0
- package/dist/transactions/replayValidation.d.ts +99 -0
- package/dist/transactions/replayValidation.js +111 -0
- package/dist/types/global.d.ts +46 -41
- package/dist/types/global.js +20 -19
- package/dist/types/index.d.ts +74 -80
- package/dist/types/index.js +22 -27
- package/dist/types/modelData.d.ts +10 -0
- package/dist/types/modelData.js +9 -0
- package/dist/types/participant.d.ts +20 -0
- package/dist/types/participant.js +10 -0
- package/dist/types/streams.d.ts +216 -209
- 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} +44 -100
- 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/{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 +35 -27
- package/dist/wire/errorEnvelope.js +38 -32
- package/dist/wire/frames.d.ts +150 -67
- package/dist/wire/frames.js +48 -1
- package/dist/wire/index.d.ts +18 -13
- package/dist/wire/index.js +36 -13
- package/dist/wire/listEnvelope.d.ts +16 -23
- package/dist/wire/listEnvelope.js +7 -6
- package/dist/wire/protocol.d.ts +38 -0
- package/dist/wire/protocol.js +38 -0
- package/dist/wire/protocolVersion.d.ts +60 -0
- package/dist/wire/protocolVersion.js +67 -0
- package/docs/api-keys.md +4 -3
- package/docs/coordination.md +59 -0
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/identity.md +4 -4
- package/docs/integration-guide.md +1 -1
- package/docs/react.md +1 -1
- package/docs/sessions.md +5 -7
- package/package.json +24 -21
- package/dist/ai-sdk/coordinated-tool.d.ts +0 -101
- package/dist/ai-sdk/coordination-context.d.ts +0 -52
- package/dist/client/index.d.ts +0 -36
- package/dist/client/index.js +0 -33
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -12
- package/dist/core/query-utils.d.ts +0 -34
- package/dist/core/query-utils.js +0 -59
- package/dist/interfaces/headless.d.ts +0 -95
- package/dist/interfaces/headless.js +0 -41
- package/dist/query/index.d.ts +0 -6
- package/dist/query/index.js +0 -5
- package/dist/realtime/index.d.ts +0 -10
- package/dist/realtime/index.js +0 -9
- package/dist/schema/plane.d.ts +0 -23
- package/dist/schema/plane.js +0 -19
- package/dist/schema/sync-delta-row.js +0 -103
- package/dist/schema/sync-delta-wire.js +0 -102
- package/dist/server/next.d.ts +0 -51
- package/dist/server/next.js +0 -47
- 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 -1
- package/dist/server/storage-mode.js +0 -18
- package/dist/source/connector-protocol.d.ts +0 -159
- package/dist/source/connector-protocol.js +0 -161
- package/dist/sync/OfflineFlush.d.ts +0 -9
- package/dist/sync/OfflineFlush.js +0 -22
- package/dist/sync/OfflineTransactionStore.d.ts +0 -37
- package/dist/sync/OfflineTransactionStore.js +0 -263
- package/dist/transactions/OptimisticEchoTracker.d.ts +0 -82
- package/dist/transactions/OptimisticEchoTracker.js +0 -104
- package/dist/transactions/index.d.ts +0 -16
- package/dist/transactions/index.js +0 -7
- package/dist/transactions/mutation-error-handler.d.ts +0 -5
- package/dist/transactions/mutation-error-handler.js +0 -39
- package/dist/utils/mobx-setup.d.ts +0 -42
|
@@ -1,34 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Creates a {@link ClaimStream} over a live sync connection. A claim is a
|
|
3
|
+
* short-lived, advisory lease a participant takes on an entity (or a field of
|
|
4
|
+
* one) to signal "I'm working on this"; the stream lets you take claims, see
|
|
5
|
+
* everyone else's, and watch the wait queue when a claim is contended.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* The stream is built directly on the sync WebSocket and shares that one
|
|
8
|
+
* connection. It learns about other participants' claims from the same
|
|
9
|
+
* `presence_update` frames the {@link createPresenceStream} presence stream
|
|
10
|
+
* consumes — the server piggybacks each participant's `activeClaims` on every
|
|
11
|
+
* presence frame — and sends its own claims as `claim_begin` and
|
|
9
12
|
* `claim_abandon` frames.
|
|
10
13
|
*
|
|
11
|
-
* Wire
|
|
12
|
-
* • Outbound
|
|
13
|
-
*
|
|
14
|
-
* • Outbound
|
|
15
|
-
*
|
|
16
|
-
* • Inbound
|
|
17
|
-
*
|
|
18
|
-
* • Inbound
|
|
19
|
-
*
|
|
20
|
-
* After the dual-engine collapse (step #36), this is the only
|
|
21
|
-
* ClaimStream factory in the SDK; the older compatibility path
|
|
22
|
-
* deletes.
|
|
14
|
+
* Wire frames:
|
|
15
|
+
* • Outbound `claim_begin` — announce a claim: `{ claimId, entityType,
|
|
16
|
+
* entityId, reason, field?, estimatedMs? }`.
|
|
17
|
+
* • Outbound `claim_abandon` — release it: `{ claimId, entityType?,
|
|
18
|
+
* entityId? }`.
|
|
19
|
+
* • Inbound, via presence — `event.activeClaims`, each stamped with
|
|
20
|
+
* `declaredAt` and `expiresAt`.
|
|
21
|
+
* • Inbound `claim_rejected` — the server refused the claim, with conflict
|
|
22
|
+
* metadata.
|
|
23
23
|
*/
|
|
24
24
|
import { asyncIteratorFrom } from '../utils/asyncIterator.js';
|
|
25
25
|
import { toMs } from '../utils/duration.js';
|
|
26
|
-
import { descriptionFromMeta, participantKindFromWire, } from '../coordination/schema.js';
|
|
26
|
+
import { claimHeartbeatAckPayloadSchema, descriptionFromMeta, participantKindFromWire, } from '../coordination/schema.js';
|
|
27
|
+
import { AbloClaimedError, AbloConnectionError } from '../errors.js';
|
|
28
|
+
import { resolveHeartbeatOptions } from '../client/claimHeartbeatLoop.js';
|
|
27
29
|
import { getContext } from '../context.js';
|
|
28
30
|
/** Readable target for the coordination trace: `documents:abc` / `documents:abc.title`. */
|
|
29
31
|
function claimLabel(type, id, field) {
|
|
30
32
|
return field ? `${type}:${id}.${field}` : `${type}:${id}`;
|
|
31
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* How long a heartbeat waits for its `claim_heartbeat_ack` before giving up
|
|
36
|
+
* as transient (the auto-heartbeat loop's next tick retries). Comfortably
|
|
37
|
+
* above a round trip, comfortably below the ttl/3 beat cadence.
|
|
38
|
+
*/
|
|
39
|
+
const HEARTBEAT_ACK_TIMEOUT_MS = 10_000;
|
|
32
40
|
export function createClaimStream(config, transport = null) {
|
|
33
41
|
const { participantId } = config;
|
|
34
42
|
// ── State: others' open claims, keyed by claimId ───────────────
|
|
@@ -49,6 +57,16 @@ export function createClaimStream(config, transport = null) {
|
|
|
49
57
|
const listeners = new Set();
|
|
50
58
|
const rejectionListeners = new Set();
|
|
51
59
|
const lostListeners = new Set();
|
|
60
|
+
// ── State: in-flight heartbeats awaiting their ack, keyed by claimId ──
|
|
61
|
+
const pendingHeartbeats = new Map();
|
|
62
|
+
const settleHeartbeat = (claimId, settle) => {
|
|
63
|
+
const pending = pendingHeartbeats.get(claimId);
|
|
64
|
+
if (!pending)
|
|
65
|
+
return;
|
|
66
|
+
pendingHeartbeats.delete(claimId);
|
|
67
|
+
clearTimeout(pending.timer);
|
|
68
|
+
settle(pending);
|
|
69
|
+
};
|
|
52
70
|
const notifyListeners = () => {
|
|
53
71
|
claimsSnapshot = Object.freeze(Array.from(activeByClaimId.values()));
|
|
54
72
|
for (const l of listeners) {
|
|
@@ -151,8 +169,9 @@ export function createClaimStream(config, transport = null) {
|
|
|
151
169
|
}
|
|
152
170
|
}
|
|
153
171
|
}));
|
|
154
|
-
// (2a) Server-side
|
|
155
|
-
// expired). Distinct from a rejection
|
|
172
|
+
// (2a) Server-side loss frames — you held the claim, then lost it
|
|
173
|
+
// (preempted or expired). Distinct from a rejection, which is a claim
|
|
174
|
+
// the server refused.
|
|
156
175
|
unsubs.push(t.subscribe('claim_lost', (payload) => {
|
|
157
176
|
const lost = payload;
|
|
158
177
|
if (!lost.claimId)
|
|
@@ -186,11 +205,12 @@ export function createClaimStream(config, transport = null) {
|
|
|
186
205
|
queueByEntity.delete(key);
|
|
187
206
|
else
|
|
188
207
|
queueByEntity.set(key, Object.freeze([...line]));
|
|
189
|
-
// If
|
|
208
|
+
// If we are in this line, trace our position (the "agent queued behind a
|
|
190
209
|
// claim" moment) — once per position change, so advancing is visible.
|
|
191
210
|
const ourIndex = line.findIndex((c) => ownClaims.has(c.id));
|
|
192
|
-
|
|
193
|
-
|
|
211
|
+
const ourClaim = ourIndex >= 0 ? line[ourIndex] : undefined;
|
|
212
|
+
if (ourClaim) {
|
|
213
|
+
const ourId = ourClaim.id;
|
|
194
214
|
if (lastLoggedQueuePos.get(ourId) !== ourIndex) {
|
|
195
215
|
lastLoggedQueuePos.set(ourId, ourIndex);
|
|
196
216
|
getContext().logger.info(`claim: queued for ${claimLabel(p.target.type, p.target.id)} — position ${ourIndex + 1} of ${line.length}, waiting`, { claimId: ourId });
|
|
@@ -198,6 +218,30 @@ export function createClaimStream(config, transport = null) {
|
|
|
198
218
|
}
|
|
199
219
|
notifyListeners();
|
|
200
220
|
}));
|
|
221
|
+
// (2c) Heartbeat replies — correlate back to the awaiting beat by
|
|
222
|
+
// claimId. `held` resolves with the extended expiry; `queued` and
|
|
223
|
+
// `lost` reject with a typed claimed error, because a heartbeat on
|
|
224
|
+
// a handle we thought we held coming back as anything but `held`
|
|
225
|
+
// means the lease is no longer ours.
|
|
226
|
+
unsubs.push(t.subscribe('claim_heartbeat_ack', (payload) => {
|
|
227
|
+
const parsed = claimHeartbeatAckPayloadSchema.safeParse(payload);
|
|
228
|
+
if (!parsed.success)
|
|
229
|
+
return;
|
|
230
|
+
const ack = parsed.data;
|
|
231
|
+
settleHeartbeat(ack.claimId, ({ resolve, reject }) => {
|
|
232
|
+
if (ack.status === 'held' && ack.expiresAt !== undefined) {
|
|
233
|
+
resolve({
|
|
234
|
+
expiresAt: ack.expiresAt,
|
|
235
|
+
...(ack.queueDepth !== undefined
|
|
236
|
+
? { queueDepth: ack.queueDepth }
|
|
237
|
+
: {}),
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const c = ownClaims.get(ack.claimId);
|
|
242
|
+
reject(new AbloClaimedError(`The lease behind ${c ? claimLabel(c.entityType, c.entityId, c.field) : `claim ${ack.claimId}`} is no longer held — it expired or was granted onward while this participant was working. Re-acquire the claim and retry; a write attempted under the old lease is rejected by its \`readAt\` guard.`, { code: 'claim_lost' }));
|
|
243
|
+
});
|
|
244
|
+
}));
|
|
201
245
|
// (3) On reconnect, re-announce every open self-claim — the
|
|
202
246
|
// server's claim state is in-memory and is lost across
|
|
203
247
|
// restarts. Without this, peers would see our claims vanish
|
|
@@ -244,6 +288,39 @@ export function createClaimStream(config, transport = null) {
|
|
|
244
288
|
},
|
|
245
289
|
});
|
|
246
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Send one heartbeat and await its ack. Rejects with
|
|
293
|
+
* {@link AbloConnectionError} (transient — the auto-heartbeat loop retries
|
|
294
|
+
* on its next tick) when the socket is down or the ack times out, and with
|
|
295
|
+
* {@link AbloClaimedError} (definitive) when the server answers that the
|
|
296
|
+
* lease is no longer ours.
|
|
297
|
+
*/
|
|
298
|
+
function sendHeartbeat(claimId, claim, options) {
|
|
299
|
+
if (!attached?.isConnected()) {
|
|
300
|
+
return Promise.reject(new AbloConnectionError(`The heartbeat for ${claimLabel(claim.entityType, claim.entityId, claim.field)} was skipped because the connection is down. The keepalive renews held leases automatically on reconnect; the next beat retries.`));
|
|
301
|
+
}
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
settleHeartbeat(claimId, ({ reject: rejectPrior }) => {
|
|
304
|
+
rejectPrior(new AbloConnectionError('A newer heartbeat for this claim superseded the one still awaiting its reply.'));
|
|
305
|
+
});
|
|
306
|
+
const timer = setTimeout(() => {
|
|
307
|
+
settleHeartbeat(claimId, ({ reject: rejectTimeout }) => {
|
|
308
|
+
rejectTimeout(new AbloConnectionError(`No reply to the heartbeat for ${claimLabel(claim.entityType, claim.entityId, claim.field)} arrived within ${HEARTBEAT_ACK_TIMEOUT_MS / 1000}s. The next beat retries.`));
|
|
309
|
+
});
|
|
310
|
+
}, HEARTBEAT_ACK_TIMEOUT_MS);
|
|
311
|
+
pendingHeartbeats.set(claimId, { resolve, reject, timer });
|
|
312
|
+
attached?.send({
|
|
313
|
+
type: 'claim_heartbeat',
|
|
314
|
+
payload: {
|
|
315
|
+
claimId,
|
|
316
|
+
entityType: claim.entityType,
|
|
317
|
+
entityId: claim.entityId,
|
|
318
|
+
...(options.ttl !== undefined ? { ttlMs: toMs(options.ttl) } : {}),
|
|
319
|
+
...(options.details !== undefined ? { details: options.details } : {}),
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
247
324
|
function sendAbandon(claimId, claim) {
|
|
248
325
|
if (!attached?.isConnected())
|
|
249
326
|
return;
|
|
@@ -280,7 +357,7 @@ export function createClaimStream(config, transport = null) {
|
|
|
280
357
|
};
|
|
281
358
|
ownClaims.set(claimId, claim);
|
|
282
359
|
sendBegin(claimId, claim);
|
|
283
|
-
// Coordination trace (info): the creator can
|
|
360
|
+
// Coordination trace (info): the creator can see their human/agent claims.
|
|
284
361
|
getContext().logger.info(`claim: requesting ${claimLabel(claim.entityType, claim.entityId, claim.field)} for "${claim.reason}"` +
|
|
285
362
|
(claim.queue ? ' (will queue if contended)' : ''), { claimId });
|
|
286
363
|
let revoked = false;
|
|
@@ -309,6 +386,7 @@ export function createClaimStream(config, transport = null) {
|
|
|
309
386
|
revoke();
|
|
310
387
|
},
|
|
311
388
|
revoke,
|
|
389
|
+
heartbeat: (options) => sendHeartbeat(claimId, claim, resolveHeartbeatOptions(options)),
|
|
312
390
|
[Symbol.asyncDispose]: async () => {
|
|
313
391
|
revoke();
|
|
314
392
|
},
|
|
@@ -376,6 +454,11 @@ export function createClaimStream(config, transport = null) {
|
|
|
376
454
|
for (const off of unsubs)
|
|
377
455
|
off();
|
|
378
456
|
unsubs.length = 0;
|
|
457
|
+
for (const claimId of [...pendingHeartbeats.keys()]) {
|
|
458
|
+
settleHeartbeat(claimId, ({ reject }) => {
|
|
459
|
+
reject(new AbloConnectionError('The claim stream was disposed while this heartbeat was awaiting its reply.'));
|
|
460
|
+
});
|
|
461
|
+
}
|
|
379
462
|
listeners.clear();
|
|
380
463
|
rejectionListeners.clear();
|
|
381
464
|
lostListeners.clear();
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Creates a {@link PresenceStream} over a live sync connection. Presence is the
|
|
3
|
+
* lightweight, ephemeral "who's here and what are they doing" view: each
|
|
4
|
+
* participant broadcasts a status and an activity, and sees everyone else's.
|
|
5
|
+
* The stream is built directly on the sync WebSocket and adds no second
|
|
6
|
+
* connection. It is the sibling of {@link createClaimStream}, which reuses the
|
|
7
|
+
* same presence frames.
|
|
3
8
|
*
|
|
4
|
-
*
|
|
5
|
-
* `SyncWebSocket`, no SyncAgent wrapper, no second connection. The
|
|
6
|
-
* older compatibility path predates this and will be deleted when
|
|
7
|
-
* the dual-engine collapse completes.
|
|
9
|
+
* There are two ways to construct it:
|
|
8
10
|
*
|
|
9
|
-
*
|
|
11
|
+
* 1. Direct — pass an already-open `transport`, for example an agent worker
|
|
12
|
+
* or a test.
|
|
13
|
+
* 2. Deferred — construct without a transport and call `attach(transport)`
|
|
14
|
+
* once the connection is ready. The returned stream object is stable from
|
|
15
|
+
* construction, so callers can hold the reference and let attachment
|
|
16
|
+
* happen later.
|
|
10
17
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Wire contract (apps/sync-server/src/hub/types.ts):
|
|
19
|
-
* • Outbound: `{ type: 'presence_update', payload: { status, activity? } }`
|
|
20
|
-
* — server stamps `userId`, `kind`, `timestamp`, `isAgent` and
|
|
21
|
-
* broadcasts to other clients on the same sync groups.
|
|
22
|
-
* • Inbound: same frame, with `kind: 'enter' | 'update' | 'leave'`.
|
|
18
|
+
* Wire frames:
|
|
19
|
+
* • Outbound `presence_update` — `{ status, activity? }`. The server stamps
|
|
20
|
+
* `userId`, `kind`, `timestamp`, and `isAgent`, then broadcasts to the
|
|
21
|
+
* other participants on the same sync groups.
|
|
22
|
+
* • Inbound — the same frame, with `kind` one of `enter`, `update`, or
|
|
23
|
+
* `leave`.
|
|
23
24
|
*/
|
|
24
25
|
import type { SyncWebSocket } from './SyncWebSocket.js';
|
|
25
26
|
import type { PresenceStream } from '../types/streams.js';
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Creates a {@link PresenceStream} over a live sync connection. Presence is the
|
|
3
|
+
* lightweight, ephemeral "who's here and what are they doing" view: each
|
|
4
|
+
* participant broadcasts a status and an activity, and sees everyone else's.
|
|
5
|
+
* The stream is built directly on the sync WebSocket and adds no second
|
|
6
|
+
* connection. It is the sibling of {@link createClaimStream}, which reuses the
|
|
7
|
+
* same presence frames.
|
|
3
8
|
*
|
|
4
|
-
*
|
|
5
|
-
* `SyncWebSocket`, no SyncAgent wrapper, no second connection. The
|
|
6
|
-
* older compatibility path predates this and will be deleted when
|
|
7
|
-
* the dual-engine collapse completes.
|
|
9
|
+
* There are two ways to construct it:
|
|
8
10
|
*
|
|
9
|
-
*
|
|
11
|
+
* 1. Direct — pass an already-open `transport`, for example an agent worker
|
|
12
|
+
* or a test.
|
|
13
|
+
* 2. Deferred — construct without a transport and call `attach(transport)`
|
|
14
|
+
* once the connection is ready. The returned stream object is stable from
|
|
15
|
+
* construction, so callers can hold the reference and let attachment
|
|
16
|
+
* happen later.
|
|
10
17
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Wire contract (apps/sync-server/src/hub/types.ts):
|
|
19
|
-
* • Outbound: `{ type: 'presence_update', payload: { status, activity? } }`
|
|
20
|
-
* — server stamps `userId`, `kind`, `timestamp`, `isAgent` and
|
|
21
|
-
* broadcasts to other clients on the same sync groups.
|
|
22
|
-
* • Inbound: same frame, with `kind: 'enter' | 'update' | 'leave'`.
|
|
18
|
+
* Wire frames:
|
|
19
|
+
* • Outbound `presence_update` — `{ status, activity? }`. The server stamps
|
|
20
|
+
* `userId`, `kind`, `timestamp`, and `isAgent`, then broadcasts to the
|
|
21
|
+
* other participants on the same sync groups.
|
|
22
|
+
* • Inbound — the same frame, with `kind` one of `enter`, `update`, or
|
|
23
|
+
* `leave`.
|
|
23
24
|
*/
|
|
24
25
|
import { asyncIteratorFrom } from '../utils/asyncIterator.js';
|
|
25
26
|
import { participantKindFromWire } from '../coordination/schema.js';
|
|
@@ -67,10 +68,9 @@ export function createPresenceStream(config, transport = null) {
|
|
|
67
68
|
if (self.activity.entityId)
|
|
68
69
|
sendUpdate(self.activity);
|
|
69
70
|
}));
|
|
70
|
-
// Inbound presence frames
|
|
71
|
-
// (userId / isAgent / timestamp) into the
|
|
72
|
-
// (participantId / participantKind / lastActive).
|
|
73
|
-
// adopts the engine names this block collapses to a pass-through.
|
|
71
|
+
// Inbound presence frames arrive in the wire vocabulary
|
|
72
|
+
// (userId / isAgent / timestamp); translate them into the shape this
|
|
73
|
+
// stream exposes (participantId / participantKind / lastActive).
|
|
74
74
|
unsubs.push(t.subscribe('presence_update', (event) => {
|
|
75
75
|
if (event.userId === participantId)
|
|
76
76
|
return; // own echo
|
|
@@ -117,10 +117,9 @@ export function createPresenceStream(config, transport = null) {
|
|
|
117
117
|
if (transport)
|
|
118
118
|
attach(transport);
|
|
119
119
|
// ── Outbound ────────────────────────────────────────────────────
|
|
120
|
-
//
|
|
121
|
-
// authoritatively from the connection's identity
|
|
122
|
-
// self-
|
|
123
|
-
// agents to peers (real bug we caught earlier).
|
|
120
|
+
// Do not include `isAgent` in the payload. The server derives it
|
|
121
|
+
// authoritatively from the connection's identity, and letting a client
|
|
122
|
+
// self-declare it once caused human sessions to broadcast as agents to peers.
|
|
124
123
|
function sendUpdate(activity) {
|
|
125
124
|
if (!attached?.isConnected())
|
|
126
125
|
return; // no-op until connected
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Captures a {@link Snapshot} of a chosen set of entities, along with a
|
|
3
|
+
* watermark, so a caller can detect when that state has gone stale. This is
|
|
4
|
+
* what an LLM caller threads into a prompt: `stamp` flows into later writes as
|
|
5
|
+
* `readAt`, so the server rejects a mutation premised on data that has since
|
|
6
|
+
* changed; `signal` is an `AbortSignal` that fires as soon as any captured
|
|
7
|
+
* entity receives a delta, so a mid-generation invalidation can abort the token
|
|
8
|
+
* stream instead of producing output against stale context.
|
|
3
9
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* server rejects mutations against now-stale data; `signal` fires on
|
|
8
|
-
* any captured-entity delta so mid-generation invalidations abort
|
|
9
|
-
* the token stream rather than producing output against dead context.
|
|
10
|
-
*
|
|
11
|
-
* Reads from the engine's MobX-reactive ObjectPool, picks up the
|
|
12
|
-
* engine's `lastSyncId`, and subscribes to delta frames on the
|
|
13
|
-
* engine's transport. Same socket as entity sync — no second
|
|
14
|
-
* connection.
|
|
10
|
+
* It reads the current entity state from the in-memory pool, reads the engine's
|
|
11
|
+
* current `lastSyncId` as the watermark, and subscribes to delta frames on the
|
|
12
|
+
* existing sync connection — no second connection.
|
|
15
13
|
*/
|
|
16
|
-
import type {
|
|
14
|
+
import type { InstanceCache } from '../InstanceCache.js';
|
|
17
15
|
import type { Schema } from '../schema/schema.js';
|
|
18
16
|
import type { SyncWebSocket } from './SyncWebSocket.js';
|
|
19
17
|
import type { Snapshot } from '../types/streams.js';
|
|
20
18
|
export interface CreateSnapshotArgs<TSchema extends Schema = Schema, K extends keyof TSchema['models'] & string = keyof TSchema['models'] & string> {
|
|
21
|
-
pool:
|
|
19
|
+
pool: InstanceCache;
|
|
22
20
|
/** Live transport for delta subscriptions. May be null if the engine
|
|
23
21
|
* hasn't connected yet — the snapshot still resolves with current
|
|
24
22
|
* pool state, but `signal` won't fire until reconnect. */
|
|
@@ -26,8 +24,6 @@ export interface CreateSnapshotArgs<TSchema extends Schema = Schema, K extends k
|
|
|
26
24
|
/** Returns the engine's current `lastSyncId`. Read at snapshot time
|
|
27
25
|
* to stamp the watermark; not re-read after. */
|
|
28
26
|
getLastSyncId: () => number;
|
|
29
|
-
entities:
|
|
30
|
-
readonly [M in K]: string | readonly string[];
|
|
31
|
-
};
|
|
27
|
+
entities: Readonly<Record<K, string | readonly string[]>>;
|
|
32
28
|
}
|
|
33
29
|
export declare function createSnapshot<TSchema extends Schema, K extends keyof TSchema['models'] & string>(args: CreateSnapshotArgs<TSchema, K>): Snapshot<TSchema, K>;
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Captures a {@link Snapshot} of a chosen set of entities, along with a
|
|
3
|
+
* watermark, so a caller can detect when that state has gone stale. This is
|
|
4
|
+
* what an LLM caller threads into a prompt: `stamp` flows into later writes as
|
|
5
|
+
* `readAt`, so the server rejects a mutation premised on data that has since
|
|
6
|
+
* changed; `signal` is an `AbortSignal` that fires as soon as any captured
|
|
7
|
+
* entity receives a delta, so a mid-generation invalidation can abort the token
|
|
8
|
+
* stream instead of producing output against stale context.
|
|
3
9
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* server rejects mutations against now-stale data; `signal` fires on
|
|
8
|
-
* any captured-entity delta so mid-generation invalidations abort
|
|
9
|
-
* the token stream rather than producing output against dead context.
|
|
10
|
-
*
|
|
11
|
-
* Reads from the engine's MobX-reactive ObjectPool, picks up the
|
|
12
|
-
* engine's `lastSyncId`, and subscribes to delta frames on the
|
|
13
|
-
* engine's transport. Same socket as entity sync — no second
|
|
14
|
-
* connection.
|
|
10
|
+
* It reads the current entity state from the in-memory pool, reads the engine's
|
|
11
|
+
* current `lastSyncId` as the watermark, and subscribes to delta frames on the
|
|
12
|
+
* existing sync connection — no second connection.
|
|
15
13
|
*/
|
|
16
14
|
import { AbloValidationError } from '../errors.js';
|
|
17
15
|
import { Model, modelAsRow } from '../Model.js';
|
|
18
16
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* The snapshot result exposes `stamp`, `signal`, and `onChange` at its top
|
|
18
|
+
* level, alongside one bucket per model. If a schema declares a model with one
|
|
19
|
+
* of these names, the two would collide, so snapshot creation throws instead.
|
|
22
20
|
*/
|
|
23
21
|
const RESERVED_SNAPSHOT_KEYS = new Set([
|
|
24
22
|
'stamp',
|
|
@@ -80,9 +78,7 @@ export function createSnapshot(args) {
|
|
|
80
78
|
const key = `${delta.modelName}:${delta.modelId}`;
|
|
81
79
|
if (!watched.has(key))
|
|
82
80
|
return;
|
|
83
|
-
//
|
|
84
|
-
// Future: distinguish metadata-only deltas (e.g., updatedAt
|
|
85
|
-
// bumps) from content changes — that's a separate scope.
|
|
81
|
+
// Every delta to a captured entity is reported as 'semantic' severity.
|
|
86
82
|
fireChange({
|
|
87
83
|
model: delta.modelName,
|
|
88
84
|
id: delta.modelId,
|
|
@@ -96,16 +92,14 @@ export function createSnapshot(args) {
|
|
|
96
92
|
signal: controller.signal,
|
|
97
93
|
onChange: (listener) => {
|
|
98
94
|
listeners.add(listener);
|
|
99
|
-
//
|
|
100
|
-
// The delta subscription
|
|
101
|
-
// there
|
|
102
|
-
//
|
|
103
|
-
// is cheap. If a long-lived consumer needs explicit teardown,
|
|
104
|
-
// we can add `.dispose()` in a follow-up.
|
|
95
|
+
// The caller unsubscribes its own listener via the returned function.
|
|
96
|
+
// The underlying delta subscription lives for the snapshot's lifetime;
|
|
97
|
+
// there is no explicit dispose because a snapshot is short-lived (one
|
|
98
|
+
// LLM call's worth) and the subscription is cheap.
|
|
105
99
|
return () => {
|
|
106
100
|
listeners.delete(listener);
|
|
107
|
-
//
|
|
108
|
-
// subscription too —
|
|
101
|
+
// Once the last listener is gone and the abort has fired, drop the
|
|
102
|
+
// delta subscription too — nothing is listening anymore.
|
|
109
103
|
if (listeners.size === 0 && controller.signal.aborted && unsubDelta) {
|
|
110
104
|
unsubDelta();
|
|
111
105
|
unsubDelta = null;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the short-lived access credential fresh. It owns the re-mint hook, a
|
|
3
|
+
* single-flight guard that stops concurrent triggers from minting more than
|
|
4
|
+
* once, and a browser-only proactive refresh — a timer plus an OS-wake listener
|
|
5
|
+
* — that renews the credential ahead of expiry. It reaches the rest of the
|
|
6
|
+
* client only through the small {@link CredentialLifecycleContext} interface,
|
|
7
|
+
* so the two can reference each other without an import cycle.
|
|
8
|
+
*/
|
|
9
|
+
import type { RecoveryClass } from '../errorCodes.js';
|
|
10
|
+
/**
|
|
11
|
+
* Tri-state outcome of a credential re-mint, mirroring the `getToken`
|
|
12
|
+
* contract (see {@link CredentialLifecycle.refresh}).
|
|
13
|
+
*/
|
|
14
|
+
export type CredentialRefreshOutcome = 'refreshed' | 'session_error' | 'network_error';
|
|
15
|
+
/**
|
|
16
|
+
* What an auth-rejected transport should do after recovery has run. `'retry'`
|
|
17
|
+
* means a fresh credential is now in place — replay the request once. `'stop'`
|
|
18
|
+
* means don't replay: either the session is gone for good, the rejection is one
|
|
19
|
+
* a re-mint can't cure, or the mint failed transiently and the caller's own
|
|
20
|
+
* retry path will recover later.
|
|
21
|
+
*/
|
|
22
|
+
export type CredentialRecoveryOutcome = 'retry' | 'stop';
|
|
23
|
+
/**
|
|
24
|
+
* What a credential refresher may resolve with. The plain-string form returns
|
|
25
|
+
* just the token. The object form also carries the mint response's `expiresAt`
|
|
26
|
+
* (an ISO string, epoch milliseconds, or a `Date`), which lets the proactive
|
|
27
|
+
* pre-roll schedule against the credential's real lifetime rather than assume
|
|
28
|
+
* the server's default expiry.
|
|
29
|
+
*/
|
|
30
|
+
export type CredentialRefreshResult = string | {
|
|
31
|
+
readonly token: string;
|
|
32
|
+
readonly expiresAt?: string | number | Date;
|
|
33
|
+
} | null;
|
|
34
|
+
/** A credential re-mint hook. `Promise<string | null>` resolvers remain
|
|
35
|
+
* assignable — the widened result type is a superset. */
|
|
36
|
+
export type CredentialRefresher = () => Promise<CredentialRefreshResult>;
|
|
37
|
+
/** The fallback pre-roll interval, and also its ceiling: 10 minutes, which sits
|
|
38
|
+
* comfortably inside the server's 15-minute default credential lifetime. Used
|
|
39
|
+
* as-is when the refresher reports no expiry. */
|
|
40
|
+
export declare const DEFAULT_PREROLL_INTERVAL_MS: number;
|
|
41
|
+
/** Floor so a very short (or already-elapsed) TTL can't hot-loop the mint. */
|
|
42
|
+
export declare const MIN_PREROLL_DELAY_MS: number;
|
|
43
|
+
/**
|
|
44
|
+
* Computes how long to wait before pre-rolling a credential that expires at
|
|
45
|
+
* `expiresAtMs`: about two-thirds of the remaining lifetime (a 15-minute
|
|
46
|
+
* credential yields 10 minutes), clamped to
|
|
47
|
+
* [{@link MIN_PREROLL_DELAY_MS}, {@link DEFAULT_PREROLL_INTERVAL_MS}]. An
|
|
48
|
+
* unknown expiry (`null`) falls back to the 10-minute interval. Exported for
|
|
49
|
+
* unit tests.
|
|
50
|
+
*/
|
|
51
|
+
export declare function computePrerollDelayMs(expiresAtMs: number | null, nowMs: number): number;
|
|
52
|
+
/**
|
|
53
|
+
* The callbacks this lifecycle needs back from the surrounding client. It is
|
|
54
|
+
* deliberately minimal — three callbacks, each resolved lazily at call time,
|
|
55
|
+
* because the connection machinery behind two of them isn't constructed until
|
|
56
|
+
* the sync connection is set up.
|
|
57
|
+
*/
|
|
58
|
+
export interface CredentialLifecycleContext {
|
|
59
|
+
/** Push a freshly-minted access token into the shared credential source
|
|
60
|
+
* (no-op when the deployment wired no credential source). */
|
|
61
|
+
setAuthToken(token: string): void;
|
|
62
|
+
/** Nudge the connection to re-probe using the credential now in place. */
|
|
63
|
+
nudgeReconnect(): void;
|
|
64
|
+
/** Report that the long-lived login is gone, so the connection can move to
|
|
65
|
+
* its signed-out state. */
|
|
66
|
+
reportSessionExpired(): void;
|
|
67
|
+
}
|
|
68
|
+
export declare class CredentialLifecycle {
|
|
69
|
+
private readonly ctx;
|
|
70
|
+
/**
|
|
71
|
+
* The hook that mints a fresh short-lived access credential (the `ek_`/`rk_`
|
|
72
|
+
* key). An integrator wires it from their own token endpoint: this lifecycle
|
|
73
|
+
* decides when to refresh (a stale-credential probe or an external nudge),
|
|
74
|
+
* and the hook decides how to mint. It follows the same contract as a
|
|
75
|
+
* `getToken` function — it resolves a token string on success, `null` when
|
|
76
|
+
* the long-lived login is gone (a terminal state), and throws on a transient
|
|
77
|
+
* or offline failure. Used by {@link refresh}. When it is absent there is no
|
|
78
|
+
* silent re-mint, as with a static `apiKey` whose credential source is
|
|
79
|
+
* refreshed elsewhere.
|
|
80
|
+
*/
|
|
81
|
+
private credentialRefresher;
|
|
82
|
+
/** Single-flight guard so a wake nudge + an in-flight request + a probe don't
|
|
83
|
+
* all mint at once (the classic "token thrash → random logout" bug). */
|
|
84
|
+
private inFlightCredentialRefresh;
|
|
85
|
+
/** Tears down the proactive credential lifecycle (the refresh timer and the
|
|
86
|
+
* OS-wake listener) installed by {@link start}; cleared when the client
|
|
87
|
+
* disconnects. Null when no refresher is wired. */
|
|
88
|
+
private credentialLifecycleTeardown;
|
|
89
|
+
/** Epoch milliseconds at which the current credential expires, when the
|
|
90
|
+
* refresher reports it (the object form of {@link CredentialRefreshResult}).
|
|
91
|
+
* `null` for the string-form resolver, in which case the pre-roll uses its
|
|
92
|
+
* fixed interval. */
|
|
93
|
+
private credentialExpiresAtMs;
|
|
94
|
+
/** Re-arms the proactive pre-roll timer (set by {@link start}). Called after
|
|
95
|
+
* every successful mint, so a refresh triggered reactively (by a probe, an
|
|
96
|
+
* OS wake, or the first mint) re-anchors the schedule to the fresh
|
|
97
|
+
* credential's real expiry instead of a stale fixed delay. */
|
|
98
|
+
private prerollReschedule;
|
|
99
|
+
constructor(ctx: CredentialLifecycleContext);
|
|
100
|
+
/**
|
|
101
|
+
* Registers the re-mint hook for the access credential — a function that
|
|
102
|
+
* mints a fresh `ek_`/`rk_` key, typically the integrator's `getToken`. See
|
|
103
|
+
* {@link credentialRefresher}.
|
|
104
|
+
*/
|
|
105
|
+
setRefresher(refresher: CredentialRefresher | null): void;
|
|
106
|
+
/**
|
|
107
|
+
* Re-mints the short-lived access credential, pushes it into the credential
|
|
108
|
+
* source, and reports a three-way outcome the connection layer acts on:
|
|
109
|
+
* - a token string → `'refreshed'` (the fresh key is in place; re-probe and reconnect)
|
|
110
|
+
* - `null` → `'session_error'` (the login itself is gone — terminal, sign out)
|
|
111
|
+
* - a thrown error → `'network_error'` (the mint endpoint was unreachable — transient)
|
|
112
|
+
*
|
|
113
|
+
* The call is single-flight: concurrent triggers (an OS wake, an in-flight
|
|
114
|
+
* request, a probe) share one in-flight promise, so the credential is never
|
|
115
|
+
* minted twice at once. This avoids the failure where every rejected request
|
|
116
|
+
* mints a new token and the resulting thrash logs the user out.
|
|
117
|
+
*
|
|
118
|
+
* With no refresher wired, it resolves `'refreshed'` as a no-op re-probe: a
|
|
119
|
+
* static-`apiKey` client has no session to mint from and its credential
|
|
120
|
+
* source is refreshed elsewhere, so it simply re-probes with what it holds.
|
|
121
|
+
*/
|
|
122
|
+
refresh(): Promise<CredentialRefreshOutcome>;
|
|
123
|
+
/**
|
|
124
|
+
* Interprets a refresh outcome and drives the connection accordingly. This is
|
|
125
|
+
* the single place the three-way outcome is acted on, shared by the proactive
|
|
126
|
+
* pre-roll, the OS-wake nudge, and the HTTP auth-recovery path, so every
|
|
127
|
+
* trigger converges on the same behavior.
|
|
128
|
+
*/
|
|
129
|
+
private routeRefreshOutcome;
|
|
130
|
+
/**
|
|
131
|
+
* The shared recovery path for a request rejected on authentication, over any
|
|
132
|
+
* transport. The WebSocket probe already routes its own 401s; HTTP callers
|
|
133
|
+
* call this instead of inventing their own handling, so every path shares one
|
|
134
|
+
* single-flight mint, one outcome routing, and one taxonomy. It classifies
|
|
135
|
+
* the same recovery codes the connection probe does:
|
|
136
|
+
* - `access_credential_expiry` — the routine case, an expired `ek_`/`rk_`:
|
|
137
|
+
* silently re-mint through the single-flight {@link refresh} and tell the
|
|
138
|
+
* caller to replay once on success. This never signs out on its own; the
|
|
139
|
+
* only terminal path is the mint resolving `null`.
|
|
140
|
+
* - `session_expiry` — the login is gone: report it (which drives sign-out)
|
|
141
|
+
* and stop, since replaying is pointless.
|
|
142
|
+
* - `auth_blocked`, `permission`, and everything else — re-minting would
|
|
143
|
+
* produce the same rejected credential, so stop and leave the connection
|
|
144
|
+
* alone.
|
|
145
|
+
*/
|
|
146
|
+
recoverFromAuthRejection(recovery: RecoveryClass): Promise<CredentialRecoveryOutcome>;
|
|
147
|
+
/**
|
|
148
|
+
* Installs the credential lifecycle. It has two parts:
|
|
149
|
+
* 1. Reactive — registers `getToken` as the re-mint hook the connection
|
|
150
|
+
* calls when a probe finds the key stale, or on a nudge.
|
|
151
|
+
* 2. Proactive — keeps the short-lived key fresh ahead of expiry with a
|
|
152
|
+
* refresh timer inside the credential's lifetime, plus a re-mint on OS
|
|
153
|
+
* wake. The whole proactive block is browser-gated on `typeof window`,
|
|
154
|
+
* because a server render has no socket to keep warm and the resolver is
|
|
155
|
+
* browser-oriented; arming it under Node would fire a relative-URL fetch
|
|
156
|
+
* and throw. (Agents pass a static `apiKey` with no resolver, so this
|
|
157
|
+
* method is never called for them.)
|
|
158
|
+
*
|
|
159
|
+
* Refreshing is automatic — a consumer never calls a refresh method. The call
|
|
160
|
+
* is idempotent: a second call replaces the first, and it is torn down when
|
|
161
|
+
* the client disconnects.
|
|
162
|
+
*
|
|
163
|
+
* `opts.proactiveInNode` arms the refresh timer on a windowless host as well.
|
|
164
|
+
* Set it for agent or system participants — long-lived server sockets whose
|
|
165
|
+
* `rk_`/`ek_` must renew before the server's keepalive check closes them.
|
|
166
|
+
* Node timers are `unref`ed, so a finishing script is never held alive by the
|
|
167
|
+
* pre-roll. The OS-wake listener stays browser-only regardless, since there
|
|
168
|
+
* is no `window` to listen on.
|
|
169
|
+
*/
|
|
170
|
+
start(getToken: CredentialRefresher, opts?: {
|
|
171
|
+
proactiveInNode?: boolean;
|
|
172
|
+
}): void;
|
|
173
|
+
/** Tear down the proactive credential lifecycle (idempotent). */
|
|
174
|
+
stop(): void;
|
|
175
|
+
}
|