@abloatai/ablo 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -2
- package/README.md +102 -86
- package/dist/BaseSyncedStore.d.ts +85 -88
- package/dist/BaseSyncedStore.js +134 -151
- package/dist/Database.d.ts +68 -69
- package/dist/Database.js +316 -135
- package/dist/{ObjectPool.d.ts → InstanceCache.d.ts} +10 -13
- package/dist/{ObjectPool.js → InstanceCache.js} +85 -83
- package/dist/LazyReferenceCollection.d.ts +11 -15
- package/dist/LazyReferenceCollection.js +12 -16
- package/dist/Model.d.ts +54 -52
- package/dist/Model.js +78 -62
- package/dist/ModelRegistry.d.ts +21 -19
- package/dist/ModelRegistry.js +23 -27
- package/dist/NetworkMonitor.d.ts +5 -6
- package/dist/NetworkMonitor.js +5 -6
- package/dist/SyncClient.d.ts +122 -118
- package/dist/SyncClient.js +541 -245
- package/dist/adapters/alwaysOnline.d.ts +6 -8
- package/dist/adapters/alwaysOnline.js +6 -8
- package/dist/adapters/inMemoryStorage.d.ts +10 -9
- package/dist/adapters/inMemoryStorage.js +21 -9
- package/dist/agent/Agent.d.ts +27 -32
- package/dist/agent/Agent.js +18 -19
- package/dist/agent/index.d.ts +4 -4
- package/dist/agent/index.js +5 -5
- package/dist/agent/session.d.ts +47 -44
- package/dist/agent/session.js +37 -48
- package/dist/agent/types.d.ts +26 -31
- package/dist/agent/types.js +6 -7
- package/dist/ai-sdk/coordinatedTool.d.ts +108 -0
- package/dist/ai-sdk/{coordinated-tool.js → coordinatedTool.js} +44 -38
- package/dist/ai-sdk/coordinationContext.d.ts +46 -0
- package/dist/ai-sdk/{coordination-context.js → coordinationContext.js} +26 -33
- package/dist/ai-sdk/index.d.ts +25 -22
- package/dist/ai-sdk/index.js +25 -22
- package/dist/ai-sdk/wrap.d.ts +6 -7
- package/dist/ai-sdk/wrap.js +1 -1
- package/dist/auth/credentialPolicy.d.ts +69 -74
- package/dist/auth/credentialPolicy.js +51 -56
- package/dist/auth/credentialSource.d.ts +6 -5
- package/dist/auth/credentialSource.js +9 -10
- package/dist/auth/index.d.ts +59 -58
- package/dist/auth/index.js +31 -37
- package/dist/auth/schemas.d.ts +5 -4
- package/dist/auth/schemas.js +5 -4
- package/dist/batching/index.d.ts +19 -21
- package/dist/batching/index.js +14 -17
- package/dist/cli.cjs +173 -121
- package/dist/client/Ablo.d.ts +97 -74
- package/dist/client/Ablo.js +129 -163
- package/dist/client/ApiClient.d.ts +30 -19
- package/dist/client/ApiClient.js +442 -81
- package/dist/client/auth.d.ts +47 -47
- package/dist/client/auth.js +108 -117
- package/dist/client/claimHeartbeatLoop.d.ts +50 -0
- package/dist/client/claimHeartbeatLoop.js +88 -0
- package/dist/client/consoleLogger.d.ts +5 -6
- package/dist/client/consoleLogger.js +5 -6
- package/dist/client/createInternalComponents.d.ts +16 -17
- package/dist/client/createInternalComponents.js +26 -31
- package/dist/client/createModelProxy.d.ts +130 -120
- package/dist/client/createModelProxy.js +152 -122
- package/dist/client/credentialEndpoint.d.ts +40 -42
- package/dist/client/credentialEndpoint.js +35 -36
- package/dist/client/functionalUpdate.d.ts +29 -27
- package/dist/client/functionalUpdate.js +21 -21
- package/dist/client/hostedEndpoints.d.ts +9 -12
- package/dist/client/hostedEndpoints.js +9 -12
- package/dist/client/httpClient.d.ts +59 -53
- package/dist/client/httpClient.js +29 -31
- package/dist/client/identity.d.ts +15 -20
- package/dist/client/identity.js +47 -58
- package/dist/client/modelRegistration.d.ts +5 -9
- package/dist/client/modelRegistration.js +78 -87
- package/dist/client/options.d.ts +157 -157
- package/dist/client/options.js +3 -7
- package/dist/client/registerDataSource.d.ts +9 -9
- package/dist/client/registerDataSource.js +15 -16
- package/dist/client/resourceTypes.d.ts +64 -75
- package/dist/client/resourceTypes.js +4 -10
- package/dist/client/schemaConfig.d.ts +31 -43
- package/dist/client/schemaConfig.js +38 -50
- package/dist/client/sessionMint.d.ts +16 -12
- package/dist/client/sessionMint.js +26 -31
- package/dist/client/validateAbloOptions.d.ts +12 -14
- package/dist/client/validateAbloOptions.js +8 -9
- package/dist/client/writeOptionsSchema.d.ts +18 -16
- package/dist/client/writeOptionsSchema.js +23 -20
- package/dist/client/wsMutationExecutor.d.ts +16 -20
- package/dist/client/wsMutationExecutor.js +18 -23
- package/dist/commit/contract.d.ts +493 -0
- package/dist/commit/contract.js +187 -0
- package/dist/commit/index.d.ts +6 -0
- package/dist/commit/index.js +5 -0
- package/dist/context.d.ts +6 -4
- package/dist/context.js +6 -4
- package/dist/coordination/index.d.ts +10 -8
- package/dist/coordination/index.js +14 -12
- package/dist/coordination/schema.d.ts +176 -128
- package/dist/coordination/schema.js +197 -133
- package/dist/coordination/trace.d.ts +9 -10
- package/dist/coordination/trace.js +13 -14
- package/dist/core/DatabaseManager.d.ts +5 -7
- package/dist/core/DatabaseManager.js +15 -19
- package/dist/core/QueryProcessor.d.ts +7 -9
- package/dist/core/QueryProcessor.js +22 -28
- package/dist/core/QueryView.d.ts +8 -8
- package/dist/core/QueryView.js +2 -2
- package/dist/core/StoreManager.d.ts +14 -14
- package/dist/core/StoreManager.js +33 -24
- package/dist/core/ViewRegistry.d.ts +5 -5
- package/dist/core/ViewRegistry.js +4 -4
- package/dist/core/index.d.ts +17 -12
- package/dist/core/index.js +32 -26
- package/dist/core/openIDBWithTimeout.d.ts +38 -36
- package/dist/core/openIDBWithTimeout.js +42 -43
- package/dist/core/queryUtils.d.ts +45 -0
- package/dist/core/queryUtils.js +69 -0
- package/dist/core/storeContract.d.ts +63 -61
- package/dist/core/storeContract.js +8 -12
- package/dist/environment.d.ts +28 -0
- package/dist/environment.js +21 -0
- package/dist/errorCodes.d.ts +107 -99
- package/dist/errorCodes.js +137 -134
- package/dist/errors.d.ts +160 -166
- package/dist/errors.js +155 -158
- package/dist/index.d.ts +36 -27
- package/dist/index.js +91 -86
- package/dist/interfaces/index.d.ts +102 -113
- package/dist/interfaces/index.js +5 -4
- package/dist/keys/index.d.ts +27 -29
- package/dist/keys/index.js +41 -40
- package/dist/mutators/RecordingTransaction.d.ts +16 -16
- package/dist/mutators/RecordingTransaction.js +31 -37
- package/dist/mutators/Transaction.d.ts +18 -26
- package/dist/mutators/Transaction.js +14 -20
- package/dist/mutators/UndoManager.d.ts +124 -131
- package/dist/mutators/UndoManager.js +177 -156
- package/dist/mutators/defineMutators.d.ts +23 -34
- package/dist/mutators/defineMutators.js +14 -20
- package/dist/mutators/inverseOp.d.ts +12 -15
- package/dist/mutators/inverseOp.js +12 -15
- package/dist/mutators/mutateActions.d.ts +10 -9
- package/dist/mutators/mutateActions.js +1 -1
- package/dist/mutators/readerActions.d.ts +9 -8
- package/dist/mutators/readerActions.js +2 -2
- package/dist/mutators/undoApply.d.ts +31 -27
- package/dist/mutators/undoApply.js +26 -24
- package/dist/policy/index.d.ts +5 -3
- package/dist/policy/index.js +5 -3
- package/dist/policy/types.d.ts +104 -100
- package/dist/policy/types.js +67 -66
- package/dist/query/client.d.ts +28 -23
- package/dist/query/client.js +45 -43
- package/dist/query/types.d.ts +37 -60
- package/dist/query/types.js +13 -33
- package/dist/react/AbloProvider.d.ts +1 -1
- package/dist/react/AbloProvider.js +2 -2
- package/dist/react/context.d.ts +25 -28
- package/dist/react/context.js +9 -10
- package/dist/react/index.d.ts +41 -42
- package/dist/react/index.js +37 -38
- package/dist/react/internalContext.d.ts +17 -19
- package/dist/react/useAblo.d.ts +28 -25
- package/dist/react/useAblo.js +41 -17
- package/dist/react/useCurrentUserId.d.ts +8 -7
- package/dist/react/useCurrentUserId.js +8 -7
- package/dist/react/useErrorListener.d.ts +7 -7
- package/dist/react/useErrorListener.js +10 -11
- package/dist/react/useMutationFailureListener.d.ts +8 -8
- package/dist/react/useMutationFailureListener.js +8 -8
- package/dist/react/useMutators.d.ts +11 -11
- package/dist/react/useMutators.js +3 -3
- package/dist/react/useReactive.js +2 -2
- package/dist/react/useSyncStatus.d.ts +4 -6
- package/dist/react/useUndoScope.d.ts +7 -9
- package/dist/react/useUndoScope.js +1 -1
- package/dist/schema/coordination.d.ts +21 -25
- package/dist/schema/coordination.js +21 -25
- package/dist/schema/ddl.d.ts +43 -39
- package/dist/schema/ddl.js +75 -68
- package/dist/schema/ddlLock.d.ts +20 -24
- package/dist/schema/ddlLock.js +18 -23
- package/dist/schema/diff.d.ts +99 -61
- package/dist/schema/diff.js +43 -34
- package/dist/schema/field.d.ts +37 -42
- package/dist/schema/field.js +35 -48
- package/dist/schema/generate.d.ts +12 -12
- package/dist/schema/generate.js +12 -12
- package/dist/schema/index.d.ts +3 -3
- package/dist/schema/index.js +21 -23
- package/dist/schema/model.d.ts +118 -143
- package/dist/schema/model.js +22 -33
- package/dist/schema/openapi.d.ts +10 -9
- package/dist/schema/openapi.js +5 -3
- package/dist/schema/queries.d.ts +29 -31
- package/dist/schema/queries.js +23 -25
- package/dist/schema/relation.d.ts +89 -99
- package/dist/schema/relation.js +13 -13
- package/dist/schema/residency.d.ts +16 -13
- package/dist/schema/residency.js +16 -13
- package/dist/schema/roles.d.ts +36 -43
- package/dist/schema/roles.js +31 -37
- package/dist/schema/schema.d.ts +64 -43
- package/dist/schema/schema.js +31 -32
- package/dist/schema/select.d.ts +13 -13
- package/dist/schema/select.js +13 -13
- package/dist/schema/serialize.d.ts +28 -31
- package/dist/schema/serialize.js +27 -31
- package/dist/schema/sugar.d.ts +17 -32
- package/dist/schema/sugar.js +14 -29
- package/dist/schema/{sync-delta-row.d.ts → syncDeltaRow.d.ts} +26 -49
- package/dist/schema/syncDeltaRow.js +89 -0
- package/dist/schema/tenancy.d.ts +44 -46
- package/dist/schema/tenancy.js +46 -48
- package/dist/server/adapter.d.ts +58 -58
- package/dist/server/adapter.js +13 -14
- package/dist/server/commit.d.ts +60 -64
- package/dist/server/index.d.ts +9 -10
- package/dist/server/index.js +1 -1
- package/dist/server/readConfig.d.ts +70 -0
- package/dist/server/readConfig.js +8 -0
- package/dist/server/storageMode.d.ts +23 -0
- package/dist/server/storageMode.js +17 -0
- package/dist/source/adapter.d.ts +30 -25
- package/dist/source/adapter.js +10 -10
- package/dist/source/adapters/drizzle.d.ts +28 -23
- package/dist/source/adapters/drizzle.js +30 -25
- package/dist/source/adapters/kysely.d.ts +27 -25
- package/dist/source/adapters/kysely.js +24 -23
- package/dist/source/adapters/memory.d.ts +8 -7
- package/dist/source/adapters/memory.js +9 -8
- package/dist/source/adapters/prisma.d.ts +13 -12
- package/dist/source/adapters/prisma.js +22 -25
- package/dist/source/conformance.d.ts +18 -11
- package/dist/source/conformance.js +17 -11
- package/dist/source/connector.d.ts +31 -32
- package/dist/source/connector.js +28 -28
- package/dist/source/connectorProtocol.d.ts +160 -0
- package/dist/source/connectorProtocol.js +162 -0
- package/dist/source/contract.d.ts +26 -27
- package/dist/source/contract.js +28 -29
- package/dist/source/factory.d.ts +46 -58
- package/dist/source/factory.js +22 -27
- package/dist/source/index.d.ts +7 -9
- package/dist/source/index.js +12 -14
- package/dist/source/migrations.d.ts +9 -9
- package/dist/source/migrations.js +9 -9
- package/dist/source/next.d.ts +9 -10
- package/dist/source/next.js +6 -7
- package/dist/source/pushQueue.d.ts +69 -47
- package/dist/source/pushQueue.js +32 -28
- package/dist/source/signing.d.ts +46 -17
- package/dist/source/signing.js +28 -11
- package/dist/source/types.d.ts +121 -104
- package/dist/source/types.js +13 -14
- package/dist/stores/ObjectStore.d.ts +24 -12
- package/dist/stores/ObjectStore.js +38 -16
- package/dist/stores/ObjectStoreContract.d.ts +14 -15
- package/dist/stores/SyncActionStore.d.ts +7 -11
- package/dist/stores/SyncActionStore.js +13 -17
- package/dist/surface.d.ts +28 -21
- package/dist/surface.js +29 -20
- package/dist/sync/{BootstrapHelper.d.ts → BootstrapFetcher.d.ts} +36 -42
- package/dist/sync/{BootstrapHelper.js → BootstrapFetcher.js} +76 -76
- package/dist/sync/ConnectionManager.d.ts +39 -50
- package/dist/sync/ConnectionManager.js +55 -66
- package/dist/sync/NetworkProbe.d.ts +24 -29
- package/dist/sync/NetworkProbe.js +63 -69
- package/dist/sync/{HydrationCoordinator.d.ts → OnDemandLoader.d.ts} +42 -41
- package/dist/sync/{HydrationCoordinator.js → OnDemandLoader.js} +59 -54
- package/dist/sync/{AreaOfInterestManager.d.ts → SubscriptionManager.d.ts} +43 -57
- package/dist/sync/{AreaOfInterestManager.js → SubscriptionManager.js} +43 -51
- package/dist/sync/SyncWebSocket.d.ts +141 -166
- package/dist/sync/SyncWebSocket.js +191 -223
- package/dist/sync/awaitClaimGrant.d.ts +18 -18
- package/dist/sync/awaitClaimGrant.js +11 -11
- package/dist/sync/bootstrapApply.d.ts +34 -24
- package/dist/sync/bootstrapApply.js +27 -19
- package/dist/sync/commitFrames.d.ts +21 -20
- package/dist/sync/commitFrames.js +18 -18
- package/dist/sync/createClaimStream.d.ts +23 -22
- package/dist/sync/createClaimStream.js +105 -23
- package/dist/sync/createPresenceStream.d.ts +19 -18
- package/dist/sync/createPresenceStream.js +25 -26
- package/dist/sync/createSnapshot.d.ts +12 -14
- package/dist/sync/createSnapshot.js +20 -26
- package/dist/sync/credentialLifecycle.d.ts +104 -104
- package/dist/sync/credentialLifecycle.js +140 -147
- package/dist/sync/deltaPipeline.d.ts +36 -34
- package/dist/sync/deltaPipeline.js +64 -65
- package/dist/sync/groupChange.d.ts +63 -61
- package/dist/sync/groupChange.js +74 -78
- package/dist/sync/heartbeat.d.ts +34 -33
- package/dist/sync/heartbeat.js +31 -31
- package/dist/sync/participants.d.ts +19 -19
- package/dist/sync/persistedPrefix.d.ts +12 -0
- package/dist/sync/persistedPrefix.js +22 -0
- package/dist/sync/schemas.d.ts +3 -2
- package/dist/sync/schemas.js +14 -10
- package/dist/sync/syncCursor.d.ts +17 -21
- package/dist/sync/syncCursor.js +17 -21
- package/dist/sync/syncPlan.d.ts +28 -36
- package/dist/sync/syncPlan.js +18 -19
- package/dist/sync/syncPosition.d.ts +54 -49
- package/dist/sync/syncPosition.js +57 -52
- package/dist/sync/wsFrameHandlers.d.ts +35 -36
- package/dist/sync/wsFrameHandlers.js +63 -67
- package/dist/testing/fixtures/bootstrap.d.ts +12 -6
- package/dist/testing/fixtures/bootstrap.js +12 -6
- package/dist/testing/fixtures/deltas.d.ts +30 -33
- package/dist/testing/fixtures/deltas.js +30 -33
- package/dist/testing/fixtures/models.d.ts +11 -10
- package/dist/testing/fixtures/models.js +11 -10
- package/dist/testing/helpers/{react-wrapper.d.ts → reactWrapper.d.ts} +13 -10
- package/dist/testing/helpers/{react-wrapper.js → reactWrapper.js} +15 -12
- package/dist/testing/helpers/{sync-engine-harness.d.ts → syncEngineHarness.d.ts} +17 -15
- package/dist/testing/helpers/{sync-engine-harness.js → syncEngineHarness.js} +12 -10
- package/dist/testing/helpers/wait.d.ts +13 -8
- package/dist/testing/helpers/wait.js +13 -8
- package/dist/testing/index.d.ts +5 -3
- package/dist/testing/index.js +3 -2
- package/dist/testing/mocks/FakeDatabase.d.ts +18 -0
- package/dist/testing/mocks/FakeDatabase.js +10 -0
- package/dist/testing/mocks/MockMutationExecutor.d.ts +18 -17
- package/dist/testing/mocks/MockMutationExecutor.js +15 -14
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +8 -8
- package/dist/testing/mocks/MockNetworkMonitor.js +8 -8
- package/dist/testing/mocks/MockSyncContext.d.ts +20 -17
- package/dist/testing/mocks/MockSyncContext.js +15 -13
- package/dist/testing/mocks/MockSyncStore.d.ts +10 -10
- package/dist/testing/mocks/MockSyncStore.js +11 -11
- package/dist/testing/mocks/MockWebSocket.d.ts +28 -23
- package/dist/testing/mocks/MockWebSocket.js +22 -21
- package/dist/transactions/TransactionQueue.d.ts +244 -181
- package/dist/transactions/TransactionQueue.js +929 -423
- package/dist/transactions/TransactionStore.d.ts +6 -4
- package/dist/transactions/TransactionStore.js +6 -4
- package/dist/transactions/UnconfirmedWrites.d.ts +82 -0
- package/dist/transactions/UnconfirmedWrites.js +104 -0
- package/dist/transactions/coalesceRules.d.ts +41 -17
- package/dist/transactions/coalesceRules.js +40 -17
- package/dist/transactions/commitEnvelope.d.ts +132 -0
- package/dist/transactions/commitEnvelope.js +139 -0
- package/dist/transactions/commitOutboxStore.d.ts +32 -0
- package/dist/transactions/commitOutboxStore.js +26 -0
- package/dist/transactions/commitPayload.d.ts +63 -52
- package/dist/transactions/commitPayload.js +54 -57
- package/dist/transactions/deltaConfirmation.d.ts +20 -22
- package/dist/transactions/deltaConfirmation.js +37 -45
- package/dist/transactions/httpCommitEnvelope.d.ts +43 -0
- package/dist/transactions/httpCommitEnvelope.js +179 -0
- package/dist/transactions/optimisticApply.d.ts +49 -0
- package/dist/transactions/optimisticApply.js +65 -0
- package/dist/transactions/replayValidation.d.ts +182 -0
- package/dist/transactions/replayValidation.js +156 -0
- package/dist/types/global.d.ts +46 -41
- package/dist/types/global.js +20 -19
- package/dist/types/index.d.ts +71 -77
- package/dist/types/index.js +22 -22
- package/dist/types/modelData.d.ts +6 -8
- package/dist/types/modelData.js +5 -7
- package/dist/types/participant.d.ts +10 -11
- package/dist/types/participant.js +6 -8
- package/dist/types/streams.d.ts +208 -195
- package/dist/types/streams.js +7 -7
- package/dist/utils/asyncIterator.d.ts +25 -32
- package/dist/utils/asyncIterator.js +25 -32
- package/dist/utils/duration.d.ts +12 -15
- package/dist/utils/duration.js +12 -15
- package/dist/utils/mobxSetup.d.ts +53 -0
- package/dist/utils/{mobx-setup.js → mobxSetup.js} +42 -98
- package/dist/webhooks/events.d.ts +21 -16
- package/dist/webhooks/events.js +10 -8
- package/dist/webhooks/index.d.ts +5 -7
- package/dist/webhooks/index.js +5 -7
- package/dist/wire/bootstrapReason.d.ts +9 -0
- package/dist/wire/bootstrapReason.js +8 -0
- package/dist/{schema/sync-delta-wire.d.ts → wire/delta.d.ts} +58 -41
- package/dist/wire/delta.js +114 -0
- package/dist/wire/errorEnvelope.d.ts +30 -31
- package/dist/wire/errorEnvelope.js +34 -40
- package/dist/wire/frames.d.ts +315 -86
- package/dist/wire/frames.js +47 -33
- package/dist/wire/index.d.ts +18 -14
- package/dist/wire/index.js +32 -27
- package/dist/wire/listEnvelope.d.ts +16 -23
- package/dist/wire/listEnvelope.js +7 -6
- package/dist/wire/protocol.d.ts +25 -32
- package/dist/wire/protocol.js +25 -32
- package/dist/wire/protocolVersion.d.ts +44 -40
- package/dist/wire/protocolVersion.js +44 -40
- package/docs/api.md +10 -10
- package/docs/coordination.md +59 -0
- package/docs/mcp.md +1 -1
- package/package.json +17 -11
- package/dist/ai-sdk/coordinated-tool.d.ts +0 -101
- package/dist/ai-sdk/coordination-context.d.ts +0 -52
- package/dist/core/query-utils.d.ts +0 -34
- package/dist/core/query-utils.js +0 -59
- package/dist/schema/sync-delta-row.js +0 -103
- package/dist/schema/sync-delta-wire.js +0 -102
- package/dist/server/read-config.d.ts +0 -67
- package/dist/server/read-config.js +0 -8
- package/dist/server/storage-mode.d.ts +0 -8
- package/dist/server/storage-mode.js +0 -28
- package/dist/source/connector-protocol.d.ts +0 -159
- package/dist/source/connector-protocol.js +0 -161
- package/dist/transactions/OptimisticEchoTracker.d.ts +0 -82
- package/dist/transactions/OptimisticEchoTracker.js +0 -104
- package/dist/transactions/mutation-error-handler.d.ts +0 -5
- package/dist/transactions/mutation-error-handler.js +0 -39
- package/dist/transactions/optimistic.d.ts +0 -24
- package/dist/transactions/optimistic.js +0 -45
- package/dist/transactions/persistedReplay.d.ts +0 -93
- package/dist/transactions/persistedReplay.js +0 -105
- package/dist/utils/mobx-setup.d.ts +0 -42
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - Automatic reconnection with exponential backoff
|
|
2
|
+
* Manages the WebSocket connection to the sync server. It owns the socket
|
|
3
|
+
* lifecycle (connect, reconnect, disconnect), receives and validates the
|
|
4
|
+
* incoming delta stream, sends commits and claims over the same socket, and
|
|
5
|
+
* reconnects automatically with exponential backoff. Consumers subscribe to its
|
|
6
|
+
* typed events (see {@link CoreSyncEventMap}) to react to deltas, presence, and
|
|
7
|
+
* connection changes.
|
|
9
8
|
*/
|
|
10
9
|
import { EventEmitter } from 'events';
|
|
11
10
|
import { getContext } from '../context.js';
|
|
12
11
|
import { AbloConnectionError, AbloError, SyncSessionError, toAbloError, } from '../errors.js';
|
|
13
|
-
import { clientSyncDeltaSchema } from '../
|
|
12
|
+
import { clientSyncDeltaSchema } from '../wire/delta.js';
|
|
14
13
|
// Commit-path frame builders (pure) — extracted leaf; the host re-exports
|
|
15
14
|
// `CommitAck` below so importers keep this module as their path.
|
|
16
15
|
import { buildCommitFrame } from './commitFrames.js';
|
|
@@ -24,11 +23,11 @@ import { HeartbeatController } from './heartbeat.js';
|
|
|
24
23
|
import { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL, } from '../auth/credentialSource.js';
|
|
25
24
|
// SyncObservability replaced by getContext().observability
|
|
26
25
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* How often, while connected, the client polls for any deltas whose best-effort
|
|
27
|
+
* broadcast was lost in transit. This is a client-side eventual-consistency
|
|
28
|
+
* setting, not part of the wire contract, so the server derives nothing from
|
|
29
|
+
* it. That it currently equals the 30-second ping is a coincidence, not a
|
|
30
|
+
* guarantee.
|
|
32
31
|
*/
|
|
33
32
|
const CATCHUP_POLL_INTERVAL_MS = 30_000;
|
|
34
33
|
/**
|
|
@@ -37,8 +36,7 @@ const CATCHUP_POLL_INTERVAL_MS = 30_000;
|
|
|
37
36
|
*/
|
|
38
37
|
const MAX_RECONNECT_DELAY_MS = 30_000;
|
|
39
38
|
// ---------------------------------------------------------------------------
|
|
40
|
-
//
|
|
41
|
-
// Consumers pass their own event types as TCollaboration generic parameter.
|
|
39
|
+
// Consumers pass their own event types as the TCollaboration generic parameter.
|
|
42
40
|
export class SyncWebSocket extends EventEmitter {
|
|
43
41
|
/**
|
|
44
42
|
* Subscribe to events with automatic cleanup.
|
|
@@ -69,10 +67,10 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
69
67
|
/** Periodic catchup interval — polls for missed deltas every 30s while connected */
|
|
70
68
|
catchupInterval = null;
|
|
71
69
|
/**
|
|
72
|
-
* Application-level heartbeat
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
70
|
+
* Application-level heartbeat: ping every 30 seconds and force-close after a
|
|
71
|
+
* 10-second silence. The {@link HeartbeatController} holds the timing and the
|
|
72
|
+
* zombie-socket rationale; the closures below are the only socket access it
|
|
73
|
+
* gets.
|
|
76
74
|
*/
|
|
77
75
|
heartbeat = new HeartbeatController({
|
|
78
76
|
isSocketOpen: () => this.ws?.readyState === WebSocket.OPEN,
|
|
@@ -111,20 +109,19 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
111
109
|
lastForceCloseReason = null;
|
|
112
110
|
sessionErrorAt = null;
|
|
113
111
|
/**
|
|
114
|
-
* Sync-position state
|
|
115
|
-
* cursor
|
|
116
|
-
*
|
|
112
|
+
* Sync-position state: the lastSyncId watermark, version vector, and server
|
|
113
|
+
* cursor. The advance discipline is documented at `sendAck` and `handleDelta`;
|
|
114
|
+
* the state itself lives in {@link SyncCursor}.
|
|
117
115
|
*/
|
|
118
116
|
cursor;
|
|
119
117
|
/** Registered collaboration event keys (colon format) for dispatch in onmessage */
|
|
120
118
|
collaborationEventTypes;
|
|
121
119
|
/**
|
|
122
|
-
*
|
|
123
|
-
* (
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* state it captures exists.
|
|
120
|
+
* A minimal session adapter handed to the inbound frame dispatch table
|
|
121
|
+
* ({@link dispatchWsFrame}). It exposes only the members the handlers touch;
|
|
122
|
+
* the closure members read live state so a reassignment here (for example the
|
|
123
|
+
* `pendingSubscriptions` reset on close) cannot strand a handler on a stale
|
|
124
|
+
* reference. Built in the constructor, after the state it captures exists.
|
|
128
125
|
*/
|
|
129
126
|
frameSession;
|
|
130
127
|
/**
|
|
@@ -135,10 +132,10 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
135
132
|
*/
|
|
136
133
|
pendingMutations = new Map();
|
|
137
134
|
/**
|
|
138
|
-
* In-flight `claim` requests keyed by claimId. Resolved when the
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
135
|
+
* In-flight `claim` requests keyed by claimId. Resolved when the matching
|
|
136
|
+
* `claim_ack` arrives, or rejected on timeout or disconnect — the same
|
|
137
|
+
* request/response pattern as `pendingMutations`, multiplexed over the one
|
|
138
|
+
* connection.
|
|
142
139
|
*/
|
|
143
140
|
pendingClaims = new Map();
|
|
144
141
|
/**
|
|
@@ -152,7 +149,7 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
152
149
|
pendingSubscriptions = [];
|
|
153
150
|
constructor(options) {
|
|
154
151
|
super();
|
|
155
|
-
// Construct WebSocket URL from base
|
|
152
|
+
// Construct the WebSocket URL from the base server URL.
|
|
156
153
|
const baseUrl = options.baseUrl || options.url || "http://localhost:8080";
|
|
157
154
|
const wsProtocol = baseUrl.startsWith('https') ? 'wss' : 'ws';
|
|
158
155
|
const wsUrl = baseUrl.replace(/^https?/, wsProtocol) + '/api/sync/ws';
|
|
@@ -173,9 +170,9 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
173
170
|
};
|
|
174
171
|
this.cursor = new SyncCursor(this.options.lastSyncId);
|
|
175
172
|
this.collaborationEventTypes = new Set(options.collaborationEvents ?? ['sheet:selection', 'slide:selection', 'slide:cursor']);
|
|
176
|
-
// Session slice for the inbound frame dispatch table — see the field
|
|
177
|
-
//
|
|
178
|
-
//
|
|
173
|
+
// Session slice for the inbound frame dispatch table — see the field doc on
|
|
174
|
+
// `frameSession` for why reassigned members are exposed through closures
|
|
175
|
+
// instead of captured references.
|
|
179
176
|
this.frameSession = {
|
|
180
177
|
emit: (event, ...args) => this.emit(event, ...args),
|
|
181
178
|
pendingMutations: this.pendingMutations,
|
|
@@ -238,18 +235,18 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
238
235
|
}
|
|
239
236
|
this.isConnecting = true;
|
|
240
237
|
this.isManualClose = false;
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
238
|
+
// One credential, server-resolved identity. The bearer travels in a
|
|
239
|
+
// `Sec-WebSocket-Protocol` value (built below), not the URL. The server is
|
|
240
|
+
// bearer-only and resolves identity from the verified token; userId and
|
|
241
|
+
// organizationId are never read from URL parameters.
|
|
245
242
|
const params = new URLSearchParams({
|
|
246
243
|
// Intentionally omit lastSyncId, capabilities from URL; these are sent in sync_request
|
|
247
244
|
// and ack messages to avoid stale baselines on reconnect.
|
|
248
245
|
cursor: this.cursor.syncCursor || '',
|
|
249
246
|
});
|
|
250
|
-
// Participant kind — defaults to `user` for
|
|
251
|
-
//
|
|
252
|
-
//
|
|
247
|
+
// Participant kind — defaults to `user` for session connections. Agent
|
|
248
|
+
// runtimes pass `'agent'` so the server's capability-token path activates
|
|
249
|
+
// instead of session auth.
|
|
253
250
|
if (this.options.kind && this.options.kind !== 'user') {
|
|
254
251
|
params.set('kind', this.options.kind);
|
|
255
252
|
}
|
|
@@ -258,13 +255,13 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
258
255
|
params.append('syncGroups', group);
|
|
259
256
|
});
|
|
260
257
|
const wsUrl = `${this.options.url}?${params.toString()}`;
|
|
261
|
-
// Carry the bearer in a `Sec-WebSocket-Protocol` value,
|
|
262
|
-
// browser
|
|
263
|
-
// subprotocols — and unlike the query string, those
|
|
264
|
-
// access logs, proxies, or browser history. The server reads
|
|
265
|
-
// `ablo.bearer.<token>` and selects the real `ablo.sync.v1` protocol,
|
|
266
|
-
//
|
|
267
|
-
// which is
|
|
258
|
+
// Carry the bearer in a `Sec-WebSocket-Protocol` value, not the URL. A
|
|
259
|
+
// browser cannot set an Authorization header on a WebSocket, but it can
|
|
260
|
+
// offer subprotocols — and unlike the query string, those do not land in
|
|
261
|
+
// load-balancer access logs, proxies, or browser history. The server reads
|
|
262
|
+
// `ablo.bearer.<token>` and selects the real `ablo.sync.v1` protocol, never
|
|
263
|
+
// echoing the token-bearing value back. The token is the raw `ek_`/`rk_`,
|
|
264
|
+
// which is safe as a subprotocol value (alphanumerics and `_`).
|
|
268
265
|
const authToken = this.resolveAuthToken();
|
|
269
266
|
const protocols = authToken
|
|
270
267
|
? [`${WS_BEARER_SUBPROTOCOL_PREFIX}${authToken}`, WS_SYNC_SUBPROTOCOL]
|
|
@@ -290,14 +287,14 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
290
287
|
* Setup WebSocket event handlers
|
|
291
288
|
*/
|
|
292
289
|
setupEventHandlers() {
|
|
293
|
-
// Capture the socket
|
|
294
|
-
// `this.ws === socket` (onclose additionally tolerates a nulled
|
|
295
|
-
// see there) so a handler firing late, after `connect()` replaced the
|
|
296
|
-
// socket, can never clobber the
|
|
297
|
-
//
|
|
298
|
-
// stopCatchupInterval(); stopHeartbeat()`
|
|
299
|
-
//
|
|
300
|
-
//
|
|
290
|
+
// Capture the socket this call wires. Every handler below guards on
|
|
291
|
+
// `this.ws === socket` (onclose additionally tolerates a nulled field —
|
|
292
|
+
// see there), so a handler firing late, after `connect()` has replaced the
|
|
293
|
+
// socket, can never clobber the new connection's shared state. Without this
|
|
294
|
+
// guard, an old socket's `onclose` would unconditionally run `this.ws =
|
|
295
|
+
// null; stopCatchupInterval(); stopHeartbeat()` — a reconnect during close
|
|
296
|
+
// teardown then orphaned the fresh socket (a zombie receiving deltas with
|
|
297
|
+
// no timers and broken send paths).
|
|
301
298
|
const socket = this.ws;
|
|
302
299
|
if (!socket)
|
|
303
300
|
return;
|
|
@@ -337,10 +334,9 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
337
334
|
}
|
|
338
335
|
this.requestIncrementalSync().catch(reportSyncRequestFailure);
|
|
339
336
|
// Start periodic catchup — polls for missed deltas every
|
|
340
|
-
// CATCHUP_POLL_INTERVAL_MS while connected. Real-time WebSocket
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
// that were committed to the DB but whose broadcast was lost in
|
|
337
|
+
// CATCHUP_POLL_INTERVAL_MS while connected. Real-time WebSocket delivery
|
|
338
|
+
// is best-effort, so this interval guarantees eventual consistency by
|
|
339
|
+
// fetching any deltas that were committed but whose broadcast was lost in
|
|
344
340
|
// transit.
|
|
345
341
|
this.stopCatchupInterval();
|
|
346
342
|
this.catchupInterval = setInterval(() => {
|
|
@@ -355,7 +351,7 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
355
351
|
});
|
|
356
352
|
}
|
|
357
353
|
}, CATCHUP_POLL_INTERVAL_MS);
|
|
358
|
-
// Start application-level heartbeat
|
|
354
|
+
// Start the application-level heartbeat (see HeartbeatController).
|
|
359
355
|
this.heartbeat.start();
|
|
360
356
|
};
|
|
361
357
|
socket.onmessage = (event) => {
|
|
@@ -366,7 +362,7 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
366
362
|
// the frame-envelope guard before dispatch. Payload-level
|
|
367
363
|
// validation (deltas etc.) happens per-frame downstream.
|
|
368
364
|
const message = JSON.parse(event.data);
|
|
369
|
-
//
|
|
365
|
+
// Any inbound frame proves the socket is alive — clear the
|
|
370
366
|
// heartbeat-timeout timer so we don't false-trip force-close
|
|
371
367
|
// during normal traffic.
|
|
372
368
|
this.heartbeat.clearHeartbeatTimeout();
|
|
@@ -376,10 +372,9 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
376
372
|
});
|
|
377
373
|
return;
|
|
378
374
|
}
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
// events are all routed there too.
|
|
375
|
+
// Dispatch by frame type (see dispatchWsFrame). The session adapter
|
|
376
|
+
// exposes only the members the handlers touch; keepalives, the older
|
|
377
|
+
// bare-delta form, and collaboration events are all routed there too.
|
|
383
378
|
dispatchWsFrame(this.frameSession, message);
|
|
384
379
|
}
|
|
385
380
|
catch (error) {
|
|
@@ -399,8 +394,8 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
399
394
|
this.emit('error', new AbloConnectionError('Network is offline', { code: 'bootstrap_offline' }));
|
|
400
395
|
return;
|
|
401
396
|
}
|
|
402
|
-
// After session error, suppress
|
|
403
|
-
// Still emit so
|
|
397
|
+
// After a session error, suppress error capture — the root cause is
|
|
398
|
+
// already reported. Still emit so the store can update UI state.
|
|
404
399
|
const error = new AbloConnectionError(`WebSocket connection failed`);
|
|
405
400
|
if (!this._sessionErrorDetected) {
|
|
406
401
|
getContext().observability.captureWebSocketError({
|
|
@@ -411,7 +406,7 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
411
406
|
this.emit('error', error);
|
|
412
407
|
};
|
|
413
408
|
socket.onclose = (event) => {
|
|
414
|
-
// Stale-socket close: a
|
|
409
|
+
// Stale-socket close: a newer socket already owns the connection
|
|
415
410
|
// state — don't null it, stop its timers, or schedule a duplicate
|
|
416
411
|
// reconnect (the orphaning race this guard exists for). The one
|
|
417
412
|
// deliberate asymmetry vs the other handlers: `this.ws === null`
|
|
@@ -480,9 +475,9 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
480
475
|
}
|
|
481
476
|
this.pendingSubscriptions = [];
|
|
482
477
|
}
|
|
483
|
-
// Protocol-version rejection (4010):
|
|
484
|
-
//
|
|
485
|
-
// forward
|
|
478
|
+
// Protocol-version rejection (4010): terminal. Reconnecting cannot heal a
|
|
479
|
+
// version mismatch — only upgrading the SDK, or rolling the server
|
|
480
|
+
// forward, can — so a blind retry here would loop forever against the
|
|
486
481
|
// same typed close. Surface it and stop.
|
|
487
482
|
if (event.code === WS_CLOSE_PROTOCOL_VERSION) {
|
|
488
483
|
getContext().observability.captureWebSocketError({
|
|
@@ -511,25 +506,24 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
511
506
|
reason: event.reason,
|
|
512
507
|
});
|
|
513
508
|
this.emit('session_error', new SyncSessionError(event.reason || 'Session expired', event.code));
|
|
514
|
-
// Don't reconnect from
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
// BaseSyncedStore.setupWebSocketSync.
|
|
509
|
+
// Don't reconnect from here. For a genuine session loss the user must
|
|
510
|
+
// re-authenticate; for an expired access credential (`credential_expired`)
|
|
511
|
+
// the store's session-error handler re-mints, clears the latch, and
|
|
512
|
+
// drives the reconnect itself.
|
|
519
513
|
this.emit('disconnected', event);
|
|
520
514
|
return;
|
|
521
515
|
}
|
|
522
516
|
// Handshake failure: `onclose` fired before `onopen` ever did, so the
|
|
523
|
-
// server rejected the upgrade (typically 401/403 on a bad cookie, but
|
|
524
|
-
// could also be a CORS/origin reject or
|
|
525
|
-
// the HTTP status behind code 1006, so we
|
|
517
|
+
// server rejected the upgrade (typically 401/403 on a bad cookie, but it
|
|
518
|
+
// could also be a CORS/origin reject or a load-balancer 5xx). The browser
|
|
519
|
+
// hides the HTTP status behind code 1006, so we cannot tell which from
|
|
520
|
+
// here.
|
|
526
521
|
//
|
|
527
|
-
// Emit a dedicated event and
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
// stale cookies.
|
|
522
|
+
// Emit a dedicated event and skip the internal reconnect — the owner
|
|
523
|
+
// should run an auth-validating HTTP probe to distinguish session expiry
|
|
524
|
+
// from a transient network issue and transition the UI accordingly.
|
|
525
|
+
// Reconnecting blindly is what produced the infinite
|
|
526
|
+
// "offline → reconnecting → offline" loop on stale cookies.
|
|
533
527
|
if (!everOpened && !this.isManualClose) {
|
|
534
528
|
getContext().observability.captureWebSocketError({
|
|
535
529
|
context: 'handshake-failed-close',
|
|
@@ -548,28 +542,25 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
548
542
|
};
|
|
549
543
|
}
|
|
550
544
|
/**
|
|
551
|
-
*
|
|
552
|
-
* seam every inbound delta (`delta` frame, batch element, `sync_response`
|
|
553
|
-
* replay,
|
|
554
|
-
* persisted
|
|
545
|
+
* Validates and normalizes a wire delta at the receive boundary — the single
|
|
546
|
+
* seam every inbound delta (a `delta` frame, a batch element, a `sync_response`
|
|
547
|
+
* replay, or the older bare frame) passes through before it is emitted,
|
|
548
|
+
* persisted, or allowed to advance any watermark.
|
|
555
549
|
*
|
|
556
|
-
* Normalization
|
|
557
|
-
* - `id`: the contract says `number`, but
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
* nullable (and `createdBy` as a nested ParticipantRef); the client
|
|
566
|
-
* contract types them as optional strings and never reads them.
|
|
567
|
-
* Normalize to absent instead of rejecting every real server delta.
|
|
550
|
+
* Normalization keeps already-deployed servers compatible:
|
|
551
|
+
* - `id`: the contract says `number`, but some servers have sent the raw
|
|
552
|
+
* Postgres BIGINT serialization — a string — and every downstream watermark
|
|
553
|
+
* gate treats a string as invalid, so acks are withheld, the resume cursor
|
|
554
|
+
* never advances, and every reconnect replays from zero. Coerce it once here.
|
|
555
|
+
* - `transactionId` / `createdBy`: the server projection sends these as
|
|
556
|
+
* nullable (and `createdBy` as a nested reference); the client contract
|
|
557
|
+
* types them as optional strings and never reads them, so normalize them to
|
|
558
|
+
* absent rather than reject every real server delta.
|
|
568
559
|
*
|
|
569
|
-
* Validation
|
|
570
|
-
* contract. A frame that fails is
|
|
571
|
-
*
|
|
572
|
-
*
|
|
560
|
+
* Validation runs `clientSyncDeltaSchema.safeParse`, the canonical wire
|
|
561
|
+
* contract. A frame that fails is dropped (returns `null`) with a debug log
|
|
562
|
+
* and an observability breadcrumb; it is never applied. There is one parse per
|
|
563
|
+
* delta — callers must not re-parse.
|
|
573
564
|
*/
|
|
574
565
|
normalizeWireDelta(raw) {
|
|
575
566
|
let candidate = raw;
|
|
@@ -614,30 +605,27 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
614
605
|
id: delta.modelId,
|
|
615
606
|
syncId: delta.id,
|
|
616
607
|
});
|
|
617
|
-
//
|
|
618
|
-
// must stay consistent with what
|
|
619
|
-
// next `requestIncrementalSync()` (and the connect-time handshake)
|
|
620
|
-
//
|
|
621
|
-
// landed in
|
|
622
|
-
// which
|
|
623
|
-
//
|
|
624
|
-
// read in the same transaction as the client view" rule.
|
|
608
|
+
// Do not advance `this.cursor.lastSyncId` on receipt. The runtime cursor
|
|
609
|
+
// must stay consistent with what has been persisted locally; otherwise the
|
|
610
|
+
// next `requestIncrementalSync()` (and the connect-time handshake) would
|
|
611
|
+
// send an optimistic cursor and the server would skip deltas that never
|
|
612
|
+
// landed in local storage. `this.cursor.lastSyncId` advances only in
|
|
613
|
+
// `sendAck()`, which the store gates on its persisted-syncId watermark, so
|
|
614
|
+
// the cursor is never read ahead of the persisted client view.
|
|
625
615
|
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
616
|
+
// The version vector is intentionally not updated here for the same reason;
|
|
617
|
+
// it is left to the persistence-gated path.
|
|
628
618
|
// Emit delta for processing. Ack will be sent by SyncedStore after persistence.
|
|
629
619
|
this.emit('delta', delta);
|
|
630
620
|
}
|
|
631
621
|
/**
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
* `
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
* cursor never gets ahead of the persisted view, so reconnect/
|
|
640
|
-
* catch-up requests can't accidentally skip un-persisted deltas.
|
|
622
|
+
* Acknowledges received deltas up to the given syncId. This is the only place
|
|
623
|
+
* `this.cursor.lastSyncId` moves forward for live deltas. The store calls it
|
|
624
|
+
* with its persisted-syncId watermark — that is, only after the deltas have
|
|
625
|
+
* committed to local storage. Advancing the cursor here, rather than at
|
|
626
|
+
* receipt in `handleDelta` or `handleSyncResponse`, keeps the cursor from
|
|
627
|
+
* getting ahead of the persisted view, so reconnect and catch-up requests
|
|
628
|
+
* cannot skip un-persisted deltas.
|
|
641
629
|
*/
|
|
642
630
|
sendAck(syncId) {
|
|
643
631
|
// Advance the local cursor *and* the version vector for this ack —
|
|
@@ -691,23 +679,15 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
691
679
|
}
|
|
692
680
|
}
|
|
693
681
|
/**
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
* `handleCommit` path on `apps/sync-server/src/hub/Hub.ts` (see the
|
|
699
|
-
* dispatch at Hub.ts:737).
|
|
700
|
-
*
|
|
701
|
-
* Historical naming note: this was originally `sendBatchAck` back when
|
|
702
|
-
* the Go sync-engine used a GraphQL `batchAck` mutation. The TS
|
|
703
|
-
* sync-server uses `type: 'commit'` over WebSocket exclusively. The
|
|
704
|
-
* method name now matches the wire protocol so the ack/commit naming
|
|
705
|
-
* confusion stops here.
|
|
682
|
+
* Sends a `commit` mutation request over the existing WebSocket and resolves
|
|
683
|
+
* when the server's `mutation_result` frame comes back with the same
|
|
684
|
+
* `clientTxId`. The wire frame is `{ type: 'commit', payload: { operations,
|
|
685
|
+
* clientTxId } }`.
|
|
706
686
|
*
|
|
707
|
-
* Times out after
|
|
708
|
-
* during an in-flight mutation (network flap, server restart);
|
|
709
|
-
*
|
|
710
|
-
*
|
|
687
|
+
* Times out after 15 seconds of silence from the server. The socket may close
|
|
688
|
+
* during an in-flight mutation (a network flap, a server restart); this does
|
|
689
|
+
* not auto-retry — the caller's transaction queue owns retry and offline
|
|
690
|
+
* replay, and the SDK does not duplicate that logic.
|
|
711
691
|
*/
|
|
712
692
|
sendCommit(operations, clientTxId, timeoutMs = 15_000, causedByTaskId, reads) {
|
|
713
693
|
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
@@ -750,21 +730,14 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
750
730
|
this.ws.send(JSON.stringify(frame));
|
|
751
731
|
}
|
|
752
732
|
/**
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
* Returns a promise that resolves with the server-canonicalized
|
|
759
|
-
* `syncGroups` and effective `ttlSeconds` once `claim_ack` arrives,
|
|
760
|
-
* or rejects with a typed error on `success: false` ack /
|
|
761
|
-
* timeout / disconnect.
|
|
733
|
+
* Activates a participant claim on this connection. One connection can hold
|
|
734
|
+
* several concurrent claims at once, each scoped to a different set of sync
|
|
735
|
+
* groups, so the SDK reuses the existing connection instead of opening a
|
|
736
|
+
* separate socket per scope.
|
|
762
737
|
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
* `apps/sync-server/docs/PARTICIPANT_CLAIMS.md` for the migration
|
|
767
|
-
* framing (Phase A.1).
|
|
738
|
+
* Returns a promise that resolves with the server-canonicalized `syncGroups`
|
|
739
|
+
* and effective `ttlSeconds` once `claim_ack` arrives, or rejects with a typed
|
|
740
|
+
* error on a failed ack, a timeout, or a disconnect.
|
|
768
741
|
*/
|
|
769
742
|
sendClaim(claimId, syncGroups, options) {
|
|
770
743
|
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
@@ -829,22 +802,21 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
829
802
|
}
|
|
830
803
|
}
|
|
831
804
|
/**
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
* area-of-interest
|
|
835
|
-
*
|
|
836
|
-
* chosen at connect.
|
|
805
|
+
* Moves this connection's read interest — replaces the connection-level sync
|
|
806
|
+
* groups mid-session as the user opens and closes entities. This is the
|
|
807
|
+
* area-of-interest navigation primitive: the server fans out deltas only for
|
|
808
|
+
* the groups currently in view, rather than the fixed set chosen at connect.
|
|
837
809
|
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
* requesting a group outside its allowlist), timeout, or disconnect. On
|
|
842
|
-
* success the new set is recorded as `options.syncGroups
|
|
843
|
-
*
|
|
810
|
+
* This is a full-set replace: pass the complete new group list, not a delta.
|
|
811
|
+
* Resolves with the server's effective set once `subscription_ack` arrives;
|
|
812
|
+
* rejects (with a typed error) on a scope denial (a restricted `rk_` key
|
|
813
|
+
* requesting a group outside its allowlist), a timeout, or a disconnect. On
|
|
814
|
+
* success the new set is recorded as `options.syncGroups`, so a later reconnect
|
|
815
|
+
* re-subscribes to the current interest rather than the connect-time set.
|
|
844
816
|
*
|
|
845
|
-
* Distinct from {@link sendClaim} (write
|
|
846
|
-
* the read side
|
|
847
|
-
* by the connection credential's grant.
|
|
817
|
+
* Distinct from {@link sendClaim} (a write claim, per operation, with a TTL):
|
|
818
|
+
* this is the read side, carries no capability token of its own, and is
|
|
819
|
+
* bounded by the connection credential's grant.
|
|
848
820
|
*/
|
|
849
821
|
updateSubscription(syncGroups, options) {
|
|
850
822
|
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
@@ -879,9 +851,9 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
879
851
|
});
|
|
880
852
|
}
|
|
881
853
|
/**
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
854
|
+
* Sets a fixed credential for callers that construct the socket directly. The
|
|
855
|
+
* SDK instead supplies `getAuthToken`, so reconnects read the shared
|
|
856
|
+
* credential source rather than this copied value.
|
|
885
857
|
*/
|
|
886
858
|
setCapabilityToken(token) {
|
|
887
859
|
this.options.capabilityToken = token;
|
|
@@ -976,17 +948,17 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
976
948
|
if (this._sessionErrorDetected) {
|
|
977
949
|
return;
|
|
978
950
|
}
|
|
979
|
-
// Don't attempt reconnection while offline.
|
|
980
|
-
//
|
|
981
|
-
//
|
|
982
|
-
//
|
|
951
|
+
// Don't attempt reconnection while offline. The owning store manages the
|
|
952
|
+
// offline→online transition: it bootstraps first, then calls `connect()`
|
|
953
|
+
// explicitly. Self-reconnecting here would bypass that bootstrap gate and
|
|
954
|
+
// surface stale data.
|
|
983
955
|
if (!getContext().onlineStatus.isOnline()) {
|
|
984
956
|
this.emit('reconnecting', { attempt: this.reconnectAttempts + 1, delay: 0 });
|
|
985
957
|
return;
|
|
986
958
|
}
|
|
987
|
-
// Give up after MAX_RECONNECT_ATTEMPTS consecutive failures.
|
|
988
|
-
//
|
|
989
|
-
//
|
|
959
|
+
// Give up after MAX_RECONNECT_ATTEMPTS consecutive failures. The user can
|
|
960
|
+
// recover by refreshing, or the store resets the attempt count and
|
|
961
|
+
// reconnects when the network returns.
|
|
990
962
|
if (this.reconnectAttempts >= SyncWebSocket.MAX_RECONNECT_ATTEMPTS) {
|
|
991
963
|
this.emit('reconnect_failed', { attempts: this.reconnectAttempts });
|
|
992
964
|
return;
|
|
@@ -1098,12 +1070,12 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
1098
1070
|
*/
|
|
1099
1071
|
notConnectedError(action) {
|
|
1100
1072
|
const d = this.getConnectionDiagnostics();
|
|
1101
|
-
//
|
|
1102
|
-
// suppressed until re-auth (or the store's credential re-mint clears the
|
|
1103
|
-
// latch), so retrying can never succeed. Reject with the
|
|
1104
|
-
//
|
|
1105
|
-
// "re-authenticate" instead of parking the write for a reconnect that
|
|
1106
|
-
//
|
|
1073
|
+
// A session-latched socket is not a transient transport hiccup: reconnection
|
|
1074
|
+
// is suppressed until re-auth (or the store's credential re-mint clears the
|
|
1075
|
+
// latch), so retrying can never succeed. Reject with the permanent session
|
|
1076
|
+
// error type — `isPermanentError` surfaces it to the caller as
|
|
1077
|
+
// "re-authenticate" instead of parking the write for a reconnect that will
|
|
1078
|
+
// never happen.
|
|
1107
1079
|
if (d.sessionErrorDetected) {
|
|
1108
1080
|
return Object.assign(new SyncSessionError(`SyncWebSocket not connected — cannot send ${action}: session expired` +
|
|
1109
1081
|
(d.lastCloseReason ? ` (${d.lastCloseReason})` : '') +
|
|
@@ -1134,10 +1106,10 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
1134
1106
|
else {
|
|
1135
1107
|
detail = 'never_connected';
|
|
1136
1108
|
}
|
|
1137
|
-
// Typed so it lands in the AbloError hierarchy
|
|
1138
|
-
//
|
|
1139
|
-
//
|
|
1140
|
-
//
|
|
1109
|
+
// Typed so it lands in the AbloError hierarchy and `isPermanentError` sees a
|
|
1110
|
+
// transient transport failure (retry on reconnect, don't roll back).
|
|
1111
|
+
// `diagnostics` stays a property — the queue's failure log walks the cause
|
|
1112
|
+
// chain for it.
|
|
1141
1113
|
const err = Object.assign(new AbloConnectionError(`SyncWebSocket not connected — cannot send ${action} (${detail})`, { code: 'ws_not_ready' }), { diagnostics: d });
|
|
1142
1114
|
return err;
|
|
1143
1115
|
}
|
|
@@ -1145,8 +1117,8 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
1145
1117
|
getSyncGroups() {
|
|
1146
1118
|
return this.options.syncGroups;
|
|
1147
1119
|
}
|
|
1148
|
-
// Cursor accessors — thin delegates; the state
|
|
1149
|
-
//
|
|
1120
|
+
// Cursor accessors — thin delegates; the state and semantics live in
|
|
1121
|
+
// SyncCursor.
|
|
1150
1122
|
/**
|
|
1151
1123
|
* Update last sync ID (for persistence)
|
|
1152
1124
|
*/
|
|
@@ -1172,7 +1144,7 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
1172
1144
|
return this.cursor.getLastSyncId();
|
|
1173
1145
|
}
|
|
1174
1146
|
/**
|
|
1175
|
-
*
|
|
1147
|
+
* Requests an incremental sync from the server, starting at the current cursor.
|
|
1176
1148
|
*/
|
|
1177
1149
|
async requestIncrementalSync() {
|
|
1178
1150
|
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
@@ -1194,7 +1166,7 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
1194
1166
|
lastSyncId: this.cursor.lastSyncId,
|
|
1195
1167
|
capabilities: capsArr,
|
|
1196
1168
|
// Protocol handshake: the server rejects an out-of-range version with
|
|
1197
|
-
//
|
|
1169
|
+
// WebSocket close code 4010 before serving any deltas.
|
|
1198
1170
|
protocolVersion: PROTOCOL_VERSION,
|
|
1199
1171
|
},
|
|
1200
1172
|
});
|
|
@@ -1228,25 +1200,22 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
1228
1200
|
const rawDeltas = Array.isArray(payload.deltas)
|
|
1229
1201
|
? payload.deltas
|
|
1230
1202
|
: null;
|
|
1231
|
-
// Cursor reconciliation
|
|
1232
|
-
//
|
|
1233
|
-
// local
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1236
|
-
//
|
|
1237
|
-
// we
|
|
1238
|
-
// when the field is absent (older server build) — we just skip the
|
|
1239
|
-
// reconciliation step.
|
|
1203
|
+
// Cursor reconciliation. The server stamps its authoritative `currentSyncId`
|
|
1204
|
+
// on every sync_response. If our local cursor is ahead of the server, our
|
|
1205
|
+
// local view has somehow diverged (corrupted metadata, a regression that
|
|
1206
|
+
// reintroduced an eager advance, or local storage lying about a successful
|
|
1207
|
+
// commit). Trust the server, reset the cursor, and request another sync so
|
|
1208
|
+
// any deltas we should have applied get re-delivered. When the field is
|
|
1209
|
+
// absent (an older server build) we simply skip this step.
|
|
1240
1210
|
//
|
|
1241
|
-
// We only reconcile when the response carries
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
1244
|
-
//
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1247
|
-
//
|
|
1248
|
-
//
|
|
1249
|
-
// server has nothing new to send).
|
|
1211
|
+
// We only reconcile when the response carries no deltas. If deltas are
|
|
1212
|
+
// present they advance our cursor through the normal persistence-gated path
|
|
1213
|
+
// anyway — and the in-flight round-trip means the snapshot's `currentSyncId`
|
|
1214
|
+
// is naturally a few syncIds behind our locally-advanced cursor at receive
|
|
1215
|
+
// time (live deltas may have landed in the meantime). Restricting to
|
|
1216
|
+
// empty-delta responses eliminates that benign false positive while still
|
|
1217
|
+
// catching the real corruption case (server head < local, and the server
|
|
1218
|
+
// has nothing new to send).
|
|
1250
1219
|
const hasDeltas = rawDeltas !== null && rawDeltas.length > 0;
|
|
1251
1220
|
if (!hasDeltas && typeof payload.currentSyncId === 'number') {
|
|
1252
1221
|
const serverHead = payload.currentSyncId;
|
|
@@ -1314,10 +1283,10 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
1314
1283
|
* Handle bootstrap response from server
|
|
1315
1284
|
*/
|
|
1316
1285
|
handleBootstrapResponse(payload) {
|
|
1317
|
-
// Emit bootstrap data for processing. (A `version` field from
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
//
|
|
1286
|
+
// Emit the bootstrap data for processing. (A `version` field from older
|
|
1287
|
+
// servers is ignored; the version vector is no longer used.) The typeof
|
|
1288
|
+
// guards mirror the cursor handling above: the frame is server-produced, so
|
|
1289
|
+
// coercion only bites on a malformed frame.
|
|
1321
1290
|
const p = (payload && typeof payload === 'object' ? payload : {});
|
|
1322
1291
|
this.emit('bootstrap_data', {
|
|
1323
1292
|
entityType: typeof p.entityType === 'string' ? p.entityType : '',
|
|
@@ -1327,13 +1296,12 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
1327
1296
|
});
|
|
1328
1297
|
}
|
|
1329
1298
|
/**
|
|
1330
|
-
*
|
|
1331
|
-
* forwarded as-is so every consumer
|
|
1332
|
-
*
|
|
1333
|
-
*
|
|
1334
|
-
* `kind`, `activity`, `syncGroups`, `isAgent` for rich consumers.
|
|
1299
|
+
* Handles a presence update from the server. The wire frame's payload is
|
|
1300
|
+
* forwarded as-is, so every consumer reads the same shape; stripping fields
|
|
1301
|
+
* here would drop `kind`, `activity`, `syncGroups`, and `isAgent` for
|
|
1302
|
+
* consumers that need them.
|
|
1335
1303
|
*
|
|
1336
|
-
*
|
|
1304
|
+
* The wire frame is:
|
|
1337
1305
|
* { type: 'presence_update', payload: { kind, userId, status,
|
|
1338
1306
|
* syncGroups, activity, isAgent, timestamp, activeClaims } }
|
|
1339
1307
|
*/
|