@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
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Builds the typed client for a single schema model — the object reached as
|
|
3
|
+
* `ablo.<model>`.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* local-graph snapshots `get` / `getAll` / `getCount`, the writes
|
|
12
|
-
* `create` / `update` / `delete`, the coordination namespace `claim`
|
|
13
|
-
* (`claim({ id })` plus `claim.state` / `claim.queue` / `claim.release` /
|
|
14
|
-
* `claim.reorder`), and `onChange`. The factory returns a plain object; the
|
|
15
|
-
* client assembles the `ablo.<model>` lookup table from these.
|
|
5
|
+
* Each schema model gets one {@link ModelOperations}: the async server reads
|
|
6
|
+
* `retrieve` and `list`, the synchronous local-graph snapshots `get`, `getAll`,
|
|
7
|
+
* and `getCount`, the writes `create`, `update`, and `delete`, the coordination
|
|
8
|
+
* namespace `claim` (callable as `claim({ id })`, plus `claim.state`,
|
|
9
|
+
* `claim.queue`, `claim.release`, and `claim.reorder`), `watch`, and `onChange`.
|
|
10
|
+
* The factory returns a plain object; the client assembles the `ablo.<model>`
|
|
11
|
+
* lookup table from one of these per model.
|
|
16
12
|
*/
|
|
17
13
|
import { autorun } from 'mobx';
|
|
18
|
-
import { AbloClaimedError, AbloValidationError, formatClaimedErrorMessage, toAbloError, } from '../errors.js';
|
|
14
|
+
import { AbloClaimedError, AbloStaleContextError, AbloValidationError, formatClaimedErrorMessage, toAbloError, } from '../errors.js';
|
|
19
15
|
import { descriptionFromMeta } from '../coordination/schema.js';
|
|
20
16
|
import { reconcileFunctionalUpdate, } from './functionalUpdate.js';
|
|
21
17
|
import { Model, modelAsRow } from '../Model.js';
|
|
22
18
|
import { toMs } from '../utils/duration.js';
|
|
19
|
+
import { LEASE_TTL_MS } from '../wire/protocol.js';
|
|
20
|
+
import { heartbeatCadenceMs, resolveHeartbeatOptions, startClaimHeartbeatLoop, } from './claimHeartbeatLoop.js';
|
|
23
21
|
import { assertWriteOptions } from './writeOptionsSchema.js';
|
|
24
22
|
import { ModelScope } from '../types/index.js';
|
|
25
23
|
const modelClientMeta = new WeakMap();
|
|
@@ -34,15 +32,15 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
34
32
|
throw new AbloValidationError(`Ablo: schema model "${schemaKey}" resolved to "${registeredModelName}", ` +
|
|
35
33
|
'but no matching constructor was registered.', { code: 'model_not_registered' });
|
|
36
34
|
}
|
|
37
|
-
// The coordination plane
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// target.model
|
|
45
|
-
// use this.
|
|
35
|
+
// The coordination plane must speak the same wire dialect as the commit
|
|
36
|
+
// plane: the lowercased typename (`task`), not the schema key (`tasks`). The
|
|
37
|
+
// server's commit-time claim guard probes the lease store with the commit
|
|
38
|
+
// operation's model name, so a lease recorded under the schema key never
|
|
39
|
+
// matches — which would silently disarm the guard for every model whose
|
|
40
|
+
// schema key differs from its typename (a plural key against a singular
|
|
41
|
+
// typename, i.e. nearly all of them). Public surfaces such as
|
|
42
|
+
// `Claim.target.model` keep the schema key; only the wire and coordination
|
|
43
|
+
// targets use this.
|
|
46
44
|
const wireModel = registeredModelName.toLowerCase();
|
|
47
45
|
// Last-line guarantee for the public surface: any rejection from a lower
|
|
48
46
|
// layer (transport timeout, IndexedDB failure, a third-party throw) is
|
|
@@ -75,19 +73,22 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
75
73
|
await syncClient.waitForConfirmation(model.getModelName(), model.id);
|
|
76
74
|
};
|
|
77
75
|
// Claims this proxy currently holds, keyed by entity id. Lets the flat
|
|
78
|
-
// `release({ id })` and `update({ id, data })` find the lease
|
|
79
|
-
// took
|
|
76
|
+
// `release({ id })` and `update({ id, data })` find the lease and snapshot a
|
|
77
|
+
// `claim({ id })` took, without a per-call handle. Released on dispose,
|
|
78
|
+
// explicit release, or TTL expiry.
|
|
80
79
|
//
|
|
81
|
-
// `target
|
|
80
|
+
// `target`, `reason`, and `expiresAt` are kept alongside the lease so
|
|
82
81
|
// `claim.state` can synthesize a self-claim: the server excludes a holder's
|
|
83
|
-
// own presence frames, so
|
|
84
|
-
//
|
|
85
|
-
// TTL (a
|
|
86
|
-
// the server's keepalive lease window when no TTL was requested.
|
|
82
|
+
// own presence frames, so this proxy is the only place that knows the client
|
|
83
|
+
// holds the row. `expiresAt` is the client's best estimate from the requested
|
|
84
|
+
// TTL (a real epoch-millisecond expiry, not a fabricated watermark), defaulting
|
|
85
|
+
// to the server's keepalive lease window when no TTL was requested.
|
|
87
86
|
const activeClaims = new Map();
|
|
88
|
-
// Server keepalive lease window
|
|
89
|
-
//
|
|
90
|
-
|
|
87
|
+
// Server keepalive lease window — the same `LEASE_TTL_MS` the wire protocol
|
|
88
|
+
// declares, so the client's estimate and the server's lease cannot drift.
|
|
89
|
+
// This is the fallback expiry estimate when a claim is taken without an
|
|
90
|
+
// explicit TTL.
|
|
91
|
+
const DEFAULT_LEASE_TTL_MS = LEASE_TTL_MS;
|
|
91
92
|
const isClaimHandle = (value) => typeof value === 'object' &&
|
|
92
93
|
value !== null &&
|
|
93
94
|
value.object === 'claim' &&
|
|
@@ -121,9 +122,9 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
121
122
|
};
|
|
122
123
|
const mutationOptions = (params) => {
|
|
123
124
|
const { id: _id, data: _data, claim: _claim, ...rest } = params;
|
|
124
|
-
//
|
|
125
|
-
// Catches plain-
|
|
126
|
-
// a typed error instead of a silent no-op or a server 400.
|
|
125
|
+
// The write-options schema — the runtime twin of the compile-time params.
|
|
126
|
+
// Catches plain-JavaScript callers (for example `onStale: 'rejct'`) at the
|
|
127
|
+
// call site with a typed error instead of a silent no-op or a server 400.
|
|
127
128
|
assertWriteOptions(rest, `${schemaKey} write`);
|
|
128
129
|
return rest;
|
|
129
130
|
};
|
|
@@ -139,12 +140,12 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
139
140
|
throw new AbloValidationError(`Model "${schemaKey}" was built without the collaboration runtime, so claim() is unavailable here. Claiming needs no per-model config — use the standard Ablo({ schema, apiKey }) client and every model is claimable.`, { code: 'model_claim_not_configured' });
|
|
140
141
|
}
|
|
141
142
|
const { id, ...options } = params;
|
|
142
|
-
// Is someone
|
|
143
|
-
// snapshot up front — it decides whether
|
|
144
|
-
// claim (a free
|
|
143
|
+
// Is someone else already on this target? Read the local coordination
|
|
144
|
+
// snapshot up front — it decides whether a re-read is needed after the
|
|
145
|
+
// claim (a free or already-held target cannot have changed underneath us).
|
|
145
146
|
const held = collaboration.state({ model: wireModel, id });
|
|
146
147
|
const contended = !!held && held.heldBy !== collaboration.selfParticipantId;
|
|
147
|
-
const failFast = options
|
|
148
|
+
const failFast = options.queue === false;
|
|
148
149
|
// Fail-fast (`queue: false`): if another participant already holds it,
|
|
149
150
|
// reject now instead of queuing. Best-effort at the client (a racing
|
|
150
151
|
// claim not yet synced into our snapshot slips through here) — the
|
|
@@ -152,13 +153,13 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
152
153
|
// the loser's first write. For work-distribution dedup that's exactly
|
|
153
154
|
// right: don't wait (that would double-process), skip.
|
|
154
155
|
if (failFast && contended) {
|
|
155
|
-
const claim =
|
|
156
|
+
const claim = claimContextFromClaim(held);
|
|
156
157
|
throw new AbloClaimedError(formatClaimedErrorMessage({
|
|
157
158
|
targetLabel: `${registeredModelName}/${id}`,
|
|
158
|
-
heldBy: held
|
|
159
|
+
heldBy: held.heldBy,
|
|
159
160
|
claim,
|
|
160
|
-
fallback: `${registeredModelName}/${id} is held by ${held
|
|
161
|
-
}), { code: 'entity_claimed', claims:
|
|
161
|
+
fallback: `${registeredModelName}/${id} is held by ${held.heldBy ?? 'another participant'}.`,
|
|
162
|
+
}), { code: 'entity_claimed', claims: [claim] });
|
|
162
163
|
}
|
|
163
164
|
// Ensure the row exists locally before claiming.
|
|
164
165
|
let model = objectPool.get(id);
|
|
@@ -169,65 +170,65 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
169
170
|
if (!model) {
|
|
170
171
|
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
171
172
|
}
|
|
172
|
-
// Write
|
|
173
|
-
// holder's claim presence broadcasts to
|
|
174
|
-
// including a peer that subscribed just before us. Pinning before the
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
173
|
+
// Write intent: enter the entity scope before acquiring the lease so the
|
|
174
|
+
// holder's claim presence broadcasts to everyone in this entity group,
|
|
175
|
+
// including a peer that subscribed just before us. Pinning before the lease
|
|
176
|
+
// rather than after closes the subscribe-versus-broadcast race: the server
|
|
177
|
+
// fans presence out at claim time, so this client must be in the group when
|
|
178
|
+
// the claim lands. Awaited because the broadcast ordering depends on it;
|
|
179
|
+
// still best-effort (the store swallows reconcile errors).
|
|
179
180
|
await collaboration.pinScope?.({ [schemaKey]: id });
|
|
180
|
-
// Acquire the lease.
|
|
181
|
-
// fair FIFO queue
|
|
182
|
-
// ours, blocking behind any current holder, with no
|
|
183
|
-
// orders contenders
|
|
184
|
-
// observed conflict above, so this just records
|
|
181
|
+
// Acquire the lease. By default (`queue` is not false) this goes through the
|
|
182
|
+
// server's fair FIFO queue: `queue: true` resolves only once the lease is
|
|
183
|
+
// genuinely ours, blocking behind any current holder, with no check-then-act
|
|
184
|
+
// gap because the server orders contenders. Fail-fast skips the queue: an
|
|
185
|
+
// observed conflict was already rejected above, so this just records the lease.
|
|
185
186
|
const lease = await collaboration.createClaim({
|
|
186
187
|
target: {
|
|
187
188
|
model: wireModel,
|
|
188
189
|
id,
|
|
189
|
-
...(options
|
|
190
|
-
...(options
|
|
191
|
-
...(options
|
|
190
|
+
...(options.field ? { field: options.field } : {}),
|
|
191
|
+
...(options.path ? { path: options.path } : {}),
|
|
192
|
+
...(options.range ? { range: options.range } : {}),
|
|
192
193
|
...(claimMeta(options) ? { meta: claimMeta(options) } : {}),
|
|
193
194
|
},
|
|
194
|
-
reason: options
|
|
195
|
-
ttl: options
|
|
195
|
+
reason: options.reason ?? 'editing',
|
|
196
|
+
ttl: options.ttl,
|
|
196
197
|
queue: !failFast,
|
|
197
|
-
maxQueueDepth: options
|
|
198
|
+
maxQueueDepth: options.maxQueueDepth,
|
|
198
199
|
});
|
|
199
|
-
// Only when
|
|
200
|
-
// changed underneath us — re-read so the claimed snapshot reflects what
|
|
201
|
-
//
|
|
202
|
-
// - `lease.waited` — the server granted
|
|
203
|
-
// provably queued behind a holder. Authoritative; works even when
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
// - `contended` — the local snapshot saw a holder up front. Kept for
|
|
207
|
-
//
|
|
200
|
+
// Only when the claim actually waited behind another holder can the row have
|
|
201
|
+
// changed underneath us — re-read so the claimed snapshot reflects what that
|
|
202
|
+
// holder committed before releasing. Either of two signals suffices:
|
|
203
|
+
// - `lease.waited` — the server granted the claim after the client
|
|
204
|
+
// provably queued behind a holder. Authoritative; it works even when the
|
|
205
|
+
// local snapshot is blind, since claim fan-out is entity-scoped and a
|
|
206
|
+
// broadly-subscribed client never observes peers' claims.
|
|
207
|
+
// - `contended` — the local snapshot saw a holder up front. Kept for the
|
|
208
|
+
// no-queue paths, where no grant frame exists.
|
|
208
209
|
if ((contended || lease.waited === true) && !failFast) {
|
|
209
|
-
// `type: 'complete'` forces the round-trip: the hydration ledger
|
|
210
|
-
// otherwise
|
|
211
|
-
// holder's final write may not have fanned out
|
|
212
|
-
// stale-snapshot race this re-read
|
|
210
|
+
// `type: 'complete'` forces the round-trip: the hydration ledger would
|
|
211
|
+
// otherwise serve the local row for an already-hydrated id, and the
|
|
212
|
+
// holder's final write may not have fanned out yet — the exact
|
|
213
|
+
// stale-snapshot race this re-read closes.
|
|
213
214
|
await load({ where: [['id', id]], type: 'complete' });
|
|
214
215
|
model = objectPool.get(id) ?? model;
|
|
215
216
|
}
|
|
216
217
|
const snapshot = collaboration.createSnapshot(schemaKey, id);
|
|
217
|
-
const reason = options
|
|
218
|
+
const reason = options.reason ?? 'editing';
|
|
218
219
|
// The self-claim's `ClaimTarget` mirrors what a peer's `claim.state` would
|
|
219
|
-
// report (`state` maps `held.target.model`
|
|
220
|
-
// peer see the
|
|
220
|
+
// report (`state` maps `held.target.model` to `type`), so a holder and a
|
|
221
|
+
// peer see the same `target.type` for one row — the wire model token.
|
|
221
222
|
const selfTarget = {
|
|
222
223
|
type: wireModel,
|
|
223
224
|
id,
|
|
224
|
-
...(options
|
|
225
|
-
...(options
|
|
226
|
-
...(options
|
|
225
|
+
...(options.field ? { field: options.field } : {}),
|
|
226
|
+
...(options.path ? { path: options.path } : {}),
|
|
227
|
+
...(options.range ? { range: options.range } : {}),
|
|
227
228
|
...(claimMeta(options) ? { meta: claimMeta(options) } : {}),
|
|
228
229
|
};
|
|
229
|
-
const
|
|
230
|
-
|
|
230
|
+
const ttlMs = options.ttl !== undefined ? toMs(options.ttl) : DEFAULT_LEASE_TTL_MS;
|
|
231
|
+
const expiresAt = Date.now() + ttlMs;
|
|
231
232
|
activeClaims.set(id, {
|
|
232
233
|
lease,
|
|
233
234
|
snapshot,
|
|
@@ -238,24 +239,57 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
238
239
|
const target = {
|
|
239
240
|
type: schemaKey,
|
|
240
241
|
id,
|
|
241
|
-
...(options
|
|
242
|
-
...(options
|
|
243
|
-
...(options
|
|
242
|
+
...(options.field ? { field: options.field } : {}),
|
|
243
|
+
...(options.path ? { path: options.path } : {}),
|
|
244
|
+
...(options.range ? { range: options.range } : {}),
|
|
244
245
|
...(claimMeta(options) ? { meta: claimMeta(options) } : {}),
|
|
245
246
|
};
|
|
246
|
-
|
|
247
|
+
// A beat resolves with the server's extended expiry; keep the local
|
|
248
|
+
// self-claim estimate in step so `claim.state` renders the real window,
|
|
249
|
+
// and surface every answer through `onHeartbeat` (pressure signal).
|
|
250
|
+
const heartbeat = async (beatOptions) => {
|
|
251
|
+
if (!lease.heartbeat) {
|
|
252
|
+
throw new AbloValidationError('This claim handle has no heartbeat wiring, which the standard Ablo({ schema, apiKey }) client provides on every claim. This appears only when a claim is minted through an internal path that predates heartbeats.', { code: 'claim_not_wired' });
|
|
253
|
+
}
|
|
254
|
+
const resolved = resolveHeartbeatOptions(beatOptions);
|
|
255
|
+
const beat = await lease.heartbeat({
|
|
256
|
+
ttl: resolved.ttl ?? options.ttl,
|
|
257
|
+
...(resolved.details !== undefined ? { details: resolved.details } : {}),
|
|
258
|
+
});
|
|
259
|
+
const held = activeClaims.get(id);
|
|
260
|
+
if (held)
|
|
261
|
+
held.expiresAt = beat.expiresAt;
|
|
262
|
+
options.onHeartbeat?.(beat);
|
|
263
|
+
return beat;
|
|
264
|
+
};
|
|
265
|
+
// Opt-in auto-heartbeat: the loop beats until release, and a definitive
|
|
266
|
+
// loss stops it and surfaces through `onHeartbeatLost`.
|
|
267
|
+
const stopHeartbeatLoop = options.heartbeat
|
|
268
|
+
? startClaimHeartbeatLoop({
|
|
269
|
+
beat: () => heartbeat(),
|
|
270
|
+
intervalMs: heartbeatCadenceMs(ttlMs, options.heartbeat),
|
|
271
|
+
...(options.onHeartbeatLost
|
|
272
|
+
? { onLost: options.onHeartbeatLost }
|
|
273
|
+
: {}),
|
|
274
|
+
})
|
|
275
|
+
: undefined;
|
|
276
|
+
const release = () => {
|
|
277
|
+
stopHeartbeatLoop?.();
|
|
278
|
+
return releaseClaim(id);
|
|
279
|
+
};
|
|
247
280
|
return {
|
|
248
281
|
object: 'claim',
|
|
249
282
|
id: lease.id,
|
|
250
283
|
readAt: snapshot.stamp,
|
|
251
284
|
target,
|
|
252
285
|
reason,
|
|
253
|
-
...(options
|
|
286
|
+
...(options.description ? { description: options.description } : {}),
|
|
254
287
|
data: modelAsRow(model),
|
|
255
288
|
release,
|
|
256
289
|
revoke: () => {
|
|
257
290
|
void release();
|
|
258
291
|
},
|
|
292
|
+
heartbeat,
|
|
259
293
|
[Symbol.asyncDispose]: release,
|
|
260
294
|
};
|
|
261
295
|
};
|
|
@@ -266,15 +300,15 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
266
300
|
// are the same object.
|
|
267
301
|
const claimApi = Object.assign(guard(claim), {
|
|
268
302
|
state(params) {
|
|
269
|
-
// Read
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
303
|
+
// Read interest: a passive observer of a row's claim state must enter that
|
|
304
|
+
// row's entity scope, or it sits only on broader `org:`/`user:` groups and
|
|
305
|
+
// never receives the holder's entity-scoped claim presence. Best-effort
|
|
306
|
+
// and fire-and-forget — it never blocks or rejects the read.
|
|
273
307
|
void collaboration?.enterScope?.({ [schemaKey]: params.id });
|
|
274
|
-
// Self-awareness: the server excludes a holder's
|
|
275
|
-
// the client skips them, so `state`
|
|
276
|
-
// Synthesize the active claim
|
|
277
|
-
//
|
|
308
|
+
// Self-awareness: the server excludes a holder's own presence frames and
|
|
309
|
+
// the client skips them, so `state` would return null for a row this client
|
|
310
|
+
// holds. Synthesize the active claim from the stored lease so the holder
|
|
311
|
+
// sees its own claim, honoring the documented contract on `claim.state`.
|
|
278
312
|
const own = activeClaims.get(params.id);
|
|
279
313
|
if (own) {
|
|
280
314
|
return {
|
|
@@ -303,10 +337,10 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
303
337
|
});
|
|
304
338
|
const operations = {
|
|
305
339
|
retrieve: guard(async (params) => {
|
|
306
|
-
// Read-interest enrolment:
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
// fire-and-forget — never
|
|
340
|
+
// Read-interest enrolment: reading a row enters its entity scope, so a
|
|
341
|
+
// client lands in the same group the holder's claim presence fans out
|
|
342
|
+
// on and `claim.state`/`claim.queue` report peers. Best-effort and
|
|
343
|
+
// fire-and-forget — it never makes the read reject or run slower.
|
|
310
344
|
void collaboration?.enterScope?.({ [schemaKey]: params.id });
|
|
311
345
|
const rows = await load({
|
|
312
346
|
...params,
|
|
@@ -315,9 +349,8 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
315
349
|
});
|
|
316
350
|
return rows[0];
|
|
317
351
|
}),
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
// enrolment is a deliberate follow-up (a bounded, opt-in policy).
|
|
352
|
+
// No automatic scope enrolment on bulk `list`/`getAll`: that would subscribe
|
|
353
|
+
// to an unbounded set of rows' entity groups.
|
|
321
354
|
list: guard(load),
|
|
322
355
|
get(id) {
|
|
323
356
|
return objectPool.get(id);
|
|
@@ -338,8 +371,9 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
338
371
|
if (options?.filter) {
|
|
339
372
|
result = result.filter(options.filter);
|
|
340
373
|
}
|
|
341
|
-
|
|
342
|
-
|
|
374
|
+
const orderEntry = options?.orderBy ? Object.entries(options.orderBy)[0] : undefined;
|
|
375
|
+
if (orderEntry) {
|
|
376
|
+
const [field, dir] = orderEntry;
|
|
343
377
|
result = [...result].sort((a, b) => {
|
|
344
378
|
const av = a[field];
|
|
345
379
|
const bv = b[field];
|
|
@@ -367,11 +401,11 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
367
401
|
if (!collaboration) {
|
|
368
402
|
throw new AbloValidationError(`Model "${schemaKey}" was built without the collaboration runtime, so claim() is unavailable here. Claiming needs no per-model config — use the standard Ablo({ schema, apiKey }) client and every model is claimable.`, { code: 'model_claim_not_configured' });
|
|
369
403
|
}
|
|
370
|
-
// Write
|
|
371
|
-
// create-claim so the holder's claim presence broadcasts to
|
|
372
|
-
// already in this entity group (closing the subscribe-
|
|
404
|
+
// Write intent: enter the new row's entity scope before acquiring the
|
|
405
|
+
// create-claim so the holder's claim presence broadcasts to everyone
|
|
406
|
+
// already in this entity group (closing the subscribe-versus-broadcast
|
|
373
407
|
// race — see `takeClaim`). Released with the lease in the `finally`
|
|
374
|
-
// below. Awaited for broadcast ordering; still
|
|
408
|
+
// below. Awaited for broadcast ordering; still best-effort.
|
|
375
409
|
await collaboration.pinScope?.({ [schemaKey]: id });
|
|
376
410
|
autoLease = await collaboration.createClaim({
|
|
377
411
|
target: {
|
|
@@ -388,10 +422,10 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
388
422
|
maxQueueDepth: claim.maxQueueDepth,
|
|
389
423
|
});
|
|
390
424
|
}
|
|
391
|
-
// Default `organizationId` from the client's identity
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
425
|
+
// Default `organizationId` from the client's identity, matching the other
|
|
426
|
+
// write path — without this, a caller that omits it would create an
|
|
427
|
+
// org-unscoped row on one write path but not the other. An explicit value
|
|
428
|
+
// in `data` still wins via the spread.
|
|
395
429
|
const orgDefault = params.data.organizationId ??
|
|
396
430
|
syncClient.getOrganizationId();
|
|
397
431
|
const model = new ModelClass({
|
|
@@ -477,7 +511,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
477
511
|
return await operations.update({ ...params, claim: handle });
|
|
478
512
|
}
|
|
479
513
|
finally {
|
|
480
|
-
await handle.release
|
|
514
|
+
await handle.release();
|
|
481
515
|
}
|
|
482
516
|
}
|
|
483
517
|
const { id } = params;
|
|
@@ -511,10 +545,10 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
511
545
|
...opts,
|
|
512
546
|
...(handle ? { claim: { id: handle.id } } : {}),
|
|
513
547
|
};
|
|
514
|
-
// Local user update: `applyChanges` keeps change tracking
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
548
|
+
// Local user update: `applyChanges` keeps change tracking on so the
|
|
549
|
+
// edited fields land in `modifiedProperties` and are actually sent to
|
|
550
|
+
// the server. (`updateFromData` is the hydration path and would discard
|
|
551
|
+
// the tracking, producing an empty `input: {}` no-op mutation.)
|
|
518
552
|
model.applyChanges(params.data);
|
|
519
553
|
syncClient.update(model, effective);
|
|
520
554
|
await waitForMutation(model, effective);
|
|
@@ -533,17 +567,17 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
533
567
|
await operations.delete({ ...params, claim: handle });
|
|
534
568
|
}
|
|
535
569
|
finally {
|
|
536
|
-
await handle.release
|
|
570
|
+
await handle.release();
|
|
537
571
|
}
|
|
538
572
|
return;
|
|
539
573
|
}
|
|
540
574
|
const { id } = params;
|
|
541
575
|
const model = objectPool.get(id);
|
|
542
|
-
// Idempotent delete
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
576
|
+
// Idempotent delete: "ensure absent". A row that isn't in this client's
|
|
577
|
+
// replicated view is already gone from its perspective, so a delete is a
|
|
578
|
+
// no-op success rather than an `entity_not_found` error. This matches the
|
|
579
|
+
// HTTP client and makes delete safe to retry or race (two actors deleting
|
|
580
|
+
// the same row).
|
|
547
581
|
if (!model)
|
|
548
582
|
return;
|
|
549
583
|
const claimed = activeClaims.get(id);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Support for the endpoint-string form of `apiKey`.
|
|
3
|
+
*
|
|
4
|
+
* With `Ablo({ schema, apiKey: '/api/ablo-session' })`, you point the client at
|
|
5
|
+
* your own session-mint route and the client owns the exchange: it POSTs to the
|
|
6
|
+
* endpoint, parses the minted token, keeps it fresh, and classifies failures
|
|
7
|
+
* onto the resolver's three outcomes. The string form covers the common case;
|
|
8
|
+
* the function form of `apiKey` remains the escape hatch for custom headers,
|
|
9
|
+
* bodies, or non-HTTP mints.
|
|
10
|
+
*
|
|
11
|
+
* The form is detected by prefix: a string starting with `/`, `http://`, or
|
|
12
|
+
* `https://` is an endpoint, and anything else is a literal key. Ablo keys are
|
|
13
|
+
* prefixed (`sk_`/`pk_`/`ek_`/`rk_`), so the two shapes cannot collide. An
|
|
14
|
+
* `ABLO_API_KEY` environment value is never treated as an endpoint — it is
|
|
15
|
+
* always a literal key (see `resolveApiKey`).
|
|
16
|
+
*
|
|
17
|
+
* Wire contract:
|
|
18
|
+
* POST <endpoint> (same-origin, `credentials: 'include'` so cookies flow)
|
|
19
|
+
* → 200 `{ token, expiresAt? }` a fresh short-lived `ek_`/`rk_`
|
|
20
|
+
* → 200 `{ token: null }` or 401/403 the login itself is gone (sign out)
|
|
21
|
+
* → anything else transient — retry, do not sign out
|
|
22
|
+
*
|
|
23
|
+
* The three-way mapping is the reason to build this in. Hand-written token
|
|
24
|
+
* fetchers routinely get it wrong — mapping any non-OK response to `null` signs
|
|
25
|
+
* the user out on a 500. Encoded here once, every consumer inherits the correct
|
|
26
|
+
* split between a terminal sign-out and a transient retry.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* An async callable that resolves the current credential. It serves two uses:
|
|
30
|
+
* credential rotation (for example against AWS STS, GCP IAM, or Vault) and the
|
|
31
|
+
* short-lived per-user browser path (minting a fresh `ek_`/`rk_` from the
|
|
32
|
+
* signed-in session). It is re-exported from `./auth` so existing import paths
|
|
33
|
+
* keep working, and defined here so the resolver it types has no import cycle.
|
|
34
|
+
*
|
|
35
|
+
* The contract has three outcomes: resolve a token; resolve `null` when the
|
|
36
|
+
* login itself is gone (terminal — the credential lifecycle treats this as
|
|
37
|
+
* `session_expired` and signs out); or throw on a transient failure (back off
|
|
38
|
+
* and retry, without signing out). A long-lived static `apiKey` string needs
|
|
39
|
+
* none of this and is used as-is.
|
|
40
|
+
*/
|
|
41
|
+
export type ApiKeySetter = () => Promise<string | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Is this `apiKey` string a session-mint endpoint rather than a literal key?
|
|
44
|
+
* Prefix rule: `/relative/path`, `http://…`, or `https://…`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function isCredentialEndpoint(value: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Builds the resolver behind an endpoint-string `apiKey`. It follows the
|
|
49
|
+
* {@link ApiKeySetter} contract end to end:
|
|
50
|
+
* - resolves the minted token string on success;
|
|
51
|
+
* - resolves `null` when the login is gone (a 401 or 403, or an explicit
|
|
52
|
+
* `{ token: null }`) — terminal, so the client signs out;
|
|
53
|
+
* - throws on anything transient (a network failure, a 5xx or 429, or a
|
|
54
|
+
* malformed response) — so the lifecycle backs off and retries without
|
|
55
|
+
* signing out.
|
|
56
|
+
*
|
|
57
|
+
* A relative endpoint invoked on a server (where `fetch` has no origin) throws,
|
|
58
|
+
* which is transient by contract; the credential lifecycle translates that exact
|
|
59
|
+
* failure into an actionable "use an absolute URL server-side" warning.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createEndpointCredentialResolver(endpoint: string): ApiKeySetter;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Support for the endpoint-string form of `apiKey`.
|
|
3
|
+
*
|
|
4
|
+
* With `Ablo({ schema, apiKey: '/api/ablo-session' })`, you point the client at
|
|
5
|
+
* your own session-mint route and the client owns the exchange: it POSTs to the
|
|
6
|
+
* endpoint, parses the minted token, keeps it fresh, and classifies failures
|
|
7
|
+
* onto the resolver's three outcomes. The string form covers the common case;
|
|
8
|
+
* the function form of `apiKey` remains the escape hatch for custom headers,
|
|
9
|
+
* bodies, or non-HTTP mints.
|
|
10
|
+
*
|
|
11
|
+
* The form is detected by prefix: a string starting with `/`, `http://`, or
|
|
12
|
+
* `https://` is an endpoint, and anything else is a literal key. Ablo keys are
|
|
13
|
+
* prefixed (`sk_`/`pk_`/`ek_`/`rk_`), so the two shapes cannot collide. An
|
|
14
|
+
* `ABLO_API_KEY` environment value is never treated as an endpoint — it is
|
|
15
|
+
* always a literal key (see `resolveApiKey`).
|
|
16
|
+
*
|
|
17
|
+
* Wire contract:
|
|
18
|
+
* POST <endpoint> (same-origin, `credentials: 'include'` so cookies flow)
|
|
19
|
+
* → 200 `{ token, expiresAt? }` a fresh short-lived `ek_`/`rk_`
|
|
20
|
+
* → 200 `{ token: null }` or 401/403 the login itself is gone (sign out)
|
|
21
|
+
* → anything else transient — retry, do not sign out
|
|
22
|
+
*
|
|
23
|
+
* The three-way mapping is the reason to build this in. Hand-written token
|
|
24
|
+
* fetchers routinely get it wrong — mapping any non-OK response to `null` signs
|
|
25
|
+
* the user out on a 500. Encoded here once, every consumer inherits the correct
|
|
26
|
+
* split between a terminal sign-out and a transient retry.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Is this `apiKey` string a session-mint endpoint rather than a literal key?
|
|
30
|
+
* Prefix rule: `/relative/path`, `http://…`, or `https://…`.
|
|
31
|
+
*/
|
|
32
|
+
export function isCredentialEndpoint(value) {
|
|
33
|
+
return value.startsWith('/') || /^https?:\/\//i.test(value);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Builds the resolver behind an endpoint-string `apiKey`. It follows the
|
|
37
|
+
* {@link ApiKeySetter} contract end to end:
|
|
38
|
+
* - resolves the minted token string on success;
|
|
39
|
+
* - resolves `null` when the login is gone (a 401 or 403, or an explicit
|
|
40
|
+
* `{ token: null }`) — terminal, so the client signs out;
|
|
41
|
+
* - throws on anything transient (a network failure, a 5xx or 429, or a
|
|
42
|
+
* malformed response) — so the lifecycle backs off and retries without
|
|
43
|
+
* signing out.
|
|
44
|
+
*
|
|
45
|
+
* A relative endpoint invoked on a server (where `fetch` has no origin) throws,
|
|
46
|
+
* which is transient by contract; the credential lifecycle translates that exact
|
|
47
|
+
* failure into an actionable "use an absolute URL server-side" warning.
|
|
48
|
+
*/
|
|
49
|
+
export function createEndpointCredentialResolver(endpoint) {
|
|
50
|
+
return async () => {
|
|
51
|
+
// `fetch()` rejections (offline, DNS, a relative URL on a server) propagate
|
|
52
|
+
// as-is: a throw is the transient signal in the resolver contract.
|
|
53
|
+
const res = await fetch(endpoint, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
credentials: 'include',
|
|
56
|
+
});
|
|
57
|
+
// The login itself is gone — terminal. Only these two statuses sign out.
|
|
58
|
+
if (res.status === 401 || res.status === 403)
|
|
59
|
+
return null;
|
|
60
|
+
// 5xx / 429 / anything unexpected: the login may be perfectly valid —
|
|
61
|
+
// transient, retry later.
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
throw new Error(`credential endpoint ${endpoint} answered ${res.status} — transient, will retry`);
|
|
64
|
+
}
|
|
65
|
+
let body;
|
|
66
|
+
try {
|
|
67
|
+
body = await res.json();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new Error(`credential endpoint ${endpoint} returned non-JSON — expected { token }`);
|
|
71
|
+
}
|
|
72
|
+
// Distinguish "explicitly signed out" (`{ token: null }`) from "not a mint
|
|
73
|
+
// endpoint at all" (no `token` key). A misconfiguration must fail loudly and
|
|
74
|
+
// transiently, never as a silent sign-out.
|
|
75
|
+
if (typeof body !== 'object' || body === null || !('token' in body)) {
|
|
76
|
+
throw new Error(`credential endpoint ${endpoint} returned no \`token\` field — expected { token }`);
|
|
77
|
+
}
|
|
78
|
+
const token = body.token;
|
|
79
|
+
if (token === null || token === undefined)
|
|
80
|
+
return null;
|
|
81
|
+
if (typeof token !== 'string') {
|
|
82
|
+
throw new Error(`credential endpoint ${endpoint} returned a non-string \`token\``);
|
|
83
|
+
}
|
|
84
|
+
return token;
|
|
85
|
+
};
|
|
86
|
+
}
|