@abloatai/humans 0.37.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/README.md +52 -0
- package/dist/Ablo.d.ts +208 -0
- package/dist/Ablo.js +120 -0
- package/dist/client.d.ts +317 -0
- package/dist/client.js +13 -0
- package/dist/core.d.ts +35 -0
- package/dist/core.js +48 -0
- package/dist/humans.d.ts +28 -0
- package/dist/humans.js +34 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/local/BaseSyncedStore.d.ts +807 -0
- package/dist/local/BaseSyncedStore.js +1516 -0
- package/dist/local/Database.d.ts +322 -0
- package/dist/local/Database.js +1589 -0
- package/dist/local/InstanceCache.d.ts +255 -0
- package/dist/local/InstanceCache.js +1263 -0
- package/dist/local/LazyReferenceCollection.d.ts +177 -0
- package/dist/local/LazyReferenceCollection.js +461 -0
- package/dist/local/Model.d.ts +475 -0
- package/dist/local/Model.js +950 -0
- package/dist/local/ModelRegistry.d.ts +225 -0
- package/dist/local/ModelRegistry.js +539 -0
- package/dist/local/NetworkMonitor.d.ts +28 -0
- package/dist/local/NetworkMonitor.js +79 -0
- package/dist/local/RuntimeContext.d.ts +52 -0
- package/dist/local/RuntimeContext.js +80 -0
- package/dist/local/SyncClient.d.ts +516 -0
- package/dist/local/SyncClient.js +1754 -0
- package/dist/local/adapters/alwaysOnline.d.ts +14 -0
- package/dist/local/adapters/alwaysOnline.js +17 -0
- package/dist/local/adapters/inMemoryStorage.d.ts +37 -0
- package/dist/local/adapters/inMemoryStorage.js +122 -0
- package/dist/local/client/clientPrelude.d.ts +52 -0
- package/dist/local/client/clientPrelude.js +60 -0
- package/dist/local/client/consoleLogger.d.ts +35 -0
- package/dist/local/client/consoleLogger.js +44 -0
- package/dist/local/client/createInternalComponents.d.ts +50 -0
- package/dist/local/client/createInternalComponents.js +98 -0
- package/dist/local/client/createModelProxy.d.ts +248 -0
- package/dist/local/client/createModelProxy.js +880 -0
- package/dist/local/client/modelRegistration.d.ts +10 -0
- package/dist/local/client/modelRegistration.js +316 -0
- package/dist/local/client/options.d.ts +440 -0
- package/dist/local/client/options.js +7 -0
- package/dist/local/client/reactiveEngine.d.ts +53 -0
- package/dist/local/client/reactiveEngine.js +705 -0
- package/dist/local/client/resourceTypes.d.ts +12 -0
- package/dist/local/client/resourceTypes.js +10 -0
- package/dist/local/client/schemaConfig.d.ts +44 -0
- package/dist/local/client/schemaConfig.js +185 -0
- package/dist/local/client/storeCluster.d.ts +46 -0
- package/dist/local/client/storeCluster.js +133 -0
- package/dist/local/client/storeLifecycle.d.ts +61 -0
- package/dist/local/client/storeLifecycle.js +236 -0
- package/dist/local/client/validateAbloOptions.d.ts +42 -0
- package/dist/local/client/validateAbloOptions.js +43 -0
- package/dist/local/client/wsMutationExecutor.d.ts +27 -0
- package/dist/local/client/wsMutationExecutor.js +72 -0
- package/dist/local/context.d.ts +42 -0
- package/dist/local/context.js +81 -0
- package/dist/local/coordination/ClaimLog.d.ts +26 -0
- package/dist/local/coordination/ClaimLog.js +32 -0
- package/dist/local/interfaces/index.d.ts +311 -0
- package/dist/local/interfaces/index.js +9 -0
- package/dist/local/localModelContract.d.ts +14 -0
- package/dist/local/localModelContract.js +1 -0
- package/dist/local/logPosition.d.ts +31 -0
- package/dist/local/logPosition.js +53 -0
- package/dist/local/mutationPersistence.d.ts +6 -0
- package/dist/local/mutationPersistence.js +1 -0
- package/dist/local/mutators/RecordingMutation.d.ts +36 -0
- package/dist/local/mutators/RecordingMutation.js +182 -0
- package/dist/local/mutators/Transaction.d.ts +40 -0
- package/dist/local/mutators/Transaction.js +58 -0
- package/dist/local/mutators/UndoManager.d.ts +258 -0
- package/dist/local/mutators/UndoManager.js +665 -0
- package/dist/local/mutators/defineMutators.d.ts +60 -0
- package/dist/local/mutators/defineMutators.js +18 -0
- package/dist/local/mutators/inverseOp.d.ts +126 -0
- package/dist/local/mutators/inverseOp.js +71 -0
- package/dist/local/mutators/mutateActions.d.ts +45 -0
- package/dist/local/mutators/mutateActions.js +105 -0
- package/dist/local/mutators/readerActions.d.ts +33 -0
- package/dist/local/mutators/readerActions.js +57 -0
- package/dist/local/mutators/undoApply.d.ts +51 -0
- package/dist/local/mutators/undoApply.js +117 -0
- package/dist/local/persistence.d.ts +7 -0
- package/dist/local/persistence.js +9 -0
- package/dist/local/query/QueryProcessor.d.ts +75 -0
- package/dist/local/query/QueryProcessor.js +255 -0
- package/dist/local/query/client.d.ts +64 -0
- package/dist/local/query/client.js +138 -0
- package/dist/local/query/types.d.ts +85 -0
- package/dist/local/query/types.js +16 -0
- package/dist/local/schema/serialize.d.ts +1 -0
- package/dist/local/schema/serialize.js +1 -0
- package/dist/local/store/queryApi.d.ts +13 -0
- package/dist/local/store/queryApi.js +35 -0
- package/dist/local/storeContract.d.ts +145 -0
- package/dist/local/storeContract.js +12 -0
- package/dist/local/stores/DatabaseManager.d.ts +112 -0
- package/dist/local/stores/DatabaseManager.js +400 -0
- package/dist/local/stores/ObjectStore.d.ts +115 -0
- package/dist/local/stores/ObjectStore.js +393 -0
- package/dist/local/stores/ObjectStoreContract.d.ts +38 -0
- package/dist/local/stores/ObjectStoreContract.js +1 -0
- package/dist/local/stores/StoreManager.d.ts +114 -0
- package/dist/local/stores/StoreManager.js +304 -0
- package/dist/local/stores/SyncActionStore.d.ts +99 -0
- package/dist/local/stores/SyncActionStore.js +506 -0
- package/dist/local/stores/openIDBWithTimeout.d.ts +65 -0
- package/dist/local/stores/openIDBWithTimeout.js +153 -0
- package/dist/local/stores/persistenceCleanup.d.ts +7 -0
- package/dist/local/stores/persistenceCleanup.js +26 -0
- package/dist/local/stores/persistenceIdentity.d.ts +27 -0
- package/dist/local/stores/persistenceIdentity.js +38 -0
- package/dist/local/stores/syncAction.d.ts +26 -0
- package/dist/local/stores/syncAction.js +16 -0
- package/dist/local/stores/v1PersistenceDeletion.d.ts +8 -0
- package/dist/local/stores/v1PersistenceDeletion.js +16 -0
- package/dist/local/sync/BootstrapFetcher.d.ts +284 -0
- package/dist/local/sync/BootstrapFetcher.js +964 -0
- package/dist/local/sync/ConnectionManager.d.ts +8 -0
- package/dist/local/sync/ConnectionManager.js +8 -0
- package/dist/local/sync/OnDemandLoader.d.ts +231 -0
- package/dist/local/sync/OnDemandLoader.js +743 -0
- package/dist/local/sync/SubscriptionManager.d.ts +159 -0
- package/dist/local/sync/SubscriptionManager.js +243 -0
- package/dist/local/sync/SyncWebSocket.d.ts +173 -0
- package/dist/local/sync/SyncWebSocket.js +438 -0
- package/dist/local/sync/bootstrapApply.d.ts +73 -0
- package/dist/local/sync/bootstrapApply.js +73 -0
- package/dist/local/sync/commitFrames.d.ts +8 -0
- package/dist/local/sync/commitFrames.js +8 -0
- package/dist/local/sync/connectionManagerLifecycle.d.ts +23 -0
- package/dist/local/sync/connectionManagerLifecycle.js +126 -0
- package/dist/local/sync/contextPorts.d.ts +18 -0
- package/dist/local/sync/contextPorts.js +31 -0
- package/dist/local/sync/createClaimStream.d.ts +64 -0
- package/dist/local/sync/createClaimStream.js +475 -0
- package/dist/local/sync/createSnapshot.d.ts +29 -0
- package/dist/local/sync/createSnapshot.js +116 -0
- package/dist/local/sync/credentialLifecycle.d.ts +7 -0
- package/dist/local/sync/credentialLifecycle.js +7 -0
- package/dist/local/sync/deltaPipeline.d.ts +116 -0
- package/dist/local/sync/deltaPipeline.js +357 -0
- package/dist/local/sync/groupChange.d.ts +116 -0
- package/dist/local/sync/groupChange.js +244 -0
- package/dist/local/sync/initialize.d.ts +27 -0
- package/dist/local/sync/initialize.js +137 -0
- package/dist/local/sync/participants.d.ts +132 -0
- package/dist/local/sync/participants.js +342 -0
- package/dist/local/sync/persistedPrefix.d.ts +12 -0
- package/dist/local/sync/persistedPrefix.js +22 -0
- package/dist/local/sync/reconnect.d.ts +23 -0
- package/dist/local/sync/reconnect.js +55 -0
- package/dist/local/sync/schemaDrift.d.ts +55 -0
- package/dist/local/sync/schemaDrift.js +53 -0
- package/dist/local/sync/schemas.d.ts +71 -0
- package/dist/local/sync/schemas.js +94 -0
- package/dist/local/sync/socketEventWiring.d.ts +31 -0
- package/dist/local/sync/socketEventWiring.js +130 -0
- package/dist/local/sync/syncCursor.d.ts +40 -0
- package/dist/local/sync/syncCursor.js +55 -0
- package/dist/local/sync/syncPlan.d.ts +54 -0
- package/dist/local/sync/syncPlan.js +50 -0
- package/dist/local/sync/terminalSessionLifecycle.d.ts +20 -0
- package/dist/local/sync/terminalSessionLifecycle.js +50 -0
- package/dist/local/sync/wsFrameHandlers.d.ts +8 -0
- package/dist/local/sync/wsFrameHandlers.js +8 -0
- package/dist/local/transactions/databaseCommitOutbox.d.ts +15 -0
- package/dist/local/transactions/databaseCommitOutbox.js +16 -0
- package/dist/local/transactions/localMutation.d.ts +10 -0
- package/dist/local/transactions/localMutation.js +37 -0
- package/dist/local/transactions/mutations/MutationQueue.d.ts +511 -0
- package/dist/local/transactions/mutations/MutationQueue.js +1498 -0
- package/dist/local/transactions/mutations/MutationStore.d.ts +20 -0
- package/dist/local/transactions/mutations/MutationStore.js +53 -0
- package/dist/local/transactions/mutations/UnconfirmedWrites.d.ts +82 -0
- package/dist/local/transactions/mutations/UnconfirmedWrites.js +104 -0
- package/dist/local/transactions/mutations/batchProcessing.d.ts +64 -0
- package/dist/local/transactions/mutations/batchProcessing.js +349 -0
- package/dist/local/transactions/mutations/coalesceRules.d.ts +58 -0
- package/dist/local/transactions/mutations/coalesceRules.js +140 -0
- package/dist/local/transactions/mutations/commitApi.d.ts +19 -0
- package/dist/local/transactions/mutations/commitApi.js +74 -0
- package/dist/local/transactions/mutations/commitLane.d.ts +70 -0
- package/dist/local/transactions/mutations/commitLane.js +140 -0
- package/dist/local/transactions/mutations/commitLatency.d.ts +52 -0
- package/dist/local/transactions/mutations/commitLatency.js +130 -0
- package/dist/local/transactions/mutations/commitPayload.d.ts +165 -0
- package/dist/local/transactions/mutations/commitPayload.js +152 -0
- package/dist/local/transactions/mutations/commitTransport.d.ts +36 -0
- package/dist/local/transactions/mutations/commitTransport.js +104 -0
- package/dist/local/transactions/mutations/deltaConfirmation.d.ts +63 -0
- package/dist/local/transactions/mutations/deltaConfirmation.js +235 -0
- package/dist/local/transactions/mutations/durableCommitRestore.d.ts +17 -0
- package/dist/local/transactions/mutations/durableCommitRestore.js +95 -0
- package/dist/local/transactions/mutations/durableWriteStore.d.ts +14 -0
- package/dist/local/transactions/mutations/durableWriteStore.js +12 -0
- package/dist/local/transactions/mutations/executionSelection.d.ts +6 -0
- package/dist/local/transactions/mutations/executionSelection.js +42 -0
- package/dist/local/transactions/mutations/failureHandling.d.ts +16 -0
- package/dist/local/transactions/mutations/failureHandling.js +130 -0
- package/dist/local/transactions/mutations/failurePolicy.d.ts +13 -0
- package/dist/local/transactions/mutations/failurePolicy.js +48 -0
- package/dist/local/transactions/mutations/localMutation.d.ts +58 -0
- package/dist/local/transactions/mutations/localMutation.js +75 -0
- package/dist/local/transactions/mutations/modelOperations.d.ts +44 -0
- package/dist/local/transactions/mutations/modelOperations.js +144 -0
- package/dist/local/transactions/mutations/mutationPersistence.d.ts +25 -0
- package/dist/local/transactions/mutations/mutationPersistence.js +188 -0
- package/dist/local/transactions/mutations/pendingDrain.d.ts +33 -0
- package/dist/local/transactions/mutations/pendingDrain.js +112 -0
- package/dist/local/transactions/mutations/processingScheduler.d.ts +14 -0
- package/dist/local/transactions/mutations/processingScheduler.js +24 -0
- package/dist/local/transactions/mutations/queueCoalescing.d.ts +13 -0
- package/dist/local/transactions/mutations/queueCoalescing.js +35 -0
- package/dist/local/transactions/mutations/replayValidation.d.ts +187 -0
- package/dist/local/transactions/mutations/replayValidation.js +164 -0
- package/dist/local/transactions/reconnectDrain.d.ts +11 -0
- package/dist/local/transactions/reconnectDrain.js +13 -0
- package/dist/local/utils/mobxSetup.d.ts +53 -0
- package/dist/local/utils/mobxSetup.js +330 -0
- package/dist/local/views/QueryView.d.ts +79 -0
- package/dist/local/views/QueryView.js +218 -0
- package/dist/local/views/ViewRegistry.d.ts +20 -0
- package/dist/local/views/ViewRegistry.js +57 -0
- package/dist/local/views/incrementalView.d.ts +45 -0
- package/dist/local/views/incrementalView.js +69 -0
- package/dist/plugin.d.ts +285 -0
- package/dist/plugin.js +106 -0
- package/dist/presenceStream.d.ts +69 -0
- package/dist/presenceStream.js +200 -0
- package/dist/react/AbloProvider.d.ts +242 -0
- package/dist/react/AbloProvider.js +456 -0
- package/dist/react/ClientSideSuspense.d.ts +36 -0
- package/dist/react/ClientSideSuspense.js +17 -0
- package/dist/react/DefaultFallback.d.ts +24 -0
- package/dist/react/DefaultFallback.js +43 -0
- package/dist/react/context.d.ts +55 -0
- package/dist/react/context.js +29 -0
- package/dist/react/createAbloReact.d.ts +50 -0
- package/dist/react/createAbloReact.js +48 -0
- package/dist/react/internalContext.d.ts +33 -0
- package/dist/react/internalContext.js +3 -0
- package/dist/react/useAblo.d.ts +96 -0
- package/dist/react/useAblo.js +120 -0
- package/dist/react/useCurrentUserId.d.ts +2 -0
- package/dist/react/useCurrentUserId.js +12 -0
- package/dist/react/useErrorListener.d.ts +2 -0
- package/dist/react/useErrorListener.js +14 -0
- package/dist/react/useMutationFailureListener.d.ts +8 -0
- package/dist/react/useMutationFailureListener.js +19 -0
- package/dist/react/useMutators.d.ts +56 -0
- package/dist/react/useMutators.js +84 -0
- package/dist/react/useSyncStatus.d.ts +19 -0
- package/dist/react/useSyncStatus.js +37 -0
- package/dist/react/useUndoScope.d.ts +34 -0
- package/dist/react/useUndoScope.js +73 -0
- package/dist/react.d.ts +18 -0
- package/dist/react.js +14 -0
- package/dist/reactRuntime.d.ts +4 -0
- package/dist/reactRuntime.js +2 -0
- package/dist/surface.d.ts +36 -0
- package/dist/surface.js +77 -0
- package/dist/useReactive.d.ts +6 -0
- package/dist/useReactive.js +43 -0
- package/package.json +119 -0
- package/src/Ablo.ts +456 -0
- package/src/client.ts +374 -0
- package/src/core.ts +104 -0
- package/src/humans.ts +61 -0
- package/src/index.ts +40 -0
- package/src/local/BaseSyncedStore.ts +1991 -0
- package/src/local/Database.ts +2052 -0
- package/src/local/InstanceCache.ts +1503 -0
- package/src/local/LazyReferenceCollection.ts +563 -0
- package/src/local/Model.ts +1124 -0
- package/src/local/ModelRegistry.ts +762 -0
- package/src/local/NetworkMonitor.ts +88 -0
- package/src/local/RuntimeContext.ts +141 -0
- package/src/local/SyncClient.ts +2131 -0
- package/src/local/adapters/alwaysOnline.ts +20 -0
- package/src/local/adapters/inMemoryStorage.ts +141 -0
- package/src/local/client/clientPrelude.ts +112 -0
- package/src/local/client/consoleLogger.ts +60 -0
- package/src/local/client/createInternalComponents.ts +163 -0
- package/src/local/client/createModelProxy.ts +1390 -0
- package/src/local/client/modelRegistration.ts +350 -0
- package/src/local/client/options.ts +526 -0
- package/src/local/client/reactiveEngine.ts +935 -0
- package/src/local/client/resourceTypes.ts +37 -0
- package/src/local/client/schemaConfig.ts +194 -0
- package/src/local/client/storeCluster.ts +172 -0
- package/src/local/client/storeLifecycle.ts +343 -0
- package/src/local/client/validateAbloOptions.ts +95 -0
- package/src/local/client/wsMutationExecutor.ts +110 -0
- package/src/local/context.ts +96 -0
- package/src/local/coordination/ClaimLog.ts +39 -0
- package/src/local/interfaces/index.ts +468 -0
- package/src/local/localModelContract.ts +15 -0
- package/src/local/logPosition.ts +84 -0
- package/src/local/mutationPersistence.ts +7 -0
- package/src/local/mutators/RecordingMutation.ts +222 -0
- package/src/local/mutators/Transaction.ts +97 -0
- package/src/local/mutators/UndoManager.ts +741 -0
- package/src/local/mutators/defineMutators.ts +76 -0
- package/src/local/mutators/inverseOp.ts +83 -0
- package/src/local/mutators/mutateActions.ts +167 -0
- package/src/local/mutators/readerActions.ts +99 -0
- package/src/local/mutators/undoApply.ts +141 -0
- package/src/local/persistence.ts +16 -0
- package/src/local/query/QueryProcessor.ts +347 -0
- package/src/local/query/client.ts +197 -0
- package/src/local/query/types.ts +102 -0
- package/src/local/schema/serialize.ts +1 -0
- package/src/local/store/queryApi.ts +56 -0
- package/src/local/storeContract.ts +146 -0
- package/src/local/stores/DatabaseManager.ts +507 -0
- package/src/local/stores/ObjectStore.ts +449 -0
- package/src/local/stores/ObjectStoreContract.ts +48 -0
- package/src/local/stores/StoreManager.ts +388 -0
- package/src/local/stores/SyncActionStore.ts +579 -0
- package/src/local/stores/openIDBWithTimeout.ts +195 -0
- package/src/local/stores/persistenceCleanup.ts +43 -0
- package/src/local/stores/persistenceIdentity.ts +83 -0
- package/src/local/stores/syncAction.ts +21 -0
- package/src/local/stores/v1PersistenceDeletion.ts +21 -0
- package/src/local/sync/BootstrapFetcher.ts +1224 -0
- package/src/local/sync/ConnectionManager.ts +15 -0
- package/src/local/sync/OnDemandLoader.ts +927 -0
- package/src/local/sync/SubscriptionManager.ts +300 -0
- package/src/local/sync/SyncWebSocket.ts +584 -0
- package/src/local/sync/bootstrapApply.ts +130 -0
- package/src/local/sync/commitFrames.ts +16 -0
- package/src/local/sync/connectionManagerLifecycle.ts +158 -0
- package/src/local/sync/contextPorts.ts +37 -0
- package/src/local/sync/createClaimStream.ts +668 -0
- package/src/local/sync/createSnapshot.ts +160 -0
- package/src/local/sync/credentialLifecycle.ts +18 -0
- package/src/local/sync/deltaPipeline.ts +473 -0
- package/src/local/sync/groupChange.ts +343 -0
- package/src/local/sync/initialize.ts +205 -0
- package/src/local/sync/participants.ts +564 -0
- package/src/local/sync/persistedPrefix.ts +27 -0
- package/src/local/sync/reconnect.ts +88 -0
- package/src/local/sync/schemaDrift.ts +86 -0
- package/src/local/sync/schemas.ts +118 -0
- package/src/local/sync/socketEventWiring.ts +196 -0
- package/src/local/sync/syncCursor.ts +62 -0
- package/src/local/sync/syncPlan.ts +89 -0
- package/src/local/sync/terminalSessionLifecycle.ts +66 -0
- package/src/local/sync/wsFrameHandlers.ts +20 -0
- package/src/local/transactions/databaseCommitOutbox.ts +33 -0
- package/src/local/transactions/localMutation.ts +58 -0
- package/src/local/transactions/mutations/MutationQueue.ts +1998 -0
- package/src/local/transactions/mutations/MutationStore.ts +65 -0
- package/src/local/transactions/mutations/UnconfirmedWrites.ts +133 -0
- package/src/local/transactions/mutations/batchProcessing.ts +469 -0
- package/src/local/transactions/mutations/coalesceRules.ts +192 -0
- package/src/local/transactions/mutations/commitApi.ts +97 -0
- package/src/local/transactions/mutations/commitLane.ts +191 -0
- package/src/local/transactions/mutations/commitLatency.ts +164 -0
- package/src/local/transactions/mutations/commitPayload.ts +281 -0
- package/src/local/transactions/mutations/commitTransport.ts +174 -0
- package/src/local/transactions/mutations/deltaConfirmation.ts +298 -0
- package/src/local/transactions/mutations/durableCommitRestore.ts +128 -0
- package/src/local/transactions/mutations/durableWriteStore.ts +21 -0
- package/src/local/transactions/mutations/executionSelection.ts +42 -0
- package/src/local/transactions/mutations/failureHandling.ts +154 -0
- package/src/local/transactions/mutations/failurePolicy.ts +61 -0
- package/src/local/transactions/mutations/localMutation.ts +135 -0
- package/src/local/transactions/mutations/modelOperations.ts +210 -0
- package/src/local/transactions/mutations/mutationPersistence.ts +231 -0
- package/src/local/transactions/mutations/pendingDrain.ts +160 -0
- package/src/local/transactions/mutations/processingScheduler.ts +35 -0
- package/src/local/transactions/mutations/queueCoalescing.ts +45 -0
- package/src/local/transactions/mutations/replayValidation.ts +192 -0
- package/src/local/transactions/reconnectDrain.ts +24 -0
- package/src/local/utils/mobxSetup.ts +388 -0
- package/src/local/views/QueryView.ts +311 -0
- package/src/local/views/ViewRegistry.ts +61 -0
- package/src/local/views/incrementalView.ts +92 -0
- package/src/plugin.ts +396 -0
- package/src/presenceStream.ts +279 -0
- package/src/react/AbloProvider.tsx +744 -0
- package/src/react/ClientSideSuspense.tsx +57 -0
- package/src/react/DefaultFallback.tsx +60 -0
- package/src/react/context.ts +89 -0
- package/src/react/createAbloReact.ts +116 -0
- package/src/react/internalContext.ts +38 -0
- package/src/react/useAblo.ts +280 -0
- package/src/react/useCurrentUserId.ts +17 -0
- package/src/react/useErrorListener.ts +22 -0
- package/src/react/useMutationFailureListener.ts +34 -0
- package/src/react/useMutators.ts +184 -0
- package/src/react/useSyncStatus.ts +42 -0
- package/src/react/useUndoScope.ts +143 -0
- package/src/react.ts +69 -0
- package/src/reactRuntime.ts +10 -0
- package/src/surface.ts +106 -0
- package/src/useReactive.ts +51 -0
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reactive engine assembly (ADR 0016). `Ablo({ ... })` resolves auth and
|
|
3
|
+
* capabilities; `humans().init` constructs the store cluster; the lifecycle
|
|
4
|
+
* — first mint, identity, ready() — lives in `./storeLifecycle.ts`. What
|
|
5
|
+
* remains here is assembly around those parts: the claim stream and
|
|
6
|
+
* participant manager, options validation, the typed model proxies, and the
|
|
7
|
+
* commit/claim/session resources — composed into the reactive client.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from the factory so the composition root stays a root: resolve,
|
|
10
|
+
* dispatch, return. The remaining assembly converts to decoration of a
|
|
11
|
+
* host-built core client with the per-model surface split — the cut's own
|
|
12
|
+
* design step (docs/plans/package-split.md).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Schema, SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
16
|
+
import {
|
|
17
|
+
durableCommitOperationSchema,
|
|
18
|
+
type DurableCommitOperation,
|
|
19
|
+
} from '@abloatai/transaction/transactions/settlement/commitEnvelope';
|
|
20
|
+
import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, claimedError } from '@abloatai/transaction/errors';
|
|
21
|
+
import type { ModelTarget, ModelClaim } from '@abloatai/transaction/coordination/schema';
|
|
22
|
+
import type { BatchFence } from '@abloatai/transaction/coordination';
|
|
23
|
+
import {
|
|
24
|
+
batchFence,
|
|
25
|
+
fenceTokenFor,
|
|
26
|
+
modelTarget,
|
|
27
|
+
streamTarget,
|
|
28
|
+
subTarget,
|
|
29
|
+
} from '@abloatai/transaction/coordination';
|
|
30
|
+
import { validateAbloOptions } from './validateAbloOptions.js';
|
|
31
|
+
import { mintSession } from '@abloatai/transaction/auth/sessionMint';
|
|
32
|
+
import type { MintSessionContext } from '@abloatai/transaction/auth/sessionMint';
|
|
33
|
+
import {
|
|
34
|
+
revokeCapability,
|
|
35
|
+
rotateCapability,
|
|
36
|
+
} from '@abloatai/transaction/auth/capabilityLifecycle';
|
|
37
|
+
import { modelWireNames } from '@abloatai/transaction/auth/capability';
|
|
38
|
+
import type { StoreCluster } from './storeCluster.js';
|
|
39
|
+
import { startStoreLifecycle } from './storeLifecycle.js';
|
|
40
|
+
import type { SyncWebSocket, CoreSyncEventMap } from '../sync/SyncWebSocket.js';
|
|
41
|
+
import { createClaimStream } from '../sync/createClaimStream.js';
|
|
42
|
+
import { awaitClaimGrant } from '@abloatai/transaction/coordination/awaitClaimGrant';
|
|
43
|
+
import { createSnapshot } from '../sync/createSnapshot.js';
|
|
44
|
+
import { createParticipantManager } from '../sync/participants.js';
|
|
45
|
+
import type { AttachablePresenceStream } from '../../presenceStream.js';
|
|
46
|
+
import type { ClaimWaitOptions, Snapshot } from '@abloatai/transaction/types/streams';
|
|
47
|
+
import type { Claim } from '@abloatai/transaction/types/streams';
|
|
48
|
+
import type { CredentialProvider } from '@abloatai/transaction/auth/apiKey';
|
|
49
|
+
import { resolveApiKeyValue, resolveBootstrapBaseUrl } from '@abloatai/transaction/auth/apiKey';
|
|
50
|
+
import type { AbloOptions } from './options.js';
|
|
51
|
+
import type { ClientPrelude } from './clientPrelude.js';
|
|
52
|
+
import type {
|
|
53
|
+
AbloSession,
|
|
54
|
+
ClaimCreateOptions,
|
|
55
|
+
ClaimResource,
|
|
56
|
+
CommitCreateOptions,
|
|
57
|
+
CommitOperationInput,
|
|
58
|
+
CommitReceipt,
|
|
59
|
+
CommitResource,
|
|
60
|
+
CreateAgentClientParams,
|
|
61
|
+
CreateAgentSessionParams,
|
|
62
|
+
CreateSessionParams,
|
|
63
|
+
} from './resourceTypes.js';
|
|
64
|
+
import { createModelProxy, type ModelOperations } from './createModelProxy.js';
|
|
65
|
+
import { assertWriteOptions } from '@abloatai/transaction/resources/writeOptionsSchema';
|
|
66
|
+
import type { AbloClient as Ablo } from '../../client.js';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What the reactive build is fed: the factory's pass over the options bag
|
|
70
|
+
* ({@link ClientPrelude} — auth, url, logging, identity, shared with the other
|
|
71
|
+
* client shapes), plus the four things that must exist before the store does.
|
|
72
|
+
*
|
|
73
|
+
* The prelude half is extended, never restated. A second copy of those fields
|
|
74
|
+
* would drift the moment one side gained a resolver the other did not.
|
|
75
|
+
*/
|
|
76
|
+
export interface ReactiveEngineInputs<S extends SchemaRecord> extends ClientPrelude<S> {
|
|
77
|
+
options: AbloOptions<S>;
|
|
78
|
+
/**
|
|
79
|
+
* The connection, constructed by the factory before the plugin list
|
|
80
|
+
* resolved — the same instance `PluginContext.transport` carries. The
|
|
81
|
+
* store takes it as a dependency and owns the lifecycle.
|
|
82
|
+
*/
|
|
83
|
+
transport: SyncWebSocket;
|
|
84
|
+
/** The humans() plugin's contribution — built by its `init`, already
|
|
85
|
+
* attached to the connection the context carried. */
|
|
86
|
+
presence: AttachablePresenceStream;
|
|
87
|
+
/**
|
|
88
|
+
* The store cluster `humans().init` constructed from the widened context:
|
|
89
|
+
* this client's runtime, the component graph, and the store. The engine
|
|
90
|
+
* assembles around it and constructs none of it.
|
|
91
|
+
*/
|
|
92
|
+
cluster: StoreCluster;
|
|
93
|
+
/**
|
|
94
|
+
* Constructs a sibling client (`ablo.agents.create(...)` mints a scoped key
|
|
95
|
+
* and builds a second engine with it). Injected by the factory — a direct
|
|
96
|
+
* import back into it would close a runtime cycle.
|
|
97
|
+
*/
|
|
98
|
+
createSibling: (options: AbloOptions<S>) => Ablo<S>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
102
|
+
inputs: ReactiveEngineInputs<S>,
|
|
103
|
+
): Ablo<S> {
|
|
104
|
+
const {
|
|
105
|
+
options,
|
|
106
|
+
internalOptions,
|
|
107
|
+
url,
|
|
108
|
+
logger,
|
|
109
|
+
configuredApiKey,
|
|
110
|
+
configuredAuthToken,
|
|
111
|
+
credentialResolver,
|
|
112
|
+
authCredentials,
|
|
113
|
+
transport,
|
|
114
|
+
participantId,
|
|
115
|
+
kind,
|
|
116
|
+
presence,
|
|
117
|
+
cluster,
|
|
118
|
+
createSibling,
|
|
119
|
+
} = inputs;
|
|
120
|
+
const schema = options.schema;
|
|
121
|
+
|
|
122
|
+
// The store cluster — this client's runtime, the component graph, the
|
|
123
|
+
// registered models, and the store — was constructed by `humans().init`
|
|
124
|
+
// from the widened plugin context (see `./storeCluster.ts`). The engine
|
|
125
|
+
// assembles around it.
|
|
126
|
+
const { components, store } = cluster;
|
|
127
|
+
const {
|
|
128
|
+
modelRegistry,
|
|
129
|
+
objectPool,
|
|
130
|
+
syncClient,
|
|
131
|
+
hydration,
|
|
132
|
+
} = components;
|
|
133
|
+
|
|
134
|
+
// Self identity, late-bound the same way the connection's values are: the
|
|
135
|
+
// construction-time guess seeds it (correct on the self-hosted path, empty
|
|
136
|
+
// on the hosted path), and `ready()` overwrites it with what identity
|
|
137
|
+
// resolution derives from the credential's scope. The model proxies read
|
|
138
|
+
// it through getters, so the is-this-claim-mine checks always compare
|
|
139
|
+
// against the resolved identity.
|
|
140
|
+
let selfParticipantId = participantId;
|
|
141
|
+
let selfParticipantKind = kind;
|
|
142
|
+
|
|
143
|
+
// Presence + claim streams — the same reference for the engine's lifetime,
|
|
144
|
+
// attached to the connection at construction (it exists — the host built
|
|
145
|
+
// it; sends before the socket opens are dropped by the transport's
|
|
146
|
+
// send-during-reconnect contract, and each stream re-announces on
|
|
147
|
+
// `connected`). The presence stream is the humans() plugin's contribution,
|
|
148
|
+
// built and attached by its `init` (ADR 0016); the claim stream is core
|
|
149
|
+
// coordination, so the root constructs it regardless of the list. Both
|
|
150
|
+
// filter own echoes by participant id, seeded in `ready()` alongside the
|
|
151
|
+
// locals above.
|
|
152
|
+
const presenceStream = presence;
|
|
153
|
+
const claimStream = createClaimStream({ participantId, logger }, transport);
|
|
154
|
+
|
|
155
|
+
// 6. Validate options up front — fail loudly on obviously wrong inputs so
|
|
156
|
+
// strangers don't get silent empty results. Validation errors are written
|
|
157
|
+
// into `store.syncStatus` (the single source of truth).
|
|
158
|
+
const _validationError = validateAbloOptions({
|
|
159
|
+
options: internalOptions,
|
|
160
|
+
url,
|
|
161
|
+
configuredApiKey,
|
|
162
|
+
configuredAuthToken,
|
|
163
|
+
});
|
|
164
|
+
if (_validationError) {
|
|
165
|
+
logger.error(_validationError.message);
|
|
166
|
+
store.syncStatus.state = 'error';
|
|
167
|
+
store.syncStatus.error = _validationError;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Deprecated identity overrides are a silent no-op under hosted cloud: when an
|
|
171
|
+
// `apiKey` is configured the SERVER derives participant kind + id from the
|
|
172
|
+
// key's scope, so `kind` / `agentId` passed here are ignored. Setting them and
|
|
173
|
+
// trusting them is the trap (you think you're an agent; the key says user).
|
|
174
|
+
// Warn loudly rather than removing the fields — `agentId` is still load-bearing
|
|
175
|
+
// on the self-hosted path (no apiKey; paired with `capabilityToken`).
|
|
176
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- reads the deprecated fields precisely to warn callers off them under a configured apiKey
|
|
177
|
+
if (configuredApiKey && (internalOptions.kind || internalOptions.agentId)) {
|
|
178
|
+
logger.warn(
|
|
179
|
+
'Ablo: `kind` / `agentId` are ignored when an `apiKey` is configured — ' +
|
|
180
|
+
'the server derives participant identity from the key’s scope. Remove ' +
|
|
181
|
+
'them (or mint a scoped session via `ablo.sessions.create({ agent })` ' +
|
|
182
|
+
'for a distinct agent identity). They apply only to the self-hosted ' +
|
|
183
|
+
'`capabilityToken` path.',
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 7. The lifecycle — first mint, identity resolution, credential refresh,
|
|
188
|
+
// and the idempotent ready() driving store.initialize() — is the
|
|
189
|
+
// materialiser's own (`./storeLifecycle.ts`); the engine wires it with
|
|
190
|
+
// the prelude's credential slice and seeds its own state through the
|
|
191
|
+
// callback: the self locals (read by the model proxies' getters) and
|
|
192
|
+
// the streams' own-echo filters, before the store ever connects.
|
|
193
|
+
/** Resolved account scope — seeded once identity resolution completes;
|
|
194
|
+
* exposed as the readonly `ablo.organizationId` accessor. */
|
|
195
|
+
let _resolvedOrganizationId: string | null = null;
|
|
196
|
+
const lifecycle = startStoreLifecycle({
|
|
197
|
+
cluster,
|
|
198
|
+
schema,
|
|
199
|
+
internalOptions,
|
|
200
|
+
authCredentials,
|
|
201
|
+
credentialResolver,
|
|
202
|
+
configuredApiKey,
|
|
203
|
+
configuredAuthToken,
|
|
204
|
+
url,
|
|
205
|
+
kind,
|
|
206
|
+
logger,
|
|
207
|
+
validationError: _validationError,
|
|
208
|
+
onIdentityResolved: ({ userId, participantKind, accountScope, syncGroups }) => {
|
|
209
|
+
selfParticipantId = userId;
|
|
210
|
+
selfParticipantKind = participantKind;
|
|
211
|
+
_resolvedOrganizationId = accountScope;
|
|
212
|
+
presenceStream.setParticipant({
|
|
213
|
+
id: userId,
|
|
214
|
+
kind: participantKind,
|
|
215
|
+
syncGroups: [...syncGroups],
|
|
216
|
+
});
|
|
217
|
+
claimStream.setParticipant({ id: userId });
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
const ready = lifecycle.ready;
|
|
221
|
+
|
|
222
|
+
const participantManager = createParticipantManager({
|
|
223
|
+
ready,
|
|
224
|
+
transport,
|
|
225
|
+
presence: presenceStream,
|
|
226
|
+
claims: claimStream,
|
|
227
|
+
schema,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// 9b. waitForFlush — drains pending mutations using the store's
|
|
231
|
+
// pendingChanges counter (already maintained by BaseSyncedStore based
|
|
232
|
+
// on MutationQueue events). Polls every 50ms; uses the existing
|
|
233
|
+
// observable rather than introducing a new event channel.
|
|
234
|
+
async function waitForFlush(timeoutMs?: number): Promise<void> {
|
|
235
|
+
const start = Date.now();
|
|
236
|
+
while (store.syncStatus.pendingChanges > 0) {
|
|
237
|
+
if (timeoutMs !== undefined && Date.now() - start > timeoutMs) {
|
|
238
|
+
throw new AbloConnectionError(
|
|
239
|
+
`Flush timeout: ${store.syncStatus.pendingChanges} pending mutations after ${timeoutMs}ms`,
|
|
240
|
+
{ code: 'flush_timeout' },
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function createClientTxId(idempotencyKey?: string | null): string {
|
|
248
|
+
if (idempotencyKey && idempotencyKey.length > 0) return idempotencyKey;
|
|
249
|
+
return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
250
|
+
? crypto.randomUUID()
|
|
251
|
+
: `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function normalizeCommitOperation(
|
|
255
|
+
op: CommitOperationInput,
|
|
256
|
+
defaults: Pick<CommitCreateOptions, 'readAt' | 'onStale'>,
|
|
257
|
+
fence: BatchFence | null,
|
|
258
|
+
): DurableCommitOperation {
|
|
259
|
+
const type = op.action.toUpperCase();
|
|
260
|
+
const id = op.id ?? '';
|
|
261
|
+
return durableCommitOperationSchema.parse({
|
|
262
|
+
type,
|
|
263
|
+
model: op.model.toLowerCase(),
|
|
264
|
+
id,
|
|
265
|
+
input: op.data ?? undefined,
|
|
266
|
+
transactionId: op.transactionId ?? undefined,
|
|
267
|
+
readAt: op.readAt ?? defaults.readAt ?? undefined,
|
|
268
|
+
onStale: op.onStale ?? defaults.onStale ?? undefined,
|
|
269
|
+
fenceToken:
|
|
270
|
+
op.fenceToken ?? fenceTokenFor(fence, op.model, op.id ?? null) ?? undefined,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function normalizeCommitOperations(
|
|
275
|
+
commitOptions: CommitCreateOptions,
|
|
276
|
+
fence: BatchFence | null,
|
|
277
|
+
): DurableCommitOperation[] {
|
|
278
|
+
if (commitOptions.operations.length === 0) {
|
|
279
|
+
throw new AbloValidationError(
|
|
280
|
+
'Commit requires a non-empty `operations` array.',
|
|
281
|
+
{ code: 'commit_operation_required' },
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return commitOptions.operations.map((op) =>
|
|
285
|
+
normalizeCommitOperation(op, commitOptions, fence),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
function modelClaimFromActive(claim: Claim): ModelClaim {
|
|
291
|
+
const target = {
|
|
292
|
+
...modelTarget(claim.target),
|
|
293
|
+
...subTarget(claim.target),
|
|
294
|
+
};
|
|
295
|
+
return {
|
|
296
|
+
id: claim.id,
|
|
297
|
+
actor: claim.heldBy ?? "",
|
|
298
|
+
participantKind: claim.participantKind ?? "user",
|
|
299
|
+
description: claim.description,
|
|
300
|
+
field: claim.target.field,
|
|
301
|
+
status: 'active',
|
|
302
|
+
expiresAt: claim.expiresAt ?? 0,
|
|
303
|
+
target,
|
|
304
|
+
// The claim's metadata read as the open record it is on the wire, so a
|
|
305
|
+
// key the coordinator wrote — a heartbeat's `progress` — is readable.
|
|
306
|
+
// `target.meta` is the same bag under the shape the program declared,
|
|
307
|
+
// and a declared shape has no member for something the holder did not
|
|
308
|
+
// write.
|
|
309
|
+
...(target.meta !== undefined ? { meta: target.meta } : {}),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function targetMatchesModel(
|
|
314
|
+
target: { readonly model?: string; readonly id?: string; readonly field?: string },
|
|
315
|
+
claim: Claim,
|
|
316
|
+
): boolean {
|
|
317
|
+
if (
|
|
318
|
+
target.model &&
|
|
319
|
+
claim.target.type.toLowerCase() !== target.model.toLowerCase()
|
|
320
|
+
) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
if (target.id && claim.target.id !== target.id) return false;
|
|
324
|
+
if (target.field && claim.target.field !== target.field) return false;
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function listModelClaims(target?: Partial<ModelTarget>): readonly ModelClaim[] {
|
|
329
|
+
return claimStream.others
|
|
330
|
+
.filter((claim) => (target ? targetMatchesModel(target, claim) : true))
|
|
331
|
+
.map(modelClaimFromActive);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function waitForModelUnclaimed(
|
|
335
|
+
target: Partial<ModelTarget>,
|
|
336
|
+
options?: ClaimWaitOptions,
|
|
337
|
+
): Promise<void> {
|
|
338
|
+
if (listModelClaims(target).length === 0) return Promise.resolve();
|
|
339
|
+
|
|
340
|
+
return new Promise((resolve, reject) => {
|
|
341
|
+
let settled = false;
|
|
342
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
343
|
+
|
|
344
|
+
const cleanup = () => {
|
|
345
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
346
|
+
unsubscribe();
|
|
347
|
+
options?.signal?.removeEventListener('abort', onAbort);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const finish = (fn: () => void) => {
|
|
351
|
+
if (settled) return;
|
|
352
|
+
settled = true;
|
|
353
|
+
cleanup();
|
|
354
|
+
fn();
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const check = () => {
|
|
358
|
+
if (listModelClaims(target).length === 0) {
|
|
359
|
+
finish(resolve);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const abortError = () =>
|
|
364
|
+
new AbloConnectionError('Claim wait aborted.', {
|
|
365
|
+
code: 'claim_wait_aborted',
|
|
366
|
+
cause: options?.signal?.reason,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const onAbort = () => {
|
|
370
|
+
finish(() => { reject(abortError()); });
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Answered before the subscription, the listener, and the timer exist,
|
|
374
|
+
// so there is nothing for `cleanup` to undo — and nothing that would
|
|
375
|
+
// read `unsubscribe` ahead of the line that binds it.
|
|
376
|
+
if (options?.signal?.aborted) {
|
|
377
|
+
reject(abortError());
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const unsubscribe = claimStream.onChange(check);
|
|
382
|
+
options?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
383
|
+
|
|
384
|
+
if (options?.timeout != null) {
|
|
385
|
+
timeoutId = setTimeout(() => {
|
|
386
|
+
finish(() =>
|
|
387
|
+
{ reject(
|
|
388
|
+
claimedError(
|
|
389
|
+
target,
|
|
390
|
+
listModelClaims(target),
|
|
391
|
+
'model_claimed_timeout',
|
|
392
|
+
),
|
|
393
|
+
); },
|
|
394
|
+
);
|
|
395
|
+
}, options.timeout);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function wrapClaimHandle(
|
|
401
|
+
claim: Claim,
|
|
402
|
+
waited = false,
|
|
403
|
+
fenceToken?: number,
|
|
404
|
+
): Claim {
|
|
405
|
+
const release = (): Promise<void> => {
|
|
406
|
+
claim.revoke?.();
|
|
407
|
+
return Promise.resolve();
|
|
408
|
+
};
|
|
409
|
+
// The token is server-stamped and arrives on the grant frame, so prefer
|
|
410
|
+
// the one `awaitClaimGrant` read there; fall back to any the local handle
|
|
411
|
+
// already carried (immediate, non-queued grants).
|
|
412
|
+
const resolvedFenceToken = fenceToken ?? claim.fenceToken;
|
|
413
|
+
return {
|
|
414
|
+
object: 'claim',
|
|
415
|
+
id: claim.id,
|
|
416
|
+
description: claim.description,
|
|
417
|
+
target: claim.target,
|
|
418
|
+
waited,
|
|
419
|
+
...(resolvedFenceToken !== undefined ? { fenceToken: resolvedFenceToken } : {}),
|
|
420
|
+
release,
|
|
421
|
+
revoke: claim.revoke,
|
|
422
|
+
// The lease-control members are forwarded explicitly — this wrapper
|
|
423
|
+
// rebuilds the handle field by field, so anything not named here is
|
|
424
|
+
// silently dropped from the public claim.
|
|
425
|
+
heartbeat: claim.heartbeat,
|
|
426
|
+
[Symbol.asyncDispose]: release,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const publicClaims: ClaimResource = Object.assign(claimStream, {
|
|
431
|
+
async create(claimOptions: ClaimCreateOptions): Promise<Claim> {
|
|
432
|
+
await ready();
|
|
433
|
+
const claim = claimStream.claim(
|
|
434
|
+
{
|
|
435
|
+
...streamTarget(claimOptions.target),
|
|
436
|
+
...subTarget(claimOptions.target),
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
description: claimOptions.description,
|
|
440
|
+
ttl: claimOptions.ttl,
|
|
441
|
+
queue: claimOptions.queue,
|
|
442
|
+
},
|
|
443
|
+
);
|
|
444
|
+
// With `queue`, the claim is only really *ours* once the server says
|
|
445
|
+
// so (`claim_acquired` if the target was free, `claim_granted` once
|
|
446
|
+
// we reach the head of the FIFO line). Block here on that grant so
|
|
447
|
+
// callers — chiefly `ablo.<model>.claim` — get a handle that already
|
|
448
|
+
// holds the lease, never a half-claimed one racing the queue.
|
|
449
|
+
let waited = false;
|
|
450
|
+
let fenceToken: number | undefined;
|
|
451
|
+
if (claimOptions.queue) {
|
|
452
|
+
try {
|
|
453
|
+
({ waited, fenceToken } = await awaitClaimGrant(transport, claim.id, {
|
|
454
|
+
timeoutMs: claimOptions.waitTimeoutMs,
|
|
455
|
+
maxQueueDepth: claimOptions.maxQueueDepth,
|
|
456
|
+
signal: claimOptions.signal,
|
|
457
|
+
logger,
|
|
458
|
+
}));
|
|
459
|
+
} catch (err) {
|
|
460
|
+
// Gave up waiting (queue too deep, timed out, or lost) — abandon
|
|
461
|
+
// the queued claim so we don't leave a phantom entry in the
|
|
462
|
+
// line that would block or mislead other claimers.
|
|
463
|
+
claim.revoke?.();
|
|
464
|
+
throw err;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return wrapClaimHandle(claim, waited, fenceToken);
|
|
468
|
+
},
|
|
469
|
+
list(target?: Partial<ModelTarget>): readonly ModelClaim[] {
|
|
470
|
+
return listModelClaims(target);
|
|
471
|
+
},
|
|
472
|
+
waitFor(target: Partial<ModelTarget>, options?: ClaimWaitOptions): Promise<void> {
|
|
473
|
+
return waitForModelUnclaimed(target, options);
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* One held claim, as the public claim-state object.
|
|
479
|
+
*
|
|
480
|
+
* The live claim stream only tracks *open* claims; terminal states
|
|
481
|
+
* (committed / expired / canceled) drop out of the list entirely — exactly
|
|
482
|
+
* the ephemeral coordination model — so a present entry is by definition
|
|
483
|
+
* `status: 'active'`.
|
|
484
|
+
*/
|
|
485
|
+
const claimStateOf = (held: ModelClaim | undefined) => {
|
|
486
|
+
if (!held) return null;
|
|
487
|
+
return {
|
|
488
|
+
object: 'claim' as const,
|
|
489
|
+
id: held.id,
|
|
490
|
+
status: 'active' as const,
|
|
491
|
+
target: {
|
|
492
|
+
...streamTarget(held.target),
|
|
493
|
+
...subTarget(held.target),
|
|
494
|
+
},
|
|
495
|
+
description: held.description ?? 'editing',
|
|
496
|
+
heldBy: held.actor,
|
|
497
|
+
participantKind: held.participantKind,
|
|
498
|
+
expiresAt: held.expiresAt,
|
|
499
|
+
// Carried, not dropped: the coordinator writes a heartbeat's `details`
|
|
500
|
+
// into `meta.progress` on the holder's record so an observer can see
|
|
501
|
+
// what a long hold is doing. It reached `ModelClaim` and stopped here,
|
|
502
|
+
// which left the beat writable and unreadable — a channel with a setter
|
|
503
|
+
// and no getter. `target.meta` beside it stays the declared shape; this
|
|
504
|
+
// is the open record, because a declared shape has no member for a key
|
|
505
|
+
// the holder did not write.
|
|
506
|
+
...(held.meta !== undefined ? { meta: held.meta } : {}),
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// Build the typed proxy — one property per model. Done after publicClaims
|
|
511
|
+
// exists so model clients can expose workflow helpers such as
|
|
512
|
+
// `ablo.files.edit(...)` without importing protocol wiring.
|
|
513
|
+
const modelProxies: Record<string, ModelOperations<unknown, unknown>> = {};
|
|
514
|
+
for (const [schemaKey, modelDef] of Object.entries(schema.models)) {
|
|
515
|
+
const registeredModelName = modelDef.typename ?? schemaKey;
|
|
516
|
+
modelProxies[schemaKey] = createModelProxy(
|
|
517
|
+
schemaKey,
|
|
518
|
+
registeredModelName,
|
|
519
|
+
objectPool,
|
|
520
|
+
syncClient,
|
|
521
|
+
modelRegistry,
|
|
522
|
+
hydration,
|
|
523
|
+
{
|
|
524
|
+
createClaim: (claimOptions) => publicClaims.create(claimOptions),
|
|
525
|
+
createSnapshot: (modelKey, id) =>
|
|
526
|
+
createSnapshot({
|
|
527
|
+
pool: objectPool,
|
|
528
|
+
transport,
|
|
529
|
+
// `position.readFloor` is the value claims and snapshots stamp as
|
|
530
|
+
// `readAt` (max of the pool-applied cursor and the acked
|
|
531
|
+
// watermark for our own writes — see logPosition.ts).
|
|
532
|
+
// Stamping a bare stream cursor made a claim taken right after
|
|
533
|
+
// an ack-confirmed write stale against that write's own delta.
|
|
534
|
+
// The socket/store cursors are persistence-gated and therefore
|
|
535
|
+
// never ahead of `applied` — no extra max() needed here.
|
|
536
|
+
getLastSyncId: () => syncClient.position.readFloor,
|
|
537
|
+
entities: { [modelKey]: id },
|
|
538
|
+
}),
|
|
539
|
+
queue: (target) =>
|
|
540
|
+
publicClaims.queueFor(streamTarget(target)),
|
|
541
|
+
reorder: (target, order) =>
|
|
542
|
+
{ publicClaims.reorder(streamTarget(target), order); },
|
|
543
|
+
// One row can have several holders — sub-row claims on disjoint
|
|
544
|
+
// targets are all granted — so `state` and `holders` read the same
|
|
545
|
+
// list and differ only in how much of it they answer with. The
|
|
546
|
+
// projection lives here once: two copies of it would drift the
|
|
547
|
+
// moment a field is added to one caller's answer.
|
|
548
|
+
state: (target) => claimStateOf(publicClaims.list(modelTarget(target))[0]),
|
|
549
|
+
holders: (target) =>
|
|
550
|
+
publicClaims
|
|
551
|
+
.list(modelTarget(target))
|
|
552
|
+
.map((held) => claimStateOf(held))
|
|
553
|
+
.filter((claim): claim is NonNullable<typeof claim> => claim !== null),
|
|
554
|
+
waitFor: (target, waitOptions) =>
|
|
555
|
+
publicClaims.waitFor(modelTarget(target), waitOptions),
|
|
556
|
+
// Getters, not copies: identity is late-bound (seeded in `ready()`),
|
|
557
|
+
// and the contended / heldBy checks must compare against whoever
|
|
558
|
+
// this client resolved to, not the construction-time guess.
|
|
559
|
+
get selfParticipantId() { return selfParticipantId; },
|
|
560
|
+
get selfParticipantKind() { return selfParticipantKind; },
|
|
561
|
+
// Read-interest / write-intent enrolment for the typed surface.
|
|
562
|
+
// `enterScope`/`pinScope` resolve the `{ [schemaKey]: id }` scope
|
|
563
|
+
// through the same resolver the claim path uses, landing this client in
|
|
564
|
+
// the entity-scoped group the holder's claim presence fans out on.
|
|
565
|
+
// Returns the store promise so the claim write path can await pinScope
|
|
566
|
+
// before acquiring the lease (closing the subscribe-vs-broadcast race);
|
|
567
|
+
// read-interest callers (`get`/`claim.state`) still `void` it and
|
|
568
|
+
// stay fire-and-forget. It's soft either way — the store swallows
|
|
569
|
+
// reconcile errors so read interest never makes a read reject or stall.
|
|
570
|
+
enterScope: (scope) => store.enterScope(scope),
|
|
571
|
+
pinScope: (scope) => store.pinScope(scope),
|
|
572
|
+
// `ablo.<model>.join(ids, { ttl })` performs a scoped participant join
|
|
573
|
+
// on this model's sync group(s). WebSocket only — `join` throws
|
|
574
|
+
// `AbloConnectionError` if the socket isn't ready.
|
|
575
|
+
// `ttl` passes straight through — both surfaces spell the lease the
|
|
576
|
+
// same way now, so there is no rename here to make a field's name
|
|
577
|
+
// disagree with the value it carries.
|
|
578
|
+
createJoin: (modelKey, ids, options) =>
|
|
579
|
+
participantManager.join({
|
|
580
|
+
scope: { [modelKey]: ids },
|
|
581
|
+
...(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
|
|
582
|
+
}),
|
|
583
|
+
},
|
|
584
|
+
// The client-wide `wait` default; a per-call `wait` still wins.
|
|
585
|
+
internalOptions.wait,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const commits: CommitResource = {
|
|
590
|
+
async create(commitOptions: CommitCreateOptions): Promise<CommitReceipt> {
|
|
591
|
+
await ready();
|
|
592
|
+
// Same runtime contract as the per-model writes — one schema.
|
|
593
|
+
assertWriteOptions(
|
|
594
|
+
{
|
|
595
|
+
idempotencyKey: commitOptions.idempotencyKey,
|
|
596
|
+
readAt: commitOptions.readAt,
|
|
597
|
+
onStale: commitOptions.onStale,
|
|
598
|
+
wait: commitOptions.wait,
|
|
599
|
+
claim: commitOptions.claim,
|
|
600
|
+
},
|
|
601
|
+
'commits.create',
|
|
602
|
+
);
|
|
603
|
+
const clientTxId = createClientTxId(commitOptions.idempotencyKey);
|
|
604
|
+
// A claim handle supplies the batch stale-guard defaults — same
|
|
605
|
+
// semantics as `ablo.<model>.update({ id, data, claim })`, so the
|
|
606
|
+
// two write doors speak one claim vocabulary. Explicit options win.
|
|
607
|
+
const claim = commitOptions.claim ?? null;
|
|
608
|
+
const operations = normalizeCommitOperations(
|
|
609
|
+
{
|
|
610
|
+
...commitOptions,
|
|
611
|
+
readAt: commitOptions.readAt ?? claim?.readAt ?? null,
|
|
612
|
+
onStale:
|
|
613
|
+
commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
|
|
614
|
+
},
|
|
615
|
+
batchFence(claim?.target, claim?.fenceToken),
|
|
616
|
+
);
|
|
617
|
+
const wait = commitOptions.wait ?? 'confirmed';
|
|
618
|
+
// Route through the MutationQueue's commit lane so the call
|
|
619
|
+
// tolerates WS disconnects: the envelope stays in memory until
|
|
620
|
+
// reconnect, mutationExecutor.commit() owns transport-level
|
|
621
|
+
// retry, and `mutation_log` server-side dedupes replays by
|
|
622
|
+
// clientTxId. Replaces the direct ws.sendCommit /
|
|
623
|
+
// sendCommitQueued path that threw synchronously on
|
|
624
|
+
// `ws.readyState !== OPEN`. The queue lives on the internal
|
|
625
|
+
// SyncClient we already hold from createInternalComponents —
|
|
626
|
+
// no need to leak an accessor through BaseSyncedStore.
|
|
627
|
+
const queue = syncClient.getMutationQueue();
|
|
628
|
+
await queue.enqueueCommit(clientTxId, operations, {
|
|
629
|
+
...(commitOptions.reads ? { reads: [...commitOptions.reads] } : {}),
|
|
630
|
+
...(commitOptions.track ? { track: [...commitOptions.track] } : {}),
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
if (wait === 'queued') {
|
|
634
|
+
return { id: clientTxId, status: 'queued' };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const { lastSyncId, notifications, missingIds } =
|
|
638
|
+
await queue.waitForCommitReceipt(clientTxId);
|
|
639
|
+
return {
|
|
640
|
+
id: clientTxId,
|
|
641
|
+
status: 'confirmed',
|
|
642
|
+
lastSyncId,
|
|
643
|
+
...(notifications && notifications.length > 0 ? { notifications } : {}),
|
|
644
|
+
...(missingIds && missingIds.length > 0 ? { missingIds } : {}),
|
|
645
|
+
};
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* The control-plane credential: always the original configured secret key.
|
|
651
|
+
* Never reads `authCredentials` — that holds the exchanged sync credential
|
|
652
|
+
* (a wide-scope `rk_` on the hosted path), which control-plane routes
|
|
653
|
+
* rightly refuse (e.g. the user-session mint is sk_-gated). Counterpart to
|
|
654
|
+
* `getAuthToken()`, which resolves the sync-plane token.
|
|
655
|
+
*
|
|
656
|
+
* The secret-key-only rule is enforced on the server; the credential-kind taxonomy
|
|
657
|
+
* (secret/restricted/ephemeral/publishable) lives in `auth/credentialPolicy`.
|
|
658
|
+
*/
|
|
659
|
+
async function controlPlaneApiKey(): Promise<string | null> {
|
|
660
|
+
return resolveApiKeyValue(configuredApiKey);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Resolve the control-plane context a session/agent mint needs (sk_ +
|
|
665
|
+
* bootstrap base URL + the schema-key→typename map the server gates on).
|
|
666
|
+
* Shared by `sessions.create` and `agents.create` so the two mint doors
|
|
667
|
+
* can never drift on how a token is minted. Throws if no `sk_` is present —
|
|
668
|
+
* minting is a backend-only operation.
|
|
669
|
+
*/
|
|
670
|
+
async function buildMintContext(resource: string): Promise<MintSessionContext> {
|
|
671
|
+
const apiKey = await controlPlaneApiKey();
|
|
672
|
+
if (!apiKey) {
|
|
673
|
+
throw new AbloAuthenticationError(
|
|
674
|
+
`${resource} requires a secret (sk_) API key — call it from your backend, not the browser.`,
|
|
675
|
+
{ code: 'apikey_missing' },
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
apiKey,
|
|
680
|
+
baseUrl: resolveBootstrapBaseUrl({
|
|
681
|
+
url,
|
|
682
|
+
bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
|
|
683
|
+
}),
|
|
684
|
+
...(internalOptions.fetch ? { fetch: internalOptions.fetch } : {}),
|
|
685
|
+
// Map every `can` schema-key to the wire typename the server gates on, so a
|
|
686
|
+
// typename override (`documents` → `Document`) doesn't mint a capability
|
|
687
|
+
// the server then denies. Derived from this client's schema by the one rule
|
|
688
|
+
// the HTTP client and the mint route also read. See `MintSessionContext`.
|
|
689
|
+
modelTypenames: modelWireNames(schema.models),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const engine = {
|
|
694
|
+
...modelProxies,
|
|
695
|
+
|
|
696
|
+
ready,
|
|
697
|
+
waitForFlush,
|
|
698
|
+
|
|
699
|
+
/** Durable frame subscription — delegates to the store's registry, which
|
|
700
|
+
* re-attaches across socket rebuilds. */
|
|
701
|
+
subscribe: <K extends keyof CoreSyncEventMap>(
|
|
702
|
+
event: K,
|
|
703
|
+
handler: (...args: CoreSyncEventMap[K]) => void,
|
|
704
|
+
): (() => void) => store.subscribe(event, handler),
|
|
705
|
+
|
|
706
|
+
setAuthToken(token: string) {
|
|
707
|
+
// The single credential source is read lazily by bootstrap HTTP,
|
|
708
|
+
// lazy query HTTP, network probes, and WebSocket reconnect URL auth.
|
|
709
|
+
// Updating it here is enough for the next request/connect to use the
|
|
710
|
+
// refreshed token; no per-transport patching.
|
|
711
|
+
authCredentials.setAuthToken(token);
|
|
712
|
+
// A fresh credential is useless to a connection parked in offline /
|
|
713
|
+
// backoff / auth_blocked until the next probe trigger — so kick one now.
|
|
714
|
+
// Harmless while connected (the FSM ignores the nudge there).
|
|
715
|
+
store.nudgeReconnect();
|
|
716
|
+
},
|
|
717
|
+
|
|
718
|
+
async getAuthToken(): Promise<string | null> {
|
|
719
|
+
// The live short-lived bearer (set via `setAuthToken` / `apiKey`-resolver refresh)
|
|
720
|
+
// is the canonical credential; fall back to a configured API key.
|
|
721
|
+
//
|
|
722
|
+
// This is the sync-plane token (bootstrap, WebSocket, query HTTP). Control-plane
|
|
723
|
+
// calls (sessions.create, datasource registration) never use it — they
|
|
724
|
+
// present the original secret key via `controlPlaneApiKey()` below. The
|
|
725
|
+
// split matters: after the startup exchange this resolver returns the
|
|
726
|
+
// derived wide-scope `rk_`, a credential the control-plane routes
|
|
727
|
+
// correctly refuse (an agent token must never mint humans).
|
|
728
|
+
return (
|
|
729
|
+
authCredentials.getAuthToken() ??
|
|
730
|
+
(await resolveApiKeyValue(configuredApiKey)) ??
|
|
731
|
+
configuredAuthToken ??
|
|
732
|
+
null
|
|
733
|
+
);
|
|
734
|
+
},
|
|
735
|
+
|
|
736
|
+
setCredentialRefresher(refresher: (() => Promise<string | null>) | null) {
|
|
737
|
+
store.setCredentialRefresher(refresher);
|
|
738
|
+
},
|
|
739
|
+
|
|
740
|
+
// The org this client resolved to — null until `ready()` completes. Exposed
|
|
741
|
+
// as a property so integrators can read it programmatically.
|
|
742
|
+
get organizationId(): string | null {
|
|
743
|
+
return _resolvedOrganizationId;
|
|
744
|
+
},
|
|
745
|
+
|
|
746
|
+
nudgeReconnect() {
|
|
747
|
+
store.nudgeReconnect();
|
|
748
|
+
},
|
|
749
|
+
|
|
750
|
+
sessions: {
|
|
751
|
+
// A backend (holding `sk_`) mints a short-lived scoped token for one end
|
|
752
|
+
// user or one agent.
|
|
753
|
+
//
|
|
754
|
+
// Both arms authenticate with the original secret key
|
|
755
|
+
// (`controlPlaneApiKey()`), never the wide-scope `rk_` the startup exchange
|
|
756
|
+
// installed as the sync credential. A derived agent credential silently
|
|
757
|
+
// replacing the secret key on control-plane calls is how humans would get
|
|
758
|
+
// minted as agents — and correct attribution is the point.
|
|
759
|
+
async create(params: CreateSessionParams<S>): Promise<AbloSession> {
|
|
760
|
+
// Both mint paths (`{ user }` → /v1/ephemeral_keys → `ek_`,
|
|
761
|
+
// `{ agent, can }` → /v1/capabilities → scoped `rk_`) resolve their
|
|
762
|
+
// control-plane context through the shared `buildMintContext`, so this
|
|
763
|
+
// client, `agents.create`, and the stateless HTTP client can't drift on
|
|
764
|
+
// how a token is minted.
|
|
765
|
+
return mintSession(params, await buildMintContext('sessions.create'));
|
|
766
|
+
},
|
|
767
|
+
async revoke({ id }) {
|
|
768
|
+
const context = await buildMintContext('sessions.revoke');
|
|
769
|
+
return revokeCapability({
|
|
770
|
+
apiKey: context.apiKey,
|
|
771
|
+
baseUrl: context.baseUrl,
|
|
772
|
+
id,
|
|
773
|
+
...(context.fetch ? { fetch: context.fetch } : {}),
|
|
774
|
+
});
|
|
775
|
+
},
|
|
776
|
+
async rotate({ id, graceSeconds, ttlSeconds }) {
|
|
777
|
+
const context = await buildMintContext('sessions.rotate');
|
|
778
|
+
return rotateCapability({
|
|
779
|
+
apiKey: context.apiKey,
|
|
780
|
+
baseUrl: context.baseUrl,
|
|
781
|
+
id,
|
|
782
|
+
...(graceSeconds !== undefined ? { graceSeconds } : {}),
|
|
783
|
+
...(ttlSeconds !== undefined ? { ttlSeconds } : {}),
|
|
784
|
+
...(context.fetch ? { fetch: context.fetch } : {}),
|
|
785
|
+
});
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
|
|
789
|
+
// Mint a scoped agent identity and hand back a connected client bound to it —
|
|
790
|
+
// `sessions.create({ agent })` plus a typed `Ablo({ schema, apiKey })` client,
|
|
791
|
+
// for agents that run in this (secret-key-holding) process. Omitting `id`
|
|
792
|
+
// yields a fresh uuid per call, so concurrent agents are distinct participants
|
|
793
|
+
// that queue behind each other (even when they share a `name`). Humans don't
|
|
794
|
+
// get a server-built client — ship them a token via `sessions.create({ user })`.
|
|
795
|
+
agents: {
|
|
796
|
+
async create(params: CreateAgentClientParams<S>): Promise<Ablo<S>> {
|
|
797
|
+
// Distinct participant by default: omit `id` → a fresh uuid, so even two
|
|
798
|
+
// agents that share a `name` are independent participants and queue
|
|
799
|
+
// behind one another. `name` is display only (→ userMeta.name); it never
|
|
800
|
+
// derives the id. Pass an explicit `id` only to re-attach an agent to
|
|
801
|
+
// its own held claims.
|
|
802
|
+
const id = params.id ?? globalThis.crypto.randomUUID();
|
|
803
|
+
const userMeta =
|
|
804
|
+
params.name !== undefined ? { ...params.userMeta, name: params.name } : params.userMeta;
|
|
805
|
+
const sessionParams = {
|
|
806
|
+
agent: { id },
|
|
807
|
+
can: params.can,
|
|
808
|
+
...(params.syncGroups ? { syncGroups: params.syncGroups } : {}),
|
|
809
|
+
...(params.ttlSeconds !== undefined ? { ttlSeconds: params.ttlSeconds } : {}),
|
|
810
|
+
...(userMeta ? { userMeta } : {}),
|
|
811
|
+
} satisfies CreateAgentSessionParams<S>;
|
|
812
|
+
// Re-mint the `rk_` on every resolver call so a long-lived agent client
|
|
813
|
+
// never hits token expiry; the `sk_` stays in this process — the child
|
|
814
|
+
// only ever sees its own short-lived `rk_`.
|
|
815
|
+
const mintToken = async (): Promise<string> =>
|
|
816
|
+
(await mintSession(sessionParams, await buildMintContext('agents.create')))
|
|
817
|
+
.token;
|
|
818
|
+
// Mint once up front so a bad key / denied scope throws HERE, not later
|
|
819
|
+
// inside the child's bootstrap; reuse that first token, re-mint on refresh.
|
|
820
|
+
let pending: string | null = await mintToken();
|
|
821
|
+
const apiKey: CredentialProvider = async () => {
|
|
822
|
+
if (pending !== null) {
|
|
823
|
+
const token = pending;
|
|
824
|
+
pending = null;
|
|
825
|
+
return token;
|
|
826
|
+
}
|
|
827
|
+
return mintToken();
|
|
828
|
+
};
|
|
829
|
+
return createSibling({ ...(internalOptions as AbloOptions<S>), apiKey });
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
|
|
833
|
+
async dispose() {
|
|
834
|
+
lifecycle.dispose();
|
|
835
|
+
try {
|
|
836
|
+
await store.disconnect();
|
|
837
|
+
} catch (err) {
|
|
838
|
+
// Best-effort teardown — a disposal hiccup isn't consumer-actionable → debug.
|
|
839
|
+
logger.debug('Error during sync engine disposal', { error: (err as Error).message });
|
|
840
|
+
}
|
|
841
|
+
presenceStream.dispose();
|
|
842
|
+
claimStream.dispose();
|
|
843
|
+
syncClient.dispose();
|
|
844
|
+
},
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Destroy every IndexedDB database owned by this engine. Disconnects
|
|
848
|
+
* the WebSocket, releases timers, and deletes all `ablo_*` / `ablo-*`
|
|
849
|
+
* databases. Typically called on session expiry or explicit logout.
|
|
850
|
+
* Best-effort — errors from individual deletions are swallowed.
|
|
851
|
+
*/
|
|
852
|
+
async purge() {
|
|
853
|
+
await store.purge();
|
|
854
|
+
syncClient.dispose();
|
|
855
|
+
},
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Subscribe to terminal session events after the store has stopped network
|
|
859
|
+
* access and completed authenticated local-state cleanup. Multiple
|
|
860
|
+
* subscribers are supported; consumers typically redirect to sign-in.
|
|
861
|
+
*/
|
|
862
|
+
onSessionError(listener: (error: Error) => void) {
|
|
863
|
+
return store.subscribeSessionError(listener);
|
|
864
|
+
},
|
|
865
|
+
|
|
866
|
+
onMutationFailure(
|
|
867
|
+
listener: (payload: {
|
|
868
|
+
transaction: import('../transactions/mutations/MutationQueue.js').QueuedMutation;
|
|
869
|
+
error: Error;
|
|
870
|
+
permanent?: boolean;
|
|
871
|
+
}) => void,
|
|
872
|
+
) {
|
|
873
|
+
return store.subscribeMutationFailure(listener);
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
onCommitLatency(
|
|
877
|
+
listener: (
|
|
878
|
+
sample: import('../transactions/mutations/commitLatency.js').CommitLatencySample,
|
|
879
|
+
) => void,
|
|
880
|
+
) {
|
|
881
|
+
return store.subscribeCommitLatency(listener);
|
|
882
|
+
},
|
|
883
|
+
|
|
884
|
+
waitForConfirmation(modelName: string, modelId: string) {
|
|
885
|
+
return store.waitForConfirmation(modelName, modelId);
|
|
886
|
+
},
|
|
887
|
+
|
|
888
|
+
// Expose the store's MobX observable directly — single source of truth.
|
|
889
|
+
// React components using observer() will re-render automatically on
|
|
890
|
+
// any state change (syncing, error, offline, pendingChanges, progress).
|
|
891
|
+
get syncStatus() {
|
|
892
|
+
return store.syncStatus;
|
|
893
|
+
},
|
|
894
|
+
|
|
895
|
+
schema,
|
|
896
|
+
|
|
897
|
+
// ── Internal accessors for framework integration ─────────────────
|
|
898
|
+
// These expose internal components for consumers that need direct
|
|
899
|
+
// access (e.g., SyncEngineProvider wiring SyncContext, collaboration
|
|
900
|
+
// events accessing the WebSocket handle, demand loaders accessing
|
|
901
|
+
// the pool). Prefixed with _ to signal "internal but stable."
|
|
902
|
+
|
|
903
|
+
/** The BaseSyncedStore — implements SyncStoreContract for SyncContext.Provider. */
|
|
904
|
+
get _store() { return store; },
|
|
905
|
+
|
|
906
|
+
/** The InstanceCache — for demand loaders that need pool.createFromData(). */
|
|
907
|
+
get _pool() { return objectPool; },
|
|
908
|
+
|
|
909
|
+
/** The SyncWebSocket — for collaboration events (selection, cursors). */
|
|
910
|
+
get _ws() { return store.getSyncWebSocket(); },
|
|
911
|
+
|
|
912
|
+
/** Presence livestream — same socket as entity sync, no second
|
|
913
|
+
* connection. Stable reference across the engine's lifetime. */
|
|
914
|
+
presence: presenceStream,
|
|
915
|
+
|
|
916
|
+
/** Claim livestream — same socket. Stable reference. */
|
|
917
|
+
claims: publicClaims,
|
|
918
|
+
|
|
919
|
+
commits,
|
|
920
|
+
|
|
921
|
+
/** Context-staleness snapshot — see `engine.snapshot(...)` JSDoc. */
|
|
922
|
+
snapshot<ModelName extends keyof S & string>(
|
|
923
|
+
entities: Readonly<Record<ModelName, string | readonly string[]>>,
|
|
924
|
+
): Snapshot<Schema<S>, ModelName> {
|
|
925
|
+
return createSnapshot<Schema<S>, ModelName>({
|
|
926
|
+
pool: objectPool,
|
|
927
|
+
transport,
|
|
928
|
+
getLastSyncId: () => transport.getLastSyncId(),
|
|
929
|
+
entities,
|
|
930
|
+
});
|
|
931
|
+
},
|
|
932
|
+
} as Ablo<S>;
|
|
933
|
+
|
|
934
|
+
return engine;
|
|
935
|
+
}
|