@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
|
@@ -7,15 +7,115 @@
|
|
|
7
7
|
* shape it returns; {@link BootstrapOptions} configures it.
|
|
8
8
|
*/
|
|
9
9
|
import { getContext } from '../context.js';
|
|
10
|
-
import {
|
|
11
|
-
import { withAuthHeaders } from '../auth/credentialSource.js';
|
|
10
|
+
import { AbloError, AbloSessionError, AbloConnectionError, translateHttpError, toAbloError, isRetryableCode } from '../transaction/errors.js';
|
|
11
|
+
import { withAuthHeaders } from '../transaction/auth/credentialSource.js';
|
|
12
|
+
import { classifySchemaDrift, describeSchemaDrift, } from './schemaDrift.js';
|
|
12
13
|
// SyncObservability replaced by getContext().observability
|
|
13
14
|
import { parseBootstrapResponse } from './schemas.js';
|
|
15
|
+
/**
|
|
16
|
+
* Rows per page for the chunked cold-start bootstrap. Matches the server's
|
|
17
|
+
* hard cap on the `limit` query parameter — asking for more is silently
|
|
18
|
+
* clamped, so this is the largest honest page.
|
|
19
|
+
*/
|
|
20
|
+
const PAGE_LIMIT = 5000;
|
|
21
|
+
/**
|
|
22
|
+
* Runaway guard for the per-model paging loop: a server that keeps returning
|
|
23
|
+
* a `nextCursor` past this many pages is looping, not paginating. At
|
|
24
|
+
* {@link PAGE_LIMIT} rows per page this allows a million rows per model
|
|
25
|
+
* before the loop is declared broken.
|
|
26
|
+
*/
|
|
27
|
+
const MAX_PAGES_PER_MODEL = 200;
|
|
28
|
+
/** How many model chunks a cold start fetches at once. */
|
|
29
|
+
const CHUNK_CONCURRENCY = 3;
|
|
30
|
+
/**
|
|
31
|
+
* The reason handed to `abort()` when a request is stopped deliberately —
|
|
32
|
+
* superseded by a newer bootstrap, or abandoned because the bootstrap it
|
|
33
|
+
* belonged to had already failed elsewhere.
|
|
34
|
+
*/
|
|
35
|
+
const cancelled = (why) => new AbloConnectionError(why, { code: 'bootstrap_cancelled' });
|
|
36
|
+
/** Matches by `name` rather than `instanceof`: an abort that crosses a worker
|
|
37
|
+
* boundary is structured-cloned, which drops the prototype. */
|
|
38
|
+
const isAbortError = (value) => typeof value === 'object' &&
|
|
39
|
+
value !== null &&
|
|
40
|
+
'name' in value &&
|
|
41
|
+
value.name === 'AbortError';
|
|
42
|
+
/**
|
|
43
|
+
* What a failed request should report.
|
|
44
|
+
*
|
|
45
|
+
* A fetch aborted *with a reason* rejects with that exact reason object, so a
|
|
46
|
+
* deliberate cancellation and a watchdog firing both arrive here already typed
|
|
47
|
+
* and pass straight through — which is the whole point of passing one. Only a
|
|
48
|
+
* bare abort needs translating: a signal aborted with no reason, the browser's
|
|
49
|
+
* stop button, a closing tab. That case is the one that genuinely means the
|
|
50
|
+
* transfer died, so it becomes a retryable timeout.
|
|
51
|
+
*
|
|
52
|
+
* The signal's reason is preferred over the thrown value because a pre-aborted
|
|
53
|
+
* signal rejects before any request is made, and because interior code may have
|
|
54
|
+
* wrapped the rejection on its way out.
|
|
55
|
+
*/
|
|
56
|
+
function classifyRequestFailure(error, controller, diedMessage) {
|
|
57
|
+
const reason = controller.signal.aborted ? controller.signal.reason : error;
|
|
58
|
+
if (reason instanceof AbloError)
|
|
59
|
+
return reason;
|
|
60
|
+
if (isAbortError(reason)) {
|
|
61
|
+
return new AbloConnectionError(diedMessage, {
|
|
62
|
+
code: 'bootstrap_fetch_timeout',
|
|
63
|
+
...(reason instanceof Error ? { cause: reason } : {}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
67
|
+
}
|
|
14
68
|
export class BootstrapFetcher {
|
|
15
69
|
options;
|
|
16
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Every in-flight request's controller, tagged with the lane it belongs to. A
|
|
72
|
+
* registry rather than a single field because a chunked cold start runs
|
|
73
|
+
* several model fetches concurrently — aborting one request (its own
|
|
74
|
+
* TTFB/stall watchdog) must never take its siblings down, while
|
|
75
|
+
* {@link abort} takes down all of them.
|
|
76
|
+
*/
|
|
77
|
+
activeControllers = new Map();
|
|
78
|
+
/**
|
|
79
|
+
* Non-scoped bootstraps currently running, keyed by request identity. A
|
|
80
|
+
* second call for the same snapshot joins the one already in flight rather
|
|
81
|
+
* than cancelling and restarting it — see {@link fetchBootstrap}.
|
|
82
|
+
*/
|
|
83
|
+
flights = new Map();
|
|
17
84
|
/** Warn about schema drift at most once per helper. */
|
|
18
85
|
schemaDriftWarned = false;
|
|
86
|
+
/**
|
|
87
|
+
* Abort every in-flight request in `lane` — or in every lane when none is
|
|
88
|
+
* given — with an explicit reason.
|
|
89
|
+
*
|
|
90
|
+
* The reason is load-bearing, not decoration. `fetch` rejects with the exact
|
|
91
|
+
* value handed to `abort()`, so passing a typed error is what lets the retry
|
|
92
|
+
* loop below tell a deliberate cancellation apart from a dead connection. A
|
|
93
|
+
* bare `abort()` produces an `AbortError` indistinguishable from the one the
|
|
94
|
+
* browser's stop button produces, and a retry loop that cannot tell them
|
|
95
|
+
* apart re-issues the requests it just killed.
|
|
96
|
+
*/
|
|
97
|
+
cancelActive(reason, lane) {
|
|
98
|
+
for (const [controller, controllerLane] of this.activeControllers) {
|
|
99
|
+
if (lane !== undefined && controllerLane !== lane)
|
|
100
|
+
continue;
|
|
101
|
+
controller.abort(reason);
|
|
102
|
+
this.activeControllers.delete(controller);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The longest a single bootstrap can run before every watchdog below has
|
|
107
|
+
* necessarily fired, derived from those watchdogs rather than guessed. A
|
|
108
|
+
* caller wanting an outer deadline reads this instead of picking a number,
|
|
109
|
+
* so it cannot set one shorter than the work it wraps. A cold start pages
|
|
110
|
+
* through its models {@link CHUNK_CONCURRENCY} at a time; each request may
|
|
111
|
+
* spend `fetchTimeout` waiting for response headers and `stallTimeout`
|
|
112
|
+
* waiting for the next body chunk, and may be retried `maxRetries` times.
|
|
113
|
+
*/
|
|
114
|
+
get budgetMs() {
|
|
115
|
+
const models = Math.max(this.options.instantModels?.length ?? 1, 1);
|
|
116
|
+
const waves = Math.ceil(models / CHUNK_CONCURRENCY);
|
|
117
|
+
return (waves * (this.options.fetchTimeout + this.options.stallTimeout) * this.options.maxRetries);
|
|
118
|
+
}
|
|
19
119
|
get baseUrl() {
|
|
20
120
|
return this.options.baseUrl;
|
|
21
121
|
}
|
|
@@ -52,6 +152,60 @@ export class BootstrapFetcher {
|
|
|
52
152
|
this.schemaDriftWarned = true;
|
|
53
153
|
const org = this.options.organizationId;
|
|
54
154
|
const where = org ? `${this.baseUrl} (org ${org})` : this.baseUrl;
|
|
155
|
+
// The whole-schema hashes differ — but that alone can't distinguish "the
|
|
156
|
+
// server gained models this build never touches" (fine, say nothing) from
|
|
157
|
+
// "a model this client uses moved" (name it). Resolve the semantic answer
|
|
158
|
+
// from the server's per-model surface before speaking; fall back to the
|
|
159
|
+
// hash message only when that surface is unavailable (older server,
|
|
160
|
+
// network hiccup). Fire-and-forget: never blocks or fails the bootstrap.
|
|
161
|
+
const clientModels = getContext().config.expectedModelHashes;
|
|
162
|
+
if (clientModels && Object.keys(clientModels).length > 0) {
|
|
163
|
+
void this.resolveSemanticDrift(clientModels, clientHash, serverHash, where);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.warnWholeHashDrift(clientHash, serverHash, where);
|
|
167
|
+
}
|
|
168
|
+
/** Fetch the server's per-model schema surface and warn precisely — or stay
|
|
169
|
+
* silent when every model this client declares matches (additive lead). */
|
|
170
|
+
async resolveSemanticDrift(clientModels, clientHash, serverHash, where) {
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetch(`${this.options.baseUrl}/schema`, {
|
|
173
|
+
method: 'GET',
|
|
174
|
+
headers: withAuthHeaders(this.options.getAuthToken, {}, this.options.authToken),
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok)
|
|
177
|
+
throw new Error(`schema read-back ${res.status}`);
|
|
178
|
+
const body = (await res.json());
|
|
179
|
+
const models = Array.isArray(body.models)
|
|
180
|
+
? body.models.flatMap((m) => {
|
|
181
|
+
const entry = m;
|
|
182
|
+
return typeof entry.key === 'string'
|
|
183
|
+
? [{ key: entry.key, ...(typeof entry.hash === 'string' ? { hash: entry.hash } : {}) }]
|
|
184
|
+
: [];
|
|
185
|
+
})
|
|
186
|
+
: [];
|
|
187
|
+
const finding = classifySchemaDrift(clientModels, models);
|
|
188
|
+
if (finding.kind === 'aligned')
|
|
189
|
+
return; // additive server lead — not this client's concern
|
|
190
|
+
if (finding.kind !== 'unknown') {
|
|
191
|
+
getContext().logger.warn(describeSchemaDrift(finding, where), {
|
|
192
|
+
clientSchemaHash: clientHash,
|
|
193
|
+
serverSchemaHash: serverHash,
|
|
194
|
+
serverUrl: this.baseUrl,
|
|
195
|
+
...(finding.kind === 'unpushed'
|
|
196
|
+
? { unpushedModels: finding.models }
|
|
197
|
+
: { changedModels: finding.models, unpushedModels: finding.unpushed }),
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
/* surface unavailable — fall through to the hash message */
|
|
204
|
+
}
|
|
205
|
+
this.warnWholeHashDrift(clientHash, serverHash, where);
|
|
206
|
+
}
|
|
207
|
+
warnWholeHashDrift(clientHash, serverHash, where) {
|
|
208
|
+
const org = this.options.organizationId;
|
|
55
209
|
// Self-brand the message ("Ablo:") rather than rely on the default logger's
|
|
56
210
|
// `[Ablo]` namespace — consumers wiring their own logger (pino, etc.) lose
|
|
57
211
|
// that prefix, and a drift warning that reads like the app's own log is
|
|
@@ -80,7 +234,11 @@ export class BootstrapFetcher {
|
|
|
80
234
|
syncGroups: [],
|
|
81
235
|
maxRetries: 3,
|
|
82
236
|
retryDelay: 1000,
|
|
83
|
-
|
|
237
|
+
// Time-to-first-byte bound only. The server currently materializes the
|
|
238
|
+
// whole snapshot before sending headers, so a cold start on a large org
|
|
239
|
+
// legitimately needs more than a "fail fast" allowance here.
|
|
240
|
+
fetchTimeout: 20_000,
|
|
241
|
+
stallTimeout: 15_000,
|
|
84
242
|
...options,
|
|
85
243
|
baseUrl: options.baseUrl ?? 'http://localhost:8080/api',
|
|
86
244
|
// Reading the deprecated `organizationId` is deliberate: it preserves the
|
|
@@ -129,6 +287,56 @@ export class BootstrapFetcher {
|
|
|
129
287
|
* request.
|
|
130
288
|
*/
|
|
131
289
|
syncGroupsOverride) {
|
|
290
|
+
// A scoped hydrate answers a different question than the full bootstrap and
|
|
291
|
+
// runs in its own lane: it never joins one, and is never superseded by one.
|
|
292
|
+
if (syncGroupsOverride)
|
|
293
|
+
return this.runBootstrap(lastSyncId, syncGroupsOverride);
|
|
294
|
+
// Single-flight. Three callers reach this independently — first load,
|
|
295
|
+
// background refresh, and reconnect — and before this they raced: each new
|
|
296
|
+
// call cancelled whatever was running and started over, so a socket that
|
|
297
|
+
// reconnected mid-cold-start restarted the whole snapshot, repeatedly. The
|
|
298
|
+
// same request now joins the one in flight instead.
|
|
299
|
+
const key = this.flightKey(lastSyncId);
|
|
300
|
+
const joined = this.flights.get(key);
|
|
301
|
+
if (joined) {
|
|
302
|
+
getContext().logger.debug('Joining the bootstrap already in flight', { key });
|
|
303
|
+
return joined;
|
|
304
|
+
}
|
|
305
|
+
// A request for something else genuinely does supersede: the running one is
|
|
306
|
+
// not the answer being asked for. Retire it from the registry first, so a
|
|
307
|
+
// caller arriving in the same tick cannot join a flight that is dying.
|
|
308
|
+
if (this.flights.size > 0) {
|
|
309
|
+
this.flights.clear();
|
|
310
|
+
this.cancelActive(cancelled('Superseded by a newer bootstrap request'), 'bootstrap');
|
|
311
|
+
}
|
|
312
|
+
const flight = this.runBootstrap(lastSyncId);
|
|
313
|
+
this.flights.set(key, flight);
|
|
314
|
+
// The cleanup chain is terminated with `catch` so this derived promise can
|
|
315
|
+
// never surface as an unhandled rejection even when every caller handled the
|
|
316
|
+
// failure, and the delete is guarded by identity so a flight registered
|
|
317
|
+
// after a supersede is not evicted by its predecessor's cleanup.
|
|
318
|
+
void flight
|
|
319
|
+
.catch(() => undefined)
|
|
320
|
+
.finally(() => {
|
|
321
|
+
if (this.flights.get(key) === flight)
|
|
322
|
+
this.flights.delete(key);
|
|
323
|
+
});
|
|
324
|
+
return flight;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* The identity of a bootstrap request: everything that determines its answer.
|
|
328
|
+
* Two calls with the same key are asking the same question, so the second can
|
|
329
|
+
* take the first's result.
|
|
330
|
+
*/
|
|
331
|
+
flightKey(lastSyncId) {
|
|
332
|
+
return JSON.stringify({
|
|
333
|
+
lastSyncId: lastSyncId !== undefined && lastSyncId > 0 ? lastSyncId : 0,
|
|
334
|
+
syncGroups: [...this.options.syncGroups].sort(),
|
|
335
|
+
models: [...(this.options.instantModels ?? [])].sort(),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
/** One bootstrap, start to finish. {@link fetchBootstrap} owns whether it runs. */
|
|
339
|
+
async runBootstrap(lastSyncId, syncGroupsOverride) {
|
|
132
340
|
// organizationId omitted — server reads it from auth identity.
|
|
133
341
|
// See `fetchBootstrapWithETag` for the full rationale.
|
|
134
342
|
const params = new URLSearchParams();
|
|
@@ -172,30 +380,78 @@ export class BootstrapFetcher {
|
|
|
172
380
|
});
|
|
173
381
|
}
|
|
174
382
|
getContext().logger.info('Fetching fresh bootstrap data', { url });
|
|
175
|
-
|
|
383
|
+
const lane = syncGroupsOverride ? 'scoped' : 'bootstrap';
|
|
384
|
+
// Chunk a COLD start by model: each instant model is its own request, so
|
|
385
|
+
// one giant model can't make the whole snapshot undeliverable, and a
|
|
386
|
+
// dropped connection costs one model, not everything. Each chunk is
|
|
387
|
+
// consistent at its own sync position; the merge anchors at the MINIMUM
|
|
388
|
+
// position, and the regular WS catch-up (`sync_request` → delta replay)
|
|
389
|
+
// closes the skew — full-row deltas make the overlapping re-apply
|
|
390
|
+
// convergent. Warm partials, scoped hydrates, and clients without a
|
|
391
|
+
// model list (server returns everything) stay on the single request.
|
|
392
|
+
const instantModels = this.options.instantModels ?? [];
|
|
393
|
+
const chunked = (lastSyncId === undefined || lastSyncId <= 0) &&
|
|
394
|
+
!syncGroupsOverride &&
|
|
395
|
+
instantModels.length > 1;
|
|
396
|
+
try {
|
|
397
|
+
const data = chunked
|
|
398
|
+
? await this.fetchChunkedBootstrap(instantModels, this.options.syncGroups)
|
|
399
|
+
: await this.fetchWithRetries(url, lane);
|
|
400
|
+
getContext().logger.info('Bootstrap data fetched', {
|
|
401
|
+
type: data.type,
|
|
402
|
+
lastSyncId: data.lastSyncId,
|
|
403
|
+
chunked,
|
|
404
|
+
modelCount: data.models ? Object.keys(data.models).length : 0,
|
|
405
|
+
deltaCount: data.deltaCount ?? 0,
|
|
406
|
+
totalItems: data.models
|
|
407
|
+
? Object.values(data.models).reduce((sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0), 0)
|
|
408
|
+
: 0,
|
|
409
|
+
});
|
|
410
|
+
// Persist for offline fallback
|
|
411
|
+
if (this.options.cacheScope) {
|
|
412
|
+
this.saveCachedBootstrap(this.options.cacheScope, data);
|
|
413
|
+
}
|
|
414
|
+
return data;
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
// Session and non-retryable errors already failed fast inside the
|
|
418
|
+
// retry loop; they must ALSO skip the cached fallback (a stale
|
|
419
|
+
// snapshot is not an answer to "your credential is invalid").
|
|
420
|
+
if (AbloSessionError.isSessionError(error)) {
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
const ablo = toAbloError(error);
|
|
424
|
+
if (ablo.code && !isRetryableCode(ablo.code)) {
|
|
425
|
+
throw ablo;
|
|
426
|
+
}
|
|
427
|
+
// Transient failure after exhausting retries → cached fallback.
|
|
428
|
+
const cached = this.options.cacheScope
|
|
429
|
+
? this.loadCachedBootstrap(this.options.cacheScope)
|
|
430
|
+
: null;
|
|
431
|
+
if (cached) {
|
|
432
|
+
getContext().observability.breadcrumb('Bootstrap cache fallback', 'sync.bootstrap', 'warning', {
|
|
433
|
+
error: ablo.message,
|
|
434
|
+
});
|
|
435
|
+
return cached;
|
|
436
|
+
}
|
|
437
|
+
throw ablo;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* One bootstrap URL, fetched with backoff. Session errors and other
|
|
442
|
+
* non-retryable failures throw immediately; only transient failures
|
|
443
|
+
* (5xx, 429, timeouts, network blips) consume attempts. A cancellation is
|
|
444
|
+
* deliberate and therefore non-retryable — it leaves through the same gate.
|
|
445
|
+
*/
|
|
446
|
+
async fetchWithRetries(url, lane) {
|
|
176
447
|
let lastError = null;
|
|
177
448
|
for (let attempt = 0; attempt < this.options.maxRetries; attempt++) {
|
|
178
449
|
try {
|
|
179
|
-
|
|
180
|
-
getContext().logger.info('Bootstrap data fetched', {
|
|
181
|
-
type: data.type,
|
|
182
|
-
lastSyncId: data.lastSyncId,
|
|
183
|
-
modelCount: data.models ? Object.keys(data.models).length : 0,
|
|
184
|
-
deltaCount: data.deltaCount ?? 0,
|
|
185
|
-
totalItems: data.models
|
|
186
|
-
? Object.values(data.models).reduce((sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0), 0)
|
|
187
|
-
: 0,
|
|
188
|
-
});
|
|
189
|
-
// Persist for offline fallback
|
|
190
|
-
if (this.options.cacheScope) {
|
|
191
|
-
this.saveCachedBootstrap(this.options.cacheScope, data);
|
|
192
|
-
}
|
|
193
|
-
return data;
|
|
450
|
+
return await this.fetchOnce(url, lane);
|
|
194
451
|
}
|
|
195
452
|
catch (error) {
|
|
196
453
|
// SessionError should NOT be retried - the session is invalid and needs re-authentication
|
|
197
|
-
|
|
198
|
-
if (SyncSessionError.isSessionError(error)) {
|
|
454
|
+
if (AbloSessionError.isSessionError(error)) {
|
|
199
455
|
getContext().observability.breadcrumb('Bootstrap session error - redirecting to sign-in', 'sync.bootstrap', 'warning', {
|
|
200
456
|
statusCode: (error).statusCode,
|
|
201
457
|
});
|
|
@@ -221,22 +477,78 @@ export class BootstrapFetcher {
|
|
|
221
477
|
}
|
|
222
478
|
}
|
|
223
479
|
}
|
|
224
|
-
// On error, attempt cached fallback (but NOT for session errors - already handled above)
|
|
225
|
-
const cached = this.options.cacheScope
|
|
226
|
-
? this.loadCachedBootstrap(this.options.cacheScope)
|
|
227
|
-
: null;
|
|
228
|
-
if (cached) {
|
|
229
|
-
getContext().observability.breadcrumb('Bootstrap cache fallback', 'sync.bootstrap', 'warning', {
|
|
230
|
-
error: lastError?.message,
|
|
231
|
-
});
|
|
232
|
-
return cached;
|
|
233
|
-
}
|
|
234
480
|
throw lastError
|
|
235
481
|
? toAbloError(lastError)
|
|
236
482
|
: new AbloConnectionError('Failed to fetch bootstrap data', {
|
|
237
483
|
code: 'bootstrap_fetch_timeout',
|
|
238
484
|
});
|
|
239
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Cold-start bootstrap, one request per instant model with a small
|
|
488
|
+
* concurrency cap. Any chunk's terminal failure fails the whole
|
|
489
|
+
* bootstrap (a partial snapshot must never masquerade as a full one)
|
|
490
|
+
* and cancels its siblings.
|
|
491
|
+
*/
|
|
492
|
+
async fetchChunkedBootstrap(models, syncGroups) {
|
|
493
|
+
getContext().logger.info('Bootstrap chunked by model', {
|
|
494
|
+
models: models.length,
|
|
495
|
+
});
|
|
496
|
+
const queue = [...models];
|
|
497
|
+
const chunks = [];
|
|
498
|
+
// Shared by the concurrent workers below, so it is deliberately re-read
|
|
499
|
+
// after `await` points where a sibling may have set it. Held on an object
|
|
500
|
+
// rather than in a `let`: the guard inside the worker narrows a plain
|
|
501
|
+
// binding to `null` for the rest of the loop body, and the compiler has no
|
|
502
|
+
// way to know a sibling can overwrite it mid-await.
|
|
503
|
+
const firstFailure = { error: null };
|
|
504
|
+
const worker = async () => {
|
|
505
|
+
for (;;) {
|
|
506
|
+
const model = queue.shift();
|
|
507
|
+
if (model === undefined || firstFailure.error !== null)
|
|
508
|
+
return;
|
|
509
|
+
try {
|
|
510
|
+
// Page through the model: each request is bounded to PAGE_LIMIT
|
|
511
|
+
// rows, so no single response grows with the model's size. A
|
|
512
|
+
// server without paging ignores `limit` and returns the whole
|
|
513
|
+
// model with no nextCursor — one page, previous behavior.
|
|
514
|
+
let cursor;
|
|
515
|
+
for (let pageNo = 0;; pageNo++) {
|
|
516
|
+
if (pageNo >= MAX_PAGES_PER_MODEL) {
|
|
517
|
+
throw new AbloConnectionError(`Bootstrap for model "${model}" exceeded ${MAX_PAGES_PER_MODEL} pages — the server keeps returning a next page`, { code: 'bootstrap_fetch_timeout' });
|
|
518
|
+
}
|
|
519
|
+
const params = new URLSearchParams();
|
|
520
|
+
syncGroups.forEach((group) => {
|
|
521
|
+
params.append('syncGroups', group);
|
|
522
|
+
});
|
|
523
|
+
params.append('models', model);
|
|
524
|
+
params.append('limit', String(PAGE_LIMIT));
|
|
525
|
+
if (cursor !== undefined)
|
|
526
|
+
params.append('cursor', cursor);
|
|
527
|
+
const url = `${this.options.baseUrl}/sync/bootstrap?${params.toString()}`;
|
|
528
|
+
const data = await this.fetchWithRetries(url, 'bootstrap');
|
|
529
|
+
chunks.push(data);
|
|
530
|
+
if (data.nextCursor === undefined)
|
|
531
|
+
break;
|
|
532
|
+
cursor = data.nextCursor;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
// First failure wins — a later sibling's error must not mask it.
|
|
537
|
+
firstFailure.error ??=
|
|
538
|
+
error instanceof Error ? error : new Error(String(error));
|
|
539
|
+
// The siblings are abandoned, not broken: the snapshot they belong to
|
|
540
|
+
// is already lost. Saying so in the abort reason is what keeps each
|
|
541
|
+
// of them from retrying a request nobody is waiting for any more.
|
|
542
|
+
this.cancelActive(cancelled(`Abandoned: the bootstrap chunk for "${model}" failed`), 'bootstrap');
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
await Promise.all(Array.from({ length: Math.min(CHUNK_CONCURRENCY, models.length) }, worker));
|
|
548
|
+
if (firstFailure.error !== null)
|
|
549
|
+
throw firstFailure.error;
|
|
550
|
+
return mergeBootstrapChunks(chunks);
|
|
551
|
+
}
|
|
240
552
|
/**
|
|
241
553
|
* Fetch bootstrap with ETag, returning 304 hints
|
|
242
554
|
*/
|
|
@@ -257,11 +569,20 @@ export class BootstrapFetcher {
|
|
|
257
569
|
// level where they own the cache-key namespace. The 304 branch below
|
|
258
570
|
// remains defensively in place for when a caller enables revalidation.
|
|
259
571
|
const headers = withAuthHeaders(this.options.getAuthToken, { 'Content-Type': 'application/json' }, this.options.authToken);
|
|
260
|
-
|
|
572
|
+
const controller = new AbortController();
|
|
573
|
+
this.activeControllers.set(controller, 'bootstrap');
|
|
574
|
+
try {
|
|
575
|
+
return await this.fetchWithETagUsing(url, headers, controller);
|
|
576
|
+
}
|
|
577
|
+
finally {
|
|
578
|
+
this.activeControllers.delete(controller);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async fetchWithETagUsing(url, headers, controller) {
|
|
261
582
|
const res = await fetch(url, {
|
|
262
583
|
method: 'GET',
|
|
263
584
|
headers,
|
|
264
|
-
signal:
|
|
585
|
+
signal: controller.signal,
|
|
265
586
|
});
|
|
266
587
|
const etag = res.headers.get('ETag');
|
|
267
588
|
if (res.status === 304) {
|
|
@@ -296,11 +617,11 @@ export class BootstrapFetcher {
|
|
|
296
617
|
translated.code === 'jwt_expired' ||
|
|
297
618
|
((res.status === 401 || res.status === 403) &&
|
|
298
619
|
translated.code === undefined)) {
|
|
299
|
-
throw new
|
|
620
|
+
throw new AbloSessionError(translated.message, res.status);
|
|
300
621
|
}
|
|
301
622
|
throw translated;
|
|
302
623
|
}
|
|
303
|
-
const data = parseBootstrapResponse(await
|
|
624
|
+
const data = parseBootstrapResponse(await this.readJsonWithStallGuard(res, controller));
|
|
304
625
|
this.warnOnSchemaDrift(data.schemaHash);
|
|
305
626
|
// Persist payload for offline
|
|
306
627
|
try {
|
|
@@ -316,19 +637,96 @@ export class BootstrapFetcher {
|
|
|
316
637
|
return { notModified: false, data, etag };
|
|
317
638
|
}
|
|
318
639
|
/**
|
|
319
|
-
*
|
|
640
|
+
* Read a response body as a stream under a progress watchdog: the stall
|
|
641
|
+
* timer re-arms on every chunk, so only a silent stream is aborted — a
|
|
642
|
+
* slow-but-moving download is never killed for total duration. A cold-start
|
|
643
|
+
* snapshot can be tens of megabytes; bounding its total transfer time was
|
|
644
|
+
* what trapped large orgs in an endless full-bootstrap retry loop.
|
|
645
|
+
*
|
|
646
|
+
* Falls back to `response.json()` when the response exposes no readable
|
|
647
|
+
* stream (empty bodies, some test doubles).
|
|
320
648
|
*/
|
|
321
|
-
async
|
|
322
|
-
|
|
323
|
-
if (
|
|
324
|
-
|
|
649
|
+
async readJsonWithStallGuard(response, controller) {
|
|
650
|
+
const body = response.body;
|
|
651
|
+
if (!body)
|
|
652
|
+
return response.json();
|
|
653
|
+
const reader = body.getReader();
|
|
654
|
+
const chunks = [];
|
|
655
|
+
let receivedBytes = 0;
|
|
656
|
+
let stallTimer;
|
|
657
|
+
// The watchdog must not depend on the stream being wired to the fetch
|
|
658
|
+
// signal (that plumbing is implementation-specific), so a stall races a
|
|
659
|
+
// rejection against each read instead of only aborting the controller.
|
|
660
|
+
let stallReject;
|
|
661
|
+
const stalled = new Promise((_, reject) => {
|
|
662
|
+
stallReject = reject;
|
|
663
|
+
});
|
|
664
|
+
// A stall can fire in the microtask gap between two read races; without a
|
|
665
|
+
// standing handler that would surface as an unhandled rejection.
|
|
666
|
+
stalled.catch(() => undefined);
|
|
667
|
+
const armStallTimer = () => {
|
|
668
|
+
clearTimeout(stallTimer);
|
|
669
|
+
stallTimer = setTimeout(() => {
|
|
670
|
+
getContext().observability.breadcrumb('Bootstrap download stalled', 'sync.bootstrap', 'warning', { receivedBytes, stallTimeoutMs: this.options.stallTimeout });
|
|
671
|
+
const stallError = new AbloConnectionError(`Bootstrap download stalled: no data received for ${this.options.stallTimeout}ms (${receivedBytes} bytes arrived before the stream went quiet)`, { code: 'bootstrap_fetch_timeout' });
|
|
672
|
+
stallReject?.(stallError);
|
|
673
|
+
// Then tear the transfer down: abort frees the socket under real
|
|
674
|
+
// fetch; cancel unblocks readers on streams not wired to the signal.
|
|
675
|
+
// Both carry the same error, so whichever path wins the race below
|
|
676
|
+
// reports one message rather than two descriptions of one stall.
|
|
677
|
+
controller.abort(stallError);
|
|
678
|
+
void reader.cancel().catch(() => undefined);
|
|
679
|
+
}, this.options.stallTimeout);
|
|
680
|
+
};
|
|
681
|
+
try {
|
|
682
|
+
armStallTimer();
|
|
683
|
+
for (;;) {
|
|
684
|
+
const { done, value } = await Promise.race([reader.read(), stalled]);
|
|
685
|
+
if (done)
|
|
686
|
+
break;
|
|
687
|
+
chunks.push(value);
|
|
688
|
+
receivedBytes += value.byteLength;
|
|
689
|
+
armStallTimer();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
throw classifyRequestFailure(error, controller, `Bootstrap download aborted after ${receivedBytes} bytes`);
|
|
694
|
+
}
|
|
695
|
+
finally {
|
|
696
|
+
clearTimeout(stallTimer);
|
|
325
697
|
}
|
|
326
|
-
|
|
698
|
+
const bytes = new Uint8Array(receivedBytes);
|
|
699
|
+
let offset = 0;
|
|
700
|
+
for (const chunk of chunks) {
|
|
701
|
+
bytes.set(chunk, offset);
|
|
702
|
+
offset += chunk.byteLength;
|
|
703
|
+
}
|
|
704
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Perform one fetch. The timeout here bounds time to response headers
|
|
708
|
+
* only; the body download is guarded by the stall watchdog in
|
|
709
|
+
* {@link readJsonWithStallGuard}. Superseding an older in-flight
|
|
710
|
+
* bootstrap is the caller's job ({@link fetchBootstrap} cancels the
|
|
711
|
+
* registry) — chunk requests run through here concurrently and must
|
|
712
|
+
* not cancel each other.
|
|
713
|
+
*/
|
|
714
|
+
async fetchOnce(url, lane) {
|
|
715
|
+
const controller = new AbortController();
|
|
716
|
+
this.activeControllers.set(controller, lane);
|
|
717
|
+
try {
|
|
718
|
+
return await this.fetchOnceWith(url, controller);
|
|
719
|
+
}
|
|
720
|
+
finally {
|
|
721
|
+
this.activeControllers.delete(controller);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async fetchOnceWith(url, controller) {
|
|
327
725
|
const timeoutId = setTimeout(() => {
|
|
328
726
|
getContext().observability.breadcrumb('Bootstrap fetch timeout', 'sync.bootstrap', 'warning', {
|
|
329
727
|
timeoutMs: this.options.fetchTimeout,
|
|
330
728
|
});
|
|
331
|
-
|
|
729
|
+
controller.abort(new AbloConnectionError(`Bootstrap fetch timed out after ${this.options.fetchTimeout}ms waiting for the server to respond`, { code: 'bootstrap_fetch_timeout' }));
|
|
332
730
|
}, this.options.fetchTimeout);
|
|
333
731
|
let response;
|
|
334
732
|
try {
|
|
@@ -339,17 +737,13 @@ export class BootstrapFetcher {
|
|
|
339
737
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
340
738
|
Pragma: 'no-cache',
|
|
341
739
|
}, this.options.authToken),
|
|
342
|
-
signal:
|
|
740
|
+
signal: controller.signal,
|
|
343
741
|
cache: 'no-store', // Force browser to not cache
|
|
344
742
|
});
|
|
345
743
|
}
|
|
346
744
|
catch (error) {
|
|
347
745
|
clearTimeout(timeoutId);
|
|
348
|
-
|
|
349
|
-
if (error instanceof Error && error.name === 'AbortError') {
|
|
350
|
-
throw new AbloConnectionError(`Bootstrap fetch timed out after ${this.options.fetchTimeout}ms`, { code: 'bootstrap_fetch_timeout', cause: error });
|
|
351
|
-
}
|
|
352
|
-
throw error;
|
|
746
|
+
throw classifyRequestFailure(error, controller, 'The bootstrap request was aborted before the server responded');
|
|
353
747
|
}
|
|
354
748
|
clearTimeout(timeoutId);
|
|
355
749
|
if (!response.ok) {
|
|
@@ -373,22 +767,14 @@ export class BootstrapFetcher {
|
|
|
373
767
|
translated.code === 'jwt_expired' ||
|
|
374
768
|
((response.status === 401 || response.status === 403) &&
|
|
375
769
|
translated.code === undefined)) {
|
|
376
|
-
throw new
|
|
770
|
+
throw new AbloSessionError(translated.message, response.status);
|
|
377
771
|
}
|
|
378
772
|
throw translated;
|
|
379
773
|
}
|
|
380
|
-
const data = parseBootstrapResponse(await
|
|
774
|
+
const data = parseBootstrapResponse(await this.readJsonWithStallGuard(response, controller));
|
|
381
775
|
this.warnOnSchemaDrift(data.schemaHash);
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
if (this.options.cacheScope) {
|
|
385
|
-
this.saveCachedBootstrap(this.options.cacheScope, data);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
catch {
|
|
389
|
-
// Offline persistence is best-effort; a failed cache write must not
|
|
390
|
-
// block returning the freshly fetched data.
|
|
391
|
-
}
|
|
776
|
+
// Offline caching happens in `fetchBootstrap` on the assembled result —
|
|
777
|
+
// caching here would let a single-model chunk overwrite the full snapshot.
|
|
392
778
|
return data;
|
|
393
779
|
}
|
|
394
780
|
/**
|
|
@@ -499,13 +885,16 @@ export class BootstrapFetcher {
|
|
|
499
885
|
}
|
|
500
886
|
}
|
|
501
887
|
/**
|
|
502
|
-
* Abort ongoing
|
|
888
|
+
* Abort every ongoing bootstrap request (including all chunks of a
|
|
889
|
+
* chunked cold start). Entity self-heal fetches are unaffected.
|
|
890
|
+
*
|
|
891
|
+
* The flight registry is cleared first and synchronously, so a caller that
|
|
892
|
+
* bootstraps again in the same tick starts a fresh request rather than
|
|
893
|
+
* joining the one being torn down.
|
|
503
894
|
*/
|
|
504
895
|
abort() {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
this.abortController = null;
|
|
508
|
-
}
|
|
896
|
+
this.flights.clear();
|
|
897
|
+
this.cancelActive(cancelled('Bootstrap aborted by its caller'));
|
|
509
898
|
}
|
|
510
899
|
/**
|
|
511
900
|
* Helper to delay execution
|
|
@@ -534,3 +923,40 @@ export class BootstrapFetcher {
|
|
|
534
923
|
}
|
|
535
924
|
}
|
|
536
925
|
}
|
|
926
|
+
/**
|
|
927
|
+
* Assemble per-model chunk responses into one full snapshot.
|
|
928
|
+
*
|
|
929
|
+
* Each chunk is internally consistent at its own sync position, and the
|
|
930
|
+
* positions differ (the chunks were served seconds apart). Anchoring the
|
|
931
|
+
* merged snapshot at the MINIMUM position turns that skew into an ordinary
|
|
932
|
+
* "briefly offline client": the WS catch-up replays every delta from the
|
|
933
|
+
* anchor, and since deltas carry full rows, re-applying one a later chunk
|
|
934
|
+
* already reflects converges to the same state. Anchoring at anything later
|
|
935
|
+
* would silently skip deltas for the earliest-fetched models.
|
|
936
|
+
*/
|
|
937
|
+
export function mergeBootstrapChunks(chunks) {
|
|
938
|
+
const models = {};
|
|
939
|
+
const failedModels = [];
|
|
940
|
+
let lastSyncId = Number.POSITIVE_INFINITY;
|
|
941
|
+
let timestamp = 0;
|
|
942
|
+
let schemaHash;
|
|
943
|
+
for (const chunk of chunks) {
|
|
944
|
+
// Concatenate per model: pages of one model arrive as separate chunks.
|
|
945
|
+
for (const [name, rows] of Object.entries(chunk.models ?? {})) {
|
|
946
|
+
(models[name] ??= []).push(...rows);
|
|
947
|
+
}
|
|
948
|
+
if (chunk.failedModels)
|
|
949
|
+
failedModels.push(...chunk.failedModels);
|
|
950
|
+
lastSyncId = Math.min(lastSyncId, chunk.lastSyncId);
|
|
951
|
+
timestamp = Math.max(timestamp, chunk.timestamp);
|
|
952
|
+
schemaHash ??= chunk.schemaHash;
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
type: 'full',
|
|
956
|
+
lastSyncId: Number.isFinite(lastSyncId) ? lastSyncId : 0,
|
|
957
|
+
models,
|
|
958
|
+
...(failedModels.length > 0 ? { failedModels } : {}),
|
|
959
|
+
timestamp,
|
|
960
|
+
...(schemaHash !== undefined ? { schemaHash } : {}),
|
|
961
|
+
};
|
|
962
|
+
}
|