@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/BaseSyncedStore.js
CHANGED
|
@@ -11,17 +11,18 @@
|
|
|
11
11
|
* in-memory models, and {@link ModelRegistry} holds their metadata.
|
|
12
12
|
*/
|
|
13
13
|
import { makeObservable, observable, action, computed, runInAction } from 'mobx';
|
|
14
|
-
import { AbloConnectionError, AbloValidationError, toAbloError } from './errors.js';
|
|
14
|
+
import { AbloConnectionError, AbloValidationError, toAbloError } from './transaction/errors.js';
|
|
15
15
|
import { ConnectionManager } from './sync/ConnectionManager.js';
|
|
16
|
+
import { contextLogger, contextSocketObservability } from './sync/contextPorts.js';
|
|
16
17
|
import { SubscriptionManager } from './sync/SubscriptionManager.js';
|
|
17
18
|
import { resolveParticipantSyncGroups, } from './sync/participants.js';
|
|
18
19
|
import { ModelRegistry } from './ModelRegistry.js';
|
|
19
|
-
import { PropertyType } from './types/index.js';
|
|
20
|
+
import { PropertyType } from './transaction/types/index.js';
|
|
20
21
|
import { SyncWebSocket, } from './sync/SyncWebSocket.js';
|
|
21
|
-
import { QueryProcessor } from './
|
|
22
|
+
import { QueryProcessor } from './query/QueryProcessor.js';
|
|
22
23
|
import { Model, rowAsModel } from './Model.js';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
24
|
+
import { globalRuntime } from './context.js';
|
|
25
|
+
import { AbloSessionError, isAccessCredentialExpiryCloseReason } from './transaction/errors.js';
|
|
25
26
|
import { ModelScope } from './InstanceCache.js';
|
|
26
27
|
import { LazyReferenceCollection } from './LazyReferenceCollection.js';
|
|
27
28
|
import { deriveSyncPlanFromSchema } from './sync/syncPlan.js';
|
|
@@ -29,9 +30,15 @@ import { CredentialLifecycle } from './sync/credentialLifecycle.js';
|
|
|
29
30
|
import * as groupChange from './sync/groupChange.js';
|
|
30
31
|
import * as bootstrapApply from './sync/bootstrapApply.js';
|
|
31
32
|
import * as deltaPipeline from './sync/deltaPipeline.js';
|
|
32
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Bootstrap retry configuration.
|
|
35
|
+
*
|
|
36
|
+
* There is deliberately no overall timeout here. How long one attempt may run
|
|
37
|
+
* is not a policy this layer gets to invent — it is a property of the fetcher's
|
|
38
|
+
* watchdogs, read from `BootstrapFetcher.budgetMs`. A second number kept here
|
|
39
|
+
* would only be able to disagree with them, which is exactly what it used to do.
|
|
40
|
+
*/
|
|
33
41
|
export const BOOTSTRAP_CONFIG = {
|
|
34
|
-
OVERALL_TIMEOUT_MS: 15_000,
|
|
35
42
|
MAX_RETRY_ATTEMPTS: 3,
|
|
36
43
|
RETRY_DELAY_MS: 500,
|
|
37
44
|
};
|
|
@@ -58,13 +65,13 @@ export { deriveSyncPlanFromSchema } from './sync/syncPlan.js';
|
|
|
58
65
|
* the underlying SyncWebSocket without casts:
|
|
59
66
|
*
|
|
60
67
|
* @example
|
|
61
|
-
* interface
|
|
62
|
-
* '
|
|
63
|
-
* '
|
|
68
|
+
* interface EditorEvents {
|
|
69
|
+
* 'document:selection': [SelectionEvent];
|
|
70
|
+
* 'document:cursor': [CursorEvent];
|
|
64
71
|
* }
|
|
65
|
-
* class
|
|
66
|
-
*
|
|
67
|
-
* return this.syncWebSocket
|
|
72
|
+
* class EditorStore extends BaseSyncedStore<EditorEvents> {
|
|
73
|
+
* subscribeToCursor(handler: (e: CursorEvent) => void) {
|
|
74
|
+
* return this.syncWebSocket.subscribe('document:cursor', handler);
|
|
68
75
|
* }
|
|
69
76
|
* }
|
|
70
77
|
*/
|
|
@@ -77,6 +84,10 @@ export class BaseSyncedStore {
|
|
|
77
84
|
isSessionError: false,
|
|
78
85
|
};
|
|
79
86
|
// ── Injected dependencies ──
|
|
87
|
+
/** The owning client's runtime; the module-global bridge when constructed directly. */
|
|
88
|
+
runtime;
|
|
89
|
+
/** The installed plugins the delta pipeline dispatches stage handlers to. */
|
|
90
|
+
stagePlugins;
|
|
80
91
|
syncClient;
|
|
81
92
|
database;
|
|
82
93
|
objectPool;
|
|
@@ -88,15 +99,20 @@ export class BaseSyncedStore {
|
|
|
88
99
|
*/
|
|
89
100
|
schema;
|
|
90
101
|
// ── Real-time sync ──
|
|
91
|
-
|
|
102
|
+
/**
|
|
103
|
+
* The connection, owned by whoever built this store (ADR 0016 follow-up
|
|
104
|
+
* 3b): the host constructs it and hands it in, the store seeds its late
|
|
105
|
+
* values during `initialize()` and owns the lifecycle from there. One
|
|
106
|
+
* instance for the store's whole lifetime — reconnects replace the socket
|
|
107
|
+
* inside it, never the object.
|
|
108
|
+
*/
|
|
109
|
+
syncWebSocket;
|
|
92
110
|
/**
|
|
93
111
|
* Dynamic read interest (area-of-interest) over the connection's sync
|
|
94
|
-
* groups.
|
|
95
|
-
*
|
|
96
|
-
* to whichever instance is current, so callers (the React participant
|
|
97
|
-
* hook) never hold a stale reference. Null until `setupWebSocketSync`.
|
|
112
|
+
* groups. Constructed with the connection; the permanent base scopes are
|
|
113
|
+
* seeded in `setupWebSocketSync` once identity resolves.
|
|
98
114
|
*/
|
|
99
|
-
areaOfInterest
|
|
115
|
+
areaOfInterest;
|
|
100
116
|
/** Sync groups whose current state has been backfilled into the pool
|
|
101
117
|
* (hydrate-on-enter). Cleared when the pool is reset on (re)bootstrap. */
|
|
102
118
|
hydratedGroups = new Set();
|
|
@@ -104,27 +120,50 @@ export class BaseSyncedStore {
|
|
|
104
120
|
* enters of the same scope so they share one fetch. */
|
|
105
121
|
hydratingGroups = new Map();
|
|
106
122
|
_syncServerUrl;
|
|
123
|
+
/** Application-declared collaboration event types; empty unless configured. */
|
|
124
|
+
_collaborationEvents = [];
|
|
107
125
|
/**
|
|
108
126
|
* Public accessor for the underlying SyncWebSocket. Used by the
|
|
109
127
|
* factory in `createSyncEngine` to wire the default mutation
|
|
110
128
|
* executor — the executor needs the WS handle to send commit
|
|
111
129
|
* frames, and the factory can't reach `protected` state through
|
|
112
|
-
* normal typing.
|
|
113
|
-
* `initialize()`.
|
|
130
|
+
* normal typing.
|
|
114
131
|
*/
|
|
115
132
|
getSyncWebSocket() {
|
|
116
133
|
return this.syncWebSocket;
|
|
117
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Subscribe to pushed frames — deltas, presence updates, claim grants and
|
|
137
|
+
* losses, connection changes, and this store's collaboration events.
|
|
138
|
+
* Durable by construction: the connection object exists for the store's
|
|
139
|
+
* whole lifetime (reconnects replace only the socket inside it), so a
|
|
140
|
+
* subscription made before the first connect starts delivering when the
|
|
141
|
+
* socket opens and keeps delivering across every reconnect. Returns the
|
|
142
|
+
* unsubscribe function.
|
|
143
|
+
*/
|
|
144
|
+
subscribe(event, handler) {
|
|
145
|
+
return this.syncWebSocket.subscribe(event, handler);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Send a collaboration event (an app-specific real-time message from this
|
|
149
|
+
* store's `TCollaboration` map). A no-op while the connection is down —
|
|
150
|
+
* presence-grade traffic is not queued.
|
|
151
|
+
*/
|
|
152
|
+
sendCollaborationEvent(messageType, payload) {
|
|
153
|
+
this.syncWebSocket.sendCollaborationEvent(messageType, payload);
|
|
154
|
+
}
|
|
118
155
|
// ── Area-of-interest (dynamic read subscription) ─────────────────
|
|
119
156
|
//
|
|
120
157
|
// `enterScope`/`leaveScope` move the connection's read interest as the
|
|
121
|
-
// user navigates (open or close a
|
|
158
|
+
// user navigates (open or close a record); `pinScope`/`unpinScope`
|
|
122
159
|
// express prominence (an active claim keeps a group subscribed). All four
|
|
123
160
|
// resolve the scope to sync-group strings through the same resolver the
|
|
124
161
|
// claim path uses (`resolveParticipantSyncGroups`), so read interest and
|
|
125
|
-
// write claims always agree on the string for a given entity.
|
|
126
|
-
//
|
|
127
|
-
// is offline (see
|
|
162
|
+
// write claims always agree on the string for a given entity. Before the
|
|
163
|
+
// connection opens they record interest without a wire send, and they
|
|
164
|
+
// never reject when the transport is offline (see
|
|
165
|
+
// {@link SubscriptionManager.reconcile}); the on-connect `resync` pushes
|
|
166
|
+
// whatever interest accumulated.
|
|
128
167
|
scopeToGroups(scope) {
|
|
129
168
|
return resolveParticipantSyncGroups(scope, this.schema);
|
|
130
169
|
}
|
|
@@ -137,11 +176,8 @@ export class BaseSyncedStore {
|
|
|
137
176
|
* and the live delta stream keeps flowing regardless.
|
|
138
177
|
*/
|
|
139
178
|
enterScope(scope, opts) {
|
|
140
|
-
const mgr = this.areaOfInterest;
|
|
141
|
-
if (!mgr)
|
|
142
|
-
return Promise.resolve();
|
|
143
179
|
const groups = this.scopeToGroups(scope);
|
|
144
|
-
const subscribed = Promise.all(groups.map((g) =>
|
|
180
|
+
const subscribed = Promise.all(groups.map((g) => this.areaOfInterest.enter(g))).then(() => undefined);
|
|
145
181
|
if (!opts?.hydrate)
|
|
146
182
|
return subscribed;
|
|
147
183
|
return subscribed.then(() => this.hydrateGroups(groups));
|
|
@@ -171,7 +207,7 @@ export class BaseSyncedStore {
|
|
|
171
207
|
this.hydratedGroups.add(g);
|
|
172
208
|
}
|
|
173
209
|
catch (err) {
|
|
174
|
-
|
|
210
|
+
this.runtime.logger.debug('[BaseSyncedStore] scoped hydrate failed', {
|
|
175
211
|
syncGroups: need,
|
|
176
212
|
error: err instanceof Error ? err.message : String(err),
|
|
177
213
|
});
|
|
@@ -188,24 +224,15 @@ export class BaseSyncedStore {
|
|
|
188
224
|
}
|
|
189
225
|
/** Leave a scope → its groups go warm (hysteresis), then drop on sweep. */
|
|
190
226
|
leaveScope(scope) {
|
|
191
|
-
|
|
192
|
-
if (!mgr)
|
|
193
|
-
return Promise.resolve();
|
|
194
|
-
return Promise.all(this.scopeToGroups(scope).map((g) => mgr.leave(g))).then(() => undefined);
|
|
227
|
+
return Promise.all(this.scopeToGroups(scope).map((g) => this.areaOfInterest.leave(g))).then(() => undefined);
|
|
195
228
|
}
|
|
196
229
|
/** Pin a scope (active claim / prominence) → never warms while pinned. */
|
|
197
230
|
pinScope(scope) {
|
|
198
|
-
|
|
199
|
-
if (!mgr)
|
|
200
|
-
return Promise.resolve();
|
|
201
|
-
return Promise.all(this.scopeToGroups(scope).map((g) => mgr.pin(g))).then(() => undefined);
|
|
231
|
+
return Promise.all(this.scopeToGroups(scope).map((g) => this.areaOfInterest.pin(g))).then(() => undefined);
|
|
202
232
|
}
|
|
203
233
|
/** Release a pin → the group transitions to warm rather than dropping. */
|
|
204
234
|
unpinScope(scope) {
|
|
205
|
-
|
|
206
|
-
if (!mgr)
|
|
207
|
-
return Promise.resolve();
|
|
208
|
-
return Promise.all(this.scopeToGroups(scope).map((g) => mgr.unpin(g))).then(() => undefined);
|
|
235
|
+
return Promise.all(this.scopeToGroups(scope).map((g) => this.areaOfInterest.unpin(g))).then(() => undefined);
|
|
209
236
|
}
|
|
210
237
|
// ── Internal helpers ──
|
|
211
238
|
queryProcessor;
|
|
@@ -237,24 +264,28 @@ export class BaseSyncedStore {
|
|
|
237
264
|
pendingDeltas = [];
|
|
238
265
|
batchTimer = null;
|
|
239
266
|
syncPromise = null;
|
|
240
|
-
/** Resume/ack cursor — delegates to the shared
|
|
241
|
-
*
|
|
267
|
+
/** Resume/ack cursor — delegates to the shared LogPosition (see
|
|
268
|
+
* logPosition.ts). Advances only after IDB persistence. */
|
|
242
269
|
get lastAckedId() {
|
|
243
270
|
return this.syncClient.position.persisted;
|
|
244
271
|
}
|
|
245
|
-
/** Pool-applied cursor — delegates to the shared
|
|
272
|
+
/** Pool-applied cursor — delegates to the shared LogPosition. */
|
|
246
273
|
get highestProcessedSyncId() {
|
|
247
274
|
return this.syncClient.position.applied;
|
|
248
275
|
}
|
|
249
276
|
// ── Delta queuing during bootstrap ──
|
|
250
277
|
bootstrapDeltaQueue = null;
|
|
251
278
|
activeBootstrapCount = 0;
|
|
279
|
+
/** The live deadline for the bootstrap attempt in flight, if any. */
|
|
280
|
+
bootstrapDeadlineTimer = null;
|
|
252
281
|
// ── Delete tracking ──
|
|
253
282
|
pendingDeletes = new Set();
|
|
254
283
|
// ── Model type hydration ──
|
|
255
284
|
modelTypesHydrated = new Set();
|
|
256
285
|
modelTypeHydrationInFlight = new Map();
|
|
257
286
|
constructor(dependencies, config = {}) {
|
|
287
|
+
this.runtime = dependencies.runtime ?? globalRuntime;
|
|
288
|
+
this.stagePlugins = dependencies.stagePlugins ?? [];
|
|
258
289
|
this.syncClient = dependencies.syncClient;
|
|
259
290
|
this.database = dependencies.database;
|
|
260
291
|
this.objectPool = dependencies.objectPool;
|
|
@@ -262,6 +293,30 @@ export class BaseSyncedStore {
|
|
|
262
293
|
this.auth = dependencies.auth;
|
|
263
294
|
this.schema = dependencies.schema;
|
|
264
295
|
this._syncServerUrl = dependencies.url;
|
|
296
|
+
this._collaborationEvents = config.collaborationEvents ?? [];
|
|
297
|
+
// The connection exists from construction (ADR 0016 follow-up 3b): the
|
|
298
|
+
// host hands one in, or the store builds its own. `deferConnect` holds
|
|
299
|
+
// it closed until `initialize()` has seeded identity and read scope, so
|
|
300
|
+
// nothing can open an unscoped connection in between.
|
|
301
|
+
this.syncWebSocket =
|
|
302
|
+
dependencies.syncWebSocket ??
|
|
303
|
+
new SyncWebSocket({
|
|
304
|
+
baseUrl: this._syncServerUrl,
|
|
305
|
+
collaborationEvents: [...this._collaborationEvents],
|
|
306
|
+
getAuthToken: this.auth?.getAuthToken,
|
|
307
|
+
deferConnect: true,
|
|
308
|
+
capabilities: {
|
|
309
|
+
partialBootstrap: true,
|
|
310
|
+
compressedDeltas: true,
|
|
311
|
+
streamingBootstrap: true,
|
|
312
|
+
batchedDeltas: true,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
this.areaOfInterest = new SubscriptionManager({ transport: this.syncWebSocket });
|
|
316
|
+
this.wireSocketEvents();
|
|
317
|
+
// QueuedMutation events for pendingChanges tracking — connection-
|
|
318
|
+
// independent, wired once for the store's lifetime.
|
|
319
|
+
this.disposers.push(this.syncClient.onTransactionEvent('created', () => { this.incrementPendingChanges(); }), this.syncClient.onTransactionEvent('completed', () => { this.decrementPendingChanges(); }), this.syncClient.onTransactionEvent('failed', () => { this.decrementPendingChanges(); }));
|
|
265
320
|
// Set this store as the global Model store
|
|
266
321
|
Model.setStore(this);
|
|
267
322
|
// ── Schema-derived sync plan ───────────────────────────────────────
|
|
@@ -414,7 +469,7 @@ export class BaseSyncedStore {
|
|
|
414
469
|
reportSessionExpired: () => {
|
|
415
470
|
this.connectionManager?.send({ type: 'BOOTSTRAP_FAILED_SESSION' });
|
|
416
471
|
},
|
|
417
|
-
});
|
|
472
|
+
}, contextLogger);
|
|
418
473
|
/**
|
|
419
474
|
* Listeners registered via `subscribeSessionError()`. Fired when the
|
|
420
475
|
* WebSocket closes with a session-invalid code (1008/4001/4003) or a
|
|
@@ -435,7 +490,7 @@ export class BaseSyncedStore {
|
|
|
435
490
|
}
|
|
436
491
|
/**
|
|
437
492
|
* Subscribe to per-mutation failure payloads. Forwarded from the
|
|
438
|
-
* underlying `SyncClient.
|
|
493
|
+
* underlying `SyncClient.mutationQueue` so consumers (toast layer,
|
|
439
494
|
* route-level reverted boundaries, telemetry) can react without
|
|
440
495
|
* reaching across the store. Returns an unsubscribe function.
|
|
441
496
|
*
|
|
@@ -448,6 +503,15 @@ export class BaseSyncedStore {
|
|
|
448
503
|
subscribeMutationFailure(listener) {
|
|
449
504
|
return this.syncClient.onMutationFailure(listener);
|
|
450
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* Subscribe to commit round-trip latency. Forwarded from the underlying
|
|
508
|
+
* `SyncClient` for the same reason as `subscribeMutationFailure` — the
|
|
509
|
+
* React provider binds against this surface, so the engine's wiring stays
|
|
510
|
+
* private while the SDK keeps one hook to expose.
|
|
511
|
+
*/
|
|
512
|
+
subscribeCommitLatency(listener) {
|
|
513
|
+
return this.syncClient.onCommitLatency(listener);
|
|
514
|
+
}
|
|
451
515
|
/**
|
|
452
516
|
* Wait for the in-flight transaction for (modelName, modelId) to be
|
|
453
517
|
* confirmed by the server. See `SyncClient.waitForConfirmation` for the
|
|
@@ -458,20 +522,20 @@ export class BaseSyncedStore {
|
|
|
458
522
|
}
|
|
459
523
|
/**
|
|
460
524
|
* Observe the LOCAL mutation stream for undo recording (see
|
|
461
|
-
* {@link import('./
|
|
462
|
-
*
|
|
525
|
+
* {@link import('./storeContract.js').LocalMutation}). Taps the
|
|
526
|
+
* MutationQueue's `transaction:created` event — fired once per local
|
|
463
527
|
* create/update/delete/archive with `previousData` already captured.
|
|
464
528
|
* Remote/collaborator deltas apply via `applyDeltaBatchToPool` and never
|
|
465
529
|
* emit here, so undo is naturally local-only (you can't undo a teammate).
|
|
466
530
|
*/
|
|
467
531
|
subscribeLocalMutations(handler) {
|
|
468
|
-
// Tap the
|
|
532
|
+
// Tap the MutationQueue directly via `onLocalTransaction`. The previous
|
|
469
533
|
// `syncClient.subscribe('transaction:created', …)` route registered the
|
|
470
534
|
// handler on SyncClient's OWN emitter, which never fires that event (only
|
|
471
535
|
// the queue's emitter does) — so undo recorded nothing. See
|
|
472
536
|
// `SyncClient.onLocalTransaction` for the full rationale.
|
|
473
537
|
return this.syncClient.onLocalTransaction((tx) => {
|
|
474
|
-
if (!tx
|
|
538
|
+
if (!tx.modelName || !tx.modelId)
|
|
475
539
|
return;
|
|
476
540
|
handler({
|
|
477
541
|
type: tx.type,
|
|
@@ -489,69 +553,113 @@ export class BaseSyncedStore {
|
|
|
489
553
|
*/
|
|
490
554
|
async executeBootstrapWithTimeout(bootstrapFn, _context, signal) {
|
|
491
555
|
let lastError = null;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if (isAbort)
|
|
522
|
-
throw error;
|
|
523
|
-
if (SyncSessionError.isSessionError(error))
|
|
524
|
-
throw error;
|
|
525
|
-
if (isNetworkError && typeof navigator !== 'undefined' && navigator.onLine === false) {
|
|
526
|
-
getContext().observability.captureBootstrapFailure(error, { type: 'network-offline' });
|
|
527
|
-
throw error;
|
|
556
|
+
// An aborted initialize has to stop the transfer, not just stop waiting for
|
|
557
|
+
// it. Without this the caller returns while a cold start keeps downloading,
|
|
558
|
+
// and those chunks are still in flight when the next initialize begins.
|
|
559
|
+
const onCallerAbort = () => { this.database.helper.abort(); };
|
|
560
|
+
signal?.addEventListener('abort', onCallerAbort, { once: true });
|
|
561
|
+
try {
|
|
562
|
+
for (let attempt = 1; attempt <= BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS; attempt++) {
|
|
563
|
+
if (signal?.aborted) {
|
|
564
|
+
throw new DOMException('Initialization aborted', 'AbortError');
|
|
565
|
+
}
|
|
566
|
+
// `navigator.onLine === false` is the MDN-reliable "definitely
|
|
567
|
+
// offline" signal. Don't use `!navigator.onLine`: Node 22+ exposes
|
|
568
|
+
// `globalThis.navigator` with `onLine === undefined`, so the
|
|
569
|
+
// negation false-positives every server-side bootstrap (e.g. the
|
|
570
|
+
// server-side agent.run dispatch path through `connectAgent`).
|
|
571
|
+
if (typeof navigator !== 'undefined' && navigator.onLine === false) {
|
|
572
|
+
this.runtime.observability.breadcrumb(`Bootstrap attempt ${attempt} skipped - offline`, 'sync.bootstrap', 'warning');
|
|
573
|
+
throw new AbloConnectionError('Bootstrap skipped - device is offline', {
|
|
574
|
+
code: 'bootstrap_offline',
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
this.runtime.logger.info(`[BaseSyncedStore] Bootstrap attempt ${attempt}/${BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS}`);
|
|
579
|
+
const result = (await Promise.race([
|
|
580
|
+
bootstrapFn(),
|
|
581
|
+
this.createBootstrapTimeout(attempt),
|
|
582
|
+
]));
|
|
583
|
+
this.runtime.logger.info('[BaseSyncedStore] Bootstrap completed successfully', { attempt });
|
|
584
|
+
return result;
|
|
528
585
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
586
|
+
catch (error) {
|
|
587
|
+
lastError = error;
|
|
588
|
+
const isTimeout = error instanceof Error && error.message.includes('timed out');
|
|
589
|
+
const isAbort = error instanceof DOMException && error.name === 'AbortError';
|
|
590
|
+
const isNetworkError = error instanceof TypeError && error.message.includes('fetch');
|
|
591
|
+
if (isAbort)
|
|
592
|
+
throw error;
|
|
593
|
+
if (AbloSessionError.isSessionError(error))
|
|
594
|
+
throw error;
|
|
595
|
+
if (isNetworkError && typeof navigator !== 'undefined' && navigator.onLine === false) {
|
|
596
|
+
this.runtime.observability.captureBootstrapFailure(error, { type: 'network-offline' });
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
599
|
+
this.runtime.observability.breadcrumb(`Bootstrap attempt ${attempt} failed`, 'sync.bootstrap', 'warning', { isTimeout, isNetworkError, willRetry: attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS });
|
|
600
|
+
if (isTimeout && attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS) {
|
|
601
|
+
this.runtime.logger.info('[BaseSyncedStore] Resetting state before bootstrap retry');
|
|
602
|
+
this.resetBootstrapState();
|
|
603
|
+
await new Promise((resolve) => setTimeout(resolve, BOOTSTRAP_CONFIG.RETRY_DELAY_MS));
|
|
604
|
+
}
|
|
605
|
+
else if (!isTimeout && attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS) {
|
|
606
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
607
|
+
}
|
|
534
608
|
}
|
|
535
|
-
|
|
536
|
-
|
|
609
|
+
finally {
|
|
610
|
+
// Disarm this attempt's deadline the moment it settles — a live timer
|
|
611
|
+
// would abort whatever the next attempt puts in flight.
|
|
612
|
+
this.clearBootstrapDeadline();
|
|
537
613
|
}
|
|
538
614
|
}
|
|
615
|
+
throw lastError
|
|
616
|
+
? toAbloError(lastError)
|
|
617
|
+
: new AbloConnectionError('Bootstrap failed after all retry attempts', {
|
|
618
|
+
code: 'bootstrap_fetch_timeout',
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
finally {
|
|
622
|
+
signal?.removeEventListener('abort', onCallerAbort);
|
|
623
|
+
this.clearBootstrapDeadline();
|
|
539
624
|
}
|
|
540
|
-
throw lastError
|
|
541
|
-
? toAbloError(lastError)
|
|
542
|
-
: new AbloConnectionError('Bootstrap failed after all retry attempts', {
|
|
543
|
-
code: 'bootstrap_fetch_timeout',
|
|
544
|
-
});
|
|
545
625
|
}
|
|
546
|
-
/**
|
|
626
|
+
/**
|
|
627
|
+
* The outer deadline for one bootstrap attempt.
|
|
628
|
+
*
|
|
629
|
+
* The length is DERIVED from the fetcher's own watchdog budget, not chosen. A
|
|
630
|
+
* chosen number is what broke this: the previous fixed 15s was shorter than a
|
|
631
|
+
* single model chunk's allowance — 20s waiting for response headers plus 15s
|
|
632
|
+
* of stall grace — so on any workspace with one slow model the deadline fired
|
|
633
|
+
* before the watchdogs it was meant to backstop, and every attempt timed out
|
|
634
|
+
* by construction. The watchdogs below are progress-based and already
|
|
635
|
+
* guarantee termination; this deadline exists only for a hang somewhere other
|
|
636
|
+
* than the network, so it must sit above them, and it can only do that
|
|
637
|
+
* reliably by asking them how long they take.
|
|
638
|
+
*
|
|
639
|
+
* Reaching it aborts the work in flight. `Promise.race` merely stops waiting:
|
|
640
|
+
* without the abort the losing bootstrap keeps running, keeps its sockets, and
|
|
641
|
+
* races the retry that replaced it — which is how one page load turned into
|
|
642
|
+
* dozens of overlapping requests.
|
|
643
|
+
*/
|
|
547
644
|
createBootstrapTimeout(attempt) {
|
|
548
|
-
const timeoutMs =
|
|
645
|
+
const timeoutMs = this.database.helper.budgetMs;
|
|
549
646
|
return new Promise((_, reject) => {
|
|
550
|
-
|
|
647
|
+
this.clearBootstrapDeadline();
|
|
648
|
+
this.bootstrapDeadlineTimer = setTimeout(() => {
|
|
649
|
+
this.database.helper.abort();
|
|
551
650
|
reject(new AbloConnectionError(`Bootstrap timed out after ${timeoutMs}ms (attempt ${attempt})`, { code: 'bootstrap_fetch_timeout' }));
|
|
552
651
|
}, timeoutMs);
|
|
553
652
|
});
|
|
554
653
|
}
|
|
654
|
+
/** Disarm the deadline once its attempt has settled. Load-bearing now that
|
|
655
|
+
* firing it aborts real work: a leftover timer would cancel a later,
|
|
656
|
+
* unrelated bootstrap. */
|
|
657
|
+
clearBootstrapDeadline() {
|
|
658
|
+
if (this.bootstrapDeadlineTimer !== null) {
|
|
659
|
+
clearTimeout(this.bootstrapDeadlineTimer);
|
|
660
|
+
this.bootstrapDeadlineTimer = null;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
555
663
|
/** Reset bootstrap-related state for a clean retry */
|
|
556
664
|
resetBootstrapState() {
|
|
557
665
|
try {
|
|
@@ -564,10 +672,10 @@ export class BaseSyncedStore {
|
|
|
564
672
|
// is stale — clear it so re-entered groups backfill again.
|
|
565
673
|
this.hydratedGroups.clear();
|
|
566
674
|
this.hydratingGroups.clear();
|
|
567
|
-
|
|
675
|
+
this.runtime.logger.info('[BaseSyncedStore] Bootstrap state reset complete');
|
|
568
676
|
}
|
|
569
677
|
catch {
|
|
570
|
-
|
|
678
|
+
this.runtime.observability.breadcrumb('Error resetting bootstrap state', 'sync.bootstrap', 'warning');
|
|
571
679
|
}
|
|
572
680
|
}
|
|
573
681
|
// ── Reconnection ─────────────────────────────────────────────────────────
|
|
@@ -588,7 +696,7 @@ export class BaseSyncedStore {
|
|
|
588
696
|
await this.syncClient.hydrateFromDatabase();
|
|
589
697
|
this.dataReady = true;
|
|
590
698
|
}
|
|
591
|
-
if (
|
|
699
|
+
if (!this.syncWebSocket.isConnected()) {
|
|
592
700
|
this.syncWebSocket.resetReconnectAttempts();
|
|
593
701
|
this.syncWebSocket.connect();
|
|
594
702
|
}
|
|
@@ -596,10 +704,10 @@ export class BaseSyncedStore {
|
|
|
596
704
|
return 'success';
|
|
597
705
|
}
|
|
598
706
|
catch (error) {
|
|
599
|
-
|
|
600
|
-
if (
|
|
601
|
-
this.syncWebSocket
|
|
602
|
-
this.syncWebSocket
|
|
707
|
+
this.runtime.observability.captureBootstrapFailure(error, { type: 'connection-store-reconnect' });
|
|
708
|
+
if (AbloSessionError.isSessionError(error)) {
|
|
709
|
+
this.syncWebSocket.setSessionErrorDetected();
|
|
710
|
+
this.syncWebSocket.disconnect();
|
|
603
711
|
this.updateSyncStatus({ state: 'error', error: error });
|
|
604
712
|
// SECURITY: Clear locally cached data when session is invalid
|
|
605
713
|
this.database.clear({ includeWriteJournal: true }).catch(() => { });
|
|
@@ -609,15 +717,19 @@ export class BaseSyncedStore {
|
|
|
609
717
|
if (!this.dataReady && this.objectPool.size === 0) {
|
|
610
718
|
try {
|
|
611
719
|
await this.syncClient.hydrateFromDatabase();
|
|
612
|
-
|
|
720
|
+
// Re-read through a local: the guard above narrowed `size` to 0, and
|
|
721
|
+
// the compiler carries that narrowing across the await even though
|
|
722
|
+
// hydrating is precisely what fills the pool.
|
|
723
|
+
const hydratedSize = this.objectPool.size;
|
|
724
|
+
if (hydratedSize > 0) {
|
|
613
725
|
this.dataReady = true;
|
|
614
|
-
|
|
615
|
-
objectPoolSize:
|
|
726
|
+
this.runtime.logger.info('[BaseSyncedStore] Hydrated from local fallback', {
|
|
727
|
+
objectPoolSize: hydratedSize,
|
|
616
728
|
});
|
|
617
729
|
}
|
|
618
730
|
}
|
|
619
731
|
catch (fallbackError) {
|
|
620
|
-
|
|
732
|
+
this.runtime.logger.debug('[BaseSyncedStore] Local fallback failed', {
|
|
621
733
|
error: fallbackError.message,
|
|
622
734
|
});
|
|
623
735
|
}
|
|
@@ -686,12 +798,13 @@ export class BaseSyncedStore {
|
|
|
686
798
|
/** Narrow context the group-change leaf talks back through. */
|
|
687
799
|
groupChangeContext() {
|
|
688
800
|
return {
|
|
801
|
+
runtime: this.runtime,
|
|
689
802
|
database: this.database,
|
|
690
803
|
objectPool: this.objectPool,
|
|
691
|
-
getSubscribedSyncGroups: () => this.syncWebSocket
|
|
804
|
+
getSubscribedSyncGroups: () => this.syncWebSocket.getSyncGroups(),
|
|
692
805
|
getCurrentSyncGroups: () => this.userContext ? this.resolveSyncGroups(this.userContext) : null,
|
|
693
806
|
getBootstrapMode: () => this.userContext?.bootstrapMode,
|
|
694
|
-
disconnectWebSocket: () => { this.syncWebSocket
|
|
807
|
+
disconnectWebSocket: () => { this.syncWebSocket.disconnect(); },
|
|
695
808
|
emitConnectionEvent: (event) => { this.onConnectionEvent?.(event); },
|
|
696
809
|
handleGroupAdded: (payload, syncId) => this.handleGroupAdded(payload, syncId),
|
|
697
810
|
computeUpdatedSyncGroups: (payload) => this.computeUpdatedSyncGroups(payload),
|
|
@@ -755,6 +868,7 @@ export class BaseSyncedStore {
|
|
|
755
868
|
poolContext() {
|
|
756
869
|
const store = this;
|
|
757
870
|
return {
|
|
871
|
+
runtime: this.runtime,
|
|
758
872
|
applyDeltaBatchToPool: (results) => {
|
|
759
873
|
this.syncClient.applyDeltaBatchToPool(results, (name, data) => this.enrichRelations(name, data));
|
|
760
874
|
},
|
|
@@ -795,8 +909,8 @@ export class BaseSyncedStore {
|
|
|
795
909
|
hasLocalData = this.objectPool.size > 0;
|
|
796
910
|
}
|
|
797
911
|
catch (hydrateError) {
|
|
798
|
-
|
|
799
|
-
|
|
912
|
+
this.runtime.logger.debug('[sync-engine] IDB hydration failed', { error: hydrateError });
|
|
913
|
+
this.runtime.observability.captureBootstrapFailure(hydrateError, { type: 'hydration-from-idb' });
|
|
800
914
|
}
|
|
801
915
|
// Get sync baseline for WebSocket
|
|
802
916
|
const lastSyncId = (yield this.database.getLastSyncId());
|
|
@@ -819,7 +933,7 @@ export class BaseSyncedStore {
|
|
|
819
933
|
// delta flow works regardless of this branch.
|
|
820
934
|
const requirements = (yield this.database.requiredBootstrap());
|
|
821
935
|
if (context.bootstrapMode === 'none') {
|
|
822
|
-
|
|
936
|
+
this.runtime.logger.info('[BaseSyncedStore] Bootstrap skipped (bootstrapMode=none)', { kind: context.kind ?? 'user' });
|
|
823
937
|
// `setupWebSocketSync` above creates the SyncWebSocket and
|
|
824
938
|
// initiates the upgrade, but it does NOT await the 'connected'
|
|
825
939
|
// event — it returns synchronously after wiring listeners.
|
|
@@ -846,7 +960,7 @@ export class BaseSyncedStore {
|
|
|
846
960
|
// delta-queue replay in withDeltaQueuing's finally (and the error
|
|
847
961
|
// handler itself), which would otherwise vanish unhandled.
|
|
848
962
|
void this.performBackgroundBootstrap(requirements, context, signal).catch((error) => {
|
|
849
|
-
|
|
963
|
+
this.runtime.observability.captureBootstrapFailure(error, {
|
|
850
964
|
type: 'background-orchestration',
|
|
851
965
|
});
|
|
852
966
|
});
|
|
@@ -877,11 +991,11 @@ export class BaseSyncedStore {
|
|
|
877
991
|
this.updateSyncStatus({ state: 'idle', progress: 0 });
|
|
878
992
|
return { success: false, error: error };
|
|
879
993
|
}
|
|
880
|
-
const isSession =
|
|
881
|
-
|
|
994
|
+
const isSession = AbloSessionError.isSessionError(error);
|
|
995
|
+
this.runtime.observability.captureBootstrapFailure(error, { type: 'initialize' });
|
|
882
996
|
if (isSession) {
|
|
883
|
-
this.syncWebSocket
|
|
884
|
-
this.syncWebSocket
|
|
997
|
+
this.syncWebSocket.setSessionErrorDetected();
|
|
998
|
+
this.syncWebSocket.disconnect();
|
|
885
999
|
this.updateSyncStatus({ state: 'error', error: error });
|
|
886
1000
|
return { success: false, error: error };
|
|
887
1001
|
}
|
|
@@ -895,7 +1009,7 @@ export class BaseSyncedStore {
|
|
|
895
1009
|
if (this.objectPool.size > 0) {
|
|
896
1010
|
this.dataReady = true;
|
|
897
1011
|
this.initialized = true;
|
|
898
|
-
this.updateSyncStatus(this.syncWebSocket
|
|
1012
|
+
this.updateSyncStatus(this.syncWebSocket.isConnected()
|
|
899
1013
|
? { state: 'idle', progress: 100 }
|
|
900
1014
|
: { state: 'offline', offlineSince: new Date() });
|
|
901
1015
|
return { success: true };
|
|
@@ -915,17 +1029,17 @@ export class BaseSyncedStore {
|
|
|
915
1029
|
this.updateSyncStatus({ state: 'idle', progress: 100 });
|
|
916
1030
|
}
|
|
917
1031
|
catch (error) {
|
|
918
|
-
|
|
1032
|
+
this.runtime.logger.debug('[sync-engine] Background bootstrap failed', {
|
|
919
1033
|
error: error instanceof Error ? error.message : String(error),
|
|
920
1034
|
cause: error,
|
|
921
1035
|
});
|
|
922
|
-
|
|
923
|
-
if (
|
|
924
|
-
this.syncWebSocket
|
|
925
|
-
this.syncWebSocket
|
|
1036
|
+
this.runtime.observability.captureBootstrapFailure(error, { type: 'background' });
|
|
1037
|
+
if (AbloSessionError.isSessionError(error)) {
|
|
1038
|
+
this.syncWebSocket.setSessionErrorDetected();
|
|
1039
|
+
this.syncWebSocket.disconnect();
|
|
926
1040
|
this.updateSyncStatus({ state: 'error', error: error });
|
|
927
1041
|
}
|
|
928
|
-
else if (!this.syncWebSocket
|
|
1042
|
+
else if (!this.syncWebSocket.isConnected()) {
|
|
929
1043
|
this.updateSyncStatus({ state: 'offline', offlineSince: new Date() });
|
|
930
1044
|
}
|
|
931
1045
|
}
|
|
@@ -984,10 +1098,19 @@ export class BaseSyncedStore {
|
|
|
984
1098
|
return null;
|
|
985
1099
|
return new ConnectionManager({
|
|
986
1100
|
baseUrl: this._syncServerUrl,
|
|
987
|
-
getAuthToken: () => this.auth?.getAuthToken() ?? this.syncWebSocket
|
|
1101
|
+
getAuthToken: () => this.auth?.getAuthToken() ?? this.syncWebSocket.getAuthToken() ?? null,
|
|
1102
|
+
logger: contextLogger,
|
|
1103
|
+
observability: contextSocketObservability,
|
|
988
1104
|
});
|
|
989
1105
|
}
|
|
990
|
-
/**
|
|
1106
|
+
/**
|
|
1107
|
+
* Disconnect and clean up all resources. Terminal: this means "the client
|
|
1108
|
+
* is finished", not "close and reopen later" — the connection object stays
|
|
1109
|
+
* assigned but closed, the event wiring is torn down, and nothing
|
|
1110
|
+
* re-initializes a disconnected store. (Mid-session closes during recovery
|
|
1111
|
+
* go through the connection FSM's `onDisconnectWebSocket`, which closes
|
|
1112
|
+
* the transport without touching the store.)
|
|
1113
|
+
*/
|
|
991
1114
|
async disconnect() {
|
|
992
1115
|
this.stopCredentialLifecycle();
|
|
993
1116
|
if (this.batchTimer) {
|
|
@@ -1003,15 +1126,12 @@ export class BaseSyncedStore {
|
|
|
1003
1126
|
this.connectionManager = null;
|
|
1004
1127
|
}
|
|
1005
1128
|
try {
|
|
1006
|
-
const last = this.syncWebSocket
|
|
1129
|
+
const last = this.syncWebSocket.getLastSyncId();
|
|
1007
1130
|
if (last > 0)
|
|
1008
1131
|
await this.database.updateWorkspaceMetadata({ lastSyncId: last });
|
|
1009
1132
|
}
|
|
1010
1133
|
catch { }
|
|
1011
|
-
|
|
1012
|
-
this.syncWebSocket.disconnect();
|
|
1013
|
-
this.syncWebSocket = null;
|
|
1014
|
-
}
|
|
1134
|
+
this.syncWebSocket.disconnect();
|
|
1015
1135
|
this.syncClient.disconnect();
|
|
1016
1136
|
this.queryProcessor.clearCache();
|
|
1017
1137
|
// Stop the pool's GC interval — the one timer the pool arms itself.
|
|
@@ -1081,8 +1201,6 @@ export class BaseSyncedStore {
|
|
|
1081
1201
|
*/
|
|
1082
1202
|
async waitForWebSocketConnected(timeoutMs) {
|
|
1083
1203
|
const ws = this.syncWebSocket;
|
|
1084
|
-
if (!ws)
|
|
1085
|
-
return false;
|
|
1086
1204
|
if (ws.isConnected())
|
|
1087
1205
|
return true;
|
|
1088
1206
|
return new Promise((resolve) => {
|
|
@@ -1100,40 +1218,53 @@ export class BaseSyncedStore {
|
|
|
1100
1218
|
return;
|
|
1101
1219
|
resolved = true;
|
|
1102
1220
|
unsubscribe();
|
|
1103
|
-
|
|
1221
|
+
this.runtime.logger.debug(`[BaseSyncedStore] waitForWebSocketConnected timed out after ${timeoutMs}ms — initialize() will return but the next mutation may race the upgrade.`);
|
|
1104
1222
|
resolve(false);
|
|
1105
1223
|
}, timeoutMs);
|
|
1106
1224
|
});
|
|
1107
1225
|
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Seed the connection's late values and open it. The socket itself exists
|
|
1228
|
+
* from construction; what identity resolution supplies — the participant
|
|
1229
|
+
* kind, the credential, the read scope, and the resume cursor — is seeded
|
|
1230
|
+
* here, and only then is the held first connect released. A retried
|
|
1231
|
+
* `initialize()` after a failed `ready()` re-runs this against the same
|
|
1232
|
+
* connection object: the reconnect counter is reset for a clean slate,
|
|
1233
|
+
* while the session-error latch deliberately survives (only the
|
|
1234
|
+
* credential-expiry recovery clears it).
|
|
1235
|
+
*/
|
|
1108
1236
|
setupWebSocketSync(context, lastSyncId) {
|
|
1109
1237
|
if (!context.userId || !context.organizationId) {
|
|
1110
|
-
|
|
1238
|
+
this.runtime.observability.breadcrumb('Cannot setup WebSocket sync without user context', 'sync.websocket', 'warning');
|
|
1111
1239
|
return;
|
|
1112
1240
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1241
|
+
if (context.kind)
|
|
1242
|
+
this.syncWebSocket.setKind(context.kind);
|
|
1243
|
+
if (context.capabilityToken) {
|
|
1244
|
+
this.syncWebSocket.setCapabilityToken(context.capabilityToken);
|
|
1245
|
+
}
|
|
1246
|
+
const syncGroups = this.resolveSyncGroups(context);
|
|
1247
|
+
this.syncWebSocket.setSyncGroups(syncGroups);
|
|
1248
|
+
this.syncWebSocket.setLastSyncId(lastSyncId || 0);
|
|
1249
|
+
// The permanent base scopes for read interest — same set the connection
|
|
1250
|
+
// subscribes to at upgrade, so the two can never disagree.
|
|
1251
|
+
this.areaOfInterest.setBaseGroups(syncGroups);
|
|
1252
|
+
// ── Connection FSM ────────────────────────────────────────────
|
|
1253
|
+
// Instantiate + start the SDK's ConnectionManager so every consumer
|
|
1254
|
+
// gets correct online/offline recovery. Guarded: a retried
|
|
1255
|
+
// `initialize()` reuses the manager it already started.
|
|
1256
|
+
if (!this.connectionManager)
|
|
1257
|
+
this.startConnectionManager(context.kind);
|
|
1258
|
+
this.syncWebSocket.resetReconnectAttempts();
|
|
1259
|
+
this.syncWebSocket.allowConnect();
|
|
1260
|
+
this.syncWebSocket.connect();
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Wire the store's handlers onto the connection. Runs once, at
|
|
1264
|
+
* construction — the connection object is stable for the store's
|
|
1265
|
+
* lifetime, so the wiring is too.
|
|
1266
|
+
*/
|
|
1267
|
+
wireSocketEvents() {
|
|
1137
1268
|
// Connection events → forward to connection lifecycle callback
|
|
1138
1269
|
const onConnected = this.syncWebSocket.subscribe('connected', () => {
|
|
1139
1270
|
this.syncClient.markConnected();
|
|
@@ -1149,7 +1280,7 @@ export class BaseSyncedStore {
|
|
|
1149
1280
|
// changed while offline; after a full reconnect the new socket's URL
|
|
1150
1281
|
// carries only base groups. `resync` re-pushes the current desired set
|
|
1151
1282
|
// so the server-side index matches what the user is actually viewing.
|
|
1152
|
-
void this.areaOfInterest
|
|
1283
|
+
void this.areaOfInterest.resync();
|
|
1153
1284
|
});
|
|
1154
1285
|
const onDisconnected = this.syncWebSocket.subscribe('disconnected', () => {
|
|
1155
1286
|
this.syncClient.disconnect();
|
|
@@ -1157,7 +1288,7 @@ export class BaseSyncedStore {
|
|
|
1157
1288
|
this.updateSyncStatus({ state: 'offline', offlineSince: new Date() });
|
|
1158
1289
|
});
|
|
1159
1290
|
const onReconnecting = this.syncWebSocket.subscribe('reconnecting', ({ attempt, delay }) => {
|
|
1160
|
-
|
|
1291
|
+
this.runtime.logger.info('[BaseSyncedStore] WebSocket reconnecting', { attempt, delay });
|
|
1161
1292
|
this.updateSyncStatus({ state: 'reconnecting' });
|
|
1162
1293
|
});
|
|
1163
1294
|
// Delta events → feed into processing pipeline
|
|
@@ -1190,7 +1321,7 @@ export class BaseSyncedStore {
|
|
|
1190
1321
|
// Terminal session loss (revocation / the login itself is gone): notify,
|
|
1191
1322
|
// route the FSM to its terminal state, and clear local data.
|
|
1192
1323
|
const handleTerminalSessionError = (error) => {
|
|
1193
|
-
|
|
1324
|
+
this.runtime.observability.captureWebSocketError({ context: 'session-error', error: error.message });
|
|
1194
1325
|
this.onConnectionEvent?.('WS_SESSION_ERROR');
|
|
1195
1326
|
for (const listener of this.sessionErrorListeners) {
|
|
1196
1327
|
try {
|
|
@@ -1203,13 +1334,13 @@ export class BaseSyncedStore {
|
|
|
1203
1334
|
// When auth is revoked, locally cached data must not persist on disk.
|
|
1204
1335
|
this.database.clear({ includeWriteJournal: true }).catch((clearErr) => {
|
|
1205
1336
|
// consumer register: session ended, but cached data may remain on disk
|
|
1206
|
-
|
|
1207
|
-
|
|
1337
|
+
this.runtime.logger.error('Your session ended, but some locally cached data could not be cleared from this device.');
|
|
1338
|
+
this.runtime.logger.debug('[BaseSyncedStore] Failed to clear database on session error', clearErr);
|
|
1208
1339
|
});
|
|
1209
1340
|
this.objectPool.clear();
|
|
1210
1341
|
};
|
|
1211
1342
|
const onSessionError = this.syncWebSocket.subscribe('session_error', (error) => {
|
|
1212
|
-
// WS analog of HTTP's `apikey_expired` (see
|
|
1343
|
+
// WS analog of HTTP's `apikey_expired` (see AbloSessionError.
|
|
1213
1344
|
// isSessionErrorResponse): the hub's keepalive reaper closes sockets
|
|
1214
1345
|
// whose SHORT-LIVED access credential (`ek_`/`rk_`) passed its expiry
|
|
1215
1346
|
// with `4001 credential_expired`. That is re-mintable from the
|
|
@@ -1218,12 +1349,12 @@ export class BaseSyncedStore {
|
|
|
1218
1349
|
// Only a mint that answers `null` (the login itself is gone) falls
|
|
1219
1350
|
// through to the terminal path. Without this branch, every credential
|
|
1220
1351
|
// TTL elapse wedged the socket behind the write-once session latch.
|
|
1221
|
-
if (
|
|
1222
|
-
|
|
1352
|
+
if (AbloSessionError.isSessionError(error) && isAccessCredentialExpiryCloseReason(error.message)) {
|
|
1353
|
+
this.runtime.observability.breadcrumb('WebSocket closed for expired access credential — re-minting', 'sync.websocket', 'warning');
|
|
1223
1354
|
// Un-latch BEFORE the async mint so the FSM's own recovery
|
|
1224
1355
|
// (probe → refreshing_credential → reconnect) is never blocked on
|
|
1225
1356
|
// our .then() ordering.
|
|
1226
|
-
this.syncWebSocket
|
|
1357
|
+
this.syncWebSocket.clearSessionError();
|
|
1227
1358
|
void this.performCredentialRefresh().then((outcome) => {
|
|
1228
1359
|
if (outcome === 'refreshed') {
|
|
1229
1360
|
if (this.connectionManager) {
|
|
@@ -1236,8 +1367,8 @@ export class BaseSyncedStore {
|
|
|
1236
1367
|
// (createConnectionManager returns null for kind 'agent') —
|
|
1237
1368
|
// reconnect the socket directly; connect() reads the
|
|
1238
1369
|
// freshly-minted credential from the credential source.
|
|
1239
|
-
this.syncWebSocket
|
|
1240
|
-
this.syncWebSocket
|
|
1370
|
+
this.syncWebSocket.resetReconnectAttempts();
|
|
1371
|
+
this.syncWebSocket.connect();
|
|
1241
1372
|
}
|
|
1242
1373
|
return;
|
|
1243
1374
|
}
|
|
@@ -1245,7 +1376,7 @@ export class BaseSyncedStore {
|
|
|
1245
1376
|
// The mint endpoint rejected: the long-lived login is gone.
|
|
1246
1377
|
// Re-latch so writes reject with the permanent session type
|
|
1247
1378
|
// (see SyncWebSocket.notConnectedError) instead of parking.
|
|
1248
|
-
this.syncWebSocket
|
|
1379
|
+
this.syncWebSocket.setSessionErrorDetected();
|
|
1249
1380
|
handleTerminalSessionError(error);
|
|
1250
1381
|
}
|
|
1251
1382
|
// 'network_error' → transient mint failure. The WS_DISCONNECTED
|
|
@@ -1267,19 +1398,20 @@ export class BaseSyncedStore {
|
|
|
1267
1398
|
});
|
|
1268
1399
|
const onReconnectFailed = this.syncWebSocket.subscribe('reconnect_failed', ({ attempts }) => {
|
|
1269
1400
|
// consumer register: reconnection exhausted — the app is now offline
|
|
1270
|
-
|
|
1271
|
-
|
|
1401
|
+
this.runtime.logger.warn('Lost connection to the sync service and could not reconnect. Your app is now offline; changes will sync once the connection is restored.');
|
|
1402
|
+
this.runtime.logger.debug('[BaseSyncedStore] WebSocket reconnection gave up', { attempts });
|
|
1272
1403
|
this.updateSyncStatus({ state: 'reconnecting' });
|
|
1273
1404
|
});
|
|
1274
|
-
this.disposers.push(onConnected, onDisconnected, onReconnecting, onDelta, onDeltaBatch, onBootstrapRequired, onBootstrapData, onPresenceUpdate, onError, onSessionError, onHandshakeFailed, onReconnectFailed, () => { this.areaOfInterest
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1405
|
+
this.disposers.push(onConnected, onDisconnected, onReconnecting, onDelta, onDeltaBatch, onBootstrapRequired, onBootstrapData, onPresenceUpdate, onError, onSessionError, onHandshakeFailed, onReconnectFailed, () => { this.areaOfInterest.dispose(); });
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Build and start the connection FSM. The `onConnectionEvent` hook is the
|
|
1409
|
+
* bridge — WS events fire the hook, the hook forwards into the FSM. Called
|
|
1410
|
+
* from `setupWebSocketSync` because the FSM's shape depends on the resolved
|
|
1411
|
+
* participant kind (agents get none — see {@link createConnectionManager}).
|
|
1412
|
+
*/
|
|
1413
|
+
startConnectionManager(kind) {
|
|
1414
|
+
this.connectionManager = this.createConnectionManager(kind);
|
|
1283
1415
|
if (this.connectionManager) {
|
|
1284
1416
|
const manager = this.connectionManager;
|
|
1285
1417
|
// Preserve any externally-set onConnectionEvent — chain rather
|
|
@@ -1310,7 +1442,7 @@ export class BaseSyncedStore {
|
|
|
1310
1442
|
onReconnect: () => this.performReconnect(),
|
|
1311
1443
|
onRefreshCredential: () => this.performCredentialRefresh(),
|
|
1312
1444
|
onSessionExpired: () => {
|
|
1313
|
-
const err = new
|
|
1445
|
+
const err = new AbloSessionError('Session expired');
|
|
1314
1446
|
for (const listener of this.sessionErrorListeners) {
|
|
1315
1447
|
try {
|
|
1316
1448
|
listener(err);
|
|
@@ -1319,7 +1451,7 @@ export class BaseSyncedStore {
|
|
|
1319
1451
|
}
|
|
1320
1452
|
},
|
|
1321
1453
|
onDisconnectWebSocket: () => {
|
|
1322
|
-
this.syncWebSocket
|
|
1454
|
+
this.syncWebSocket.disconnect();
|
|
1323
1455
|
},
|
|
1324
1456
|
// Mirror FSM transitions into the visible `syncStatus.state` so
|
|
1325
1457
|
// the UI can show "Reconnecting…" while the FSM cycles through
|
|
@@ -1371,12 +1503,6 @@ export class BaseSyncedStore {
|
|
|
1371
1503
|
},
|
|
1372
1504
|
});
|
|
1373
1505
|
}
|
|
1374
|
-
// Transaction events for pendingChanges tracking
|
|
1375
|
-
const unsubCreated = this.syncClient.onTransactionEvent('created', () => { this.incrementPendingChanges(); });
|
|
1376
|
-
const unsubCompleted = this.syncClient.onTransactionEvent('completed', () => { this.decrementPendingChanges(); });
|
|
1377
|
-
const unsubFailed = this.syncClient.onTransactionEvent('failed', () => { this.decrementPendingChanges(); });
|
|
1378
|
-
this.disposers.push(unsubCreated, unsubCompleted, unsubFailed);
|
|
1379
|
-
this.syncWebSocket.connect();
|
|
1380
1506
|
}
|
|
1381
1507
|
// ── Delta processing pipeline ─────────────────────────────────────────────
|
|
1382
1508
|
//
|
|
@@ -1395,6 +1521,8 @@ export class BaseSyncedStore {
|
|
|
1395
1521
|
return this._deltaPipelineContext;
|
|
1396
1522
|
const store = this;
|
|
1397
1523
|
this._deltaPipelineContext = {
|
|
1524
|
+
runtime: this.runtime,
|
|
1525
|
+
stagePlugins: this.stagePlugins,
|
|
1398
1526
|
// Shared pipeline state, backed by the host fields.
|
|
1399
1527
|
get pendingDeltas() { return store.pendingDeltas; },
|
|
1400
1528
|
set pendingDeltas(deltas) { store.pendingDeltas = deltas; },
|
|
@@ -1412,10 +1540,8 @@ export class BaseSyncedStore {
|
|
|
1412
1540
|
advancePersisted: (syncId) => { this.syncClient.position.advancePersisted(syncId); },
|
|
1413
1541
|
// Persistence + pool writes.
|
|
1414
1542
|
processDeltaBatch: (deltas) => this.database.processDeltaBatch(deltas),
|
|
1415
|
-
applyDeltaBatchToPool: (results) => {
|
|
1416
|
-
|
|
1417
|
-
},
|
|
1418
|
-
acknowledge: (syncId) => { this.syncWebSocket?.acknowledge?.(syncId); },
|
|
1543
|
+
applyDeltaBatchToPool: (results) => { this.applyChangesToPool(results); },
|
|
1544
|
+
acknowledge: (syncId) => { this.syncWebSocket.acknowledge(syncId); },
|
|
1419
1545
|
get objectPool() { return store.objectPool; },
|
|
1420
1546
|
// Dynamic-dispatch hooks — protected override points on this class.
|
|
1421
1547
|
getStateFields: (modelName) => this.getStateFields(modelName),
|
|
@@ -1433,6 +1559,14 @@ export class BaseSyncedStore {
|
|
|
1433
1559
|
};
|
|
1434
1560
|
return this._deltaPipelineContext;
|
|
1435
1561
|
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Lands persisted changes in the in-memory pool, with this store's
|
|
1564
|
+
* relation enrichment bound. The one apply path: the pipeline's bridge
|
|
1565
|
+
* (no plugins installed) and the `humans()` apply handler both call it.
|
|
1566
|
+
*/
|
|
1567
|
+
applyChangesToPool(changes) {
|
|
1568
|
+
this.syncClient.applyDeltaBatchToPool(changes, (name, data) => this.enrichRelations(name, data));
|
|
1569
|
+
}
|
|
1436
1570
|
/** Get fields that represent meaningful state for deduplication. Override for model-specific fields. */
|
|
1437
1571
|
getStateFields(_modelName) {
|
|
1438
1572
|
return ['status', 'state', 'isActive'];
|
|
@@ -1525,7 +1659,7 @@ export class BaseSyncedStore {
|
|
|
1525
1659
|
* schema build time) to find children. The previous implementation did
|
|
1526
1660
|
* `getByType(ctor).filter(e => e.toJSON()[foreignKey] === parentId)` —
|
|
1527
1661
|
* a full pool scan per child model + a `toJSON()` allocation per
|
|
1528
|
-
* candidate. For a
|
|
1662
|
+
* candidate. For a report delete with 10K blocks in the pool, that was
|
|
1529
1663
|
* 10K toJSON allocations per cascade level. The FK-indexed lookup
|
|
1530
1664
|
* skips both the scan AND the allocation.
|
|
1531
1665
|
*/
|
|
@@ -1545,7 +1679,7 @@ export class BaseSyncedStore {
|
|
|
1545
1679
|
}
|
|
1546
1680
|
}
|
|
1547
1681
|
if (totalCancelled > 0) {
|
|
1548
|
-
|
|
1682
|
+
this.runtime.logger.info('[BaseSyncedStore] Cascade cancelled orphaned transactions', {
|
|
1549
1683
|
parentModel: parentModelName,
|
|
1550
1684
|
parentId: parentId.slice(0, 12),
|
|
1551
1685
|
totalCancelled,
|
|
@@ -1584,7 +1718,7 @@ export class BaseSyncedStore {
|
|
|
1584
1718
|
* Save a model (create or update).
|
|
1585
1719
|
*
|
|
1586
1720
|
* Accepts any entity shape with `{ id: string }` so consumers can pass the
|
|
1587
|
-
* Zod-inferred model types from `
|
|
1721
|
+
* Zod-inferred model types from `Model<Schema, K>` without knowing
|
|
1588
1722
|
* about the internal `Model` base class. At runtime, every entity reaching
|
|
1589
1723
|
* this method came through the object pool (via `store.create`, a query
|
|
1590
1724
|
* accessor, or an optimistic insert) and IS a `Model` instance — the one
|
|
@@ -1607,7 +1741,7 @@ export class BaseSyncedStore {
|
|
|
1607
1741
|
this.syncClient.update(model);
|
|
1608
1742
|
}
|
|
1609
1743
|
}
|
|
1610
|
-
/** Save with an atomic server mutation (e.g.,
|
|
1744
|
+
/** Save with an atomic server mutation (e.g., createSectionWithBlocks) */
|
|
1611
1745
|
async saveWithAtomicMutation(model, mutation) {
|
|
1612
1746
|
this.objectPool.add(model, ModelScope.live);
|
|
1613
1747
|
await mutation(this.syncClient.gql);
|
|
@@ -1632,7 +1766,7 @@ export class BaseSyncedStore {
|
|
|
1632
1766
|
this.syncClient.update(model);
|
|
1633
1767
|
}
|
|
1634
1768
|
// ── Query API ────────────────────────────────────────────────────────────
|
|
1635
|
-
// `ablo.<model>.
|
|
1769
|
+
// `ablo.<model>.local.retrieve` / `.local.list` is the read surface for
|
|
1636
1770
|
// application code. Custom mutators read transactionally through
|
|
1637
1771
|
// `tx.<model>`, backed by `createReaderActions`.
|
|
1638
1772
|
/** Retrieve a single entity by id. Synchronous pool read. */
|
|
@@ -1655,13 +1789,13 @@ export class BaseSyncedStore {
|
|
|
1655
1789
|
* Create a model instance locally, typed via the schema.
|
|
1656
1790
|
*
|
|
1657
1791
|
* ```ts
|
|
1658
|
-
* const
|
|
1659
|
-
* //
|
|
1792
|
+
* const ledger = store.create('ledgers', { name, reportId });
|
|
1793
|
+
* // ledger: Ledger | null — no cast needed
|
|
1660
1794
|
* ```
|
|
1661
1795
|
*
|
|
1662
1796
|
* The `typename` arg is the schema key (camelCase plural, e.g.
|
|
1663
|
-
* `'
|
|
1664
|
-
* `
|
|
1797
|
+
* `'ledgers'`); the returned instance has the
|
|
1798
|
+
* `Model<Schema, K>` shape including computeds + relation accessors.
|
|
1665
1799
|
* Wraps `pool.create(...)` — the underlying runtime is unchanged, just
|
|
1666
1800
|
* type-narrowed.
|
|
1667
1801
|
*/
|
|
@@ -1718,20 +1852,20 @@ export class BaseSyncedStore {
|
|
|
1718
1852
|
/**
|
|
1719
1853
|
* Get all models of a type. Returns Model[] honestly — callers that need
|
|
1720
1854
|
* narrow types should use `useAblo((ablo) => ablo.<model>.list(...))`
|
|
1721
|
-
* which does proper inference via `
|
|
1855
|
+
* which does proper inference via `Model<S, K>`.
|
|
1722
1856
|
*/
|
|
1723
1857
|
allModelsOfType(modelClass, scope) {
|
|
1724
1858
|
return this.objectPool.getByType(modelClass, scope ?? ModelScope.live);
|
|
1725
1859
|
}
|
|
1726
1860
|
/** Error handler for fire-and-forget flushPendingDeltas calls */
|
|
1727
1861
|
handleFlushError = (error) => {
|
|
1728
|
-
|
|
1862
|
+
this.runtime.observability.captureMutationFailure({
|
|
1729
1863
|
context: 'flush-pending-deltas',
|
|
1730
1864
|
modelName: 'batch',
|
|
1731
1865
|
modelId: 'batch',
|
|
1732
1866
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
1733
1867
|
});
|
|
1734
|
-
|
|
1868
|
+
this.runtime.logger.debug('[BaseSyncedStore] Delta flush error', {
|
|
1735
1869
|
error: error instanceof Error ? error.message : String(error),
|
|
1736
1870
|
});
|
|
1737
1871
|
};
|
|
@@ -1744,8 +1878,6 @@ export class BaseSyncedStore {
|
|
|
1744
1878
|
modelId: delta.modelId,
|
|
1745
1879
|
data: typeof delta.data === 'string' ? JSON.parse(delta.data) : delta.data,
|
|
1746
1880
|
});
|
|
1747
|
-
if (!dbResult)
|
|
1748
|
-
return;
|
|
1749
1881
|
// Track pending deletes for query filtering
|
|
1750
1882
|
if (dbResult.action === 'remove') {
|
|
1751
1883
|
this.pendingDeletes.add(dbResult.modelId);
|