@abloatai/ablo 0.26.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/CHANGELOG.md +14 -0
- package/README.md +101 -85
- package/dist/BaseSyncedStore.d.ts +85 -88
- package/dist/BaseSyncedStore.js +131 -147
- package/dist/Database.d.ts +54 -68
- package/dist/Database.js +97 -113
- 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 +37 -52
- package/dist/Model.js +46 -61
- 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 +112 -112
- package/dist/SyncClient.js +165 -172
- 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 +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 +167 -119
- package/dist/client/Ablo.d.ts +73 -73
- package/dist/client/Ablo.js +125 -160
- package/dist/client/ApiClient.d.ts +30 -19
- package/dist/client/ApiClient.js +133 -38
- 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 +14 -17
- package/dist/client/createInternalComponents.js +25 -30
- 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 +57 -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 +67 -87
- package/dist/client/options.d.ts +134 -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 +15 -20
- package/dist/client/wsMutationExecutor.js +17 -23
- 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 +12 -14
- package/dist/core/StoreManager.js +21 -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 +131 -132
- package/dist/errors.d.ts +160 -166
- package/dist/errors.js +155 -158
- package/dist/index.d.ts +30 -27
- package/dist/index.js +89 -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 +122 -131
- package/dist/mutators/UndoManager.js +145 -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 +23 -22
- package/dist/react/useAblo.js +16 -14
- 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 +2 -2
- 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 +33 -42
- 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 +10 -11
- package/dist/stores/ObjectStore.js +11 -12
- package/dist/stores/ObjectStoreContract.d.ts +12 -15
- package/dist/stores/SyncActionStore.d.ts +7 -11
- package/dist/stores/SyncActionStore.js +13 -17
- package/dist/surface.d.ts +27 -20
- package/dist/surface.js +27 -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 +139 -165
- 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/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 +3 -3
- package/dist/testing/index.js +2 -2
- 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 +26 -22
- package/dist/testing/mocks/MockWebSocket.js +22 -21
- package/dist/transactions/TransactionQueue.d.ts +181 -176
- package/dist/transactions/TransactionQueue.js +338 -350
- 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/commitPayload.d.ts +48 -52
- package/dist/transactions/commitPayload.js +48 -57
- package/dist/transactions/deltaConfirmation.d.ts +20 -22
- package/dist/transactions/deltaConfirmation.js +37 -45
- package/dist/transactions/optimisticApply.d.ts +49 -0
- package/dist/transactions/optimisticApply.js +65 -0
- package/dist/transactions/{persistedReplay.d.ts → replayValidation.d.ts} +28 -22
- package/dist/transactions/{persistedReplay.js → replayValidation.js} +33 -27
- 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/{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 +79 -86
- package/dist/wire/frames.js +26 -33
- package/dist/wire/index.d.ts +14 -12
- package/dist/wire/index.js +30 -26
- 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/coordination.md +59 -0
- package/package.json +11 -10
- 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/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,7 +205,7 @@ 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
211
|
const ourClaim = ourIndex >= 0 ? line[ourIndex] : undefined;
|
|
@@ -199,6 +218,30 @@ export function createClaimStream(config, transport = null) {
|
|
|
199
218
|
}
|
|
200
219
|
notifyListeners();
|
|
201
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
|
+
}));
|
|
202
245
|
// (3) On reconnect, re-announce every open self-claim — the
|
|
203
246
|
// server's claim state is in-memory and is lost across
|
|
204
247
|
// restarts. Without this, peers would see our claims vanish
|
|
@@ -245,6 +288,39 @@ export function createClaimStream(config, transport = null) {
|
|
|
245
288
|
},
|
|
246
289
|
});
|
|
247
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
|
+
}
|
|
248
324
|
function sendAbandon(claimId, claim) {
|
|
249
325
|
if (!attached?.isConnected())
|
|
250
326
|
return;
|
|
@@ -281,7 +357,7 @@ export function createClaimStream(config, transport = null) {
|
|
|
281
357
|
};
|
|
282
358
|
ownClaims.set(claimId, claim);
|
|
283
359
|
sendBegin(claimId, claim);
|
|
284
|
-
// Coordination trace (info): the creator can
|
|
360
|
+
// Coordination trace (info): the creator can see their human/agent claims.
|
|
285
361
|
getContext().logger.info(`claim: requesting ${claimLabel(claim.entityType, claim.entityId, claim.field)} for "${claim.reason}"` +
|
|
286
362
|
(claim.queue ? ' (will queue if contended)' : ''), { claimId });
|
|
287
363
|
let revoked = false;
|
|
@@ -310,6 +386,7 @@ export function createClaimStream(config, transport = null) {
|
|
|
310
386
|
revoke();
|
|
311
387
|
},
|
|
312
388
|
revoke,
|
|
389
|
+
heartbeat: (options) => sendHeartbeat(claimId, claim, resolveHeartbeatOptions(options)),
|
|
313
390
|
[Symbol.asyncDispose]: async () => {
|
|
314
391
|
revoke();
|
|
315
392
|
},
|
|
@@ -377,6 +454,11 @@ export function createClaimStream(config, transport = null) {
|
|
|
377
454
|
for (const off of unsubs)
|
|
378
455
|
off();
|
|
379
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
|
+
}
|
|
380
462
|
listeners.clear();
|
|
381
463
|
rejectionListeners.clear();
|
|
382
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. */
|
|
@@ -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;
|