@abloatai/ablo 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -3
- package/CHANGELOG.md +34 -0
- package/README.md +104 -88
- package/dist/BaseSyncedStore.d.ts +140 -266
- package/dist/BaseSyncedStore.js +338 -739
- package/dist/Database.d.ts +62 -77
- package/dist/Database.js +106 -127
- package/dist/{ObjectPool.d.ts → InstanceCache.d.ts} +10 -13
- package/dist/{ObjectPool.js → InstanceCache.js} +91 -83
- package/dist/LazyReferenceCollection.d.ts +11 -15
- package/dist/LazyReferenceCollection.js +16 -15
- package/dist/Model.d.ts +37 -52
- package/dist/Model.js +52 -69
- package/dist/ModelRegistry.d.ts +46 -25
- package/dist/ModelRegistry.js +32 -30
- package/dist/NetworkMonitor.d.ts +5 -6
- package/dist/NetworkMonitor.js +6 -7
- package/dist/SyncClient.d.ts +119 -109
- package/dist/SyncClient.js +303 -224
- package/dist/SyncEngineContext.d.ts +1 -3
- package/dist/SyncEngineContext.js +1 -2
- package/dist/adapters/alwaysOnline.d.ts +6 -8
- package/dist/adapters/alwaysOnline.js +6 -8
- package/dist/adapters/inMemoryStorage.d.ts +9 -9
- package/dist/adapters/inMemoryStorage.js +9 -9
- package/dist/agent/Agent.d.ts +39 -31
- package/dist/agent/Agent.js +35 -23
- package/dist/agent/index.d.ts +4 -4
- package/dist/agent/index.js +5 -5
- package/dist/agent/session.d.ts +47 -44
- package/dist/agent/session.js +37 -48
- package/dist/agent/types.d.ts +26 -31
- package/dist/agent/types.js +6 -7
- package/dist/ai-sdk/coordinatedTool.d.ts +108 -0
- package/dist/ai-sdk/{coordinated-tool.js → coordinatedTool.js} +44 -38
- package/dist/ai-sdk/coordinationContext.d.ts +46 -0
- package/dist/ai-sdk/{coordination-context.js → coordinationContext.js} +30 -31
- package/dist/ai-sdk/index.d.ts +25 -22
- package/dist/ai-sdk/index.js +25 -22
- package/dist/ai-sdk/wrap.d.ts +7 -8
- package/dist/ai-sdk/wrap.js +2 -2
- package/dist/auth/credentialPolicy.d.ts +74 -71
- package/dist/auth/credentialPolicy.js +51 -56
- package/dist/auth/credentialSource.d.ts +7 -18
- package/dist/auth/credentialSource.js +10 -18
- package/dist/auth/index.d.ts +59 -58
- package/dist/auth/index.js +34 -40
- package/dist/auth/schemas.d.ts +5 -4
- package/dist/auth/schemas.js +5 -4
- package/dist/batching/index.d.ts +19 -21
- package/dist/batching/index.js +14 -17
- package/dist/cli.cjs +483 -369
- package/dist/client/Ablo.d.ts +107 -836
- package/dist/client/Ablo.js +174 -833
- package/dist/client/ApiClient.d.ts +44 -20
- package/dist/client/ApiClient.js +193 -44
- package/dist/client/auth.d.ts +51 -60
- package/dist/client/auth.js +137 -110
- package/dist/client/claimHeartbeatLoop.d.ts +50 -0
- package/dist/client/claimHeartbeatLoop.js +88 -0
- package/dist/client/consoleLogger.d.ts +35 -0
- package/dist/client/consoleLogger.js +44 -0
- package/dist/client/createInternalComponents.d.ts +14 -17
- package/dist/client/createInternalComponents.js +26 -31
- package/dist/client/createModelProxy.d.ts +130 -120
- package/dist/client/createModelProxy.js +158 -124
- package/dist/client/credentialEndpoint.d.ts +61 -0
- package/dist/client/credentialEndpoint.js +86 -0
- package/dist/client/functionalUpdate.d.ts +29 -27
- package/dist/client/functionalUpdate.js +21 -21
- package/dist/client/hostedEndpoints.d.ts +21 -0
- package/dist/client/hostedEndpoints.js +21 -0
- package/dist/client/httpClient.d.ts +58 -54
- package/dist/client/httpClient.js +29 -31
- package/dist/client/identity.d.ts +15 -20
- package/dist/client/identity.js +49 -59
- package/dist/client/modelRegistration.d.ts +10 -0
- package/dist/client/modelRegistration.js +301 -0
- package/dist/client/options.d.ts +373 -0
- package/dist/client/options.js +6 -0
- package/dist/client/registerDataSource.d.ts +9 -9
- package/dist/client/registerDataSource.js +15 -16
- package/dist/client/resourceTypes.d.ts +333 -0
- package/dist/client/resourceTypes.js +7 -0
- package/dist/client/schemaConfig.d.ts +44 -0
- package/dist/client/schemaConfig.js +176 -0
- package/dist/client/sessionMint.d.ts +17 -13
- package/dist/client/sessionMint.js +26 -31
- package/dist/client/validateAbloOptions.d.ts +12 -14
- package/dist/client/validateAbloOptions.js +9 -10
- package/dist/client/writeOptionsSchema.d.ts +18 -16
- package/dist/client/writeOptionsSchema.js +23 -20
- package/dist/client/wsMutationExecutor.d.ts +28 -0
- package/dist/client/wsMutationExecutor.js +71 -0
- package/dist/context.d.ts +6 -4
- package/dist/context.js +6 -7
- package/dist/coordination/index.d.ts +13 -4
- package/dist/coordination/index.js +29 -4
- package/dist/coordination/schema.d.ts +176 -128
- package/dist/coordination/schema.js +197 -133
- package/dist/coordination/trace.d.ts +9 -11
- package/dist/coordination/trace.js +13 -15
- package/dist/core/DatabaseManager.d.ts +5 -8
- package/dist/core/DatabaseManager.js +38 -40
- package/dist/core/QueryProcessor.d.ts +7 -9
- package/dist/core/QueryProcessor.js +27 -34
- package/dist/core/QueryView.d.ts +17 -5
- package/dist/core/QueryView.js +6 -7
- package/dist/core/StoreManager.d.ts +14 -16
- package/dist/core/StoreManager.js +26 -25
- package/dist/core/ViewRegistry.d.ts +5 -5
- package/dist/core/ViewRegistry.js +4 -4
- package/dist/core/index.d.ts +18 -13
- package/dist/core/index.js +32 -26
- package/dist/core/openIDBWithTimeout.d.ts +38 -36
- package/dist/core/openIDBWithTimeout.js +57 -54
- package/dist/core/queryUtils.d.ts +45 -0
- package/dist/core/queryUtils.js +69 -0
- package/dist/core/storeContract.d.ts +145 -0
- package/dist/core/storeContract.js +12 -0
- package/dist/environment.d.ts +28 -0
- package/dist/environment.js +21 -0
- package/dist/errorCodes.d.ts +118 -101
- package/dist/errorCodes.js +277 -260
- package/dist/errors.d.ts +170 -165
- package/dist/errors.js +161 -151
- package/dist/index.d.ts +30 -27
- package/dist/index.js +90 -82
- package/dist/interfaces/index.d.ts +108 -133
- package/dist/interfaces/index.js +5 -4
- package/dist/keys/index.d.ts +27 -29
- package/dist/keys/index.js +59 -49
- package/dist/mutators/RecordingTransaction.d.ts +16 -16
- package/dist/mutators/RecordingTransaction.js +31 -37
- package/dist/mutators/Transaction.d.ts +18 -26
- package/dist/mutators/Transaction.js +14 -20
- package/dist/mutators/UndoManager.d.ts +122 -131
- package/dist/mutators/UndoManager.js +149 -155
- package/dist/mutators/defineMutators.d.ts +24 -37
- package/dist/mutators/defineMutators.js +14 -20
- package/dist/mutators/inverseOp.d.ts +12 -15
- package/dist/mutators/inverseOp.js +12 -15
- package/dist/mutators/mutateActions.d.ts +10 -9
- package/dist/mutators/mutateActions.js +1 -1
- package/dist/mutators/readerActions.d.ts +9 -8
- package/dist/mutators/readerActions.js +2 -2
- package/dist/mutators/undoApply.d.ts +31 -27
- package/dist/mutators/undoApply.js +26 -24
- package/dist/policy/index.d.ts +5 -3
- package/dist/policy/index.js +5 -3
- package/dist/policy/types.d.ts +105 -101
- package/dist/policy/types.js +67 -66
- package/dist/query/client.d.ts +32 -16
- package/dist/query/client.js +103 -72
- package/dist/query/types.d.ts +37 -60
- package/dist/query/types.js +13 -33
- package/dist/react/AbloProvider.d.ts +7 -11
- package/dist/react/AbloProvider.js +24 -17
- package/dist/react/context.d.ts +27 -146
- package/dist/react/context.js +9 -10
- package/dist/react/index.d.ts +41 -42
- package/dist/react/index.js +37 -38
- package/dist/react/internalContext.d.ts +17 -19
- package/dist/react/useAblo.d.ts +23 -22
- package/dist/react/useAblo.js +17 -15
- package/dist/react/useCurrentUserId.d.ts +8 -7
- package/dist/react/useCurrentUserId.js +8 -7
- package/dist/react/useErrorListener.d.ts +7 -7
- package/dist/react/useErrorListener.js +11 -12
- package/dist/react/useMutationFailureListener.d.ts +8 -8
- package/dist/react/useMutationFailureListener.js +9 -9
- package/dist/react/useMutators.d.ts +11 -11
- package/dist/react/useMutators.js +10 -4
- package/dist/react/useReactive.js +2 -3
- package/dist/react/useSyncStatus.d.ts +4 -6
- package/dist/react/useUndoScope.d.ts +7 -9
- package/dist/react/useUndoScope.js +3 -3
- package/dist/schema/coordination.d.ts +21 -25
- package/dist/schema/coordination.js +21 -25
- package/dist/schema/ddl.d.ts +43 -39
- package/dist/schema/ddl.js +75 -68
- package/dist/schema/ddlLock.d.ts +35 -0
- package/dist/schema/ddlLock.js +46 -0
- package/dist/schema/diff.d.ts +99 -61
- package/dist/schema/diff.js +43 -34
- package/dist/schema/field.d.ts +37 -42
- package/dist/schema/field.js +36 -49
- package/dist/schema/generate.d.ts +12 -12
- package/dist/schema/generate.js +12 -12
- package/dist/schema/index.d.ts +5 -4
- package/dist/schema/index.js +29 -21
- package/dist/schema/model.d.ts +121 -146
- package/dist/schema/model.js +24 -35
- package/dist/schema/openapi.d.ts +10 -9
- package/dist/schema/openapi.js +7 -1
- package/dist/schema/queries.d.ts +30 -32
- package/dist/schema/queries.js +24 -25
- package/dist/schema/relation.d.ts +89 -99
- package/dist/schema/relation.js +13 -13
- package/dist/schema/residency.d.ts +38 -0
- package/dist/schema/residency.js +30 -0
- package/dist/schema/roles.d.ts +45 -27
- package/dist/schema/roles.js +52 -21
- package/dist/schema/schema.d.ts +36 -45
- package/dist/schema/schema.js +42 -39
- package/dist/schema/select.d.ts +13 -13
- package/dist/schema/select.js +13 -13
- package/dist/schema/serialize.d.ts +36 -39
- package/dist/schema/serialize.js +27 -31
- package/dist/schema/sugar.d.ts +17 -32
- package/dist/schema/sugar.js +14 -29
- package/dist/schema/{sync-delta-row.d.ts → syncDeltaRow.d.ts} +27 -50
- package/dist/schema/syncDeltaRow.js +89 -0
- package/dist/schema/tenancy.d.ts +44 -46
- package/dist/schema/tenancy.js +46 -48
- package/dist/server/adapter.d.ts +58 -58
- package/dist/server/adapter.js +13 -14
- package/dist/server/commit.d.ts +60 -64
- package/dist/server/index.d.ts +9 -10
- package/dist/server/index.js +1 -1
- package/dist/server/readConfig.d.ts +70 -0
- package/dist/server/readConfig.js +8 -0
- package/dist/server/storageMode.d.ts +23 -0
- package/dist/server/storageMode.js +17 -0
- package/dist/source/adapter.d.ts +31 -26
- package/dist/source/adapter.js +10 -10
- package/dist/source/adapters/drizzle.d.ts +28 -23
- package/dist/source/adapters/drizzle.js +34 -28
- package/dist/source/adapters/kysely.d.ts +27 -25
- package/dist/source/adapters/kysely.js +28 -26
- package/dist/source/adapters/memory.d.ts +8 -7
- package/dist/source/adapters/memory.js +10 -9
- package/dist/source/adapters/prisma.d.ts +13 -12
- package/dist/source/adapters/prisma.js +27 -29
- package/dist/source/conformance.d.ts +18 -11
- package/dist/source/conformance.js +27 -19
- package/dist/source/connector.d.ts +31 -32
- package/dist/source/connector.js +30 -28
- package/dist/source/connectorProtocol.d.ts +160 -0
- package/dist/source/connectorProtocol.js +162 -0
- package/dist/source/contract.d.ts +26 -27
- package/dist/source/contract.js +28 -29
- package/dist/source/factory.d.ts +94 -0
- package/dist/source/factory.js +268 -0
- package/dist/source/index.d.ts +10 -462
- package/dist/source/index.js +17 -421
- package/dist/source/migrations.d.ts +9 -9
- package/dist/source/migrations.js +9 -9
- package/dist/source/next.d.ts +10 -11
- package/dist/source/next.js +7 -8
- package/dist/source/pushQueue.d.ts +70 -48
- package/dist/source/pushQueue.js +36 -29
- package/dist/source/signing.d.ts +88 -0
- package/dist/source/signing.js +159 -0
- package/dist/source/types.d.ts +351 -0
- package/dist/source/types.js +43 -0
- package/dist/stores/ObjectStore.d.ts +11 -12
- package/dist/stores/ObjectStore.js +34 -35
- package/dist/stores/ObjectStoreContract.d.ts +12 -15
- package/dist/stores/SyncActionStore.d.ts +8 -12
- package/dist/stores/SyncActionStore.js +77 -46
- package/dist/surface.d.ts +28 -21
- package/dist/surface.js +28 -20
- package/dist/sync/{BootstrapHelper.d.ts → BootstrapFetcher.d.ts} +37 -45
- package/dist/sync/{BootstrapHelper.js → BootstrapFetcher.js} +101 -80
- package/dist/sync/ConnectionManager.d.ts +47 -50
- package/dist/sync/ConnectionManager.js +74 -70
- package/dist/sync/NetworkProbe.d.ts +27 -31
- package/dist/sync/NetworkProbe.js +67 -72
- package/dist/sync/{HydrationCoordinator.d.ts → OnDemandLoader.d.ts} +49 -36
- package/dist/sync/{HydrationCoordinator.js → OnDemandLoader.js} +79 -54
- package/dist/sync/{AreaOfInterestManager.d.ts → SubscriptionManager.d.ts} +45 -59
- package/dist/sync/{AreaOfInterestManager.js → SubscriptionManager.js} +44 -52
- package/dist/sync/SyncWebSocket.d.ts +175 -250
- package/dist/sync/SyncWebSocket.js +431 -769
- package/dist/sync/awaitClaimGrant.d.ts +18 -18
- package/dist/sync/awaitClaimGrant.js +38 -30
- package/dist/sync/bootstrapApply.d.ts +70 -0
- package/dist/sync/bootstrapApply.js +73 -0
- package/dist/sync/commitFrames.d.ts +44 -0
- package/dist/sync/commitFrames.js +94 -0
- package/dist/sync/createClaimStream.d.ts +23 -22
- package/dist/sync/createClaimStream.js +108 -25
- package/dist/sync/createPresenceStream.d.ts +19 -18
- package/dist/sync/createPresenceStream.js +25 -26
- package/dist/sync/createSnapshot.d.ts +13 -17
- package/dist/sync/createSnapshot.js +20 -26
- package/dist/sync/credentialLifecycle.d.ts +175 -0
- package/dist/sync/credentialLifecycle.js +322 -0
- package/dist/sync/deltaPipeline.d.ts +113 -0
- package/dist/sync/deltaPipeline.js +261 -0
- package/dist/sync/groupChange.d.ts +113 -0
- package/dist/sync/groupChange.js +242 -0
- package/dist/sync/heartbeat.d.ts +63 -0
- package/dist/sync/heartbeat.js +91 -0
- package/dist/sync/participants.d.ts +27 -27
- package/dist/sync/schemas.d.ts +3 -2
- package/dist/sync/schemas.js +14 -10
- package/dist/sync/syncCursor.d.ts +40 -0
- package/dist/sync/syncCursor.js +55 -0
- package/dist/sync/syncPlan.d.ts +54 -0
- package/dist/sync/syncPlan.js +50 -0
- package/dist/sync/syncPosition.d.ts +54 -49
- package/dist/sync/syncPosition.js +57 -52
- package/dist/sync/wsFrameHandlers.d.ts +116 -0
- package/dist/sync/wsFrameHandlers.js +374 -0
- package/dist/testing/fixtures/bootstrap.d.ts +21 -17
- package/dist/testing/fixtures/bootstrap.js +12 -6
- package/dist/testing/fixtures/deltas.d.ts +31 -34
- package/dist/testing/fixtures/deltas.js +30 -33
- package/dist/testing/fixtures/models.d.ts +11 -10
- package/dist/testing/fixtures/models.js +12 -10
- package/dist/testing/helpers/{react-wrapper.d.ts → reactWrapper.d.ts} +13 -10
- package/dist/testing/helpers/{react-wrapper.js → reactWrapper.js} +15 -12
- package/dist/testing/helpers/{sync-engine-harness.d.ts → syncEngineHarness.d.ts} +17 -18
- package/dist/testing/helpers/{sync-engine-harness.js → syncEngineHarness.js} +14 -11
- package/dist/testing/helpers/wait.d.ts +13 -8
- package/dist/testing/helpers/wait.js +13 -8
- package/dist/testing/index.d.ts +4 -4
- package/dist/testing/index.js +3 -3
- package/dist/testing/mocks/MockMutationExecutor.d.ts +18 -17
- package/dist/testing/mocks/MockMutationExecutor.js +15 -14
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +8 -8
- package/dist/testing/mocks/MockNetworkMonitor.js +8 -8
- package/dist/testing/mocks/MockSyncContext.d.ts +21 -34
- package/dist/testing/mocks/MockSyncContext.js +16 -45
- package/dist/testing/mocks/MockSyncStore.d.ts +14 -14
- package/dist/testing/mocks/MockSyncStore.js +11 -11
- package/dist/testing/mocks/MockWebSocket.d.ts +28 -24
- package/dist/testing/mocks/MockWebSocket.js +22 -21
- package/dist/transactions/TransactionQueue.d.ts +190 -221
- package/dist/transactions/TransactionQueue.js +424 -822
- package/dist/transactions/TransactionStore.d.ts +20 -0
- package/dist/transactions/TransactionStore.js +53 -0
- package/dist/transactions/UnconfirmedWrites.d.ts +82 -0
- package/dist/transactions/UnconfirmedWrites.js +104 -0
- package/dist/transactions/coalesceRules.d.ts +58 -0
- package/dist/transactions/coalesceRules.js +140 -0
- package/dist/transactions/commitPayload.d.ts +130 -0
- package/dist/transactions/commitPayload.js +143 -0
- package/dist/transactions/deltaConfirmation.d.ts +58 -0
- package/dist/transactions/deltaConfirmation.js +215 -0
- package/dist/transactions/optimisticApply.d.ts +49 -0
- package/dist/transactions/optimisticApply.js +65 -0
- package/dist/transactions/replayValidation.d.ts +99 -0
- package/dist/transactions/replayValidation.js +111 -0
- package/dist/types/global.d.ts +46 -41
- package/dist/types/global.js +20 -19
- package/dist/types/index.d.ts +74 -80
- package/dist/types/index.js +22 -27
- package/dist/types/modelData.d.ts +10 -0
- package/dist/types/modelData.js +9 -0
- package/dist/types/participant.d.ts +20 -0
- package/dist/types/participant.js +10 -0
- package/dist/types/streams.d.ts +216 -209
- package/dist/types/streams.js +7 -7
- package/dist/utils/asyncIterator.d.ts +25 -32
- package/dist/utils/asyncIterator.js +25 -32
- package/dist/utils/duration.d.ts +12 -15
- package/dist/utils/duration.js +12 -15
- package/dist/utils/mobxSetup.d.ts +53 -0
- package/dist/utils/{mobx-setup.js → mobxSetup.js} +44 -100
- package/dist/webhooks/events.d.ts +21 -16
- package/dist/webhooks/events.js +10 -8
- package/dist/webhooks/index.d.ts +5 -7
- package/dist/webhooks/index.js +5 -7
- package/dist/{schema/sync-delta-wire.d.ts → wire/delta.d.ts} +58 -41
- package/dist/wire/delta.js +114 -0
- package/dist/wire/errorEnvelope.d.ts +35 -27
- package/dist/wire/errorEnvelope.js +38 -32
- package/dist/wire/frames.d.ts +150 -67
- package/dist/wire/frames.js +48 -1
- package/dist/wire/index.d.ts +18 -13
- package/dist/wire/index.js +36 -13
- package/dist/wire/listEnvelope.d.ts +16 -23
- package/dist/wire/listEnvelope.js +7 -6
- package/dist/wire/protocol.d.ts +38 -0
- package/dist/wire/protocol.js +38 -0
- package/dist/wire/protocolVersion.d.ts +60 -0
- package/dist/wire/protocolVersion.js +67 -0
- package/docs/api-keys.md +4 -3
- package/docs/coordination.md +59 -0
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/identity.md +4 -4
- package/docs/integration-guide.md +1 -1
- package/docs/react.md +1 -1
- package/docs/sessions.md +5 -7
- package/package.json +24 -21
- package/dist/ai-sdk/coordinated-tool.d.ts +0 -101
- package/dist/ai-sdk/coordination-context.d.ts +0 -52
- package/dist/client/index.d.ts +0 -36
- package/dist/client/index.js +0 -33
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -12
- package/dist/core/query-utils.d.ts +0 -34
- package/dist/core/query-utils.js +0 -59
- package/dist/interfaces/headless.d.ts +0 -95
- package/dist/interfaces/headless.js +0 -41
- package/dist/query/index.d.ts +0 -6
- package/dist/query/index.js +0 -5
- package/dist/realtime/index.d.ts +0 -10
- package/dist/realtime/index.js +0 -9
- package/dist/schema/plane.d.ts +0 -23
- package/dist/schema/plane.js +0 -19
- package/dist/schema/sync-delta-row.js +0 -103
- package/dist/schema/sync-delta-wire.js +0 -102
- package/dist/server/next.d.ts +0 -51
- package/dist/server/next.js +0 -47
- package/dist/server/read-config.d.ts +0 -67
- package/dist/server/read-config.js +0 -8
- package/dist/server/storage-mode.d.ts +0 -1
- package/dist/server/storage-mode.js +0 -18
- package/dist/source/connector-protocol.d.ts +0 -159
- package/dist/source/connector-protocol.js +0 -161
- package/dist/sync/OfflineFlush.d.ts +0 -9
- package/dist/sync/OfflineFlush.js +0 -22
- package/dist/sync/OfflineTransactionStore.d.ts +0 -37
- package/dist/sync/OfflineTransactionStore.js +0 -263
- package/dist/transactions/OptimisticEchoTracker.d.ts +0 -82
- package/dist/transactions/OptimisticEchoTracker.js +0 -104
- package/dist/transactions/index.d.ts +0 -16
- package/dist/transactions/index.js +0 -7
- package/dist/transactions/mutation-error-handler.d.ts +0 -5
- package/dist/transactions/mutation-error-handler.js +0 -39
- package/dist/utils/mobx-setup.d.ts +0 -42
package/dist/client/Ablo.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ablo —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* `Ablo` — the one-call entry point to the sync engine client. It hides the
|
|
3
|
+
* internal wiring — the object pool, local database, sync client, WebSocket,
|
|
4
|
+
* bootstrap, and offline queue — behind a single function that returns a typed
|
|
5
|
+
* client with one property per model in your schema.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
* import { Ablo } from '@abloatai/ablo
|
|
8
|
+
* import { Ablo } from '@abloatai/ablo';
|
|
9
9
|
* import { schema } from './schema';
|
|
10
10
|
*
|
|
11
11
|
* const sync = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
@@ -18,21 +18,18 @@
|
|
|
18
18
|
* });
|
|
19
19
|
* await sync.reports.delete({ id: reportId });
|
|
20
20
|
*/
|
|
21
|
-
import {
|
|
22
|
-
import { baseFieldsSchema } from '../schema/schema.js';
|
|
23
|
-
import { schemaHash } from '../schema/serialize.js';
|
|
24
|
-
import { AbloError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, AbloNotFoundError, translateHttpError, toAbloError, claimedError } from '../errors.js';
|
|
21
|
+
import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, AbloNotFoundError, translateHttpError, toAbloError, claimedError } from '../errors.js';
|
|
25
22
|
import { descriptionFromMeta } from '../coordination/schema.js';
|
|
26
|
-
import { LoadStrategy, PropertyType } from '../types/index.js';
|
|
27
23
|
import { initSyncEngine } from '../context.js';
|
|
28
24
|
import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../SyncEngineContext.js';
|
|
29
25
|
import { alwaysOnline } from '../adapters/alwaysOnline.js';
|
|
30
26
|
import { validateAbloOptions } from './validateAbloOptions.js';
|
|
27
|
+
import { InstanceCache } from '../InstanceCache.js';
|
|
28
|
+
import {} from '../auth/index.js';
|
|
31
29
|
import { mintSession } from './sessionMint.js';
|
|
32
30
|
import { createAuthCredentialSource } from '../auth/credentialSource.js';
|
|
33
31
|
import { createInternalComponents } from './createInternalComponents.js';
|
|
34
32
|
import { resolveParticipantIdentity } from './identity.js';
|
|
35
|
-
import { Model } from '../Model.js';
|
|
36
33
|
import { BaseSyncedStore } from '../BaseSyncedStore.js';
|
|
37
34
|
import { createPresenceStream } from '../sync/createPresenceStream.js';
|
|
38
35
|
import { createClaimStream } from '../sync/createClaimStream.js';
|
|
@@ -41,688 +38,35 @@ import { createSnapshot } from '../sync/createSnapshot.js';
|
|
|
41
38
|
import { reconcileFunctionalUpdate } from './functionalUpdate.js';
|
|
42
39
|
import { createParticipantManager } from '../sync/participants.js';
|
|
43
40
|
import { createProtocolClient, } from './ApiClient.js';
|
|
44
|
-
// Value import is cycle-safe: httpClient.js
|
|
45
|
-
//
|
|
41
|
+
// Value import is cycle-safe: httpClient.js and ApiClient.js take the client
|
|
42
|
+
// types from the `options`/`resourceTypes` leaves, never from this module.
|
|
46
43
|
import { createAbloHttpClient, } from './httpClient.js';
|
|
47
44
|
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfCliKeyMismatch, warnIfDatabaseUrlEnvIgnored, warnIfDatabaseUrlDeprecated, } from './auth.js';
|
|
48
45
|
import { registerDataSource } from './registerDataSource.js';
|
|
49
|
-
import { shouldUseInMemoryPersistence
|
|
46
|
+
import { shouldUseInMemoryPersistence } from './persistence.js';
|
|
47
|
+
import { deriveConfigFromSchema } from './schemaConfig.js';
|
|
48
|
+
import { registerModelsFromSchema } from './modelRegistration.js';
|
|
49
|
+
import { createConsoleLogger, resolveLogLevel } from './consoleLogger.js';
|
|
50
|
+
import { createDefaultMutationExecutor } from './wsMutationExecutor.js';
|
|
51
|
+
export { computeFKDepthPriority } from './schemaConfig.js';
|
|
50
52
|
import { createModelProxy } from './createModelProxy.js';
|
|
51
53
|
import { assertWriteOptions } from './writeOptionsSchema.js';
|
|
52
|
-
// ── Config derivation from schema ─────────────────────────────────────────
|
|
53
|
-
/**
|
|
54
|
-
* Compute a create-priority map from schema `belongsTo` relations using
|
|
55
|
-
* Tarjan's strongly-connected-components algorithm.
|
|
56
|
-
*
|
|
57
|
-
* The FK graph has an edge `child → parent` for every `belongsTo`. Tarjan
|
|
58
|
-
* runs a single linear DFS that simultaneously (a) detects cycles by
|
|
59
|
-
* grouping mutually-reachable nodes into SCCs and (b) emits those SCCs
|
|
60
|
-
* in reverse topological order of the condensation graph. In this edge
|
|
61
|
-
* convention a "sink" SCC has no outgoing edges — i.e. no parents — so
|
|
62
|
-
* it is an *FK root* (`organizations`, `themes`, etc.). Tarjan emits
|
|
63
|
-
* roots first and leaves last, exactly the order in which rows must be
|
|
64
|
-
* inserted to satisfy FK constraints.
|
|
65
|
-
*
|
|
66
|
-
* Priorities are assigned by emit order: SCC #0 → 10, SCC #1 → 20, …
|
|
67
|
-
* Members of the same SCC share a priority, so insertion order wins the
|
|
68
|
-
* tiebreak inside a cycle (this matters for cyclic schemas like
|
|
69
|
-
* `slideDecks ↔ layouts`, where one direction is the user's chosen
|
|
70
|
-
* "soft" edge — only the consumer's mutator sequence knows which one).
|
|
71
|
-
*
|
|
72
|
-
* This algorithm is iteration-order-independent: starting the DFS from
|
|
73
|
-
* any node yields the same SCC partitioning, and SCCs always come out
|
|
74
|
-
* in valid topological order. The previous DFS-with-memoization
|
|
75
|
-
* heuristic broke under cycles by treating the back-edge as depth 0,
|
|
76
|
-
* which made priorities depend on which node the walk happened to
|
|
77
|
-
* enter the cycle at.
|
|
78
|
-
*
|
|
79
|
-
* Schema authors can mark one side of a cycle with
|
|
80
|
-
* `belongsTo(target, fk, { defer: true })`. Those edges are excluded
|
|
81
|
-
* from the dependency graph entirely, which deterministically breaks
|
|
82
|
-
* the cycle and turns the SCC into a chain — the marked child gets a
|
|
83
|
-
* strictly higher priority than its parent instead of being tied with
|
|
84
|
-
* it. Pair with a Postgres `DEFERRABLE INITIALLY DEFERRED` constraint
|
|
85
|
-
* if you want the database side of the cycle to also relax. See
|
|
86
|
-
* {@link BelongsToOptions.defer}.
|
|
87
|
-
*
|
|
88
|
-
* The returned map is keyed by {@link ModelDef.typename} (falling back
|
|
89
|
-
* to the schema key), because that is what `Model.getModelName()`
|
|
90
|
-
* returns at transaction time — keying by schema key would silently
|
|
91
|
-
* miss the lookup and every model would fall through to
|
|
92
|
-
* `defaultCreatePriority`.
|
|
93
|
-
*
|
|
94
|
-
* Reference: Tarjan, R. (1972), "Depth-first search and linear graph
|
|
95
|
-
* algorithms." Linear in V + E.
|
|
96
|
-
*/
|
|
97
|
-
export function computeFKDepthPriority(schema) {
|
|
98
|
-
// schemaKey → typename (wire name used at transaction time)
|
|
99
|
-
const keyToTypename = new Map();
|
|
100
|
-
for (const [key, def] of Object.entries(schema.models)) {
|
|
101
|
-
keyToTypename.set(key, def.typename ?? key);
|
|
102
|
-
}
|
|
103
|
-
// Adjacency: schemaKey → parent schema keys pulled from `belongsTo`.
|
|
104
|
-
// Parents not in the schema (e.g. external types) are dropped so the
|
|
105
|
-
// graph stays closed. Edges marked `{ defer: true }` are also
|
|
106
|
-
// dropped — the schema author has declared this side of a cycle to
|
|
107
|
-
// be the "soft" one (insert with null FK, patch later), so the
|
|
108
|
-
// dependency-graph walker treats it as if the edge weren't there.
|
|
109
|
-
// That breaks the cycle deterministically and lets the other side
|
|
110
|
-
// become a strict topological predecessor.
|
|
111
|
-
const parentsOf = new Map();
|
|
112
|
-
for (const [key, def] of Object.entries(schema.models)) {
|
|
113
|
-
const out = [];
|
|
114
|
-
for (const rel of Object.values(def.relations)) {
|
|
115
|
-
if (rel.type !== 'belongsTo')
|
|
116
|
-
continue;
|
|
117
|
-
if (!keyToTypename.has(rel.target))
|
|
118
|
-
continue;
|
|
119
|
-
if (rel.options?.defer === true)
|
|
120
|
-
continue;
|
|
121
|
-
out.push(rel.target);
|
|
122
|
-
}
|
|
123
|
-
parentsOf.set(key, out);
|
|
124
|
-
}
|
|
125
|
-
// Tarjan SCC bookkeeping
|
|
126
|
-
const dfsIndex = new Map();
|
|
127
|
-
const lowlink = new Map();
|
|
128
|
-
const onStack = new Set();
|
|
129
|
-
const stack = [];
|
|
130
|
-
const sccs = [];
|
|
131
|
-
let counter = 0;
|
|
132
|
-
function strongconnect(v) {
|
|
133
|
-
dfsIndex.set(v, counter);
|
|
134
|
-
lowlink.set(v, counter);
|
|
135
|
-
counter++;
|
|
136
|
-
stack.push(v);
|
|
137
|
-
onStack.add(v);
|
|
138
|
-
for (const w of parentsOf.get(v) ?? []) {
|
|
139
|
-
if (!dfsIndex.has(w)) {
|
|
140
|
-
strongconnect(w);
|
|
141
|
-
lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
|
|
142
|
-
}
|
|
143
|
-
else if (onStack.has(w)) {
|
|
144
|
-
// Back-edge into the active DFS path — w is in the same SCC as v.
|
|
145
|
-
lowlink.set(v, Math.min(lowlink.get(v), dfsIndex.get(w)));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// v is the root of an SCC: pop everything down to v inclusive.
|
|
149
|
-
if (lowlink.get(v) === dfsIndex.get(v)) {
|
|
150
|
-
const component = [];
|
|
151
|
-
let w;
|
|
152
|
-
do {
|
|
153
|
-
w = stack.pop();
|
|
154
|
-
onStack.delete(w);
|
|
155
|
-
component.push(w);
|
|
156
|
-
} while (w !== v);
|
|
157
|
-
sccs.push(component);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
for (const key of keyToTypename.keys()) {
|
|
161
|
-
if (!dfsIndex.has(key))
|
|
162
|
-
strongconnect(key);
|
|
163
|
-
}
|
|
164
|
-
// Tarjan emits SCCs in reverse topological order of the condensation.
|
|
165
|
-
// In our edge convention (child→parent), reverse-topo of the
|
|
166
|
-
// condensation means root-SCCs (no outgoing edges = no parents)
|
|
167
|
-
// first, leaf-SCCs (deepest descendants) last. We could just use
|
|
168
|
-
// emit-order as the priority — but that gives independent sibling
|
|
169
|
-
// SCCs different priorities, which is semantically wrong: siblings
|
|
170
|
-
// don't depend on each other and shouldn't be ordered relative to
|
|
171
|
-
// each other.
|
|
172
|
-
//
|
|
173
|
-
// Instead, do one more pass to compute *longest-path depth* on the
|
|
174
|
-
// condensation DAG: depth(SCC) = max(depth(parent SCC)) + 1, or 0
|
|
175
|
-
// for SCCs with no in-schema parents. SCCs at the same depth get
|
|
176
|
-
// the same priority — siblings stay tied, insertion order in the
|
|
177
|
-
// queue breaks the tie. Priority = (depth + 1) * 10.
|
|
178
|
-
//
|
|
179
|
-
// We can compute this in a single pass over the SCCs because
|
|
180
|
-
// Tarjan's emit-order *is* a valid topological order of the
|
|
181
|
-
// condensation: when we process sccs[i], every parent SCC has
|
|
182
|
-
// already been assigned a depth.
|
|
183
|
-
const nodeToSccIdx = new Map();
|
|
184
|
-
sccs.forEach((scc, i) => {
|
|
185
|
-
for (const node of scc)
|
|
186
|
-
nodeToSccIdx.set(node, i);
|
|
187
|
-
});
|
|
188
|
-
const sccDepth = new Map();
|
|
189
|
-
sccs.forEach((scc, i) => {
|
|
190
|
-
let maxParentDepth = -1;
|
|
191
|
-
for (const node of scc) {
|
|
192
|
-
for (const parent of parentsOf.get(node) ?? []) {
|
|
193
|
-
const parentSccIdx = nodeToSccIdx.get(parent);
|
|
194
|
-
if (parentSccIdx === undefined)
|
|
195
|
-
continue;
|
|
196
|
-
if (parentSccIdx === i)
|
|
197
|
-
continue; // intra-SCC edge — not a dep
|
|
198
|
-
const d = sccDepth.get(parentSccIdx);
|
|
199
|
-
if (d !== undefined && d > maxParentDepth)
|
|
200
|
-
maxParentDepth = d;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
sccDepth.set(i, maxParentDepth + 1);
|
|
204
|
-
});
|
|
205
|
-
const out = new Map();
|
|
206
|
-
sccs.forEach((scc, i) => {
|
|
207
|
-
const priority = (sccDepth.get(i) + 1) * 10;
|
|
208
|
-
for (const key of scc) {
|
|
209
|
-
out.set(keyToTypename.get(key), priority);
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
return out;
|
|
213
|
-
}
|
|
214
|
-
function deriveConfigFromSchema(schema) {
|
|
215
|
-
// Commit payload projection is done directly inside `TransactionQueue`
|
|
216
|
-
// — see `projectCommitPayload` there. Each model's field metadata
|
|
217
|
-
// rides on `ModelRegistry` (populated by `registerModelsFromSchema`),
|
|
218
|
-
// so there's no config-layer shim: the queue asks the registry for
|
|
219
|
-
// the declared fields and serializes accordingly.
|
|
220
|
-
return {
|
|
221
|
-
modelCreatePriority: computeFKDepthPriority(schema),
|
|
222
|
-
defaultCreatePriority: 40,
|
|
223
|
-
defaultNonCreatePriority: 50,
|
|
224
|
-
essentialFields: {},
|
|
225
|
-
classNameFallbackMap: {},
|
|
226
|
-
// Hash this client's schema once so bootstrap can detect drift against the
|
|
227
|
-
// server's active hash (same `schemaHash` the CLI push + server compute).
|
|
228
|
-
expectedSchemaHash: schemaHash(schema),
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
// ── Auto model registration from schema ───────────────────────────────────
|
|
232
|
-
function registerModelsFromSchema(schema, registry) {
|
|
233
|
-
registry.startBatch();
|
|
234
|
-
for (const [schemaKey, modelDef] of Object.entries(schema.models)) {
|
|
235
|
-
// Use typename as the model name — this is the wire-format name that
|
|
236
|
-
// the server sends in bootstrap responses and sync deltas. The pool's
|
|
237
|
-
// typeIndex, the ModelRegistry, and getModelName() all use this name.
|
|
238
|
-
// Schema key (camelCase plural) is only for the consumer-facing proxy API.
|
|
239
|
-
const modelName = modelDef.typename ?? schemaKey;
|
|
240
|
-
// Collect JSON sub-property fields to generate ${field}Json getters
|
|
241
|
-
const jsonSubFields = [];
|
|
242
|
-
for (const [fieldName, zodType] of Object.entries(modelDef.shape)) {
|
|
243
|
-
const inner = unwrapZodType(zodType);
|
|
244
|
-
if (isZodObject(inner)) {
|
|
245
|
-
jsonSubFields.push({ fieldName, subSchema: inner });
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
// Create a dynamic Model subclass with JSON sub-property getters.
|
|
249
|
-
//
|
|
250
|
-
// Field-level MobX observability is ON BY DEFAULT. A reactive read like
|
|
251
|
-
// `useAblo((a) => a.documents.get(id))` must re-render when a remote delta
|
|
252
|
-
// mutates the row IN PLACE (the common collaborative case); without
|
|
253
|
-
// per-field observability that update fires no reaction and the UI silently
|
|
254
|
-
// goes stale. Models opt OUT with an explicit `lazyObservable: false` —
|
|
255
|
-
// appropriate only for very large read-only list models where per-field
|
|
256
|
-
// atoms cost more than the QueryView's entry-replaced reactivity already
|
|
257
|
-
// provides. json fields register as `observable.ref` (see
|
|
258
|
-
// `registerModelsFromSchema`), so the default is ~one atom per scalar field
|
|
259
|
-
// per loaded row — cheap — not a deep atom tree per blob.
|
|
260
|
-
const isLazy = modelDef.lazyObservable !== false;
|
|
261
|
-
// Base provenance fields (`organizationId`, `createdBy`) live in
|
|
262
|
-
// `baseFieldsSchema`, not the per-model `shape`. The server stamps + emits
|
|
263
|
-
// them (camelCased on the wire), but hydration (`Model.assignFieldsFromData`)
|
|
264
|
-
// only assigns keys that already exist as an own/prototype property — so
|
|
265
|
-
// without a slot here, `deck.createdBy` / `deck.organizationId` silently read
|
|
266
|
-
// `undefined` (this is why the profile decks tab showed nothing: it filters
|
|
267
|
-
// `decks.filter(d => d.createdBy === userId)`). `id`/`createdAt`/`updatedAt`
|
|
268
|
-
// are already seeded by the base Model constructor, so they're excluded.
|
|
269
|
-
const fieldNames = [
|
|
270
|
-
...Object.keys(modelDef.shape),
|
|
271
|
-
...Object.keys(baseFieldsSchema.shape).filter((f) => f !== 'id' && f !== 'createdAt' && f !== 'updatedAt' && !(f in modelDef.shape)),
|
|
272
|
-
];
|
|
273
|
-
const computed = modelDef.computed;
|
|
274
|
-
const DynamicModel = createDynamicModelClass(modelName, jsonSubFields, fieldNames, computed, isLazy);
|
|
275
|
-
// Respect the schema's load strategy so lazy models skip IDB hydration + bootstrap
|
|
276
|
-
const loadStrategy = modelDef.load === 'lazy' || modelDef.load === 'manual'
|
|
277
|
-
? LoadStrategy.lazy
|
|
278
|
-
: LoadStrategy.instant;
|
|
279
|
-
registry.registerModel(modelName, DynamicModel, {
|
|
280
|
-
loadStrategy,
|
|
281
|
-
fields: modelDef.fields,
|
|
282
|
-
autoFill: modelDef.autoFill,
|
|
283
|
-
requiredFields: modelDef.requiredFields,
|
|
284
|
-
});
|
|
285
|
-
// Collect the set of fields that should get an IDB secondary index.
|
|
286
|
-
//
|
|
287
|
-
// Matches Linear's opt-in model (see wzhudev/reverse-linear-sync-engine):
|
|
288
|
-
// `@Reference(..., { indexed: true })`. Only `belongsTo` relations that
|
|
289
|
-
// explicitly set `{ index: true }` in their options get an IDB secondary
|
|
290
|
-
// index. Every other FK (and every scalar) is resolved via in-memory
|
|
291
|
-
// ObjectPool scans, which are fast enough at org-scope sizes (~10k rows)
|
|
292
|
-
// and reactive via MobX.
|
|
293
|
-
//
|
|
294
|
-
// Auto-indexing every belongsTo was wrong: it bloated write amplification
|
|
295
|
-
// for the vast majority of FKs that are never queried by fk. Indexing
|
|
296
|
-
// every scalar (like the legacy Go backend did) is even worse.
|
|
297
|
-
const indexedFields = new Set();
|
|
298
|
-
for (const relDef of Object.values(modelDef.relations)) {
|
|
299
|
-
if (relDef.type === 'belongsTo' && relDef.foreignKey && relDef.options?.index === true) {
|
|
300
|
-
indexedFields.add(relDef.foreignKey);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
// Register fields as properties (from Zod shape).
|
|
304
|
-
for (const [fieldName, rawZodType] of Object.entries(modelDef.shape)) {
|
|
305
|
-
const zodType = rawZodType;
|
|
306
|
-
const isOptional = zodType.isOptional?.() ?? false;
|
|
307
|
-
// A field is indexed if it's the FK of a `belongsTo({ index: true })`
|
|
308
|
-
// relation. Legacy `description === 'indexed'` still works for
|
|
309
|
-
// consumers using `field.*().indexed()`.
|
|
310
|
-
const isIndexed = indexedFields.has(fieldName) || zodType.description === 'indexed';
|
|
311
|
-
// JSON-typed fields (per the schema's wire-type tag) are opaque
|
|
312
|
-
// blobs from MobX's perspective — chart specs, ProseMirror docs,
|
|
313
|
-
// style maps. Deep observability on them recursively walks every
|
|
314
|
-
// nested property and creates an atom for each leaf, producing a
|
|
315
|
-
// microtask storm on every commit/streaming update. `ref` tracks
|
|
316
|
-
// only reassignment, which is how blob consumers actually use them.
|
|
317
|
-
const wireType = modelDef.fields?.[fieldName]?.type;
|
|
318
|
-
const observability = wireType === 'json' ? 'ref' : undefined;
|
|
319
|
-
registry.registerProperty(modelName, fieldName, {
|
|
320
|
-
type: PropertyType.property,
|
|
321
|
-
indexed: isIndexed,
|
|
322
|
-
optional: isOptional,
|
|
323
|
-
observability,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
// Register relations
|
|
327
|
-
for (const [relName, relDef] of Object.entries(modelDef.relations)) {
|
|
328
|
-
if (relDef.type === 'belongsTo') {
|
|
329
|
-
registry.registerReference(modelName, relName, {
|
|
330
|
-
referencedModel: () => {
|
|
331
|
-
const targetModel = registry.getModelByName(relDef.target);
|
|
332
|
-
return targetModel ?? DynamicModel;
|
|
333
|
-
},
|
|
334
|
-
indexed: true,
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
else if (relDef.type === 'hasMany') {
|
|
338
|
-
// Generate a getter on the parent model that returns all children
|
|
339
|
-
// matching the FK via Model.getStore().getByForeignKey(). The FK
|
|
340
|
-
// index on the target model is registered by deriveSyncPlanFromSchema.
|
|
341
|
-
const targetName = relDef.target;
|
|
342
|
-
const foreignKey = relDef.foreignKey;
|
|
343
|
-
const orderByField = relDef._orderBy;
|
|
344
|
-
// Resolve the target typename from the schema (might differ from the key)
|
|
345
|
-
const targetDef = schema.models[targetName];
|
|
346
|
-
const targetTypename = targetDef?.typename ?? targetName;
|
|
347
|
-
Object.defineProperty(DynamicModel.prototype, relName, {
|
|
348
|
-
get() {
|
|
349
|
-
const store = Model.getStore();
|
|
350
|
-
if (!store)
|
|
351
|
-
return [];
|
|
352
|
-
const results = store.getByForeignKey(targetTypename, foreignKey, this.id);
|
|
353
|
-
if (orderByField && results.length > 1) {
|
|
354
|
-
return [...results].sort((a, b) => {
|
|
355
|
-
// `orderByField` is a runtime string from the schema's
|
|
356
|
-
// hasMany({ orderBy }) — Models have dynamic typed
|
|
357
|
-
// fields produced by createDynamicModelClass, so the
|
|
358
|
-
// static type doesn't carry an index signature for
|
|
359
|
-
// arbitrary field reads. `Reflect.get` is the typed
|
|
360
|
-
// bridge — returns `unknown`, narrowed below.
|
|
361
|
-
const va = Reflect.get(a, orderByField);
|
|
362
|
-
const vb = Reflect.get(b, orderByField);
|
|
363
|
-
if (typeof va === 'number' && typeof vb === 'number')
|
|
364
|
-
return va - vb;
|
|
365
|
-
if (typeof va === 'string' && typeof vb === 'string')
|
|
366
|
-
return va.localeCompare(vb);
|
|
367
|
-
return 0;
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
return results;
|
|
371
|
-
},
|
|
372
|
-
enumerable: true,
|
|
373
|
-
configurable: true,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
registry.endBatch();
|
|
379
|
-
}
|
|
380
|
-
// ── JSON sub-property helpers ─────────────────────────────────────────────
|
|
381
|
-
/**
|
|
382
|
-
* Unwrap a Zod schema through .optional(), .nullable(), .default(),
|
|
383
|
-
* .readonly() to find the innermost type. Needed to detect whether a
|
|
384
|
-
* field.json() call wraps a ZodObject (has sub-properties) or a plain
|
|
385
|
-
* type (ZodUnknown, ZodArray, etc.).
|
|
386
|
-
*
|
|
387
|
-
* Uses Zod's public `.unwrap()` API per wrapper type — no `_def`
|
|
388
|
-
* digging. Bounded loop guards against pathological self-referential
|
|
389
|
-
* wrappers.
|
|
390
|
-
*/
|
|
391
|
-
function unwrapZodType(schema) {
|
|
392
|
-
let current = schema;
|
|
393
|
-
for (let i = 0; i < 10; i++) {
|
|
394
|
-
if (current instanceof z.ZodOptional) {
|
|
395
|
-
current = current.unwrap();
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
if (current instanceof z.ZodNullable) {
|
|
399
|
-
current = current.unwrap();
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
if (current instanceof z.ZodDefault) {
|
|
403
|
-
// v4 deprecates removeDefault in favor of unwrap, but the
|
|
404
|
-
// installed @types declarations only expose removeDefault on
|
|
405
|
-
// ZodDefault. Use it — it's the same runtime function.
|
|
406
|
-
current = current.unwrap();
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
if (current instanceof z.ZodReadonly) {
|
|
410
|
-
current = current.unwrap();
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
break;
|
|
414
|
-
}
|
|
415
|
-
return current;
|
|
416
|
-
}
|
|
417
|
-
/** Type guard: is this a ZodObject with a .shape property? */
|
|
418
|
-
function isZodObject(schema) {
|
|
419
|
-
return schema instanceof z.ZodObject;
|
|
420
|
-
}
|
|
421
|
-
/** Create a Model subclass for a schema-defined model */
|
|
422
|
-
function createDynamicModelClass(modelName, jsonSubFields, fieldNames, computed, lazyObservable = false) {
|
|
423
|
-
const ModelClass = class extends Model {
|
|
424
|
-
_modelName = modelName;
|
|
425
|
-
constructor(data) {
|
|
426
|
-
super(data);
|
|
427
|
-
// Gate `propertyChanged`-via-`observe` tracking during initial
|
|
428
|
-
// hydration. M1 installs a MobX `observe()` listener per schema
|
|
429
|
-
// property that forwards writes to `propertyChanged()` so direct
|
|
430
|
-
// assignments like `layer.position = newPos` still round-trip
|
|
431
|
-
// through the transaction queue. During construction we're writing
|
|
432
|
-
// wire data, NOT user edits — flagging this as "constructing" lets
|
|
433
|
-
// the listener early-return on those writes so `modifiedProperties`
|
|
434
|
-
// doesn't get polluted with every field of every hydrated model.
|
|
435
|
-
//
|
|
436
|
-
// The listener is installed by `makeObservable()` below (inside
|
|
437
|
-
// M1), so writes that happen BEFORE that line won't fire it; this
|
|
438
|
-
// flag is defensive in case a subclass or call path reorders the
|
|
439
|
-
// steps later.
|
|
440
|
-
this._isConstructing = true;
|
|
441
|
-
// MobX 6 requires fields to exist as own properties BEFORE makeObservable().
|
|
442
|
-
// Model base only sets id/createdAt/updatedAt. Schema fields (title, userId, etc.)
|
|
443
|
-
// must be initialized here so M1's annotations can find them.
|
|
444
|
-
for (const field of fieldNames) {
|
|
445
|
-
if (!(field in this)) {
|
|
446
|
-
this[field] = data?.[field] ?? undefined;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
// Per-field MobX observability opt-in via `lazyObservable: true` on
|
|
450
|
-
// the model definition. Defaults to plain objects — reactivity comes
|
|
451
|
-
// from the QueryView "entry replaced" pattern, which is cheap for
|
|
452
|
-
// read-only list UIs but invisible to in-place field mutations.
|
|
453
|
-
//
|
|
454
|
-
// Multiplayer editors need live field-level reactivity so remote
|
|
455
|
-
// deltas AND local drag/resize/rename mutations surface through
|
|
456
|
-
// `observer()` components without the whole pool entry being
|
|
457
|
-
// replaced. Without observability, `layer.position.x = 500` emits
|
|
458
|
-
// nothing and the UI lags until some unrelated state change triggers
|
|
459
|
-
// a pass (toolbar close, deselect).
|
|
460
|
-
//
|
|
461
|
-
// Delegates to `Model.makeObservable()` (the inherited method) so
|
|
462
|
-
// MobX annotations are derived from the same registry that M1 reads.
|
|
463
|
-
// That means computed getters, reference collections, custom
|
|
464
|
-
// getters/setters, and property-change tracking all integrate
|
|
465
|
-
// correctly — reimplementing `makeObservable` inline here would miss
|
|
466
|
-
// those seams.
|
|
467
|
-
if (lazyObservable) {
|
|
468
|
-
this.makeObservable();
|
|
469
|
-
}
|
|
470
|
-
this._isConstructing = false;
|
|
471
|
-
}
|
|
472
|
-
getModelName() {
|
|
473
|
-
return this._modelName;
|
|
474
|
-
}
|
|
475
|
-
};
|
|
476
|
-
// Generate ${field}Json getters for JSON fields with sub-properties.
|
|
477
|
-
//
|
|
478
|
-
// The getter reads the raw JSON string from the instance (set via
|
|
479
|
-
// updateFromData), parses it, applies Zod defaults, and caches by
|
|
480
|
-
// raw value. This replaces the hand-coded metadataObject + sub-property
|
|
481
|
-
// getter pattern that 11+ Ablo models currently repeat.
|
|
482
|
-
//
|
|
483
|
-
// Example: field named 'metadata' with sub-schema { icon: z.string().default('presentation') }
|
|
484
|
-
// → model.metadataJson returns { icon: 'presentation', ... } (typed, cached)
|
|
485
|
-
for (const { fieldName, subSchema } of jsonSubFields) {
|
|
486
|
-
const getterName = `${fieldName}Json`;
|
|
487
|
-
const cacheKey = `__${fieldName}JsonCache`;
|
|
488
|
-
Object.defineProperty(ModelClass.prototype, getterName, {
|
|
489
|
-
get() {
|
|
490
|
-
const raw = this[fieldName];
|
|
491
|
-
// Cache check: same raw value → same parsed result
|
|
492
|
-
const cache = this[cacheKey];
|
|
493
|
-
if (cache && cache.raw === raw)
|
|
494
|
-
return cache.parsed;
|
|
495
|
-
// Parse: handle string (from DB/wire), object (already parsed), null/undefined
|
|
496
|
-
let input;
|
|
497
|
-
try {
|
|
498
|
-
if (typeof raw === 'string') {
|
|
499
|
-
input = JSON.parse(raw);
|
|
500
|
-
}
|
|
501
|
-
else if (raw && typeof raw === 'object') {
|
|
502
|
-
input = raw;
|
|
503
|
-
}
|
|
504
|
-
else {
|
|
505
|
-
input = {};
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
catch {
|
|
509
|
-
input = {};
|
|
510
|
-
}
|
|
511
|
-
// Apply Zod parse for type coercion + defaults. safeParse so
|
|
512
|
-
// malformed metadata doesn't crash — falls back to all defaults.
|
|
513
|
-
const result = subSchema.safeParse(input);
|
|
514
|
-
const parsed = result.success ? result.data : subSchema.safeParse({}).data ?? {};
|
|
515
|
-
this[cacheKey] = { raw, parsed };
|
|
516
|
-
return parsed;
|
|
517
|
-
},
|
|
518
|
-
enumerable: true,
|
|
519
|
-
configurable: true,
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
// Install schema-declared computed getters on the prototype.
|
|
523
|
-
// Each getter receives `this` (the model instance) and returns the computed value.
|
|
524
|
-
if (computed) {
|
|
525
|
-
for (const [name, fn] of Object.entries(computed)) {
|
|
526
|
-
Object.defineProperty(ModelClass.prototype, name, {
|
|
527
|
-
get() {
|
|
528
|
-
return fn(this);
|
|
529
|
-
},
|
|
530
|
-
enumerable: true,
|
|
531
|
-
configurable: true,
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
return ModelClass;
|
|
536
|
-
}
|
|
537
|
-
const LOG_LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40, silent: 99 };
|
|
538
|
-
/**
|
|
539
|
-
* Resolve the effective level. Precedence: explicit `logLevel` option →
|
|
540
|
-
* `debug: true` (⇒ debug) → `ABLO_LOG_LEVEL` env → default `warn`. `debug: false`
|
|
541
|
-
* / omitted just means "don't raise the level" — it falls through to env/default
|
|
542
|
-
* rather than force-silencing an ops-set env override.
|
|
543
|
-
*/
|
|
544
|
-
function resolveLogLevel(opts) {
|
|
545
|
-
if (opts?.logLevel && opts.logLevel in LOG_LEVEL_RANK)
|
|
546
|
-
return opts.logLevel;
|
|
547
|
-
if (opts?.debug === true)
|
|
548
|
-
return 'debug';
|
|
549
|
-
// `globalThis.process` guard keeps this safe in browser/edge runtimes that
|
|
550
|
-
// have no `process` binding — there we fall through to the default.
|
|
551
|
-
const raw = globalThis.process?.env?.ABLO_LOG_LEVEL;
|
|
552
|
-
const normalized = raw?.toLowerCase();
|
|
553
|
-
if (normalized && normalized in LOG_LEVEL_RANK)
|
|
554
|
-
return normalized;
|
|
555
|
-
return 'warn';
|
|
556
|
-
}
|
|
557
|
-
/**
|
|
558
|
-
* Build the default logger, gated at `level` and prefixed `[Ablo]` so a creator
|
|
559
|
-
* with a console full of other tools' logs can see at a glance what's ours.
|
|
560
|
-
*/
|
|
561
|
-
function createConsoleLogger(level) {
|
|
562
|
-
const threshold = LOG_LEVEL_RANK[level];
|
|
563
|
-
const emit = (lvl, fn, args) => {
|
|
564
|
-
if (typeof console === 'undefined' || LOG_LEVEL_RANK[lvl] < threshold)
|
|
565
|
-
return;
|
|
566
|
-
fn('[Ablo]', ...args);
|
|
567
|
-
};
|
|
568
|
-
return {
|
|
569
|
-
debug: (...args) => emit('debug', console.debug, args),
|
|
570
|
-
info: (...args) => emit('info', console.info, args),
|
|
571
|
-
warn: (...args) => emit('warn', console.warn, args),
|
|
572
|
-
error: (...args) => emit('error', console.error, args),
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
54
|
// `readProcessEnv` lives in `./auth` alongside the other resolvers
|
|
576
55
|
// that read it. Re-exported there for use elsewhere in the file.
|
|
577
|
-
// ── Default mutation executor (wire: `commit` frame over WebSocket) ──────
|
|
578
|
-
/**
|
|
579
|
-
* Derive a stable `Idempotency-Key` from the batch's operation set.
|
|
580
|
-
*
|
|
581
|
-
* Retries of the same batch compute the same key — a reconnecting
|
|
582
|
-
* client that rebuilds the identical mutations from its offline queue
|
|
583
|
-
* sends the identical key, so the server's `mutation_log` replay path
|
|
584
|
-
* returns the cached response instead of re-executing the mutators.
|
|
585
|
-
*
|
|
586
|
-
* Content-addressed: sort operations by (model, id, type) then sha256
|
|
587
|
-
* the serialized form. Separator-safe — adjacent fields are delimited
|
|
588
|
-
* by a character (`\x1e`, the ASCII record separator) that cannot
|
|
589
|
-
* appear in a JSON string literal. Output length is 70 chars — safely
|
|
590
|
-
* under Stripe's documented 255-char cap.
|
|
591
|
-
*
|
|
592
|
-
* Uses the Web Crypto API (cross-runtime: Node 20+ and browsers), same
|
|
593
|
-
* primitive as the offline queue's AES-GCM encryption.
|
|
594
|
-
*
|
|
595
|
-
* @internal — exported as unexported file-local; callers go through
|
|
596
|
-
* the executor's own `Idempotency-Key` plumbing.
|
|
597
|
-
*/
|
|
598
|
-
async function deriveOperationsIdempotencyKey(operations) {
|
|
599
|
-
const normalized = [...operations]
|
|
600
|
-
.map((op) => ({
|
|
601
|
-
type: op.type,
|
|
602
|
-
model: op.model,
|
|
603
|
-
id: op.id,
|
|
604
|
-
input: op.input ?? null,
|
|
605
|
-
}))
|
|
606
|
-
.sort((a, b) => {
|
|
607
|
-
if (a.model !== b.model)
|
|
608
|
-
return a.model < b.model ? -1 : 1;
|
|
609
|
-
if (a.id !== b.id)
|
|
610
|
-
return a.id < b.id ? -1 : 1;
|
|
611
|
-
return a.type < b.type ? -1 : a.type > b.type ? 1 : 0;
|
|
612
|
-
});
|
|
613
|
-
const encoded = new TextEncoder().encode(JSON.stringify(normalized));
|
|
614
|
-
const digest = await crypto.subtle.digest('SHA-256', encoded);
|
|
615
|
-
const bytes = new Uint8Array(digest);
|
|
616
|
-
let hex = '';
|
|
617
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
618
|
-
hex += bytes[i].toString(16).padStart(2, '0');
|
|
619
|
-
}
|
|
620
|
-
return `batch-${hex}`;
|
|
621
|
-
}
|
|
622
|
-
/**
|
|
623
|
-
* Default mutation executor: sends `{ type: 'commit', payload: ... }` over
|
|
624
|
-
* the sync engine's own WebSocket.
|
|
625
|
-
*
|
|
626
|
-
* Transport ownership follows the Zero / Liveblocks pattern — the engine
|
|
627
|
-
* owns its socket end-to-end and the executor is internal. Apps pass URLs
|
|
628
|
-
* and auth; they do NOT inject transport callbacks. That's why this
|
|
629
|
-
* factory takes a `getWs` closure instead of a full SyncWebSocket: the WS
|
|
630
|
-
* doesn't exist when the executor is constructed (it's created later in
|
|
631
|
-
* `Ablo` during `BaseSyncedStore` init), so we resolve it
|
|
632
|
-
* lazily at commit time. Same trick Zero uses internally — see
|
|
633
|
-
* `packages/zero-client/src/client/zero.ts` where `Pusher`/`Puller` are
|
|
634
|
-
* constructed before the socket then wired up at connect time.
|
|
635
|
-
*
|
|
636
|
-
* `options.idempotencyKey` becomes the wire-level `clientTxId` when set,
|
|
637
|
-
* matching Stripe-style retry semantics. Otherwise the SDK generates one.
|
|
638
|
-
*/
|
|
639
|
-
function createDefaultMutationExecutor(getWs) {
|
|
640
|
-
async function commit(operations, options) {
|
|
641
|
-
const ws = getWs();
|
|
642
|
-
if (!ws?.sendCommit) {
|
|
643
|
-
throw new AbloConnectionError('SyncWebSocket not ready for commit. The engine must finish bootstrap ' +
|
|
644
|
-
'before mutations can be sent.', { code: 'ws_not_ready' });
|
|
645
|
-
}
|
|
646
|
-
const clientTxId = options?.idempotencyKey ??
|
|
647
|
-
(typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
648
|
-
? crypto.randomUUID()
|
|
649
|
-
: `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`);
|
|
650
|
-
try {
|
|
651
|
-
return await ws.sendCommit(operations, clientTxId, undefined, // use sendCommit's built-in 15s default; no per-call override
|
|
652
|
-
options?.causedByTaskId, options?.reads);
|
|
653
|
-
}
|
|
654
|
-
catch (err) {
|
|
655
|
-
// Wrap transport-level failures as connection errors so the
|
|
656
|
-
// TransactionQueue's retry classifier treats them as transient
|
|
657
|
-
// (matches the old HTTP path's network-error handling).
|
|
658
|
-
if (err instanceof AbloError)
|
|
659
|
-
throw err;
|
|
660
|
-
if (err instanceof Error) {
|
|
661
|
-
if (/not connected|timed out|connection|ECONN/i.test(err.message)) {
|
|
662
|
-
const wrapped = new AbloConnectionError(err.message, { cause: err });
|
|
663
|
-
// Preserve any `diagnostics` snapshot the underlying SyncWebSocket
|
|
664
|
-
// attached to the rejection. Without this, the wrapped error
|
|
665
|
-
// bottoms out at "AbloConnectionError: not connected" with no
|
|
666
|
-
// attribution to which close code / heartbeat trip / session
|
|
667
|
-
// error caused it. See SyncWebSocket.notConnectedError().
|
|
668
|
-
if (err &&
|
|
669
|
-
typeof err === 'object' &&
|
|
670
|
-
'diagnostics' in err &&
|
|
671
|
-
err.diagnostics) {
|
|
672
|
-
wrapped.diagnostics = err.diagnostics;
|
|
673
|
-
}
|
|
674
|
-
throw wrapped;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
throw err;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
return {
|
|
681
|
-
commit,
|
|
682
|
-
executeCreate: (model, id, input, _txId, options) => commit([{ type: 'CREATE', model: model.toLowerCase(), id, input }], options).then(() => { }),
|
|
683
|
-
executeUpdate: (model, id, data, _txId, options) => commit([{ type: 'UPDATE', model: model.toLowerCase(), id, input: data }], options),
|
|
684
|
-
executeDelete: (model, id, _txId, options) => commit([{ type: 'DELETE', model: model.toLowerCase(), id }], options).then(() => { }),
|
|
685
|
-
executeArchive: (model, id, _txId, options) => commit([{ type: 'ARCHIVE', model: model.toLowerCase(), id }], options).then(() => { }),
|
|
686
|
-
executeUnarchive: (model, id, _txId, options) => commit([{ type: 'UNARCHIVE', model: model.toLowerCase(), id }], options).then(() => { }),
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
// ── Default mutation dispatcher (for offline flush) ───────────────────────
|
|
690
|
-
function createDefaultMutationDispatcher(executor) {
|
|
691
|
-
return {
|
|
692
|
-
async dispatch(opName, variables) {
|
|
693
|
-
const prefixes = ['Create', 'Update', 'Delete', 'Archive', 'Unarchive'];
|
|
694
|
-
for (const prefix of prefixes) {
|
|
695
|
-
if (opName.startsWith(prefix)) {
|
|
696
|
-
const model = opName.slice(prefix.length);
|
|
697
|
-
const v = variables;
|
|
698
|
-
const input = (prefix === 'Create' || prefix === 'Update')
|
|
699
|
-
? v.input
|
|
700
|
-
: undefined;
|
|
701
|
-
await executor.commit([{
|
|
702
|
-
type: prefix.toUpperCase(),
|
|
703
|
-
model: model.toLowerCase(),
|
|
704
|
-
id: v.id ?? '',
|
|
705
|
-
input,
|
|
706
|
-
}]);
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
},
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
56
|
// ── Auth normalization ─────────────────────────────────────────────────────
|
|
714
57
|
/**
|
|
715
|
-
* The
|
|
716
|
-
* or `null` when auth is static
|
|
717
|
-
* refresh
|
|
58
|
+
* The single resolver the credential lifecycle needs: an async
|
|
59
|
+
* `() => token | null`, or `null` when auth is static — a plain long-lived
|
|
60
|
+
* `apiKey` string with no refresh, which is the common case.
|
|
718
61
|
*
|
|
719
|
-
* The short-lived per-user browser path passes a
|
|
720
|
-
*
|
|
721
|
-
* mint-before-connect, the proactive refresh timer
|
|
722
|
-
* and the reactive `credential_stale` re-mint. The
|
|
723
|
-
* `ApiKeySetter` contract end
|
|
724
|
-
* login is gone (terminal
|
|
725
|
-
* transient failure (
|
|
62
|
+
* The short-lived per-user browser path passes a function `apiKey` (an
|
|
63
|
+
* {@link ApiKeySetter}), and the SDK then drives the whole credential lifecycle
|
|
64
|
+
* from it: mint-before-connect, the proactive refresh timer with its
|
|
65
|
+
* wake/online/focus re-mint, and the reactive `credential_stale` re-mint. The
|
|
66
|
+
* resolver follows the `ApiKeySetter` contract end to end: resolve a token,
|
|
67
|
+
* resolve `null` when the login is gone (terminal — surfaces `session_expired`
|
|
68
|
+
* and signs the user out), or throw on a transient failure (backs off, without
|
|
69
|
+
* signing out).
|
|
726
70
|
*/
|
|
727
71
|
function resolveCredentialResolver(apiKey) {
|
|
728
72
|
if (typeof apiKey === 'function')
|
|
@@ -743,12 +87,14 @@ export function Ablo(options) {
|
|
|
743
87
|
const authInput = { options, env };
|
|
744
88
|
const configuredApiKey = resolveApiKey(authInput);
|
|
745
89
|
const configuredAuthToken = resolveAuthToken(authInput);
|
|
746
|
-
// The client
|
|
747
|
-
// drives both the reactive re-mint (
|
|
748
|
-
// refresh timer
|
|
749
|
-
// `apiKey` path
|
|
90
|
+
// The client owns its credential lifecycle (not the React layer): this resolver
|
|
91
|
+
// drives both the reactive re-mint (the connection's `credential_stale` state)
|
|
92
|
+
// and the proactive refresh timer with its wake/online/focus triggers. Null for
|
|
93
|
+
// the common static `apiKey` path, which needs no refresh.
|
|
750
94
|
const credentialResolver = resolveCredentialResolver(configuredApiKey);
|
|
751
|
-
const authCredentials = createAuthCredentialSource(
|
|
95
|
+
const authCredentials = createAuthCredentialSource(
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- load-bearing on the self-hosted path; server-internal cap-mint (Phase 3) not shipped
|
|
97
|
+
internalOptions.capabilityToken ?? configuredAuthToken);
|
|
752
98
|
const configuredDatabaseUrl = resolveDatabaseUrl(authInput);
|
|
753
99
|
assertBrowserSafety({
|
|
754
100
|
apiKey: configuredApiKey,
|
|
@@ -761,9 +107,9 @@ export function Ablo(options) {
|
|
|
761
107
|
createConsoleLogger(resolveLogLevel({ debug: options.debug, logLevel: options.logLevel }));
|
|
762
108
|
// Nudge (once) if a stray DATABASE_URL is in the env but `databaseUrl` wasn't
|
|
763
109
|
// passed — the env value is no longer auto-adopted (see resolveDatabaseUrl).
|
|
764
|
-
warnIfDatabaseUrlEnvIgnored(authInput, (m) => logger.warn(m));
|
|
765
|
-
warnIfDatabaseUrlDeprecated(authInput, (m) => logger.warn(m));
|
|
766
|
-
void warnIfCliKeyMismatch(authInput, (m) => logger.warn(m));
|
|
110
|
+
warnIfDatabaseUrlEnvIgnored(authInput, (m) => { logger.warn(m); });
|
|
111
|
+
warnIfDatabaseUrlDeprecated(authInput, (m) => { logger.warn(m); });
|
|
112
|
+
void warnIfCliKeyMismatch(authInput, (m) => { logger.warn(m); });
|
|
767
113
|
const schema = options.schema;
|
|
768
114
|
const url = resolveBaseURL(authInput);
|
|
769
115
|
// 1. Derive config from schema
|
|
@@ -773,24 +119,19 @@ export function Ablo(options) {
|
|
|
773
119
|
...deriveConfigFromSchema(schema),
|
|
774
120
|
...internalOptions.configOverrides,
|
|
775
121
|
};
|
|
776
|
-
// 2. Create the mutation executor
|
|
122
|
+
// 2. Create the mutation executor and dispatcher.
|
|
777
123
|
//
|
|
778
|
-
// The default executor sends `{ type: 'commit', ... }` over the
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
124
|
+
// The default executor sends `{ type: 'commit', ... }` over the engine's
|
|
125
|
+
// WebSocket. The socket doesn't exist yet at this point (it's created later
|
|
126
|
+
// when the store initializes), so the default takes a lazy getter that
|
|
127
|
+
// resolves the live socket at commit time. `storeHolder` is captured by the
|
|
128
|
+
// closure and assigned below once the store is built — JS closures close
|
|
129
|
+
// over bindings, not values, so by the time the first commit fires the store
|
|
130
|
+
// is live.
|
|
785
131
|
//
|
|
786
|
-
// Caller-supplied executors are still honored for advanced cases
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
// never need to override transport. See Zero's `ClientOptions`
|
|
790
|
-
// (packages/zero-client/src/client/options.ts) and Liveblocks'
|
|
791
|
-
// `ClientOptions` (packages/liveblocks-core/src/client.ts) for the
|
|
792
|
-
// reference shape: URLs + auth + declarative mutators, never a
|
|
793
|
-
// pluggable commit transport.
|
|
132
|
+
// Caller-supplied executors are still honored for advanced cases (test
|
|
133
|
+
// mocks, alternative transports), but apps should almost never need to
|
|
134
|
+
// override the transport.
|
|
794
135
|
// Captured-by-reference binding — assigned below after BaseSyncedStore
|
|
795
136
|
// is constructed. The default executor's `getWs` closure reads it
|
|
796
137
|
// lazily at commit time.
|
|
@@ -802,7 +143,6 @@ export function Ablo(options) {
|
|
|
802
143
|
const ws = storeHolder.store?.getSyncWebSocket() ?? null;
|
|
803
144
|
return ws;
|
|
804
145
|
});
|
|
805
|
-
const dispatcher = internalOptions.mutationDispatcher ?? createDefaultMutationDispatcher(executor);
|
|
806
146
|
// 3. Initialize SDK context (one call — hides all DI wiring).
|
|
807
147
|
// Each provider can be overridden individually; the noop defaults
|
|
808
148
|
// are preserved for the zero-config consumer path.
|
|
@@ -817,15 +157,13 @@ export function Ablo(options) {
|
|
|
817
157
|
: browserOnlineStatus),
|
|
818
158
|
config,
|
|
819
159
|
mutationExecutor: executor,
|
|
820
|
-
mutationDispatcher: dispatcher,
|
|
821
160
|
});
|
|
822
161
|
// 4. Create internal components (user never sees these). See
|
|
823
162
|
// `./createInternalComponents.ts` for the construction order
|
|
824
163
|
// and what each component does. Model registration happens
|
|
825
|
-
// here
|
|
826
|
-
// the schema-to-Model-class translation
|
|
827
|
-
//
|
|
828
|
-
// that aren't worth pulling into the components module.
|
|
164
|
+
// here (via `registerModelsFromSchema`, in `./modelRegistration.ts`)
|
|
165
|
+
// because the schema-to-Model-class translation is client-construction
|
|
166
|
+
// wiring that isn't worth pulling into the components module.
|
|
829
167
|
const { modelRegistry, objectPool, bootstrapHelper, database, syncClient, hydration, } = createInternalComponents({
|
|
830
168
|
schema,
|
|
831
169
|
url,
|
|
@@ -853,25 +191,46 @@ export function Ablo(options) {
|
|
|
853
191
|
});
|
|
854
192
|
// Hand the credential lifecycle to the client (refresher + proactive refresh
|
|
855
193
|
// timer + wake/online/focus re-mint). Installed once here so refresh works for
|
|
856
|
-
//
|
|
194
|
+
// any consumer of `Ablo({ auth })`, not only those who render `<AbloProvider>`.
|
|
857
195
|
// The first mint happens in `ready()` so the first connection carries a token.
|
|
196
|
+
//
|
|
197
|
+
// Long-lived server clients also get the pre-roll timer on windowless hosts
|
|
198
|
+
// (`proactiveInNode`): their socket must renew its `rk_` or `ek_` before the
|
|
199
|
+
// server's keepalive reaper closes it (4001 `credential_expired`). Two signals
|
|
200
|
+
// qualify — an agent or system participant, and an absolute endpoint-string
|
|
201
|
+
// `apiKey` (a relative one can't be fetched in Node, so an absolute URL is
|
|
202
|
+
// unambiguously a deliberate server client). User-kind clients in Node (an
|
|
203
|
+
// SSR/RSC module evaluating scaffolded browser code) stay reactive-only.
|
|
858
204
|
if (credentialResolver) {
|
|
859
|
-
|
|
205
|
+
const rawEndpoint = internalOptions.authEndpoint ?? internalOptions.apiKey;
|
|
206
|
+
const absoluteEndpoint = typeof rawEndpoint === 'string' && /^https?:\/\//i.test(rawEndpoint);
|
|
207
|
+
store.startCredentialLifecycle(credentialResolver, {
|
|
208
|
+
/* eslint-disable @typescript-eslint/no-deprecated -- `kind` gates the self-hosted proactive pre-roll; hosted path derives it from the apiKey scope */
|
|
209
|
+
proactiveInNode: internalOptions.kind === 'agent' ||
|
|
210
|
+
internalOptions.kind === 'system' ||
|
|
211
|
+
absoluteEndpoint,
|
|
212
|
+
/* eslint-enable @typescript-eslint/no-deprecated */
|
|
213
|
+
});
|
|
860
214
|
}
|
|
215
|
+
// Put the lazy-query lane on the same auth-recovery path as the WebSocket probe
|
|
216
|
+
// and the proactive pre-roll: a 401 on `/sync/query` re-mints via the store's
|
|
217
|
+
// single-flight lifecycle and replays once, instead of silently returning empty
|
|
218
|
+
// rows against an expired `ek_` until the next proactive tick. Late-bound
|
|
219
|
+
// because the coordinator is constructed before the store exists.
|
|
220
|
+
hydration.setCredentialRecovery((recovery) => store.recoverFromAuthRejection(recovery));
|
|
861
221
|
// Wire the store back into the default executor's lazy getter (see
|
|
862
222
|
// `storeHolder` above). The executor was constructed before the store
|
|
863
223
|
// existed; this late binding closes the loop so commits dispatch over
|
|
864
224
|
// the engine's WebSocket once it opens.
|
|
865
225
|
storeHolder.store = store;
|
|
866
|
-
// Bind
|
|
867
|
-
//
|
|
868
|
-
// `
|
|
869
|
-
//
|
|
870
|
-
//
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
// `ws_not_ready` forever (terminal AgentJob writes hang on retry).
|
|
226
|
+
// Bind this executor to this client's TransactionQueue. Without it, the queue
|
|
227
|
+
// resolves `mutationExecutor` from the module-level `getContext()`, which
|
|
228
|
+
// `initSyncEngine()` overwrites on every client construction. In multi-client
|
|
229
|
+
// flows (for example a worker plus a per-job peer) the second `initSyncEngine()`
|
|
230
|
+
// call would silently redirect the first client's queue through the second
|
|
231
|
+
// client's executor closure — and when the second client disposes, its
|
|
232
|
+
// `storeHolder.store` becomes null, so the first client's commits start throwing
|
|
233
|
+
// `ws_not_ready` forever.
|
|
875
234
|
syncClient.getTransactionQueue().setMutationExecutor(executor);
|
|
876
235
|
// Presence + claim streams — built eagerly so `engine.presence`
|
|
877
236
|
// and `engine.claims` return the same reference for the engine's
|
|
@@ -883,10 +242,13 @@ export function Ablo(options) {
|
|
|
883
242
|
// The server stamps `isAgent` on outbound presence frames from the
|
|
884
243
|
// connection's authenticated identity prefix, but the local `self`
|
|
885
244
|
// entry uses the kind we know at construction.
|
|
886
|
-
const participantId =
|
|
245
|
+
const participantId =
|
|
246
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- self-hosted identity fallback; hosted path derives identity from the apiKey scope
|
|
247
|
+
(internalOptions.kind === 'agent' ? internalOptions.agentId : internalOptions.user?.id) ?? '';
|
|
887
248
|
const presenceStream = createPresenceStream({
|
|
888
249
|
participantId,
|
|
889
250
|
syncGroups: internalOptions.syncGroups ?? [],
|
|
251
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- local self-entry kind; server re-stamps isAgent from the authenticated identity
|
|
890
252
|
isAgent: internalOptions.kind === 'agent',
|
|
891
253
|
});
|
|
892
254
|
const claimStream = createClaimStream({ participantId });
|
|
@@ -900,6 +262,7 @@ export function Ablo(options) {
|
|
|
900
262
|
// 6. Validate options up front — fail loudly on obviously wrong inputs so
|
|
901
263
|
// strangers don't get silent empty results. Validation errors are written
|
|
902
264
|
// into `store.syncStatus` (the single source of truth).
|
|
265
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- self-hosted default; hosted path ignores it (server derives kind from the apiKey scope)
|
|
903
266
|
const kind = internalOptions.kind ?? 'user';
|
|
904
267
|
const _validationError = validateAbloOptions({
|
|
905
268
|
options: internalOptions,
|
|
@@ -918,6 +281,7 @@ export function Ablo(options) {
|
|
|
918
281
|
// trusting them is the trap (you think you're an agent; the key says user).
|
|
919
282
|
// Warn loudly rather than removing the fields — `agentId` is still load-bearing
|
|
920
283
|
// on the self-hosted path (no apiKey; paired with `capabilityToken`).
|
|
284
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- reads the deprecated fields precisely to warn callers off them under a configured apiKey
|
|
921
285
|
if (configuredApiKey && (internalOptions.kind || internalOptions.agentId)) {
|
|
922
286
|
logger.warn('Ablo: `kind` / `agentId` are ignored when an `apiKey` is configured — ' +
|
|
923
287
|
'the server derives participant identity from the key’s scope. Remove ' +
|
|
@@ -945,13 +309,13 @@ export function Ablo(options) {
|
|
|
945
309
|
}
|
|
946
310
|
_readyPromise = (async () => {
|
|
947
311
|
try {
|
|
948
|
-
// Mint the
|
|
949
|
-
// WebSocket upgrade
|
|
950
|
-
// connect that has to self-heal). Only when a refreshing resolver is
|
|
951
|
-
//
|
|
952
|
-
//
|
|
953
|
-
// app shows sign-in); a
|
|
954
|
-
// and the lifecycle's online/wake triggers retry).
|
|
312
|
+
// Mint the first access credential before we connect, so the initial
|
|
313
|
+
// WebSocket upgrade and bootstrap carry a valid bearer (no tokenless first
|
|
314
|
+
// connect that has to self-heal). Only when a refreshing resolver is wired
|
|
315
|
+
// and no static credential is already present. Follows the `apiKey`
|
|
316
|
+
// resolver contract: `null` means the login is gone (terminal — fail ready
|
|
317
|
+
// so the app shows sign-in); a throw means transient (rethrown; autoStart
|
|
318
|
+
// swallows it and the lifecycle's online/wake triggers retry).
|
|
955
319
|
if (credentialResolver && !authCredentials.getAuthToken()) {
|
|
956
320
|
const token = await credentialResolver();
|
|
957
321
|
if (!token) {
|
|
@@ -959,7 +323,7 @@ export function Ablo(options) {
|
|
|
959
323
|
}
|
|
960
324
|
authCredentials.setAuthToken(token);
|
|
961
325
|
}
|
|
962
|
-
// Register the caller's own database for write-back
|
|
326
|
+
// Register the caller's own database for write-back before bootstrap, so
|
|
963
327
|
// the server resolves this org's data plane to the customer's DB rather
|
|
964
328
|
// than serving an empty/wrong store. The org is derived server-side from
|
|
965
329
|
// the API key. Idempotent server-side (register-or-update). Skipped when
|
|
@@ -981,15 +345,15 @@ export function Ablo(options) {
|
|
|
981
345
|
url,
|
|
982
346
|
kind,
|
|
983
347
|
configuredApiKey,
|
|
984
|
-
// Resolve identity against the
|
|
985
|
-
// `configuredAuthToken`. Consumers using a function `apiKey`
|
|
986
|
-
//
|
|
987
|
-
//
|
|
988
|
-
//
|
|
989
|
-
//
|
|
990
|
-
//
|
|
991
|
-
//
|
|
992
|
-
//
|
|
348
|
+
// Resolve identity against the live token, not the construction-time
|
|
349
|
+
// `configuredAuthToken`. Consumers using a function `apiKey` never pass
|
|
350
|
+
// `authToken` at construction — the lifecycle mints the first `ek_` or
|
|
351
|
+
// `rk_` and calls `setAuthToken()` before `ready()`, which updates the
|
|
352
|
+
// shared credential source. Reading the frozen `configuredAuthToken`
|
|
353
|
+
// here made `/auth/identity` fire with no bearer (returning
|
|
354
|
+
// `no_matching_provider` / `session_expired`) even though the token was
|
|
355
|
+
// present. This reads the shared credential source, like every other
|
|
356
|
+
// transport.
|
|
993
357
|
configuredAuthToken: authCredentials.getAuthToken() ?? configuredAuthToken,
|
|
994
358
|
bootstrapHelper,
|
|
995
359
|
auth: authCredentials,
|
|
@@ -997,11 +361,11 @@ export function Ablo(options) {
|
|
|
997
361
|
});
|
|
998
362
|
const { userId, accountScope, teamIds, capabilityToken, syncGroups, participantKind, } = resolved;
|
|
999
363
|
// Fail-loud guard: detect the degenerate "no real sync groups
|
|
1000
|
-
// resolved" state before opening the
|
|
1001
|
-
//
|
|
364
|
+
// resolved" state before opening the socket. It is the same class of bug as
|
|
365
|
+
// a
|
|
1002
366
|
// sensible-looking default that's functionally broken: the
|
|
1003
367
|
// SDK ends up subscribing only to the server-side
|
|
1004
|
-
// `['default']` fallback
|
|
368
|
+
// `['default']` fallback, no
|
|
1005
369
|
// delta has that tag, live fan-out silently never delivers.
|
|
1006
370
|
// For human users (kind:'user') this is almost certainly a
|
|
1007
371
|
// misconfiguration upstream — either the caller didn't pass
|
|
@@ -1013,9 +377,9 @@ export function Ablo(options) {
|
|
|
1013
377
|
if (participantKind === 'user' &&
|
|
1014
378
|
(resolvedSyncGroups.length === 0 ||
|
|
1015
379
|
(resolvedSyncGroups.length === 1 && resolvedSyncGroups[0] === 'default'))) {
|
|
1016
|
-
// Actionable and
|
|
1017
|
-
// kept at warn
|
|
1018
|
-
//
|
|
380
|
+
// Actionable and not self-healing (no live updates until fixed):
|
|
381
|
+
// kept at warn level for consumers; the low-level diagnostic
|
|
382
|
+
// fields ride the debug log below.
|
|
1019
383
|
logger.warn('This client was started without sync groups, so it will not receive ' +
|
|
1020
384
|
'live updates. Pass `syncGroups` (for example ' +
|
|
1021
385
|
'`["org:<id>", "user:<id>"]`) or check that your auth provider supplies them.');
|
|
@@ -1087,15 +451,15 @@ export function Ablo(options) {
|
|
|
1087
451
|
httpStatus: error.httpStatus,
|
|
1088
452
|
error: error.message,
|
|
1089
453
|
});
|
|
1090
|
-
// Clear the memo so a
|
|
1091
|
-
// replaying this rejection forever. Bootstrap failures here are
|
|
1092
|
-
//
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
//
|
|
1098
|
-
//
|
|
454
|
+
// Clear the memo so a future `ready()` re-attempts bootstrap instead of
|
|
455
|
+
// replaying this rejection forever. Bootstrap failures here are transient
|
|
456
|
+
// by nature — offline, an IndexedDB open timeout, a bootstrap fetch
|
|
457
|
+
// hiccup — and the early `if (_readyPromise) return _readyPromise` guard
|
|
458
|
+
// would otherwise hand every later caller this same dead promise, bricking
|
|
459
|
+
// the engine until a full page reload. Nulling it lets the provider's
|
|
460
|
+
// online/wake/retry triggers drive a clean re-bootstrap. (The terminal
|
|
461
|
+
// `_validationError` branch above intentionally stays cached — config
|
|
462
|
+
// can't change without recreating the engine.)
|
|
1099
463
|
_readyPromise = null;
|
|
1100
464
|
throw error;
|
|
1101
465
|
}
|
|
@@ -1202,27 +566,6 @@ export function Ablo(options) {
|
|
|
1202
566
|
},
|
|
1203
567
|
};
|
|
1204
568
|
}
|
|
1205
|
-
function modelClaimFromQueued(claim) {
|
|
1206
|
-
return {
|
|
1207
|
-
id: claim.id,
|
|
1208
|
-
actor: claim.heldBy ?? "",
|
|
1209
|
-
participantKind: claim.participantKind ?? "user",
|
|
1210
|
-
reason: claim.reason,
|
|
1211
|
-
...(claim.description ? { description: claim.description } : {}),
|
|
1212
|
-
field: claim.target.field,
|
|
1213
|
-
status: 'queued',
|
|
1214
|
-
position: claim.position,
|
|
1215
|
-
expiresAt: claim.expiresAt ?? 0,
|
|
1216
|
-
target: {
|
|
1217
|
-
model: claim.target.type,
|
|
1218
|
-
id: claim.target.id,
|
|
1219
|
-
path: claim.target.path,
|
|
1220
|
-
range: claim.target.range,
|
|
1221
|
-
field: claim.target.field,
|
|
1222
|
-
meta: claim.target.meta,
|
|
1223
|
-
},
|
|
1224
|
-
};
|
|
1225
|
-
}
|
|
1226
569
|
function targetMatchesModel(target, claim) {
|
|
1227
570
|
if (target.model &&
|
|
1228
571
|
claim.target.type.toLowerCase() !== target.model.toLowerCase()) {
|
|
@@ -1239,14 +582,6 @@ export function Ablo(options) {
|
|
|
1239
582
|
.filter((claim) => (target ? targetMatchesModel(target, claim) : true))
|
|
1240
583
|
.map(modelClaimFromActive);
|
|
1241
584
|
}
|
|
1242
|
-
function listModelClaimQueue(target) {
|
|
1243
|
-
if (!target?.model || !target.id)
|
|
1244
|
-
return [];
|
|
1245
|
-
return publicClaims
|
|
1246
|
-
.queueFor({ type: target.model, id: target.id })
|
|
1247
|
-
.filter((claim) => (target.field ? claim.target.field === target.field : true))
|
|
1248
|
-
.map(modelClaimFromQueued);
|
|
1249
|
-
}
|
|
1250
585
|
function waitForModelUnclaimed(target, options) {
|
|
1251
586
|
if (listModelClaims(target).length === 0)
|
|
1252
587
|
return Promise.resolve();
|
|
@@ -1274,10 +609,12 @@ export function Ablo(options) {
|
|
|
1274
609
|
}
|
|
1275
610
|
};
|
|
1276
611
|
const onAbort = () => {
|
|
1277
|
-
finish(() =>
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
612
|
+
finish(() => {
|
|
613
|
+
reject(new AbloConnectionError('Claim wait aborted.', {
|
|
614
|
+
code: 'claim_wait_aborted',
|
|
615
|
+
cause: options?.signal?.reason,
|
|
616
|
+
}));
|
|
617
|
+
});
|
|
1281
618
|
};
|
|
1282
619
|
if (options?.signal?.aborted) {
|
|
1283
620
|
onAbort();
|
|
@@ -1287,7 +624,9 @@ export function Ablo(options) {
|
|
|
1287
624
|
options?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
1288
625
|
if (options?.timeout != null) {
|
|
1289
626
|
timeoutId = setTimeout(() => {
|
|
1290
|
-
finish(() =>
|
|
627
|
+
finish(() => {
|
|
628
|
+
reject(claimedError(target, listModelClaims(target), 'model_claimed_timeout'));
|
|
629
|
+
});
|
|
1291
630
|
}, options.timeout);
|
|
1292
631
|
}
|
|
1293
632
|
});
|
|
@@ -1314,6 +653,10 @@ export function Ablo(options) {
|
|
|
1314
653
|
waited,
|
|
1315
654
|
release,
|
|
1316
655
|
revoke: claim.revoke,
|
|
656
|
+
// The lease-control members are forwarded explicitly — this wrapper
|
|
657
|
+
// rebuilds the handle field by field, so anything not named here is
|
|
658
|
+
// silently dropped from the public claim.
|
|
659
|
+
heartbeat: claim.heartbeat,
|
|
1317
660
|
[Symbol.asyncDispose]: release,
|
|
1318
661
|
};
|
|
1319
662
|
}
|
|
@@ -1376,7 +719,7 @@ export function Ablo(options) {
|
|
|
1376
719
|
createSnapshot: (modelKey, id) => createSnapshot({
|
|
1377
720
|
pool: objectPool,
|
|
1378
721
|
transport: store.getSyncWebSocket(),
|
|
1379
|
-
// `position.readFloor` is
|
|
722
|
+
// `position.readFloor` is the value claims and snapshots stamp as
|
|
1380
723
|
// `readAt` (max of the pool-applied cursor and the acked
|
|
1381
724
|
// watermark for our own writes — see sync/syncPosition.ts).
|
|
1382
725
|
// Stamping a bare stream cursor made a claim taken right after
|
|
@@ -1387,7 +730,7 @@ export function Ablo(options) {
|
|
|
1387
730
|
entities: { [modelKey]: id },
|
|
1388
731
|
}),
|
|
1389
732
|
queue: (target) => publicClaims.queueFor({ type: target.model, id: target.id }),
|
|
1390
|
-
reorder: (target, order) => publicClaims.reorder({ type: target.model, id: target.id }, order),
|
|
733
|
+
reorder: (target, order) => { publicClaims.reorder({ type: target.model, id: target.id }, order); },
|
|
1391
734
|
state: (target) => {
|
|
1392
735
|
// The live claim stream only tracks *open* (active) claims;
|
|
1393
736
|
// terminal states (committed / expired / canceled) drop out of
|
|
@@ -1422,19 +765,18 @@ export function Ablo(options) {
|
|
|
1422
765
|
selfParticipantKind: kind,
|
|
1423
766
|
// Read-interest / write-intent enrolment for the typed surface.
|
|
1424
767
|
// `enterScope`/`pinScope` resolve the `{ [schemaKey]: id }` scope
|
|
1425
|
-
// through the
|
|
768
|
+
// through the same resolver the claim path uses, landing this client in
|
|
1426
769
|
// the entity-scoped group the holder's claim presence fans out on.
|
|
1427
|
-
//
|
|
1428
|
-
//
|
|
770
|
+
// Returns the store promise so the claim write path can await pinScope
|
|
771
|
+
// before acquiring the lease (closing the subscribe-vs-broadcast race);
|
|
1429
772
|
// read-interest callers (`retrieve`/`claim.state`) still `void` it and
|
|
1430
|
-
// stay fire-and-forget.
|
|
1431
|
-
// errors so read interest never makes a read reject or stall.
|
|
773
|
+
// stay fire-and-forget. It's soft either way — the store swallows
|
|
774
|
+
// reconcile errors so read interest never makes a read reject or stall.
|
|
1432
775
|
enterScope: (scope) => store.enterScope(scope),
|
|
1433
776
|
pinScope: (scope) => store.pinScope(scope),
|
|
1434
|
-
// `ablo.<model>.watch(ids, { ttl })`
|
|
1435
|
-
// this model's sync group(s).
|
|
1436
|
-
// `
|
|
1437
|
-
// only — `join` throws AbloConnectionError if the socket isn't ready.
|
|
777
|
+
// `ablo.<model>.watch(ids, { ttl })` performs a scoped participant join
|
|
778
|
+
// on this model's sync group(s). WebSocket only — `join` throws
|
|
779
|
+
// `AbloConnectionError` if the socket isn't ready.
|
|
1438
780
|
createWatch: (modelKey, ids, options) => participantManager.join({
|
|
1439
781
|
scope: { [modelKey]: ids },
|
|
1440
782
|
...(options?.ttl !== undefined ? { ttlSeconds: options.ttl } : {}),
|
|
@@ -1584,7 +926,7 @@ export function Ablo(options) {
|
|
|
1584
926
|
const id = params.id ?? createModelId();
|
|
1585
927
|
await applyClaimedPolicy({ model: name, id }, params);
|
|
1586
928
|
// Confirm, then return the authoritative row (with framework defaults;
|
|
1587
|
-
// the
|
|
929
|
+
// the existing row on an idempotent re-create) — mirrors the WebSocket client.
|
|
1588
930
|
await commits.create({
|
|
1589
931
|
claimRef: params.claimRef,
|
|
1590
932
|
idempotencyKey: params.idempotencyKey,
|
|
@@ -1629,13 +971,13 @@ export function Ablo(options) {
|
|
|
1629
971
|
};
|
|
1630
972
|
}
|
|
1631
973
|
/**
|
|
1632
|
-
* The
|
|
974
|
+
* The control-plane credential: always the original configured secret key.
|
|
1633
975
|
* Never reads `authCredentials` — that holds the exchanged sync credential
|
|
1634
976
|
* (a wide-scope `rk_` on the hosted path), which control-plane routes
|
|
1635
977
|
* rightly refuse (e.g. the user-session mint is sk_-gated). Counterpart to
|
|
1636
978
|
* `getAuthToken()`, which resolves the sync-plane token.
|
|
1637
979
|
*
|
|
1638
|
-
* The
|
|
980
|
+
* The secret-key-only rule is enforced on the server; the credential-kind taxonomy
|
|
1639
981
|
* (secret/restricted/ephemeral/publishable) lives in `auth/credentialPolicy`.
|
|
1640
982
|
*/
|
|
1641
983
|
async function controlPlaneApiKey() {
|
|
@@ -1643,7 +985,7 @@ export function Ablo(options) {
|
|
|
1643
985
|
}
|
|
1644
986
|
/**
|
|
1645
987
|
* Resolve the control-plane context a session/agent mint needs (sk_ +
|
|
1646
|
-
* bootstrap base URL + the schema-key→typename map the
|
|
988
|
+
* bootstrap base URL + the schema-key→typename map the server gates on).
|
|
1647
989
|
* Shared by `sessions.create` and `agents.create` so the two mint doors
|
|
1648
990
|
* can never drift on how a token is minted. Throws if no `sk_` is present —
|
|
1649
991
|
* minting is a backend-only operation.
|
|
@@ -1660,12 +1002,12 @@ export function Ablo(options) {
|
|
|
1660
1002
|
bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
|
|
1661
1003
|
}),
|
|
1662
1004
|
...(internalOptions.fetch ? { fetch: internalOptions.fetch } : {}),
|
|
1663
|
-
// Map every `can` schema-key to the wire typename the
|
|
1005
|
+
// Map every `can` schema-key to the wire typename the server gates on, so a
|
|
1664
1006
|
// typename override (`documents` → `Document`) doesn't mint a capability
|
|
1665
1007
|
// the server then denies. See `MintSessionContext`.
|
|
1666
1008
|
modelTypenames: Object.fromEntries(Object.entries(schema.models).map(([key, def]) => [
|
|
1667
1009
|
key,
|
|
1668
|
-
def.typename ?? key,
|
|
1010
|
+
(def).typename ?? key,
|
|
1669
1011
|
])),
|
|
1670
1012
|
};
|
|
1671
1013
|
}
|
|
@@ -1688,9 +1030,9 @@ export function Ablo(options) {
|
|
|
1688
1030
|
// The live short-lived bearer (set via `setAuthToken` / `apiKey`-resolver refresh)
|
|
1689
1031
|
// is the canonical credential; fall back to a configured API key.
|
|
1690
1032
|
//
|
|
1691
|
-
// This is the
|
|
1033
|
+
// This is the sync-plane token (bootstrap, WebSocket, query HTTP). Control-plane
|
|
1692
1034
|
// calls (sessions.create, datasource registration) never use it — they
|
|
1693
|
-
// present the
|
|
1035
|
+
// present the original secret key via `controlPlaneApiKey()` below. The
|
|
1694
1036
|
// split matters: after the startup exchange this resolver returns the
|
|
1695
1037
|
// derived wide-scope `rk_`, a credential the control-plane routes
|
|
1696
1038
|
// correctly refuse (an agent token must never mint humans).
|
|
@@ -1702,9 +1044,8 @@ export function Ablo(options) {
|
|
|
1702
1044
|
setCredentialRefresher(refresher) {
|
|
1703
1045
|
store.setCredentialRefresher(refresher);
|
|
1704
1046
|
},
|
|
1705
|
-
// The org this client resolved to — null until `ready()` completes.
|
|
1706
|
-
//
|
|
1707
|
-
// agent regex-scraped `ablo status` output); now it's a property.
|
|
1047
|
+
// The org this client resolved to — null until `ready()` completes. Exposed
|
|
1048
|
+
// as a property so integrators can read it programmatically.
|
|
1708
1049
|
get organizationId() {
|
|
1709
1050
|
return _resolvedOrganizationId;
|
|
1710
1051
|
},
|
|
@@ -1712,33 +1053,33 @@ export function Ablo(options) {
|
|
|
1712
1053
|
store.nudgeReconnect();
|
|
1713
1054
|
},
|
|
1714
1055
|
sessions: {
|
|
1715
|
-
//
|
|
1716
|
-
//
|
|
1056
|
+
// A backend (holding `sk_`) mints a short-lived scoped token for one end
|
|
1057
|
+
// user or one agent.
|
|
1717
1058
|
//
|
|
1718
|
-
//
|
|
1719
|
-
//
|
|
1720
|
-
//
|
|
1721
|
-
//
|
|
1722
|
-
//
|
|
1059
|
+
// Both arms authenticate with the original secret key
|
|
1060
|
+
// (`controlPlaneApiKey()`), never the wide-scope `rk_` the startup exchange
|
|
1061
|
+
// installed as the sync credential. A derived agent credential silently
|
|
1062
|
+
// replacing the secret key on control-plane calls is how humans would get
|
|
1063
|
+
// minted as agents — and correct attribution is the point.
|
|
1723
1064
|
async create(params) {
|
|
1724
|
-
// Both mint
|
|
1065
|
+
// Both mint paths (`{ user }` → /auth/ephemeral-keys → `ek_`,
|
|
1725
1066
|
// `{ agent, can }` → /auth/capability → scoped `rk_`) resolve their
|
|
1726
1067
|
// control-plane context through the shared `buildMintContext`, so this
|
|
1727
|
-
// client, `agents.create`, and the stateless HTTP client can
|
|
1728
|
-
//
|
|
1068
|
+
// client, `agents.create`, and the stateless HTTP client can't drift on
|
|
1069
|
+
// how a token is minted.
|
|
1729
1070
|
return mintSession(params, await buildMintContext('sessions.create'));
|
|
1730
1071
|
},
|
|
1731
1072
|
},
|
|
1732
|
-
// Mint a scoped agent
|
|
1733
|
-
//
|
|
1734
|
-
// for agents that run in
|
|
1735
|
-
// fresh uuid per call, so concurrent agents are distinct participants
|
|
1736
|
-
// queue behind each other (even when they share a `name`). Humans don't
|
|
1737
|
-
// a server-built client — ship them a token via `sessions.create({ user })`.
|
|
1073
|
+
// Mint a scoped agent identity and hand back a connected client bound to it —
|
|
1074
|
+
// `sessions.create({ agent })` plus `Ablo({ apiKey })` fused into one call,
|
|
1075
|
+
// for agents that run in this (secret-key-holding) process. Omitting `id`
|
|
1076
|
+
// yields a fresh uuid per call, so concurrent agents are distinct participants
|
|
1077
|
+
// that queue behind each other (even when they share a `name`). Humans don't
|
|
1078
|
+
// get a server-built client — ship them a token via `sessions.create({ user })`.
|
|
1738
1079
|
agents: {
|
|
1739
1080
|
async create(params) {
|
|
1740
1081
|
// Distinct participant by default: omit `id` → a fresh uuid, so even two
|
|
1741
|
-
// agents that share a `name` are
|
|
1082
|
+
// agents that share a `name` are independent participants and queue
|
|
1742
1083
|
// behind one another. `name` is display only (→ userMeta.name); it never
|
|
1743
1084
|
// derives the id. Pass an explicit `id` only to re-attach an agent to
|
|
1744
1085
|
// its own held claims.
|
|
@@ -1752,7 +1093,7 @@ export function Ablo(options) {
|
|
|
1752
1093
|
...(userMeta ? { userMeta } : {}),
|
|
1753
1094
|
};
|
|
1754
1095
|
// Re-mint the `rk_` on every resolver call so a long-lived agent client
|
|
1755
|
-
// never hits token expiry; the `sk_` stays in
|
|
1096
|
+
// never hits token expiry; the `sk_` stays in this process — the child
|
|
1756
1097
|
// only ever sees its own short-lived `rk_`.
|
|
1757
1098
|
const mintToken = async () => (await mintSession(sessionParams, await buildMintContext('agents.create')))
|
|
1758
1099
|
.token;
|
|
@@ -1799,7 +1140,7 @@ export function Ablo(options) {
|
|
|
1799
1140
|
* the session (WebSocket close code 1008/4001/4003 or a session_error
|
|
1800
1141
|
* frame). Multiple subscribers supported; returns an unsubscribe
|
|
1801
1142
|
* function. Consumers typically use this to trigger auth-failed UI
|
|
1802
|
-
* flows (e.g., redirect to sign-in). Does
|
|
1143
|
+
* flows (e.g., redirect to sign-in). Does not automatically purge the
|
|
1803
1144
|
* IndexedDB — call `engine.purge()` from the listener if you need
|
|
1804
1145
|
* that behavior (the SDK's `<AbloProvider>` does this by default).
|
|
1805
1146
|
*/
|
|
@@ -1826,7 +1167,7 @@ export function Ablo(options) {
|
|
|
1826
1167
|
// the pool). Prefixed with _ to signal "internal but stable."
|
|
1827
1168
|
/** The BaseSyncedStore — implements SyncStoreContract for SyncContext.Provider. */
|
|
1828
1169
|
get _store() { return store; },
|
|
1829
|
-
/** The
|
|
1170
|
+
/** The InstanceCache — for demand loaders that need pool.createFromData(). */
|
|
1830
1171
|
get _pool() { return objectPool; },
|
|
1831
1172
|
/** The SyncWebSocket — for collaboration events (slide selection, cursors). */
|
|
1832
1173
|
get _ws() { return store.getSyncWebSocket() ?? null; },
|