@abloatai/ablo 0.26.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/CHANGELOG.md +14 -0
- package/README.md +101 -85
- package/dist/BaseSyncedStore.d.ts +85 -88
- package/dist/BaseSyncedStore.js +131 -147
- package/dist/Database.d.ts +54 -68
- package/dist/Database.js +97 -113
- package/dist/{ObjectPool.d.ts → InstanceCache.d.ts} +10 -13
- package/dist/{ObjectPool.js → InstanceCache.js} +85 -83
- package/dist/LazyReferenceCollection.d.ts +11 -15
- package/dist/LazyReferenceCollection.js +12 -16
- package/dist/Model.d.ts +37 -52
- package/dist/Model.js +46 -61
- package/dist/ModelRegistry.d.ts +21 -19
- package/dist/ModelRegistry.js +23 -27
- package/dist/NetworkMonitor.d.ts +5 -6
- package/dist/NetworkMonitor.js +5 -6
- package/dist/SyncClient.d.ts +112 -112
- package/dist/SyncClient.js +165 -172
- 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 +27 -32
- package/dist/agent/Agent.js +18 -19
- 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} +26 -33
- package/dist/ai-sdk/index.d.ts +25 -22
- package/dist/ai-sdk/index.js +25 -22
- package/dist/ai-sdk/wrap.d.ts +6 -7
- package/dist/ai-sdk/wrap.js +1 -1
- package/dist/auth/credentialPolicy.d.ts +69 -74
- package/dist/auth/credentialPolicy.js +51 -56
- package/dist/auth/credentialSource.d.ts +6 -5
- package/dist/auth/credentialSource.js +9 -10
- package/dist/auth/index.d.ts +59 -58
- package/dist/auth/index.js +31 -37
- 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 +167 -119
- package/dist/client/Ablo.d.ts +73 -73
- package/dist/client/Ablo.js +125 -160
- package/dist/client/ApiClient.d.ts +30 -19
- package/dist/client/ApiClient.js +133 -38
- package/dist/client/auth.d.ts +47 -47
- package/dist/client/auth.js +108 -117
- package/dist/client/claimHeartbeatLoop.d.ts +50 -0
- package/dist/client/claimHeartbeatLoop.js +88 -0
- package/dist/client/consoleLogger.d.ts +5 -6
- package/dist/client/consoleLogger.js +5 -6
- package/dist/client/createInternalComponents.d.ts +14 -17
- package/dist/client/createInternalComponents.js +25 -30
- package/dist/client/createModelProxy.d.ts +130 -120
- package/dist/client/createModelProxy.js +152 -122
- package/dist/client/credentialEndpoint.d.ts +40 -42
- package/dist/client/credentialEndpoint.js +35 -36
- package/dist/client/functionalUpdate.d.ts +29 -27
- package/dist/client/functionalUpdate.js +21 -21
- package/dist/client/hostedEndpoints.d.ts +9 -12
- package/dist/client/hostedEndpoints.js +9 -12
- package/dist/client/httpClient.d.ts +57 -53
- package/dist/client/httpClient.js +29 -31
- package/dist/client/identity.d.ts +15 -20
- package/dist/client/identity.js +47 -58
- package/dist/client/modelRegistration.d.ts +5 -9
- package/dist/client/modelRegistration.js +67 -87
- package/dist/client/options.d.ts +134 -157
- package/dist/client/options.js +3 -7
- package/dist/client/registerDataSource.d.ts +9 -9
- package/dist/client/registerDataSource.js +15 -16
- package/dist/client/resourceTypes.d.ts +64 -75
- package/dist/client/resourceTypes.js +4 -10
- package/dist/client/schemaConfig.d.ts +31 -43
- package/dist/client/schemaConfig.js +38 -50
- package/dist/client/sessionMint.d.ts +16 -12
- package/dist/client/sessionMint.js +26 -31
- package/dist/client/validateAbloOptions.d.ts +12 -14
- package/dist/client/validateAbloOptions.js +8 -9
- package/dist/client/writeOptionsSchema.d.ts +18 -16
- package/dist/client/writeOptionsSchema.js +23 -20
- package/dist/client/wsMutationExecutor.d.ts +15 -20
- package/dist/client/wsMutationExecutor.js +17 -23
- package/dist/context.d.ts +6 -4
- package/dist/context.js +6 -4
- package/dist/coordination/index.d.ts +10 -8
- package/dist/coordination/index.js +14 -12
- package/dist/coordination/schema.d.ts +176 -128
- package/dist/coordination/schema.js +197 -133
- package/dist/coordination/trace.d.ts +9 -10
- package/dist/coordination/trace.js +13 -14
- package/dist/core/DatabaseManager.d.ts +5 -7
- package/dist/core/DatabaseManager.js +15 -19
- package/dist/core/QueryProcessor.d.ts +7 -9
- package/dist/core/QueryProcessor.js +22 -28
- package/dist/core/QueryView.d.ts +8 -8
- package/dist/core/QueryView.js +2 -2
- package/dist/core/StoreManager.d.ts +12 -14
- package/dist/core/StoreManager.js +21 -24
- package/dist/core/ViewRegistry.d.ts +5 -5
- package/dist/core/ViewRegistry.js +4 -4
- package/dist/core/index.d.ts +17 -12
- package/dist/core/index.js +32 -26
- package/dist/core/openIDBWithTimeout.d.ts +38 -36
- package/dist/core/openIDBWithTimeout.js +42 -43
- package/dist/core/queryUtils.d.ts +45 -0
- package/dist/core/queryUtils.js +69 -0
- package/dist/core/storeContract.d.ts +63 -61
- package/dist/core/storeContract.js +8 -12
- package/dist/environment.d.ts +28 -0
- package/dist/environment.js +21 -0
- package/dist/errorCodes.d.ts +107 -99
- package/dist/errorCodes.js +131 -132
- package/dist/errors.d.ts +160 -166
- package/dist/errors.js +155 -158
- package/dist/index.d.ts +30 -27
- package/dist/index.js +89 -86
- package/dist/interfaces/index.d.ts +102 -113
- package/dist/interfaces/index.js +5 -4
- package/dist/keys/index.d.ts +27 -29
- package/dist/keys/index.js +41 -40
- 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 +145 -156
- package/dist/mutators/defineMutators.d.ts +23 -34
- 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 +104 -100
- package/dist/policy/types.js +67 -66
- package/dist/query/client.d.ts +28 -23
- package/dist/query/client.js +45 -43
- package/dist/query/types.d.ts +37 -60
- package/dist/query/types.js +13 -33
- package/dist/react/AbloProvider.d.ts +1 -1
- package/dist/react/AbloProvider.js +2 -2
- package/dist/react/context.d.ts +25 -28
- 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 +16 -14
- 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 +10 -11
- package/dist/react/useMutationFailureListener.d.ts +8 -8
- package/dist/react/useMutationFailureListener.js +8 -8
- package/dist/react/useMutators.d.ts +11 -11
- package/dist/react/useMutators.js +3 -3
- package/dist/react/useReactive.js +2 -2
- package/dist/react/useSyncStatus.d.ts +4 -6
- package/dist/react/useUndoScope.d.ts +7 -9
- package/dist/react/useUndoScope.js +1 -1
- 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 +20 -24
- package/dist/schema/ddlLock.js +18 -23
- 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 +35 -48
- package/dist/schema/generate.d.ts +12 -12
- package/dist/schema/generate.js +12 -12
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +21 -23
- package/dist/schema/model.d.ts +118 -143
- package/dist/schema/model.js +22 -33
- package/dist/schema/openapi.d.ts +10 -9
- package/dist/schema/openapi.js +5 -3
- package/dist/schema/queries.d.ts +29 -31
- package/dist/schema/queries.js +23 -25
- package/dist/schema/relation.d.ts +89 -99
- package/dist/schema/relation.js +13 -13
- package/dist/schema/residency.d.ts +16 -13
- package/dist/schema/residency.js +16 -13
- package/dist/schema/roles.d.ts +36 -43
- package/dist/schema/roles.js +31 -37
- package/dist/schema/schema.d.ts +33 -42
- package/dist/schema/schema.js +31 -32
- package/dist/schema/select.d.ts +13 -13
- package/dist/schema/select.js +13 -13
- package/dist/schema/serialize.d.ts +28 -31
- 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} +26 -49
- 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 +30 -25
- package/dist/source/adapter.js +10 -10
- package/dist/source/adapters/drizzle.d.ts +28 -23
- package/dist/source/adapters/drizzle.js +30 -25
- package/dist/source/adapters/kysely.d.ts +27 -25
- package/dist/source/adapters/kysely.js +24 -23
- package/dist/source/adapters/memory.d.ts +8 -7
- package/dist/source/adapters/memory.js +9 -8
- package/dist/source/adapters/prisma.d.ts +13 -12
- package/dist/source/adapters/prisma.js +22 -25
- package/dist/source/conformance.d.ts +18 -11
- package/dist/source/conformance.js +17 -11
- package/dist/source/connector.d.ts +31 -32
- package/dist/source/connector.js +28 -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 +46 -58
- package/dist/source/factory.js +22 -27
- package/dist/source/index.d.ts +7 -9
- package/dist/source/index.js +12 -14
- package/dist/source/migrations.d.ts +9 -9
- package/dist/source/migrations.js +9 -9
- package/dist/source/next.d.ts +9 -10
- package/dist/source/next.js +6 -7
- package/dist/source/pushQueue.d.ts +69 -47
- package/dist/source/pushQueue.js +32 -28
- package/dist/source/signing.d.ts +46 -17
- package/dist/source/signing.js +28 -11
- package/dist/source/types.d.ts +121 -104
- package/dist/source/types.js +13 -14
- package/dist/stores/ObjectStore.d.ts +10 -11
- package/dist/stores/ObjectStore.js +11 -12
- package/dist/stores/ObjectStoreContract.d.ts +12 -15
- package/dist/stores/SyncActionStore.d.ts +7 -11
- package/dist/stores/SyncActionStore.js +13 -17
- package/dist/surface.d.ts +27 -20
- package/dist/surface.js +27 -20
- package/dist/sync/{BootstrapHelper.d.ts → BootstrapFetcher.d.ts} +36 -42
- package/dist/sync/{BootstrapHelper.js → BootstrapFetcher.js} +76 -76
- package/dist/sync/ConnectionManager.d.ts +39 -50
- package/dist/sync/ConnectionManager.js +55 -66
- package/dist/sync/NetworkProbe.d.ts +24 -29
- package/dist/sync/NetworkProbe.js +63 -69
- package/dist/sync/{HydrationCoordinator.d.ts → OnDemandLoader.d.ts} +42 -41
- package/dist/sync/{HydrationCoordinator.js → OnDemandLoader.js} +59 -54
- package/dist/sync/{AreaOfInterestManager.d.ts → SubscriptionManager.d.ts} +43 -57
- package/dist/sync/{AreaOfInterestManager.js → SubscriptionManager.js} +43 -51
- package/dist/sync/SyncWebSocket.d.ts +139 -165
- package/dist/sync/SyncWebSocket.js +191 -223
- package/dist/sync/awaitClaimGrant.d.ts +18 -18
- package/dist/sync/awaitClaimGrant.js +11 -11
- package/dist/sync/bootstrapApply.d.ts +34 -24
- package/dist/sync/bootstrapApply.js +27 -19
- package/dist/sync/commitFrames.d.ts +21 -20
- package/dist/sync/commitFrames.js +18 -18
- package/dist/sync/createClaimStream.d.ts +23 -22
- package/dist/sync/createClaimStream.js +105 -23
- package/dist/sync/createPresenceStream.d.ts +19 -18
- package/dist/sync/createPresenceStream.js +25 -26
- package/dist/sync/createSnapshot.d.ts +12 -14
- package/dist/sync/createSnapshot.js +20 -26
- package/dist/sync/credentialLifecycle.d.ts +104 -104
- package/dist/sync/credentialLifecycle.js +140 -147
- package/dist/sync/deltaPipeline.d.ts +36 -34
- package/dist/sync/deltaPipeline.js +64 -65
- package/dist/sync/groupChange.d.ts +63 -61
- package/dist/sync/groupChange.js +74 -78
- package/dist/sync/heartbeat.d.ts +34 -33
- package/dist/sync/heartbeat.js +31 -31
- package/dist/sync/participants.d.ts +19 -19
- package/dist/sync/schemas.d.ts +3 -2
- package/dist/sync/schemas.js +14 -10
- package/dist/sync/syncCursor.d.ts +17 -21
- package/dist/sync/syncCursor.js +17 -21
- package/dist/sync/syncPlan.d.ts +28 -36
- package/dist/sync/syncPlan.js +18 -19
- package/dist/sync/syncPosition.d.ts +54 -49
- package/dist/sync/syncPosition.js +57 -52
- package/dist/sync/wsFrameHandlers.d.ts +35 -36
- package/dist/sync/wsFrameHandlers.js +63 -67
- package/dist/testing/fixtures/bootstrap.d.ts +12 -6
- package/dist/testing/fixtures/bootstrap.js +12 -6
- package/dist/testing/fixtures/deltas.d.ts +30 -33
- package/dist/testing/fixtures/deltas.js +30 -33
- package/dist/testing/fixtures/models.d.ts +11 -10
- package/dist/testing/fixtures/models.js +11 -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 -15
- package/dist/testing/helpers/{sync-engine-harness.js → syncEngineHarness.js} +12 -10
- package/dist/testing/helpers/wait.d.ts +13 -8
- package/dist/testing/helpers/wait.js +13 -8
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +2 -2
- 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 +20 -17
- package/dist/testing/mocks/MockSyncContext.js +15 -13
- package/dist/testing/mocks/MockSyncStore.d.ts +10 -10
- package/dist/testing/mocks/MockSyncStore.js +11 -11
- package/dist/testing/mocks/MockWebSocket.d.ts +26 -22
- package/dist/testing/mocks/MockWebSocket.js +22 -21
- package/dist/transactions/TransactionQueue.d.ts +181 -176
- package/dist/transactions/TransactionQueue.js +338 -350
- package/dist/transactions/TransactionStore.d.ts +6 -4
- package/dist/transactions/TransactionStore.js +6 -4
- package/dist/transactions/UnconfirmedWrites.d.ts +82 -0
- package/dist/transactions/UnconfirmedWrites.js +104 -0
- package/dist/transactions/coalesceRules.d.ts +41 -17
- package/dist/transactions/coalesceRules.js +40 -17
- package/dist/transactions/commitPayload.d.ts +48 -52
- package/dist/transactions/commitPayload.js +48 -57
- package/dist/transactions/deltaConfirmation.d.ts +20 -22
- package/dist/transactions/deltaConfirmation.js +37 -45
- package/dist/transactions/optimisticApply.d.ts +49 -0
- package/dist/transactions/optimisticApply.js +65 -0
- package/dist/transactions/{persistedReplay.d.ts → replayValidation.d.ts} +28 -22
- package/dist/transactions/{persistedReplay.js → replayValidation.js} +33 -27
- package/dist/types/global.d.ts +46 -41
- package/dist/types/global.js +20 -19
- package/dist/types/index.d.ts +71 -77
- package/dist/types/index.js +22 -22
- package/dist/types/modelData.d.ts +6 -8
- package/dist/types/modelData.js +5 -7
- package/dist/types/participant.d.ts +10 -11
- package/dist/types/participant.js +6 -8
- package/dist/types/streams.d.ts +208 -195
- 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} +42 -98
- 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 +30 -31
- package/dist/wire/errorEnvelope.js +34 -40
- package/dist/wire/frames.d.ts +79 -86
- package/dist/wire/frames.js +26 -33
- package/dist/wire/index.d.ts +14 -12
- package/dist/wire/index.js +30 -26
- package/dist/wire/listEnvelope.d.ts +16 -23
- package/dist/wire/listEnvelope.js +7 -6
- package/dist/wire/protocol.d.ts +25 -32
- package/dist/wire/protocol.js +25 -32
- package/dist/wire/protocolVersion.d.ts +44 -40
- package/dist/wire/protocolVersion.js +44 -40
- package/docs/coordination.md +59 -0
- package/package.json +11 -10
- package/dist/ai-sdk/coordinated-tool.d.ts +0 -101
- package/dist/ai-sdk/coordination-context.d.ts +0 -52
- package/dist/core/query-utils.d.ts +0 -34
- package/dist/core/query-utils.js +0 -59
- package/dist/schema/sync-delta-row.js +0 -103
- package/dist/schema/sync-delta-wire.js +0 -102
- 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 -8
- package/dist/server/storage-mode.js +0 -28
- package/dist/source/connector-protocol.d.ts +0 -159
- package/dist/source/connector-protocol.js +0 -161
- package/dist/transactions/OptimisticEchoTracker.d.ts +0 -82
- package/dist/transactions/OptimisticEchoTracker.js +0 -104
- package/dist/transactions/mutation-error-handler.d.ts +0 -5
- package/dist/transactions/mutation-error-handler.js +0 -39
- package/dist/transactions/optimistic.d.ts +0 -24
- package/dist/transactions/optimistic.js +0 -45
- package/dist/utils/mobx-setup.d.ts +0 -42
package/dist/Database.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* The local persistence layer for synced models. It stores rows in the
|
|
3
|
+
* browser's IndexedDB (or in-memory maps when run headlessly), applies inbound
|
|
4
|
+
* deltas to that store, and fetches the bootstrap snapshot from your sync
|
|
5
|
+
* server. {@link BaseSyncedStore} drives it, and {@link InstanceCache} holds the
|
|
6
|
+
* in-memory mirror of what this class persists.
|
|
4
7
|
*/
|
|
5
8
|
import { type DatabaseInfo, type WorkspaceMetadata } from './core/DatabaseManager.js';
|
|
6
9
|
import { ModelRegistry } from './ModelRegistry.js';
|
|
7
10
|
import { LoadStrategy } from './types/index.js';
|
|
8
|
-
import type {
|
|
11
|
+
import type { BootstrapFetcher, BootstrapData } from './sync/BootstrapFetcher.js';
|
|
9
12
|
import { InMemoryObjectStore } from './adapters/inMemoryStorage.js';
|
|
10
13
|
/** Generic record type for model data */
|
|
11
14
|
type ModelData = Record<string, unknown>;
|
|
@@ -43,19 +46,17 @@ interface PersistedTransaction {
|
|
|
43
46
|
[key: string]: unknown;
|
|
44
47
|
}
|
|
45
48
|
/**
|
|
46
|
-
*
|
|
49
|
+
* How a session establishes its baseline state at startup.
|
|
47
50
|
*
|
|
48
|
-
* 'full'
|
|
49
|
-
*
|
|
50
|
-
* - Clear IndexedDB
|
|
51
|
-
* - Load snapshot data
|
|
52
|
-
* - Use snapshot's lastSyncId
|
|
51
|
+
* 'full' — Fetch a complete snapshot from the server, clear the local store,
|
|
52
|
+
* load the snapshot, and adopt its `lastSyncId`.
|
|
53
53
|
*
|
|
54
|
-
* '
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
54
|
+
* 'partial' — Fetch only the deltas since the stored `lastSyncId` and apply
|
|
55
|
+
* them on top of the existing local data.
|
|
56
|
+
*
|
|
57
|
+
* 'local' — Skip the server entirely: hydrate the {@link InstanceCache} from the
|
|
58
|
+
* local store, connect the WebSocket with the stored `lastSyncId`, and
|
|
59
|
+
* receive deltas from there onward. Used when offline with valid local data.
|
|
59
60
|
*/
|
|
60
61
|
export type BootstrapType = 'full' | 'partial' | 'local';
|
|
61
62
|
export interface BootstrapRequirements {
|
|
@@ -67,7 +68,7 @@ export interface BootstrapRequirements {
|
|
|
67
68
|
export interface BootstrapResult {
|
|
68
69
|
modelsLoaded: number;
|
|
69
70
|
modelsStored: number;
|
|
70
|
-
/** The raw bootstrap response — callers can apply models directly to
|
|
71
|
+
/** The raw bootstrap response — callers can apply models directly to InstanceCache */
|
|
71
72
|
bootstrapData: BootstrapData;
|
|
72
73
|
/**
|
|
73
74
|
* Results of applying partial-bootstrap deltas to IDB. Present only when
|
|
@@ -90,13 +91,13 @@ export declare class Database {
|
|
|
90
91
|
private modelRegistry;
|
|
91
92
|
private bootstrapHelper;
|
|
92
93
|
/** The pre-configured query helper for lazy-loading data from the sync server. */
|
|
93
|
-
get helper():
|
|
94
|
+
get helper(): BootstrapFetcher;
|
|
94
95
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
96
|
+
* Fetch the current rows of the given sync groups as a side-effect-free
|
|
97
|
+
* snapshot, used to hydrate a scope as the user enters it. Unlike
|
|
97
98
|
* {@link bootstrapFromServer}, it does not persist to IndexedDB and does not
|
|
98
|
-
*
|
|
99
|
-
*
|
|
99
|
+
* change the connection's subscribed sync groups. The caller applies the
|
|
100
|
+
* result to the pool through the scoped apply path.
|
|
100
101
|
*/
|
|
101
102
|
fetchScopedBootstrapData(syncGroups: readonly string[]): Promise<BootstrapData>;
|
|
102
103
|
private currentDbInfo;
|
|
@@ -128,7 +129,7 @@ export declare class Database {
|
|
|
128
129
|
private inMemoryStores;
|
|
129
130
|
/** In-memory workspace metadata when inMemory=true. */
|
|
130
131
|
private inMemoryMetadata;
|
|
131
|
-
constructor(modelRegistry: ModelRegistry, bootstrapHelper:
|
|
132
|
+
constructor(modelRegistry: ModelRegistry, bootstrapHelper: BootstrapFetcher, options?: {
|
|
132
133
|
inMemory?: boolean;
|
|
133
134
|
});
|
|
134
135
|
/**
|
|
@@ -150,17 +151,14 @@ export declare class Database {
|
|
|
150
151
|
private logPreservedFields;
|
|
151
152
|
open(userId: string, organizationId: string, version?: number): Promise<void>;
|
|
152
153
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* ARCHITECTURE: By design, this method receives plain objects, not MobX observables:
|
|
159
|
-
* - WebSocket deltas: Already JSON-parsed (SyncedStore.ts:889)
|
|
160
|
-
* - Optimistic updates: Models call toJSON() which uses toJS() (SlideLayer.ts:224)
|
|
161
|
-
* - Bootstrap data: Plain JSON from server
|
|
154
|
+
* Shrink a record before persisting it. Drops `undefined` fields, empty
|
|
155
|
+
* arrays, empty objects, and the redundant markers `__typename`, `__class`,
|
|
156
|
+
* `clientId`, and `syncStatus`. Explicit `null` values are preserved, since
|
|
157
|
+
* a null is a meaningful "clear this field" in a nullable column.
|
|
162
158
|
*
|
|
163
|
-
*
|
|
159
|
+
* By design this receives plain objects, never live observables: WebSocket
|
|
160
|
+
* deltas arrive already parsed, optimistic updates come through `toJSON()`,
|
|
161
|
+
* and bootstrap data is plain JSON from the server.
|
|
164
162
|
*/
|
|
165
163
|
private compactRecord;
|
|
166
164
|
/**
|
|
@@ -174,7 +172,9 @@ export declare class Database {
|
|
|
174
172
|
*/
|
|
175
173
|
requiredBootstrap(): Promise<BootstrapRequirements>;
|
|
176
174
|
/**
|
|
177
|
-
*
|
|
175
|
+
* Fetch a bootstrap snapshot (or delta batch) from the sync server and load
|
|
176
|
+
* it into the local store, then return a {@link BootstrapResult} the caller
|
|
177
|
+
* applies to the {@link InstanceCache}.
|
|
178
178
|
*/
|
|
179
179
|
bootstrapFromServer(requirements: BootstrapRequirements,
|
|
180
180
|
/** Full sync-group subscription list — what the WS subscribes to
|
|
@@ -183,17 +183,16 @@ export declare class Database {
|
|
|
183
183
|
* team-derived groups. */
|
|
184
184
|
syncGroups: readonly string[], onProgress?: (loaded: number) => void): Promise<BootstrapResult>;
|
|
185
185
|
/**
|
|
186
|
-
*
|
|
186
|
+
* Apply a single inbound delta from the WebSocket to the local store.
|
|
187
187
|
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
188
|
+
* This handles one delta at a time. To apply several, prefer
|
|
189
|
+
* {@link processDeltaBatch}, which commits them in one IndexedDB transaction
|
|
190
|
+
* rather than two transactions per delta.
|
|
191
191
|
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* - Explicit null values ARE preserved: {position: null} clears the field
|
|
192
|
+
* Update deltas carry only the changed fields, so they are merged onto the
|
|
193
|
+
* existing record rather than replacing it. That preserves fields the delta
|
|
194
|
+
* omits (such as deckId or title), and an explicit null is kept as a value,
|
|
195
|
+
* clearing that field.
|
|
197
196
|
*/
|
|
198
197
|
processDelta(delta: {
|
|
199
198
|
syncId?: number;
|
|
@@ -214,27 +213,20 @@ export declare class Database {
|
|
|
214
213
|
data?: ModelData | null;
|
|
215
214
|
}>;
|
|
216
215
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* 2. Opening a single transaction per store for all writes
|
|
222
|
-
* 3. Merging UPDATE deltas with existing data to preserve unmodified fields
|
|
223
|
-
* 4. Updating metadata only once at the end with highest syncId
|
|
224
|
-
*
|
|
225
|
-
* Performance impact: 186 deltas goes from ~372 transactions to just 1 transaction
|
|
216
|
+
* Apply many deltas to the local store in as few IndexedDB transactions as
|
|
217
|
+
* possible. Deltas are grouped by store, and each store's writes commit in a
|
|
218
|
+
* single transaction, so a batch of 186 deltas becomes roughly one
|
|
219
|
+
* transaction per store instead of two per delta.
|
|
226
220
|
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* - Handles explicit null: {field: null} clears the field correctly
|
|
221
|
+
* The method reads the existing records for update deltas up front, then
|
|
222
|
+
* merges each update onto its existing record so fields the delta omits are
|
|
223
|
+
* preserved and an explicit null still clears its field. It advances the
|
|
224
|
+
* persisted sync cursor once, to the highest committed sync id.
|
|
232
225
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* - Prevents 404 errors from fetching already-deleted entities
|
|
226
|
+
* Conflict resolution follows a delete-wins rule: it first indexes the
|
|
227
|
+
* delete deltas by entity, then skips any insert or update whose sync id is
|
|
228
|
+
* at or below a delete for the same entity. This avoids resurrecting a
|
|
229
|
+
* deleted entity and avoids fetching one that no longer exists.
|
|
238
230
|
*/
|
|
239
231
|
processDeltaBatch(deltas: {
|
|
240
232
|
syncId?: number;
|
|
@@ -268,7 +260,7 @@ export declare class Database {
|
|
|
268
260
|
/**
|
|
269
261
|
* Highest syncId whose IDB store transaction actually committed in this
|
|
270
262
|
* batch. The runtime delta cursor (WS `lastSyncId`, server-side
|
|
271
|
-
* `lastAckedSyncId`) must only advance to
|
|
263
|
+
* `lastAckedSyncId`) must only advance to this value — not the input
|
|
272
264
|
* batch's range max — or it diverges from the persisted view and the
|
|
273
265
|
* next catch-up request skips the un-persisted gap forever. Mirrors
|
|
274
266
|
* the metadata-cursor invariant at `updateWorkspaceMetadata` below.
|
|
@@ -283,13 +275,7 @@ export declare class Database {
|
|
|
283
275
|
/** Get data by index. `value` is an IDB key — string, number, Date,
|
|
284
276
|
* BufferSource, or array thereof. */
|
|
285
277
|
getDataByIndex(modelName: string, indexName: string, value: IDBValidKey): Promise<ModelData[]>;
|
|
286
|
-
/**
|
|
287
|
-
* Update workspace metadata
|
|
288
|
-
*/
|
|
289
|
-
/**
|
|
290
|
-
* Get the last sync ID from workspace metadata
|
|
291
|
-
*/
|
|
292
|
-
/** Read workspace metadata from IDB (returns null if db not open). */
|
|
278
|
+
/** Read workspace metadata from IndexedDB. Returns null when the database is not open. */
|
|
293
279
|
getWorkspaceMetadata(): Promise<WorkspaceMetadata | null>;
|
|
294
280
|
getLastSyncId(): Promise<number>;
|
|
295
281
|
updateWorkspaceMetadata(metadata: Partial<WorkspaceMetadata>): Promise<void>;
|
package/dist/Database.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* The local persistence layer for synced models. It stores rows in the
|
|
3
|
+
* browser's IndexedDB (or in-memory maps when run headlessly), applies inbound
|
|
4
|
+
* deltas to that store, and fetches the bootstrap snapshot from your sync
|
|
5
|
+
* server. {@link BaseSyncedStore} drives it, and {@link InstanceCache} holds the
|
|
6
|
+
* in-memory mirror of what this class persists.
|
|
4
7
|
*/
|
|
5
8
|
import { DatabaseManager } from './core/DatabaseManager.js';
|
|
6
9
|
import { StoreManager } from './core/StoreManager.js';
|
|
@@ -22,11 +25,11 @@ export class Database {
|
|
|
22
25
|
return this.bootstrapHelper;
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
28
|
+
* Fetch the current rows of the given sync groups as a side-effect-free
|
|
29
|
+
* snapshot, used to hydrate a scope as the user enters it. Unlike
|
|
27
30
|
* {@link bootstrapFromServer}, it does not persist to IndexedDB and does not
|
|
28
|
-
*
|
|
29
|
-
*
|
|
31
|
+
* change the connection's subscribed sync groups. The caller applies the
|
|
32
|
+
* result to the pool through the scoped apply path.
|
|
30
33
|
*/
|
|
31
34
|
async fetchScopedBootstrapData(syncGroups) {
|
|
32
35
|
// No lastSyncId → a full snapshot of exactly these groups.
|
|
@@ -156,17 +159,14 @@ export class Database {
|
|
|
156
159
|
getContext().logger.info(`Database opened: ${this.currentDbInfo.name} (${readiness.readyStores.length}/${readiness.totalStores} stores ready)`);
|
|
157
160
|
}
|
|
158
161
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
162
|
+
* Shrink a record before persisting it. Drops `undefined` fields, empty
|
|
163
|
+
* arrays, empty objects, and the redundant markers `__typename`, `__class`,
|
|
164
|
+
* `clientId`, and `syncStatus`. Explicit `null` values are preserved, since
|
|
165
|
+
* a null is a meaningful "clear this field" in a nullable column.
|
|
163
166
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* - Bootstrap data: Plain JSON from server
|
|
168
|
-
*
|
|
169
|
-
* Note: We do NOT drop required defaults; server provides them.
|
|
167
|
+
* By design this receives plain objects, never live observables: WebSocket
|
|
168
|
+
* deltas arrive already parsed, optimistic updates come through `toJSON()`,
|
|
169
|
+
* and bootstrap data is plain JSON from the server.
|
|
170
170
|
*/
|
|
171
171
|
compactRecord(_modelName, data) {
|
|
172
172
|
if (!data || typeof data !== 'object')
|
|
@@ -177,8 +177,8 @@ export class Database {
|
|
|
177
177
|
if (key === '__typename' || key === '__class' || key === 'clientId' || key === 'syncStatus') {
|
|
178
178
|
continue;
|
|
179
179
|
}
|
|
180
|
-
//
|
|
181
|
-
//
|
|
180
|
+
// Skip only `undefined`; preserve explicit `null`, which is a
|
|
181
|
+
// meaningful value for a nullable column.
|
|
182
182
|
if (value === undefined) {
|
|
183
183
|
continue;
|
|
184
184
|
}
|
|
@@ -272,26 +272,24 @@ export class Database {
|
|
|
272
272
|
// point). Invalid → 0 → full bootstrap, the safe degradation.
|
|
273
273
|
const metadataLastSyncId = syncPositionSchema.shape.persisted.safeParse(metadata?.lastSyncId).data ?? 0;
|
|
274
274
|
const dataAge = metadata?.updatedAt ? Date.now() - metadata.updatedAt.getTime() : Infinity;
|
|
275
|
-
// ──
|
|
275
|
+
// ── Cache-validity check ─────────────────────────────────────
|
|
276
276
|
//
|
|
277
277
|
// The cursor (lastSyncId) is only valid if the data it refers to
|
|
278
|
-
// actually exists in the stores. If
|
|
279
|
-
// fresh in-memory session), the metadata's lastSyncId is
|
|
280
|
-
// sending it to the server would trigger a partial bootstrap
|
|
281
|
-
// returns zero deltas because the gap is 0, leaving the client
|
|
282
|
-
// with an empty
|
|
278
|
+
// actually exists in the stores. If the local store was cleared (or
|
|
279
|
+
// this is a fresh in-memory session), the metadata's lastSyncId is
|
|
280
|
+
// stale — sending it to the server would trigger a partial bootstrap
|
|
281
|
+
// that returns zero deltas because the gap is 0, leaving the client
|
|
282
|
+
// with an empty InstanceCache.
|
|
283
283
|
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
// property by sampling the actual stores — if they're empty, the
|
|
287
|
-
// cursor is meaningless regardless of what metadata claims.
|
|
284
|
+
// The fix is to sample the actual stores: if they hold no rows, the
|
|
285
|
+
// cursor is meaningless regardless of what the metadata claims.
|
|
288
286
|
const dataExists = this.inMemory
|
|
289
287
|
? false // In-memory mode: no persistent data across sessions
|
|
290
288
|
: await this.storeManager.hasAnyData();
|
|
291
289
|
// The effective lastSyncId: only trust the metadata cursor when
|
|
292
290
|
// we've confirmed the data it refers to actually exists in the stores.
|
|
293
291
|
const lastSyncId = dataExists ? metadataLastSyncId : 0;
|
|
294
|
-
//
|
|
292
|
+
// Log the resolved database state for diagnostics.
|
|
295
293
|
getContext().logger.debug('[Database.requiredBootstrap] State check', {
|
|
296
294
|
readinessReady: readiness.ready,
|
|
297
295
|
hasMetadata: !!metadata,
|
|
@@ -312,7 +310,8 @@ export class Database {
|
|
|
312
310
|
getContext().logger.info('Offline detected with local data - using local bootstrap');
|
|
313
311
|
}
|
|
314
312
|
else {
|
|
315
|
-
//
|
|
313
|
+
// The server is the source of truth: always use a full bootstrap
|
|
314
|
+
// when online.
|
|
316
315
|
type = 'full';
|
|
317
316
|
getContext().logger.info('Full bootstrap - server is source of truth', {
|
|
318
317
|
reason: offline ? 'offline_no_data' : 'server_authoritative',
|
|
@@ -329,7 +328,9 @@ export class Database {
|
|
|
329
328
|
};
|
|
330
329
|
}
|
|
331
330
|
/**
|
|
332
|
-
*
|
|
331
|
+
* Fetch a bootstrap snapshot (or delta batch) from the sync server and load
|
|
332
|
+
* it into the local store, then return a {@link BootstrapResult} the caller
|
|
333
|
+
* applies to the {@link InstanceCache}.
|
|
333
334
|
*/
|
|
334
335
|
async bootstrapFromServer(requirements,
|
|
335
336
|
/** Full sync-group subscription list — what the WS subscribes to
|
|
@@ -348,8 +349,8 @@ export class Database {
|
|
|
348
349
|
modelsToLoad: requirements.modelsToLoad,
|
|
349
350
|
});
|
|
350
351
|
try {
|
|
351
|
-
//
|
|
352
|
-
//
|
|
352
|
+
// Fetch before any destructive operation, so a failed network
|
|
353
|
+
// request can't leave the local store empty.
|
|
353
354
|
const startTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
354
355
|
getContext().logger.info('Fetching bootstrap data from server (before clearing local data)', {
|
|
355
356
|
type: requirements.type,
|
|
@@ -363,8 +364,9 @@ export class Database {
|
|
|
363
364
|
hasDeltas: !!bootstrapData.deltas,
|
|
364
365
|
deltaCount: bootstrapData.deltaCount ?? 0,
|
|
365
366
|
});
|
|
366
|
-
//
|
|
367
|
-
//
|
|
367
|
+
// Clear only after a successful fetch, for transactional safety.
|
|
368
|
+
// Clear when the server says the response is a full snapshot,
|
|
369
|
+
// regardless of what type was requested.
|
|
368
370
|
if (bootstrapData.type === 'full') {
|
|
369
371
|
await this.clear();
|
|
370
372
|
}
|
|
@@ -379,7 +381,7 @@ export class Database {
|
|
|
379
381
|
// Apply deltas to IndexedDB using processDeltaBatch for better performance.
|
|
380
382
|
// Capture the return value so the pool can be updated by the caller —
|
|
381
383
|
// without this, partial-bootstrap DELETEs persist to IDB but don't
|
|
382
|
-
// evict entities from the in-memory
|
|
384
|
+
// evict entities from the in-memory InstanceCache, leaving ghost rows
|
|
383
385
|
// visible on the canvas until a full reload rebuilds the pool.
|
|
384
386
|
let deltasApplied = 0;
|
|
385
387
|
let deltaResults;
|
|
@@ -501,17 +503,16 @@ export class Database {
|
|
|
501
503
|
}
|
|
502
504
|
// bootstrapSpecificModels removed per request
|
|
503
505
|
/**
|
|
504
|
-
*
|
|
506
|
+
* Apply a single inbound delta from the WebSocket to the local store.
|
|
505
507
|
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
508
|
+
* This handles one delta at a time. To apply several, prefer
|
|
509
|
+
* {@link processDeltaBatch}, which commits them in one IndexedDB transaction
|
|
510
|
+
* rather than two transactions per delta.
|
|
509
511
|
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
* - Explicit null values ARE preserved: {position: null} clears the field
|
|
512
|
+
* Update deltas carry only the changed fields, so they are merged onto the
|
|
513
|
+
* existing record rather than replacing it. That preserves fields the delta
|
|
514
|
+
* omits (such as deckId or title), and an explicit null is kept as a value,
|
|
515
|
+
* clearing that field.
|
|
515
516
|
*/
|
|
516
517
|
async processDelta(delta) {
|
|
517
518
|
const { actionType, modelName, modelId, data, syncId } = delta;
|
|
@@ -519,7 +520,7 @@ export class Database {
|
|
|
519
520
|
if (!store) {
|
|
520
521
|
return { action: 'verify', modelName, modelId };
|
|
521
522
|
}
|
|
522
|
-
//
|
|
523
|
+
// Idempotency gate: ignore already-applied deltas by comparing with the persisted lastSyncId
|
|
523
524
|
try {
|
|
524
525
|
const lastApplied = await this.getLastSyncId();
|
|
525
526
|
const incomingId = typeof syncId === 'number' ? syncId : undefined;
|
|
@@ -572,11 +573,12 @@ export class Database {
|
|
|
572
573
|
return { action: 'add', modelName, modelId, data: compacted };
|
|
573
574
|
}
|
|
574
575
|
case 'U': {
|
|
575
|
-
//
|
|
576
|
-
// Read existing record first
|
|
576
|
+
// Update: merge onto the existing record (partial-delta pattern).
|
|
577
|
+
// Read the existing record first.
|
|
577
578
|
const existing = await store.get(modelId);
|
|
578
|
-
//
|
|
579
|
-
//
|
|
579
|
+
// Skip the update when there's no existing record to merge with:
|
|
580
|
+
// building a record from partial update data would corrupt it
|
|
581
|
+
// (missing deckId, and so on).
|
|
580
582
|
if (!existing) {
|
|
581
583
|
getContext().observability.breadcrumb('Skipping UPDATE delta - no existing record to merge with', 'sync.database', 'warning', {
|
|
582
584
|
modelName,
|
|
@@ -619,7 +621,7 @@ export class Database {
|
|
|
619
621
|
getContext().observability.breadcrumb(`IndexedDB delete failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
|
|
620
622
|
error: err instanceof Error ? err.message : String(err),
|
|
621
623
|
});
|
|
622
|
-
// Surface failure so caller does not mutate
|
|
624
|
+
// Surface failure so caller does not mutate InstanceCache inconsistently
|
|
623
625
|
throw err;
|
|
624
626
|
}
|
|
625
627
|
return { action: 'remove', modelName, modelId };
|
|
@@ -659,27 +661,20 @@ export class Database {
|
|
|
659
661
|
}
|
|
660
662
|
}
|
|
661
663
|
/**
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
* 2. Opening a single transaction per store for all writes
|
|
667
|
-
* 3. Merging UPDATE deltas with existing data to preserve unmodified fields
|
|
668
|
-
* 4. Updating metadata only once at the end with highest syncId
|
|
664
|
+
* Apply many deltas to the local store in as few IndexedDB transactions as
|
|
665
|
+
* possible. Deltas are grouped by store, and each store's writes commit in a
|
|
666
|
+
* single transaction, so a batch of 186 deltas becomes roughly one
|
|
667
|
+
* transaction per store instead of two per delta.
|
|
669
668
|
*
|
|
670
|
-
*
|
|
669
|
+
* The method reads the existing records for update deltas up front, then
|
|
670
|
+
* merges each update onto its existing record so fields the delta omits are
|
|
671
|
+
* preserved and an explicit null still clears its field. It advances the
|
|
672
|
+
* persisted sync cursor once, to the highest committed sync id.
|
|
671
673
|
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
* - Handles explicit null: {field: null} clears the field correctly
|
|
677
|
-
*
|
|
678
|
-
* 🔄 LINEAR-STYLE CONFLICT RESOLUTION:
|
|
679
|
-
* - Builds a map of DELETE deltas with their syncIds
|
|
680
|
-
* - Before processing UPDATE/INSERT, checks for DELETE with higher syncId
|
|
681
|
-
* - Skips stale updates for entities that will be/were deleted
|
|
682
|
-
* - Prevents 404 errors from fetching already-deleted entities
|
|
674
|
+
* Conflict resolution follows a delete-wins rule: it first indexes the
|
|
675
|
+
* delete deltas by entity, then skips any insert or update whose sync id is
|
|
676
|
+
* at or below a delete for the same entity. This avoids resurrecting a
|
|
677
|
+
* deleted entity and avoids fetching one that no longer exists.
|
|
683
678
|
*/
|
|
684
679
|
async processDeltaBatch(deltas) {
|
|
685
680
|
if ((!this.workspaceDb && !this.inMemory) || this.isClosing || deltas.length === 0) {
|
|
@@ -722,14 +717,11 @@ export class Database {
|
|
|
722
717
|
}
|
|
723
718
|
// Prepare results aligned with input order
|
|
724
719
|
const results = new Array(deltas.length);
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
|
|
730
|
-
// for entities that have been cascade-deleted (where DELETE delta exists).
|
|
731
|
-
// ========================================================================
|
|
732
|
-
const deleteSyncIds = new Map(); // key: "ModelName:modelId" -> DELETE syncId
|
|
720
|
+
// Build a delete index for conflict resolution. When a delete has a sync
|
|
721
|
+
// id at or above a later insert or update for the same entity, that entity
|
|
722
|
+
// is not (re)created — which drops stale updates for cascade-deleted
|
|
723
|
+
// entities.
|
|
724
|
+
const deleteSyncIds = new Map(); // key: "ModelName:modelId" -> delete syncId
|
|
733
725
|
for (const delta of deltas) {
|
|
734
726
|
if (delta.actionType === 'D' && delta.syncId) {
|
|
735
727
|
const key = `${delta.modelName}:${delta.modelId}`;
|
|
@@ -749,18 +741,17 @@ export class Database {
|
|
|
749
741
|
}
|
|
750
742
|
// Group deltas by store for efficient transaction management.
|
|
751
743
|
//
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
//
|
|
744
|
+
// The method tracks two high-water marks: `highestSyncId` for the total
|
|
745
|
+
// range seen, and `highestPersistedSyncId`, accumulated only from deltas
|
|
746
|
+
// whose store transaction actually committed. The cursor advance (at
|
|
747
|
+
// `updateWorkspaceMetadata`) uses only the persisted one.
|
|
756
748
|
//
|
|
757
|
-
// Without this split, a single store-level
|
|
758
|
-
//
|
|
759
|
-
//
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
// recover it" failure mode.
|
|
749
|
+
// Without this split, a single store-level failure (a compacted record
|
|
750
|
+
// missing a required field, a validation abort) would advance the cursor
|
|
751
|
+
// past deltas that never wrote to IndexedDB. The next partial bootstrap
|
|
752
|
+
// would ask "what's new since {advanced cursor}?", the skipped rows would
|
|
753
|
+
// fall into the already-seen range forever, and the local store would stay
|
|
754
|
+
// permanently behind the server with no way to recover on reload.
|
|
764
755
|
const deltasByStore = new Map();
|
|
765
756
|
let highestSyncId = 0;
|
|
766
757
|
let highestPersistedSyncId = 0;
|
|
@@ -773,9 +764,8 @@ export class Database {
|
|
|
773
764
|
if (typeof deltaSyncIdNum === 'number' && !isNaN(deltaSyncIdNum) && deltaSyncIdNum > highestSyncId) {
|
|
774
765
|
highestSyncId = deltaSyncIdNum;
|
|
775
766
|
}
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
// ========================================================================
|
|
767
|
+
// Conflict check: skip an insert or update when a delete for the same
|
|
768
|
+
// entity has an equal or higher sync id.
|
|
779
769
|
if (delta.actionType === 'U' ||
|
|
780
770
|
delta.actionType === 'I' ||
|
|
781
771
|
delta.actionType === 'C' ||
|
|
@@ -823,8 +813,8 @@ export class Database {
|
|
|
823
813
|
if (!store)
|
|
824
814
|
continue;
|
|
825
815
|
try {
|
|
826
|
-
//
|
|
827
|
-
// Step 1: Identify which deltas need existing data (
|
|
816
|
+
// Batch read-modify-write.
|
|
817
|
+
// Step 1: Identify which deltas need existing data (updates)
|
|
828
818
|
const updateDeltas = storeDeltas.filter(({ delta }) => delta.actionType === 'U');
|
|
829
819
|
const updateIds = updateDeltas.map(({ delta }) => delta.modelId);
|
|
830
820
|
// Step 2: Batch read all existing records in a SINGLE IDB transaction
|
|
@@ -849,8 +839,9 @@ export class Database {
|
|
|
849
839
|
}
|
|
850
840
|
}
|
|
851
841
|
}
|
|
852
|
-
//
|
|
853
|
-
// Track
|
|
842
|
+
// Self-heal by fetching missing records for update deltas.
|
|
843
|
+
// Track ids that failed to fetch (a 404 means the entity was deleted,
|
|
844
|
+
// so its delta is skipped).
|
|
854
845
|
const failedToFetch = new Set();
|
|
855
846
|
if (missingIds.size > 0) {
|
|
856
847
|
getContext().logger.info(`[Database.processDeltaBatch] Found ${missingIds.size} missing records for ${modelName}, fetching from server...`);
|
|
@@ -924,13 +915,11 @@ export class Database {
|
|
|
924
915
|
});
|
|
925
916
|
break;
|
|
926
917
|
case 'U': {
|
|
927
|
-
//
|
|
918
|
+
// Update: merge the delta onto the existing record (already fetched).
|
|
928
919
|
const existing = existingRecords.get(modelId);
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
// Per Linear's architecture, skip it instead of creating incomplete data.
|
|
933
|
-
// ========================================================================
|
|
920
|
+
// Skip a stale update: if the entity is neither in the local
|
|
921
|
+
// store nor fetchable from the server (a 404), it was deleted,
|
|
922
|
+
// so skip it rather than create an incomplete record.
|
|
934
923
|
if (!existing && failedToFetch.has(modelId)) {
|
|
935
924
|
getContext().logger.debug('[Database.processDeltaBatch] Skipping UPDATE for deleted entity', {
|
|
936
925
|
modelName,
|
|
@@ -939,8 +928,9 @@ export class Database {
|
|
|
939
928
|
stagedResults.push({ action: 'verify', modelName, modelId, idx });
|
|
940
929
|
break; // Skip this delta
|
|
941
930
|
}
|
|
942
|
-
//
|
|
943
|
-
//
|
|
931
|
+
// Skip the update when there's no existing record to merge with:
|
|
932
|
+
// building a record from partial update data would corrupt it
|
|
933
|
+
// (missing deckId, and so on).
|
|
944
934
|
if (!existing) {
|
|
945
935
|
getContext().observability.breadcrumb('Batch: Skipping UPDATE delta - no existing record', 'sync.database', 'warning', {
|
|
946
936
|
modelName,
|
|
@@ -994,8 +984,8 @@ export class Database {
|
|
|
994
984
|
tx.onerror = () => { reject(tx.error); };
|
|
995
985
|
});
|
|
996
986
|
// Only commit staged results to the global results if the transaction succeeded.
|
|
997
|
-
//
|
|
998
|
-
//
|
|
987
|
+
// Advance `highestPersistedSyncId` only for deltas in this successful
|
|
988
|
+
// transaction, so the cursor can't advance past rows that never wrote to IDB.
|
|
999
989
|
for (const r of stagedResults) {
|
|
1000
990
|
// Resolve the originating delta so we can carry its
|
|
1001
991
|
// transactionId through to the result. Echo detection in
|
|
@@ -1104,13 +1094,7 @@ export class Database {
|
|
|
1104
1094
|
const store = this.getRequiredStore(modelName);
|
|
1105
1095
|
return await store.getAllFromIndex(indexName, value);
|
|
1106
1096
|
}
|
|
1107
|
-
/**
|
|
1108
|
-
* Update workspace metadata
|
|
1109
|
-
*/
|
|
1110
|
-
/**
|
|
1111
|
-
* Get the last sync ID from workspace metadata
|
|
1112
|
-
*/
|
|
1113
|
-
/** Read workspace metadata from IDB (returns null if db not open). */
|
|
1097
|
+
/** Read workspace metadata from IndexedDB. Returns null when the database is not open. */
|
|
1114
1098
|
async getWorkspaceMetadata() {
|
|
1115
1099
|
if (this.inMemory)
|
|
1116
1100
|
return this.inMemoryMetadata;
|