@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,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The semantic schema-drift classifier — the question that matters is not
|
|
3
|
+
* "did the schema change at all?" (one whole-schema hash answers that, and
|
|
4
|
+
* welds every client to every push) but "does THIS client use anything that
|
|
5
|
+
* changed?". This module compares only the models the client declares against
|
|
6
|
+
* the server's per-model surface (`GET /api/schema`), so:
|
|
7
|
+
*
|
|
8
|
+
* - a purely additive server change (models this build never references) is
|
|
9
|
+
* SILENCE — deployed clients don't care what they don't use;
|
|
10
|
+
* - a model this build declares that the server doesn't have is named, with
|
|
11
|
+
* the push as the fix;
|
|
12
|
+
* - a shared model whose content differs is named, so "which field moved"
|
|
13
|
+
* is one `ablo status` away instead of a hash-guessing game.
|
|
14
|
+
*
|
|
15
|
+
* Pure and transport-free; the BootstrapFetcher owns fetching the surface.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** One model as the server's schema read-back reports it. */
|
|
19
|
+
export interface ServerSchemaModel {
|
|
20
|
+
readonly key: string;
|
|
21
|
+
/** Per-model content hash; absent on servers older than this check. */
|
|
22
|
+
readonly hash?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SchemaDriftFinding =
|
|
26
|
+
/** Every model this client declares exists server-side with matching content
|
|
27
|
+
* (the server may know more — that's an additive lead, not drift). */
|
|
28
|
+
| { readonly kind: 'aligned' }
|
|
29
|
+
/** Models this build declares that the server has no idea about — writes to
|
|
30
|
+
* them will be declined until the schema is pushed. */
|
|
31
|
+
| { readonly kind: 'unpushed'; readonly models: readonly string[] }
|
|
32
|
+
/** Shared models whose content differs between this build and the server
|
|
33
|
+
* (may also carry unpushed models found alongside). */
|
|
34
|
+
| {
|
|
35
|
+
readonly kind: 'changed';
|
|
36
|
+
readonly models: readonly string[];
|
|
37
|
+
readonly unpushed: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
/** The server surface carries no per-model hashes (older server) — the
|
|
40
|
+
* caller falls back to the whole-hash comparison. */
|
|
41
|
+
| { readonly kind: 'unknown' };
|
|
42
|
+
|
|
43
|
+
export function classifySchemaDrift(
|
|
44
|
+
clientModels: Readonly<Record<string, string>>,
|
|
45
|
+
serverModels: readonly ServerSchemaModel[],
|
|
46
|
+
): SchemaDriftFinding {
|
|
47
|
+
if (serverModels.length > 0 && serverModels.every((m) => !m.hash)) {
|
|
48
|
+
return { kind: 'unknown' };
|
|
49
|
+
}
|
|
50
|
+
const server = new Map(serverModels.map((m) => [m.key, m.hash]));
|
|
51
|
+
const unpushed: string[] = [];
|
|
52
|
+
const changed: string[] = [];
|
|
53
|
+
for (const [key, hash] of Object.entries(clientModels)) {
|
|
54
|
+
const serverHash = server.get(key);
|
|
55
|
+
if (serverHash === undefined) unpushed.push(key);
|
|
56
|
+
else if (serverHash !== hash) changed.push(key);
|
|
57
|
+
}
|
|
58
|
+
if (changed.length > 0) return { kind: 'changed', models: changed, unpushed };
|
|
59
|
+
if (unpushed.length > 0) return { kind: 'unpushed', models: unpushed };
|
|
60
|
+
return { kind: 'aligned' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The warning for a real, named divergence. Calm and specific: which models,
|
|
65
|
+
* what that means for this client, and the one next step. Never speaks about
|
|
66
|
+
* hashes — the point of the semantic check is that nobody has to compare hex.
|
|
67
|
+
*/
|
|
68
|
+
export function describeSchemaDrift(
|
|
69
|
+
finding: Extract<SchemaDriftFinding, { kind: 'unpushed' | 'changed' }>,
|
|
70
|
+
serverLabel: string,
|
|
71
|
+
): string {
|
|
72
|
+
if (finding.kind === 'unpushed') {
|
|
73
|
+
return (
|
|
74
|
+
`Ablo: This build declares models the server at ${serverLabel} doesn't have yet ` +
|
|
75
|
+
`(${finding.models.join(', ')}). Writes to them will be declined until the schema is ` +
|
|
76
|
+
`pushed — run \`ablo push\` (and \`ablo status\` to confirm it targets this server).`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const alsoUnpushed = finding.unpushed.length > 0 ? ` (${finding.unpushed.join(', ')} not pushed yet)` : '';
|
|
80
|
+
return (
|
|
81
|
+
`Ablo: These models differ between this build and the server at ${serverLabel}: ` +
|
|
82
|
+
`${finding.models.join(', ')}${alsoUnpushed}. Reads and writes touching what changed may be ` +
|
|
83
|
+
`declined — \`ablo status\` shows the deployed shape; pushing your schema or deploying a ` +
|
|
84
|
+
`current build aligns them.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod validation schemas for sync engine server responses.
|
|
3
|
+
*
|
|
4
|
+
* Validates data at the fetch boundary before it enters the sync engine.
|
|
5
|
+
* Unknown keys are kept, so a server that adds a field does not break a client
|
|
6
|
+
* that predates it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { globalRuntime } from "../context.js";
|
|
11
|
+
import type { RuntimeContext } from "../RuntimeContext.js";
|
|
12
|
+
import { AbloValidationError } from "@abloatai/transaction/errors";
|
|
13
|
+
import { syncDeltaWireCoreSchema } from '@abloatai/transaction/wire/delta';
|
|
14
|
+
|
|
15
|
+
// ─── Server Delta Schema ─────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A delta as it arrives in a bootstrap payload.
|
|
19
|
+
*
|
|
20
|
+
* The bootstrap routes return the same rows the broadcast path does, so the
|
|
21
|
+
* fields are taken from {@link syncDeltaWireCoreSchema} rather than restated
|
|
22
|
+
* here — a delta is one shape, and this is the reader for it, not a second
|
|
23
|
+
* definition of it. Unknown keys are kept, because the server sends its own
|
|
24
|
+
* wider projection (attribution, `projectId`) that later stages may read.
|
|
25
|
+
*/
|
|
26
|
+
export const ServerDeltaSchema = syncDeltaWireCoreSchema
|
|
27
|
+
.pick({
|
|
28
|
+
id: true,
|
|
29
|
+
actionType: true,
|
|
30
|
+
modelName: true,
|
|
31
|
+
modelId: true,
|
|
32
|
+
data: true,
|
|
33
|
+
})
|
|
34
|
+
.loose();
|
|
35
|
+
|
|
36
|
+
export type ValidatedServerDelta = z.infer<typeof ServerDeltaSchema>;
|
|
37
|
+
|
|
38
|
+
// ─── Model Value Schema ─────────────────────────────────────────────────────
|
|
39
|
+
// A model's values can arrive in more than one shape depending on how the
|
|
40
|
+
// server serialized them:
|
|
41
|
+
// - Array: an already-parsed JSON array (the common case)
|
|
42
|
+
// - String: a JSON array still encoded as a string, which must be parsed
|
|
43
|
+
// - null: no matching rows
|
|
44
|
+
// This schema normalizes every variant into an array before downstream use.
|
|
45
|
+
|
|
46
|
+
const ModelValueSchema = z
|
|
47
|
+
.union([z.array(z.unknown()), z.string(), z.null()])
|
|
48
|
+
.transform((val): unknown[] => {
|
|
49
|
+
if (val === null) return [];
|
|
50
|
+
if (typeof val === 'string') {
|
|
51
|
+
try {
|
|
52
|
+
const parsed: unknown = JSON.parse(val);
|
|
53
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return val;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ─── Bootstrap Response Schema ───────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export const BootstrapResponseSchema = z
|
|
64
|
+
.object({
|
|
65
|
+
type: z.enum(['full', 'partial']),
|
|
66
|
+
lastSyncId: z.number(),
|
|
67
|
+
models: z.record(z.string(), ModelValueSchema).optional(),
|
|
68
|
+
deltas: z.array(ServerDeltaSchema).optional(),
|
|
69
|
+
deltaCount: z.number().optional(),
|
|
70
|
+
failedModels: z.array(z.string()).optional(),
|
|
71
|
+
timestamp: z.number().default(() => Date.now()),
|
|
72
|
+
// The server's active schema hash, used to detect schema drift. Optional:
|
|
73
|
+
// absent when the server predates this field or the tenant has never
|
|
74
|
+
// pushed a schema.
|
|
75
|
+
schemaHash: z.string().optional(),
|
|
76
|
+
// Present when a paged single-model request stopped at its row limit
|
|
77
|
+
// with rows remaining: pass it back as the next page's cursor. Absent
|
|
78
|
+
// on the final page, on unpaged responses, and from older servers.
|
|
79
|
+
nextCursor: z.string().optional(),
|
|
80
|
+
})
|
|
81
|
+
.loose();
|
|
82
|
+
|
|
83
|
+
export type ValidatedBootstrapResponse = z.infer<typeof BootstrapResponseSchema>;
|
|
84
|
+
|
|
85
|
+
// ─── Parse Helpers ───────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validates a raw bootstrap response from the server and returns the typed
|
|
89
|
+
* result. On failure it records a diagnostic breadcrumb and throws an
|
|
90
|
+
* {@link AbloValidationError} describing which fields were invalid.
|
|
91
|
+
*/
|
|
92
|
+
export function parseBootstrapResponse(
|
|
93
|
+
raw: unknown,
|
|
94
|
+
runtime: RuntimeContext = globalRuntime,
|
|
95
|
+
): ValidatedBootstrapResponse {
|
|
96
|
+
const result = BootstrapResponseSchema.safeParse(raw);
|
|
97
|
+
|
|
98
|
+
if (!result.success) {
|
|
99
|
+
const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
|
|
100
|
+
|
|
101
|
+
runtime.observability.breadcrumb(
|
|
102
|
+
'Bootstrap response validation failed',
|
|
103
|
+
'sync.bootstrap',
|
|
104
|
+
'error',
|
|
105
|
+
{
|
|
106
|
+
issues,
|
|
107
|
+
rawType: typeof raw,
|
|
108
|
+
rawKeys: raw && typeof raw === 'object' ? Object.keys(raw).join(',') : 'n/a',
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
throw new AbloValidationError(`Invalid bootstrap response: ${issues}`, {
|
|
113
|
+
code: 'bootstrap_response_schema_invalid',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result.data;
|
|
118
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { AbloSessionError, isAccessCredentialExpiryCloseReason } from '@abloatai/transaction/errors';
|
|
2
|
+
import type { RuntimeContext } from '../RuntimeContext.js';
|
|
3
|
+
import type { SyncClient } from '../SyncClient.js';
|
|
4
|
+
import type { Database } from '../Database.js';
|
|
5
|
+
import type { InstanceCache } from '../InstanceCache.js';
|
|
6
|
+
import type { ConnectionManager } from './ConnectionManager.js';
|
|
7
|
+
import type { SubscriptionManager } from './SubscriptionManager.js';
|
|
8
|
+
import type { SyncStatus } from '../storeContract.js';
|
|
9
|
+
import type {
|
|
10
|
+
BootstrapHint,
|
|
11
|
+
BootstrapDataEvent,
|
|
12
|
+
PresenceUpdate,
|
|
13
|
+
SyncWebSocket,
|
|
14
|
+
EventMap,
|
|
15
|
+
} from './SyncWebSocket.js';
|
|
16
|
+
import type { SyncDelta } from './SyncWebSocket.js';
|
|
17
|
+
|
|
18
|
+
export interface SocketEventHost<TCollaboration extends EventMap<TCollaboration>> {
|
|
19
|
+
syncWebSocket: SyncWebSocket<TCollaboration>;
|
|
20
|
+
syncClient: SyncClient;
|
|
21
|
+
database: Database;
|
|
22
|
+
objectPool: InstanceCache;
|
|
23
|
+
areaOfInterest: SubscriptionManager;
|
|
24
|
+
runtime: RuntimeContext;
|
|
25
|
+
dataReady: boolean;
|
|
26
|
+
connectionManager: ConnectionManager | null;
|
|
27
|
+
disposers: (() => void)[];
|
|
28
|
+
onConnectionEvent?: (event: string) => void;
|
|
29
|
+
updateSyncStatus(updates: Partial<SyncStatus>): void;
|
|
30
|
+
processDeltaWithBatching(delta: SyncDelta): void;
|
|
31
|
+
applyDeltaFrame(deltas: SyncDelta[]): void;
|
|
32
|
+
handleBootstrapRequired(hint: BootstrapHint): void;
|
|
33
|
+
handleBootstrapData(data: BootstrapDataEvent): void;
|
|
34
|
+
handlePresenceUpdate(data: PresenceUpdate): void;
|
|
35
|
+
performCredentialRefresh(): Promise<'refreshed' | 'session_error' | 'network_error'>;
|
|
36
|
+
handleTerminalSessionError(error: Error): void;
|
|
37
|
+
nudgeReconnect(): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function wireSocketEvents<TCollaboration extends EventMap<TCollaboration>>(
|
|
41
|
+
deps: SocketEventHost<TCollaboration>,
|
|
42
|
+
): void {
|
|
43
|
+
|
|
44
|
+
// Connection events → forward to connection lifecycle callback
|
|
45
|
+
const onConnected = deps.syncWebSocket.subscribe('connected', () => {
|
|
46
|
+
deps.syncClient.markConnected();
|
|
47
|
+
deps.onConnectionEvent?.('WS_CONNECTED');
|
|
48
|
+
if (deps.dataReady) {
|
|
49
|
+
deps.updateSyncStatus({ state: 'idle', offlineSince: undefined });
|
|
50
|
+
} else {
|
|
51
|
+
deps.updateSyncStatus({ offlineSince: undefined });
|
|
52
|
+
}
|
|
53
|
+
// Re-assert read interest on every (re)connect. After a transient
|
|
54
|
+
// reconnect the socket re-sends its URL groups, but interest may have
|
|
55
|
+
// changed while offline; after a full reconnect the new socket's URL
|
|
56
|
+
// carries only base groups. `resync` re-pushes the current desired set
|
|
57
|
+
// so the server-side index matches what the user is actually viewing.
|
|
58
|
+
void deps.areaOfInterest.resync();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const onDisconnected = deps.syncWebSocket.subscribe('disconnected', () => {
|
|
62
|
+
deps.syncClient.disconnect();
|
|
63
|
+
deps.onConnectionEvent?.('WS_DISCONNECTED');
|
|
64
|
+
deps.updateSyncStatus({ state: 'offline', offlineSince: new Date() });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const onReconnecting = deps.syncWebSocket.subscribe('reconnecting', (...args) => {
|
|
68
|
+
const [{ attempt, delay }] = args;
|
|
69
|
+
deps.runtime.logger.info('[BaseSyncedStore] WebSocket reconnecting', { attempt, delay });
|
|
70
|
+
deps.updateSyncStatus({ state: 'reconnecting' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Delta events → feed into processing pipeline
|
|
74
|
+
const onDelta = deps.syncWebSocket.subscribe('delta', (delta: SyncDelta) => {
|
|
75
|
+
deps.processDeltaWithBatching(delta);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const onDeltaBatch = deps.syncWebSocket.subscribe('delta_batch', (deltas: SyncDelta[]) => {
|
|
79
|
+
// A catch-up/reconnect frame is already complete — apply it as ONE
|
|
80
|
+
// atomic flush so the gallery re-renders once, not once per 50-delta
|
|
81
|
+
// chunk. See `applyDeltaFrame`.
|
|
82
|
+
deps.applyDeltaFrame(deltas);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Bootstrap events
|
|
86
|
+
const onBootstrapRequired = deps.syncWebSocket.subscribe(
|
|
87
|
+
'bootstrap_required',
|
|
88
|
+
(hint: BootstrapHint) => { deps.handleBootstrapRequired(hint); }
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const onBootstrapData = deps.syncWebSocket.subscribe('bootstrap_data', (...args) => {
|
|
92
|
+
const data = args[0];
|
|
93
|
+
deps.handleBootstrapData(data);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const onPresenceUpdate = deps.syncWebSocket.subscribe('presence_update', (...args) => {
|
|
97
|
+
const data = args[0];
|
|
98
|
+
deps.handlePresenceUpdate(data);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Error events
|
|
102
|
+
const onError = deps.syncWebSocket.subscribe('error', (error: Error) => {
|
|
103
|
+
if (error.message === 'Network is offline' || error.message === 'WebSocket connection failed') {
|
|
104
|
+
deps.updateSyncStatus({ state: 'offline', offlineSince: new Date() });
|
|
105
|
+
} else {
|
|
106
|
+
deps.updateSyncStatus({ state: 'error', error });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Terminal session loss (revocation / the login itself is gone): notify,
|
|
111
|
+
// route the FSM to its terminal state, and clear local data.
|
|
112
|
+
const handleTerminalSessionError = (error: Error): void => {
|
|
113
|
+
deps.onConnectionEvent?.('WS_SESSION_ERROR');
|
|
114
|
+
deps.handleTerminalSessionError(error);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const onSessionError = deps.syncWebSocket.subscribe('session_error', (error: Error) => {
|
|
118
|
+
// WS analog of HTTP's `apikey_expired` (see AbloSessionError.
|
|
119
|
+
// isSessionErrorResponse): the hub's keepalive reaper closes sockets
|
|
120
|
+
// whose SHORT-LIVED access credential (`ek_`/`rk_`) passed its expiry
|
|
121
|
+
// with `4001 credential_expired`. That is re-mintable from the
|
|
122
|
+
// still-valid login — recover silently (un-latch, single-flight
|
|
123
|
+
// re-mint, reconnect) instead of signing out and clearing local data.
|
|
124
|
+
// Only a mint that answers `null` (the login itself is gone) falls
|
|
125
|
+
// through to the terminal path. Without this branch, every credential
|
|
126
|
+
// TTL elapse wedged the socket behind the write-once session latch.
|
|
127
|
+
if (AbloSessionError.isSessionError(error) && isAccessCredentialExpiryCloseReason(error.message)) {
|
|
128
|
+
deps.runtime.observability.breadcrumb(
|
|
129
|
+
'WebSocket closed for expired access credential — re-minting',
|
|
130
|
+
'sync.websocket',
|
|
131
|
+
'warning',
|
|
132
|
+
);
|
|
133
|
+
// Un-latch BEFORE the async mint so the FSM's own recovery
|
|
134
|
+
// (probe → refreshing_credential → reconnect) is never blocked on
|
|
135
|
+
// our .then() ordering.
|
|
136
|
+
deps.syncWebSocket.clearSessionError();
|
|
137
|
+
void deps.performCredentialRefresh().then((outcome) => {
|
|
138
|
+
if (outcome === 'refreshed') {
|
|
139
|
+
if (deps.connectionManager) {
|
|
140
|
+
// Kick a parked FSM; a concurrent probe joins the same
|
|
141
|
+
// single-flight mint, so this never double-mints.
|
|
142
|
+
deps.nudgeReconnect();
|
|
143
|
+
} else {
|
|
144
|
+
// Agent/system clients have no connection FSM
|
|
145
|
+
// (createConnectionManager returns null for kind 'agent') —
|
|
146
|
+
// reconnect the socket directly; connect() reads the
|
|
147
|
+
// freshly-minted credential from the credential source.
|
|
148
|
+
deps.syncWebSocket.resetReconnectAttempts();
|
|
149
|
+
deps.syncWebSocket.connect();
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (outcome === 'session_error') {
|
|
154
|
+
// The mint endpoint rejected: the long-lived login is gone.
|
|
155
|
+
// Re-latch so writes reject with the permanent session type
|
|
156
|
+
// (see SyncWebSocket.notConnectedError) instead of parking.
|
|
157
|
+
deps.syncWebSocket.setSessionErrorDetected();
|
|
158
|
+
handleTerminalSessionError(error);
|
|
159
|
+
}
|
|
160
|
+
// 'network_error' → transient mint failure. The WS_DISCONNECTED
|
|
161
|
+
// that follows this event already put the FSM on its probe/backoff
|
|
162
|
+
// loop, which retries through the same single-flight refresh.
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
handleTerminalSessionError(error);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Handshake failed: WS close before open. The HTTP status is hidden
|
|
170
|
+
// behind close code 1006, so we can't tell whether the server rejected
|
|
171
|
+
// auth (401/403) or the connection never reached the server (DNS/TLS/LB).
|
|
172
|
+
// Forward a dedicated event so the connection-lifecycle owner can run
|
|
173
|
+
// an authenticated HTTP probe to disambiguate.
|
|
174
|
+
const onHandshakeFailed = deps.syncWebSocket.subscribe('handshake_failed', () => {
|
|
175
|
+
deps.onConnectionEvent?.('WS_HANDSHAKE_FAILED');
|
|
176
|
+
deps.updateSyncStatus({ state: 'offline', offlineSince: new Date() });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const onReconnectFailed = deps.syncWebSocket.subscribe('reconnect_failed', (...args) => {
|
|
180
|
+
const [{ attempts }] = args;
|
|
181
|
+
// consumer register: reconnection exhausted — the app is now offline
|
|
182
|
+
deps.runtime.logger.warn(
|
|
183
|
+
'Lost connection to the sync service and could not reconnect. Your app is now offline; changes will sync once the connection is restored.',
|
|
184
|
+
);
|
|
185
|
+
deps.runtime.logger.debug('[BaseSyncedStore] WebSocket reconnection gave up', { attempts });
|
|
186
|
+
deps.updateSyncStatus({ state: 'reconnecting' });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
deps.disposers.push(
|
|
190
|
+
onConnected, onDisconnected, onReconnecting,
|
|
191
|
+
onDelta, onDeltaBatch, onBootstrapRequired,
|
|
192
|
+
onBootstrapData, onPresenceUpdate,
|
|
193
|
+
onError, onSessionError, onHandshakeFailed, onReconnectFailed,
|
|
194
|
+
() => { deps.areaOfInterest.dispose(); },
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds the resume state of one WebSocket sync session: the `lastSyncId`
|
|
3
|
+
* watermark, which marks the highest delta the client has seen, and an opaque
|
|
4
|
+
* server cursor used for incremental sync. The transport carries both across
|
|
5
|
+
* reconnects so the session can resume where it left off.
|
|
6
|
+
*
|
|
7
|
+
* The watermark advances under a strict rule — it moves forward only on an
|
|
8
|
+
* acknowledgement that is gated on durable persistence — which the transport
|
|
9
|
+
* enforces at its acknowledgement and delta-handling call sites.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class SyncCursor {
|
|
13
|
+
lastSyncId: number;
|
|
14
|
+
syncCursor: string | null;
|
|
15
|
+
|
|
16
|
+
constructor(lastSyncId: number) {
|
|
17
|
+
this.lastSyncId = lastSyncId;
|
|
18
|
+
this.syncCursor = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Advances the watermark in response to an acknowledgement. This becomes the
|
|
23
|
+
* value the next incremental-sync request and the connect handshake send, and
|
|
24
|
+
* the value {@link SyncCursor.getLastSyncId} reports when persisting on a
|
|
25
|
+
* clean shutdown. The move is monotonic: a stale, lower acknowledgement never
|
|
26
|
+
* pulls the watermark backward.
|
|
27
|
+
*/
|
|
28
|
+
ackAdvance(syncId: number): void {
|
|
29
|
+
if (syncId > this.lastSyncId) {
|
|
30
|
+
this.lastSyncId = syncId;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Sets the watermark outright, used when restoring persisted state.
|
|
36
|
+
*/
|
|
37
|
+
setLastSyncId(syncId: number): void {
|
|
38
|
+
this.lastSyncId = syncId;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sets the opaque server cursor used for incremental sync.
|
|
43
|
+
*/
|
|
44
|
+
setSyncCursor(cursor: string | null): void {
|
|
45
|
+
this.syncCursor = cursor;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns the current opaque server cursor, or null if none is set.
|
|
50
|
+
*/
|
|
51
|
+
getSyncCursor(): string | null {
|
|
52
|
+
return this.syncCursor;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns the highest delta id seen this session, for persistence on a clean
|
|
57
|
+
* shutdown.
|
|
58
|
+
*/
|
|
59
|
+
getLastSyncId(): number {
|
|
60
|
+
return this.lastSyncId || 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives a client's sync plan from its {@link Schema}. Walking the schema's
|
|
3
|
+
* models and relations, it produces two declarative arrays consumed when the
|
|
4
|
+
* store is constructed: the foreign-key indexes to register on the in-memory
|
|
5
|
+
* object pool, and the enrichment rules that attach related parents to
|
|
6
|
+
* incoming rows. See {@link deriveSyncPlanFromSchema}.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Schema } from '@abloatai/transaction/schema/schema';
|
|
10
|
+
|
|
11
|
+
/** A foreign-key index to register on the in-memory object pool when the store is constructed. */
|
|
12
|
+
export interface ForeignKeyIndexSpec {
|
|
13
|
+
/**
|
|
14
|
+
* The name of the child model, where the foreign-key field lives, and the
|
|
15
|
+
* name the object pool indexes by. Use the wire type-name casing (for
|
|
16
|
+
* example `'Block'`, not `'block'`), since that is the value
|
|
17
|
+
* stamped onto reconstructed models and the key the pool looks up.
|
|
18
|
+
*/
|
|
19
|
+
readonly modelName: string;
|
|
20
|
+
/** The foreign-key field name on the child model, for example `'sectionId'`. */
|
|
21
|
+
readonly fieldName: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A declarative rule for enriching an incoming row with its related parent.
|
|
26
|
+
*
|
|
27
|
+
* When a delta for `modelName` arrives and its row has been constructed, the
|
|
28
|
+
* store reads the row's `foreignKey` value, looks up the matching parent in
|
|
29
|
+
* the object pool, and attaches it under `relationKey`. Enrichment is
|
|
30
|
+
* best-effort: if the parent is not in the pool yet — for example, it arrives
|
|
31
|
+
* later in the same bootstrap batch — the step is skipped without error.
|
|
32
|
+
*/
|
|
33
|
+
export interface EnrichmentPlanEntry {
|
|
34
|
+
/** The child model whose incoming deltas should be enriched. */
|
|
35
|
+
readonly modelName: string;
|
|
36
|
+
/** The foreign-key field on the child that points at the parent's id. */
|
|
37
|
+
readonly foreignKey: string;
|
|
38
|
+
/** The property name under which to attach the parent model. */
|
|
39
|
+
readonly relationKey: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Walks a schema and derives the two sync-plan arrays used when the store is
|
|
44
|
+
* constructed: the foreign-key indexes to register on the object pool and the
|
|
45
|
+
* enrichment plan. See {@link ForeignKeyIndexSpec} and
|
|
46
|
+
* {@link EnrichmentPlanEntry}.
|
|
47
|
+
*
|
|
48
|
+
* Both are drawn from each `belongsTo` relation that sets `options.index` or
|
|
49
|
+
* `options.enrich`; relations without those options are skipped. Enabling them
|
|
50
|
+
* is opt-in, so adding a `belongsTo` relation never silently changes how deltas
|
|
51
|
+
* apply or how lookups resolve. A `hasMany` or `hasOne` relation registers its
|
|
52
|
+
* index on the target model, since that is where the foreign key lives. The
|
|
53
|
+
* function has no side effects and is called once at construction.
|
|
54
|
+
*/
|
|
55
|
+
export function deriveSyncPlanFromSchema(schema: Schema): {
|
|
56
|
+
enrichmentPlan: EnrichmentPlanEntry[];
|
|
57
|
+
foreignKeyIndexes: ForeignKeyIndexSpec[];
|
|
58
|
+
} {
|
|
59
|
+
const enrichmentPlan: EnrichmentPlanEntry[] = [];
|
|
60
|
+
const foreignKeyIndexes: ForeignKeyIndexSpec[] = [];
|
|
61
|
+
|
|
62
|
+
for (const [modelName, def] of Object.entries(schema.models)) {
|
|
63
|
+
const typename = def.typename ?? modelName;
|
|
64
|
+
|
|
65
|
+
for (const [relationKey, rel] of Object.entries(def.relations)) {
|
|
66
|
+
if (rel.type === 'belongsTo') {
|
|
67
|
+
if (rel.options?.index) {
|
|
68
|
+
foreignKeyIndexes.push({ modelName: typename, fieldName: rel.foreignKey });
|
|
69
|
+
}
|
|
70
|
+
if (rel.options?.enrich) {
|
|
71
|
+
enrichmentPlan.push({
|
|
72
|
+
modelName: typename,
|
|
73
|
+
foreignKey: rel.foreignKey,
|
|
74
|
+
relationKey,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
} else if (rel.type === 'hasMany' || rel.type === 'hasOne') {
|
|
78
|
+
// For hasMany and hasOne, the foreign key lives on the target model,
|
|
79
|
+
// not the current one, so register the index on the target. Its wire
|
|
80
|
+
// type name is resolved from the schema here.
|
|
81
|
+
const targetDef = schema.models[rel.target];
|
|
82
|
+
const targetTypename = targetDef?.typename ?? rel.target;
|
|
83
|
+
foreignKeyIndexes.push({ modelName: targetTypename, fieldName: rel.foreignKey });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { enrichmentPlan, foreignKeyIndexes };
|
|
89
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { RuntimeContext } from '../RuntimeContext.js';
|
|
2
|
+
import type { SyncStatus } from '../storeContract.js';
|
|
3
|
+
|
|
4
|
+
export interface TerminalSessionLifecycleContext {
|
|
5
|
+
readonly runtime: RuntimeContext;
|
|
6
|
+
readonly listeners: Set<(error: Error) => void>;
|
|
7
|
+
purgeAuthenticatedState(): Promise<void>;
|
|
8
|
+
updateSyncStatus(updates: Partial<SyncStatus>): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Owns the one-way authenticated -> terminal transition. Cleanup is
|
|
13
|
+
* single-flight and listeners run only after the local boundary has settled.
|
|
14
|
+
*/
|
|
15
|
+
export class TerminalSessionLifecycle {
|
|
16
|
+
private inFlight: Promise<void> | null = null;
|
|
17
|
+
|
|
18
|
+
constructor(private readonly context: TerminalSessionLifecycleContext) {}
|
|
19
|
+
|
|
20
|
+
start(error: Error): void {
|
|
21
|
+
if (this.inFlight) return;
|
|
22
|
+
this.context.runtime.observability.captureWebSocketError({
|
|
23
|
+
context: 'session-error',
|
|
24
|
+
error: error.message,
|
|
25
|
+
});
|
|
26
|
+
this.context.updateSyncStatus({
|
|
27
|
+
state: 'error',
|
|
28
|
+
error,
|
|
29
|
+
isSessionError: true,
|
|
30
|
+
});
|
|
31
|
+
this.inFlight = this.run(error);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
settled(): Promise<void> | null {
|
|
35
|
+
return this.inFlight;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async run(sessionError: Error): Promise<void> {
|
|
39
|
+
let reportedError = sessionError;
|
|
40
|
+
try {
|
|
41
|
+
await this.context.purgeAuthenticatedState();
|
|
42
|
+
} catch (cleanupError) {
|
|
43
|
+
this.context.runtime.logger.error(
|
|
44
|
+
'Your session ended, but authenticated local data could not be completely removed.',
|
|
45
|
+
);
|
|
46
|
+
this.context.runtime.logger.debug(
|
|
47
|
+
'[TerminalSessionLifecycle] Local cleanup failed',
|
|
48
|
+
cleanupError,
|
|
49
|
+
);
|
|
50
|
+
reportedError =
|
|
51
|
+
cleanupError instanceof Error
|
|
52
|
+
? cleanupError
|
|
53
|
+
: new Error(String(cleanupError));
|
|
54
|
+
}
|
|
55
|
+
for (const listener of this.context.listeners) {
|
|
56
|
+
try {
|
|
57
|
+
listener(reportedError);
|
|
58
|
+
} catch (listenerError) {
|
|
59
|
+
this.context.runtime.logger.debug(
|
|
60
|
+
'[TerminalSessionLifecycle] Session-error listener failed',
|
|
61
|
+
listenerError,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moved to the settlement core with the duplex transport (ADR 0016): the
|
|
3
|
+
* inbound frame dispatch is the receiving half of the wire protocol, and the
|
|
4
|
+
* handlers already worked against the minimal `WsSession` port rather than
|
|
5
|
+
* the transport object. This path re-exports it so existing importers stay
|
|
6
|
+
* unchanged.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
isRecord,
|
|
11
|
+
readWsInboundFrame,
|
|
12
|
+
wsFrameHandlers,
|
|
13
|
+
dispatchWsFrame,
|
|
14
|
+
type PendingCommit,
|
|
15
|
+
type PendingClaim,
|
|
16
|
+
type PendingSubscription,
|
|
17
|
+
type WsInboundFrame,
|
|
18
|
+
type WsSession,
|
|
19
|
+
type WsFrameHandler,
|
|
20
|
+
} from '@abloatai/transaction/transport/wsFrameHandlers';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DurableWriteStore,
|
|
3
|
+
} from '@abloatai/transaction/durableWrites';
|
|
4
|
+
import type { PendingWrite } from '@abloatai/transaction/transactions/settlement/pendingWrite';
|
|
5
|
+
|
|
6
|
+
export interface CommitOutboxDatabase {
|
|
7
|
+
sealTransactionRecord(
|
|
8
|
+
record: PendingWrite,
|
|
9
|
+
consumedRecordIds: readonly string[],
|
|
10
|
+
): Promise<unknown>;
|
|
11
|
+
getPersistedTransactions(): Promise<readonly unknown[]>;
|
|
12
|
+
removeTransaction(id: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** IndexedDB-backed durable commit adapter owned by the human materialiser. */
|
|
16
|
+
export class DatabaseCommitOutboxStore implements DurableWriteStore {
|
|
17
|
+
constructor(private readonly database: CommitOutboxDatabase) {}
|
|
18
|
+
|
|
19
|
+
async seal(
|
|
20
|
+
envelope: PendingWrite,
|
|
21
|
+
consumedRecordIds: readonly string[],
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
await this.database.sealTransactionRecord(envelope, consumedRecordIds);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
list(): Promise<readonly unknown[]> {
|
|
27
|
+
return this.database.getPersistedTransactions();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
remove(envelopeRecordId: string): Promise<void> {
|
|
31
|
+
return this.database.removeTransaction(envelopeRecordId);
|
|
32
|
+
}
|
|
33
|
+
}
|