@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,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Fetches the initial snapshot the sync engine needs before it can go live: the
|
|
3
|
+
* current rows for the requested models plus the sync position from which to
|
|
4
|
+
* resume live updates. It calls the sync server's `/sync/bootstrap` HTTP
|
|
5
|
+
* endpoint, retries transient failures with backoff, and can fall back to a
|
|
6
|
+
* cached snapshot when the device is offline. {@link BootstrapData} is the
|
|
7
|
+
* shape it returns; {@link BootstrapOptions} configures it.
|
|
4
8
|
*/
|
|
5
9
|
import { getContext } from '../context.js';
|
|
6
10
|
import { SyncSessionError, AbloConnectionError, translateHttpError, toAbloError, isRetryableCode } from '../errors.js';
|
|
7
11
|
import { withAuthHeaders } from '../auth/credentialSource.js';
|
|
8
12
|
// SyncObservability replaced by getContext().observability
|
|
9
13
|
import { parseBootstrapResponse } from './schemas.js';
|
|
10
|
-
export class
|
|
14
|
+
export class BootstrapFetcher {
|
|
11
15
|
options;
|
|
12
16
|
abortController = null;
|
|
13
17
|
/** Warn about schema drift at most once per helper. */
|
|
@@ -39,23 +43,20 @@ export class BootstrapHelper {
|
|
|
39
43
|
`\`ablo push\` to deploy your schema (or update this app to match the deployed one).`, { clientSchemaHash: clientHash, serverSchemaHash: serverHash });
|
|
40
44
|
}
|
|
41
45
|
constructor(options) {
|
|
42
|
-
// Defaults are spread first; the explicit `baseUrl` then takes precedence
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// Historical note: a previous version of this constructor placed
|
|
46
|
-
// `baseUrl: \`${baseUrl}/api\`` BEFORE the `...options` spread, which
|
|
47
|
-
// meant the spread silently overwrote it back to the caller's value
|
|
48
|
-
// and the `/api` suffix was dead code. Both Ablo and `createSyncEngine`
|
|
49
|
-
// already pass `${url}/api` explicitly, so removing the suffix here
|
|
50
|
-
// preserves the actual on-the-wire behavior while making the contract
|
|
51
|
-
// explicit: callers pass the full base URL including `/api`.
|
|
46
|
+
// Defaults are spread first; the explicit `baseUrl` then takes precedence,
|
|
47
|
+
// resolved from `options.baseUrl` or the localhost fallback. Callers pass
|
|
48
|
+
// the full base URL, including the `/api` prefix.
|
|
52
49
|
this.options = {
|
|
53
50
|
syncGroups: [],
|
|
54
51
|
maxRetries: 3,
|
|
55
52
|
retryDelay: 1000,
|
|
56
53
|
fetchTimeout: 10_000, // 10 second timeout per request - fail fast for good UX
|
|
57
54
|
...options,
|
|
58
|
-
baseUrl: options.baseUrl
|
|
55
|
+
baseUrl: options.baseUrl ?? 'http://localhost:8080/api',
|
|
56
|
+
// Reading the deprecated `organizationId` is deliberate: it preserves the
|
|
57
|
+
// cache namespace for callers that still construct BootstrapFetcher
|
|
58
|
+
// directly with the old field instead of `cacheScope`.
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
59
60
|
cacheScope: options.cacheScope ?? options.organizationId ?? null,
|
|
60
61
|
};
|
|
61
62
|
// Do not clear cache here; keep offline fallback available
|
|
@@ -73,8 +74,8 @@ export class BootstrapHelper {
|
|
|
73
74
|
this.options.syncGroups = [...(syncGroups ?? [])];
|
|
74
75
|
}
|
|
75
76
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
77
|
+
* Sets a fixed credential for callers that construct the helper directly.
|
|
78
|
+
* The SDK instead supplies `getAuthToken` and never calls this.
|
|
78
79
|
*/
|
|
79
80
|
setAuthToken(authToken) {
|
|
80
81
|
if (!authToken) {
|
|
@@ -83,26 +84,6 @@ export class BootstrapHelper {
|
|
|
83
84
|
}
|
|
84
85
|
this.options.authToken = authToken;
|
|
85
86
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Create a promise that rejects after a timeout
|
|
88
|
-
* Used to race against fetch requests that may hang indefinitely
|
|
89
|
-
*/
|
|
90
|
-
createTimeoutPromise(ms, operation) {
|
|
91
|
-
return new Promise((_, reject) => {
|
|
92
|
-
setTimeout(() => {
|
|
93
|
-
reject(new AbloConnectionError(`Bootstrap ${operation} timed out after ${ms}ms`, {
|
|
94
|
-
code: 'bootstrap_fetch_timeout',
|
|
95
|
-
}));
|
|
96
|
-
}, ms);
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Wrap a promise with a timeout - if the promise doesn't resolve within
|
|
101
|
-
* the timeout period, the AbortController is triggered and an error is thrown
|
|
102
|
-
*/
|
|
103
|
-
async withTimeout(promise, timeoutMs, operation) {
|
|
104
|
-
return Promise.race([promise, this.createTimeoutPromise(timeoutMs, operation)]);
|
|
105
|
-
}
|
|
106
87
|
/**
|
|
107
88
|
* Fetch bootstrap data from sync engine with partial bootstrap support
|
|
108
89
|
* @param lastSyncId - Optional: client's current lastSyncId for partial bootstrap
|
|
@@ -110,11 +91,12 @@ export class BootstrapHelper {
|
|
|
110
91
|
*/
|
|
111
92
|
async fetchBootstrap(lastSyncId,
|
|
112
93
|
/**
|
|
113
|
-
*
|
|
114
|
-
* the request uses
|
|
115
|
-
*
|
|
116
|
-
* unaffected
|
|
117
|
-
* holds the
|
|
94
|
+
* A per-call set of sync groups for a scoped hydrate-on-enter. When given,
|
|
95
|
+
* the request uses these groups instead of the configured `syncGroups`, and
|
|
96
|
+
* does so without mutating the shared options, so a concurrent full
|
|
97
|
+
* bootstrap is unaffected. It also bypasses the offline snapshot cache,
|
|
98
|
+
* which holds the full bootstrap and would be a wrong answer to a subset
|
|
99
|
+
* request.
|
|
118
100
|
*/
|
|
119
101
|
syncGroupsOverride) {
|
|
120
102
|
// organizationId omitted — server reads it from auth identity.
|
|
@@ -135,10 +117,19 @@ export class BootstrapHelper {
|
|
|
135
117
|
params.append('models', this.options.instantModels.join(','));
|
|
136
118
|
}
|
|
137
119
|
const url = `${this.options.baseUrl}/sync/bootstrap?${params.toString()}`;
|
|
138
|
-
// If offline, try cached bootstrap. Skipped for a scoped override
|
|
139
|
-
// cache holds the
|
|
120
|
+
// If offline, try the cached bootstrap. Skipped for a scoped override: the
|
|
121
|
+
// cache holds the full snapshot, which is not a valid answer to a subset
|
|
140
122
|
// request; a scoped hydrate just soft-fails offline and retries on re-enter.
|
|
141
|
-
|
|
123
|
+
//
|
|
124
|
+
// Only an explicit `false` means offline. `navigator.onLine` is *typed*
|
|
125
|
+
// `boolean`, but at runtime it is `boolean | undefined`: Node 21+ exposes a
|
|
126
|
+
// global `navigator` whose `onLine` is `undefined`. Reading `!navigator.onLine`
|
|
127
|
+
// would treat that `undefined` as offline and falsely short-circuit to the
|
|
128
|
+
// (empty, under `persistence: 'memory'`) cache — throwing instead of fetching.
|
|
129
|
+
// Capturing it at its true runtime type keeps the `=== false` honest (and lets
|
|
130
|
+
// the boolean-literal-compare lint rule see the nullable it really is).
|
|
131
|
+
const navigatorOnline = typeof navigator !== 'undefined' ? navigator.onLine : undefined;
|
|
132
|
+
if (!syncGroupsOverride && navigatorOnline === false) {
|
|
142
133
|
const cached = this.options.cacheScope
|
|
143
134
|
? this.loadCachedBootstrap(this.options.cacheScope)
|
|
144
135
|
: null;
|
|
@@ -160,7 +151,7 @@ export class BootstrapHelper {
|
|
|
160
151
|
type: data.type,
|
|
161
152
|
lastSyncId: data.lastSyncId,
|
|
162
153
|
modelCount: data.models ? Object.keys(data.models).length : 0,
|
|
163
|
-
deltaCount: data.deltaCount
|
|
154
|
+
deltaCount: data.deltaCount ?? 0,
|
|
164
155
|
totalItems: data.models
|
|
165
156
|
? Object.values(data.models).reduce((sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0), 0)
|
|
166
157
|
: 0,
|
|
@@ -220,11 +211,9 @@ export class BootstrapHelper {
|
|
|
220
211
|
* Fetch bootstrap with ETag, returning 304 hints
|
|
221
212
|
*/
|
|
222
213
|
async fetchBootstrapWithETag() {
|
|
223
|
-
//
|
|
224
|
-
// the authenticated identity
|
|
225
|
-
//
|
|
226
|
-
// client-side was historical: it predated the auth-context pipeline
|
|
227
|
-
// and forced a cross-org guard to defend against the SDK lying.
|
|
214
|
+
// The organization id is intentionally not sent. The server resolves it
|
|
215
|
+
// from the authenticated identity, so the client cannot select or spoof an
|
|
216
|
+
// organization it is not scoped to.
|
|
228
217
|
const params = new URLSearchParams();
|
|
229
218
|
this.options.syncGroups.forEach((g) => { params.append('syncGroups', g); });
|
|
230
219
|
if (this.options.instantModels && this.options.instantModels.length > 0) {
|
|
@@ -252,7 +241,10 @@ export class BootstrapHelper {
|
|
|
252
241
|
}
|
|
253
242
|
if (!res.ok) {
|
|
254
243
|
const bodyText = await res.text().catch(() => '');
|
|
255
|
-
|
|
244
|
+
// Map an empty body to undefined so the `??` below falls through to the
|
|
245
|
+
// synthetic message — translateHttpError renders an empty string body as
|
|
246
|
+
// an empty error message, which is useless to the caller.
|
|
247
|
+
let parsed = bodyText || undefined;
|
|
256
248
|
if (bodyText) {
|
|
257
249
|
try {
|
|
258
250
|
parsed = JSON.parse(bodyText);
|
|
@@ -261,12 +253,13 @@ export class BootstrapHelper {
|
|
|
261
253
|
// Keep as string.
|
|
262
254
|
}
|
|
263
255
|
}
|
|
264
|
-
// Translate the canonical envelope
|
|
265
|
-
// message survive (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
//
|
|
269
|
-
// code
|
|
256
|
+
// Translate the canonical envelope first so the server's specific code
|
|
257
|
+
// and message survive (for example `api_key_required` or
|
|
258
|
+
// `jwt_issuer_untrusted`).
|
|
259
|
+
const translated = translateHttpError(res.status, parsed ?? `Bootstrap fetch failed: ${res.status} ${res.statusText}`, res.headers.get('x-request-id') ?? undefined);
|
|
260
|
+
// Only a genuine session or JWT expiry — or a bare auth failure carrying
|
|
261
|
+
// no structured code — should drive the sign-in redirect. A specific auth
|
|
262
|
+
// code like `api_key_required` is not an expired session: signing in again
|
|
270
263
|
// mints the same credential and loops. Surface it as its real typed error
|
|
271
264
|
// instead of a `session_expired` wrapping the stringified body.
|
|
272
265
|
if (translated.code === 'session_expired' ||
|
|
@@ -277,8 +270,7 @@ export class BootstrapHelper {
|
|
|
277
270
|
}
|
|
278
271
|
throw translated;
|
|
279
272
|
}
|
|
280
|
-
const
|
|
281
|
-
const data = parseBootstrapResponse(rawJson);
|
|
273
|
+
const data = parseBootstrapResponse(await res.json());
|
|
282
274
|
this.warnOnSchemaDrift(data.schemaHash);
|
|
283
275
|
// Persist payload for offline
|
|
284
276
|
try {
|
|
@@ -286,7 +278,10 @@ export class BootstrapHelper {
|
|
|
286
278
|
this.saveCachedBootstrap(this.options.cacheScope, data);
|
|
287
279
|
}
|
|
288
280
|
}
|
|
289
|
-
catch {
|
|
281
|
+
catch {
|
|
282
|
+
// Offline persistence is best-effort; a failed cache write must not
|
|
283
|
+
// block returning the freshly fetched data.
|
|
284
|
+
}
|
|
290
285
|
getContext().logger.info('[Bootstrap] 200 OK - received new data');
|
|
291
286
|
return { notModified: false, data, etag };
|
|
292
287
|
}
|
|
@@ -329,7 +324,9 @@ export class BootstrapHelper {
|
|
|
329
324
|
clearTimeout(timeoutId);
|
|
330
325
|
if (!response.ok) {
|
|
331
326
|
const bodyText = await response.text().catch(() => '');
|
|
332
|
-
|
|
327
|
+
// Map an empty body to undefined so the `??` below falls through to the
|
|
328
|
+
// synthetic message (see the note on the primary fetch path).
|
|
329
|
+
let parsed = bodyText || undefined;
|
|
333
330
|
if (bodyText) {
|
|
334
331
|
try {
|
|
335
332
|
parsed = JSON.parse(bodyText);
|
|
@@ -341,7 +338,7 @@ export class BootstrapHelper {
|
|
|
341
338
|
// Same code-aware handling as the primary bootstrap fetch: preserve the
|
|
342
339
|
// server's specific code/message; only a genuine expiry (or a bare,
|
|
343
340
|
// code-less auth failure) drives the sign-in redirect.
|
|
344
|
-
const translated = translateHttpError(response.status, parsed
|
|
341
|
+
const translated = translateHttpError(response.status, parsed ?? `Bootstrap fetch failed: ${response.status} ${response.statusText}`, response.headers.get('x-request-id') ?? undefined);
|
|
345
342
|
if (translated.code === 'session_expired' ||
|
|
346
343
|
translated.code === 'jwt_expired' ||
|
|
347
344
|
((response.status === 401 || response.status === 403) &&
|
|
@@ -350,8 +347,7 @@ export class BootstrapHelper {
|
|
|
350
347
|
}
|
|
351
348
|
throw translated;
|
|
352
349
|
}
|
|
353
|
-
const
|
|
354
|
-
const data = parseBootstrapResponse(rawJson);
|
|
350
|
+
const data = parseBootstrapResponse(await response.json());
|
|
355
351
|
this.warnOnSchemaDrift(data.schemaHash);
|
|
356
352
|
// Save a copy for offline
|
|
357
353
|
try {
|
|
@@ -359,7 +355,10 @@ export class BootstrapHelper {
|
|
|
359
355
|
this.saveCachedBootstrap(this.options.cacheScope, data);
|
|
360
356
|
}
|
|
361
357
|
}
|
|
362
|
-
catch {
|
|
358
|
+
catch {
|
|
359
|
+
// Offline persistence is best-effort; a failed cache write must not
|
|
360
|
+
// block returning the freshly fetched data.
|
|
361
|
+
}
|
|
363
362
|
return data;
|
|
364
363
|
}
|
|
365
364
|
/**
|
|
@@ -369,10 +368,9 @@ export class BootstrapHelper {
|
|
|
369
368
|
*/
|
|
370
369
|
async fetchEntity(modelName, id) {
|
|
371
370
|
const url = `${this.options.baseUrl}/sync/entity/${modelName}/${id}`;
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
// so an entity self-heal never cancels a concurrent bootstrap fetch.
|
|
371
|
+
// Uses the same `fetchTimeout` deadline as `performFetch`. A local
|
|
372
|
+
// AbortController, rather than the shared `this.abortController`, means an
|
|
373
|
+
// entity self-heal never cancels a concurrent bootstrap fetch.
|
|
376
374
|
const controller = new AbortController();
|
|
377
375
|
const timeoutId = setTimeout(() => { controller.abort(); }, this.options.fetchTimeout);
|
|
378
376
|
let response;
|
|
@@ -401,7 +399,9 @@ export class BootstrapHelper {
|
|
|
401
399
|
}
|
|
402
400
|
if (!response.ok) {
|
|
403
401
|
const bodyText = await response.text().catch(() => '');
|
|
404
|
-
|
|
402
|
+
// Map an empty body to undefined so the `??` below falls through to the
|
|
403
|
+
// synthetic message (see the note on the primary fetch path).
|
|
404
|
+
let parsed = bodyText || undefined;
|
|
405
405
|
if (bodyText) {
|
|
406
406
|
try {
|
|
407
407
|
parsed = JSON.parse(bodyText);
|
|
@@ -410,9 +410,9 @@ export class BootstrapHelper {
|
|
|
410
410
|
// Keep as string.
|
|
411
411
|
}
|
|
412
412
|
}
|
|
413
|
-
throw translateHttpError(response.status, parsed
|
|
413
|
+
throw translateHttpError(response.status, parsed ?? `Entity fetch failed: ${response.status} ${response.statusText}`, response.headers.get('x-request-id') ?? undefined);
|
|
414
414
|
}
|
|
415
|
-
return await response.json();
|
|
415
|
+
return (await response.json());
|
|
416
416
|
}
|
|
417
417
|
// ─────────────────────────────────────────────────────────────────────
|
|
418
418
|
/**
|
|
@@ -426,7 +426,7 @@ export class BootstrapHelper {
|
|
|
426
426
|
const keysToRemove = [];
|
|
427
427
|
for (let i = 0; i < localStorage.length; i++) {
|
|
428
428
|
const key = localStorage.key(i);
|
|
429
|
-
if (key
|
|
429
|
+
if (key?.includes('sync-bootstrap')) {
|
|
430
430
|
keysToRemove.push(key);
|
|
431
431
|
}
|
|
432
432
|
}
|
|
@@ -495,10 +495,10 @@ export class BootstrapHelper {
|
|
|
495
495
|
});
|
|
496
496
|
if (!response.ok)
|
|
497
497
|
return false;
|
|
498
|
-
const
|
|
499
|
-
return
|
|
498
|
+
const body = (await response.json());
|
|
499
|
+
return body.status === 'healthy';
|
|
500
500
|
}
|
|
501
|
-
catch
|
|
501
|
+
catch {
|
|
502
502
|
getContext().observability.breadcrumb('Health check failed', 'sync.bootstrap', 'warning');
|
|
503
503
|
return false;
|
|
504
504
|
}
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Owns the sync engine's connection lifecycle: the state machine that carries a
|
|
3
|
+
* client from a healthy connection, through a dropout, and back to live. It
|
|
4
|
+
* watches the browser's online/offline and visibility events, probes real
|
|
5
|
+
* connectivity and session validity with {@link probeNetwork}, applies retry
|
|
6
|
+
* backoff with a ceiling and jitter, and runs a watchdog for the cases where
|
|
7
|
+
* the browser never fires an event (VPN flips, captive portals). When it
|
|
8
|
+
* decides to recover, it drives the reconnect sequence — bootstrap, then
|
|
9
|
+
* WebSocket connect — through the {@link ConnectionCallbacks.onReconnect}
|
|
10
|
+
* callback and reacts to the outcome.
|
|
6
11
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - Retry backoff with ceiling, jitter, and offline-aware parking
|
|
12
|
-
* - Watchdog for browser events that never fire (VPN, captive portal)
|
|
13
|
-
* - The reconnect → bootstrap → WebSocket connect sequence
|
|
14
|
-
*
|
|
15
|
-
* What it DOES NOT own:
|
|
16
|
-
* - The actual bootstrap / IndexedDB / ObjectPool work — that lives in
|
|
17
|
-
* `BaseSyncedStore.performReconnect()`. This class calls it via the
|
|
18
|
-
* `onReconnect` callback and reacts to the outcome.
|
|
19
|
-
*
|
|
20
|
-
* Designed to be embedded by `BaseSyncedStore`: one instance per store,
|
|
21
|
-
* started on first successful connect, disposed on teardown.
|
|
12
|
+
* It deliberately does not perform the bootstrap, local-storage, or object-pool
|
|
13
|
+
* work itself; that lives in the embedding store, which the manager reaches
|
|
14
|
+
* only through its callbacks. One manager is created per store, started on the
|
|
15
|
+
* first successful connect and disposed on teardown.
|
|
22
16
|
*
|
|
23
17
|
* CONNECTED ──(socket drop)──► PROBING_NETWORK ──► RECONNECTING ──► CONNECTED
|
|
24
18
|
* │ │ │
|
|
@@ -29,20 +23,15 @@
|
|
|
29
23
|
* ▼
|
|
30
24
|
* WAITING_FOR_NETWORK
|
|
31
25
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* 1006 is browser-local and carries no connectivity signal, so on a
|
|
42
|
-
* healthy machine no `online`/`offline` event ever fires — parking in
|
|
43
|
-
* `offline` stranded recovery until the 30s watchdog, long enough for
|
|
44
|
-
* queued commits to roll back. Only a genuine OS-level `NETWORK_LOST`
|
|
45
|
-
* parks in `offline` and waits for the `online` event.
|
|
26
|
+
* Three behaviors are worth calling out. A network return or tab focus during a
|
|
27
|
+
* backoff delay jumps straight to probing instead of waiting out the full
|
|
28
|
+
* interval. Reaching the retry ceiling while the browser reports offline parks
|
|
29
|
+
* in `waiting_for_network` rather than hard-reloading a browser that has no
|
|
30
|
+
* network. And a socket drop (close code 1006) goes straight to probing rather
|
|
31
|
+
* than the passive `offline` state — 1006 is generated locally and carries no
|
|
32
|
+
* connectivity signal, so on a healthy machine no online/offline event ever
|
|
33
|
+
* fires; only a genuine operating-system network loss parks in `offline` to
|
|
34
|
+
* wait for the `online` event.
|
|
46
35
|
*/
|
|
47
36
|
import { type ProbeResult } from './NetworkProbe.js';
|
|
48
37
|
import type { AuthTokenGetter } from '../auth/credentialSource.js';
|
|
@@ -93,17 +82,18 @@ export interface ConnectionCallbacks {
|
|
|
93
82
|
/** Run bootstrap + WebSocket reconnect. Returns the outcome. */
|
|
94
83
|
onReconnect: () => Promise<'success' | 'session_error' | 'network_error'>;
|
|
95
84
|
/**
|
|
96
|
-
* Re-
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* (`PROBE_CREDENTIAL_STALE`).
|
|
100
|
-
* - `'refreshed'` → a fresh credential is in place; re-probe
|
|
101
|
-
* - `'session_error'` → the
|
|
102
|
-
* 401/403); terminal
|
|
103
|
-
* - `'network_error'` →
|
|
104
|
-
* transient
|
|
105
|
-
*
|
|
106
|
-
*
|
|
85
|
+
* Re-mints the short-lived access credential (the `ek_`/`rk_`) and pushes it
|
|
86
|
+
* into the credential source, then reports the outcome. Invoked in the
|
|
87
|
+
* `refreshing_credential` state — that is, when a probe found the access key
|
|
88
|
+
* stale (`PROBE_CREDENTIAL_STALE`). The three outcomes map onto recovery:
|
|
89
|
+
* - `'refreshed'` → a fresh credential is in place; re-probe and reconnect.
|
|
90
|
+
* - `'session_error'` → the long-lived login itself is gone (the mint
|
|
91
|
+
* returned null, a 401/403); terminal, so sign out.
|
|
92
|
+
* - `'network_error'` → the mint endpoint was unreachable (offline, 5xx, or
|
|
93
|
+
* a throw); transient, so back off and retry rather
|
|
94
|
+
* than sign out.
|
|
95
|
+
* Optional: a deployment with no re-mint path (for example a static `apiKey`)
|
|
96
|
+
* omits it, and the state machine falls back to a plain re-probe.
|
|
107
97
|
*/
|
|
108
98
|
onRefreshCredential?: () => Promise<'refreshed' | 'session_error' | 'network_error'>;
|
|
109
99
|
/** Called when the session is confirmed expired — route to signin. */
|
|
@@ -111,12 +101,11 @@ export interface ConnectionCallbacks {
|
|
|
111
101
|
/** Called to tear down the WebSocket when entering a dead state. */
|
|
112
102
|
onDisconnectWebSocket: () => void;
|
|
113
103
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* FSM was opaque to the UI.
|
|
104
|
+
* Fires on every state transition. It lets the embedding store mirror
|
|
105
|
+
* recovery progress into its visible `syncStatus`, so the UI can show
|
|
106
|
+
* "Reconnecting…" instead of a sticky "offline" while the machine cycles
|
|
107
|
+
* through `probing_network` → `reconnecting` → `backoff`. Optional; when
|
|
108
|
+
* omitted, the state machine is simply opaque to the UI.
|
|
120
109
|
*/
|
|
121
110
|
onStateChange?: (next: ConnectionState, prev: ConnectionState) => void;
|
|
122
111
|
}
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Owns the sync engine's connection lifecycle: the state machine that carries a
|
|
3
|
+
* client from a healthy connection, through a dropout, and back to live. It
|
|
4
|
+
* watches the browser's online/offline and visibility events, probes real
|
|
5
|
+
* connectivity and session validity with {@link probeNetwork}, applies retry
|
|
6
|
+
* backoff with a ceiling and jitter, and runs a watchdog for the cases where
|
|
7
|
+
* the browser never fires an event (VPN flips, captive portals). When it
|
|
8
|
+
* decides to recover, it drives the reconnect sequence — bootstrap, then
|
|
9
|
+
* WebSocket connect — through the {@link ConnectionCallbacks.onReconnect}
|
|
10
|
+
* callback and reacts to the outcome.
|
|
6
11
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - Retry backoff with ceiling, jitter, and offline-aware parking
|
|
12
|
-
* - Watchdog for browser events that never fire (VPN, captive portal)
|
|
13
|
-
* - The reconnect → bootstrap → WebSocket connect sequence
|
|
14
|
-
*
|
|
15
|
-
* What it DOES NOT own:
|
|
16
|
-
* - The actual bootstrap / IndexedDB / ObjectPool work — that lives in
|
|
17
|
-
* `BaseSyncedStore.performReconnect()`. This class calls it via the
|
|
18
|
-
* `onReconnect` callback and reacts to the outcome.
|
|
19
|
-
*
|
|
20
|
-
* Designed to be embedded by `BaseSyncedStore`: one instance per store,
|
|
21
|
-
* started on first successful connect, disposed on teardown.
|
|
12
|
+
* It deliberately does not perform the bootstrap, local-storage, or object-pool
|
|
13
|
+
* work itself; that lives in the embedding store, which the manager reaches
|
|
14
|
+
* only through its callbacks. One manager is created per store, started on the
|
|
15
|
+
* first successful connect and disposed on teardown.
|
|
22
16
|
*
|
|
23
17
|
* CONNECTED ──(socket drop)──► PROBING_NETWORK ──► RECONNECTING ──► CONNECTED
|
|
24
18
|
* │ │ │
|
|
@@ -29,20 +23,15 @@
|
|
|
29
23
|
* ▼
|
|
30
24
|
* WAITING_FOR_NETWORK
|
|
31
25
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* 1006 is browser-local and carries no connectivity signal, so on a
|
|
42
|
-
* healthy machine no `online`/`offline` event ever fires — parking in
|
|
43
|
-
* `offline` stranded recovery until the 30s watchdog, long enough for
|
|
44
|
-
* queued commits to roll back. Only a genuine OS-level `NETWORK_LOST`
|
|
45
|
-
* parks in `offline` and waits for the `online` event.
|
|
26
|
+
* Three behaviors are worth calling out. A network return or tab focus during a
|
|
27
|
+
* backoff delay jumps straight to probing instead of waiting out the full
|
|
28
|
+
* interval. Reaching the retry ceiling while the browser reports offline parks
|
|
29
|
+
* in `waiting_for_network` rather than hard-reloading a browser that has no
|
|
30
|
+
* network. And a socket drop (close code 1006) goes straight to probing rather
|
|
31
|
+
* than the passive `offline` state — 1006 is generated locally and carries no
|
|
32
|
+
* connectivity signal, so on a healthy machine no online/offline event ever
|
|
33
|
+
* fires; only a genuine operating-system network loss parks in `offline` to
|
|
34
|
+
* wait for the `online` event.
|
|
46
35
|
*/
|
|
47
36
|
import { makeAutoObservable, runInAction } from 'mobx';
|
|
48
37
|
import { getContext } from '../context.js';
|
|
@@ -158,13 +147,13 @@ export class ConnectionManager {
|
|
|
158
147
|
// work.
|
|
159
148
|
return 'offline';
|
|
160
149
|
case 'WS_DISCONNECTED':
|
|
161
|
-
// The socket died (typically code 1006) but the
|
|
162
|
-
// almost certainly fine — 1006 is generated locally when
|
|
163
|
-
//
|
|
164
|
-
// fires no online/offline event. Probe
|
|
165
|
-
// landing in the passive `offline` dead-end (which only escaped
|
|
166
|
-
// the 30s watchdog, long after queued commits rolled back). The
|
|
167
|
-
// probe fast-fails if we genuinely
|
|
150
|
+
// The socket died (typically code 1006) but the operating-system
|
|
151
|
+
// network is almost certainly fine — 1006 is generated locally when
|
|
152
|
+
// the TCP connection vanishes and carries no connectivity signal, so
|
|
153
|
+
// the browser fires no online/offline event. Probe immediately rather
|
|
154
|
+
// than landing in the passive `offline` dead-end (which only escaped
|
|
155
|
+
// via the 30s watchdog, long after queued commits rolled back). The
|
|
156
|
+
// probe fast-fails if we genuinely are offline → waiting_for_network.
|
|
168
157
|
return 'probing_network';
|
|
169
158
|
case 'WS_SESSION_ERROR':
|
|
170
159
|
case 'BOOTSTRAP_FAILED_SESSION':
|
|
@@ -233,15 +222,15 @@ export class ConnectionManager {
|
|
|
233
222
|
return null;
|
|
234
223
|
}
|
|
235
224
|
case 'refreshing_credential':
|
|
236
|
-
// Re-minting the short-lived access key (the
|
|
237
|
-
//
|
|
225
|
+
// Re-minting the short-lived access key (the `ek_`/`rk_`). The login is
|
|
226
|
+
// presumed valid; this is not a sign-out state.
|
|
238
227
|
switch (event.type) {
|
|
239
228
|
case 'CREDENTIAL_REFRESHED':
|
|
240
229
|
// Fresh key in hand — re-probe so we reconnect with it.
|
|
241
230
|
return 'probing_network';
|
|
242
231
|
case 'BOOTSTRAP_FAILED_SESSION':
|
|
243
232
|
// The re-mint hit a genuine 401/403: the long-lived login itself is
|
|
244
|
-
// gone.
|
|
233
|
+
// gone. This is the only path from here to sign-out.
|
|
245
234
|
return 'session_expired';
|
|
246
235
|
case 'RECONNECT_FAILED':
|
|
247
236
|
// Couldn't reach the mint endpoint (offline/5xx/throw) — transient.
|
|
@@ -336,15 +325,15 @@ export class ConnectionManager {
|
|
|
336
325
|
this.callbacks?.onDisconnectWebSocket();
|
|
337
326
|
break;
|
|
338
327
|
case 'probing_network':
|
|
339
|
-
// A socket drop (`WS_DISCONNECTED`)
|
|
340
|
-
// starts immediately. Tear the dead socket down
|
|
328
|
+
// A socket drop (`WS_DISCONNECTED`) lands here directly so recovery
|
|
329
|
+
// starts immediately. Tear the dead socket down first — that is what
|
|
341
330
|
// sets SyncWebSocket's `isManualClose=true` and suppresses its own
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
// (
|
|
331
|
+
// reconnect, keeping this machine the single reconnect authority on the
|
|
332
|
+
// human path. The teardown runs synchronously inside the `disconnected`
|
|
333
|
+
// emit, before `SyncWebSocket.onclose` checks the flag, so the timing
|
|
334
|
+
// matches the previous `offline`-entry teardown. We gate on the drop
|
|
335
|
+
// event specifically: the other paths into `probing_network`
|
|
336
|
+
// (tab-focus re-validation, handshake retry, backoff elapse) must not
|
|
348
337
|
// tear down a socket that may still be live.
|
|
349
338
|
if (event.type === 'WS_DISCONNECTED') {
|
|
350
339
|
this.callbacks?.onDisconnectWebSocket();
|
|
@@ -364,11 +353,12 @@ export class ConnectionManager {
|
|
|
364
353
|
this.scheduleBackoff();
|
|
365
354
|
break;
|
|
366
355
|
case 'auth_blocked':
|
|
367
|
-
// Stop — reachable but the credential
|
|
368
|
-
// api_key_required
|
|
369
|
-
// reconnecting nor re-
|
|
370
|
-
// manual retry
|
|
371
|
-
// and
|
|
356
|
+
// Stop here — the server is reachable but rejected the credential (for
|
|
357
|
+
// example api_key_required or jwt_issuer_untrusted from the data plane).
|
|
358
|
+
// Neither reconnecting nor re-authenticating fixes it. Drop the socket
|
|
359
|
+
// and wait for a manual retry or a re-probe. This deliberately does not
|
|
360
|
+
// call onSessionExpired (no sign-out) and does not reconnect — that is
|
|
361
|
+
// the whole point of the state.
|
|
372
362
|
this.clearBackoffTimer();
|
|
373
363
|
this.callbacks?.onDisconnectWebSocket();
|
|
374
364
|
getContext().observability.breadcrumb('Auth blocked — reachable but credential rejected; not reconnecting or signing out', 'sync.offline', 'error');
|
|
@@ -602,16 +592,15 @@ export class ConnectionManager {
|
|
|
602
592
|
this.watchdogTimer = setInterval(() => {
|
|
603
593
|
if (this.disposed)
|
|
604
594
|
return;
|
|
605
|
-
// "Stuck"
|
|
606
|
-
// waiting_for_network, backoff).
|
|
607
|
-
// `navigator.onLine === true
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
// so an unconditional retry costs nothing in the genuine case.
|
|
595
|
+
// "Stuck" means parked in a non-active recovery state (offline,
|
|
596
|
+
// waiting_for_network, backoff). This deliberately does not gate on
|
|
597
|
+
// `navigator.onLine === true`: per MDN, `navigator.onLine` is only
|
|
598
|
+
// reliable when it returns false ("definitely offline"), and even that
|
|
599
|
+
// lies after laptop wake or VPN flips. Gating the watchdog on `onLine`
|
|
600
|
+
// was what stranded the machine in `offline` forever — when the browser
|
|
601
|
+
// briefly reported offline and never re-fired the `online` event, there
|
|
602
|
+
// was no escape. The probe itself fast-fails when truly offline, so an
|
|
603
|
+
// unconditional retry costs nothing in the genuine case.
|
|
615
604
|
const isStuck = this.state !== 'connected' &&
|
|
616
605
|
this.state !== 'session_expired' &&
|
|
617
606
|
this.state !== 'probing_network' &&
|