@abloatai/ablo 0.34.1 → 0.35.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 +674 -5
- package/README.md +39 -22
- package/dist/BaseSyncedStore.d.ts +152 -44
- package/dist/BaseSyncedStore.js +300 -184
- package/dist/Database.d.ts +9 -24
- package/dist/Database.js +37 -22
- package/dist/InstanceCache.d.ts +25 -4
- package/dist/InstanceCache.js +48 -15
- package/dist/LazyReferenceCollection.d.ts +3 -3
- package/dist/LazyReferenceCollection.js +4 -4
- package/dist/Model.d.ts +6 -6
- package/dist/Model.js +10 -10
- package/dist/ModelRegistry.d.ts +4 -4
- package/dist/ModelRegistry.js +3 -3
- package/dist/{SyncEngineContext.d.ts → RuntimeContext.d.ts} +20 -13
- package/dist/{SyncEngineContext.js → RuntimeContext.js} +11 -12
- package/dist/SyncClient.d.ts +42 -32
- package/dist/SyncClient.js +166 -110
- 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/cli.cjs +3344 -1073
- package/dist/client/Ablo.d.ts +42 -287
- package/dist/client/Ablo.js +118 -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 +4 -4
- package/dist/client/createInternalComponents.js +9 -8
- package/dist/client/createModelProxy.d.ts +78 -373
- package/dist/client/createModelProxy.js +114 -86
- package/dist/client/humans.d.ts +48 -0
- package/dist/client/humans.js +52 -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 +48 -0
- package/dist/client/reactiveEngine.js +910 -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/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 +9 -9
- package/dist/context.js +10 -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/DatabaseManager.js +1 -1
- package/dist/core/QueryView.d.ts +1 -1
- package/dist/core/QueryView.js +1 -1
- package/dist/core/StoreManager.d.ts +4 -23
- package/dist/core/StoreManager.js +5 -55
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/core/storeContract.d.ts +2 -2
- package/dist/docs/catalog.d.ts +72 -0
- package/dist/docs/catalog.js +227 -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 +40 -34
- package/dist/index.js +26 -20
- 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 +2 -2
- package/dist/query/client.js +4 -4
- package/dist/query/types.d.ts +6 -41
- package/dist/query/types.js +2 -2
- package/dist/react/AbloProvider.d.ts +6 -8
- package/dist/react/AbloProvider.js +5 -7
- package/dist/react/context.d.ts +1 -1
- package/dist/react/context.js +1 -1
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.js +3 -3
- package/dist/react/internalContext.d.ts +1 -1
- package/dist/react/useAblo.d.ts +3 -3
- package/dist/react/useAblo.js +1 -1
- 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 +86 -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 +10 -10
- package/dist/schema/index.js +18 -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 +3 -3
- package/dist/schema/serialize.d.ts +15 -6
- package/dist/schema/serialize.js +17 -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/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/footprint.d.ts +111 -0
- package/dist/source/footprint.js +0 -0
- 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/stores/ObjectStore.d.ts +1 -1
- package/dist/stores/SyncActionStore.d.ts +1 -1
- package/dist/stores/SyncActionStore.js +2 -10
- 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 +123 -6
- package/dist/sync/BootstrapFetcher.js +492 -66
- package/dist/sync/ConnectionManager.d.ts +6 -198
- package/dist/sync/ConnectionManager.js +6 -677
- package/dist/sync/OnDemandLoader.d.ts +2 -2
- package/dist/sync/OnDemandLoader.js +60 -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/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 +1 -1
- package/dist/sync/participants.d.ts +5 -4
- package/dist/sync/participants.js +29 -22
- package/dist/sync/schemaDrift.d.ts +55 -0
- package/dist/sync/schemaDrift.js +53 -0
- package/dist/sync/schemas.d.ts +21 -32
- package/dist/sync/schemas.js +26 -17
- 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/testing/fixtures/bootstrap.d.ts +1 -1
- package/dist/testing/fixtures/deltas.d.ts +1 -1
- package/dist/testing/fixtures/httpResponses.d.ts +70 -0
- package/dist/testing/fixtures/httpResponses.js +90 -0
- package/dist/testing/fixtures/models.js +1 -1
- package/dist/testing/helpers/wait.js +1 -1
- package/dist/testing/mocks/MockMutationExecutor.d.ts +2 -2
- package/dist/testing/mocks/MockMutationExecutor.js +8 -14
- package/dist/testing/mocks/MockSyncContext.d.ts +11 -11
- package/dist/testing/mocks/MockSyncContext.js +10 -9
- package/dist/testing/mocks/MockSyncStore.js +1 -1
- package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
- 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 +177 -0
- package/dist/transaction/auth/capability.js +199 -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 +49 -0
- package/dist/transaction/coordination/awaitClaimGrant.js +112 -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 +44 -0
- package/dist/transaction/coordination/locator.d.ts +83 -0
- package/dist/transaction/coordination/locator.js +82 -0
- package/dist/transaction/coordination/schema.d.ts +1473 -0
- package/dist/{coordination → transaction/coordination}/schema.js +490 -55
- package/dist/transaction/coordination/targetConflict.d.ts +2 -0
- package/dist/transaction/coordination/targetConflict.js +103 -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} +11 -11
- package/dist/{errorCodes.js → transaction/errorCodes.js} +35 -12
- package/dist/{errors.d.ts → transaction/errors.d.ts} +37 -13
- package/dist/{errors.js → transaction/errors.js} +85 -16
- 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 +192 -0
- package/dist/transaction/plugin.js +87 -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 +266 -0
- package/dist/transaction/resources/httpResources.js +7 -0
- package/dist/transaction/resources/modelOperations.d.ts +319 -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 +85 -0
- package/dist/transaction/resources/where.js +70 -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 +5 -5
- package/dist/{schema → transaction/schema}/field.js +5 -5
- 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 +57 -0
- package/dist/transaction/schema/openapi.js +340 -0
- package/dist/{schema → transaction/schema}/relation.d.ts +14 -14
- 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 +12 -10
- package/dist/{schema → transaction/schema}/schema.js +4 -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 +4 -5
- 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} +10 -36
- 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/{client → transaction/transport}/httpClient.d.ts +59 -16
- package/dist/{client → transaction/transport}/httpClient.js +5 -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 +171 -85
- 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 +576 -0
- package/dist/transaction/transport/wsTransport.js +1017 -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 +63 -45
- 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 +351 -0
- package/dist/transaction/wire/accountResponses.js +255 -0
- package/dist/transaction/wire/auth.d.ts +49 -0
- package/dist/transaction/wire/auth.js +57 -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 +463 -0
- package/dist/transaction/wire/claims.js +229 -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 +177 -0
- package/dist/transaction/wire/feedEvent.js +39 -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 +552 -0
- package/dist/transaction/wire/inboundFrames.js +116 -0
- package/dist/transaction/wire/index.d.ts +50 -0
- package/dist/transaction/wire/index.js +74 -0
- package/dist/{wire → transaction/wire}/listEnvelope.d.ts +13 -14
- package/dist/transaction/wire/listEnvelope.js +42 -0
- package/dist/transaction/wire/modelResponses.d.ts +85 -0
- package/dist/transaction/wire/modelResponses.js +43 -0
- package/dist/transactions/{TransactionQueue.d.ts → mutations/MutationQueue.d.ts} +79 -38
- package/dist/transactions/{TransactionQueue.js → mutations/MutationQueue.js} +110 -59
- 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} +16 -15
- package/dist/transactions/{commitPayload.js → mutations/commitPayload.js} +12 -12
- package/dist/transactions/{deltaConfirmation.d.ts → mutations/deltaConfirmation.d.ts} +11 -11
- package/dist/transactions/{deltaConfirmation.js → mutations/deltaConfirmation.js} +7 -7
- 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} +3 -3
- package/dist/transactions/{replayValidation.js → mutations/replayValidation.js} +4 -3
- package/dist/utils/mobxSetup.d.ts +1 -1
- package/dist/utils/mobxSetup.js +5 -2
- 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 +19 -12
- package/docs/api-keys.md +8 -4
- package/docs/api.md +22 -18
- package/docs/audit.md +2 -0
- package/docs/cli.md +31 -3
- package/docs/client-behavior.md +8 -6
- package/docs/concurrency-convention.md +30 -24
- package/docs/coordination.md +48 -38
- package/docs/data-sources.md +3 -1
- package/docs/debugging.md +5 -3
- 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 +45 -44
- package/docs/examples/server-agent.md +46 -26
- package/docs/groups.md +32 -29
- package/docs/guarantees.md +4 -2
- package/docs/how-it-works.md +9 -7
- package/docs/idempotency.md +126 -0
- package/docs/identity.md +58 -54
- package/docs/index.md +172 -86
- package/docs/integration-guide.md +17 -16
- package/docs/interaction-model.md +6 -4
- package/docs/mcp.md +41 -16
- package/docs/migration.md +63 -5
- package/docs/operating-on-your-database.md +3 -1
- package/docs/projects.md +2 -0
- package/docs/quickstart.md +22 -5
- package/docs/react.md +12 -10
- package/docs/schema-contract.md +5 -3
- package/docs/session-settings.md +108 -0
- package/docs/sessions.md +3 -1
- package/docs/webhooks.md +3 -1
- package/llms.txt +47 -17
- package/package.json +10 -8
- 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/client/durableWrites.d.ts +0 -21
- package/dist/coordination/schema.d.ts +0 -722
- package/dist/schema/openapi.d.ts +0 -29
- package/dist/schema/openapi.js +0 -124
- package/dist/transactions/durableWriteStore.js +0 -30
- 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/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/coordination}/claimHeartbeatLoop.d.ts +0 -0
- /package/dist/{client → transaction/coordination}/claimHeartbeatLoop.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/{utils → transaction/utils}/duration.d.ts +0 -0
- /package/dist/{utils → transaction/utils}/duration.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/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
22
|
import { QueryProcessor } from './core/QueryProcessor.js';
|
|
22
23
|
import { Model, rowAsModel } from './Model.js';
|
|
23
24
|
import { getContext } from './context.js';
|
|
24
|
-
import {
|
|
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
|
*/
|
|
@@ -88,15 +95,20 @@ export class BaseSyncedStore {
|
|
|
88
95
|
*/
|
|
89
96
|
schema;
|
|
90
97
|
// ── Real-time sync ──
|
|
91
|
-
|
|
98
|
+
/**
|
|
99
|
+
* The connection, owned by whoever built this store (ADR 0016 follow-up
|
|
100
|
+
* 3b): the host constructs it and hands it in, the store seeds its late
|
|
101
|
+
* values during `initialize()` and owns the lifecycle from there. One
|
|
102
|
+
* instance for the store's whole lifetime — reconnects replace the socket
|
|
103
|
+
* inside it, never the object.
|
|
104
|
+
*/
|
|
105
|
+
syncWebSocket;
|
|
92
106
|
/**
|
|
93
107
|
* 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`.
|
|
108
|
+
* groups. Constructed with the connection; the permanent base scopes are
|
|
109
|
+
* seeded in `setupWebSocketSync` once identity resolves.
|
|
98
110
|
*/
|
|
99
|
-
areaOfInterest
|
|
111
|
+
areaOfInterest;
|
|
100
112
|
/** Sync groups whose current state has been backfilled into the pool
|
|
101
113
|
* (hydrate-on-enter). Cleared when the pool is reset on (re)bootstrap. */
|
|
102
114
|
hydratedGroups = new Set();
|
|
@@ -104,27 +116,50 @@ export class BaseSyncedStore {
|
|
|
104
116
|
* enters of the same scope so they share one fetch. */
|
|
105
117
|
hydratingGroups = new Map();
|
|
106
118
|
_syncServerUrl;
|
|
119
|
+
/** Application-declared collaboration event types; empty unless configured. */
|
|
120
|
+
_collaborationEvents = [];
|
|
107
121
|
/**
|
|
108
122
|
* Public accessor for the underlying SyncWebSocket. Used by the
|
|
109
123
|
* factory in `createSyncEngine` to wire the default mutation
|
|
110
124
|
* executor — the executor needs the WS handle to send commit
|
|
111
125
|
* frames, and the factory can't reach `protected` state through
|
|
112
|
-
* normal typing.
|
|
113
|
-
* `initialize()`.
|
|
126
|
+
* normal typing.
|
|
114
127
|
*/
|
|
115
128
|
getSyncWebSocket() {
|
|
116
129
|
return this.syncWebSocket;
|
|
117
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Subscribe to pushed frames — deltas, presence updates, claim grants and
|
|
133
|
+
* losses, connection changes, and this store's collaboration events.
|
|
134
|
+
* Durable by construction: the connection object exists for the store's
|
|
135
|
+
* whole lifetime (reconnects replace only the socket inside it), so a
|
|
136
|
+
* subscription made before the first connect starts delivering when the
|
|
137
|
+
* socket opens and keeps delivering across every reconnect. Returns the
|
|
138
|
+
* unsubscribe function.
|
|
139
|
+
*/
|
|
140
|
+
subscribe(event, handler) {
|
|
141
|
+
return this.syncWebSocket.subscribe(event, handler);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Send a collaboration event (an app-specific real-time message from this
|
|
145
|
+
* store's `TCollaboration` map). A no-op while the connection is down —
|
|
146
|
+
* presence-grade traffic is not queued.
|
|
147
|
+
*/
|
|
148
|
+
sendCollaborationEvent(messageType, payload) {
|
|
149
|
+
this.syncWebSocket.sendCollaborationEvent(messageType, payload);
|
|
150
|
+
}
|
|
118
151
|
// ── Area-of-interest (dynamic read subscription) ─────────────────
|
|
119
152
|
//
|
|
120
153
|
// `enterScope`/`leaveScope` move the connection's read interest as the
|
|
121
|
-
// user navigates (open or close a
|
|
154
|
+
// user navigates (open or close a record); `pinScope`/`unpinScope`
|
|
122
155
|
// express prominence (an active claim keeps a group subscribed). All four
|
|
123
156
|
// resolve the scope to sync-group strings through the same resolver the
|
|
124
157
|
// 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
|
|
158
|
+
// write claims always agree on the string for a given entity. Before the
|
|
159
|
+
// connection opens they record interest without a wire send, and they
|
|
160
|
+
// never reject when the transport is offline (see
|
|
161
|
+
// {@link SubscriptionManager.reconcile}); the on-connect `resync` pushes
|
|
162
|
+
// whatever interest accumulated.
|
|
128
163
|
scopeToGroups(scope) {
|
|
129
164
|
return resolveParticipantSyncGroups(scope, this.schema);
|
|
130
165
|
}
|
|
@@ -137,11 +172,8 @@ export class BaseSyncedStore {
|
|
|
137
172
|
* and the live delta stream keeps flowing regardless.
|
|
138
173
|
*/
|
|
139
174
|
enterScope(scope, opts) {
|
|
140
|
-
const mgr = this.areaOfInterest;
|
|
141
|
-
if (!mgr)
|
|
142
|
-
return Promise.resolve();
|
|
143
175
|
const groups = this.scopeToGroups(scope);
|
|
144
|
-
const subscribed = Promise.all(groups.map((g) =>
|
|
176
|
+
const subscribed = Promise.all(groups.map((g) => this.areaOfInterest.enter(g))).then(() => undefined);
|
|
145
177
|
if (!opts?.hydrate)
|
|
146
178
|
return subscribed;
|
|
147
179
|
return subscribed.then(() => this.hydrateGroups(groups));
|
|
@@ -188,24 +220,15 @@ export class BaseSyncedStore {
|
|
|
188
220
|
}
|
|
189
221
|
/** Leave a scope → its groups go warm (hysteresis), then drop on sweep. */
|
|
190
222
|
leaveScope(scope) {
|
|
191
|
-
|
|
192
|
-
if (!mgr)
|
|
193
|
-
return Promise.resolve();
|
|
194
|
-
return Promise.all(this.scopeToGroups(scope).map((g) => mgr.leave(g))).then(() => undefined);
|
|
223
|
+
return Promise.all(this.scopeToGroups(scope).map((g) => this.areaOfInterest.leave(g))).then(() => undefined);
|
|
195
224
|
}
|
|
196
225
|
/** Pin a scope (active claim / prominence) → never warms while pinned. */
|
|
197
226
|
pinScope(scope) {
|
|
198
|
-
|
|
199
|
-
if (!mgr)
|
|
200
|
-
return Promise.resolve();
|
|
201
|
-
return Promise.all(this.scopeToGroups(scope).map((g) => mgr.pin(g))).then(() => undefined);
|
|
227
|
+
return Promise.all(this.scopeToGroups(scope).map((g) => this.areaOfInterest.pin(g))).then(() => undefined);
|
|
202
228
|
}
|
|
203
229
|
/** Release a pin → the group transitions to warm rather than dropping. */
|
|
204
230
|
unpinScope(scope) {
|
|
205
|
-
|
|
206
|
-
if (!mgr)
|
|
207
|
-
return Promise.resolve();
|
|
208
|
-
return Promise.all(this.scopeToGroups(scope).map((g) => mgr.unpin(g))).then(() => undefined);
|
|
231
|
+
return Promise.all(this.scopeToGroups(scope).map((g) => this.areaOfInterest.unpin(g))).then(() => undefined);
|
|
209
232
|
}
|
|
210
233
|
// ── Internal helpers ──
|
|
211
234
|
queryProcessor;
|
|
@@ -237,18 +260,20 @@ export class BaseSyncedStore {
|
|
|
237
260
|
pendingDeltas = [];
|
|
238
261
|
batchTimer = null;
|
|
239
262
|
syncPromise = null;
|
|
240
|
-
/** Resume/ack cursor — delegates to the shared
|
|
241
|
-
*
|
|
263
|
+
/** Resume/ack cursor — delegates to the shared LogPosition (see
|
|
264
|
+
* logPosition.ts). Advances only after IDB persistence. */
|
|
242
265
|
get lastAckedId() {
|
|
243
266
|
return this.syncClient.position.persisted;
|
|
244
267
|
}
|
|
245
|
-
/** Pool-applied cursor — delegates to the shared
|
|
268
|
+
/** Pool-applied cursor — delegates to the shared LogPosition. */
|
|
246
269
|
get highestProcessedSyncId() {
|
|
247
270
|
return this.syncClient.position.applied;
|
|
248
271
|
}
|
|
249
272
|
// ── Delta queuing during bootstrap ──
|
|
250
273
|
bootstrapDeltaQueue = null;
|
|
251
274
|
activeBootstrapCount = 0;
|
|
275
|
+
/** The live deadline for the bootstrap attempt in flight, if any. */
|
|
276
|
+
bootstrapDeadlineTimer = null;
|
|
252
277
|
// ── Delete tracking ──
|
|
253
278
|
pendingDeletes = new Set();
|
|
254
279
|
// ── Model type hydration ──
|
|
@@ -262,6 +287,30 @@ export class BaseSyncedStore {
|
|
|
262
287
|
this.auth = dependencies.auth;
|
|
263
288
|
this.schema = dependencies.schema;
|
|
264
289
|
this._syncServerUrl = dependencies.url;
|
|
290
|
+
this._collaborationEvents = config.collaborationEvents ?? [];
|
|
291
|
+
// The connection exists from construction (ADR 0016 follow-up 3b): the
|
|
292
|
+
// host hands one in, or the store builds its own. `deferConnect` holds
|
|
293
|
+
// it closed until `initialize()` has seeded identity and read scope, so
|
|
294
|
+
// nothing can open an unscoped connection in between.
|
|
295
|
+
this.syncWebSocket =
|
|
296
|
+
dependencies.syncWebSocket ??
|
|
297
|
+
new SyncWebSocket({
|
|
298
|
+
baseUrl: this._syncServerUrl,
|
|
299
|
+
collaborationEvents: [...this._collaborationEvents],
|
|
300
|
+
getAuthToken: this.auth?.getAuthToken,
|
|
301
|
+
deferConnect: true,
|
|
302
|
+
capabilities: {
|
|
303
|
+
partialBootstrap: true,
|
|
304
|
+
compressedDeltas: true,
|
|
305
|
+
streamingBootstrap: true,
|
|
306
|
+
batchedDeltas: true,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
this.areaOfInterest = new SubscriptionManager({ transport: this.syncWebSocket });
|
|
310
|
+
this.wireSocketEvents();
|
|
311
|
+
// QueuedMutation events for pendingChanges tracking — connection-
|
|
312
|
+
// independent, wired once for the store's lifetime.
|
|
313
|
+
this.disposers.push(this.syncClient.onTransactionEvent('created', () => { this.incrementPendingChanges(); }), this.syncClient.onTransactionEvent('completed', () => { this.decrementPendingChanges(); }), this.syncClient.onTransactionEvent('failed', () => { this.decrementPendingChanges(); }));
|
|
265
314
|
// Set this store as the global Model store
|
|
266
315
|
Model.setStore(this);
|
|
267
316
|
// ── Schema-derived sync plan ───────────────────────────────────────
|
|
@@ -414,7 +463,7 @@ export class BaseSyncedStore {
|
|
|
414
463
|
reportSessionExpired: () => {
|
|
415
464
|
this.connectionManager?.send({ type: 'BOOTSTRAP_FAILED_SESSION' });
|
|
416
465
|
},
|
|
417
|
-
});
|
|
466
|
+
}, contextLogger);
|
|
418
467
|
/**
|
|
419
468
|
* Listeners registered via `subscribeSessionError()`. Fired when the
|
|
420
469
|
* WebSocket closes with a session-invalid code (1008/4001/4003) or a
|
|
@@ -435,7 +484,7 @@ export class BaseSyncedStore {
|
|
|
435
484
|
}
|
|
436
485
|
/**
|
|
437
486
|
* Subscribe to per-mutation failure payloads. Forwarded from the
|
|
438
|
-
* underlying `SyncClient.
|
|
487
|
+
* underlying `SyncClient.mutationQueue` so consumers (toast layer,
|
|
439
488
|
* route-level reverted boundaries, telemetry) can react without
|
|
440
489
|
* reaching across the store. Returns an unsubscribe function.
|
|
441
490
|
*
|
|
@@ -448,6 +497,15 @@ export class BaseSyncedStore {
|
|
|
448
497
|
subscribeMutationFailure(listener) {
|
|
449
498
|
return this.syncClient.onMutationFailure(listener);
|
|
450
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* Subscribe to commit round-trip latency. Forwarded from the underlying
|
|
502
|
+
* `SyncClient` for the same reason as `subscribeMutationFailure` — the
|
|
503
|
+
* React provider binds against this surface, so the engine's wiring stays
|
|
504
|
+
* private while the SDK keeps one hook to expose.
|
|
505
|
+
*/
|
|
506
|
+
subscribeCommitLatency(listener) {
|
|
507
|
+
return this.syncClient.onCommitLatency(listener);
|
|
508
|
+
}
|
|
451
509
|
/**
|
|
452
510
|
* Wait for the in-flight transaction for (modelName, modelId) to be
|
|
453
511
|
* confirmed by the server. See `SyncClient.waitForConfirmation` for the
|
|
@@ -459,19 +517,19 @@ export class BaseSyncedStore {
|
|
|
459
517
|
/**
|
|
460
518
|
* Observe the LOCAL mutation stream for undo recording (see
|
|
461
519
|
* {@link import('./core/storeContract.js').LocalMutation}). Taps the
|
|
462
|
-
*
|
|
520
|
+
* MutationQueue's `transaction:created` event — fired once per local
|
|
463
521
|
* create/update/delete/archive with `previousData` already captured.
|
|
464
522
|
* Remote/collaborator deltas apply via `applyDeltaBatchToPool` and never
|
|
465
523
|
* emit here, so undo is naturally local-only (you can't undo a teammate).
|
|
466
524
|
*/
|
|
467
525
|
subscribeLocalMutations(handler) {
|
|
468
|
-
// Tap the
|
|
526
|
+
// Tap the MutationQueue directly via `onLocalTransaction`. The previous
|
|
469
527
|
// `syncClient.subscribe('transaction:created', …)` route registered the
|
|
470
528
|
// handler on SyncClient's OWN emitter, which never fires that event (only
|
|
471
529
|
// the queue's emitter does) — so undo recorded nothing. See
|
|
472
530
|
// `SyncClient.onLocalTransaction` for the full rationale.
|
|
473
531
|
return this.syncClient.onLocalTransaction((tx) => {
|
|
474
|
-
if (!tx
|
|
532
|
+
if (!tx.modelName || !tx.modelId)
|
|
475
533
|
return;
|
|
476
534
|
handler({
|
|
477
535
|
type: tx.type,
|
|
@@ -489,69 +547,113 @@ export class BaseSyncedStore {
|
|
|
489
547
|
*/
|
|
490
548
|
async executeBootstrapWithTimeout(bootstrapFn, _context, signal) {
|
|
491
549
|
let lastError = null;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (typeof navigator !== 'undefined' && navigator.onLine === false) {
|
|
502
|
-
getContext().observability.breadcrumb(`Bootstrap attempt ${attempt} skipped - offline`, 'sync.bootstrap', 'warning');
|
|
503
|
-
throw new AbloConnectionError('Bootstrap skipped - device is offline', {
|
|
504
|
-
code: 'bootstrap_offline',
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
try {
|
|
508
|
-
getContext().logger.info(`[BaseSyncedStore] Bootstrap attempt ${attempt}/${BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS}`);
|
|
509
|
-
const result = (await Promise.race([
|
|
510
|
-
bootstrapFn(),
|
|
511
|
-
this.createBootstrapTimeout(attempt),
|
|
512
|
-
]));
|
|
513
|
-
getContext().logger.info('[BaseSyncedStore] Bootstrap completed successfully', { attempt });
|
|
514
|
-
return result;
|
|
515
|
-
}
|
|
516
|
-
catch (error) {
|
|
517
|
-
lastError = error;
|
|
518
|
-
const isTimeout = error instanceof Error && error.message.includes('timed out');
|
|
519
|
-
const isAbort = error instanceof DOMException && error.name === 'AbortError';
|
|
520
|
-
const isNetworkError = error instanceof TypeError && error.message.includes('fetch');
|
|
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;
|
|
550
|
+
// An aborted initialize has to stop the transfer, not just stop waiting for
|
|
551
|
+
// it. Without this the caller returns while a cold start keeps downloading,
|
|
552
|
+
// and those chunks are still in flight when the next initialize begins.
|
|
553
|
+
const onCallerAbort = () => { this.database.helper.abort(); };
|
|
554
|
+
signal?.addEventListener('abort', onCallerAbort, { once: true });
|
|
555
|
+
try {
|
|
556
|
+
for (let attempt = 1; attempt <= BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS; attempt++) {
|
|
557
|
+
if (signal?.aborted) {
|
|
558
|
+
throw new DOMException('Initialization aborted', 'AbortError');
|
|
528
559
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
560
|
+
// `navigator.onLine === false` is the MDN-reliable "definitely
|
|
561
|
+
// offline" signal. Don't use `!navigator.onLine`: Node 22+ exposes
|
|
562
|
+
// `globalThis.navigator` with `onLine === undefined`, so the
|
|
563
|
+
// negation false-positives every server-side bootstrap (e.g. the
|
|
564
|
+
// server-side agent.run dispatch path through `connectAgent`).
|
|
565
|
+
if (typeof navigator !== 'undefined' && navigator.onLine === false) {
|
|
566
|
+
getContext().observability.breadcrumb(`Bootstrap attempt ${attempt} skipped - offline`, 'sync.bootstrap', 'warning');
|
|
567
|
+
throw new AbloConnectionError('Bootstrap skipped - device is offline', {
|
|
568
|
+
code: 'bootstrap_offline',
|
|
569
|
+
});
|
|
534
570
|
}
|
|
535
|
-
|
|
536
|
-
|
|
571
|
+
try {
|
|
572
|
+
getContext().logger.info(`[BaseSyncedStore] Bootstrap attempt ${attempt}/${BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS}`);
|
|
573
|
+
const result = (await Promise.race([
|
|
574
|
+
bootstrapFn(),
|
|
575
|
+
this.createBootstrapTimeout(attempt),
|
|
576
|
+
]));
|
|
577
|
+
getContext().logger.info('[BaseSyncedStore] Bootstrap completed successfully', { attempt });
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
lastError = error;
|
|
582
|
+
const isTimeout = error instanceof Error && error.message.includes('timed out');
|
|
583
|
+
const isAbort = error instanceof DOMException && error.name === 'AbortError';
|
|
584
|
+
const isNetworkError = error instanceof TypeError && error.message.includes('fetch');
|
|
585
|
+
if (isAbort)
|
|
586
|
+
throw error;
|
|
587
|
+
if (AbloSessionError.isSessionError(error))
|
|
588
|
+
throw error;
|
|
589
|
+
if (isNetworkError && typeof navigator !== 'undefined' && navigator.onLine === false) {
|
|
590
|
+
getContext().observability.captureBootstrapFailure(error, { type: 'network-offline' });
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
getContext().observability.breadcrumb(`Bootstrap attempt ${attempt} failed`, 'sync.bootstrap', 'warning', { isTimeout, isNetworkError, willRetry: attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS });
|
|
594
|
+
if (isTimeout && attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS) {
|
|
595
|
+
getContext().logger.info('[BaseSyncedStore] Resetting state before bootstrap retry');
|
|
596
|
+
this.resetBootstrapState();
|
|
597
|
+
await new Promise((resolve) => setTimeout(resolve, BOOTSTRAP_CONFIG.RETRY_DELAY_MS));
|
|
598
|
+
}
|
|
599
|
+
else if (!isTimeout && attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS) {
|
|
600
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
finally {
|
|
604
|
+
// Disarm this attempt's deadline the moment it settles — a live timer
|
|
605
|
+
// would abort whatever the next attempt puts in flight.
|
|
606
|
+
this.clearBootstrapDeadline();
|
|
537
607
|
}
|
|
538
608
|
}
|
|
609
|
+
throw lastError
|
|
610
|
+
? toAbloError(lastError)
|
|
611
|
+
: new AbloConnectionError('Bootstrap failed after all retry attempts', {
|
|
612
|
+
code: 'bootstrap_fetch_timeout',
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
finally {
|
|
616
|
+
signal?.removeEventListener('abort', onCallerAbort);
|
|
617
|
+
this.clearBootstrapDeadline();
|
|
539
618
|
}
|
|
540
|
-
throw lastError
|
|
541
|
-
? toAbloError(lastError)
|
|
542
|
-
: new AbloConnectionError('Bootstrap failed after all retry attempts', {
|
|
543
|
-
code: 'bootstrap_fetch_timeout',
|
|
544
|
-
});
|
|
545
619
|
}
|
|
546
|
-
/**
|
|
620
|
+
/**
|
|
621
|
+
* The outer deadline for one bootstrap attempt.
|
|
622
|
+
*
|
|
623
|
+
* The length is DERIVED from the fetcher's own watchdog budget, not chosen. A
|
|
624
|
+
* chosen number is what broke this: the previous fixed 15s was shorter than a
|
|
625
|
+
* single model chunk's allowance — 20s waiting for response headers plus 15s
|
|
626
|
+
* of stall grace — so on any workspace with one slow model the deadline fired
|
|
627
|
+
* before the watchdogs it was meant to backstop, and every attempt timed out
|
|
628
|
+
* by construction. The watchdogs below are progress-based and already
|
|
629
|
+
* guarantee termination; this deadline exists only for a hang somewhere other
|
|
630
|
+
* than the network, so it must sit above them, and it can only do that
|
|
631
|
+
* reliably by asking them how long they take.
|
|
632
|
+
*
|
|
633
|
+
* Reaching it aborts the work in flight. `Promise.race` merely stops waiting:
|
|
634
|
+
* without the abort the losing bootstrap keeps running, keeps its sockets, and
|
|
635
|
+
* races the retry that replaced it — which is how one page load turned into
|
|
636
|
+
* dozens of overlapping requests.
|
|
637
|
+
*/
|
|
547
638
|
createBootstrapTimeout(attempt) {
|
|
548
|
-
const timeoutMs =
|
|
639
|
+
const timeoutMs = this.database.helper.budgetMs;
|
|
549
640
|
return new Promise((_, reject) => {
|
|
550
|
-
|
|
641
|
+
this.clearBootstrapDeadline();
|
|
642
|
+
this.bootstrapDeadlineTimer = setTimeout(() => {
|
|
643
|
+
this.database.helper.abort();
|
|
551
644
|
reject(new AbloConnectionError(`Bootstrap timed out after ${timeoutMs}ms (attempt ${attempt})`, { code: 'bootstrap_fetch_timeout' }));
|
|
552
645
|
}, timeoutMs);
|
|
553
646
|
});
|
|
554
647
|
}
|
|
648
|
+
/** Disarm the deadline once its attempt has settled. Load-bearing now that
|
|
649
|
+
* firing it aborts real work: a leftover timer would cancel a later,
|
|
650
|
+
* unrelated bootstrap. */
|
|
651
|
+
clearBootstrapDeadline() {
|
|
652
|
+
if (this.bootstrapDeadlineTimer !== null) {
|
|
653
|
+
clearTimeout(this.bootstrapDeadlineTimer);
|
|
654
|
+
this.bootstrapDeadlineTimer = null;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
555
657
|
/** Reset bootstrap-related state for a clean retry */
|
|
556
658
|
resetBootstrapState() {
|
|
557
659
|
try {
|
|
@@ -588,7 +690,7 @@ export class BaseSyncedStore {
|
|
|
588
690
|
await this.syncClient.hydrateFromDatabase();
|
|
589
691
|
this.dataReady = true;
|
|
590
692
|
}
|
|
591
|
-
if (
|
|
693
|
+
if (!this.syncWebSocket.isConnected()) {
|
|
592
694
|
this.syncWebSocket.resetReconnectAttempts();
|
|
593
695
|
this.syncWebSocket.connect();
|
|
594
696
|
}
|
|
@@ -597,9 +699,9 @@ export class BaseSyncedStore {
|
|
|
597
699
|
}
|
|
598
700
|
catch (error) {
|
|
599
701
|
getContext().observability.captureBootstrapFailure(error, { type: 'connection-store-reconnect' });
|
|
600
|
-
if (
|
|
601
|
-
this.syncWebSocket
|
|
602
|
-
this.syncWebSocket
|
|
702
|
+
if (AbloSessionError.isSessionError(error)) {
|
|
703
|
+
this.syncWebSocket.setSessionErrorDetected();
|
|
704
|
+
this.syncWebSocket.disconnect();
|
|
603
705
|
this.updateSyncStatus({ state: 'error', error: error });
|
|
604
706
|
// SECURITY: Clear locally cached data when session is invalid
|
|
605
707
|
this.database.clear({ includeWriteJournal: true }).catch(() => { });
|
|
@@ -609,10 +711,14 @@ export class BaseSyncedStore {
|
|
|
609
711
|
if (!this.dataReady && this.objectPool.size === 0) {
|
|
610
712
|
try {
|
|
611
713
|
await this.syncClient.hydrateFromDatabase();
|
|
612
|
-
|
|
714
|
+
// Re-read through a local: the guard above narrowed `size` to 0, and
|
|
715
|
+
// the compiler carries that narrowing across the await even though
|
|
716
|
+
// hydrating is precisely what fills the pool.
|
|
717
|
+
const hydratedSize = this.objectPool.size;
|
|
718
|
+
if (hydratedSize > 0) {
|
|
613
719
|
this.dataReady = true;
|
|
614
720
|
getContext().logger.info('[BaseSyncedStore] Hydrated from local fallback', {
|
|
615
|
-
objectPoolSize:
|
|
721
|
+
objectPoolSize: hydratedSize,
|
|
616
722
|
});
|
|
617
723
|
}
|
|
618
724
|
}
|
|
@@ -688,10 +794,10 @@ export class BaseSyncedStore {
|
|
|
688
794
|
return {
|
|
689
795
|
database: this.database,
|
|
690
796
|
objectPool: this.objectPool,
|
|
691
|
-
getSubscribedSyncGroups: () => this.syncWebSocket
|
|
797
|
+
getSubscribedSyncGroups: () => this.syncWebSocket.getSyncGroups(),
|
|
692
798
|
getCurrentSyncGroups: () => this.userContext ? this.resolveSyncGroups(this.userContext) : null,
|
|
693
799
|
getBootstrapMode: () => this.userContext?.bootstrapMode,
|
|
694
|
-
disconnectWebSocket: () => { this.syncWebSocket
|
|
800
|
+
disconnectWebSocket: () => { this.syncWebSocket.disconnect(); },
|
|
695
801
|
emitConnectionEvent: (event) => { this.onConnectionEvent?.(event); },
|
|
696
802
|
handleGroupAdded: (payload, syncId) => this.handleGroupAdded(payload, syncId),
|
|
697
803
|
computeUpdatedSyncGroups: (payload) => this.computeUpdatedSyncGroups(payload),
|
|
@@ -877,11 +983,11 @@ export class BaseSyncedStore {
|
|
|
877
983
|
this.updateSyncStatus({ state: 'idle', progress: 0 });
|
|
878
984
|
return { success: false, error: error };
|
|
879
985
|
}
|
|
880
|
-
const isSession =
|
|
986
|
+
const isSession = AbloSessionError.isSessionError(error);
|
|
881
987
|
getContext().observability.captureBootstrapFailure(error, { type: 'initialize' });
|
|
882
988
|
if (isSession) {
|
|
883
|
-
this.syncWebSocket
|
|
884
|
-
this.syncWebSocket
|
|
989
|
+
this.syncWebSocket.setSessionErrorDetected();
|
|
990
|
+
this.syncWebSocket.disconnect();
|
|
885
991
|
this.updateSyncStatus({ state: 'error', error: error });
|
|
886
992
|
return { success: false, error: error };
|
|
887
993
|
}
|
|
@@ -895,7 +1001,7 @@ export class BaseSyncedStore {
|
|
|
895
1001
|
if (this.objectPool.size > 0) {
|
|
896
1002
|
this.dataReady = true;
|
|
897
1003
|
this.initialized = true;
|
|
898
|
-
this.updateSyncStatus(this.syncWebSocket
|
|
1004
|
+
this.updateSyncStatus(this.syncWebSocket.isConnected()
|
|
899
1005
|
? { state: 'idle', progress: 100 }
|
|
900
1006
|
: { state: 'offline', offlineSince: new Date() });
|
|
901
1007
|
return { success: true };
|
|
@@ -920,12 +1026,12 @@ export class BaseSyncedStore {
|
|
|
920
1026
|
cause: error,
|
|
921
1027
|
});
|
|
922
1028
|
getContext().observability.captureBootstrapFailure(error, { type: 'background' });
|
|
923
|
-
if (
|
|
924
|
-
this.syncWebSocket
|
|
925
|
-
this.syncWebSocket
|
|
1029
|
+
if (AbloSessionError.isSessionError(error)) {
|
|
1030
|
+
this.syncWebSocket.setSessionErrorDetected();
|
|
1031
|
+
this.syncWebSocket.disconnect();
|
|
926
1032
|
this.updateSyncStatus({ state: 'error', error: error });
|
|
927
1033
|
}
|
|
928
|
-
else if (!this.syncWebSocket
|
|
1034
|
+
else if (!this.syncWebSocket.isConnected()) {
|
|
929
1035
|
this.updateSyncStatus({ state: 'offline', offlineSince: new Date() });
|
|
930
1036
|
}
|
|
931
1037
|
}
|
|
@@ -984,10 +1090,19 @@ export class BaseSyncedStore {
|
|
|
984
1090
|
return null;
|
|
985
1091
|
return new ConnectionManager({
|
|
986
1092
|
baseUrl: this._syncServerUrl,
|
|
987
|
-
getAuthToken: () => this.auth?.getAuthToken() ?? this.syncWebSocket
|
|
1093
|
+
getAuthToken: () => this.auth?.getAuthToken() ?? this.syncWebSocket.getAuthToken() ?? null,
|
|
1094
|
+
logger: contextLogger,
|
|
1095
|
+
observability: contextSocketObservability,
|
|
988
1096
|
});
|
|
989
1097
|
}
|
|
990
|
-
/**
|
|
1098
|
+
/**
|
|
1099
|
+
* Disconnect and clean up all resources. Terminal: this means "the client
|
|
1100
|
+
* is finished", not "close and reopen later" — the connection object stays
|
|
1101
|
+
* assigned but closed, the event wiring is torn down, and nothing
|
|
1102
|
+
* re-initializes a disconnected store. (Mid-session closes during recovery
|
|
1103
|
+
* go through the connection FSM's `onDisconnectWebSocket`, which closes
|
|
1104
|
+
* the transport without touching the store.)
|
|
1105
|
+
*/
|
|
991
1106
|
async disconnect() {
|
|
992
1107
|
this.stopCredentialLifecycle();
|
|
993
1108
|
if (this.batchTimer) {
|
|
@@ -1003,15 +1118,12 @@ export class BaseSyncedStore {
|
|
|
1003
1118
|
this.connectionManager = null;
|
|
1004
1119
|
}
|
|
1005
1120
|
try {
|
|
1006
|
-
const last = this.syncWebSocket
|
|
1121
|
+
const last = this.syncWebSocket.getLastSyncId();
|
|
1007
1122
|
if (last > 0)
|
|
1008
1123
|
await this.database.updateWorkspaceMetadata({ lastSyncId: last });
|
|
1009
1124
|
}
|
|
1010
1125
|
catch { }
|
|
1011
|
-
|
|
1012
|
-
this.syncWebSocket.disconnect();
|
|
1013
|
-
this.syncWebSocket = null;
|
|
1014
|
-
}
|
|
1126
|
+
this.syncWebSocket.disconnect();
|
|
1015
1127
|
this.syncClient.disconnect();
|
|
1016
1128
|
this.queryProcessor.clearCache();
|
|
1017
1129
|
// Stop the pool's GC interval — the one timer the pool arms itself.
|
|
@@ -1081,8 +1193,6 @@ export class BaseSyncedStore {
|
|
|
1081
1193
|
*/
|
|
1082
1194
|
async waitForWebSocketConnected(timeoutMs) {
|
|
1083
1195
|
const ws = this.syncWebSocket;
|
|
1084
|
-
if (!ws)
|
|
1085
|
-
return false;
|
|
1086
1196
|
if (ws.isConnected())
|
|
1087
1197
|
return true;
|
|
1088
1198
|
return new Promise((resolve) => {
|
|
@@ -1105,35 +1215,48 @@ export class BaseSyncedStore {
|
|
|
1105
1215
|
}, timeoutMs);
|
|
1106
1216
|
});
|
|
1107
1217
|
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Seed the connection's late values and open it. The socket itself exists
|
|
1220
|
+
* from construction; what identity resolution supplies — the participant
|
|
1221
|
+
* kind, the credential, the read scope, and the resume cursor — is seeded
|
|
1222
|
+
* here, and only then is the held first connect released. A retried
|
|
1223
|
+
* `initialize()` after a failed `ready()` re-runs this against the same
|
|
1224
|
+
* connection object: the reconnect counter is reset for a clean slate,
|
|
1225
|
+
* while the session-error latch deliberately survives (only the
|
|
1226
|
+
* credential-expiry recovery clears it).
|
|
1227
|
+
*/
|
|
1108
1228
|
setupWebSocketSync(context, lastSyncId) {
|
|
1109
1229
|
if (!context.userId || !context.organizationId) {
|
|
1110
1230
|
getContext().observability.breadcrumb('Cannot setup WebSocket sync without user context', 'sync.websocket', 'warning');
|
|
1111
1231
|
return;
|
|
1112
1232
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1233
|
+
if (context.kind)
|
|
1234
|
+
this.syncWebSocket.setKind(context.kind);
|
|
1235
|
+
if (context.capabilityToken) {
|
|
1236
|
+
this.syncWebSocket.setCapabilityToken(context.capabilityToken);
|
|
1237
|
+
}
|
|
1238
|
+
const syncGroups = this.resolveSyncGroups(context);
|
|
1239
|
+
this.syncWebSocket.setSyncGroups(syncGroups);
|
|
1240
|
+
this.syncWebSocket.setLastSyncId(lastSyncId || 0);
|
|
1241
|
+
// The permanent base scopes for read interest — same set the connection
|
|
1242
|
+
// subscribes to at upgrade, so the two can never disagree.
|
|
1243
|
+
this.areaOfInterest.setBaseGroups(syncGroups);
|
|
1244
|
+
// ── Connection FSM ────────────────────────────────────────────
|
|
1245
|
+
// Instantiate + start the SDK's ConnectionManager so every consumer
|
|
1246
|
+
// gets correct online/offline recovery. Guarded: a retried
|
|
1247
|
+
// `initialize()` reuses the manager it already started.
|
|
1248
|
+
if (!this.connectionManager)
|
|
1249
|
+
this.startConnectionManager(context.kind);
|
|
1250
|
+
this.syncWebSocket.resetReconnectAttempts();
|
|
1251
|
+
this.syncWebSocket.allowConnect();
|
|
1252
|
+
this.syncWebSocket.connect();
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Wire the store's handlers onto the connection. Runs once, at
|
|
1256
|
+
* construction — the connection object is stable for the store's
|
|
1257
|
+
* lifetime, so the wiring is too.
|
|
1258
|
+
*/
|
|
1259
|
+
wireSocketEvents() {
|
|
1137
1260
|
// Connection events → forward to connection lifecycle callback
|
|
1138
1261
|
const onConnected = this.syncWebSocket.subscribe('connected', () => {
|
|
1139
1262
|
this.syncClient.markConnected();
|
|
@@ -1149,7 +1272,7 @@ export class BaseSyncedStore {
|
|
|
1149
1272
|
// changed while offline; after a full reconnect the new socket's URL
|
|
1150
1273
|
// carries only base groups. `resync` re-pushes the current desired set
|
|
1151
1274
|
// so the server-side index matches what the user is actually viewing.
|
|
1152
|
-
void this.areaOfInterest
|
|
1275
|
+
void this.areaOfInterest.resync();
|
|
1153
1276
|
});
|
|
1154
1277
|
const onDisconnected = this.syncWebSocket.subscribe('disconnected', () => {
|
|
1155
1278
|
this.syncClient.disconnect();
|
|
@@ -1209,7 +1332,7 @@ export class BaseSyncedStore {
|
|
|
1209
1332
|
this.objectPool.clear();
|
|
1210
1333
|
};
|
|
1211
1334
|
const onSessionError = this.syncWebSocket.subscribe('session_error', (error) => {
|
|
1212
|
-
// WS analog of HTTP's `apikey_expired` (see
|
|
1335
|
+
// WS analog of HTTP's `apikey_expired` (see AbloSessionError.
|
|
1213
1336
|
// isSessionErrorResponse): the hub's keepalive reaper closes sockets
|
|
1214
1337
|
// whose SHORT-LIVED access credential (`ek_`/`rk_`) passed its expiry
|
|
1215
1338
|
// with `4001 credential_expired`. That is re-mintable from the
|
|
@@ -1218,12 +1341,12 @@ export class BaseSyncedStore {
|
|
|
1218
1341
|
// Only a mint that answers `null` (the login itself is gone) falls
|
|
1219
1342
|
// through to the terminal path. Without this branch, every credential
|
|
1220
1343
|
// TTL elapse wedged the socket behind the write-once session latch.
|
|
1221
|
-
if (
|
|
1344
|
+
if (AbloSessionError.isSessionError(error) && isAccessCredentialExpiryCloseReason(error.message)) {
|
|
1222
1345
|
getContext().observability.breadcrumb('WebSocket closed for expired access credential — re-minting', 'sync.websocket', 'warning');
|
|
1223
1346
|
// Un-latch BEFORE the async mint so the FSM's own recovery
|
|
1224
1347
|
// (probe → refreshing_credential → reconnect) is never blocked on
|
|
1225
1348
|
// our .then() ordering.
|
|
1226
|
-
this.syncWebSocket
|
|
1349
|
+
this.syncWebSocket.clearSessionError();
|
|
1227
1350
|
void this.performCredentialRefresh().then((outcome) => {
|
|
1228
1351
|
if (outcome === 'refreshed') {
|
|
1229
1352
|
if (this.connectionManager) {
|
|
@@ -1236,8 +1359,8 @@ export class BaseSyncedStore {
|
|
|
1236
1359
|
// (createConnectionManager returns null for kind 'agent') —
|
|
1237
1360
|
// reconnect the socket directly; connect() reads the
|
|
1238
1361
|
// freshly-minted credential from the credential source.
|
|
1239
|
-
this.syncWebSocket
|
|
1240
|
-
this.syncWebSocket
|
|
1362
|
+
this.syncWebSocket.resetReconnectAttempts();
|
|
1363
|
+
this.syncWebSocket.connect();
|
|
1241
1364
|
}
|
|
1242
1365
|
return;
|
|
1243
1366
|
}
|
|
@@ -1245,7 +1368,7 @@ export class BaseSyncedStore {
|
|
|
1245
1368
|
// The mint endpoint rejected: the long-lived login is gone.
|
|
1246
1369
|
// Re-latch so writes reject with the permanent session type
|
|
1247
1370
|
// (see SyncWebSocket.notConnectedError) instead of parking.
|
|
1248
|
-
this.syncWebSocket
|
|
1371
|
+
this.syncWebSocket.setSessionErrorDetected();
|
|
1249
1372
|
handleTerminalSessionError(error);
|
|
1250
1373
|
}
|
|
1251
1374
|
// 'network_error' → transient mint failure. The WS_DISCONNECTED
|
|
@@ -1271,15 +1394,16 @@ export class BaseSyncedStore {
|
|
|
1271
1394
|
getContext().logger.debug('[BaseSyncedStore] WebSocket reconnection gave up', { attempts });
|
|
1272
1395
|
this.updateSyncStatus({ state: 'reconnecting' });
|
|
1273
1396
|
});
|
|
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
|
-
|
|
1397
|
+
this.disposers.push(onConnected, onDisconnected, onReconnecting, onDelta, onDeltaBatch, onBootstrapRequired, onBootstrapData, onPresenceUpdate, onError, onSessionError, onHandshakeFailed, onReconnectFailed, () => { this.areaOfInterest.dispose(); });
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Build and start the connection FSM. The `onConnectionEvent` hook is the
|
|
1401
|
+
* bridge — WS events fire the hook, the hook forwards into the FSM. Called
|
|
1402
|
+
* from `setupWebSocketSync` because the FSM's shape depends on the resolved
|
|
1403
|
+
* participant kind (agents get none — see {@link createConnectionManager}).
|
|
1404
|
+
*/
|
|
1405
|
+
startConnectionManager(kind) {
|
|
1406
|
+
this.connectionManager = this.createConnectionManager(kind);
|
|
1283
1407
|
if (this.connectionManager) {
|
|
1284
1408
|
const manager = this.connectionManager;
|
|
1285
1409
|
// Preserve any externally-set onConnectionEvent — chain rather
|
|
@@ -1310,7 +1434,7 @@ export class BaseSyncedStore {
|
|
|
1310
1434
|
onReconnect: () => this.performReconnect(),
|
|
1311
1435
|
onRefreshCredential: () => this.performCredentialRefresh(),
|
|
1312
1436
|
onSessionExpired: () => {
|
|
1313
|
-
const err = new
|
|
1437
|
+
const err = new AbloSessionError('Session expired');
|
|
1314
1438
|
for (const listener of this.sessionErrorListeners) {
|
|
1315
1439
|
try {
|
|
1316
1440
|
listener(err);
|
|
@@ -1319,7 +1443,7 @@ export class BaseSyncedStore {
|
|
|
1319
1443
|
}
|
|
1320
1444
|
},
|
|
1321
1445
|
onDisconnectWebSocket: () => {
|
|
1322
|
-
this.syncWebSocket
|
|
1446
|
+
this.syncWebSocket.disconnect();
|
|
1323
1447
|
},
|
|
1324
1448
|
// Mirror FSM transitions into the visible `syncStatus.state` so
|
|
1325
1449
|
// the UI can show "Reconnecting…" while the FSM cycles through
|
|
@@ -1371,12 +1495,6 @@ export class BaseSyncedStore {
|
|
|
1371
1495
|
},
|
|
1372
1496
|
});
|
|
1373
1497
|
}
|
|
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
1498
|
}
|
|
1381
1499
|
// ── Delta processing pipeline ─────────────────────────────────────────────
|
|
1382
1500
|
//
|
|
@@ -1415,7 +1533,7 @@ export class BaseSyncedStore {
|
|
|
1415
1533
|
applyDeltaBatchToPool: (results) => {
|
|
1416
1534
|
this.syncClient.applyDeltaBatchToPool(results, (name, data) => this.enrichRelations(name, data));
|
|
1417
1535
|
},
|
|
1418
|
-
acknowledge: (syncId) => { this.syncWebSocket
|
|
1536
|
+
acknowledge: (syncId) => { this.syncWebSocket.acknowledge(syncId); },
|
|
1419
1537
|
get objectPool() { return store.objectPool; },
|
|
1420
1538
|
// Dynamic-dispatch hooks — protected override points on this class.
|
|
1421
1539
|
getStateFields: (modelName) => this.getStateFields(modelName),
|
|
@@ -1525,7 +1643,7 @@ export class BaseSyncedStore {
|
|
|
1525
1643
|
* schema build time) to find children. The previous implementation did
|
|
1526
1644
|
* `getByType(ctor).filter(e => e.toJSON()[foreignKey] === parentId)` —
|
|
1527
1645
|
* a full pool scan per child model + a `toJSON()` allocation per
|
|
1528
|
-
* candidate. For a
|
|
1646
|
+
* candidate. For a report delete with 10K blocks in the pool, that was
|
|
1529
1647
|
* 10K toJSON allocations per cascade level. The FK-indexed lookup
|
|
1530
1648
|
* skips both the scan AND the allocation.
|
|
1531
1649
|
*/
|
|
@@ -1584,7 +1702,7 @@ export class BaseSyncedStore {
|
|
|
1584
1702
|
* Save a model (create or update).
|
|
1585
1703
|
*
|
|
1586
1704
|
* Accepts any entity shape with `{ id: string }` so consumers can pass the
|
|
1587
|
-
* Zod-inferred model types from `
|
|
1705
|
+
* Zod-inferred model types from `Model<Schema, K>` without knowing
|
|
1588
1706
|
* about the internal `Model` base class. At runtime, every entity reaching
|
|
1589
1707
|
* this method came through the object pool (via `store.create`, a query
|
|
1590
1708
|
* accessor, or an optimistic insert) and IS a `Model` instance — the one
|
|
@@ -1607,7 +1725,7 @@ export class BaseSyncedStore {
|
|
|
1607
1725
|
this.syncClient.update(model);
|
|
1608
1726
|
}
|
|
1609
1727
|
}
|
|
1610
|
-
/** Save with an atomic server mutation (e.g.,
|
|
1728
|
+
/** Save with an atomic server mutation (e.g., createSectionWithBlocks) */
|
|
1611
1729
|
async saveWithAtomicMutation(model, mutation) {
|
|
1612
1730
|
this.objectPool.add(model, ModelScope.live);
|
|
1613
1731
|
await mutation(this.syncClient.gql);
|
|
@@ -1632,7 +1750,7 @@ export class BaseSyncedStore {
|
|
|
1632
1750
|
this.syncClient.update(model);
|
|
1633
1751
|
}
|
|
1634
1752
|
// ── Query API ────────────────────────────────────────────────────────────
|
|
1635
|
-
// `ablo.<model>.
|
|
1753
|
+
// `ablo.<model>.local.retrieve` / `.local.list` is the read surface for
|
|
1636
1754
|
// application code. Custom mutators read transactionally through
|
|
1637
1755
|
// `tx.<model>`, backed by `createReaderActions`.
|
|
1638
1756
|
/** Retrieve a single entity by id. Synchronous pool read. */
|
|
@@ -1655,13 +1773,13 @@ export class BaseSyncedStore {
|
|
|
1655
1773
|
* Create a model instance locally, typed via the schema.
|
|
1656
1774
|
*
|
|
1657
1775
|
* ```ts
|
|
1658
|
-
* const
|
|
1659
|
-
* //
|
|
1776
|
+
* const ledger = store.create('ledgers', { name, reportId });
|
|
1777
|
+
* // ledger: Ledger | null — no cast needed
|
|
1660
1778
|
* ```
|
|
1661
1779
|
*
|
|
1662
1780
|
* The `typename` arg is the schema key (camelCase plural, e.g.
|
|
1663
|
-
* `'
|
|
1664
|
-
* `
|
|
1781
|
+
* `'ledgers'`); the returned instance has the
|
|
1782
|
+
* `Model<Schema, K>` shape including computeds + relation accessors.
|
|
1665
1783
|
* Wraps `pool.create(...)` — the underlying runtime is unchanged, just
|
|
1666
1784
|
* type-narrowed.
|
|
1667
1785
|
*/
|
|
@@ -1718,14 +1836,14 @@ export class BaseSyncedStore {
|
|
|
1718
1836
|
/**
|
|
1719
1837
|
* Get all models of a type. Returns Model[] honestly — callers that need
|
|
1720
1838
|
* narrow types should use `useAblo((ablo) => ablo.<model>.list(...))`
|
|
1721
|
-
* which does proper inference via `
|
|
1839
|
+
* which does proper inference via `Model<S, K>`.
|
|
1722
1840
|
*/
|
|
1723
1841
|
allModelsOfType(modelClass, scope) {
|
|
1724
1842
|
return this.objectPool.getByType(modelClass, scope ?? ModelScope.live);
|
|
1725
1843
|
}
|
|
1726
1844
|
/** Error handler for fire-and-forget flushPendingDeltas calls */
|
|
1727
1845
|
handleFlushError = (error) => {
|
|
1728
|
-
getContext().observability.
|
|
1846
|
+
getContext().observability.captureMutationFailure({
|
|
1729
1847
|
context: 'flush-pending-deltas',
|
|
1730
1848
|
modelName: 'batch',
|
|
1731
1849
|
modelId: 'batch',
|
|
@@ -1744,8 +1862,6 @@ export class BaseSyncedStore {
|
|
|
1744
1862
|
modelId: delta.modelId,
|
|
1745
1863
|
data: typeof delta.data === 'string' ? JSON.parse(delta.data) : delta.data,
|
|
1746
1864
|
});
|
|
1747
|
-
if (!dbResult)
|
|
1748
|
-
return;
|
|
1749
1865
|
// Track pending deletes for query filtering
|
|
1750
1866
|
if (dbResult.action === 'remove') {
|
|
1751
1867
|
this.pendingDeletes.add(dbResult.modelId);
|