@abloatai/ablo 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -3
- package/CHANGELOG.md +34 -0
- package/README.md +104 -88
- package/dist/BaseSyncedStore.d.ts +140 -266
- package/dist/BaseSyncedStore.js +338 -739
- package/dist/Database.d.ts +62 -77
- package/dist/Database.js +106 -127
- package/dist/{ObjectPool.d.ts → InstanceCache.d.ts} +10 -13
- package/dist/{ObjectPool.js → InstanceCache.js} +91 -83
- package/dist/LazyReferenceCollection.d.ts +11 -15
- package/dist/LazyReferenceCollection.js +16 -15
- package/dist/Model.d.ts +37 -52
- package/dist/Model.js +52 -69
- package/dist/ModelRegistry.d.ts +46 -25
- package/dist/ModelRegistry.js +32 -30
- package/dist/NetworkMonitor.d.ts +5 -6
- package/dist/NetworkMonitor.js +6 -7
- package/dist/SyncClient.d.ts +119 -109
- package/dist/SyncClient.js +303 -224
- package/dist/SyncEngineContext.d.ts +1 -3
- package/dist/SyncEngineContext.js +1 -2
- package/dist/adapters/alwaysOnline.d.ts +6 -8
- package/dist/adapters/alwaysOnline.js +6 -8
- package/dist/adapters/inMemoryStorage.d.ts +9 -9
- package/dist/adapters/inMemoryStorage.js +9 -9
- package/dist/agent/Agent.d.ts +39 -31
- package/dist/agent/Agent.js +35 -23
- package/dist/agent/index.d.ts +4 -4
- package/dist/agent/index.js +5 -5
- package/dist/agent/session.d.ts +47 -44
- package/dist/agent/session.js +37 -48
- package/dist/agent/types.d.ts +26 -31
- package/dist/agent/types.js +6 -7
- package/dist/ai-sdk/coordinatedTool.d.ts +108 -0
- package/dist/ai-sdk/{coordinated-tool.js → coordinatedTool.js} +44 -38
- package/dist/ai-sdk/coordinationContext.d.ts +46 -0
- package/dist/ai-sdk/{coordination-context.js → coordinationContext.js} +30 -31
- package/dist/ai-sdk/index.d.ts +25 -22
- package/dist/ai-sdk/index.js +25 -22
- package/dist/ai-sdk/wrap.d.ts +7 -8
- package/dist/ai-sdk/wrap.js +2 -2
- package/dist/auth/credentialPolicy.d.ts +74 -71
- package/dist/auth/credentialPolicy.js +51 -56
- package/dist/auth/credentialSource.d.ts +7 -18
- package/dist/auth/credentialSource.js +10 -18
- package/dist/auth/index.d.ts +59 -58
- package/dist/auth/index.js +34 -40
- package/dist/auth/schemas.d.ts +5 -4
- package/dist/auth/schemas.js +5 -4
- package/dist/batching/index.d.ts +19 -21
- package/dist/batching/index.js +14 -17
- package/dist/cli.cjs +483 -369
- package/dist/client/Ablo.d.ts +107 -836
- package/dist/client/Ablo.js +174 -833
- package/dist/client/ApiClient.d.ts +44 -20
- package/dist/client/ApiClient.js +193 -44
- package/dist/client/auth.d.ts +51 -60
- package/dist/client/auth.js +137 -110
- package/dist/client/claimHeartbeatLoop.d.ts +50 -0
- package/dist/client/claimHeartbeatLoop.js +88 -0
- package/dist/client/consoleLogger.d.ts +35 -0
- package/dist/client/consoleLogger.js +44 -0
- package/dist/client/createInternalComponents.d.ts +14 -17
- package/dist/client/createInternalComponents.js +26 -31
- package/dist/client/createModelProxy.d.ts +130 -120
- package/dist/client/createModelProxy.js +158 -124
- package/dist/client/credentialEndpoint.d.ts +61 -0
- package/dist/client/credentialEndpoint.js +86 -0
- package/dist/client/functionalUpdate.d.ts +29 -27
- package/dist/client/functionalUpdate.js +21 -21
- package/dist/client/hostedEndpoints.d.ts +21 -0
- package/dist/client/hostedEndpoints.js +21 -0
- package/dist/client/httpClient.d.ts +58 -54
- package/dist/client/httpClient.js +29 -31
- package/dist/client/identity.d.ts +15 -20
- package/dist/client/identity.js +49 -59
- package/dist/client/modelRegistration.d.ts +10 -0
- package/dist/client/modelRegistration.js +301 -0
- package/dist/client/options.d.ts +373 -0
- package/dist/client/options.js +6 -0
- package/dist/client/registerDataSource.d.ts +9 -9
- package/dist/client/registerDataSource.js +15 -16
- package/dist/client/resourceTypes.d.ts +333 -0
- package/dist/client/resourceTypes.js +7 -0
- package/dist/client/schemaConfig.d.ts +44 -0
- package/dist/client/schemaConfig.js +176 -0
- package/dist/client/sessionMint.d.ts +17 -13
- package/dist/client/sessionMint.js +26 -31
- package/dist/client/validateAbloOptions.d.ts +12 -14
- package/dist/client/validateAbloOptions.js +9 -10
- package/dist/client/writeOptionsSchema.d.ts +18 -16
- package/dist/client/writeOptionsSchema.js +23 -20
- package/dist/client/wsMutationExecutor.d.ts +28 -0
- package/dist/client/wsMutationExecutor.js +71 -0
- package/dist/context.d.ts +6 -4
- package/dist/context.js +6 -7
- package/dist/coordination/index.d.ts +13 -4
- package/dist/coordination/index.js +29 -4
- package/dist/coordination/schema.d.ts +176 -128
- package/dist/coordination/schema.js +197 -133
- package/dist/coordination/trace.d.ts +9 -11
- package/dist/coordination/trace.js +13 -15
- package/dist/core/DatabaseManager.d.ts +5 -8
- package/dist/core/DatabaseManager.js +38 -40
- package/dist/core/QueryProcessor.d.ts +7 -9
- package/dist/core/QueryProcessor.js +27 -34
- package/dist/core/QueryView.d.ts +17 -5
- package/dist/core/QueryView.js +6 -7
- package/dist/core/StoreManager.d.ts +14 -16
- package/dist/core/StoreManager.js +26 -25
- package/dist/core/ViewRegistry.d.ts +5 -5
- package/dist/core/ViewRegistry.js +4 -4
- package/dist/core/index.d.ts +18 -13
- package/dist/core/index.js +32 -26
- package/dist/core/openIDBWithTimeout.d.ts +38 -36
- package/dist/core/openIDBWithTimeout.js +57 -54
- package/dist/core/queryUtils.d.ts +45 -0
- package/dist/core/queryUtils.js +69 -0
- package/dist/core/storeContract.d.ts +145 -0
- package/dist/core/storeContract.js +12 -0
- package/dist/environment.d.ts +28 -0
- package/dist/environment.js +21 -0
- package/dist/errorCodes.d.ts +118 -101
- package/dist/errorCodes.js +277 -260
- package/dist/errors.d.ts +170 -165
- package/dist/errors.js +161 -151
- package/dist/index.d.ts +30 -27
- package/dist/index.js +90 -82
- package/dist/interfaces/index.d.ts +108 -133
- package/dist/interfaces/index.js +5 -4
- package/dist/keys/index.d.ts +27 -29
- package/dist/keys/index.js +59 -49
- package/dist/mutators/RecordingTransaction.d.ts +16 -16
- package/dist/mutators/RecordingTransaction.js +31 -37
- package/dist/mutators/Transaction.d.ts +18 -26
- package/dist/mutators/Transaction.js +14 -20
- package/dist/mutators/UndoManager.d.ts +122 -131
- package/dist/mutators/UndoManager.js +149 -155
- package/dist/mutators/defineMutators.d.ts +24 -37
- package/dist/mutators/defineMutators.js +14 -20
- package/dist/mutators/inverseOp.d.ts +12 -15
- package/dist/mutators/inverseOp.js +12 -15
- package/dist/mutators/mutateActions.d.ts +10 -9
- package/dist/mutators/mutateActions.js +1 -1
- package/dist/mutators/readerActions.d.ts +9 -8
- package/dist/mutators/readerActions.js +2 -2
- package/dist/mutators/undoApply.d.ts +31 -27
- package/dist/mutators/undoApply.js +26 -24
- package/dist/policy/index.d.ts +5 -3
- package/dist/policy/index.js +5 -3
- package/dist/policy/types.d.ts +105 -101
- package/dist/policy/types.js +67 -66
- package/dist/query/client.d.ts +32 -16
- package/dist/query/client.js +103 -72
- package/dist/query/types.d.ts +37 -60
- package/dist/query/types.js +13 -33
- package/dist/react/AbloProvider.d.ts +7 -11
- package/dist/react/AbloProvider.js +24 -17
- package/dist/react/context.d.ts +27 -146
- package/dist/react/context.js +9 -10
- package/dist/react/index.d.ts +41 -42
- package/dist/react/index.js +37 -38
- package/dist/react/internalContext.d.ts +17 -19
- package/dist/react/useAblo.d.ts +23 -22
- package/dist/react/useAblo.js +17 -15
- package/dist/react/useCurrentUserId.d.ts +8 -7
- package/dist/react/useCurrentUserId.js +8 -7
- package/dist/react/useErrorListener.d.ts +7 -7
- package/dist/react/useErrorListener.js +11 -12
- package/dist/react/useMutationFailureListener.d.ts +8 -8
- package/dist/react/useMutationFailureListener.js +9 -9
- package/dist/react/useMutators.d.ts +11 -11
- package/dist/react/useMutators.js +10 -4
- package/dist/react/useReactive.js +2 -3
- package/dist/react/useSyncStatus.d.ts +4 -6
- package/dist/react/useUndoScope.d.ts +7 -9
- package/dist/react/useUndoScope.js +3 -3
- package/dist/schema/coordination.d.ts +21 -25
- package/dist/schema/coordination.js +21 -25
- package/dist/schema/ddl.d.ts +43 -39
- package/dist/schema/ddl.js +75 -68
- package/dist/schema/ddlLock.d.ts +35 -0
- package/dist/schema/ddlLock.js +46 -0
- package/dist/schema/diff.d.ts +99 -61
- package/dist/schema/diff.js +43 -34
- package/dist/schema/field.d.ts +37 -42
- package/dist/schema/field.js +36 -49
- package/dist/schema/generate.d.ts +12 -12
- package/dist/schema/generate.js +12 -12
- package/dist/schema/index.d.ts +5 -4
- package/dist/schema/index.js +29 -21
- package/dist/schema/model.d.ts +121 -146
- package/dist/schema/model.js +24 -35
- package/dist/schema/openapi.d.ts +10 -9
- package/dist/schema/openapi.js +7 -1
- package/dist/schema/queries.d.ts +30 -32
- package/dist/schema/queries.js +24 -25
- package/dist/schema/relation.d.ts +89 -99
- package/dist/schema/relation.js +13 -13
- package/dist/schema/residency.d.ts +38 -0
- package/dist/schema/residency.js +30 -0
- package/dist/schema/roles.d.ts +45 -27
- package/dist/schema/roles.js +52 -21
- package/dist/schema/schema.d.ts +36 -45
- package/dist/schema/schema.js +42 -39
- package/dist/schema/select.d.ts +13 -13
- package/dist/schema/select.js +13 -13
- package/dist/schema/serialize.d.ts +36 -39
- package/dist/schema/serialize.js +27 -31
- package/dist/schema/sugar.d.ts +17 -32
- package/dist/schema/sugar.js +14 -29
- package/dist/schema/{sync-delta-row.d.ts → syncDeltaRow.d.ts} +27 -50
- package/dist/schema/syncDeltaRow.js +89 -0
- package/dist/schema/tenancy.d.ts +44 -46
- package/dist/schema/tenancy.js +46 -48
- package/dist/server/adapter.d.ts +58 -58
- package/dist/server/adapter.js +13 -14
- package/dist/server/commit.d.ts +60 -64
- package/dist/server/index.d.ts +9 -10
- package/dist/server/index.js +1 -1
- package/dist/server/readConfig.d.ts +70 -0
- package/dist/server/readConfig.js +8 -0
- package/dist/server/storageMode.d.ts +23 -0
- package/dist/server/storageMode.js +17 -0
- package/dist/source/adapter.d.ts +31 -26
- package/dist/source/adapter.js +10 -10
- package/dist/source/adapters/drizzle.d.ts +28 -23
- package/dist/source/adapters/drizzle.js +34 -28
- package/dist/source/adapters/kysely.d.ts +27 -25
- package/dist/source/adapters/kysely.js +28 -26
- package/dist/source/adapters/memory.d.ts +8 -7
- package/dist/source/adapters/memory.js +10 -9
- package/dist/source/adapters/prisma.d.ts +13 -12
- package/dist/source/adapters/prisma.js +27 -29
- package/dist/source/conformance.d.ts +18 -11
- package/dist/source/conformance.js +27 -19
- package/dist/source/connector.d.ts +31 -32
- package/dist/source/connector.js +30 -28
- package/dist/source/connectorProtocol.d.ts +160 -0
- package/dist/source/connectorProtocol.js +162 -0
- package/dist/source/contract.d.ts +26 -27
- package/dist/source/contract.js +28 -29
- package/dist/source/factory.d.ts +94 -0
- package/dist/source/factory.js +268 -0
- package/dist/source/index.d.ts +10 -462
- package/dist/source/index.js +17 -421
- package/dist/source/migrations.d.ts +9 -9
- package/dist/source/migrations.js +9 -9
- package/dist/source/next.d.ts +10 -11
- package/dist/source/next.js +7 -8
- package/dist/source/pushQueue.d.ts +70 -48
- package/dist/source/pushQueue.js +36 -29
- package/dist/source/signing.d.ts +88 -0
- package/dist/source/signing.js +159 -0
- package/dist/source/types.d.ts +351 -0
- package/dist/source/types.js +43 -0
- package/dist/stores/ObjectStore.d.ts +11 -12
- package/dist/stores/ObjectStore.js +34 -35
- package/dist/stores/ObjectStoreContract.d.ts +12 -15
- package/dist/stores/SyncActionStore.d.ts +8 -12
- package/dist/stores/SyncActionStore.js +77 -46
- package/dist/surface.d.ts +28 -21
- package/dist/surface.js +28 -20
- package/dist/sync/{BootstrapHelper.d.ts → BootstrapFetcher.d.ts} +37 -45
- package/dist/sync/{BootstrapHelper.js → BootstrapFetcher.js} +101 -80
- package/dist/sync/ConnectionManager.d.ts +47 -50
- package/dist/sync/ConnectionManager.js +74 -70
- package/dist/sync/NetworkProbe.d.ts +27 -31
- package/dist/sync/NetworkProbe.js +67 -72
- package/dist/sync/{HydrationCoordinator.d.ts → OnDemandLoader.d.ts} +49 -36
- package/dist/sync/{HydrationCoordinator.js → OnDemandLoader.js} +79 -54
- package/dist/sync/{AreaOfInterestManager.d.ts → SubscriptionManager.d.ts} +45 -59
- package/dist/sync/{AreaOfInterestManager.js → SubscriptionManager.js} +44 -52
- package/dist/sync/SyncWebSocket.d.ts +175 -250
- package/dist/sync/SyncWebSocket.js +431 -769
- package/dist/sync/awaitClaimGrant.d.ts +18 -18
- package/dist/sync/awaitClaimGrant.js +38 -30
- package/dist/sync/bootstrapApply.d.ts +70 -0
- package/dist/sync/bootstrapApply.js +73 -0
- package/dist/sync/commitFrames.d.ts +44 -0
- package/dist/sync/commitFrames.js +94 -0
- package/dist/sync/createClaimStream.d.ts +23 -22
- package/dist/sync/createClaimStream.js +108 -25
- package/dist/sync/createPresenceStream.d.ts +19 -18
- package/dist/sync/createPresenceStream.js +25 -26
- package/dist/sync/createSnapshot.d.ts +13 -17
- package/dist/sync/createSnapshot.js +20 -26
- package/dist/sync/credentialLifecycle.d.ts +175 -0
- package/dist/sync/credentialLifecycle.js +322 -0
- package/dist/sync/deltaPipeline.d.ts +113 -0
- package/dist/sync/deltaPipeline.js +261 -0
- package/dist/sync/groupChange.d.ts +113 -0
- package/dist/sync/groupChange.js +242 -0
- package/dist/sync/heartbeat.d.ts +63 -0
- package/dist/sync/heartbeat.js +91 -0
- package/dist/sync/participants.d.ts +27 -27
- package/dist/sync/schemas.d.ts +3 -2
- package/dist/sync/schemas.js +14 -10
- package/dist/sync/syncCursor.d.ts +40 -0
- package/dist/sync/syncCursor.js +55 -0
- package/dist/sync/syncPlan.d.ts +54 -0
- package/dist/sync/syncPlan.js +50 -0
- package/dist/sync/syncPosition.d.ts +54 -49
- package/dist/sync/syncPosition.js +57 -52
- package/dist/sync/wsFrameHandlers.d.ts +116 -0
- package/dist/sync/wsFrameHandlers.js +374 -0
- package/dist/testing/fixtures/bootstrap.d.ts +21 -17
- package/dist/testing/fixtures/bootstrap.js +12 -6
- package/dist/testing/fixtures/deltas.d.ts +31 -34
- package/dist/testing/fixtures/deltas.js +30 -33
- package/dist/testing/fixtures/models.d.ts +11 -10
- package/dist/testing/fixtures/models.js +12 -10
- package/dist/testing/helpers/{react-wrapper.d.ts → reactWrapper.d.ts} +13 -10
- package/dist/testing/helpers/{react-wrapper.js → reactWrapper.js} +15 -12
- package/dist/testing/helpers/{sync-engine-harness.d.ts → syncEngineHarness.d.ts} +17 -18
- package/dist/testing/helpers/{sync-engine-harness.js → syncEngineHarness.js} +14 -11
- package/dist/testing/helpers/wait.d.ts +13 -8
- package/dist/testing/helpers/wait.js +13 -8
- package/dist/testing/index.d.ts +4 -4
- package/dist/testing/index.js +3 -3
- package/dist/testing/mocks/MockMutationExecutor.d.ts +18 -17
- package/dist/testing/mocks/MockMutationExecutor.js +15 -14
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +8 -8
- package/dist/testing/mocks/MockNetworkMonitor.js +8 -8
- package/dist/testing/mocks/MockSyncContext.d.ts +21 -34
- package/dist/testing/mocks/MockSyncContext.js +16 -45
- package/dist/testing/mocks/MockSyncStore.d.ts +14 -14
- package/dist/testing/mocks/MockSyncStore.js +11 -11
- package/dist/testing/mocks/MockWebSocket.d.ts +28 -24
- package/dist/testing/mocks/MockWebSocket.js +22 -21
- package/dist/transactions/TransactionQueue.d.ts +190 -221
- package/dist/transactions/TransactionQueue.js +424 -822
- package/dist/transactions/TransactionStore.d.ts +20 -0
- package/dist/transactions/TransactionStore.js +53 -0
- package/dist/transactions/UnconfirmedWrites.d.ts +82 -0
- package/dist/transactions/UnconfirmedWrites.js +104 -0
- package/dist/transactions/coalesceRules.d.ts +58 -0
- package/dist/transactions/coalesceRules.js +140 -0
- package/dist/transactions/commitPayload.d.ts +130 -0
- package/dist/transactions/commitPayload.js +143 -0
- package/dist/transactions/deltaConfirmation.d.ts +58 -0
- package/dist/transactions/deltaConfirmation.js +215 -0
- package/dist/transactions/optimisticApply.d.ts +49 -0
- package/dist/transactions/optimisticApply.js +65 -0
- package/dist/transactions/replayValidation.d.ts +99 -0
- package/dist/transactions/replayValidation.js +111 -0
- package/dist/types/global.d.ts +46 -41
- package/dist/types/global.js +20 -19
- package/dist/types/index.d.ts +74 -80
- package/dist/types/index.js +22 -27
- package/dist/types/modelData.d.ts +10 -0
- package/dist/types/modelData.js +9 -0
- package/dist/types/participant.d.ts +20 -0
- package/dist/types/participant.js +10 -0
- package/dist/types/streams.d.ts +216 -209
- package/dist/types/streams.js +7 -7
- package/dist/utils/asyncIterator.d.ts +25 -32
- package/dist/utils/asyncIterator.js +25 -32
- package/dist/utils/duration.d.ts +12 -15
- package/dist/utils/duration.js +12 -15
- package/dist/utils/mobxSetup.d.ts +53 -0
- package/dist/utils/{mobx-setup.js → mobxSetup.js} +44 -100
- package/dist/webhooks/events.d.ts +21 -16
- package/dist/webhooks/events.js +10 -8
- package/dist/webhooks/index.d.ts +5 -7
- package/dist/webhooks/index.js +5 -7
- package/dist/{schema/sync-delta-wire.d.ts → wire/delta.d.ts} +58 -41
- package/dist/wire/delta.js +114 -0
- package/dist/wire/errorEnvelope.d.ts +35 -27
- package/dist/wire/errorEnvelope.js +38 -32
- package/dist/wire/frames.d.ts +150 -67
- package/dist/wire/frames.js +48 -1
- package/dist/wire/index.d.ts +18 -13
- package/dist/wire/index.js +36 -13
- package/dist/wire/listEnvelope.d.ts +16 -23
- package/dist/wire/listEnvelope.js +7 -6
- package/dist/wire/protocol.d.ts +38 -0
- package/dist/wire/protocol.js +38 -0
- package/dist/wire/protocolVersion.d.ts +60 -0
- package/dist/wire/protocolVersion.js +67 -0
- package/docs/api-keys.md +4 -3
- package/docs/coordination.md +59 -0
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/identity.md +4 -4
- package/docs/integration-guide.md +1 -1
- package/docs/react.md +1 -1
- package/docs/sessions.md +5 -7
- package/package.json +24 -21
- package/dist/ai-sdk/coordinated-tool.d.ts +0 -101
- package/dist/ai-sdk/coordination-context.d.ts +0 -52
- package/dist/client/index.d.ts +0 -36
- package/dist/client/index.js +0 -33
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -12
- package/dist/core/query-utils.d.ts +0 -34
- package/dist/core/query-utils.js +0 -59
- package/dist/interfaces/headless.d.ts +0 -95
- package/dist/interfaces/headless.js +0 -41
- package/dist/query/index.d.ts +0 -6
- package/dist/query/index.js +0 -5
- package/dist/realtime/index.d.ts +0 -10
- package/dist/realtime/index.js +0 -9
- package/dist/schema/plane.d.ts +0 -23
- package/dist/schema/plane.js +0 -19
- package/dist/schema/sync-delta-row.js +0 -103
- package/dist/schema/sync-delta-wire.js +0 -102
- package/dist/server/next.d.ts +0 -51
- package/dist/server/next.js +0 -47
- package/dist/server/read-config.d.ts +0 -67
- package/dist/server/read-config.js +0 -8
- package/dist/server/storage-mode.d.ts +0 -1
- package/dist/server/storage-mode.js +0 -18
- package/dist/source/connector-protocol.d.ts +0 -159
- package/dist/source/connector-protocol.js +0 -161
- package/dist/sync/OfflineFlush.d.ts +0 -9
- package/dist/sync/OfflineFlush.js +0 -22
- package/dist/sync/OfflineTransactionStore.d.ts +0 -37
- package/dist/sync/OfflineTransactionStore.js +0 -263
- package/dist/transactions/OptimisticEchoTracker.d.ts +0 -82
- package/dist/transactions/OptimisticEchoTracker.js +0 -104
- package/dist/transactions/index.d.ts +0 -16
- package/dist/transactions/index.js +0 -7
- package/dist/transactions/mutation-error-handler.d.ts +0 -5
- package/dist/transactions/mutation-error-handler.js +0 -39
- package/dist/utils/mobx-setup.d.ts +0 -42
package/dist/SyncClient.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* Applies model mutations and manages the offline write queue. The
|
|
3
|
+
* SyncClient turns local create, update, delete, and archive calls into
|
|
4
|
+
* optimistic changes, holds them while the client is offline, sends them to
|
|
5
|
+
* the server when connectivity returns, and resolves conflicts when the
|
|
6
|
+
* server's version of a row disagrees with the local one. It sits between the
|
|
7
|
+
* reactive object pool and the {@link TransactionQueue} that delivers writes
|
|
8
|
+
* over the network.
|
|
9
9
|
*/
|
|
10
10
|
import { runInAction } from 'mobx';
|
|
11
|
-
import { ModelScope } from './
|
|
11
|
+
import { InstanceCache, ModelScope } from './InstanceCache.js';
|
|
12
|
+
import { Model } from './Model.js';
|
|
12
13
|
// ModelRegistry instance accessed via this.objectPool.registry
|
|
13
14
|
import { LoadStrategy } from './types/index.js';
|
|
14
15
|
import { getContext } from './context.js';
|
|
@@ -16,16 +17,18 @@ import { AbloAuthenticationError, AbloError, AbloValidationError } from './error
|
|
|
16
17
|
import { EventEmitter } from 'events';
|
|
17
18
|
import { NetworkMonitor } from './NetworkMonitor.js';
|
|
18
19
|
import { TransactionQueue } from './transactions/TransactionQueue.js';
|
|
19
|
-
import {
|
|
20
|
+
import { persistedMutationSchema } from './transactions/replayValidation.js';
|
|
21
|
+
import { UnconfirmedWrites, } from './transactions/UnconfirmedWrites.js';
|
|
20
22
|
import { SyncPosition } from './sync/syncPosition.js';
|
|
21
23
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
24
|
+
* Reports whether an incoming snapshot record is strictly newer than the
|
|
25
|
+
* model already in the pool. The comparison uses the server-stamped
|
|
26
|
+
* `updatedAt` timestamp, since rows carry no numeric version and the delta
|
|
27
|
+
* pipeline resolves order by arrival (last write wins). An undefined incoming
|
|
28
|
+
* timestamp counts as not newer, so a known row is never clobbered; an
|
|
29
|
+
* undefined existing timestamp means the pooled row is unversioned, so the
|
|
30
|
+
* incoming record wins. The scoped hydrate-on-enter path uses this to drop
|
|
31
|
+
* snapshot rows that a live delta has already advanced past.
|
|
29
32
|
*/
|
|
30
33
|
function rawRecordIsNewer(data, existing) {
|
|
31
34
|
const raw = data.updatedAt;
|
|
@@ -43,6 +46,22 @@ function rawRecordIsNewer(data, existing) {
|
|
|
43
46
|
return true;
|
|
44
47
|
return inMs > exMs;
|
|
45
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Converts an untyped server `updatedAt` value — an ISO string, epoch number,
|
|
51
|
+
* or Date read off an untyped row — into epoch milliseconds for
|
|
52
|
+
* last-write-wins comparison. Falsy or non-date values become 0, matching the
|
|
53
|
+
* conflict resolver's rule that a missing timestamp sorts as the epoch.
|
|
54
|
+
*/
|
|
55
|
+
function toEpochMs(value) {
|
|
56
|
+
if (!value)
|
|
57
|
+
return 0;
|
|
58
|
+
if (value instanceof Date)
|
|
59
|
+
return value.getTime();
|
|
60
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
61
|
+
return new Date(value).getTime();
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
46
65
|
export class SyncClient extends EventEmitter {
|
|
47
66
|
objectPool;
|
|
48
67
|
database;
|
|
@@ -56,32 +75,28 @@ export class SyncClient extends EventEmitter {
|
|
|
56
75
|
// Pending mutations queue
|
|
57
76
|
pendingMutations = [];
|
|
58
77
|
/**
|
|
59
|
-
* Tracks
|
|
60
|
-
* the server has not yet confirmed.
|
|
61
|
-
* to recognize
|
|
62
|
-
*
|
|
63
|
-
* because the delta is the authoritative version of the row.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* it, an optimistically-applied DELETE followed by a
|
|
68
|
-
* server-confirming CREATE echo resurrects the row for the window
|
|
69
|
-
* between the two confirmations (the chart-delete flicker).
|
|
78
|
+
* Tracks the ids of transactions the client has applied optimistically but
|
|
79
|
+
* the server has not yet confirmed. When a delta arrives, the receive path
|
|
80
|
+
* consults this set to recognize the echo of the client's own mutation and
|
|
81
|
+
* skip the now-redundant pool update; the IndexedDB write still runs,
|
|
82
|
+
* because the delta is the authoritative version of the row. Without this
|
|
83
|
+
* discriminator, an optimistically applied delete followed by a
|
|
84
|
+
* server-confirmed create echo would resurrect the row for the window
|
|
85
|
+
* between the two confirmations.
|
|
70
86
|
*
|
|
71
|
-
*
|
|
87
|
+
* The set is bounded with first-in-first-out eviction, and
|
|
88
|
+
* {@link SyncClient.getEchoMetrics} exposes its counters.
|
|
72
89
|
*/
|
|
73
|
-
echoTracker = new
|
|
90
|
+
echoTracker = new UnconfirmedWrites();
|
|
74
91
|
// Connection state
|
|
75
92
|
connectionState = 'disconnected';
|
|
76
|
-
offlineSince;
|
|
77
93
|
// Configuration
|
|
78
|
-
maxRetries = 3;
|
|
79
94
|
isDisposed = false;
|
|
80
95
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* `
|
|
84
|
-
*
|
|
96
|
+
* The client's position in the global delta order, held as the single
|
|
97
|
+
* canonical {@link SyncPosition} instance. The store advances `applied` and
|
|
98
|
+
* `persisted` as deltas land, the queue advances `acked` on commit
|
|
99
|
+
* responses, and snapshots and claims read `readFloor`.
|
|
85
100
|
*/
|
|
86
101
|
position = new SyncPosition();
|
|
87
102
|
constructor(objectPool, database) {
|
|
@@ -92,8 +107,8 @@ export class SyncClient extends EventEmitter {
|
|
|
92
107
|
// Initialize TransactionQueue with proper configuration
|
|
93
108
|
this.transactionQueue = new TransactionQueue({
|
|
94
109
|
position: this.position,
|
|
95
|
-
maxBatchSize: 50, //
|
|
96
|
-
//
|
|
110
|
+
maxBatchSize: 50, // Larger batches keep the batch count low for bulk operations
|
|
111
|
+
// A short delay keeps writes responsive; coalescing still groups them
|
|
97
112
|
batchDelay: 150,
|
|
98
113
|
maxRetries: 3,
|
|
99
114
|
enableOptimistic: true,
|
|
@@ -104,16 +119,17 @@ export class SyncClient extends EventEmitter {
|
|
|
104
119
|
});
|
|
105
120
|
// Provide connection state to TransactionQueue - prevents rollbacks during disconnection
|
|
106
121
|
this.transactionQueue.setConnectionChecker(() => this.connectionState === 'connected');
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
122
|
+
// Restore object-pool state when a transaction is rolled back. If the
|
|
123
|
+
// server rejects a write or it times out, the model's previous state is
|
|
124
|
+
// put back. Because writes are no longer applied to IndexedDB
|
|
125
|
+
// optimistically, that store already holds the correct state.
|
|
110
126
|
this.setupTransactionRollbackHandling();
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
127
|
+
// Forward reconciliation requests from the transaction queue. When delta
|
|
128
|
+
// confirmation times out, the client cycles the WebSocket connection to
|
|
129
|
+
// trigger a catch-up from the server rather than rolling the write back.
|
|
114
130
|
this.setupReconciliationForwarding();
|
|
115
|
-
//
|
|
116
|
-
//
|
|
131
|
+
// Persist unconfirmed transactions to IndexedDB. When delta retries are
|
|
132
|
+
// exhausted, the write is cached so it survives a tab close.
|
|
117
133
|
this.setupAwaitingTransactionPersistence();
|
|
118
134
|
// Setup network monitoring
|
|
119
135
|
this.setupNetworkMonitoring();
|
|
@@ -122,8 +138,25 @@ export class SyncClient extends EventEmitter {
|
|
|
122
138
|
* Setup network monitoring handlers
|
|
123
139
|
*/
|
|
124
140
|
setupNetworkMonitoring() {
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
// Both handlers emit to external listeners (which can throw) before/around
|
|
142
|
+
// their own try/catch — route rejections into observability rather than
|
|
143
|
+
// losing a failed reconnect flush silently.
|
|
144
|
+
this.networkMonitor.on('online', () => {
|
|
145
|
+
void this.handleReconnection().catch((error) => {
|
|
146
|
+
getContext().observability.captureTransactionFailure({
|
|
147
|
+
context: 'network-online-reconnection',
|
|
148
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
this.networkMonitor.on('offline', () => {
|
|
153
|
+
void this.handleDisconnection().catch((error) => {
|
|
154
|
+
getContext().observability.captureTransactionFailure({
|
|
155
|
+
context: 'network-offline-handler',
|
|
156
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
127
160
|
}
|
|
128
161
|
/**
|
|
129
162
|
* Handle transaction rollback. Two distinct shapes flow through this
|
|
@@ -254,10 +287,10 @@ export class SyncClient extends EventEmitter {
|
|
|
254
287
|
});
|
|
255
288
|
}
|
|
256
289
|
/**
|
|
257
|
-
* Forward reconciliation requests from TransactionQueue to the
|
|
258
|
-
* When delta confirmation times out,
|
|
259
|
-
* instead of rolling back
|
|
260
|
-
*
|
|
290
|
+
* Forward reconciliation requests from the {@link TransactionQueue} to the
|
|
291
|
+
* sync layer. When delta confirmation times out, the queue emits
|
|
292
|
+
* `reconciliation:needed` instead of rolling back, so optimistic state the
|
|
293
|
+
* server may already have committed is never destroyed.
|
|
261
294
|
*/
|
|
262
295
|
setupReconciliationForwarding() {
|
|
263
296
|
this.transactionQueue.on('reconciliation:needed', (event) => {
|
|
@@ -275,52 +308,20 @@ export class SyncClient extends EventEmitter {
|
|
|
275
308
|
});
|
|
276
309
|
}
|
|
277
310
|
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
311
|
+
* Persist unconfirmed transactions to IndexedDB. When delta-confirmation
|
|
312
|
+
* retries are exhausted, the transaction is cached so it survives a tab
|
|
313
|
+
* close. On the next session, a WebSocket reconnect and delta catch-up
|
|
314
|
+
* deliver the missing deltas and confirm the transaction.
|
|
282
315
|
*/
|
|
283
316
|
setupAwaitingTransactionPersistence() {
|
|
284
|
-
this.transactionQueue.on('transaction:persist_awaiting',
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
try {
|
|
288
|
-
await this.database.saveTransaction({
|
|
289
|
-
id: `awaiting_${event.txId}`,
|
|
290
|
-
type: 'awaiting_delta',
|
|
291
|
-
timestamp: Date.now(),
|
|
292
|
-
awaitingDelta: {
|
|
293
|
-
syncIdNeeded: event.syncIdNeeded ?? 0,
|
|
294
|
-
modelName: event.model,
|
|
295
|
-
modelId: event.modelId,
|
|
296
|
-
operationType: event.operationType,
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
|
-
getContext().observability.breadcrumb('Persisted unconfirmed transaction to IDB', 'sync.transaction', 'info', {
|
|
300
|
-
txId: event.txId,
|
|
301
|
-
model: event.model,
|
|
302
|
-
modelId: event.modelId,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
catch (error) {
|
|
306
|
-
getContext().observability.captureTransactionFailure({
|
|
307
|
-
context: 'persist-awaiting-transaction',
|
|
308
|
-
modelName: event.model,
|
|
309
|
-
modelId: event.modelId,
|
|
310
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
311
|
-
});
|
|
312
|
-
}
|
|
317
|
+
this.transactionQueue.on('transaction:persist_awaiting', (event) => {
|
|
318
|
+
// void is safe: the handler's body is fully try/catch'd.
|
|
319
|
+
void this.persistAwaitingTransaction(event);
|
|
313
320
|
});
|
|
314
321
|
// Clean up persisted awaiting transactions when they're finally confirmed
|
|
315
|
-
this.transactionQueue.on('transaction:completed',
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
try {
|
|
319
|
-
await this.database.removeTransaction(`awaiting_${tx.id}`);
|
|
320
|
-
}
|
|
321
|
-
catch {
|
|
322
|
-
// Ignore — might not have been persisted
|
|
323
|
-
}
|
|
322
|
+
this.transactionQueue.on('transaction:completed', (tx) => {
|
|
323
|
+
// void is safe: the handler's body is fully try/catch'd.
|
|
324
|
+
void this.removeAwaitingTransaction(tx.id);
|
|
324
325
|
});
|
|
325
326
|
// Echo detection bridge. When the queue stages a transaction, the
|
|
326
327
|
// client has already optimistically applied the change to the
|
|
@@ -338,6 +339,48 @@ export class SyncClient extends EventEmitter {
|
|
|
338
339
|
this.echoTracker.drainOnRollback(event.transaction.id);
|
|
339
340
|
});
|
|
340
341
|
}
|
|
342
|
+
/** Persist an unconfirmed transaction to IndexedDB (never rejects — failures are captured). */
|
|
343
|
+
async persistAwaitingTransaction(event) {
|
|
344
|
+
if (!this.database)
|
|
345
|
+
return;
|
|
346
|
+
try {
|
|
347
|
+
await this.database.saveTransaction({
|
|
348
|
+
id: `awaiting_${event.txId}`,
|
|
349
|
+
type: 'awaiting_delta',
|
|
350
|
+
timestamp: Date.now(),
|
|
351
|
+
awaitingDelta: {
|
|
352
|
+
syncIdNeeded: event.syncIdNeeded ?? 0,
|
|
353
|
+
modelName: event.model,
|
|
354
|
+
modelId: event.modelId,
|
|
355
|
+
operationType: event.operationType,
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
getContext().observability.breadcrumb('Persisted unconfirmed transaction to IDB', 'sync.transaction', 'info', {
|
|
359
|
+
txId: event.txId,
|
|
360
|
+
model: event.model,
|
|
361
|
+
modelId: event.modelId,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
getContext().observability.captureTransactionFailure({
|
|
366
|
+
context: 'persist-awaiting-transaction',
|
|
367
|
+
modelName: event.model,
|
|
368
|
+
modelId: event.modelId,
|
|
369
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/** Drop the persisted awaiting-row once confirmed (never rejects). */
|
|
374
|
+
async removeAwaitingTransaction(txId) {
|
|
375
|
+
if (!this.database)
|
|
376
|
+
return;
|
|
377
|
+
try {
|
|
378
|
+
await this.database.removeTransaction(`awaiting_${txId}`);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// Ignore — might not have been persisted
|
|
382
|
+
}
|
|
383
|
+
}
|
|
341
384
|
/**
|
|
342
385
|
* Initialize sync client with authentication
|
|
343
386
|
*/
|
|
@@ -347,19 +390,17 @@ export class SyncClient extends EventEmitter {
|
|
|
347
390
|
getContext().observability.setContext(userId, organizationId);
|
|
348
391
|
// Restore queued mutations from previous session
|
|
349
392
|
await this.restoreMutationQueue();
|
|
350
|
-
//
|
|
351
|
-
// In the browser this
|
|
352
|
-
//
|
|
353
|
-
// online
|
|
354
|
-
//
|
|
355
|
-
// status snapshot at registerUser() time.
|
|
393
|
+
// Read the initial network status from the injected OnlineStatusProvider.
|
|
394
|
+
// In the browser this reflects the host's connectivity signal; in Node it
|
|
395
|
+
// reports online by default. NetworkMonitor drives the ongoing
|
|
396
|
+
// online/offline transitions below — this read is only the initial
|
|
397
|
+
// snapshot taken when identity is set.
|
|
356
398
|
if (getContext().onlineStatus.isOnline()) {
|
|
357
399
|
this.setConnectionState('connected');
|
|
358
400
|
}
|
|
359
401
|
else {
|
|
360
402
|
// Offline - start in offline mode
|
|
361
403
|
this.setConnectionState('disconnected');
|
|
362
|
-
this.offlineSince = new Date();
|
|
363
404
|
this.emit('sync:offline');
|
|
364
405
|
}
|
|
365
406
|
}
|
|
@@ -375,7 +416,7 @@ export class SyncClient extends EventEmitter {
|
|
|
375
416
|
* Self-healing helper for individual model records.
|
|
376
417
|
*
|
|
377
418
|
* Two registry-driven repair passes run on every row hydrated from
|
|
378
|
-
*
|
|
419
|
+
* IndexedDB or merged from a delta:
|
|
379
420
|
*
|
|
380
421
|
* 1. **Auto-fill** — for each `autoFill` rule the consumer's schema
|
|
381
422
|
* declares on this model, copy the corresponding identity value
|
|
@@ -431,7 +472,7 @@ export class SyncClient extends EventEmitter {
|
|
|
431
472
|
return { data: result, healed };
|
|
432
473
|
}
|
|
433
474
|
/**
|
|
434
|
-
* Hydrate
|
|
475
|
+
* Hydrate InstanceCache with data from Database
|
|
435
476
|
* Called after bootstrap is complete
|
|
436
477
|
*/
|
|
437
478
|
async hydrateFromDatabase() {
|
|
@@ -483,8 +524,9 @@ export class SyncClient extends EventEmitter {
|
|
|
483
524
|
// Persist healed records back to IndexedDB (fire-and-forget, non-blocking)
|
|
484
525
|
if (recordsToHeal.length > 0 && this.database) {
|
|
485
526
|
getContext().logger.info(`[SyncClient.hydrate] Persisting ${recordsToHeal.length} healed ${modelType} records to IndexedDB`);
|
|
486
|
-
// Use fire-and-forget to not block hydration
|
|
487
|
-
|
|
527
|
+
// Use fire-and-forget to not block hydration.
|
|
528
|
+
// void is safe: the handler's body is fully try/catch'd.
|
|
529
|
+
void Promise.resolve().then(async () => {
|
|
488
530
|
try {
|
|
489
531
|
for (const { id, data } of recordsToHeal) {
|
|
490
532
|
await this.database.putRecord(modelType, id, data);
|
|
@@ -501,13 +543,6 @@ export class SyncClient extends EventEmitter {
|
|
|
501
543
|
});
|
|
502
544
|
}
|
|
503
545
|
const typeEnd = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
504
|
-
// Dev-only hydration summary
|
|
505
|
-
if (modelType === 'InboxItem' && process.env.NODE_ENV !== 'production') {
|
|
506
|
-
getContext().logger.debug('[SyncClient] InboxItem hydration summary', {
|
|
507
|
-
fetched: rawData.length,
|
|
508
|
-
added: modelsForType.length,
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
546
|
perTypePerfLogs.push({
|
|
512
547
|
type: modelType,
|
|
513
548
|
fetched: rawData.length,
|
|
@@ -552,7 +587,7 @@ export class SyncClient extends EventEmitter {
|
|
|
552
587
|
catch { }
|
|
553
588
|
}
|
|
554
589
|
/**
|
|
555
|
-
* Re-hydrate
|
|
590
|
+
* Re-hydrate InstanceCache from IndexedDB when the pool already has data.
|
|
556
591
|
*
|
|
557
592
|
* Unlike hydrateFromDatabase() (which uses addBatch and skips existing IDs),
|
|
558
593
|
* this method properly:
|
|
@@ -600,7 +635,8 @@ export class SyncClient extends EventEmitter {
|
|
|
600
635
|
if (this.database) {
|
|
601
636
|
const id = healResult.data.id;
|
|
602
637
|
const healedData = healResult.data;
|
|
603
|
-
|
|
638
|
+
// void is safe: the handler's body is fully try/catch'd.
|
|
639
|
+
void Promise.resolve().then(async () => {
|
|
604
640
|
try {
|
|
605
641
|
await this.database.putRecord(modelType, id, healedData);
|
|
606
642
|
}
|
|
@@ -686,13 +722,14 @@ export class SyncClient extends EventEmitter {
|
|
|
686
722
|
return stats;
|
|
687
723
|
}
|
|
688
724
|
/**
|
|
689
|
-
*
|
|
690
|
-
* IndexedDB is only
|
|
725
|
+
* Apply a mutation to a model optimistically and queue it for server sync.
|
|
726
|
+
* IndexedDB is updated only once the server confirms the change with a delta
|
|
727
|
+
* packet.
|
|
691
728
|
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
729
|
+
* A model's changes are captured before the pool action runs, because a pool
|
|
730
|
+
* operation such as an upsert can clear the model's local change set;
|
|
731
|
+
* capturing first ensures those changes are never lost. The captured set is
|
|
732
|
+
* frozen and handed to {@link queueMutation}.
|
|
696
733
|
*/
|
|
697
734
|
mutate(type, model, poolAction, writeOptions) {
|
|
698
735
|
// No-op UPDATE guard (O(1)). An update with no dirty fields would travel
|
|
@@ -709,9 +746,9 @@ export class SyncClient extends EventEmitter {
|
|
|
709
746
|
// real write. Only a genuine Model with an empty dirty-set is skipped.
|
|
710
747
|
if (type === 'update' && model.hasChanges === false)
|
|
711
748
|
return;
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
//
|
|
749
|
+
// Capture changes before the pool action runs. Pool operations —
|
|
750
|
+
// upsert in particular — can clear the model's local changes, so
|
|
751
|
+
// capturing first ensures they are never lost.
|
|
715
752
|
const capturedChanges = type === 'update' || type === 'create' ? this.captureModelChanges(model) : undefined;
|
|
716
753
|
poolAction();
|
|
717
754
|
this.queueMutation({ type, model, timestamp: new Date(), capturedChanges, writeOptions });
|
|
@@ -755,11 +792,11 @@ export class SyncClient extends EventEmitter {
|
|
|
755
792
|
}
|
|
756
793
|
/** Add new model (CREATE) - works offline */
|
|
757
794
|
add(model, options) {
|
|
758
|
-
this.mutate('create', model, () => this.objectPool.add(model, ModelScope.live), options);
|
|
795
|
+
this.mutate('create', model, () => { this.objectPool.add(model, ModelScope.live); }, options);
|
|
759
796
|
}
|
|
760
797
|
/** Update existing model (UPDATE) - works offline */
|
|
761
798
|
update(model, options) {
|
|
762
|
-
this.mutate('update', model, () => this.objectPool.upsert(model, ModelScope.live), options);
|
|
799
|
+
this.mutate('update', model, () => { this.objectPool.upsert(model, ModelScope.live); }, options);
|
|
763
800
|
}
|
|
764
801
|
/**
|
|
765
802
|
* Update existing model with pre-computed changes.
|
|
@@ -822,8 +859,9 @@ export class SyncClient extends EventEmitter {
|
|
|
822
859
|
}
|
|
823
860
|
}
|
|
824
861
|
/**
|
|
825
|
-
* Upload file and create attachment
|
|
826
|
-
*
|
|
862
|
+
* Upload a file and create its attachment record. The upload runs through
|
|
863
|
+
* the {@link TransactionQueue}, and a model is built from the server's
|
|
864
|
+
* response and added to the pool.
|
|
827
865
|
*/
|
|
828
866
|
async uploadFile(file, options) {
|
|
829
867
|
if (!this.userId || !this.organizationId) {
|
|
@@ -912,19 +950,20 @@ export class SyncClient extends EventEmitter {
|
|
|
912
950
|
}
|
|
913
951
|
/** Archive model (ARCHIVE) - works offline */
|
|
914
952
|
archive(model) {
|
|
915
|
-
this.mutate('archive', model, () => this.objectPool.updateScope(model.id, ModelScope.archived));
|
|
953
|
+
this.mutate('archive', model, () => { this.objectPool.updateScope(model.id, ModelScope.archived); });
|
|
916
954
|
}
|
|
917
955
|
/**
|
|
918
|
-
* Append a mutation and schedule its sync work.
|
|
956
|
+
* Append a mutation to the pending queue and schedule its sync work.
|
|
919
957
|
*
|
|
920
|
-
*
|
|
921
|
-
* pushes
|
|
922
|
-
* process call. Without the deferral, queueing
|
|
923
|
-
*
|
|
924
|
-
* growing queue
|
|
958
|
+
* IndexedDB persistence and the server push are deferred to a microtask, so
|
|
959
|
+
* many pushes within the same tick collapse into a single serialization and
|
|
960
|
+
* a single process call. Without the deferral, queueing a hundred mutations
|
|
961
|
+
* at once — a large paste, a document import, bulk layer creation — would
|
|
962
|
+
* reserialize the whole growing queue a hundred times, an O(N²) cost in
|
|
963
|
+
* `model.toJSON()`.
|
|
925
964
|
*
|
|
926
|
-
* @param mutation.capturedChanges - Pre-captured changes
|
|
927
|
-
*
|
|
965
|
+
* @param mutation.capturedChanges - Pre-captured, frozen changes, used to
|
|
966
|
+
* avoid re-reading a model after pool operations that might clear them.
|
|
928
967
|
*/
|
|
929
968
|
queueMutation(mutation) {
|
|
930
969
|
this.pendingMutations.push(mutation);
|
|
@@ -969,10 +1008,22 @@ export class SyncClient extends EventEmitter {
|
|
|
969
1008
|
timestamp: Date.now(),
|
|
970
1009
|
});
|
|
971
1010
|
}
|
|
972
|
-
catch (error) {
|
|
1011
|
+
catch (error) {
|
|
1012
|
+
// Best-effort persistence — the in-memory queue still processes; only
|
|
1013
|
+
// a tab close before reconnect loses these. Forensic → debug.
|
|
1014
|
+
getContext().logger.debug('[SyncClient] Failed to persist offline mutation queue', {
|
|
1015
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
973
1018
|
}
|
|
974
1019
|
/**
|
|
975
|
-
* Restore mutation queue from IndexedDB
|
|
1020
|
+
* Restore the mutation queue from IndexedDB.
|
|
1021
|
+
*
|
|
1022
|
+
* The persisted record was written by an earlier session, possibly by an
|
|
1023
|
+
* older build of the SDK, so each entry is validated as it is replayed:
|
|
1024
|
+
* corrupt entries are dropped and logged at debug level, and a failure is
|
|
1025
|
+
* never swallowed silently, because the survival of offline writes must be
|
|
1026
|
+
* observable.
|
|
976
1027
|
*/
|
|
977
1028
|
async restoreMutationQueue() {
|
|
978
1029
|
if (!this.database || !this.userId)
|
|
@@ -982,19 +1033,39 @@ export class SyncClient extends EventEmitter {
|
|
|
982
1033
|
const queue = stored.find((t) => t.id === 'mutation-queue');
|
|
983
1034
|
if (queue?.mutations) {
|
|
984
1035
|
for (const mutation of queue.mutations) {
|
|
985
|
-
const
|
|
1036
|
+
const parsed = persistedMutationSchema.safeParse(mutation);
|
|
1037
|
+
if (!parsed.success) {
|
|
1038
|
+
getContext().logger.debug('[SyncClient] Dropping malformed persisted mutation', {
|
|
1039
|
+
issues: parsed.error.issues.map((i) => i.path.join('.')).join(', '),
|
|
1040
|
+
});
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
const model = this.objectPool.createFromData(parsed.data.modelData);
|
|
986
1044
|
if (model) {
|
|
987
1045
|
this.pendingMutations.push({
|
|
988
|
-
type:
|
|
1046
|
+
type: parsed.data.type,
|
|
989
1047
|
model,
|
|
990
|
-
timestamp: new Date(
|
|
991
|
-
writeOptions
|
|
1048
|
+
timestamp: new Date(parsed.data.timestamp),
|
|
1049
|
+
...(parsed.data.writeOptions !== undefined
|
|
1050
|
+
? { writeOptions: parsed.data.writeOptions }
|
|
1051
|
+
: {}),
|
|
992
1052
|
});
|
|
993
1053
|
}
|
|
994
1054
|
}
|
|
995
1055
|
}
|
|
996
1056
|
}
|
|
997
|
-
catch (error) {
|
|
1057
|
+
catch (error) {
|
|
1058
|
+
// A restore failure means queued offline writes did NOT rehydrate.
|
|
1059
|
+
// Self-healing is impossible here (the record may be unreadable), but
|
|
1060
|
+
// the failure must be visible for diagnosis instead of silent loss.
|
|
1061
|
+
getContext().logger.debug('[SyncClient] Failed to restore offline mutation queue', {
|
|
1062
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1063
|
+
});
|
|
1064
|
+
getContext().observability.captureTransactionFailure({
|
|
1065
|
+
context: 'restore-mutation-queue',
|
|
1066
|
+
error: error instanceof Error ? error : String(error),
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
998
1069
|
}
|
|
999
1070
|
/**
|
|
1000
1071
|
* Process pending mutations - can be called by SyncedStore when online
|
|
@@ -1035,8 +1106,8 @@ export class SyncClient extends EventEmitter {
|
|
|
1035
1106
|
this.pendingMutations = [];
|
|
1036
1107
|
// Clear persisted queue before processing
|
|
1037
1108
|
await this.persistMutationQueue();
|
|
1038
|
-
//
|
|
1039
|
-
//
|
|
1109
|
+
// Stage every mutation synchronously within the same event-loop tick;
|
|
1110
|
+
// the transaction queue's microtask batches and sends them together.
|
|
1040
1111
|
for (const mutation of mutations) {
|
|
1041
1112
|
// Skip mutations for deleted models (prevents "not found" errors)
|
|
1042
1113
|
if (mutation.type !== 'delete' && !this.objectPool.get(mutation.model.id)) {
|
|
@@ -1055,20 +1126,33 @@ export class SyncClient extends EventEmitter {
|
|
|
1055
1126
|
if (!this.userId || !this.organizationId)
|
|
1056
1127
|
return;
|
|
1057
1128
|
const ctx = { userId: this.userId, organizationId: this.organizationId };
|
|
1129
|
+
// Settlement is delivered via transaction.confirmation, not this promise —
|
|
1130
|
+
// it only rejects when staging itself throws (change extraction, optimistic
|
|
1131
|
+
// apply, store add). That means the write never entered the queue, so
|
|
1132
|
+
// capture it instead of dropping it silently.
|
|
1133
|
+
const captureStagingFailure = (error) => {
|
|
1134
|
+
getContext().observability.captureTransactionFailure({
|
|
1135
|
+
context: `stage-mutation-${mutation.type}`,
|
|
1136
|
+
modelName: mutation.model.getModelName(),
|
|
1137
|
+
modelId: mutation.model.id,
|
|
1138
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
1139
|
+
});
|
|
1140
|
+
};
|
|
1058
1141
|
if (mutation.type === 'update') {
|
|
1059
|
-
this.transactionQueue
|
|
1142
|
+
this.transactionQueue
|
|
1143
|
+
.update(mutation.model, ctx, mutation.capturedChanges, mutation.writeOptions)
|
|
1144
|
+
.catch(captureStagingFailure);
|
|
1060
1145
|
}
|
|
1061
1146
|
else {
|
|
1062
1147
|
const handler = this.transactionQueue[mutation.type].bind(this.transactionQueue);
|
|
1063
|
-
handler(mutation.model, ctx, mutation.writeOptions);
|
|
1148
|
+
handler(mutation.model, ctx, mutation.writeOptions).catch(captureStagingFailure);
|
|
1064
1149
|
}
|
|
1065
1150
|
}
|
|
1066
1151
|
/**
|
|
1067
|
-
* Resolve
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
* even when there are local changes, to maintain data consistency.
|
|
1152
|
+
* Resolve a conflict between the local model and incoming server data,
|
|
1153
|
+
* called while processing deltas from the WebSocket. Certain server states,
|
|
1154
|
+
* such as deletions and deactivations, always take precedence even when the
|
|
1155
|
+
* local model has unsynced changes, so the two sides stay consistent.
|
|
1072
1156
|
*/
|
|
1073
1157
|
resolveConflicts(localModel, serverData) {
|
|
1074
1158
|
const hasLocalChanges = localModel.hasChanges;
|
|
@@ -1078,7 +1162,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1078
1162
|
? localModel.updatedAt.getTime()
|
|
1079
1163
|
: new Date(localModel.updatedAt).getTime()
|
|
1080
1164
|
: 0;
|
|
1081
|
-
const serverUpdatedAt =
|
|
1165
|
+
const serverUpdatedAt = toEpochMs(serverData.updatedAt);
|
|
1082
1166
|
getContext().logger.debug('Conflict resolution', {
|
|
1083
1167
|
modelId: localModel.id,
|
|
1084
1168
|
modelType: localModel.getModelName(),
|
|
@@ -1114,7 +1198,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1114
1198
|
// Merge: server baseline + local dirty fields win
|
|
1115
1199
|
const merged = { ...serverData, ...(localChanges || {}) };
|
|
1116
1200
|
// Preserve the most recent updatedAt without clearing dirty flags
|
|
1117
|
-
if (serverData
|
|
1201
|
+
if (serverData.updatedAt || localModel.updatedAt) {
|
|
1118
1202
|
const mergedUpdatedAt = new Date(Math.max(localUpdatedAt, serverUpdatedAt));
|
|
1119
1203
|
// updateFromData accepts Date or ISO string for dates
|
|
1120
1204
|
merged.updatedAt = mergedUpdatedAt;
|
|
@@ -1133,8 +1217,9 @@ export class SyncClient extends EventEmitter {
|
|
|
1133
1217
|
return localModel;
|
|
1134
1218
|
}
|
|
1135
1219
|
/**
|
|
1136
|
-
* Extract critical state fields from server data
|
|
1137
|
-
*
|
|
1220
|
+
* Extract the critical state fields from server data. These are the states
|
|
1221
|
+
* that must be honored even when the local model has unsynced changes. The
|
|
1222
|
+
* conflict resolver reads exactly these fields and no others.
|
|
1138
1223
|
*/
|
|
1139
1224
|
extractCriticalState(serverData) {
|
|
1140
1225
|
const critical = {};
|
|
@@ -1181,8 +1266,6 @@ export class SyncClient extends EventEmitter {
|
|
|
1181
1266
|
await this.processPendingMutations();
|
|
1182
1267
|
this.setConnectionState('connected');
|
|
1183
1268
|
this.emit('sync:reconnected');
|
|
1184
|
-
// Clear offline timestamp
|
|
1185
|
-
this.offlineSince = undefined;
|
|
1186
1269
|
}
|
|
1187
1270
|
catch (error) {
|
|
1188
1271
|
getContext().observability.captureTransactionFailure({
|
|
@@ -1198,7 +1281,6 @@ export class SyncClient extends EventEmitter {
|
|
|
1198
1281
|
async handleDisconnection() {
|
|
1199
1282
|
getContext().observability.breadcrumb('Network disconnected', 'sync.offline');
|
|
1200
1283
|
this.setConnectionState('disconnected');
|
|
1201
|
-
this.offlineSince = new Date();
|
|
1202
1284
|
this.emit('sync:offline');
|
|
1203
1285
|
}
|
|
1204
1286
|
/**
|
|
@@ -1295,9 +1377,10 @@ export class SyncClient extends EventEmitter {
|
|
|
1295
1377
|
this.removeAllListeners();
|
|
1296
1378
|
}
|
|
1297
1379
|
/**
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
1300
|
-
*
|
|
1380
|
+
* Notify the {@link TransactionQueue} of an incoming delta so it can confirm
|
|
1381
|
+
* transactions by sync-id threshold. A transaction is confirmed once any
|
|
1382
|
+
* delta with an id at or beyond its `lastSyncId` threshold arrives.
|
|
1383
|
+
* @param syncId - The sync id of the received delta.
|
|
1301
1384
|
*/
|
|
1302
1385
|
onDeltaReceived(syncId) {
|
|
1303
1386
|
try {
|
|
@@ -1310,22 +1393,21 @@ export class SyncClient extends EventEmitter {
|
|
|
1310
1393
|
}
|
|
1311
1394
|
}
|
|
1312
1395
|
/**
|
|
1313
|
-
*
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1316
|
-
* Cancels pending transactions for children that reference the deleted parent.
|
|
1396
|
+
* Cancel pending transactions for child entities orphaned by a parent's
|
|
1397
|
+
* deletion. The store calls this when a delete delta arrives for a parent,
|
|
1398
|
+
* cancelling any queued writes on children that reference it.
|
|
1317
1399
|
*
|
|
1318
|
-
* @param childModelName - The child model type (
|
|
1319
|
-
* @param foreignKey - The
|
|
1320
|
-
* @param parentId - The deleted parent
|
|
1321
|
-
* @returns
|
|
1400
|
+
* @param childModelName - The child model type (for example, `SlideLayer`).
|
|
1401
|
+
* @param foreignKey - The foreign-key property name (for example, `slideId`).
|
|
1402
|
+
* @param parentId - The id of the deleted parent.
|
|
1403
|
+
* @returns The number of transactions cancelled.
|
|
1322
1404
|
*/
|
|
1323
1405
|
cancelTransactionsByForeignKey(childModelName, foreignKey, parentId) {
|
|
1324
1406
|
return this.transactionQueue.cancelTransactionsByForeignKey(childModelName, foreignKey, parentId);
|
|
1325
1407
|
}
|
|
1326
1408
|
/**
|
|
1327
|
-
* Wait for a transaction to be confirmed
|
|
1328
|
-
*
|
|
1409
|
+
* Wait for a transaction to be confirmed by its delta echo. Delegates to the
|
|
1410
|
+
* {@link TransactionQueue}, which handles the confirmation timeout.
|
|
1329
1411
|
*/
|
|
1330
1412
|
waitForDeltaConfirmation(transactionId) {
|
|
1331
1413
|
return this.transactionQueue.waitForConfirmation(transactionId);
|
|
@@ -1339,7 +1421,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1339
1421
|
/**
|
|
1340
1422
|
* Get sync statistics. Return type is inferred from the literal so
|
|
1341
1423
|
* the call site sees the actual shape — `connectionState` narrowed
|
|
1342
|
-
* to its three states, `objectPoolStats` typed by `
|
|
1424
|
+
* to its three states, `objectPoolStats` typed by `InstanceCache.getStats`.
|
|
1343
1425
|
*/
|
|
1344
1426
|
getSyncStats() {
|
|
1345
1427
|
return {
|
|
@@ -1374,25 +1456,26 @@ export class SyncClient extends EventEmitter {
|
|
|
1374
1456
|
* can render typed UI (toast keyed by `AbloError.type`, route-level
|
|
1375
1457
|
* "this entity reverted" boundaries, telemetry).
|
|
1376
1458
|
*
|
|
1377
|
-
* Distinct from `onTransactionEvent('failed', cb)`, which
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1459
|
+
* Distinct from `onTransactionEvent('failed', cb)`, which serves the
|
|
1460
|
+
* parameterless `pendingChanges` counter and intentionally drops the
|
|
1461
|
+
* payload. The two coexist: the counter callback stays lightweight, while
|
|
1462
|
+
* this typed listener drives user-visible surfaces.
|
|
1381
1463
|
*/
|
|
1382
1464
|
onMutationFailure(listener) {
|
|
1383
1465
|
this.transactionQueue.on('transaction:failed', listener);
|
|
1384
1466
|
return () => this.transactionQueue.off('transaction:failed', listener);
|
|
1385
1467
|
}
|
|
1386
1468
|
/**
|
|
1387
|
-
* Subscribe to
|
|
1469
|
+
* Subscribe to local transaction creation with the full {@link Transaction}
|
|
1388
1470
|
* payload (`type`, `modelName`, `modelId`, `data`, `previousData`). This is
|
|
1389
|
-
* the feed
|
|
1471
|
+
* the feed the store's local-mutation subscription taps for undo recording.
|
|
1390
1472
|
*
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
* (reached
|
|
1394
|
-
* `subscribe('transaction:created')`
|
|
1395
|
-
*
|
|
1473
|
+
* It subscribes to the {@link TransactionQueue}'s emitter directly, since
|
|
1474
|
+
* that is the only emitter that fires `transaction:created`. The SyncClient's
|
|
1475
|
+
* own emitter (reached through {@link subscribe}) never rebroadcasts that
|
|
1476
|
+
* event, so routing undo through `subscribe('transaction:created')` would
|
|
1477
|
+
* record nothing. {@link onMutationFailure} taps the queue for the same
|
|
1478
|
+
* reason.
|
|
1396
1479
|
*/
|
|
1397
1480
|
onLocalTransaction(listener) {
|
|
1398
1481
|
this.transactionQueue.on('transaction:created', listener);
|
|
@@ -1488,11 +1571,11 @@ export class SyncClient extends EventEmitter {
|
|
|
1488
1571
|
assigneeId,
|
|
1489
1572
|
});
|
|
1490
1573
|
}
|
|
1491
|
-
// ── Delta + Bootstrap application (owns
|
|
1574
|
+
// ── Delta + Bootstrap application (owns InstanceCache writes) ──────────────
|
|
1492
1575
|
/**
|
|
1493
|
-
* Apply a batch of delta results from Database to the
|
|
1576
|
+
* Apply a batch of delta results from Database to the InstanceCache.
|
|
1494
1577
|
* Owns: model creation, upsert, remove, archive, conflict resolution.
|
|
1495
|
-
* Returns: nothing —
|
|
1578
|
+
* Returns: nothing — InstanceCache is updated in place.
|
|
1496
1579
|
*/
|
|
1497
1580
|
/**
|
|
1498
1581
|
* Mark a local transaction as optimistically applied. The matching
|
|
@@ -1514,12 +1597,12 @@ export class SyncClient extends EventEmitter {
|
|
|
1514
1597
|
return this.echoTracker.getMetrics();
|
|
1515
1598
|
}
|
|
1516
1599
|
/**
|
|
1517
|
-
* Package-internal accessor for the TransactionQueue. Used by
|
|
1518
|
-
* `Ablo.commits.create()` to route raw multi-
|
|
1519
|
-
* same retry-on-reconnect lane as the
|
|
1520
|
-
*
|
|
1521
|
-
* instance the SyncClient subscribes to.
|
|
1522
|
-
* consumers
|
|
1600
|
+
* Package-internal accessor for the {@link TransactionQueue}. Used by
|
|
1601
|
+
* `Ablo.commits.create()` to route raw multi-operation envelopes through the
|
|
1602
|
+
* same retry-on-reconnect lane as the model proxy path, and by tests to
|
|
1603
|
+
* exercise the queue's interaction with {@link markTransactionPending} on the
|
|
1604
|
+
* real instance the SyncClient subscribes to. It is not re-exported to SDK
|
|
1605
|
+
* consumers; `Ablo` is the public surface.
|
|
1523
1606
|
*/
|
|
1524
1607
|
getTransactionQueue() {
|
|
1525
1608
|
return this.transactionQueue;
|
|
@@ -1547,16 +1630,14 @@ export class SyncClient extends EventEmitter {
|
|
|
1547
1630
|
}
|
|
1548
1631
|
for (const result of dbResults) {
|
|
1549
1632
|
const { modelName, modelId, action, transactionId } = result;
|
|
1550
|
-
//
|
|
1551
|
-
//
|
|
1552
|
-
//
|
|
1553
|
-
//
|
|
1554
|
-
//
|
|
1555
|
-
// the
|
|
1556
|
-
//
|
|
1557
|
-
//
|
|
1558
|
-
// for the ~2s window before the matching DELETE confirmation
|
|
1559
|
-
// lands. See `OPTIMISTIC_RECONCILIATION.md` for the framing.
|
|
1633
|
+
// Echo detection: if this delta carries a transaction id that matches
|
|
1634
|
+
// one already applied optimistically, the pool already reflects the
|
|
1635
|
+
// mutation, so the pool operation is skipped. The IndexedDB write in
|
|
1636
|
+
// Database.processDeltaBatch still runs; only the in-memory pool update
|
|
1637
|
+
// is suppressed. This prevents a resurrection flicker: a server-confirmed
|
|
1638
|
+
// create arriving after the user has optimistically deleted the row would
|
|
1639
|
+
// otherwise re-add it for the brief window before the matching delete
|
|
1640
|
+
// confirmation lands.
|
|
1560
1641
|
if (this.echoTracker.consumeEcho(transactionId)) {
|
|
1561
1642
|
continue;
|
|
1562
1643
|
}
|
|
@@ -1622,17 +1703,15 @@ export class SyncClient extends EventEmitter {
|
|
|
1622
1703
|
break;
|
|
1623
1704
|
}
|
|
1624
1705
|
}
|
|
1625
|
-
// Reveal the whole frame in
|
|
1626
|
-
// `removeBatch
|
|
1627
|
-
// so calling them
|
|
1628
|
-
// boundary — a catch-up frame that adds
|
|
1629
|
-
// every dependent reaction
|
|
1630
|
-
//
|
|
1631
|
-
//
|
|
1632
|
-
//
|
|
1633
|
-
//
|
|
1634
|
-
// equivalent of Replicache's "atomically reveal the new state" — the
|
|
1635
|
-
// app never observes a partially-applied frame.
|
|
1706
|
+
// Reveal the whole frame in a single MobX action. `addBatch`,
|
|
1707
|
+
// `upsertBatch`, `removeBatch`, and `updateScope` are each individually
|
|
1708
|
+
// wrapped in an action, so calling them in sequence flushes reactions at
|
|
1709
|
+
// every action boundary — a catch-up frame that adds, updates, and removes
|
|
1710
|
+
// would fire every dependent reaction several times in a row, re-rendering
|
|
1711
|
+
// and re-sorting on each. Wrapping them in one outer `runInAction` defers
|
|
1712
|
+
// all reaction flushes to a single boundary, so dependents recompute
|
|
1713
|
+
// exactly once regardless of how many models or operation kinds the frame
|
|
1714
|
+
// touched. The app therefore never observes a partially applied frame.
|
|
1636
1715
|
runInAction(() => {
|
|
1637
1716
|
if (modelsToAdd.length > 0)
|
|
1638
1717
|
this.objectPool.addBatch(modelsToAdd, ModelScope.live);
|
|
@@ -1651,7 +1730,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1651
1730
|
});
|
|
1652
1731
|
}
|
|
1653
1732
|
/**
|
|
1654
|
-
* Apply bootstrap data to the
|
|
1733
|
+
* Apply bootstrap data to the InstanceCache with ghost removal.
|
|
1655
1734
|
* Owns: model creation, batch upsert, ghost detection + removal.
|
|
1656
1735
|
*/
|
|
1657
1736
|
applyBootstrapDataToPool(bootstrapData, protectedIds, options) {
|
|
@@ -1689,12 +1768,12 @@ export class SyncClient extends EventEmitter {
|
|
|
1689
1768
|
const recordId = data.id;
|
|
1690
1769
|
if (recordId)
|
|
1691
1770
|
idsForType.add(recordId);
|
|
1692
|
-
// Scoped backfill
|
|
1693
|
-
// a server watermark. If a concurrent live delta already
|
|
1694
|
-
// row past the snapshot, skip it
|
|
1695
|
-
// model
|
|
1696
|
-
// version guard
|
|
1697
|
-
// late, the row would already be clobbered.
|
|
1771
|
+
// Scoped backfill for the hydrate-on-enter path: a subset snapshot is
|
|
1772
|
+
// taken at a server watermark. If a concurrent live delta already
|
|
1773
|
+
// advanced this row past the snapshot, skip it. `createFromData`
|
|
1774
|
+
// mutates the pooled model in place to keep instances alive, so this
|
|
1775
|
+
// version guard has to run before it; a guard at the upsert layer would
|
|
1776
|
+
// be too late, because the row would already be clobbered.
|
|
1698
1777
|
if (options?.scoped && recordId) {
|
|
1699
1778
|
const existing = this.objectPool.get(recordId);
|
|
1700
1779
|
if (existing && !rawRecordIsNewer(data, existing)) {
|
|
@@ -1718,10 +1797,10 @@ export class SyncClient extends EventEmitter {
|
|
|
1718
1797
|
this.objectPool.upsertBatch(allModels, ModelScope.live);
|
|
1719
1798
|
const addedCount = this.objectPool.size - beforeSize;
|
|
1720
1799
|
const updatedCount = allModels.length - addedCount;
|
|
1721
|
-
// Ghost removal
|
|
1722
|
-
// valid for a
|
|
1723
|
-
// returned type. A
|
|
1724
|
-
// type that belong to other
|
|
1800
|
+
// Ghost removal: drop pool entities absent from the server snapshot. This
|
|
1801
|
+
// is valid only for a full bootstrap, where the snapshot is authoritative
|
|
1802
|
+
// for each returned type. A scoped subset snapshot must not remove rows of
|
|
1803
|
+
// the same type that belong to other, unhydrated groups.
|
|
1725
1804
|
let removedCount = 0;
|
|
1726
1805
|
if (!options?.scoped) {
|
|
1727
1806
|
const ghostIds = [];
|