@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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-level heartbeat for the sync WebSocket.
|
|
3
|
+
*
|
|
4
|
+
* The browser WebSocket API hides RFC 6455 protocol-level ping and pong frames
|
|
5
|
+
* from JavaScript, so the server's keepalive cannot be observed by client
|
|
6
|
+
* code. That leaves the client unable to tell a healthy idle connection apart
|
|
7
|
+
* from a "zombie" socket whose underlying TCP connection has silently broken
|
|
8
|
+
* (laptop sleep, NAT timeout, mobile handoff). To close the gap, the client
|
|
9
|
+
* sends an application-level `{ type: 'ping' }` every 30 seconds and
|
|
10
|
+
* force-closes the socket if no inbound traffic arrives within 10 seconds. Any
|
|
11
|
+
* inbound message counts as proof the connection is alive; the explicit `pong`
|
|
12
|
+
* merely guarantees that something arrives even on an otherwise idle stream.
|
|
13
|
+
*/
|
|
14
|
+
import { getContext } from '../context.js';
|
|
15
|
+
import { PING_INTERVAL_MS } from '../wire/protocol.js';
|
|
16
|
+
/**
|
|
17
|
+
* The interval between application-level pings, shared with both sides of the
|
|
18
|
+
* connection: the client pings at the same {@link PING_INTERVAL_MS} the server
|
|
19
|
+
* uses for its own keepalive, and the claim lease window is derived from the
|
|
20
|
+
* same constant.
|
|
21
|
+
*/
|
|
22
|
+
export const HEARTBEAT_INTERVAL_MS = PING_INTERVAL_MS;
|
|
23
|
+
export const HEARTBEAT_TIMEOUT_MS = 10_000;
|
|
24
|
+
/**
|
|
25
|
+
* Runs the application-level heartbeat for one socket. While the socket is
|
|
26
|
+
* open, it sends a `{ type: 'ping' }` frame every {@link HEARTBEAT_INTERVAL_MS}
|
|
27
|
+
* and arms a {@link HEARTBEAT_TIMEOUT_MS} watchdog. Any inbound frame clears
|
|
28
|
+
* the watchdog — the owner calls {@link HeartbeatController.clearHeartbeatTimeout}
|
|
29
|
+
* from its message handler. If the watchdog fires first, the connection is
|
|
30
|
+
* treated as a zombie and force-closed, which lets the owner's reconnect path
|
|
31
|
+
* run.
|
|
32
|
+
*
|
|
33
|
+
* The heartbeat exists because the client cannot see the protocol-level
|
|
34
|
+
* keepalive: browsers answer the server's pings automatically but never expose
|
|
35
|
+
* those frames to JavaScript. On a half-open connection (laptop wake, NAT
|
|
36
|
+
* timeout, mobile handoff) the socket can report itself open for minutes
|
|
37
|
+
* before the operating system surfaces the break, so observable application
|
|
38
|
+
* traffic is the only reliable signal.
|
|
39
|
+
*/
|
|
40
|
+
export class HeartbeatController {
|
|
41
|
+
transport;
|
|
42
|
+
heartbeatTimer = null;
|
|
43
|
+
heartbeatTimeoutTimer = null;
|
|
44
|
+
constructor(transport) {
|
|
45
|
+
this.transport = transport;
|
|
46
|
+
}
|
|
47
|
+
start() {
|
|
48
|
+
this.stop();
|
|
49
|
+
this.heartbeatTimer = setInterval(() => {
|
|
50
|
+
if (!this.transport.isSocketOpen())
|
|
51
|
+
return;
|
|
52
|
+
// Send the ping. If it throws, the socket is already dead, so
|
|
53
|
+
// force-close it to let the close event drive the reconnect cycle.
|
|
54
|
+
try {
|
|
55
|
+
this.transport.sendPing();
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
getContext().observability.captureWebSocketError({
|
|
59
|
+
context: 'heartbeat-send-failed',
|
|
60
|
+
error: err instanceof Error ? err.message : String(err),
|
|
61
|
+
});
|
|
62
|
+
this.transport.forceClose('heartbeat-send-failed');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Arm the timeout. Any inbound message clears it; an explicit `pong` is
|
|
66
|
+
// not required, since a delta or any other frame is equally good proof
|
|
67
|
+
// that the connection is alive.
|
|
68
|
+
if (this.heartbeatTimeoutTimer)
|
|
69
|
+
clearTimeout(this.heartbeatTimeoutTimer);
|
|
70
|
+
this.heartbeatTimeoutTimer = setTimeout(() => {
|
|
71
|
+
getContext().observability.captureWebSocketError({
|
|
72
|
+
context: 'heartbeat-timeout',
|
|
73
|
+
});
|
|
74
|
+
this.transport.forceClose('heartbeat-timeout');
|
|
75
|
+
}, HEARTBEAT_TIMEOUT_MS);
|
|
76
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
77
|
+
}
|
|
78
|
+
stop() {
|
|
79
|
+
if (this.heartbeatTimer) {
|
|
80
|
+
clearInterval(this.heartbeatTimer);
|
|
81
|
+
this.heartbeatTimer = null;
|
|
82
|
+
}
|
|
83
|
+
this.clearHeartbeatTimeout();
|
|
84
|
+
}
|
|
85
|
+
clearHeartbeatTimeout() {
|
|
86
|
+
if (this.heartbeatTimeoutTimer) {
|
|
87
|
+
clearTimeout(this.heartbeatTimeoutTimer);
|
|
88
|
+
this.heartbeatTimeoutTimer = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { SyncWebSocket } from './SyncWebSocket.js';
|
|
2
|
-
import type { Schema
|
|
2
|
+
import type { Schema } from '../schema/schema.js';
|
|
3
3
|
import type { Claim, Activity, ClaimTarget, ClaimStream, Peer, PresenceStream, PresenceTarget } from '../types/streams.js';
|
|
4
4
|
import type { AttachableClaimStream } from './createClaimStream.js';
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The scope a participant can be joined to. The usual form is an entity target
|
|
7
|
+
* (`{ type, id }`); raw sync-group strings are an advanced escape hatch for
|
|
8
|
+
* addressing a transport scope directly.
|
|
9
9
|
*/
|
|
10
10
|
export type ParticipantScope = ClaimTarget | readonly ClaimTarget[] | string | readonly string[] | {
|
|
11
11
|
readonly syncGroup: string;
|
|
@@ -19,26 +19,27 @@ export interface EngineParticipant {
|
|
|
19
19
|
}
|
|
20
20
|
export interface ParticipantJoinOptions {
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
* narrowed to a path, field, or range. When `scope` is omitted,
|
|
24
|
-
*
|
|
22
|
+
* The initial focus target, named in your schema's vocabulary and optionally
|
|
23
|
+
* narrowed to a path, field, or range. When `scope` is omitted, this target
|
|
24
|
+
* also becomes the routing scope.
|
|
25
25
|
*/
|
|
26
26
|
readonly target?: PresenceTarget;
|
|
27
27
|
/** Alias for `target` when the participant is joined to a broader scope. */
|
|
28
28
|
readonly focus?: PresenceTarget;
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* The routing scope: one entity, many entities, or a raw sync-group escape
|
|
31
|
+
* hatch. Use it for "joined to the folder, focused on one file" shapes,
|
|
32
|
+
* where the participant listens more broadly than its focus target.
|
|
33
33
|
*/
|
|
34
34
|
readonly scope?: ParticipantScope;
|
|
35
35
|
/** Present a narrower capability for this logical participant. */
|
|
36
36
|
readonly capabilityToken?: string;
|
|
37
|
-
/**
|
|
37
|
+
/** How long the claim lives, in seconds or a compact duration string (`30s`, `5m`). */
|
|
38
38
|
readonly ttlSeconds?: number | string | null;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
* `reading` when `target` is present. Pass false to join
|
|
40
|
+
* The activity to announce as soon as the claim is acknowledged. Defaults to
|
|
41
|
+
* `reading` when a `target` is present. Pass `false` to join without
|
|
42
|
+
* announcing anything.
|
|
42
43
|
*/
|
|
43
44
|
readonly activity?: 'reading' | 'viewing' | 'editing' | false;
|
|
44
45
|
readonly detail?: string;
|
|
@@ -46,7 +47,7 @@ export interface ParticipantJoinOptions {
|
|
|
46
47
|
export interface ScopedPresence {
|
|
47
48
|
readonly self: Peer;
|
|
48
49
|
readonly focus: ClaimTarget | null;
|
|
49
|
-
readonly others:
|
|
50
|
+
readonly others: readonly Peer[];
|
|
50
51
|
update(activity: Activity): void;
|
|
51
52
|
reading(detail?: string): void;
|
|
52
53
|
reading(target: PresenceTarget, detail?: string): void;
|
|
@@ -63,17 +64,16 @@ export interface ScopedClaimOptions {
|
|
|
63
64
|
/** Free-form reason. Defaults to `'editing'`. Common: `'editing'`,
|
|
64
65
|
* `'writing'`, `'reviewing'`, app-specific phases. */
|
|
65
66
|
readonly reason?: string;
|
|
66
|
-
/**
|
|
67
|
+
/** How long the claim lives; the server expires it automatically after this. */
|
|
67
68
|
readonly ttl?: import('../types/streams.js').Duration;
|
|
68
69
|
}
|
|
69
70
|
export interface ScopedClaims {
|
|
70
71
|
readonly focus: ClaimTarget | null;
|
|
71
|
-
readonly others:
|
|
72
|
+
readonly others: readonly Claim[];
|
|
72
73
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* collapsed into this one method.
|
|
74
|
+
* Takes an exclusive claim on the participant's focus target, or on an
|
|
75
|
+
* explicit target passed via `opts.target`. While the claim is held, other
|
|
76
|
+
* participants that request an overlapping target are rejected.
|
|
77
77
|
*/
|
|
78
78
|
claim(opts?: ScopedClaimOptions): Claim;
|
|
79
79
|
onRejected(listener: Parameters<ClaimStream['onRejected']>[0]): () => void;
|
|
@@ -84,15 +84,15 @@ export interface ParticipantFocusOptions {
|
|
|
84
84
|
readonly detail?: string;
|
|
85
85
|
}
|
|
86
86
|
export interface JoinedParticipant {
|
|
87
|
-
/**
|
|
87
|
+
/** The exact entity this participant is currently reading or editing. */
|
|
88
88
|
readonly target: ClaimTarget | null;
|
|
89
89
|
readonly focusTarget: ClaimTarget | null;
|
|
90
|
-
/**
|
|
90
|
+
/** The transport scopes this participant is joined to, which govern what it sees and receives. */
|
|
91
91
|
readonly syncGroups: readonly string[];
|
|
92
92
|
readonly presence: ScopedPresence;
|
|
93
93
|
readonly claims: ScopedClaims;
|
|
94
|
-
readonly peers:
|
|
95
|
-
readonly activeClaims:
|
|
94
|
+
readonly peers: readonly Peer[];
|
|
95
|
+
readonly activeClaims: readonly Claim[];
|
|
96
96
|
focus(target: PresenceTarget, options?: ParticipantFocusOptions): JoinedParticipant;
|
|
97
97
|
leave(): void;
|
|
98
98
|
[Symbol.asyncDispose](): Promise<void>;
|
|
@@ -106,10 +106,10 @@ export interface ParticipantManagerConfig {
|
|
|
106
106
|
readonly getTransport: () => SyncWebSocket | null;
|
|
107
107
|
readonly presence: PresenceStream;
|
|
108
108
|
readonly claims: AttachableClaimStream;
|
|
109
|
-
readonly schema?: Schema
|
|
109
|
+
readonly schema?: Schema;
|
|
110
110
|
}
|
|
111
111
|
export declare function createParticipantManager(config: ParticipantManagerConfig): ParticipantManager;
|
|
112
|
-
export declare function resolveParticipantSyncGroups(scope: ParticipantScope | undefined, schema?: Schema
|
|
113
|
-
export declare function syncGroupFromEntityRef(ref: ClaimTarget, schema?: Schema
|
|
112
|
+
export declare function resolveParticipantSyncGroups(scope: ParticipantScope | undefined, schema?: Schema): string[];
|
|
113
|
+
export declare function syncGroupFromEntityRef(ref: ClaimTarget, schema?: Schema): string;
|
|
114
114
|
export declare function parseParticipantTtlSeconds(value: number | string | null | undefined): number | undefined;
|
|
115
115
|
export declare function createParticipantClaimId(): string;
|
package/dist/sync/schemas.d.ts
CHANGED
|
@@ -74,7 +74,8 @@ export declare const BootstrapResponseSchema: z.ZodObject<{
|
|
|
74
74
|
}, z.core.$loose>;
|
|
75
75
|
export type ValidatedBootstrapResponse = z.infer<typeof BootstrapResponseSchema>;
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
77
|
+
* Validates a raw bootstrap response from the server and returns the typed
|
|
78
|
+
* result. On failure it records a diagnostic breadcrumb and throws an
|
|
79
|
+
* {@link AbloValidationError} describing which fields were invalid.
|
|
79
80
|
*/
|
|
80
81
|
export declare function parseBootstrapResponse(raw: unknown): ValidatedBootstrapResponse;
|
package/dist/sync/schemas.js
CHANGED
|
@@ -8,7 +8,8 @@ import { z } from 'zod';
|
|
|
8
8
|
import { getContext } from "../context.js";
|
|
9
9
|
import { AbloValidationError } from "../errors.js";
|
|
10
10
|
// ─── Sync Action Types ───────────────────────────────────────────────────────
|
|
11
|
-
//
|
|
11
|
+
// The action codes a server delta can carry, matching the wire protocol's
|
|
12
|
+
// action-type set.
|
|
12
13
|
const SYNC_ACTION_VALUES = ['I', 'U', 'D', 'A', 'C', 'G', 'S', 'V'];
|
|
13
14
|
// ─── Server Delta Schema ─────────────────────────────────────────────────────
|
|
14
15
|
export const ServerDeltaSchema = z
|
|
@@ -23,11 +24,12 @@ export const ServerDeltaSchema = z
|
|
|
23
24
|
})
|
|
24
25
|
.passthrough();
|
|
25
26
|
// ─── Model Value Schema ─────────────────────────────────────────────────────
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// -
|
|
29
|
-
// -
|
|
30
|
-
//
|
|
27
|
+
// A model's values can arrive in more than one shape depending on how the
|
|
28
|
+
// server serialized them:
|
|
29
|
+
// - Array: an already-parsed JSON array (the common case)
|
|
30
|
+
// - String: a JSON array still encoded as a string, which must be parsed
|
|
31
|
+
// - null: no matching rows
|
|
32
|
+
// This schema normalizes every variant into an array before downstream use.
|
|
31
33
|
const ModelValueSchema = z
|
|
32
34
|
.union([z.array(z.unknown()), z.string(), z.null()])
|
|
33
35
|
.transform((val) => {
|
|
@@ -54,15 +56,17 @@ export const BootstrapResponseSchema = z
|
|
|
54
56
|
deltaCount: z.number().optional(),
|
|
55
57
|
failedModels: z.array(z.string()).optional(),
|
|
56
58
|
timestamp: z.number().default(() => Date.now()),
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
+
// The server's active schema hash, used to detect schema drift. Optional:
|
|
60
|
+
// absent when the server predates this field or the tenant has never
|
|
61
|
+
// pushed a schema.
|
|
59
62
|
schemaHash: z.string().optional(),
|
|
60
63
|
})
|
|
61
64
|
.passthrough();
|
|
62
65
|
// ─── Parse Helpers ───────────────────────────────────────────────────────────
|
|
63
66
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
67
|
+
* Validates a raw bootstrap response from the server and returns the typed
|
|
68
|
+
* result. On failure it records a diagnostic breadcrumb and throws an
|
|
69
|
+
* {@link AbloValidationError} describing which fields were invalid.
|
|
66
70
|
*/
|
|
67
71
|
export function parseBootstrapResponse(raw) {
|
|
68
72
|
const result = BootstrapResponseSchema.safeParse(raw);
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
export declare class SyncCursor {
|
|
12
|
+
lastSyncId: number;
|
|
13
|
+
syncCursor: string | null;
|
|
14
|
+
constructor(lastSyncId: number);
|
|
15
|
+
/**
|
|
16
|
+
* Advances the watermark in response to an acknowledgement. This becomes the
|
|
17
|
+
* value the next incremental-sync request and the connect handshake send, and
|
|
18
|
+
* the value {@link SyncCursor.getLastSyncId} reports when persisting on a
|
|
19
|
+
* clean shutdown. The move is monotonic: a stale, lower acknowledgement never
|
|
20
|
+
* pulls the watermark backward.
|
|
21
|
+
*/
|
|
22
|
+
ackAdvance(syncId: number): void;
|
|
23
|
+
/**
|
|
24
|
+
* Sets the watermark outright, used when restoring persisted state.
|
|
25
|
+
*/
|
|
26
|
+
setLastSyncId(syncId: number): void;
|
|
27
|
+
/**
|
|
28
|
+
* Sets the opaque server cursor used for incremental sync.
|
|
29
|
+
*/
|
|
30
|
+
setSyncCursor(cursor: string | null): void;
|
|
31
|
+
/**
|
|
32
|
+
* Returns the current opaque server cursor, or null if none is set.
|
|
33
|
+
*/
|
|
34
|
+
getSyncCursor(): string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Returns the highest delta id seen this session, for persistence on a clean
|
|
37
|
+
* shutdown.
|
|
38
|
+
*/
|
|
39
|
+
getLastSyncId(): number;
|
|
40
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
export class SyncCursor {
|
|
12
|
+
lastSyncId;
|
|
13
|
+
syncCursor;
|
|
14
|
+
constructor(lastSyncId) {
|
|
15
|
+
this.lastSyncId = lastSyncId;
|
|
16
|
+
this.syncCursor = null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Advances the watermark in response to an acknowledgement. This becomes the
|
|
20
|
+
* value the next incremental-sync request and the connect handshake send, and
|
|
21
|
+
* the value {@link SyncCursor.getLastSyncId} reports when persisting on a
|
|
22
|
+
* clean shutdown. The move is monotonic: a stale, lower acknowledgement never
|
|
23
|
+
* pulls the watermark backward.
|
|
24
|
+
*/
|
|
25
|
+
ackAdvance(syncId) {
|
|
26
|
+
if (syncId > this.lastSyncId) {
|
|
27
|
+
this.lastSyncId = syncId;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Sets the watermark outright, used when restoring persisted state.
|
|
32
|
+
*/
|
|
33
|
+
setLastSyncId(syncId) {
|
|
34
|
+
this.lastSyncId = syncId;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Sets the opaque server cursor used for incremental sync.
|
|
38
|
+
*/
|
|
39
|
+
setSyncCursor(cursor) {
|
|
40
|
+
this.syncCursor = cursor;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns the current opaque server cursor, or null if none is set.
|
|
44
|
+
*/
|
|
45
|
+
getSyncCursor() {
|
|
46
|
+
return this.syncCursor;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Returns the highest delta id seen this session, for persistence on a clean
|
|
50
|
+
* shutdown.
|
|
51
|
+
*/
|
|
52
|
+
getLastSyncId() {
|
|
53
|
+
return this.lastSyncId || 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
import type { Schema } from '../schema/schema.js';
|
|
9
|
+
/** A foreign-key index to register on the in-memory object pool when the store is constructed. */
|
|
10
|
+
export interface ForeignKeyIndexSpec {
|
|
11
|
+
/**
|
|
12
|
+
* The name of the child model, where the foreign-key field lives, and the
|
|
13
|
+
* name the object pool indexes by. Use the wire type-name casing (for
|
|
14
|
+
* example `'SlideLayer'`, not `'slideLayer'`), since that is the value
|
|
15
|
+
* stamped onto reconstructed models and the key the pool looks up.
|
|
16
|
+
*/
|
|
17
|
+
readonly modelName: string;
|
|
18
|
+
/** The foreign-key field name on the child model, for example `'slideId'`. */
|
|
19
|
+
readonly fieldName: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A declarative rule for enriching an incoming row with its related parent.
|
|
23
|
+
*
|
|
24
|
+
* When a delta for `modelName` arrives and its row has been constructed, the
|
|
25
|
+
* store reads the row's `foreignKey` value, looks up the matching parent in
|
|
26
|
+
* the object pool, and attaches it under `relationKey`. Enrichment is
|
|
27
|
+
* best-effort: if the parent is not in the pool yet — for example, it arrives
|
|
28
|
+
* later in the same bootstrap batch — the step is skipped without error.
|
|
29
|
+
*/
|
|
30
|
+
export interface EnrichmentPlanEntry {
|
|
31
|
+
/** The child model whose incoming deltas should be enriched. */
|
|
32
|
+
readonly modelName: string;
|
|
33
|
+
/** The foreign-key field on the child that points at the parent's id. */
|
|
34
|
+
readonly foreignKey: string;
|
|
35
|
+
/** The property name under which to attach the parent model. */
|
|
36
|
+
readonly relationKey: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Walks a schema and derives the two sync-plan arrays used when the store is
|
|
40
|
+
* constructed: the foreign-key indexes to register on the object pool and the
|
|
41
|
+
* enrichment plan. See {@link ForeignKeyIndexSpec} and
|
|
42
|
+
* {@link EnrichmentPlanEntry}.
|
|
43
|
+
*
|
|
44
|
+
* Both are drawn from each `belongsTo` relation that sets `options.index` or
|
|
45
|
+
* `options.enrich`; relations without those options are skipped. Enabling them
|
|
46
|
+
* is opt-in, so adding a `belongsTo` relation never silently changes how deltas
|
|
47
|
+
* apply or how lookups resolve. A `hasMany` or `hasOne` relation registers its
|
|
48
|
+
* index on the target model, since that is where the foreign key lives. The
|
|
49
|
+
* function has no side effects and is called once at construction.
|
|
50
|
+
*/
|
|
51
|
+
export declare function deriveSyncPlanFromSchema(schema: Schema): {
|
|
52
|
+
enrichmentPlan: EnrichmentPlanEntry[];
|
|
53
|
+
foreignKeyIndexes: ForeignKeyIndexSpec[];
|
|
54
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
* Walks a schema and derives the two sync-plan arrays used when the store is
|
|
10
|
+
* constructed: the foreign-key indexes to register on the object pool and the
|
|
11
|
+
* enrichment plan. See {@link ForeignKeyIndexSpec} and
|
|
12
|
+
* {@link EnrichmentPlanEntry}.
|
|
13
|
+
*
|
|
14
|
+
* Both are drawn from each `belongsTo` relation that sets `options.index` or
|
|
15
|
+
* `options.enrich`; relations without those options are skipped. Enabling them
|
|
16
|
+
* is opt-in, so adding a `belongsTo` relation never silently changes how deltas
|
|
17
|
+
* apply or how lookups resolve. A `hasMany` or `hasOne` relation registers its
|
|
18
|
+
* index on the target model, since that is where the foreign key lives. The
|
|
19
|
+
* function has no side effects and is called once at construction.
|
|
20
|
+
*/
|
|
21
|
+
export function deriveSyncPlanFromSchema(schema) {
|
|
22
|
+
const enrichmentPlan = [];
|
|
23
|
+
const foreignKeyIndexes = [];
|
|
24
|
+
for (const [modelName, def] of Object.entries(schema.models)) {
|
|
25
|
+
const typename = def.typename ?? modelName;
|
|
26
|
+
for (const [relationKey, rel] of Object.entries(def.relations)) {
|
|
27
|
+
if (rel.type === 'belongsTo') {
|
|
28
|
+
if (rel.options?.index) {
|
|
29
|
+
foreignKeyIndexes.push({ modelName: typename, fieldName: rel.foreignKey });
|
|
30
|
+
}
|
|
31
|
+
if (rel.options?.enrich) {
|
|
32
|
+
enrichmentPlan.push({
|
|
33
|
+
modelName: typename,
|
|
34
|
+
foreignKey: rel.foreignKey,
|
|
35
|
+
relationKey,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (rel.type === 'hasMany' || rel.type === 'hasOne') {
|
|
40
|
+
// For hasMany and hasOne, the foreign key lives on the target model,
|
|
41
|
+
// not the current one, so register the index on the target. Its wire
|
|
42
|
+
// type name is resolved from the schema here.
|
|
43
|
+
const targetDef = schema.models[rel.target];
|
|
44
|
+
const targetTypename = targetDef?.typename ?? rel.target;
|
|
45
|
+
foreignKeyIndexes.push({ modelName: targetTypename, fieldName: rel.foreignKey });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { enrichmentPlan, foreignKeyIndexes };
|
|
50
|
+
}
|
|
@@ -1,40 +1,39 @@
|
|
|
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 declare const syncPositionSchema: z.ZodObject<{
|
|
@@ -44,35 +43,41 @@ export declare const syncPositionSchema: z.ZodObject<{
|
|
|
44
43
|
}, z.core.$strip>;
|
|
45
44
|
export type SyncPositionSnapshot = z.infer<typeof syncPositionSchema>;
|
|
46
45
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
* Only the `persisted` cursor is stored durably; `applied` and `acked` are
|
|
47
|
+
* not. On resume the object pool is rebuilt from the persisted state, so the
|
|
48
|
+
* correct restore is simply to advance `persisted` to the stored value, which
|
|
49
|
+
* also implies `applied`. `acked` starts at zero, because a past session's
|
|
50
|
+
* acknowledgements carry no read authority and the offline queue
|
|
51
|
+
* re-acknowledges its own replays.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* Validates an untrusted value, such as one loaded from disk, into a position
|
|
55
|
+
* snapshot, or returns null when it does not match the schema.
|
|
55
56
|
*/
|
|
56
|
-
/** Validate a persisted/foreign value into a position snapshot. */
|
|
57
57
|
export declare function parseSyncPosition(value: unknown): SyncPositionSnapshot | null;
|
|
58
|
-
/**
|
|
59
|
-
*
|
|
58
|
+
/**
|
|
59
|
+
* The live sync position: one instance per client. Three producers each
|
|
60
|
+
* advance their own cursor, and consumers read the result.
|
|
61
|
+
*/
|
|
60
62
|
export declare class SyncPosition {
|
|
61
63
|
#private;
|
|
62
|
-
/**
|
|
64
|
+
/** Returns a copy of the current state in the schema's shape. */
|
|
63
65
|
snapshot(): SyncPositionSnapshot;
|
|
64
66
|
get persisted(): number;
|
|
65
67
|
get applied(): number;
|
|
66
68
|
get acked(): number;
|
|
67
|
-
/**
|
|
69
|
+
/** The position a snapshot or claim stamps as its read point: the greater of `applied` and `acked`. */
|
|
68
70
|
get readFloor(): number;
|
|
69
|
-
/**
|
|
70
|
-
*
|
|
71
|
+
/**
|
|
72
|
+
* Records that deltas through `syncId` have committed to durable local
|
|
73
|
+
* storage. This also advances `applied`, since the flush applies each delta
|
|
74
|
+
* before or as it persists.
|
|
75
|
+
*/
|
|
71
76
|
advancePersisted(syncId: number): void;
|
|
72
|
-
/**
|
|
77
|
+
/** Records that a delta was applied to the in-memory object pool. */
|
|
73
78
|
advanceApplied(syncId: number): void;
|
|
74
|
-
/**
|
|
79
|
+
/** Records that the server acknowledged one of this client's commits at the given position. */
|
|
75
80
|
noteAck(lastSyncId: number | undefined): void;
|
|
76
|
-
/**
|
|
81
|
+
/** Restores from an already-validated snapshot, for example on resume from disk. The merge is monotonic. */
|
|
77
82
|
restore(snapshot: SyncPositionSnapshot): void;
|
|
78
83
|
}
|