@abloatai/ablo 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -3
- package/CHANGELOG.md +34 -0
- package/README.md +104 -88
- package/dist/BaseSyncedStore.d.ts +140 -266
- package/dist/BaseSyncedStore.js +338 -739
- package/dist/Database.d.ts +62 -77
- package/dist/Database.js +106 -127
- package/dist/{ObjectPool.d.ts → InstanceCache.d.ts} +10 -13
- package/dist/{ObjectPool.js → InstanceCache.js} +91 -83
- package/dist/LazyReferenceCollection.d.ts +11 -15
- package/dist/LazyReferenceCollection.js +16 -15
- package/dist/Model.d.ts +37 -52
- package/dist/Model.js +52 -69
- package/dist/ModelRegistry.d.ts +46 -25
- package/dist/ModelRegistry.js +32 -30
- package/dist/NetworkMonitor.d.ts +5 -6
- package/dist/NetworkMonitor.js +6 -7
- package/dist/SyncClient.d.ts +119 -109
- package/dist/SyncClient.js +303 -224
- package/dist/SyncEngineContext.d.ts +1 -3
- package/dist/SyncEngineContext.js +1 -2
- package/dist/adapters/alwaysOnline.d.ts +6 -8
- package/dist/adapters/alwaysOnline.js +6 -8
- package/dist/adapters/inMemoryStorage.d.ts +9 -9
- package/dist/adapters/inMemoryStorage.js +9 -9
- package/dist/agent/Agent.d.ts +39 -31
- package/dist/agent/Agent.js +35 -23
- package/dist/agent/index.d.ts +4 -4
- package/dist/agent/index.js +5 -5
- package/dist/agent/session.d.ts +47 -44
- package/dist/agent/session.js +37 -48
- package/dist/agent/types.d.ts +26 -31
- package/dist/agent/types.js +6 -7
- package/dist/ai-sdk/coordinatedTool.d.ts +108 -0
- package/dist/ai-sdk/{coordinated-tool.js → coordinatedTool.js} +44 -38
- package/dist/ai-sdk/coordinationContext.d.ts +46 -0
- package/dist/ai-sdk/{coordination-context.js → coordinationContext.js} +30 -31
- package/dist/ai-sdk/index.d.ts +25 -22
- package/dist/ai-sdk/index.js +25 -22
- package/dist/ai-sdk/wrap.d.ts +7 -8
- package/dist/ai-sdk/wrap.js +2 -2
- package/dist/auth/credentialPolicy.d.ts +74 -71
- package/dist/auth/credentialPolicy.js +51 -56
- package/dist/auth/credentialSource.d.ts +7 -18
- package/dist/auth/credentialSource.js +10 -18
- package/dist/auth/index.d.ts +59 -58
- package/dist/auth/index.js +34 -40
- package/dist/auth/schemas.d.ts +5 -4
- package/dist/auth/schemas.js +5 -4
- package/dist/batching/index.d.ts +19 -21
- package/dist/batching/index.js +14 -17
- package/dist/cli.cjs +483 -369
- package/dist/client/Ablo.d.ts +107 -836
- package/dist/client/Ablo.js +174 -833
- package/dist/client/ApiClient.d.ts +44 -20
- package/dist/client/ApiClient.js +193 -44
- package/dist/client/auth.d.ts +51 -60
- package/dist/client/auth.js +137 -110
- package/dist/client/claimHeartbeatLoop.d.ts +50 -0
- package/dist/client/claimHeartbeatLoop.js +88 -0
- package/dist/client/consoleLogger.d.ts +35 -0
- package/dist/client/consoleLogger.js +44 -0
- package/dist/client/createInternalComponents.d.ts +14 -17
- package/dist/client/createInternalComponents.js +26 -31
- package/dist/client/createModelProxy.d.ts +130 -120
- package/dist/client/createModelProxy.js +158 -124
- package/dist/client/credentialEndpoint.d.ts +61 -0
- package/dist/client/credentialEndpoint.js +86 -0
- package/dist/client/functionalUpdate.d.ts +29 -27
- package/dist/client/functionalUpdate.js +21 -21
- package/dist/client/hostedEndpoints.d.ts +21 -0
- package/dist/client/hostedEndpoints.js +21 -0
- package/dist/client/httpClient.d.ts +58 -54
- package/dist/client/httpClient.js +29 -31
- package/dist/client/identity.d.ts +15 -20
- package/dist/client/identity.js +49 -59
- package/dist/client/modelRegistration.d.ts +10 -0
- package/dist/client/modelRegistration.js +301 -0
- package/dist/client/options.d.ts +373 -0
- package/dist/client/options.js +6 -0
- package/dist/client/registerDataSource.d.ts +9 -9
- package/dist/client/registerDataSource.js +15 -16
- package/dist/client/resourceTypes.d.ts +333 -0
- package/dist/client/resourceTypes.js +7 -0
- package/dist/client/schemaConfig.d.ts +44 -0
- package/dist/client/schemaConfig.js +176 -0
- package/dist/client/sessionMint.d.ts +17 -13
- package/dist/client/sessionMint.js +26 -31
- package/dist/client/validateAbloOptions.d.ts +12 -14
- package/dist/client/validateAbloOptions.js +9 -10
- package/dist/client/writeOptionsSchema.d.ts +18 -16
- package/dist/client/writeOptionsSchema.js +23 -20
- package/dist/client/wsMutationExecutor.d.ts +28 -0
- package/dist/client/wsMutationExecutor.js +71 -0
- package/dist/context.d.ts +6 -4
- package/dist/context.js +6 -7
- package/dist/coordination/index.d.ts +13 -4
- package/dist/coordination/index.js +29 -4
- package/dist/coordination/schema.d.ts +176 -128
- package/dist/coordination/schema.js +197 -133
- package/dist/coordination/trace.d.ts +9 -11
- package/dist/coordination/trace.js +13 -15
- package/dist/core/DatabaseManager.d.ts +5 -8
- package/dist/core/DatabaseManager.js +38 -40
- package/dist/core/QueryProcessor.d.ts +7 -9
- package/dist/core/QueryProcessor.js +27 -34
- package/dist/core/QueryView.d.ts +17 -5
- package/dist/core/QueryView.js +6 -7
- package/dist/core/StoreManager.d.ts +14 -16
- package/dist/core/StoreManager.js +26 -25
- package/dist/core/ViewRegistry.d.ts +5 -5
- package/dist/core/ViewRegistry.js +4 -4
- package/dist/core/index.d.ts +18 -13
- package/dist/core/index.js +32 -26
- package/dist/core/openIDBWithTimeout.d.ts +38 -36
- package/dist/core/openIDBWithTimeout.js +57 -54
- package/dist/core/queryUtils.d.ts +45 -0
- package/dist/core/queryUtils.js +69 -0
- package/dist/core/storeContract.d.ts +145 -0
- package/dist/core/storeContract.js +12 -0
- package/dist/environment.d.ts +28 -0
- package/dist/environment.js +21 -0
- package/dist/errorCodes.d.ts +118 -101
- package/dist/errorCodes.js +277 -260
- package/dist/errors.d.ts +170 -165
- package/dist/errors.js +161 -151
- package/dist/index.d.ts +30 -27
- package/dist/index.js +90 -82
- package/dist/interfaces/index.d.ts +108 -133
- package/dist/interfaces/index.js +5 -4
- package/dist/keys/index.d.ts +27 -29
- package/dist/keys/index.js +59 -49
- package/dist/mutators/RecordingTransaction.d.ts +16 -16
- package/dist/mutators/RecordingTransaction.js +31 -37
- package/dist/mutators/Transaction.d.ts +18 -26
- package/dist/mutators/Transaction.js +14 -20
- package/dist/mutators/UndoManager.d.ts +122 -131
- package/dist/mutators/UndoManager.js +149 -155
- package/dist/mutators/defineMutators.d.ts +24 -37
- package/dist/mutators/defineMutators.js +14 -20
- package/dist/mutators/inverseOp.d.ts +12 -15
- package/dist/mutators/inverseOp.js +12 -15
- package/dist/mutators/mutateActions.d.ts +10 -9
- package/dist/mutators/mutateActions.js +1 -1
- package/dist/mutators/readerActions.d.ts +9 -8
- package/dist/mutators/readerActions.js +2 -2
- package/dist/mutators/undoApply.d.ts +31 -27
- package/dist/mutators/undoApply.js +26 -24
- package/dist/policy/index.d.ts +5 -3
- package/dist/policy/index.js +5 -3
- package/dist/policy/types.d.ts +105 -101
- package/dist/policy/types.js +67 -66
- package/dist/query/client.d.ts +32 -16
- package/dist/query/client.js +103 -72
- package/dist/query/types.d.ts +37 -60
- package/dist/query/types.js +13 -33
- package/dist/react/AbloProvider.d.ts +7 -11
- package/dist/react/AbloProvider.js +24 -17
- package/dist/react/context.d.ts +27 -146
- package/dist/react/context.js +9 -10
- package/dist/react/index.d.ts +41 -42
- package/dist/react/index.js +37 -38
- package/dist/react/internalContext.d.ts +17 -19
- package/dist/react/useAblo.d.ts +23 -22
- package/dist/react/useAblo.js +17 -15
- package/dist/react/useCurrentUserId.d.ts +8 -7
- package/dist/react/useCurrentUserId.js +8 -7
- package/dist/react/useErrorListener.d.ts +7 -7
- package/dist/react/useErrorListener.js +11 -12
- package/dist/react/useMutationFailureListener.d.ts +8 -8
- package/dist/react/useMutationFailureListener.js +9 -9
- package/dist/react/useMutators.d.ts +11 -11
- package/dist/react/useMutators.js +10 -4
- package/dist/react/useReactive.js +2 -3
- package/dist/react/useSyncStatus.d.ts +4 -6
- package/dist/react/useUndoScope.d.ts +7 -9
- package/dist/react/useUndoScope.js +3 -3
- package/dist/schema/coordination.d.ts +21 -25
- package/dist/schema/coordination.js +21 -25
- package/dist/schema/ddl.d.ts +43 -39
- package/dist/schema/ddl.js +75 -68
- package/dist/schema/ddlLock.d.ts +35 -0
- package/dist/schema/ddlLock.js +46 -0
- package/dist/schema/diff.d.ts +99 -61
- package/dist/schema/diff.js +43 -34
- package/dist/schema/field.d.ts +37 -42
- package/dist/schema/field.js +36 -49
- package/dist/schema/generate.d.ts +12 -12
- package/dist/schema/generate.js +12 -12
- package/dist/schema/index.d.ts +5 -4
- package/dist/schema/index.js +29 -21
- package/dist/schema/model.d.ts +121 -146
- package/dist/schema/model.js +24 -35
- package/dist/schema/openapi.d.ts +10 -9
- package/dist/schema/openapi.js +7 -1
- package/dist/schema/queries.d.ts +30 -32
- package/dist/schema/queries.js +24 -25
- package/dist/schema/relation.d.ts +89 -99
- package/dist/schema/relation.js +13 -13
- package/dist/schema/residency.d.ts +38 -0
- package/dist/schema/residency.js +30 -0
- package/dist/schema/roles.d.ts +45 -27
- package/dist/schema/roles.js +52 -21
- package/dist/schema/schema.d.ts +36 -45
- package/dist/schema/schema.js +42 -39
- package/dist/schema/select.d.ts +13 -13
- package/dist/schema/select.js +13 -13
- package/dist/schema/serialize.d.ts +36 -39
- package/dist/schema/serialize.js +27 -31
- package/dist/schema/sugar.d.ts +17 -32
- package/dist/schema/sugar.js +14 -29
- package/dist/schema/{sync-delta-row.d.ts → syncDeltaRow.d.ts} +27 -50
- package/dist/schema/syncDeltaRow.js +89 -0
- package/dist/schema/tenancy.d.ts +44 -46
- package/dist/schema/tenancy.js +46 -48
- package/dist/server/adapter.d.ts +58 -58
- package/dist/server/adapter.js +13 -14
- package/dist/server/commit.d.ts +60 -64
- package/dist/server/index.d.ts +9 -10
- package/dist/server/index.js +1 -1
- package/dist/server/readConfig.d.ts +70 -0
- package/dist/server/readConfig.js +8 -0
- package/dist/server/storageMode.d.ts +23 -0
- package/dist/server/storageMode.js +17 -0
- package/dist/source/adapter.d.ts +31 -26
- package/dist/source/adapter.js +10 -10
- package/dist/source/adapters/drizzle.d.ts +28 -23
- package/dist/source/adapters/drizzle.js +34 -28
- package/dist/source/adapters/kysely.d.ts +27 -25
- package/dist/source/adapters/kysely.js +28 -26
- package/dist/source/adapters/memory.d.ts +8 -7
- package/dist/source/adapters/memory.js +10 -9
- package/dist/source/adapters/prisma.d.ts +13 -12
- package/dist/source/adapters/prisma.js +27 -29
- package/dist/source/conformance.d.ts +18 -11
- package/dist/source/conformance.js +27 -19
- package/dist/source/connector.d.ts +31 -32
- package/dist/source/connector.js +30 -28
- package/dist/source/connectorProtocol.d.ts +160 -0
- package/dist/source/connectorProtocol.js +162 -0
- package/dist/source/contract.d.ts +26 -27
- package/dist/source/contract.js +28 -29
- package/dist/source/factory.d.ts +94 -0
- package/dist/source/factory.js +268 -0
- package/dist/source/index.d.ts +10 -462
- package/dist/source/index.js +17 -421
- package/dist/source/migrations.d.ts +9 -9
- package/dist/source/migrations.js +9 -9
- package/dist/source/next.d.ts +10 -11
- package/dist/source/next.js +7 -8
- package/dist/source/pushQueue.d.ts +70 -48
- package/dist/source/pushQueue.js +36 -29
- package/dist/source/signing.d.ts +88 -0
- package/dist/source/signing.js +159 -0
- package/dist/source/types.d.ts +351 -0
- package/dist/source/types.js +43 -0
- package/dist/stores/ObjectStore.d.ts +11 -12
- package/dist/stores/ObjectStore.js +34 -35
- package/dist/stores/ObjectStoreContract.d.ts +12 -15
- package/dist/stores/SyncActionStore.d.ts +8 -12
- package/dist/stores/SyncActionStore.js +77 -46
- package/dist/surface.d.ts +28 -21
- package/dist/surface.js +28 -20
- package/dist/sync/{BootstrapHelper.d.ts → BootstrapFetcher.d.ts} +37 -45
- package/dist/sync/{BootstrapHelper.js → BootstrapFetcher.js} +101 -80
- package/dist/sync/ConnectionManager.d.ts +47 -50
- package/dist/sync/ConnectionManager.js +74 -70
- package/dist/sync/NetworkProbe.d.ts +27 -31
- package/dist/sync/NetworkProbe.js +67 -72
- package/dist/sync/{HydrationCoordinator.d.ts → OnDemandLoader.d.ts} +49 -36
- package/dist/sync/{HydrationCoordinator.js → OnDemandLoader.js} +79 -54
- package/dist/sync/{AreaOfInterestManager.d.ts → SubscriptionManager.d.ts} +45 -59
- package/dist/sync/{AreaOfInterestManager.js → SubscriptionManager.js} +44 -52
- package/dist/sync/SyncWebSocket.d.ts +175 -250
- package/dist/sync/SyncWebSocket.js +431 -769
- package/dist/sync/awaitClaimGrant.d.ts +18 -18
- package/dist/sync/awaitClaimGrant.js +38 -30
- package/dist/sync/bootstrapApply.d.ts +70 -0
- package/dist/sync/bootstrapApply.js +73 -0
- package/dist/sync/commitFrames.d.ts +44 -0
- package/dist/sync/commitFrames.js +94 -0
- package/dist/sync/createClaimStream.d.ts +23 -22
- package/dist/sync/createClaimStream.js +108 -25
- package/dist/sync/createPresenceStream.d.ts +19 -18
- package/dist/sync/createPresenceStream.js +25 -26
- package/dist/sync/createSnapshot.d.ts +13 -17
- package/dist/sync/createSnapshot.js +20 -26
- package/dist/sync/credentialLifecycle.d.ts +175 -0
- package/dist/sync/credentialLifecycle.js +322 -0
- package/dist/sync/deltaPipeline.d.ts +113 -0
- package/dist/sync/deltaPipeline.js +261 -0
- package/dist/sync/groupChange.d.ts +113 -0
- package/dist/sync/groupChange.js +242 -0
- package/dist/sync/heartbeat.d.ts +63 -0
- package/dist/sync/heartbeat.js +91 -0
- package/dist/sync/participants.d.ts +27 -27
- package/dist/sync/schemas.d.ts +3 -2
- package/dist/sync/schemas.js +14 -10
- package/dist/sync/syncCursor.d.ts +40 -0
- package/dist/sync/syncCursor.js +55 -0
- package/dist/sync/syncPlan.d.ts +54 -0
- package/dist/sync/syncPlan.js +50 -0
- package/dist/sync/syncPosition.d.ts +54 -49
- package/dist/sync/syncPosition.js +57 -52
- package/dist/sync/wsFrameHandlers.d.ts +116 -0
- package/dist/sync/wsFrameHandlers.js +374 -0
- package/dist/testing/fixtures/bootstrap.d.ts +21 -17
- package/dist/testing/fixtures/bootstrap.js +12 -6
- package/dist/testing/fixtures/deltas.d.ts +31 -34
- package/dist/testing/fixtures/deltas.js +30 -33
- package/dist/testing/fixtures/models.d.ts +11 -10
- package/dist/testing/fixtures/models.js +12 -10
- package/dist/testing/helpers/{react-wrapper.d.ts → reactWrapper.d.ts} +13 -10
- package/dist/testing/helpers/{react-wrapper.js → reactWrapper.js} +15 -12
- package/dist/testing/helpers/{sync-engine-harness.d.ts → syncEngineHarness.d.ts} +17 -18
- package/dist/testing/helpers/{sync-engine-harness.js → syncEngineHarness.js} +14 -11
- package/dist/testing/helpers/wait.d.ts +13 -8
- package/dist/testing/helpers/wait.js +13 -8
- package/dist/testing/index.d.ts +4 -4
- package/dist/testing/index.js +3 -3
- package/dist/testing/mocks/MockMutationExecutor.d.ts +18 -17
- package/dist/testing/mocks/MockMutationExecutor.js +15 -14
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +8 -8
- package/dist/testing/mocks/MockNetworkMonitor.js +8 -8
- package/dist/testing/mocks/MockSyncContext.d.ts +21 -34
- package/dist/testing/mocks/MockSyncContext.js +16 -45
- package/dist/testing/mocks/MockSyncStore.d.ts +14 -14
- package/dist/testing/mocks/MockSyncStore.js +11 -11
- package/dist/testing/mocks/MockWebSocket.d.ts +28 -24
- package/dist/testing/mocks/MockWebSocket.js +22 -21
- package/dist/transactions/TransactionQueue.d.ts +190 -221
- package/dist/transactions/TransactionQueue.js +424 -822
- package/dist/transactions/TransactionStore.d.ts +20 -0
- package/dist/transactions/TransactionStore.js +53 -0
- package/dist/transactions/UnconfirmedWrites.d.ts +82 -0
- package/dist/transactions/UnconfirmedWrites.js +104 -0
- package/dist/transactions/coalesceRules.d.ts +58 -0
- package/dist/transactions/coalesceRules.js +140 -0
- package/dist/transactions/commitPayload.d.ts +130 -0
- package/dist/transactions/commitPayload.js +143 -0
- package/dist/transactions/deltaConfirmation.d.ts +58 -0
- package/dist/transactions/deltaConfirmation.js +215 -0
- package/dist/transactions/optimisticApply.d.ts +49 -0
- package/dist/transactions/optimisticApply.js +65 -0
- package/dist/transactions/replayValidation.d.ts +99 -0
- package/dist/transactions/replayValidation.js +111 -0
- package/dist/types/global.d.ts +46 -41
- package/dist/types/global.js +20 -19
- package/dist/types/index.d.ts +74 -80
- package/dist/types/index.js +22 -27
- package/dist/types/modelData.d.ts +10 -0
- package/dist/types/modelData.js +9 -0
- package/dist/types/participant.d.ts +20 -0
- package/dist/types/participant.js +10 -0
- package/dist/types/streams.d.ts +216 -209
- package/dist/types/streams.js +7 -7
- package/dist/utils/asyncIterator.d.ts +25 -32
- package/dist/utils/asyncIterator.js +25 -32
- package/dist/utils/duration.d.ts +12 -15
- package/dist/utils/duration.js +12 -15
- package/dist/utils/mobxSetup.d.ts +53 -0
- package/dist/utils/{mobx-setup.js → mobxSetup.js} +44 -100
- package/dist/webhooks/events.d.ts +21 -16
- package/dist/webhooks/events.js +10 -8
- package/dist/webhooks/index.d.ts +5 -7
- package/dist/webhooks/index.js +5 -7
- package/dist/{schema/sync-delta-wire.d.ts → wire/delta.d.ts} +58 -41
- package/dist/wire/delta.js +114 -0
- package/dist/wire/errorEnvelope.d.ts +35 -27
- package/dist/wire/errorEnvelope.js +38 -32
- package/dist/wire/frames.d.ts +150 -67
- package/dist/wire/frames.js +48 -1
- package/dist/wire/index.d.ts +18 -13
- package/dist/wire/index.js +36 -13
- package/dist/wire/listEnvelope.d.ts +16 -23
- package/dist/wire/listEnvelope.js +7 -6
- package/dist/wire/protocol.d.ts +38 -0
- package/dist/wire/protocol.js +38 -0
- package/dist/wire/protocolVersion.d.ts +60 -0
- package/dist/wire/protocolVersion.js +67 -0
- package/docs/api-keys.md +4 -3
- package/docs/coordination.md +59 -0
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/identity.md +4 -4
- package/docs/integration-guide.md +1 -1
- package/docs/react.md +1 -1
- package/docs/sessions.md +5 -7
- package/package.json +24 -21
- package/dist/ai-sdk/coordinated-tool.d.ts +0 -101
- package/dist/ai-sdk/coordination-context.d.ts +0 -52
- package/dist/client/index.d.ts +0 -36
- package/dist/client/index.js +0 -33
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -12
- package/dist/core/query-utils.d.ts +0 -34
- package/dist/core/query-utils.js +0 -59
- package/dist/interfaces/headless.d.ts +0 -95
- package/dist/interfaces/headless.js +0 -41
- package/dist/query/index.d.ts +0 -6
- package/dist/query/index.js +0 -5
- package/dist/realtime/index.d.ts +0 -10
- package/dist/realtime/index.js +0 -9
- package/dist/schema/plane.d.ts +0 -23
- package/dist/schema/plane.js +0 -19
- package/dist/schema/sync-delta-row.js +0 -103
- package/dist/schema/sync-delta-wire.js +0 -102
- package/dist/server/next.d.ts +0 -51
- package/dist/server/next.js +0 -47
- package/dist/server/read-config.d.ts +0 -67
- package/dist/server/read-config.js +0 -8
- package/dist/server/storage-mode.d.ts +0 -1
- package/dist/server/storage-mode.js +0 -18
- package/dist/source/connector-protocol.d.ts +0 -159
- package/dist/source/connector-protocol.js +0 -161
- package/dist/sync/OfflineFlush.d.ts +0 -9
- package/dist/sync/OfflineFlush.js +0 -22
- package/dist/sync/OfflineTransactionStore.d.ts +0 -37
- package/dist/sync/OfflineTransactionStore.js +0 -263
- package/dist/transactions/OptimisticEchoTracker.d.ts +0 -82
- package/dist/transactions/OptimisticEchoTracker.js +0 -104
- package/dist/transactions/index.d.ts +0 -16
- package/dist/transactions/index.js +0 -7
- package/dist/transactions/mutation-error-handler.d.ts +0 -5
- package/dist/transactions/mutation-error-handler.js +0 -39
- package/dist/utils/mobx-setup.d.ts +0 -42
|
@@ -1,61 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Records where this client stands in the global delta order. It is a single
|
|
3
|
+
* typed object holding three related but distinct positions, each with its own
|
|
4
|
+
* rule for when it may advance. Keeping them separate is deliberate: collapsing
|
|
5
|
+
* them into one counter is a classic source of sync bugs.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* - `persisted` — the resume cursor. It advances only after deltas have
|
|
8
|
+
* committed to durable local storage. This is the value reconnect catch-up
|
|
9
|
+
* sends to the server, so it must never run ahead of what actually landed
|
|
10
|
+
* on disk; otherwise the server would skip deltas the client never stored.
|
|
9
11
|
*
|
|
10
|
-
* - `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* - `applied` — the in-memory cursor: the last delta applied to the object
|
|
13
|
+
* pool. It drives the guards that deduplicate and reject replayed deltas.
|
|
14
|
+
* It may run ahead of `persisted`, because the pool is updated before the
|
|
15
|
+
* flush to disk, and behind what has merely been received, because
|
|
16
|
+
* bootstrap-queued deltas arrive before they are applied.
|
|
15
17
|
*
|
|
16
|
-
* - `
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* - `acked` — the highest server position acknowledged for this client's own
|
|
19
|
+
* commits. An acknowledgement at N means the server applied our write at N;
|
|
20
|
+
* the optimistic pool already reflects it, so for the entities we wrote we
|
|
21
|
+
* have effectively read through N even before the echo returns on the
|
|
22
|
+
* stream.
|
|
20
23
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
24
|
+
* One value is derived: `readFloor` is the greater of `applied` and `acked`,
|
|
25
|
+
* and is the only position a snapshot or claim should stamp as its read point.
|
|
26
|
+
* Using the raw stream cursor alone would make a claim taken right after a
|
|
27
|
+
* confirmed write look stale against that write's own delta; using the raw
|
|
28
|
+
* acknowledgement alone would be wrong for read-only clients. The maximum is
|
|
29
|
+
* correct per entity, because a competing change to an entity we just wrote
|
|
30
|
+
* necessarily lands above our acknowledgement and still rejects as stale.
|
|
25
31
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* ABOVE our ack and still stale-rejects.
|
|
32
|
-
*
|
|
33
|
-
* The Zod schema IS the state shape — the class holds exactly one
|
|
34
|
-
* `SyncPositionSnapshot` and applies monotonic merges to it, so
|
|
35
|
-
* snapshot/restore are identity-shaped and the schema is the single gate
|
|
36
|
-
* for anything loaded from disk (`parseSyncPosition`; a corrupted stored
|
|
37
|
-
* cursor "ahead of reality" is an existing, known failure mode).
|
|
32
|
+
* The validation schema is the state shape: the class holds exactly one
|
|
33
|
+
* {@link SyncPositionSnapshot} and merges monotonically into it, so snapshot
|
|
34
|
+
* and restore share that shape and {@link parseSyncPosition} is the single gate
|
|
35
|
+
* for anything loaded from disk — a corrupted cursor stored "ahead of reality"
|
|
36
|
+
* being a known failure mode.
|
|
38
37
|
*/
|
|
39
38
|
import { z } from 'zod';
|
|
40
39
|
export const syncPositionSchema = z.object({
|
|
41
|
-
/**
|
|
40
|
+
/** The resume cursor; advances only after deltas persist to durable local storage. */
|
|
42
41
|
persisted: z.number().int().nonnegative(),
|
|
43
|
-
/**
|
|
42
|
+
/** The in-memory cursor: the last delta applied to the object pool. */
|
|
44
43
|
applied: z.number().int().nonnegative(),
|
|
45
|
-
/**
|
|
44
|
+
/** The highest server position acknowledged for this client's own commits. */
|
|
46
45
|
acked: z.number().int().nonnegative(),
|
|
47
46
|
});
|
|
48
47
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
* Only the `persisted` cursor is stored durably; `applied` and `acked` are
|
|
49
|
+
* not. On resume the object pool is rebuilt from the persisted state, so the
|
|
50
|
+
* correct restore is simply to advance `persisted` to the stored value, which
|
|
51
|
+
* also implies `applied`. `acked` starts at zero, because a past session's
|
|
52
|
+
* acknowledgements carry no read authority and the offline queue
|
|
53
|
+
* re-acknowledges its own replays.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* Validates an untrusted value, such as one loaded from disk, into a position
|
|
57
|
+
* snapshot, or returns null when it does not match the schema.
|
|
57
58
|
*/
|
|
58
|
-
/** Validate a persisted/foreign value into a position snapshot. */
|
|
59
59
|
export function parseSyncPosition(value) {
|
|
60
60
|
const result = syncPositionSchema.safeParse(value);
|
|
61
61
|
return result.success ? result.data : null;
|
|
@@ -69,11 +69,13 @@ function advance(state, next) {
|
|
|
69
69
|
acked: Math.max(state.acked, next.acked ?? 0),
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
73
|
-
*
|
|
72
|
+
/**
|
|
73
|
+
* The live sync position: one instance per client. Three producers each
|
|
74
|
+
* advance their own cursor, and consumers read the result.
|
|
75
|
+
*/
|
|
74
76
|
export class SyncPosition {
|
|
75
77
|
#state = ZERO;
|
|
76
|
-
/**
|
|
78
|
+
/** Returns a copy of the current state in the schema's shape. */
|
|
77
79
|
snapshot() {
|
|
78
80
|
return { ...this.#state };
|
|
79
81
|
}
|
|
@@ -86,25 +88,28 @@ export class SyncPosition {
|
|
|
86
88
|
get acked() {
|
|
87
89
|
return this.#state.acked;
|
|
88
90
|
}
|
|
89
|
-
/**
|
|
91
|
+
/** The position a snapshot or claim stamps as its read point: the greater of `applied` and `acked`. */
|
|
90
92
|
get readFloor() {
|
|
91
93
|
return Math.max(this.#state.applied, this.#state.acked);
|
|
92
94
|
}
|
|
93
|
-
/**
|
|
94
|
-
*
|
|
95
|
+
/**
|
|
96
|
+
* Records that deltas through `syncId` have committed to durable local
|
|
97
|
+
* storage. This also advances `applied`, since the flush applies each delta
|
|
98
|
+
* before or as it persists.
|
|
99
|
+
*/
|
|
95
100
|
advancePersisted(syncId) {
|
|
96
101
|
this.#state = advance(this.#state, { persisted: syncId, applied: syncId });
|
|
97
102
|
}
|
|
98
|
-
/**
|
|
103
|
+
/** Records that a delta was applied to the in-memory object pool. */
|
|
99
104
|
advanceApplied(syncId) {
|
|
100
105
|
this.#state = advance(this.#state, { applied: syncId });
|
|
101
106
|
}
|
|
102
|
-
/**
|
|
107
|
+
/** Records that the server acknowledged one of this client's commits at the given position. */
|
|
103
108
|
noteAck(lastSyncId) {
|
|
104
109
|
if (lastSyncId !== undefined)
|
|
105
110
|
this.#state = advance(this.#state, { acked: lastSyncId });
|
|
106
111
|
}
|
|
107
|
-
/**
|
|
112
|
+
/** Restores from an already-validated snapshot, for example on resume from disk. The merge is monotonic. */
|
|
108
113
|
restore(snapshot) {
|
|
109
114
|
this.#state = advance(this.#state, snapshot);
|
|
110
115
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes each inbound frame from the sync WebSocket to the handler for its
|
|
3
|
+
* type. Handlers work against a minimal {@link WsSession} interface — only
|
|
4
|
+
* the members they actually touch, rather than the transport object itself,
|
|
5
|
+
* which keeps this module free of an import cycle. Reading a message off the
|
|
6
|
+
* socket, parsing its JSON, and tracking heartbeats all happen before this
|
|
7
|
+
* point; every parsed frame then passes through {@link dispatchWsFrame}.
|
|
8
|
+
*/
|
|
9
|
+
import { type CommitAck } from './commitFrames.js';
|
|
10
|
+
/**
|
|
11
|
+
* In-flight `commit` request record, keyed by clientTxId in the session.
|
|
12
|
+
* Resolved when a matching `mutation_result` frame arrives from the
|
|
13
|
+
* server, or rejected on timeout / disconnect.
|
|
14
|
+
*/
|
|
15
|
+
export interface PendingCommit {
|
|
16
|
+
resolve: (value: CommitAck) => void;
|
|
17
|
+
reject: (err: Error) => void;
|
|
18
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* In-flight `claim` request record, keyed by claimId. Resolved when the
|
|
22
|
+
* matching `claim_ack` arrives, or rejected on timeout/disconnect.
|
|
23
|
+
*/
|
|
24
|
+
export interface PendingClaim {
|
|
25
|
+
resolve: (value: {
|
|
26
|
+
syncGroups: string[];
|
|
27
|
+
ttlSeconds?: number;
|
|
28
|
+
}) => void;
|
|
29
|
+
reject: (err: Error) => void;
|
|
30
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* An in-flight `update_subscription` request awaiting its
|
|
34
|
+
* `subscription_ack`. The wire carries no correlation id, so requests are
|
|
35
|
+
* matched to their acknowledgements in first-in, first-out order, the same
|
|
36
|
+
* order the server applies them.
|
|
37
|
+
*/
|
|
38
|
+
export interface PendingSubscription {
|
|
39
|
+
resolve: (value: {
|
|
40
|
+
syncGroups: string[];
|
|
41
|
+
}) => void;
|
|
42
|
+
reject: (err: Error) => void;
|
|
43
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A parsed inbound wire frame, straight from `JSON.parse`. The data is
|
|
47
|
+
* untrusted: every payload is loosely typed, and each handler narrows it
|
|
48
|
+
* defensively before use.
|
|
49
|
+
*/
|
|
50
|
+
export interface WsInboundFrame {
|
|
51
|
+
type?: string;
|
|
52
|
+
payload?: unknown;
|
|
53
|
+
/** Some delta frames carry their delta fields at the top level rather than under `payload`. */
|
|
54
|
+
actionType?: unknown;
|
|
55
|
+
modelName?: unknown;
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
/** Narrow arbitrary wire data to a plain string-keyed record. */
|
|
59
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
60
|
+
/**
|
|
61
|
+
* A type guard for the parsed result of an inbound message. A frame is any
|
|
62
|
+
* plain object whose `type`, when present, is a string. Validating the
|
|
63
|
+
* payload itself is left to each handler; delta payloads, for example, are
|
|
64
|
+
* checked against the canonical delta schema before they are applied.
|
|
65
|
+
*/
|
|
66
|
+
export declare function isWsInboundFrame(value: unknown): value is WsInboundFrame;
|
|
67
|
+
/**
|
|
68
|
+
* The subset of the sync WebSocket that the frame handlers need. The
|
|
69
|
+
* transport builds a single object exposing these members over its own
|
|
70
|
+
* private state. Handlers read the fields live rather than capturing them,
|
|
71
|
+
* so resetting a field elsewhere — such as clearing pending subscriptions
|
|
72
|
+
* on close — never leaves a handler holding a stale value.
|
|
73
|
+
*/
|
|
74
|
+
export interface WsSession {
|
|
75
|
+
/** EventEmitter surface — handlers emit the typed transport events. */
|
|
76
|
+
emit(event: string, ...args: unknown[]): boolean;
|
|
77
|
+
/** In-flight commit acks keyed by clientTxId. */
|
|
78
|
+
pendingMutations: Map<string, PendingCommit>;
|
|
79
|
+
/** In-flight claim acks keyed by claimId. */
|
|
80
|
+
pendingClaims: Map<string, PendingClaim>;
|
|
81
|
+
/** Removes and returns the oldest in-flight `update_subscription` request. */
|
|
82
|
+
shiftPendingSubscription(): PendingSubscription | undefined;
|
|
83
|
+
/** Connection options subset the handlers write back (acked sync groups). */
|
|
84
|
+
options: {
|
|
85
|
+
syncGroups: string[];
|
|
86
|
+
};
|
|
87
|
+
/** Registered collaboration event keys (colon format). */
|
|
88
|
+
collaborationEventTypes: ReadonlySet<string>;
|
|
89
|
+
/**
|
|
90
|
+
* Processes one inbound delta. The argument is untrusted wire data; the
|
|
91
|
+
* transport validates it against the canonical delta schema and drops
|
|
92
|
+
* anything malformed, so handlers here never cast.
|
|
93
|
+
*/
|
|
94
|
+
handleDelta(delta: unknown): void;
|
|
95
|
+
handleSyncResponse(payload: unknown): void;
|
|
96
|
+
handleBootstrapResponse(payload: unknown): void;
|
|
97
|
+
handlePresenceUpdate(message: {
|
|
98
|
+
payload?: unknown;
|
|
99
|
+
[k: string]: unknown;
|
|
100
|
+
}): void;
|
|
101
|
+
}
|
|
102
|
+
export type WsFrameHandler = (session: WsSession, message: WsInboundFrame) => void;
|
|
103
|
+
/**
|
|
104
|
+
* Maps each frame type to its handler. Every named server frame this
|
|
105
|
+
* package understands is dispatched from this table; anything else falls
|
|
106
|
+
* through to the collaboration-event and unknown-type path in
|
|
107
|
+
* {@link dispatchWsFrame}.
|
|
108
|
+
*/
|
|
109
|
+
export declare const wsFrameHandlers: Record<string, WsFrameHandler>;
|
|
110
|
+
/**
|
|
111
|
+
* Routes one parsed inbound frame to its handler. Keepalive frames are
|
|
112
|
+
* ignored, a missing `type` is treated as a bare delta, and any unknown
|
|
113
|
+
* type falls through to the collaboration-event map, whose wire names use
|
|
114
|
+
* underscores and whose event keys use colons.
|
|
115
|
+
*/
|
|
116
|
+
export declare function dispatchWsFrame(session: WsSession, message: WsInboundFrame): void;
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes each inbound frame from the sync WebSocket to the handler for its
|
|
3
|
+
* type. Handlers work against a minimal {@link WsSession} interface — only
|
|
4
|
+
* the members they actually touch, rather than the transport object itself,
|
|
5
|
+
* which keeps this module free of an import cycle. Reading a message off the
|
|
6
|
+
* socket, parsing its JSON, and tracking heartbeats all happen before this
|
|
7
|
+
* point; every parsed frame then passes through {@link dispatchWsFrame}.
|
|
8
|
+
*/
|
|
9
|
+
import { getContext } from '../context.js';
|
|
10
|
+
import { CapabilityError, errorFromWire, } from '../errors.js';
|
|
11
|
+
import { subscriptionAckPayloadSchema } from '../coordination/schema.js';
|
|
12
|
+
import { formatConflict } from '../coordination/trace.js';
|
|
13
|
+
import { parseNotifications, recordClaim } from './commitFrames.js';
|
|
14
|
+
/** Narrow arbitrary wire data to a plain string-keyed record. */
|
|
15
|
+
export function isRecord(value) {
|
|
16
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A type guard for the parsed result of an inbound message. A frame is any
|
|
20
|
+
* plain object whose `type`, when present, is a string. Validating the
|
|
21
|
+
* payload itself is left to each handler; delta payloads, for example, are
|
|
22
|
+
* checked against the canonical delta schema before they are applied.
|
|
23
|
+
*/
|
|
24
|
+
export function isWsInboundFrame(value) {
|
|
25
|
+
if (!isRecord(value))
|
|
26
|
+
return false;
|
|
27
|
+
const type = value.type;
|
|
28
|
+
return type === undefined || typeof type === 'string';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Handles the acknowledgement of a `commit` request. The canonical wire
|
|
32
|
+
* shape is `MutationResultMessage`. The payload is parsed defensively
|
|
33
|
+
* rather than cast, since it is untrusted and may be malformed or sent by
|
|
34
|
+
* an older server.
|
|
35
|
+
*/
|
|
36
|
+
const handleMutationResult = (session, message) => {
|
|
37
|
+
const p = (message.payload ?? message);
|
|
38
|
+
const { clientTxId, success, lastSyncId, error } = p ?? {};
|
|
39
|
+
// Defensive: validate notifications against the canonical schema —
|
|
40
|
+
// untrusted wire data from a possibly-older/newer server.
|
|
41
|
+
const notifications = parseNotifications(p?.notifications);
|
|
42
|
+
const pending = typeof clientTxId === 'string'
|
|
43
|
+
? session.pendingMutations.get(clientTxId)
|
|
44
|
+
: undefined;
|
|
45
|
+
if (!pending)
|
|
46
|
+
return;
|
|
47
|
+
clearTimeout(pending.timeout);
|
|
48
|
+
// `pending` exists ⇒ clientTxId was a string key (the guard above).
|
|
49
|
+
session.pendingMutations.delete(clientTxId);
|
|
50
|
+
if (success) {
|
|
51
|
+
// Coerce defensively — bigint columns serialize as strings
|
|
52
|
+
// from older servers (see normalizeWireDelta).
|
|
53
|
+
const ackedSyncId = Number(lastSyncId);
|
|
54
|
+
// The write succeeded, but a guarded premise shifted underneath it.
|
|
55
|
+
// Emit the advisory signal so a caller can react, and still resolve
|
|
56
|
+
// the receipt, since the commit itself went through.
|
|
57
|
+
if (notifications && notifications.length > 0) {
|
|
58
|
+
const txId = typeof clientTxId === 'string' ? clientTxId : '';
|
|
59
|
+
const event = {
|
|
60
|
+
clientTxId: txId,
|
|
61
|
+
rows: notifications.map((n) => ({
|
|
62
|
+
model: n.model,
|
|
63
|
+
id: n.id,
|
|
64
|
+
fields: n.conflictingFields,
|
|
65
|
+
writtenBy: n.writtenBy?.kind,
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
const message = formatConflict(event);
|
|
69
|
+
const ctx = getContext();
|
|
70
|
+
ctx.logger.warn(message);
|
|
71
|
+
ctx.observability.breadcrumb(message, 'sync.coordination', 'warning');
|
|
72
|
+
ctx.observability.captureConflict(event);
|
|
73
|
+
session.emit('conflict:notified', {
|
|
74
|
+
clientTxId: txId,
|
|
75
|
+
notifications,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
pending.resolve({
|
|
79
|
+
lastSyncId: Number.isFinite(ackedSyncId) ? ackedSyncId : 0,
|
|
80
|
+
...(notifications && notifications.length > 0
|
|
81
|
+
? { notifications }
|
|
82
|
+
: {}),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Capture the full server error so the caller can see what actually
|
|
87
|
+
// rejected the mutation, rather than a generic "mutation failed on
|
|
88
|
+
// server". Object errors are stringified so structured server payloads,
|
|
89
|
+
// such as validation issues, survive being wrapped in an Error.
|
|
90
|
+
let errorMessage;
|
|
91
|
+
let errorCode;
|
|
92
|
+
let requiredCapability;
|
|
93
|
+
if (typeof error === 'string') {
|
|
94
|
+
errorMessage = error;
|
|
95
|
+
}
|
|
96
|
+
else if (error != null && typeof error === 'object') {
|
|
97
|
+
const obj = error;
|
|
98
|
+
if (typeof obj.code === 'string')
|
|
99
|
+
errorCode = obj.code;
|
|
100
|
+
if (typeof obj.message === 'string') {
|
|
101
|
+
errorMessage = obj.message;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
try {
|
|
105
|
+
errorMessage = JSON.stringify(error);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
errorMessage = String(error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (obj.requiredCapability != null &&
|
|
112
|
+
typeof obj.requiredCapability === 'object' &&
|
|
113
|
+
typeof obj.requiredCapability.scope === 'string') {
|
|
114
|
+
requiredCapability = obj.requiredCapability;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
errorMessage = 'mutation failed on server';
|
|
119
|
+
}
|
|
120
|
+
// A stale-context rejection (the write read state that has since
|
|
121
|
+
// changed) or a foreign-claim conflict is a coordination collision.
|
|
122
|
+
// The success-with-notifications path above records the conflict, and a
|
|
123
|
+
// hard rejection must record it too, or the collision count would miss
|
|
124
|
+
// every rejected write. The conflicting rows ride along on the typed
|
|
125
|
+
// error's `conflicts` detail.
|
|
126
|
+
if (errorCode === 'stale_context' ||
|
|
127
|
+
errorCode === 'claim_conflict' ||
|
|
128
|
+
errorCode === 'entity_claimed' ||
|
|
129
|
+
errorCode?.startsWith('policy:') === true) {
|
|
130
|
+
const rawConflicts = error != null &&
|
|
131
|
+
typeof error === 'object' &&
|
|
132
|
+
Array.isArray(error.conflicts)
|
|
133
|
+
? error
|
|
134
|
+
.conflicts
|
|
135
|
+
: [];
|
|
136
|
+
const conflictEvent = {
|
|
137
|
+
clientTxId: typeof clientTxId === 'string' ? clientTxId : '',
|
|
138
|
+
rows: rawConflicts.map((r) => ({
|
|
139
|
+
model: typeof r.model === 'string' ? r.model : 'unknown',
|
|
140
|
+
id: typeof r.id === 'string' ? r.id : 'unknown',
|
|
141
|
+
fields: [],
|
|
142
|
+
})),
|
|
143
|
+
};
|
|
144
|
+
const ctx = getContext();
|
|
145
|
+
ctx.observability.breadcrumb(formatConflict(conflictEvent), 'sync.coordination', 'warning');
|
|
146
|
+
ctx.observability.captureConflict(conflictEvent);
|
|
147
|
+
}
|
|
148
|
+
// Build the proper typed AbloError from the wire code via the
|
|
149
|
+
// shared factory — the same code→class mapping the HTTP commit
|
|
150
|
+
// path uses (`translateHttpError`). This keeps rejected commits
|
|
151
|
+
// inside the typed hierarchy (capability denials →
|
|
152
|
+
// CapabilityError with `.requiredCapability`; foreign-claim
|
|
153
|
+
// conflicts → AbloClaimedError; everything else → the subclass
|
|
154
|
+
// its registry `httpStatus` implies) instead of a hand-rolled
|
|
155
|
+
// `new Error`, so callers can `instanceof`/`e.type` it and
|
|
156
|
+
// downstream retry logic can read the contract's retryability.
|
|
157
|
+
pending.reject(errorFromWire(errorMessage, {
|
|
158
|
+
code: errorCode,
|
|
159
|
+
requiredCapability,
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Handles the acknowledgement of a `claim` request. The frame has the shape
|
|
165
|
+
* `{ type: 'claim_ack', payload: { claimId, success, syncGroups?,
|
|
166
|
+
* ttlSeconds?, error? } }`.
|
|
167
|
+
*/
|
|
168
|
+
const handleClaimAck = (session, message) => {
|
|
169
|
+
const p = (message.payload ?? {});
|
|
170
|
+
const { claimId, success, syncGroups, ttlSeconds, error } = p;
|
|
171
|
+
const pending = typeof claimId === 'string'
|
|
172
|
+
? session.pendingClaims.get(claimId)
|
|
173
|
+
: undefined;
|
|
174
|
+
if (!pending)
|
|
175
|
+
return;
|
|
176
|
+
clearTimeout(pending.timeout);
|
|
177
|
+
// `pending` exists ⇒ claimId was a string key (the guard above).
|
|
178
|
+
session.pendingClaims.delete(claimId);
|
|
179
|
+
if (success) {
|
|
180
|
+
pending.resolve({
|
|
181
|
+
syncGroups: Array.isArray(syncGroups) ? syncGroups : [],
|
|
182
|
+
ttlSeconds: typeof ttlSeconds === 'number' ? ttlSeconds : undefined,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const err = error;
|
|
187
|
+
const code = err?.code && typeof err.code === 'string'
|
|
188
|
+
? err.code
|
|
189
|
+
: 'claim_rejected';
|
|
190
|
+
const msg = err?.message && typeof err.message === 'string'
|
|
191
|
+
? err.message
|
|
192
|
+
: 'claim rejected by server';
|
|
193
|
+
// Capability denials get the typed CapabilityError so
|
|
194
|
+
// callers can read `.requiredCapability` and attenuate-
|
|
195
|
+
// and-retry the claim with a narrower token.
|
|
196
|
+
if (code === 'capability_scope_denied' ||
|
|
197
|
+
code === 'capability_invalid') {
|
|
198
|
+
const rc = error
|
|
199
|
+
?.requiredCapability;
|
|
200
|
+
const requiredCapability = rc != null &&
|
|
201
|
+
typeof rc === 'object' &&
|
|
202
|
+
typeof rc.scope === 'string'
|
|
203
|
+
? rc
|
|
204
|
+
: undefined;
|
|
205
|
+
pending.reject(new CapabilityError(code, msg, requiredCapability));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
// Route through the shared factory so a failed claim_ack is a
|
|
209
|
+
// typed AbloError (registry code → right subclass), symmetric
|
|
210
|
+
// with the commit `mutation_result` path — never a bare Error.
|
|
211
|
+
pending.reject(errorFromWire(msg, { code }));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Handles the acknowledgement of an `update_subscription` request. The wire
|
|
217
|
+
* carries no correlation id, so the ack is matched to the oldest pending
|
|
218
|
+
* request in first-in, first-out order, since the server applies and
|
|
219
|
+
* acknowledges subscription updates in the order it receives them. The
|
|
220
|
+
* payload is validated against its canonical schema before use.
|
|
221
|
+
*/
|
|
222
|
+
const handleSubscriptionAck = (session, message) => {
|
|
223
|
+
const pending = session.shiftPendingSubscription();
|
|
224
|
+
if (!pending)
|
|
225
|
+
return;
|
|
226
|
+
clearTimeout(pending.timeout);
|
|
227
|
+
const parsed = subscriptionAckPayloadSchema.safeParse(message.payload);
|
|
228
|
+
if (!parsed.success) {
|
|
229
|
+
// Unreadable ack — resolve the pending request as a failure
|
|
230
|
+
// rather than hang it until timeout.
|
|
231
|
+
pending.reject(errorFromWire('malformed subscription_ack from server', {
|
|
232
|
+
code: 'malformed_subscription',
|
|
233
|
+
}));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const ack = parsed.data;
|
|
237
|
+
if (ack.success) {
|
|
238
|
+
// Keep the reconnect URL aligned with current interest: a
|
|
239
|
+
// reconnect re-subscribes from `this.options.syncGroups`.
|
|
240
|
+
session.options.syncGroups = ack.syncGroups;
|
|
241
|
+
pending.resolve({ syncGroups: ack.syncGroups });
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
pending.reject(errorFromWire(ack.error?.message ?? 'update_subscription rejected by server', { code: ack.error?.code ?? 'malformed_subscription' }));
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* Handles a `delta` frame, which carries either a single delta or a
|
|
249
|
+
* `{ deltas: [...] }` batch. This only tells the two shapes apart; each
|
|
250
|
+
* delta is validated once downstream, so batch elements are passed along
|
|
251
|
+
* raw rather than parsed here.
|
|
252
|
+
*/
|
|
253
|
+
const handleDeltaFrame = (session, message) => {
|
|
254
|
+
const p = message.payload;
|
|
255
|
+
if (!isRecord(p))
|
|
256
|
+
return;
|
|
257
|
+
if (p.actionType || p.modelName) {
|
|
258
|
+
session.handleDelta(p);
|
|
259
|
+
}
|
|
260
|
+
else if (Array.isArray(p.deltas)) {
|
|
261
|
+
for (const d of p.deltas) {
|
|
262
|
+
session.handleDelta(d);
|
|
263
|
+
}
|
|
264
|
+
// `p.newVersions` from older servers is ignored; `sync_id` is the
|
|
265
|
+
// causality token.
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* Maps each frame type to its handler. Every named server frame this
|
|
270
|
+
* package understands is dispatched from this table; anything else falls
|
|
271
|
+
* through to the collaboration-event and unknown-type path in
|
|
272
|
+
* {@link dispatchWsFrame}.
|
|
273
|
+
*/
|
|
274
|
+
export const wsFrameHandlers = {
|
|
275
|
+
sync_response: (session, message) => { session.handleSyncResponse(message.payload); },
|
|
276
|
+
bootstrap_response: (session, message) => { session.handleBootstrapResponse(message.payload); },
|
|
277
|
+
presence_update: (session, message) => { session.handlePresenceUpdate(message); },
|
|
278
|
+
mutation_result: handleMutationResult,
|
|
279
|
+
claim_ack: handleClaimAck,
|
|
280
|
+
subscription_ack: handleSubscriptionAck,
|
|
281
|
+
claim_expired: (session, message) => {
|
|
282
|
+
// Server-initiated expiry notification. Emit as a typed
|
|
283
|
+
// event so consumers can react (re-claim with a fresh
|
|
284
|
+
// capability, or accept the drop). The claim is already
|
|
285
|
+
// inactive server-side by the time this arrives.
|
|
286
|
+
const p = (message.payload ?? {});
|
|
287
|
+
if (typeof p.claimId === 'string') {
|
|
288
|
+
recordClaim('expired', p);
|
|
289
|
+
session.emit('claim_expired', { claimId: p.claimId });
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
claim_rejected: (session, message) => {
|
|
293
|
+
// The server denied a claim because the target is already held by
|
|
294
|
+
// another participant. The payload is forwarded as-is for the claim
|
|
295
|
+
// stream consumer to interpret (peerId, target, and so on).
|
|
296
|
+
recordClaim('rejected', (message.payload ?? {}));
|
|
297
|
+
session.emit('claim_rejected', message.payload ?? {});
|
|
298
|
+
},
|
|
299
|
+
claim_acquired: (session, message) => {
|
|
300
|
+
// Opt-in fair queue: the target was free, so the lease is ours
|
|
301
|
+
// immediately (no waiting). Payload carries { claimId, target }.
|
|
302
|
+
recordClaim('acquired', (message.payload ?? {}));
|
|
303
|
+
session.emit('claim_acquired', message.payload ?? {});
|
|
304
|
+
},
|
|
305
|
+
claim_queue: (session, message) => {
|
|
306
|
+
// Per-entity wait-queue snapshot for reactive `queue(id)`. Not a
|
|
307
|
+
// single claim's state change, so it isn't logged — the per-claim
|
|
308
|
+
// `queued`/`granted` events already tell that story.
|
|
309
|
+
session.emit('claim_queue', message.payload ?? {});
|
|
310
|
+
},
|
|
311
|
+
claim_queued: (session, message) => {
|
|
312
|
+
// Opt-in fair queue: our claim is waiting in line. Payload
|
|
313
|
+
// carries { claimId, target, position }.
|
|
314
|
+
recordClaim('queued', (message.payload ?? {}));
|
|
315
|
+
session.emit('claim_queued', message.payload ?? {});
|
|
316
|
+
},
|
|
317
|
+
claim_granted: (session, message) => {
|
|
318
|
+
// Our queued claim reached the head — the lease is now ours.
|
|
319
|
+
recordClaim('granted', (message.payload ?? {}));
|
|
320
|
+
session.emit('claim_granted', message.payload ?? {});
|
|
321
|
+
},
|
|
322
|
+
claim_lost: (session, message) => {
|
|
323
|
+
// A held/granted claim was taken from us (TTL lapse, revoke).
|
|
324
|
+
recordClaim('lost', (message.payload ?? {}));
|
|
325
|
+
session.emit('claim_lost', message.payload ?? {});
|
|
326
|
+
},
|
|
327
|
+
claim_heartbeat_ack: (session, message) => {
|
|
328
|
+
// Reply to our `claim_heartbeat` — the claim stream correlates it back
|
|
329
|
+
// to the awaiting caller by claimId. Not logged per-frame: heartbeats
|
|
330
|
+
// are a cadence, and the interesting transitions (lost) surface through
|
|
331
|
+
// the caller's error path.
|
|
332
|
+
session.emit('claim_heartbeat_ack', message.payload ?? {});
|
|
333
|
+
},
|
|
334
|
+
delta: handleDeltaFrame,
|
|
335
|
+
};
|
|
336
|
+
/**
|
|
337
|
+
* Routes one parsed inbound frame to its handler. Keepalive frames are
|
|
338
|
+
* ignored, a missing `type` is treated as a bare delta, and any unknown
|
|
339
|
+
* type falls through to the collaboration-event map, whose wire names use
|
|
340
|
+
* underscores and whose event keys use colons.
|
|
341
|
+
*/
|
|
342
|
+
export function dispatchWsFrame(session, message) {
|
|
343
|
+
if (message.type === 'pong' || message.type === 'ping') {
|
|
344
|
+
// Ignore keepalive messages
|
|
345
|
+
getContext().logger.debug('Received keepalive', { type: message.type });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (message.type === undefined) {
|
|
349
|
+
// A bare delta, validated downstream like every other delta.
|
|
350
|
+
if (message.actionType || message.modelName) {
|
|
351
|
+
session.handleDelta(message);
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
// Look up own properties only, so a wire type like 'toString' can't match
|
|
356
|
+
// an inherited Object.prototype member; such types fall through to the
|
|
357
|
+
// unknown-type path.
|
|
358
|
+
const handler = Object.prototype.hasOwnProperty.call(wsFrameHandlers, message.type)
|
|
359
|
+
? wsFrameHandlers[message.type]
|
|
360
|
+
: undefined;
|
|
361
|
+
if (handler) {
|
|
362
|
+
handler(session, message);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// Collaboration events use underscore wire format (e.g., 'sheet_selection')
|
|
366
|
+
// Convert to colon format for the event map (e.g., 'sheet:selection')
|
|
367
|
+
const eventKey = message.type?.replace(/_/g, ':');
|
|
368
|
+
if (eventKey && session.collaborationEventTypes.has(eventKey)) {
|
|
369
|
+
session.emit(eventKey, message.payload);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
getContext().logger.debug('Received unknown message type', { message });
|
|
373
|
+
}
|
|
374
|
+
}
|