@abloatai/ablo 0.34.1 → 0.36.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 +2 -1
- package/CHANGELOG.md +758 -5
- package/README.md +56 -502
- package/bin/ablo.cjs +39 -0
- package/dist/BaseSyncedStore.d.ts +176 -48
- package/dist/BaseSyncedStore.js +346 -214
- package/dist/Database.d.ts +17 -44
- package/dist/Database.js +96 -79
- package/dist/InstanceCache.d.ts +31 -6
- package/dist/InstanceCache.js +65 -30
- package/dist/LazyReferenceCollection.d.ts +3 -3
- package/dist/LazyReferenceCollection.js +4 -4
- package/dist/Model.d.ts +23 -13
- package/dist/Model.js +27 -17
- package/dist/ModelRegistry.d.ts +8 -4
- package/dist/ModelRegistry.js +20 -18
- package/dist/NetworkMonitor.d.ts +3 -1
- package/dist/NetworkMonitor.js +7 -5
- package/dist/{SyncEngineContext.d.ts → RuntimeContext.d.ts} +20 -13
- package/dist/{SyncEngineContext.js → RuntimeContext.js} +11 -12
- package/dist/SyncClient.d.ts +47 -47
- package/dist/SyncClient.js +215 -156
- package/dist/ai-sdk/coordinatedTool.d.ts +2 -2
- package/dist/ai-sdk/coordinatedTool.js +1 -1
- package/dist/ai-sdk/coordinationContext.d.ts +2 -2
- package/dist/ai-sdk/coordinationContext.js +1 -1
- package/dist/ai-sdk/wrap.d.ts +3 -3
- package/dist/ai-sdk/wrap.js +2 -2
- package/dist/auth/index.d.ts +1 -156
- package/dist/auth/index.js +8 -301
- package/dist/client/Ablo.d.ts +42 -287
- package/dist/client/Ablo.js +129 -963
- package/dist/client/abloClient.d.ts +309 -0
- package/dist/client/abloClient.js +13 -0
- package/dist/client/clientPrelude.d.ts +52 -0
- package/dist/client/clientPrelude.js +60 -0
- package/dist/client/consoleLogger.d.ts +2 -2
- package/dist/client/coreClient.d.ts +60 -0
- package/dist/client/coreClient.js +118 -0
- package/dist/client/createInternalComponents.d.ts +8 -4
- package/dist/client/createInternalComponents.js +17 -10
- package/dist/client/createModelProxy.d.ts +98 -373
- package/dist/client/createModelProxy.js +233 -139
- package/dist/client/humans.d.ts +69 -0
- package/dist/client/humans.js +78 -0
- package/dist/client/modelRegistration.d.ts +1 -1
- package/dist/client/modelRegistration.js +9 -9
- package/dist/client/options.d.ts +73 -17
- package/dist/client/reactiveEngine.d.ts +53 -0
- package/dist/client/reactiveEngine.js +688 -0
- package/dist/client/resourceTypes.d.ts +9 -250
- package/dist/client/resourceTypes.js +8 -5
- package/dist/client/schemaConfig.d.ts +4 -4
- package/dist/client/schemaConfig.js +6 -2
- package/dist/client/storeCluster.d.ts +47 -0
- package/dist/client/storeCluster.js +118 -0
- package/dist/client/storeLifecycle.d.ts +61 -0
- package/dist/client/storeLifecycle.js +231 -0
- package/dist/client/validateAbloOptions.d.ts +3 -2
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/client/wsMutationExecutor.d.ts +3 -3
- package/dist/client/wsMutationExecutor.js +3 -3
- package/dist/context.d.ts +22 -9
- package/dist/context.js +33 -9
- package/dist/coordination/ClaimLog.d.ts +26 -0
- package/dist/coordination/ClaimLog.js +32 -0
- package/dist/coordination/index.d.ts +1 -15
- package/dist/coordination/index.js +8 -31
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +2 -2
- package/dist/docs/catalog.d.ts +72 -0
- package/dist/docs/catalog.js +230 -0
- package/dist/docs/index.d.ts +10 -0
- package/dist/docs/index.js +10 -0
- package/dist/environment.d.ts +1 -40
- package/dist/environment.js +8 -37
- package/dist/index.d.ts +44 -36
- package/dist/index.js +30 -22
- package/dist/interfaces/index.d.ts +44 -134
- package/dist/keys/index.d.ts +1 -77
- package/dist/keys/index.js +8 -190
- package/dist/mutators/{RecordingTransaction.d.ts → RecordingMutation.d.ts} +4 -4
- package/dist/mutators/{RecordingTransaction.js → RecordingMutation.js} +2 -2
- package/dist/mutators/Transaction.d.ts +1 -1
- package/dist/mutators/Transaction.js +1 -1
- package/dist/mutators/UndoManager.d.ts +6 -6
- package/dist/mutators/UndoManager.js +5 -5
- package/dist/mutators/defineMutators.d.ts +3 -3
- package/dist/mutators/defineMutators.js +1 -1
- package/dist/mutators/inverseOp.js +2 -2
- package/dist/mutators/mutateActions.d.ts +3 -3
- package/dist/mutators/mutateActions.js +1 -1
- package/dist/mutators/readerActions.d.ts +1 -1
- package/dist/mutators/undoApply.d.ts +1 -1
- package/dist/mutators/undoApply.js +1 -1
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +1 -1
- package/dist/query/client.d.ts +5 -2
- package/dist/query/client.js +10 -9
- package/dist/query/types.d.ts +6 -41
- package/dist/query/types.js +2 -2
- package/dist/react/AbloProvider.d.ts +18 -8
- package/dist/react/AbloProvider.js +10 -9
- package/dist/react/context.d.ts +3 -3
- package/dist/react/context.js +1 -1
- package/dist/react/createAbloReact.d.ts +56 -0
- package/dist/react/createAbloReact.js +51 -0
- package/dist/react/index.d.ts +6 -5
- package/dist/react/index.js +6 -3
- package/dist/react/internalContext.d.ts +1 -1
- package/dist/react/useAblo.d.ts +12 -5
- package/dist/react/useAblo.js +26 -8
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.d.ts +2 -2
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useMutators.d.ts +3 -3
- package/dist/react/useMutators.js +3 -3
- package/dist/react/useUndoScope.d.ts +5 -5
- package/dist/react/useUndoScope.js +1 -1
- package/dist/schema/coordination.d.ts +69 -10
- package/dist/schema/coordination.js +90 -9
- package/dist/schema/ddl.js +2 -2
- package/dist/schema/diff.d.ts +1 -1
- package/dist/schema/generate.js +1 -1
- package/dist/schema/index.d.ts +11 -10
- package/dist/schema/index.js +22 -18
- package/dist/schema/queries.d.ts +27 -27
- package/dist/schema/queries.js +23 -23
- package/dist/schema/select.d.ts +3 -3
- package/dist/schema/select.js +6 -3
- package/dist/schema/serialize.d.ts +15 -6
- package/dist/schema/serialize.js +20 -3
- package/dist/schema/sugar.d.ts +6 -7
- package/dist/schema/sugar.js +9 -12
- package/dist/schema/syncDeltaRow.d.ts +4 -152
- package/dist/schema/syncDeltaRow.js +4 -105
- package/dist/server/adapter.d.ts +18 -1
- package/dist/server/commit.d.ts +10 -16
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/readConfig.d.ts +1 -1
- package/dist/source/adapter.d.ts +7 -5
- package/dist/source/adapter.js +7 -5
- package/dist/source/adapters/drizzle.d.ts +1 -1
- package/dist/source/adapters/drizzle.js +2 -2
- package/dist/source/adapters/kysely.d.ts +1 -1
- package/dist/source/adapters/kysely.js +1 -1
- package/dist/source/adapters/kyselyMutationCore.d.ts +1 -1
- package/dist/source/adapters/kyselyMutationCore.js +2 -2
- package/dist/source/adapters/memory.js +1 -1
- package/dist/source/adapters/prisma.d.ts +8 -3
- package/dist/source/adapters/prisma.js +1 -1
- package/dist/source/connector.js +1 -1
- package/dist/source/connectorProtocol.d.ts +2 -8
- package/dist/source/connectorProtocol.js +3 -2
- package/dist/source/contract.d.ts +29 -17
- package/dist/source/contract.js +27 -22
- package/dist/source/factory.d.ts +1 -1
- package/dist/source/idempotency.js +2 -2
- package/dist/source/index.d.ts +1 -0
- package/dist/source/index.js +3 -0
- package/dist/source/next.d.ts +1 -1
- package/dist/source/signing.d.ts +9 -2
- package/dist/source/signing.js +4 -1
- package/dist/source/types.d.ts +6 -4
- package/dist/source/types.js +1 -1
- package/dist/{core/storeContract.d.ts → storeContract.d.ts} +6 -6
- package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
- package/dist/{core → stores}/DatabaseManager.js +14 -13
- package/dist/stores/ObjectStore.d.ts +1 -1
- package/dist/{core → stores}/StoreManager.d.ts +9 -26
- package/dist/{core → stores}/StoreManager.js +29 -77
- package/dist/stores/SyncActionStore.d.ts +4 -2
- package/dist/stores/SyncActionStore.js +11 -17
- package/dist/stores/syncAction.d.ts +26 -0
- package/dist/stores/syncAction.js +16 -0
- package/dist/surface.d.ts +3 -3
- package/dist/surface.js +6 -4
- package/dist/sync/BootstrapFetcher.d.ts +127 -6
- package/dist/sync/BootstrapFetcher.js +511 -83
- package/dist/sync/ConnectionManager.d.ts +6 -198
- package/dist/sync/ConnectionManager.js +6 -677
- package/dist/sync/OnDemandLoader.d.ts +5 -2
- package/dist/sync/OnDemandLoader.js +61 -21
- package/dist/sync/SubscriptionManager.d.ts +13 -2
- package/dist/sync/SubscriptionManager.js +23 -5
- package/dist/sync/SyncWebSocket.d.ts +27 -510
- package/dist/sync/SyncWebSocket.js +76 -954
- package/dist/sync/awaitClaimGrant.d.ts +4 -44
- package/dist/sync/awaitClaimGrant.js +4 -109
- package/dist/sync/bootstrapApply.d.ts +3 -0
- package/dist/sync/bootstrapApply.js +2 -2
- package/dist/sync/commitFrames.d.ts +6 -40
- package/dist/sync/commitFrames.js +6 -97
- package/dist/sync/contextPorts.d.ts +18 -0
- package/dist/sync/contextPorts.js +31 -0
- package/dist/sync/createClaimStream.d.ts +5 -49
- package/dist/sync/createClaimStream.js +5 -469
- package/dist/sync/createPresenceStream.d.ts +26 -4
- package/dist/sync/createPresenceStream.js +28 -20
- package/dist/sync/createSnapshot.d.ts +2 -2
- package/dist/sync/createSnapshot.js +1 -1
- package/dist/sync/credentialLifecycle.d.ts +5 -173
- package/dist/sync/credentialLifecycle.js +5 -320
- package/dist/sync/deltaPipeline.d.ts +13 -12
- package/dist/sync/deltaPipeline.js +21 -4
- package/dist/sync/groupChange.d.ts +3 -0
- package/dist/sync/groupChange.js +16 -14
- package/dist/sync/participants.d.ts +24 -6
- package/dist/sync/participants.js +32 -23
- package/dist/sync/schemaDrift.d.ts +55 -0
- package/dist/sync/schemaDrift.js +53 -0
- package/dist/sync/schemas.d.ts +23 -33
- package/dist/sync/schemas.js +29 -20
- package/dist/sync/syncPlan.d.ts +3 -3
- package/dist/sync/wsFrameHandlers.d.ts +6 -114
- package/dist/sync/wsFrameHandlers.js +6 -392
- package/dist/syncLog/contract.d.ts +20 -0
- package/dist/syncLog/contract.js +19 -0
- package/dist/syncLog/index.d.ts +1 -0
- package/dist/syncLog/index.js +1 -0
- package/dist/transaction/ablo.d.ts +88 -0
- package/dist/transaction/ablo.js +33 -0
- package/dist/{client/auth.d.ts → transaction/auth/apiKey.d.ts} +43 -8
- package/dist/{client/auth.js → transaction/auth/apiKey.js} +19 -0
- package/dist/transaction/auth/bootstrapScope.d.ts +15 -0
- package/dist/transaction/auth/bootstrapScope.js +1 -0
- package/dist/transaction/auth/capability.d.ts +212 -0
- package/dist/transaction/auth/capability.js +224 -0
- package/dist/{auth → transaction/auth}/credentialSource.d.ts +8 -1
- package/dist/{client → transaction/auth}/identity.d.ts +8 -7
- package/dist/{client → transaction/auth}/identity.js +1 -1
- package/dist/transaction/auth/index.d.ts +162 -0
- package/dist/transaction/auth/index.js +304 -0
- package/dist/{auth → transaction/auth}/schemas.d.ts +1 -1
- package/dist/{auth → transaction/auth}/schemas.js +13 -13
- package/dist/{client → transaction/auth}/sessionMint.d.ts +8 -8
- package/dist/{client → transaction/auth}/sessionMint.js +4 -7
- package/dist/transaction/coordination/awaitClaimGrant.d.ts +56 -0
- package/dist/transaction/coordination/awaitClaimGrant.js +124 -0
- package/dist/{client → transaction/coordination}/claimHeartbeatLoop.d.ts +34 -0
- package/dist/{client → transaction/coordination}/claimHeartbeatLoop.js +20 -0
- package/dist/transaction/coordination/claimMeta.d.ts +49 -0
- package/dist/transaction/coordination/claimMeta.js +52 -0
- package/dist/transaction/coordination/createClaimStream.d.ts +64 -0
- package/dist/transaction/coordination/createClaimStream.js +475 -0
- package/dist/transaction/coordination/events.d.ts +74 -0
- package/dist/transaction/coordination/events.js +7 -0
- package/dist/transaction/coordination/index.d.ts +19 -0
- package/dist/transaction/coordination/index.js +45 -0
- package/dist/transaction/coordination/locator.d.ts +104 -0
- package/dist/transaction/coordination/locator.js +102 -0
- package/dist/transaction/coordination/schema.d.ts +1536 -0
- package/dist/transaction/coordination/schema.js +1177 -0
- package/dist/transaction/coordination/targetConflict.d.ts +2 -0
- package/dist/transaction/coordination/targetConflict.js +107 -0
- package/dist/{coordination → transaction/coordination}/trace.d.ts +7 -18
- package/dist/{coordination → transaction/coordination}/trace.js +18 -25
- package/dist/transaction/durableWrites.d.ts +62 -0
- package/dist/{client → transaction}/durableWrites.js +28 -3
- package/dist/transaction/environment.d.ts +105 -0
- package/dist/transaction/environment.js +108 -0
- package/dist/{errorCodes.d.ts → transaction/errorCodes.d.ts} +12 -12
- package/dist/{errorCodes.js → transaction/errorCodes.js} +45 -18
- package/dist/{errors.d.ts → transaction/errors.d.ts} +37 -13
- package/dist/{errors.js → transaction/errors.js} +85 -16
- package/dist/transaction/footprint.d.ts +111 -0
- package/dist/transaction/footprint.js +0 -0
- package/dist/transaction/index.d.ts +20 -0
- package/dist/transaction/index.js +20 -0
- package/dist/transaction/keys/index.d.ts +87 -0
- package/dist/transaction/keys/index.js +207 -0
- package/dist/transaction/log/syncDeltaRow.d.ts +158 -0
- package/dist/transaction/log/syncDeltaRow.js +95 -0
- package/dist/{sync/syncPosition.d.ts → transaction/logPosition.d.ts} +22 -8
- package/dist/{sync/syncPosition.js → transaction/logPosition.js} +15 -6
- package/dist/transaction/logger.d.ts +16 -0
- package/dist/transaction/logger.js +7 -0
- package/dist/transaction/observability.d.ts +53 -0
- package/dist/transaction/observability.js +19 -0
- package/dist/transaction/plugin.d.ts +285 -0
- package/dist/transaction/plugin.js +106 -0
- package/dist/{policy → transaction/policy}/types.d.ts +3 -3
- package/dist/{policy → transaction/policy}/types.js +2 -0
- package/dist/transaction/resources/httpResources.d.ts +321 -0
- package/dist/transaction/resources/httpResources.js +7 -0
- package/dist/transaction/resources/modelOperations.d.ts +427 -0
- package/dist/transaction/resources/modelOperations.js +12 -0
- package/dist/transaction/resources/mutationOptions.d.ts +66 -0
- package/dist/transaction/resources/mutationOptions.js +9 -0
- package/dist/transaction/resources/where.d.ts +101 -0
- package/dist/transaction/resources/where.js +115 -0
- package/dist/{client → transaction/resources}/writeOptionsSchema.d.ts +2 -6
- package/dist/{client → transaction/resources}/writeOptionsSchema.js +9 -5
- package/dist/{schema → transaction/schema}/field.d.ts +17 -23
- package/dist/{schema → transaction/schema}/field.js +5 -5
- package/dist/transaction/schema/fieldRef.d.ts +38 -0
- package/dist/transaction/schema/fieldRef.js +11 -0
- package/dist/transaction/schema/loadStrategy.d.ts +45 -0
- package/dist/transaction/schema/loadStrategy.js +46 -0
- package/dist/{schema → transaction/schema}/model.d.ts +50 -35
- package/dist/{schema → transaction/schema}/model.js +30 -20
- package/dist/transaction/schema/openapi.d.ts +58 -0
- package/dist/transaction/schema/openapi.js +501 -0
- package/dist/{schema → transaction/schema}/relation.d.ts +21 -16
- package/dist/{schema → transaction/schema}/relation.js +7 -7
- package/dist/{schema → transaction/schema}/residency.d.ts +0 -9
- package/dist/{schema → transaction/schema}/residency.js +0 -5
- package/dist/{schema → transaction/schema}/roles.d.ts +5 -5
- package/dist/{schema → transaction/schema}/roles.js +5 -5
- package/dist/{schema → transaction/schema}/schema.d.ts +39 -10
- package/dist/{schema → transaction/schema}/schema.js +24 -3
- package/dist/{schema → transaction/schema}/tenancy.d.ts +1 -1
- package/dist/{schema → transaction/schema}/tenancy.js +7 -4
- package/dist/transaction/transactionLayer.d.ts +82 -0
- package/dist/transaction/transactionLayer.js +24 -0
- package/dist/{transactions → transaction/transactions/settlement}/commitEnvelope.d.ts +5 -6
- package/dist/{transactions → transaction/transactions/settlement}/commitEnvelope.js +9 -13
- package/dist/{transactions → transaction/transactions/settlement}/httpCommitEnvelope.d.ts +9 -2
- package/dist/{transactions → transaction/transactions/settlement}/httpCommitEnvelope.js +5 -9
- package/dist/{transactions/durableWriteStore.d.ts → transaction/transactions/settlement/pendingWrite.d.ts} +11 -37
- package/dist/transaction/transactions/settlement/pendingWrite.js +20 -0
- package/dist/transaction/transport/commitFrames.d.ts +90 -0
- package/dist/transaction/transport/commitFrames.js +134 -0
- package/dist/transaction/transport/connectionManager.d.ts +215 -0
- package/dist/transaction/transport/connectionManager.js +673 -0
- package/dist/transaction/transport/credentialLifecycle.d.ts +177 -0
- package/dist/transaction/transport/credentialLifecycle.js +324 -0
- package/dist/{sync → transaction/transport}/heartbeat.d.ts +3 -1
- package/dist/{sync → transaction/transport}/heartbeat.js +6 -4
- package/dist/transaction/transport/httpClient.d.ts +131 -0
- package/dist/{client → transaction/transport}/httpClient.js +6 -5
- package/dist/transaction/transport/httpOptions.d.ts +33 -0
- package/dist/transaction/transport/httpOptions.js +12 -0
- package/dist/{client → transaction/transport}/httpTransport.js +295 -97
- package/dist/{sync/NetworkProbe.d.ts → transaction/transport/networkProbe.d.ts} +7 -4
- package/dist/{sync/NetworkProbe.js → transaction/transport/networkProbe.js} +14 -13
- package/dist/transaction/transport/wsFrameHandlers.d.ts +128 -0
- package/dist/transaction/transport/wsFrameHandlers.js +429 -0
- package/dist/transaction/transport/wsTransport.d.ts +574 -0
- package/dist/transaction/transport/wsTransport.js +1023 -0
- package/dist/transaction/types/assertExact.d.ts +17 -0
- package/dist/transaction/types/assertExact.js +1 -0
- package/dist/{types → transaction/types}/global.d.ts +17 -2
- package/dist/{types → transaction/types}/global.js +2 -1
- package/dist/{types → transaction/types}/index.d.ts +14 -46
- package/dist/{types → transaction/types}/index.js +7 -16
- package/dist/{types → transaction/types}/streams.d.ts +73 -45
- package/dist/transaction/utils/duration.d.ts +50 -0
- package/dist/{utils → transaction/utils}/duration.js +32 -0
- package/dist/{utils → transaction/utils}/json.d.ts +18 -0
- package/dist/transaction/utils/json.js +276 -0
- package/dist/transaction/wire/accountResponses.d.ts +420 -0
- package/dist/transaction/wire/accountResponses.js +290 -0
- package/dist/transaction/wire/auth.d.ts +56 -0
- package/dist/transaction/wire/auth.js +63 -0
- package/dist/transaction/wire/claimEvent.d.ts +76 -0
- package/dist/transaction/wire/claimEvent.js +73 -0
- package/dist/transaction/wire/claims.d.ts +530 -0
- package/dist/transaction/wire/claims.js +327 -0
- package/dist/{wire → transaction/wire}/commit.d.ts +125 -193
- package/dist/{wire → transaction/wire}/commit.js +68 -47
- package/dist/{wire → transaction/wire}/delta.d.ts +66 -17
- package/dist/{wire → transaction/wire}/delta.js +37 -13
- package/dist/transaction/wire/errorEnvelope.d.ts +72 -0
- package/dist/{wire → transaction/wire}/errorEnvelope.js +36 -5
- package/dist/transaction/wire/feedCursor.d.ts +60 -0
- package/dist/transaction/wire/feedCursor.js +82 -0
- package/dist/transaction/wire/feedEvent.d.ts +204 -0
- package/dist/transaction/wire/feedEvent.js +65 -0
- package/dist/transaction/wire/frames.d.ts +194 -0
- package/dist/transaction/wire/frames.js +50 -0
- package/dist/transaction/wire/inboundFrames.d.ts +562 -0
- package/dist/transaction/wire/inboundFrames.js +116 -0
- package/dist/transaction/wire/index.d.ts +54 -0
- package/dist/transaction/wire/index.js +83 -0
- package/dist/{wire → transaction/wire}/listEnvelope.d.ts +13 -14
- package/dist/transaction/wire/listEnvelope.js +42 -0
- package/dist/transaction/wire/modelMutations.d.ts +31 -0
- package/dist/transaction/wire/modelMutations.js +52 -0
- package/dist/transaction/wire/modelResponses.d.ts +85 -0
- package/dist/transaction/wire/modelResponses.js +43 -0
- package/dist/transaction/wire/modelShape.d.ts +78 -0
- package/dist/transaction/wire/modelShape.js +74 -0
- package/dist/transactions/{TransactionQueue.d.ts → mutations/MutationQueue.d.ts} +85 -38
- package/dist/transactions/{TransactionQueue.js → mutations/MutationQueue.js} +141 -80
- package/dist/transactions/{TransactionStore.d.ts → mutations/MutationStore.d.ts} +8 -8
- package/dist/transactions/{TransactionStore.js → mutations/MutationStore.js} +2 -2
- package/dist/transactions/{coalesceRules.d.ts → mutations/coalesceRules.d.ts} +10 -10
- package/dist/transactions/{coalesceRules.js → mutations/coalesceRules.js} +1 -1
- package/dist/transactions/mutations/commitLatency.d.ts +52 -0
- package/dist/transactions/mutations/commitLatency.js +130 -0
- package/dist/transactions/{commitOutboxStore.d.ts → mutations/commitOutboxStore.d.ts} +1 -5
- package/dist/transactions/{commitOutboxStore.js → mutations/commitOutboxStore.js} +1 -1
- package/dist/transactions/{commitPayload.d.ts → mutations/commitPayload.d.ts} +18 -16
- package/dist/transactions/{commitPayload.js → mutations/commitPayload.js} +15 -15
- package/dist/transactions/{deltaConfirmation.d.ts → mutations/deltaConfirmation.d.ts} +15 -11
- package/dist/transactions/{deltaConfirmation.js → mutations/deltaConfirmation.js} +14 -12
- package/dist/transactions/mutations/durableWriteStore.d.ts +14 -0
- package/dist/transactions/mutations/durableWriteStore.js +12 -0
- package/dist/transactions/{optimisticApply.d.ts → mutations/optimisticApply.d.ts} +7 -7
- package/dist/transactions/{replayValidation.d.ts → mutations/replayValidation.d.ts} +4 -3
- package/dist/transactions/{replayValidation.js → mutations/replayValidation.js} +7 -5
- package/dist/utils/mobxSetup.d.ts +1 -1
- package/dist/utils/mobxSetup.js +5 -2
- package/dist/{core → views}/QueryView.d.ts +2 -2
- package/dist/{core → views}/QueryView.js +2 -2
- package/dist/{core → views}/ViewRegistry.d.ts +1 -1
- package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
- package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
- package/dist/webhooks/events.d.ts +2 -2
- package/dist/wire/index.d.ts +1 -34
- package/dist/wire/index.js +8 -49
- package/docs/agent-messaging.md +3 -3
- package/docs/agents.md +20 -13
- package/docs/api-keys.md +14 -10
- package/docs/api.md +27 -61
- package/docs/audit.md +6 -3
- package/docs/cli.md +41 -13
- package/docs/client-behavior.md +11 -9
- package/docs/concurrency-convention.md +49 -57
- package/docs/coordination.md +283 -121
- package/docs/data-sources.md +7 -5
- package/docs/debugging.md +39 -15
- package/docs/deployment.md +267 -0
- package/docs/examples/agent-human.md +49 -42
- package/docs/examples/ai-sdk-tool.md +69 -44
- package/docs/examples/existing-python-backend.md +8 -6
- package/docs/examples/nextjs.md +129 -47
- package/docs/examples/scoped-agent.md +46 -45
- package/docs/examples/server-agent.md +46 -26
- package/docs/groups.md +87 -30
- package/docs/guarantees.md +41 -12
- package/docs/how-it-works.md +38 -12
- package/docs/idempotency.md +126 -0
- package/docs/identity.md +77 -74
- package/docs/index.md +172 -86
- package/docs/integration-guide.md +31 -19
- package/docs/mcp.md +46 -21
- package/docs/migration.md +95 -18
- package/docs/operating-on-your-database.md +3 -1
- package/docs/projects.md +3 -1
- package/docs/quickstart.md +22 -5
- package/docs/react.md +31 -18
- package/docs/schema-contract.md +5 -3
- package/docs/session-settings.md +108 -0
- package/docs/sessions.md +4 -2
- package/docs/webhooks.md +12 -10
- package/llms.txt +48 -18
- package/package.json +21 -26
- package/dist/agent/Agent.d.ts +0 -366
- package/dist/agent/Agent.js +0 -514
- package/dist/agent/index.d.ts +0 -115
- package/dist/agent/index.js +0 -128
- package/dist/agent/session.d.ts +0 -93
- package/dist/agent/session.js +0 -149
- package/dist/agent/types.d.ts +0 -68
- package/dist/agent/types.js +0 -9
- package/dist/cli.cjs +0 -286329
- package/dist/client/durableWrites.d.ts +0 -21
- package/dist/client/httpClient.d.ts +0 -80
- package/dist/coordination/schema.d.ts +0 -722
- package/dist/coordination/schema.js +0 -578
- package/dist/schema/openapi.d.ts +0 -29
- package/dist/schema/openapi.js +0 -124
- package/dist/testing/fixtures/bootstrap.d.ts +0 -49
- package/dist/testing/fixtures/bootstrap.js +0 -59
- package/dist/testing/fixtures/deltas.d.ts +0 -83
- package/dist/testing/fixtures/deltas.js +0 -136
- package/dist/testing/fixtures/models.d.ts +0 -83
- package/dist/testing/fixtures/models.js +0 -272
- package/dist/testing/helpers/reactWrapper.d.ts +0 -69
- package/dist/testing/helpers/reactWrapper.js +0 -67
- package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
- package/dist/testing/helpers/syncEngineHarness.js +0 -73
- package/dist/testing/helpers/wait.d.ts +0 -30
- package/dist/testing/helpers/wait.js +0 -49
- package/dist/testing/index.d.ts +0 -23
- package/dist/testing/index.js +0 -33
- package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
- package/dist/testing/mocks/FakeDatabase.js +0 -10
- package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
- package/dist/testing/mocks/MockMutationExecutor.js +0 -192
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
- package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
- package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
- package/dist/testing/mocks/MockSyncContext.js +0 -71
- package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
- package/dist/testing/mocks/MockSyncStore.js +0 -171
- package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
- package/dist/testing/mocks/MockWebSocket.js +0 -118
- package/dist/transactions/durableWriteStore.js +0 -30
- package/dist/utils/duration.d.ts +0 -25
- package/dist/utils/json.js +0 -88
- package/dist/wire/errorEnvelope.d.ts +0 -55
- package/dist/wire/frames.d.ts +0 -197
- package/dist/wire/frames.js +0 -49
- package/dist/wire/listEnvelope.js +0 -18
- package/docs/interaction-model.md +0 -97
- /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
- /package/dist/{core → query}/QueryProcessor.js +0 -0
- /package/dist/{core/storeContract.js → storeContract.js} +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
- /package/dist/{client → transaction/auth}/credentialEndpoint.d.ts +0 -0
- /package/dist/{client → transaction/auth}/credentialEndpoint.js +0 -0
- /package/dist/{auth → transaction/auth}/credentialPolicy.d.ts +0 -0
- /package/dist/{auth → transaction/auth}/credentialPolicy.js +0 -0
- /package/dist/{auth → transaction/auth}/credentialSource.js +0 -0
- /package/dist/{client → transaction/auth}/hostedEndpoints.d.ts +0 -0
- /package/dist/{client → transaction/auth}/hostedEndpoints.js +0 -0
- /package/dist/{client → transaction}/persistence.d.ts +0 -0
- /package/dist/{client → transaction}/persistence.js +0 -0
- /package/dist/{client → transaction/resources}/functionalUpdate.d.ts +0 -0
- /package/dist/{client → transaction/resources}/functionalUpdate.js +0 -0
- /package/dist/{transactions → transaction/transactions/settlement}/idempotencyKey.d.ts +0 -0
- /package/dist/{transactions → transaction/transactions/settlement}/idempotencyKey.js +0 -0
- /package/dist/{client → transaction/transport}/httpTransport.d.ts +0 -0
- /package/dist/{types → transaction/types}/modelData.d.ts +0 -0
- /package/dist/{types → transaction/types}/modelData.js +0 -0
- /package/dist/{types → transaction/types}/participant.d.ts +0 -0
- /package/dist/{types → transaction/types}/participant.js +0 -0
- /package/dist/{types → transaction/types}/streams.js +0 -0
- /package/dist/{utils → transaction/utils}/asyncIterator.d.ts +0 -0
- /package/dist/{utils → transaction/utils}/asyncIterator.js +0 -0
- /package/dist/{wire → transaction/wire}/bootstrapReason.d.ts +0 -0
- /package/dist/{wire → transaction/wire}/bootstrapReason.js +0 -0
- /package/dist/{wire → transaction/wire}/protocol.d.ts +0 -0
- /package/dist/{wire → transaction/wire}/protocol.js +0 -0
- /package/dist/{wire → transaction/wire}/protocolVersion.d.ts +0 -0
- /package/dist/{wire → transaction/wire}/protocolVersion.js +0 -0
- /package/dist/transactions/{UnconfirmedWrites.d.ts → mutations/UnconfirmedWrites.d.ts} +0 -0
- /package/dist/transactions/{UnconfirmedWrites.js → mutations/UnconfirmedWrites.js} +0 -0
- /package/dist/transactions/{optimisticApply.js → mutations/optimisticApply.js} +0 -0
- /package/dist/{core → views}/ViewRegistry.js +0 -0
package/dist/SyncClient.js
CHANGED
|
@@ -4,24 +4,26 @@
|
|
|
4
4
|
* optimistic changes, holds them while the client is offline, sends them to
|
|
5
5
|
* the server when connectivity returns, and resolves conflicts when the
|
|
6
6
|
* server's version of a row disagrees with the local one. It sits between the
|
|
7
|
-
* reactive object pool and the {@link
|
|
7
|
+
* reactive object pool and the {@link MutationQueue} that delivers writes
|
|
8
8
|
* over the network.
|
|
9
9
|
*/
|
|
10
10
|
import { runInAction } from 'mobx';
|
|
11
11
|
import { v4 as uuid } from 'uuid';
|
|
12
12
|
import { InstanceCache, ModelScope } from './InstanceCache.js';
|
|
13
13
|
import { Model } from './Model.js';
|
|
14
|
+
import { snapshotJsonValue } from './transaction/utils/json.js';
|
|
14
15
|
// ModelRegistry instance accessed via this.objectPool.registry
|
|
15
|
-
import { LoadStrategy } from './types/index.js';
|
|
16
|
-
import {
|
|
17
|
-
import { AbloAuthenticationError, AbloError, AbloValidationError } from './errors.js';
|
|
16
|
+
import { LoadStrategy } from './transaction/types/index.js';
|
|
17
|
+
import { globalRuntime } from './context.js';
|
|
18
|
+
import { AbloAuthenticationError, AbloError, AbloValidationError } from './transaction/errors.js';
|
|
18
19
|
import { EventEmitter } from 'events';
|
|
19
20
|
import { NetworkMonitor } from './NetworkMonitor.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
21
|
+
import { MutationQueue } from './transactions/mutations/MutationQueue.js';
|
|
22
|
+
import { observeCommitLatency, } from './transactions/mutations/commitLatency.js';
|
|
23
|
+
import { legacyPendingMutationRecordSchema, PENDING_MUTATION_REPLAY_WINDOW_MS, pendingMutationRecordId, pendingMutationRecordSchema, persistedMutationSchema, } from './transactions/mutations/replayValidation.js';
|
|
24
|
+
import { UnconfirmedWrites, } from './transactions/mutations/UnconfirmedWrites.js';
|
|
25
|
+
import { LogPosition } from './transaction/logPosition.js';
|
|
26
|
+
import { DatabaseCommitOutboxStore, } from './transactions/mutations/commitOutboxStore.js';
|
|
25
27
|
/**
|
|
26
28
|
* Reports whether an incoming snapshot record is strictly newer than the
|
|
27
29
|
* model already in the pool. The comparison uses the server-stamped
|
|
@@ -65,15 +67,16 @@ function toEpochMs(value) {
|
|
|
65
67
|
return 0;
|
|
66
68
|
}
|
|
67
69
|
export class SyncClient extends EventEmitter {
|
|
70
|
+
runtime;
|
|
68
71
|
objectPool;
|
|
69
72
|
database;
|
|
70
|
-
get mutationExecutor() { return
|
|
73
|
+
get mutationExecutor() { return this.runtime.mutationExecutor; }
|
|
71
74
|
networkMonitor;
|
|
72
75
|
/**
|
|
73
76
|
* @internal — test seam, stripped from the published declarations by
|
|
74
77
|
* `stripInternal`. Unit suites deliver queue lifecycle events directly.
|
|
75
78
|
*/
|
|
76
|
-
|
|
79
|
+
mutationQueue;
|
|
77
80
|
observers = new Set();
|
|
78
81
|
// Authentication context
|
|
79
82
|
userId = null;
|
|
@@ -104,20 +107,22 @@ export class SyncClient extends EventEmitter {
|
|
|
104
107
|
isDisposed = false;
|
|
105
108
|
/**
|
|
106
109
|
* The client's position in the global delta order, held as the single
|
|
107
|
-
* canonical {@link
|
|
110
|
+
* canonical {@link LogPosition} instance. The store advances `applied` and
|
|
108
111
|
* `persisted` as deltas land, the queue advances `acked` on commit
|
|
109
112
|
* responses, and snapshots and claims read `readFloor`.
|
|
110
113
|
*/
|
|
111
|
-
position = new
|
|
112
|
-
constructor(objectPool, database, commitOutbox = new DatabaseCommitOutboxStore(database), commitOutboxNamespace = 'default') {
|
|
114
|
+
position = new LogPosition();
|
|
115
|
+
constructor(objectPool, database, commitOutbox = new DatabaseCommitOutboxStore(database), commitOutboxNamespace = 'default', runtime = globalRuntime) {
|
|
113
116
|
super();
|
|
117
|
+
this.runtime = runtime;
|
|
114
118
|
this.objectPool = objectPool;
|
|
115
119
|
this.database = database;
|
|
116
120
|
this.commitOutboxNamespace = commitOutboxNamespace;
|
|
117
|
-
this.networkMonitor = new NetworkMonitor();
|
|
118
|
-
// Initialize
|
|
119
|
-
this.
|
|
121
|
+
this.networkMonitor = new NetworkMonitor(this.runtime);
|
|
122
|
+
// Initialize MutationQueue with proper configuration
|
|
123
|
+
this.mutationQueue = new MutationQueue({
|
|
120
124
|
position: this.position,
|
|
125
|
+
runtime: this.runtime,
|
|
121
126
|
maxBatchSize: 50, // Larger batches keep the batch count low for bulk operations
|
|
122
127
|
// A short delay keeps writes responsive; coalescing still groups them
|
|
123
128
|
batchDelay: 150,
|
|
@@ -128,8 +133,8 @@ export class SyncClient extends EventEmitter {
|
|
|
128
133
|
strategy: 'last-write-wins',
|
|
129
134
|
},
|
|
130
135
|
});
|
|
131
|
-
this.
|
|
132
|
-
this.
|
|
136
|
+
this.mutationQueue.setCommitOutbox(commitOutbox);
|
|
137
|
+
this.mutationQueue.on('commit:envelope_persisted', (event) => {
|
|
133
138
|
if (event.sourceMutationIds.length === 0)
|
|
134
139
|
return;
|
|
135
140
|
const consumed = new Set(event.sourceMutationIds);
|
|
@@ -140,7 +145,7 @@ export class SyncClient extends EventEmitter {
|
|
|
140
145
|
this.scheduleSync();
|
|
141
146
|
}
|
|
142
147
|
});
|
|
143
|
-
this.
|
|
148
|
+
this.mutationQueue.on('transaction:completed', (transaction) => {
|
|
144
149
|
const completed = new Set(transaction.sourceMutationIds ?? []);
|
|
145
150
|
if (completed.size > 0) {
|
|
146
151
|
this.pendingMutations = this.pendingMutations.filter((mutation) => !completed.has(mutation.mutationId));
|
|
@@ -155,7 +160,7 @@ export class SyncClient extends EventEmitter {
|
|
|
155
160
|
this.scheduleSync();
|
|
156
161
|
}
|
|
157
162
|
});
|
|
158
|
-
this.
|
|
163
|
+
this.mutationQueue.on('transaction:failed', ({ transaction }) => {
|
|
159
164
|
const failed = transaction.sourceMutationIds ?? [];
|
|
160
165
|
if (failed.length === 0)
|
|
161
166
|
return;
|
|
@@ -175,8 +180,8 @@ export class SyncClient extends EventEmitter {
|
|
|
175
180
|
this.scheduleSync();
|
|
176
181
|
}
|
|
177
182
|
});
|
|
178
|
-
// Provide connection state to
|
|
179
|
-
this.
|
|
183
|
+
// Provide connection state to MutationQueue - prevents rollbacks during disconnection
|
|
184
|
+
this.mutationQueue.setConnectionChecker(() => this.connectionState === 'connected');
|
|
180
185
|
// Restore object-pool state when a transaction is rolled back. If the
|
|
181
186
|
// server rejects a write or it times out, the model's previous state is
|
|
182
187
|
// put back. Because writes are no longer applied to IndexedDB
|
|
@@ -201,7 +206,7 @@ export class SyncClient extends EventEmitter {
|
|
|
201
206
|
// losing a failed reconnect flush silently.
|
|
202
207
|
this.networkMonitor.on('online', () => {
|
|
203
208
|
void this.handleReconnection().catch((error) => {
|
|
204
|
-
|
|
209
|
+
this.runtime.observability.captureMutationFailure({
|
|
205
210
|
context: 'network-online-reconnection',
|
|
206
211
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
207
212
|
});
|
|
@@ -209,7 +214,7 @@ export class SyncClient extends EventEmitter {
|
|
|
209
214
|
});
|
|
210
215
|
this.networkMonitor.on('offline', () => {
|
|
211
216
|
void this.handleDisconnection().catch((error) => {
|
|
212
|
-
|
|
217
|
+
this.runtime.observability.captureMutationFailure({
|
|
213
218
|
context: 'network-offline-handler',
|
|
214
219
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
215
220
|
});
|
|
@@ -233,12 +238,12 @@ export class SyncClient extends EventEmitter {
|
|
|
233
238
|
* discard the optimistic state silently.
|
|
234
239
|
*
|
|
235
240
|
* Treating both paths the same caused the deletion-flicker bug: every
|
|
236
|
-
* cancelled update on a multi-
|
|
241
|
+
* cancelled update on a multi-child record fired a per-model observer
|
|
237
242
|
* event and a `[SyncClient.rollback]` warn, producing N renders and N
|
|
238
243
|
* spam log lines for one user-initiated delete.
|
|
239
244
|
*/
|
|
240
245
|
setupTransactionRollbackHandling() {
|
|
241
|
-
this.
|
|
246
|
+
this.mutationQueue.on('optimistic:rollback', (event) => {
|
|
242
247
|
const { model, previousState, transaction, reason, error } = event;
|
|
243
248
|
// Local cleanup path — discard quietly. The optimistic state was
|
|
244
249
|
// applied to a model that's already disposed by the cascading
|
|
@@ -253,13 +258,13 @@ export class SyncClient extends EventEmitter {
|
|
|
253
258
|
// `AbloServerError` with `httpStatus: 500`). Falling back to
|
|
254
259
|
// generic message lets us still see unstructured errors.
|
|
255
260
|
// Mechanic-level breadcrumb only. The authoritative, user-facing
|
|
256
|
-
// reason is logged once at `warn` by `
|
|
261
|
+
// reason is logged once at `warn` by `MutationQueue.handleFailure`
|
|
257
262
|
// (`Permanent error - rolling back`). Logging the same typed cause
|
|
258
263
|
// again here at `warn` is what produced three identical dumps per
|
|
259
264
|
// rejected write — keep it at `debug` so the rollback mechanics are
|
|
260
265
|
// available when debugging but don't double the console noise.
|
|
261
266
|
const abloErr = error instanceof AbloError ? error : undefined;
|
|
262
|
-
|
|
267
|
+
this.runtime.logger.debug('[SyncClient.rollback]', {
|
|
263
268
|
txType: transaction.type,
|
|
264
269
|
modelName: transaction.modelName,
|
|
265
270
|
modelId: transaction.modelId.slice(0, 12),
|
|
@@ -270,7 +275,7 @@ export class SyncClient extends EventEmitter {
|
|
|
270
275
|
requestId: abloErr?.requestId,
|
|
271
276
|
message: error?.message,
|
|
272
277
|
});
|
|
273
|
-
|
|
278
|
+
this.runtime.observability.captureRollback({
|
|
274
279
|
transactionType: transaction.type,
|
|
275
280
|
modelName: transaction.modelName,
|
|
276
281
|
modelId: transaction.modelId,
|
|
@@ -289,7 +294,7 @@ export class SyncClient extends EventEmitter {
|
|
|
289
294
|
// DELETE "not found" rollback: the entity doesn't exist on the server.
|
|
290
295
|
// Instead of restoring a ghost entity, remove it locally too.
|
|
291
296
|
// Both sides agree: this entity should not exist.
|
|
292
|
-
|
|
297
|
+
this.runtime.observability.breadcrumb('DELETE rolled back with "not found" - removing ghost entity', 'sync.conflict', 'info', {
|
|
293
298
|
modelId: transaction.modelId,
|
|
294
299
|
modelName: transaction.modelName,
|
|
295
300
|
});
|
|
@@ -303,11 +308,11 @@ export class SyncClient extends EventEmitter {
|
|
|
303
308
|
if (model.disposed) {
|
|
304
309
|
// Follow-on of an already-logged permanent error, not its own
|
|
305
310
|
// problem: the tx that failed has already surfaced the cause in
|
|
306
|
-
//
|
|
311
|
+
// MutationQueue. Restoring a disposed model is a no-op by
|
|
307
312
|
// design (can't revive the private isDisposed flag), so keep this
|
|
308
313
|
// at `debug` instead of emitting a second `warn` that reads as a
|
|
309
314
|
// distinct failure in the console.
|
|
310
|
-
|
|
315
|
+
this.runtime.logger.debug('[SyncClient] Rollback skipped restore (model already disposed)', {
|
|
311
316
|
modelId: transaction.modelId,
|
|
312
317
|
modelName: transaction.modelName,
|
|
313
318
|
reason,
|
|
@@ -334,7 +339,7 @@ export class SyncClient extends EventEmitter {
|
|
|
334
339
|
});
|
|
335
340
|
}
|
|
336
341
|
catch (error) {
|
|
337
|
-
|
|
342
|
+
this.runtime.observability.captureMutationFailure({
|
|
338
343
|
context: 'rollback-failed',
|
|
339
344
|
transactionId: transaction.id,
|
|
340
345
|
modelName: transaction.modelName,
|
|
@@ -345,14 +350,14 @@ export class SyncClient extends EventEmitter {
|
|
|
345
350
|
});
|
|
346
351
|
}
|
|
347
352
|
/**
|
|
348
|
-
* Forward reconciliation requests from the {@link
|
|
353
|
+
* Forward reconciliation requests from the {@link MutationQueue} to the
|
|
349
354
|
* sync layer. When delta confirmation times out, the queue emits
|
|
350
355
|
* `reconciliation:needed` instead of rolling back, so optimistic state the
|
|
351
356
|
* server may already have committed is never destroyed.
|
|
352
357
|
*/
|
|
353
358
|
setupReconciliationForwarding() {
|
|
354
|
-
this.
|
|
355
|
-
|
|
359
|
+
this.mutationQueue.on('reconciliation:needed', (event) => {
|
|
360
|
+
this.runtime.observability.captureReconciliation({
|
|
356
361
|
reason: event.reason,
|
|
357
362
|
model: event.model,
|
|
358
363
|
modelId: event.modelId,
|
|
@@ -372,12 +377,12 @@ export class SyncClient extends EventEmitter {
|
|
|
372
377
|
* deliver the missing deltas and confirm the transaction.
|
|
373
378
|
*/
|
|
374
379
|
setupAwaitingTransactionPersistence() {
|
|
375
|
-
this.
|
|
380
|
+
this.mutationQueue.on('transaction:persist_awaiting', (event) => {
|
|
376
381
|
// void is safe: the handler's body is fully try/catch'd.
|
|
377
382
|
void this.persistAwaitingTransaction(event);
|
|
378
383
|
});
|
|
379
384
|
// Clean up persisted awaiting transactions when they're finally confirmed
|
|
380
|
-
this.
|
|
385
|
+
this.mutationQueue.on('transaction:completed', (tx) => {
|
|
381
386
|
// void is safe: the handler's body is fully try/catch'd.
|
|
382
387
|
void this.removeAwaitingTransaction(tx.id);
|
|
383
388
|
});
|
|
@@ -389,11 +394,11 @@ export class SyncClient extends EventEmitter {
|
|
|
389
394
|
// server processes it, we drain on rollback too so a stale id
|
|
390
395
|
// doesn't permanently silence a foreign delta sharing the same id
|
|
391
396
|
// (vanishingly unlikely for UUIDs, but cheap insurance).
|
|
392
|
-
this.
|
|
397
|
+
this.mutationQueue.on('transaction:created', (tx) => {
|
|
393
398
|
if (!tx.localOnly)
|
|
394
399
|
this.echoTracker.markPending(tx.id);
|
|
395
400
|
});
|
|
396
|
-
this.
|
|
401
|
+
this.mutationQueue.on('optimistic:rollback', (event) => {
|
|
397
402
|
this.echoTracker.drainOnRollback(event.transaction.id);
|
|
398
403
|
});
|
|
399
404
|
}
|
|
@@ -413,14 +418,14 @@ export class SyncClient extends EventEmitter {
|
|
|
413
418
|
operationType: event.operationType,
|
|
414
419
|
},
|
|
415
420
|
});
|
|
416
|
-
|
|
421
|
+
this.runtime.observability.breadcrumb('Persisted unconfirmed transaction to IDB', 'sync.transaction', 'info', {
|
|
417
422
|
txId: event.txId,
|
|
418
423
|
model: event.model,
|
|
419
424
|
modelId: event.modelId,
|
|
420
425
|
});
|
|
421
426
|
}
|
|
422
427
|
catch (error) {
|
|
423
|
-
|
|
428
|
+
this.runtime.observability.captureMutationFailure({
|
|
424
429
|
context: 'persist-awaiting-transaction',
|
|
425
430
|
modelName: event.model,
|
|
426
431
|
modelId: event.modelId,
|
|
@@ -445,8 +450,8 @@ export class SyncClient extends EventEmitter {
|
|
|
445
450
|
async initialize(userId, organizationId) {
|
|
446
451
|
this.userId = userId;
|
|
447
452
|
this.organizationId = organizationId;
|
|
448
|
-
|
|
449
|
-
this.
|
|
453
|
+
this.runtime.observability.setContext(userId, organizationId);
|
|
454
|
+
this.mutationQueue.setCommitOutboxScope({
|
|
450
455
|
organizationId,
|
|
451
456
|
participantId: userId,
|
|
452
457
|
namespace: this.commitOutboxNamespace,
|
|
@@ -461,14 +466,14 @@ export class SyncClient extends EventEmitter {
|
|
|
461
466
|
// Restore exact, already-sealed requests first. The returned source ids
|
|
462
467
|
// suppress any legacy queue entry left behind by an older non-atomic
|
|
463
468
|
// handoff.
|
|
464
|
-
const sealedMutationIds = await this.
|
|
469
|
+
const sealedMutationIds = await this.mutationQueue.restoreDurableCommits();
|
|
465
470
|
await this.restoreMutationQueue(sealedMutationIds);
|
|
466
471
|
// Read the initial network status from the injected OnlineStatusProvider.
|
|
467
472
|
// In the browser this reflects the host's connectivity signal; in Node it
|
|
468
473
|
// reports online by default. NetworkMonitor drives the ongoing
|
|
469
474
|
// online/offline transitions below — this read is only the initial
|
|
470
475
|
// snapshot taken when identity is set.
|
|
471
|
-
if (
|
|
476
|
+
if (this.runtime.onlineStatus.isOnline()) {
|
|
472
477
|
this.setConnectionState('connected');
|
|
473
478
|
}
|
|
474
479
|
else {
|
|
@@ -501,7 +506,7 @@ export class SyncClient extends EventEmitter {
|
|
|
501
506
|
* 2. **Required-field gate** — if the row is missing any field listed
|
|
502
507
|
* in the model's `requiredFields`, return `null` so the caller
|
|
503
508
|
* skips this record. Used for FK columns whose absence renders the
|
|
504
|
-
* row unrecoverable (e.g. a
|
|
509
|
+
* row unrecoverable (e.g. a Block with no sectionId).
|
|
505
510
|
*
|
|
506
511
|
* The engine itself is product-neutral: model identity (which fields
|
|
507
512
|
* to back-fill, which absences are fatal) lives entirely in the
|
|
@@ -521,7 +526,7 @@ export class SyncClient extends EventEmitter {
|
|
|
521
526
|
const replacement = rule.from === 'organizationId' ? this.organizationId : this.userId;
|
|
522
527
|
if (!replacement)
|
|
523
528
|
continue;
|
|
524
|
-
|
|
529
|
+
this.runtime.observability.captureSelfHealing({
|
|
525
530
|
modelName: modelType,
|
|
526
531
|
modelId: idPrefix,
|
|
527
532
|
field: rule.field,
|
|
@@ -535,7 +540,7 @@ export class SyncClient extends EventEmitter {
|
|
|
535
540
|
for (const field of meta.requiredFields) {
|
|
536
541
|
if (result[field])
|
|
537
542
|
continue;
|
|
538
|
-
|
|
543
|
+
this.runtime.observability.captureSelfHealing({
|
|
539
544
|
modelName: modelType,
|
|
540
545
|
modelId: idPrefix,
|
|
541
546
|
field,
|
|
@@ -559,7 +564,7 @@ export class SyncClient extends EventEmitter {
|
|
|
559
564
|
// Get model types that should be hydrated on startup (skip lazy per LSE)
|
|
560
565
|
const modelTypes = this.objectPool.registry.getRegisteredModelNames().filter((name) => {
|
|
561
566
|
const meta = this.objectPool.registry.getMetadata(name);
|
|
562
|
-
return
|
|
567
|
+
return meta?.loadStrategy === LoadStrategy.instant;
|
|
563
568
|
});
|
|
564
569
|
const totalStart = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
565
570
|
// Phase 1: Fetch all data from IndexedDB and create model instances (async I/O).
|
|
@@ -598,7 +603,7 @@ export class SyncClient extends EventEmitter {
|
|
|
598
603
|
allModelsToAdd.push(...modelsForType);
|
|
599
604
|
// Persist healed records back to IndexedDB (fire-and-forget, non-blocking)
|
|
600
605
|
if (recordsToHeal.length > 0 && this.database) {
|
|
601
|
-
|
|
606
|
+
this.runtime.logger.info(`[SyncClient.hydrate] Persisting ${recordsToHeal.length} healed ${modelType} records to IndexedDB`);
|
|
602
607
|
// Use fire-and-forget to not block hydration.
|
|
603
608
|
// void is safe: the handler's body is fully try/catch'd.
|
|
604
609
|
void Promise.resolve().then(async () => {
|
|
@@ -606,10 +611,10 @@ export class SyncClient extends EventEmitter {
|
|
|
606
611
|
for (const { id, data } of recordsToHeal) {
|
|
607
612
|
await this.database.putRecord(modelType, id, data);
|
|
608
613
|
}
|
|
609
|
-
|
|
614
|
+
this.runtime.logger.info(`[SyncClient.hydrate] Successfully healed ${recordsToHeal.length} ${modelType} records`);
|
|
610
615
|
}
|
|
611
616
|
catch (err) {
|
|
612
|
-
|
|
617
|
+
this.runtime.observability.captureMutationFailure({
|
|
613
618
|
context: 'persist-healed-records',
|
|
614
619
|
modelName: modelType,
|
|
615
620
|
error: err instanceof Error ? err : new Error(String(err)),
|
|
@@ -627,7 +632,7 @@ export class SyncClient extends EventEmitter {
|
|
|
627
632
|
});
|
|
628
633
|
}
|
|
629
634
|
catch (error) {
|
|
630
|
-
|
|
635
|
+
this.runtime.observability.captureBootstrapFailure(error, { type: `hydrate-${modelType}` });
|
|
631
636
|
}
|
|
632
637
|
}
|
|
633
638
|
// Phase 2: Single MobX action — add ALL models across all types at once.
|
|
@@ -636,7 +641,7 @@ export class SyncClient extends EventEmitter {
|
|
|
636
641
|
const addEnd = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
637
642
|
// Log per-type perf after the batched add (so logs still show per-type breakdown)
|
|
638
643
|
for (const entry of perTypePerfLogs) {
|
|
639
|
-
|
|
644
|
+
this.runtime.logger.debug('hydrate:type', parseFloat(entry.fetchMs) + parseFloat(entry.createMs), {
|
|
640
645
|
type: entry.type,
|
|
641
646
|
fetched: entry.fetched,
|
|
642
647
|
added: entry.added,
|
|
@@ -645,7 +650,7 @@ export class SyncClient extends EventEmitter {
|
|
|
645
650
|
});
|
|
646
651
|
}
|
|
647
652
|
const totalEnd = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
648
|
-
|
|
653
|
+
this.runtime.logger.debug('hydrate:total', totalEnd - totalStart, {
|
|
649
654
|
totalModels: totalAdded,
|
|
650
655
|
addBatchMs: (addEnd - addStart).toFixed(2),
|
|
651
656
|
});
|
|
@@ -653,7 +658,7 @@ export class SyncClient extends EventEmitter {
|
|
|
653
658
|
try {
|
|
654
659
|
const preseededTypes = this.objectPool.registry.getRegisteredModelNames();
|
|
655
660
|
const stats = this.objectPool.getStats();
|
|
656
|
-
|
|
661
|
+
this.runtime.logger.info('startup_summary', {
|
|
657
662
|
typesPreseeded: preseededTypes.length,
|
|
658
663
|
poolSize: stats.size,
|
|
659
664
|
typeCounts: stats.typeCounts,
|
|
@@ -681,7 +686,7 @@ export class SyncClient extends EventEmitter {
|
|
|
681
686
|
// Model types to rehydrate (same filter as hydrateFromDatabase)
|
|
682
687
|
const modelTypes = this.objectPool.registry.getRegisteredModelNames().filter((name) => {
|
|
683
688
|
const meta = this.objectPool.registry.getMetadata(name);
|
|
684
|
-
return
|
|
689
|
+
return meta?.loadStrategy === LoadStrategy.instant;
|
|
685
690
|
});
|
|
686
691
|
// ── Phase 1: Read from IndexedDB & create model instances (async I/O) ──
|
|
687
692
|
const allModels = [];
|
|
@@ -734,7 +739,7 @@ export class SyncClient extends EventEmitter {
|
|
|
734
739
|
}
|
|
735
740
|
}
|
|
736
741
|
catch (error) {
|
|
737
|
-
|
|
742
|
+
this.runtime.observability.breadcrumb('Model creation failed during rehydration', 'sync.bootstrap', 'warning', {
|
|
738
743
|
modelType,
|
|
739
744
|
modelId: recordId?.slice(0, 8) ?? 'unknown',
|
|
740
745
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -744,7 +749,7 @@ export class SyncClient extends EventEmitter {
|
|
|
744
749
|
}
|
|
745
750
|
}
|
|
746
751
|
catch (error) {
|
|
747
|
-
|
|
752
|
+
this.runtime.observability.captureBootstrapFailure(error, { type: `rehydrate-${modelType}` });
|
|
748
753
|
}
|
|
749
754
|
}
|
|
750
755
|
// ── Phase 2: Upsert batch (single MobX action) ──
|
|
@@ -783,12 +788,12 @@ export class SyncClient extends EventEmitter {
|
|
|
783
788
|
healed: healedCount,
|
|
784
789
|
elapsedMs,
|
|
785
790
|
};
|
|
786
|
-
|
|
791
|
+
this.runtime.logger.info('[SyncClient.rehydrate] Complete', {
|
|
787
792
|
...stats,
|
|
788
793
|
poolSize: this.objectPool.size,
|
|
789
794
|
ghostIds: ghostIds.length > 0 ? ghostIds.slice(0, 5).map((id) => id.slice(0, 8)) : [],
|
|
790
795
|
});
|
|
791
|
-
|
|
796
|
+
this.runtime.observability.breadcrumb('Rehydration complete', 'sync.bootstrap', 'info', {
|
|
792
797
|
added: stats.added,
|
|
793
798
|
updated: stats.updated,
|
|
794
799
|
removed: stats.removed,
|
|
@@ -819,8 +824,9 @@ export class SyncClient extends EventEmitter {
|
|
|
819
824
|
// object can reach here with `hasChanges === undefined`. `undefined === false`
|
|
820
825
|
// is false → we fall through to the normal path rather than risk dropping a
|
|
821
826
|
// real write. Only a genuine Model with an empty dirty-set is skipped.
|
|
822
|
-
if (type === 'update' && model.hasChanges === false)
|
|
827
|
+
if (type === 'update' && model.hasChanges === false) {
|
|
823
828
|
return;
|
|
829
|
+
}
|
|
824
830
|
// Capture changes before the pool action runs. Pool operations —
|
|
825
831
|
// upsert in particular — can clear the model's local changes, so
|
|
826
832
|
// capturing first ensures they are never lost.
|
|
@@ -834,7 +840,7 @@ export class SyncClient extends EventEmitter {
|
|
|
834
840
|
modelId: model.id,
|
|
835
841
|
});
|
|
836
842
|
// QueryProcessor uses `models:changed` to invalidate caches. Coalesce
|
|
837
|
-
// to one event per microtask: a paste of 100
|
|
843
|
+
// to one event per microtask: a paste of 100 rows should re-run
|
|
838
844
|
// affected queries ONCE, not 100×.
|
|
839
845
|
this.markModelChanged(model.getModelName());
|
|
840
846
|
}
|
|
@@ -876,10 +882,10 @@ export class SyncClient extends EventEmitter {
|
|
|
876
882
|
/**
|
|
877
883
|
* Update existing model with pre-computed changes.
|
|
878
884
|
* Used by saveManyOptimized when incoming models have empty change-tracking
|
|
879
|
-
* (e.g. freshly constructed
|
|
885
|
+
* (e.g. freshly constructed cell models from a bulk document decomposition).
|
|
880
886
|
*/
|
|
881
887
|
updateWithChanges(model, changes) {
|
|
882
|
-
|
|
888
|
+
this.runtime.logger.debug(`SyncClient.updateWithChanges`, {
|
|
883
889
|
modelId: model.id,
|
|
884
890
|
modelType: model.getModelName(),
|
|
885
891
|
});
|
|
@@ -903,7 +909,7 @@ export class SyncClient extends EventEmitter {
|
|
|
903
909
|
modelId: model.id,
|
|
904
910
|
});
|
|
905
911
|
}
|
|
906
|
-
/** Expose the GraphQL client for atomic mutations (e.g.,
|
|
912
|
+
/** Expose the GraphQL client for atomic mutations (e.g., createSectionWithBlocks).
|
|
907
913
|
* Used by SyncedStore for operations that bypass the transaction queue
|
|
908
914
|
* but still need optimistic pool updates at the sync layer. */
|
|
909
915
|
get gql() {
|
|
@@ -917,7 +923,7 @@ export class SyncClient extends EventEmitter {
|
|
|
917
923
|
}
|
|
918
924
|
/**
|
|
919
925
|
* Clear all pending mutations for a specific model
|
|
920
|
-
* Called before deletion to prevent "
|
|
926
|
+
* Called before deletion to prevent "record not found" errors on the server
|
|
921
927
|
*/
|
|
922
928
|
clearPendingMutationsForModel(modelId) {
|
|
923
929
|
const beforeCount = this.pendingMutations.length;
|
|
@@ -925,13 +931,13 @@ export class SyncClient extends EventEmitter {
|
|
|
925
931
|
this.pendingMutations = this.pendingMutations.filter((m) => m.model.id !== modelId);
|
|
926
932
|
const afterCount = this.pendingMutations.length;
|
|
927
933
|
if (beforeCount !== afterCount) {
|
|
928
|
-
|
|
934
|
+
this.runtime.logger.debug('[SyncClient.clearPendingMutationsForModel] Cleared pending mutations', {
|
|
929
935
|
modelId,
|
|
930
936
|
clearedCount: beforeCount - afterCount,
|
|
931
937
|
remainingCount: afterCount,
|
|
932
938
|
});
|
|
933
939
|
for (const mutation of removed) {
|
|
934
|
-
// Once staged,
|
|
940
|
+
// Once staged, MutationQueue owns cancellation and transfers this
|
|
935
941
|
// source id into the superseding delete envelope. Deleting the journal
|
|
936
942
|
// row here would make that valid atomic promotion look like a
|
|
937
943
|
// multi-tab loser. Truly unstaged work can be canceled locally.
|
|
@@ -946,7 +952,7 @@ export class SyncClient extends EventEmitter {
|
|
|
946
952
|
}
|
|
947
953
|
/**
|
|
948
954
|
* Upload a file and create its attachment record. The upload runs through
|
|
949
|
-
* the {@link
|
|
955
|
+
* the {@link MutationQueue}, and a model is built from the server's
|
|
950
956
|
* response and added to the pool.
|
|
951
957
|
*/
|
|
952
958
|
async uploadFile(file, options) {
|
|
@@ -956,8 +962,8 @@ export class SyncClient extends EventEmitter {
|
|
|
956
962
|
});
|
|
957
963
|
}
|
|
958
964
|
try {
|
|
959
|
-
// Use
|
|
960
|
-
const result = await this.
|
|
965
|
+
// Use MutationQueue to handle the upload mutation
|
|
966
|
+
const result = await this.mutationQueue.uploadAttachment(file, {
|
|
961
967
|
id: options.id,
|
|
962
968
|
attachableType: options.attachableType,
|
|
963
969
|
attachableId: options.attachableId,
|
|
@@ -985,7 +991,7 @@ export class SyncClient extends EventEmitter {
|
|
|
985
991
|
return null;
|
|
986
992
|
}
|
|
987
993
|
catch (error) {
|
|
988
|
-
|
|
994
|
+
this.runtime.observability.captureMutationFailure({
|
|
989
995
|
context: 'file-upload',
|
|
990
996
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
991
997
|
});
|
|
@@ -1015,7 +1021,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1015
1021
|
attachableId: options.attachableId,
|
|
1016
1022
|
metadata: options.metadata,
|
|
1017
1023
|
}));
|
|
1018
|
-
const results = await this.
|
|
1024
|
+
const results = await this.mutationQueue.batchUploadAttachments(files, items, {
|
|
1019
1025
|
userId: this.userId,
|
|
1020
1026
|
organizationId: this.organizationId,
|
|
1021
1027
|
});
|
|
@@ -1044,7 +1050,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1044
1050
|
* IndexedDB persistence and the server push are deferred to a microtask, so
|
|
1045
1051
|
* many pushes within the same tick collapse into a single serialization and
|
|
1046
1052
|
* a single process call. Without the deferral, queueing a hundred mutations
|
|
1047
|
-
* at once — a large paste, a document import, bulk
|
|
1053
|
+
* at once — a large paste, a document import, bulk row creation — would
|
|
1048
1054
|
* reserialize the whole growing queue a hundred times, an O(N²) cost in
|
|
1049
1055
|
* `model.toJSON()`.
|
|
1050
1056
|
*
|
|
@@ -1113,20 +1119,59 @@ export class SyncClient extends EventEmitter {
|
|
|
1113
1119
|
return;
|
|
1114
1120
|
}
|
|
1115
1121
|
try {
|
|
1116
|
-
|
|
1122
|
+
// Build each durable row independently. A malformed mutation must not
|
|
1123
|
+
// prevent unrelated writes from the same event-loop burst from being
|
|
1124
|
+
// journaled and dispatched.
|
|
1125
|
+
const entries = [];
|
|
1126
|
+
for (const mutation of batch) {
|
|
1127
|
+
try {
|
|
1128
|
+
entries.push({
|
|
1129
|
+
mutation,
|
|
1130
|
+
record: this.pendingMutationRecord(mutation),
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
catch (error) {
|
|
1134
|
+
mutation.rejectJournal?.(error);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (entries.length === 0)
|
|
1138
|
+
return;
|
|
1117
1139
|
const database = this.database;
|
|
1140
|
+
const saveOne = async (record) => {
|
|
1141
|
+
// Some adapters (including IndexedDB's put path) can throw before
|
|
1142
|
+
// returning a Promise. The async wrapper turns that into an individual
|
|
1143
|
+
// rejected outcome instead of aborting construction of the whole list.
|
|
1144
|
+
await database.saveTransaction(record);
|
|
1145
|
+
};
|
|
1146
|
+
let outcomes;
|
|
1118
1147
|
if (database.saveTransactions) {
|
|
1119
|
-
|
|
1148
|
+
try {
|
|
1149
|
+
await database.saveTransactions(entries.map(({ record }) => record));
|
|
1150
|
+
outcomes = entries.map(() => ({
|
|
1151
|
+
status: 'fulfilled',
|
|
1152
|
+
value: undefined,
|
|
1153
|
+
}));
|
|
1154
|
+
}
|
|
1155
|
+
catch {
|
|
1156
|
+
// IndexedDB aborts a multi-row transaction when any value cannot be
|
|
1157
|
+
// cloned. Retry one row per transaction to identify the offender and
|
|
1158
|
+
// preserve every valid sibling. Re-putting a row is idempotent by id.
|
|
1159
|
+
outcomes = await Promise.allSettled(entries.map(({ record }) => saveOne(record)));
|
|
1160
|
+
}
|
|
1120
1161
|
}
|
|
1121
1162
|
else {
|
|
1122
|
-
await Promise.
|
|
1163
|
+
outcomes = await Promise.allSettled(entries.map(({ record }) => saveOne(record)));
|
|
1164
|
+
}
|
|
1165
|
+
for (const [index, entry] of entries.entries()) {
|
|
1166
|
+
const outcome = outcomes[index];
|
|
1167
|
+
if (outcome?.status === 'fulfilled') {
|
|
1168
|
+
entry.mutation.resolveJournal?.();
|
|
1169
|
+
}
|
|
1170
|
+
else {
|
|
1171
|
+
entry.mutation.rejectJournal?.(outcome?.reason ??
|
|
1172
|
+
new Error('Mutation journal persistence failed'));
|
|
1173
|
+
}
|
|
1123
1174
|
}
|
|
1124
|
-
for (const mutation of batch)
|
|
1125
|
-
mutation.resolveJournal?.();
|
|
1126
|
-
}
|
|
1127
|
-
catch (error) {
|
|
1128
|
-
for (const mutation of batch)
|
|
1129
|
-
mutation.rejectJournal?.(error);
|
|
1130
1175
|
}
|
|
1131
1176
|
finally {
|
|
1132
1177
|
for (const mutation of batch) {
|
|
@@ -1145,9 +1190,9 @@ export class SyncClient extends EventEmitter {
|
|
|
1145
1190
|
: (cb) => Promise.resolve().then(cb);
|
|
1146
1191
|
schedule(() => {
|
|
1147
1192
|
this.syncScheduled = false;
|
|
1148
|
-
if (
|
|
1193
|
+
if (this.runtime.onlineStatus.isOnline()) {
|
|
1149
1194
|
this.processPendingMutations().catch((err) => {
|
|
1150
|
-
|
|
1195
|
+
this.runtime.observability.breadcrumb('Background sync failed', 'sync.transaction', 'warning', { error: err instanceof Error ? err.message : String(err) });
|
|
1151
1196
|
});
|
|
1152
1197
|
}
|
|
1153
1198
|
});
|
|
@@ -1156,7 +1201,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1156
1201
|
if (!this.userId || !this.organizationId) {
|
|
1157
1202
|
throw new AbloValidationError('Cannot persist a mutation before participant scope is initialized', { code: 'write_options_invalid' });
|
|
1158
1203
|
}
|
|
1159
|
-
|
|
1204
|
+
const candidate = {
|
|
1160
1205
|
id: pendingMutationRecordId(mutation.mutationId),
|
|
1161
1206
|
type: 'pending_mutation',
|
|
1162
1207
|
storageVersion: 2,
|
|
@@ -1179,7 +1224,10 @@ export class SyncClient extends EventEmitter {
|
|
|
1179
1224
|
namespace: this.commitOutboxNamespace,
|
|
1180
1225
|
},
|
|
1181
1226
|
timestamp: mutation.timestamp.getTime(),
|
|
1182
|
-
}
|
|
1227
|
+
};
|
|
1228
|
+
// One framework-neutral boundary contract owns both Proxy unwrapping and
|
|
1229
|
+
// rejection of values JSON would silently corrupt before IndexedDB sees it.
|
|
1230
|
+
return pendingMutationRecordSchema.parse(snapshotJsonValue(candidate, '$.pendingMutation'));
|
|
1183
1231
|
}
|
|
1184
1232
|
async persistPendingMutation(mutation) {
|
|
1185
1233
|
await this.database.saveTransaction(this.pendingMutationRecord(mutation));
|
|
@@ -1203,7 +1251,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1203
1251
|
const restore = async (mutation, migrateLegacy, legacyMutationId) => {
|
|
1204
1252
|
const parsed = persistedMutationSchema.safeParse(mutation);
|
|
1205
1253
|
if (!parsed.success) {
|
|
1206
|
-
|
|
1254
|
+
this.runtime.logger.debug('[SyncClient] Dropping malformed persisted mutation', {
|
|
1207
1255
|
issues: parsed.error.issues.map((i) => i.path.join('.')).join(', '),
|
|
1208
1256
|
});
|
|
1209
1257
|
return;
|
|
@@ -1215,7 +1263,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1215
1263
|
const age = Date.now() - writtenAt;
|
|
1216
1264
|
if (!(age < PENDING_MUTATION_REPLAY_WINDOW_MS)) {
|
|
1217
1265
|
heldForReview += 1;
|
|
1218
|
-
|
|
1266
|
+
this.runtime.logger.warn('A saved local write is older than the server idempotency window and was held for review.');
|
|
1219
1267
|
return;
|
|
1220
1268
|
}
|
|
1221
1269
|
const mutationId = parsed.data.mutationId ?? legacyMutationId ?? `mutation_${uuid()}`;
|
|
@@ -1259,7 +1307,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1259
1307
|
await restore(legacy.data.mutation, true);
|
|
1260
1308
|
continue;
|
|
1261
1309
|
}
|
|
1262
|
-
|
|
1310
|
+
this.runtime.logger.debug('[SyncClient] Dropping malformed pending mutation record', {
|
|
1263
1311
|
rowId: row.id,
|
|
1264
1312
|
});
|
|
1265
1313
|
continue;
|
|
@@ -1267,7 +1315,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1267
1315
|
if (parsed.data.scope.organizationId !== this.organizationId ||
|
|
1268
1316
|
parsed.data.scope.participantId !== this.userId ||
|
|
1269
1317
|
parsed.data.scope.namespace !== this.commitOutboxNamespace) {
|
|
1270
|
-
|
|
1318
|
+
this.runtime.logger.warn('A saved local write belongs to a different account or server and was held for review.');
|
|
1271
1319
|
continue;
|
|
1272
1320
|
}
|
|
1273
1321
|
await restore(parsed.data.mutation, false);
|
|
@@ -1289,10 +1337,10 @@ export class SyncClient extends EventEmitter {
|
|
|
1289
1337
|
// A restore failure means queued offline writes did NOT rehydrate.
|
|
1290
1338
|
// Self-healing is impossible here (the record may be unreadable), but
|
|
1291
1339
|
// the failure must be visible for diagnosis instead of silent loss.
|
|
1292
|
-
|
|
1340
|
+
this.runtime.logger.debug('[SyncClient] Failed to restore offline mutation queue', {
|
|
1293
1341
|
error: error instanceof Error ? error.message : String(error),
|
|
1294
1342
|
});
|
|
1295
|
-
|
|
1343
|
+
this.runtime.observability.captureMutationFailure({
|
|
1296
1344
|
context: 'restore-mutation-queue',
|
|
1297
1345
|
error: error instanceof Error ? error : String(error),
|
|
1298
1346
|
});
|
|
@@ -1303,7 +1351,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1303
1351
|
*
|
|
1304
1352
|
* Best Practice: Only sync models that still exist locally (local-first principle)
|
|
1305
1353
|
* - If a model was deleted locally → skip any pending updates/creates for it
|
|
1306
|
-
* - This prevents "
|
|
1354
|
+
* - This prevents "record not found" errors from fast copy-paste-delete workflows
|
|
1307
1355
|
*/
|
|
1308
1356
|
async processPendingMutations() {
|
|
1309
1357
|
if (this.pendingMutations.length === 0)
|
|
@@ -1322,14 +1370,14 @@ export class SyncClient extends EventEmitter {
|
|
|
1322
1370
|
// identity, which surfaces downstream as "writes never confirm"; we do
|
|
1323
1371
|
// NOT name internal wiring (`SyncClient.initialize`) here because that
|
|
1324
1372
|
// method isn't part of the @abloatai/ablo surface a reader could act on.
|
|
1325
|
-
|
|
1373
|
+
this.runtime.logger.debug('[sync] writes waiting for identity (user/org not set yet) — queued, will retry', {
|
|
1326
1374
|
pending: this.pendingMutations.length,
|
|
1327
1375
|
userId: this.userId,
|
|
1328
1376
|
organizationId: this.organizationId,
|
|
1329
1377
|
});
|
|
1330
1378
|
return;
|
|
1331
1379
|
}
|
|
1332
|
-
if (!
|
|
1380
|
+
if (!this.runtime.onlineStatus.isOnline())
|
|
1333
1381
|
return; // Skip if offline
|
|
1334
1382
|
if (this.isDisposed)
|
|
1335
1383
|
return; // Skip if disposed
|
|
@@ -1374,7 +1422,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1374
1422
|
this.stagedMutationIds.delete(mutation.mutationId);
|
|
1375
1423
|
this.pendingMutations = this.pendingMutations.filter((pending) => pending.mutationId !== mutation.mutationId);
|
|
1376
1424
|
mutation.rejectStaged?.(outcome.reason);
|
|
1377
|
-
|
|
1425
|
+
this.runtime.observability.captureMutationFailure({
|
|
1378
1426
|
context: 'persist-pending-mutation',
|
|
1379
1427
|
error: outcome.reason instanceof Error
|
|
1380
1428
|
? outcome.reason
|
|
@@ -1384,12 +1432,12 @@ export class SyncClient extends EventEmitter {
|
|
|
1384
1432
|
// Stage every mutation synchronously within the same event-loop tick;
|
|
1385
1433
|
// the transaction queue's microtask batches and sends them together.
|
|
1386
1434
|
for (const mutation of journaledMutations) {
|
|
1387
|
-
// Stage synchronously -
|
|
1435
|
+
// Stage synchronously - MutationQueue handles batching, retry, and errors
|
|
1388
1436
|
this.stageMutation(mutation);
|
|
1389
1437
|
}
|
|
1390
1438
|
}
|
|
1391
1439
|
/**
|
|
1392
|
-
* Stage mutation to
|
|
1440
|
+
* Stage mutation to MutationQueue - mutations in same tick are batched via microtask
|
|
1393
1441
|
*
|
|
1394
1442
|
* @param mutation.capturedChanges - Pre-captured changes to use instead of re-reading from model
|
|
1395
1443
|
*/
|
|
@@ -1406,7 +1454,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1406
1454
|
// apply, store add). That means the write never entered the queue, so
|
|
1407
1455
|
// capture it instead of dropping it silently.
|
|
1408
1456
|
const captureStagingFailure = (error) => {
|
|
1409
|
-
|
|
1457
|
+
this.runtime.observability.captureMutationFailure({
|
|
1410
1458
|
context: `stage-mutation-${mutation.type}`,
|
|
1411
1459
|
modelName: mutation.model.getModelName(),
|
|
1412
1460
|
modelId: mutation.model.id,
|
|
@@ -1415,8 +1463,8 @@ export class SyncClient extends EventEmitter {
|
|
|
1415
1463
|
this.stagedMutationIds.delete(mutation.mutationId);
|
|
1416
1464
|
};
|
|
1417
1465
|
const staging = mutation.type === 'update'
|
|
1418
|
-
? this.
|
|
1419
|
-
: this.
|
|
1466
|
+
? this.mutationQueue.update(mutation.model, ctx, mutation.capturedChanges, mutation.writeOptions, mutation.mutationId)
|
|
1467
|
+
: this.mutationQueue[mutation.type].bind(this.mutationQueue)(mutation.model, ctx, mutation.writeOptions, mutation.mutationId);
|
|
1420
1468
|
staging
|
|
1421
1469
|
.then(() => mutation.resolveStaged?.())
|
|
1422
1470
|
.catch((error) => {
|
|
@@ -1439,7 +1487,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1439
1487
|
: new Date(localModel.updatedAt).getTime()
|
|
1440
1488
|
: 0;
|
|
1441
1489
|
const serverUpdatedAt = toEpochMs(serverData.updatedAt);
|
|
1442
|
-
|
|
1490
|
+
this.runtime.logger.debug('Conflict resolution', {
|
|
1443
1491
|
modelId: localModel.id,
|
|
1444
1492
|
modelType: localModel.getModelName(),
|
|
1445
1493
|
hasLocalChanges,
|
|
@@ -1453,7 +1501,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1453
1501
|
const criticalServerStates = this.extractCriticalState(serverData);
|
|
1454
1502
|
const shouldForceAcceptServer = this.hasCriticalStateChange(criticalServerStates);
|
|
1455
1503
|
if (shouldForceAcceptServer) {
|
|
1456
|
-
|
|
1504
|
+
this.runtime.logger.debug('Accepting server update - critical state change detected', {
|
|
1457
1505
|
modelId: localModel.id,
|
|
1458
1506
|
criticalStates: criticalServerStates,
|
|
1459
1507
|
});
|
|
@@ -1467,7 +1515,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1467
1515
|
// Keep locally changed fields; apply server for the rest.
|
|
1468
1516
|
if (hasLocalChanges) {
|
|
1469
1517
|
const localChanges = localModel.getChanges();
|
|
1470
|
-
|
|
1518
|
+
this.runtime.logger.debug('Merging server update with local dirty fields', {
|
|
1471
1519
|
modelId: localModel.id,
|
|
1472
1520
|
keptFields: Object.keys(localChanges || {}),
|
|
1473
1521
|
});
|
|
@@ -1486,7 +1534,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1486
1534
|
// No local changes: fall back to LWW to converge
|
|
1487
1535
|
// Accept server regardless of timestamp equality to stay in sync
|
|
1488
1536
|
const acceptReason = serverUpdatedAt > localUpdatedAt ? 'server is newer' : 'no local changes';
|
|
1489
|
-
|
|
1537
|
+
this.runtime.logger.debug(`Accepting server update - ${acceptReason}`);
|
|
1490
1538
|
localModel.updateFromData(serverData);
|
|
1491
1539
|
localModel.clearChanges();
|
|
1492
1540
|
localModel.markAsSynced();
|
|
@@ -1530,12 +1578,12 @@ export class SyncClient extends EventEmitter {
|
|
|
1530
1578
|
* Handle network reconnection
|
|
1531
1579
|
*/
|
|
1532
1580
|
async handleReconnection() {
|
|
1533
|
-
|
|
1581
|
+
this.runtime.observability.breadcrumb('Network reconnected', 'sync.offline');
|
|
1534
1582
|
this.emit('sync:reconnecting');
|
|
1535
1583
|
try {
|
|
1536
1584
|
// Prefer a single batch flush for pending mutations (fast path)
|
|
1537
1585
|
try {
|
|
1538
|
-
await this.
|
|
1586
|
+
await this.mutationQueue.flushOfflineQueue();
|
|
1539
1587
|
}
|
|
1540
1588
|
catch { }
|
|
1541
1589
|
// Process all queued mutations
|
|
@@ -1544,7 +1592,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1544
1592
|
this.emit('sync:reconnected');
|
|
1545
1593
|
}
|
|
1546
1594
|
catch (error) {
|
|
1547
|
-
|
|
1595
|
+
this.runtime.observability.captureMutationFailure({
|
|
1548
1596
|
context: 'reconnection-sync',
|
|
1549
1597
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
1550
1598
|
});
|
|
@@ -1555,7 +1603,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1555
1603
|
* Handle network disconnection
|
|
1556
1604
|
*/
|
|
1557
1605
|
async handleDisconnection() {
|
|
1558
|
-
|
|
1606
|
+
this.runtime.observability.breadcrumb('Network disconnected', 'sync.offline');
|
|
1559
1607
|
this.setConnectionState('disconnected');
|
|
1560
1608
|
this.emit('sync:offline');
|
|
1561
1609
|
}
|
|
@@ -1577,15 +1625,15 @@ export class SyncClient extends EventEmitter {
|
|
|
1577
1625
|
const oldState = this.connectionState;
|
|
1578
1626
|
this.connectionState = state;
|
|
1579
1627
|
if (oldState !== state) {
|
|
1580
|
-
|
|
1581
|
-
|
|
1628
|
+
this.runtime.observability.setConnectionState(state);
|
|
1629
|
+
this.runtime.observability.breadcrumb(`Connection: ${oldState} → ${state}`, 'sync.websocket');
|
|
1582
1630
|
if (state === 'connected') {
|
|
1583
1631
|
this.emit('connection:established');
|
|
1584
|
-
this.
|
|
1632
|
+
this.mutationQueue.setConnectionState('connected');
|
|
1585
1633
|
}
|
|
1586
1634
|
else if (state === 'disconnected') {
|
|
1587
1635
|
this.emit('connection:disconnected');
|
|
1588
|
-
this.
|
|
1636
|
+
this.mutationQueue.setConnectionState('disconnected');
|
|
1589
1637
|
}
|
|
1590
1638
|
}
|
|
1591
1639
|
}
|
|
@@ -1621,7 +1669,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1621
1669
|
observer.onSync(event);
|
|
1622
1670
|
}
|
|
1623
1671
|
catch (error) {
|
|
1624
|
-
|
|
1672
|
+
this.runtime.observability.breadcrumb('Observer error', 'sync.transaction', 'error', {
|
|
1625
1673
|
error: error instanceof Error ? error.message : String(error),
|
|
1626
1674
|
});
|
|
1627
1675
|
}
|
|
@@ -1643,8 +1691,8 @@ export class SyncClient extends EventEmitter {
|
|
|
1643
1691
|
// Browser online state may have marked the client connected before the
|
|
1644
1692
|
// WebSocket itself was ready. Always kick both durable lanes on the real
|
|
1645
1693
|
// socket event, even when the high-level state did not change.
|
|
1646
|
-
void this.
|
|
1647
|
-
|
|
1694
|
+
void this.mutationQueue.flushOfflineQueue().catch((error) => {
|
|
1695
|
+
this.runtime.observability.captureMutationFailure({
|
|
1648
1696
|
context: 'restore-commit-outbox',
|
|
1649
1697
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
1650
1698
|
});
|
|
@@ -1663,7 +1711,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1663
1711
|
this.removeAllListeners();
|
|
1664
1712
|
}
|
|
1665
1713
|
/**
|
|
1666
|
-
* Notify the {@link
|
|
1714
|
+
* Notify the {@link MutationQueue} of an incoming delta so it can confirm
|
|
1667
1715
|
* hosted writes by sync-id threshold and queued forwards by their echoed
|
|
1668
1716
|
* source-batch correlation id.
|
|
1669
1717
|
* @param syncId - The sync id of the received delta.
|
|
@@ -1672,10 +1720,10 @@ export class SyncClient extends EventEmitter {
|
|
|
1672
1720
|
*/
|
|
1673
1721
|
onDeltaReceived(syncId, transactionId, correlationId) {
|
|
1674
1722
|
try {
|
|
1675
|
-
this.
|
|
1723
|
+
this.mutationQueue.onDeltaReceived(syncId, transactionId, correlationId);
|
|
1676
1724
|
}
|
|
1677
1725
|
catch (e) {
|
|
1678
|
-
|
|
1726
|
+
this.runtime.observability.breadcrumb('Failed to notify delta received', 'sync.transaction', 'warning', {
|
|
1679
1727
|
syncId,
|
|
1680
1728
|
transactionId,
|
|
1681
1729
|
correlationId,
|
|
@@ -1687,20 +1735,20 @@ export class SyncClient extends EventEmitter {
|
|
|
1687
1735
|
* deletion. The store calls this when a delete delta arrives for a parent,
|
|
1688
1736
|
* cancelling any queued writes on children that reference it.
|
|
1689
1737
|
*
|
|
1690
|
-
* @param childModelName - The child model type (for example, `
|
|
1691
|
-
* @param foreignKey - The foreign-key property name (for example, `
|
|
1738
|
+
* @param childModelName - The child model type (for example, `Block`).
|
|
1739
|
+
* @param foreignKey - The foreign-key property name (for example, `sectionId`).
|
|
1692
1740
|
* @param parentId - The id of the deleted parent.
|
|
1693
1741
|
* @returns The number of transactions cancelled.
|
|
1694
1742
|
*/
|
|
1695
1743
|
cancelTransactionsByForeignKey(childModelName, foreignKey, parentId) {
|
|
1696
|
-
return this.
|
|
1744
|
+
return this.mutationQueue.cancelTransactionsByForeignKey(childModelName, foreignKey, parentId);
|
|
1697
1745
|
}
|
|
1698
1746
|
/**
|
|
1699
1747
|
* Wait for a transaction to be confirmed by its delta echo. Delegates to the
|
|
1700
|
-
* {@link
|
|
1748
|
+
* {@link MutationQueue}, which handles the confirmation timeout.
|
|
1701
1749
|
*/
|
|
1702
1750
|
waitForDeltaConfirmation(transactionId) {
|
|
1703
|
-
return this.
|
|
1751
|
+
return this.mutationQueue.waitForConfirmation(transactionId);
|
|
1704
1752
|
}
|
|
1705
1753
|
/**
|
|
1706
1754
|
* Force sync now - process pending mutations
|
|
@@ -1728,11 +1776,11 @@ export class SyncClient extends EventEmitter {
|
|
|
1728
1776
|
};
|
|
1729
1777
|
}
|
|
1730
1778
|
/**
|
|
1731
|
-
* Get pending transaction count from
|
|
1779
|
+
* Get pending transaction count from MutationQueue
|
|
1732
1780
|
* Used by SyncedStore to compute hasUnsyncedChanges
|
|
1733
1781
|
*/
|
|
1734
1782
|
getPendingTransactionCount() {
|
|
1735
|
-
const stats = this.
|
|
1783
|
+
const stats = this.mutationQueue.getStats();
|
|
1736
1784
|
// Include pending and executing as "unsynced"
|
|
1737
1785
|
// awaiting_delta transactions are included in 'executing' until confirmed
|
|
1738
1786
|
// Completed and failed are "synced" (either done or gave up)
|
|
@@ -1744,12 +1792,12 @@ export class SyncClient extends EventEmitter {
|
|
|
1744
1792
|
*/
|
|
1745
1793
|
onTransactionEvent(event, callback) {
|
|
1746
1794
|
const eventName = `transaction:${event}`;
|
|
1747
|
-
this.
|
|
1748
|
-
return () => this.
|
|
1795
|
+
this.mutationQueue.on(eventName, callback);
|
|
1796
|
+
return () => this.mutationQueue.off(eventName, callback);
|
|
1749
1797
|
}
|
|
1750
1798
|
/**
|
|
1751
1799
|
* Subscribe to mutation failures with the full payload. Mirrors the
|
|
1752
|
-
* underlying
|
|
1800
|
+
* underlying MutationQueue 'transaction:failed' shape so consumers
|
|
1753
1801
|
* can render typed UI (toast keyed by `AbloError.type`, route-level
|
|
1754
1802
|
* "this entity reverted" boundaries, telemetry).
|
|
1755
1803
|
*
|
|
@@ -1759,15 +1807,26 @@ export class SyncClient extends EventEmitter {
|
|
|
1759
1807
|
* this typed listener drives user-visible surfaces.
|
|
1760
1808
|
*/
|
|
1761
1809
|
onMutationFailure(listener) {
|
|
1762
|
-
this.
|
|
1763
|
-
return () => this.
|
|
1810
|
+
this.mutationQueue.on('transaction:failed', listener);
|
|
1811
|
+
return () => this.mutationQueue.off('transaction:failed', listener);
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Subscribe to commit round-trip latency, split into the local seal and the
|
|
1815
|
+
* remote acknowledgement. Fires once per completed commit.
|
|
1816
|
+
*
|
|
1817
|
+
* Taps the {@link MutationQueue} emitter for the same reason
|
|
1818
|
+
* {@link onMutationFailure} does: the commit lifecycle events originate
|
|
1819
|
+
* there and the SyncClient's own emitter never rebroadcasts them.
|
|
1820
|
+
*/
|
|
1821
|
+
onCommitLatency(listener) {
|
|
1822
|
+
return observeCommitLatency(this.mutationQueue, listener);
|
|
1764
1823
|
}
|
|
1765
1824
|
/**
|
|
1766
|
-
* Subscribe to local transaction creation with the full {@link
|
|
1825
|
+
* Subscribe to local transaction creation with the full {@link QueuedMutation}
|
|
1767
1826
|
* payload (`type`, `modelName`, `modelId`, `data`, `previousData`). This is
|
|
1768
1827
|
* the feed the store's local-mutation subscription taps for undo recording.
|
|
1769
1828
|
*
|
|
1770
|
-
* It subscribes to the {@link
|
|
1829
|
+
* It subscribes to the {@link MutationQueue}'s emitter directly, since
|
|
1771
1830
|
* that is the only emitter that fires `transaction:created`. The SyncClient's
|
|
1772
1831
|
* own emitter (reached through {@link subscribe}) never rebroadcasts that
|
|
1773
1832
|
* event, so routing undo through `subscribe('transaction:created')` would
|
|
@@ -1775,7 +1834,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1775
1834
|
* reason.
|
|
1776
1835
|
*/
|
|
1777
1836
|
onLocalTransaction(listener) {
|
|
1778
|
-
this.
|
|
1837
|
+
this.mutationQueue.on('transaction:created', listener);
|
|
1779
1838
|
const snapshotsByCommit = new Map();
|
|
1780
1839
|
const onCommitStaging = (payload) => {
|
|
1781
1840
|
snapshotsByCommit.set(payload.clientTxId, payload.operations.map((operation) => {
|
|
@@ -1843,14 +1902,14 @@ export class SyncClient extends EventEmitter {
|
|
|
1843
1902
|
});
|
|
1844
1903
|
});
|
|
1845
1904
|
};
|
|
1846
|
-
this.
|
|
1847
|
-
this.
|
|
1848
|
-
this.
|
|
1905
|
+
this.mutationQueue.on('commit:staging', onCommitStaging);
|
|
1906
|
+
this.mutationQueue.on('commit:seal_failed', onCommitSealFailed);
|
|
1907
|
+
this.mutationQueue.on('commit:created', onCommitCreated);
|
|
1849
1908
|
return () => {
|
|
1850
|
-
this.
|
|
1851
|
-
this.
|
|
1852
|
-
this.
|
|
1853
|
-
this.
|
|
1909
|
+
this.mutationQueue.off('transaction:created', listener);
|
|
1910
|
+
this.mutationQueue.off('commit:staging', onCommitStaging);
|
|
1911
|
+
this.mutationQueue.off('commit:seal_failed', onCommitSealFailed);
|
|
1912
|
+
this.mutationQueue.off('commit:created', onCommitCreated);
|
|
1854
1913
|
snapshotsByCommit.clear();
|
|
1855
1914
|
};
|
|
1856
1915
|
}
|
|
@@ -1858,14 +1917,14 @@ export class SyncClient extends EventEmitter {
|
|
|
1858
1917
|
* Wait for the latest in-flight transaction for (modelName, modelId)
|
|
1859
1918
|
* to be confirmed by the server, or reject if it's rolled back.
|
|
1860
1919
|
* Resolves immediately when no transaction is in flight — see
|
|
1861
|
-
* `
|
|
1920
|
+
* `MutationQueue.confirmationFor` for the lookup contract.
|
|
1862
1921
|
*
|
|
1863
1922
|
* Distinct from `waitForDeltaConfirmation(transactionId)` which keys
|
|
1864
1923
|
* off a known tx id; this variant is for call sites that hold a
|
|
1865
1924
|
* Model reference but never see the underlying transaction.
|
|
1866
1925
|
*/
|
|
1867
1926
|
waitForConfirmation(modelName, modelId) {
|
|
1868
|
-
return this.
|
|
1927
|
+
return this.mutationQueue.confirmationFor(modelName, modelId);
|
|
1869
1928
|
}
|
|
1870
1929
|
/**
|
|
1871
1930
|
* Get detailed debug info for the sync debug page
|
|
@@ -1874,7 +1933,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1874
1933
|
return {
|
|
1875
1934
|
connectionState: this.connectionState,
|
|
1876
1935
|
pendingMutationsCount: this.pendingMutations.length,
|
|
1877
|
-
|
|
1936
|
+
mutationQueue: this.mutationQueue.getDebugInfo(),
|
|
1878
1937
|
};
|
|
1879
1938
|
}
|
|
1880
1939
|
// --- Best-practice assignment ops ---
|
|
@@ -1900,7 +1959,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1900
1959
|
* Mark a local transaction as optimistically applied. The matching
|
|
1901
1960
|
* server delta (when it arrives with the same `transactionId`) will
|
|
1902
1961
|
* be recognized as an echo and skip the pool mutation. Called
|
|
1903
|
-
* automatically by `
|
|
1962
|
+
* automatically by `MutationQueue` when a transaction is staged;
|
|
1904
1963
|
* exposed publicly so tests can drive the API directly.
|
|
1905
1964
|
*/
|
|
1906
1965
|
markTransactionPending(transactionId) {
|
|
@@ -1916,15 +1975,15 @@ export class SyncClient extends EventEmitter {
|
|
|
1916
1975
|
return this.echoTracker.getMetrics();
|
|
1917
1976
|
}
|
|
1918
1977
|
/**
|
|
1919
|
-
* Package-internal accessor for the {@link
|
|
1978
|
+
* Package-internal accessor for the {@link MutationQueue}. Used by
|
|
1920
1979
|
* `Ablo.commits.create()` to route raw multi-operation envelopes through the
|
|
1921
1980
|
* same retry-on-reconnect lane as the model proxy path, and by tests to
|
|
1922
1981
|
* exercise the queue's interaction with {@link markTransactionPending} on the
|
|
1923
1982
|
* real instance the SyncClient subscribes to. It is not re-exported to SDK
|
|
1924
1983
|
* consumers; `Ablo` is the public surface.
|
|
1925
1984
|
*/
|
|
1926
|
-
|
|
1927
|
-
return this.
|
|
1985
|
+
getMutationQueue() {
|
|
1986
|
+
return this.mutationQueue;
|
|
1928
1987
|
}
|
|
1929
1988
|
applyDeltaBatchToPool(dbResults, enrichRelations) {
|
|
1930
1989
|
const modelsToAdd = [];
|
|
@@ -1932,8 +1991,8 @@ export class SyncClient extends EventEmitter {
|
|
|
1932
1991
|
const idsToRemove = [];
|
|
1933
1992
|
const idsToArchive = [];
|
|
1934
1993
|
// Pre-pass: collect every id slated for `remove` in this batch. The
|
|
1935
|
-
//
|
|
1936
|
-
// user themself) deletes a
|
|
1994
|
+
// parent-delete flicker came from this exact pattern: a peer (or the
|
|
1995
|
+
// user themself) deletes a parent with N children; the commit produces
|
|
1937
1996
|
// BOTH residual `update` deltas (from the optimistic edits that
|
|
1938
1997
|
// happened just before the delete) AND `remove` deltas. The
|
|
1939
1998
|
// `update` branch below would `createFromData` the row back into
|
|
@@ -1962,7 +2021,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1962
2021
|
}
|
|
1963
2022
|
// If a later op in this batch will remove this id, skip earlier
|
|
1964
2023
|
// add/update ops on it. Server FK ordering can produce
|
|
1965
|
-
// U(
|
|
2024
|
+
// U(child)+D(child) when an optimistic edit and a delete both
|
|
1966
2025
|
// commit in the same window; only the final state matters.
|
|
1967
2026
|
if ((action === 'add' || action === 'update') && idsBeingRemoved.has(modelId)) {
|
|
1968
2027
|
continue;
|
|
@@ -2015,7 +2074,7 @@ export class SyncClient extends EventEmitter {
|
|
|
2015
2074
|
// fall-through.
|
|
2016
2075
|
// Self-healing: the next catch-up poll / reconnect re-fetches and
|
|
2017
2076
|
// re-applies this delta, so it's forensic, not consumer-actionable → debug.
|
|
2018
|
-
|
|
2077
|
+
this.runtime.logger.debug('[SyncClient.applyDeltaBatchToPool] skipping pool op for unpersisted delta', {
|
|
2019
2078
|
modelName,
|
|
2020
2079
|
modelId: modelId.slice(0, 12),
|
|
2021
2080
|
});
|