@abloatai/ablo 0.35.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +71 -1929
- package/NOTICE +2 -2
- package/README.md +23 -532
- package/assets/banner.png +0 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +2 -0
- package/dist/auth.js.map +1 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +3 -0
- package/dist/client.js.map +1 -0
- package/dist/coordination.d.ts +2 -0
- package/dist/coordination.d.ts.map +1 -0
- package/dist/coordination.js +2 -0
- package/dist/coordination.js.map +1 -0
- package/dist/index.d.ts +3 -112
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -161
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +4 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +3 -0
- package/dist/react.js.map +1 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +2 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -0
- package/dist/source-conformance.d.ts +2 -0
- package/dist/source-conformance.d.ts.map +1 -0
- package/dist/source-conformance.js +2 -0
- package/dist/source-conformance.js.map +1 -0
- package/dist/source-drizzle.d.ts +2 -0
- package/dist/source-drizzle.d.ts.map +1 -0
- package/dist/source-drizzle.js +2 -0
- package/dist/source-drizzle.js.map +1 -0
- package/dist/source-kysely.d.ts +2 -0
- package/dist/source-kysely.d.ts.map +1 -0
- package/dist/source-kysely.js +2 -0
- package/dist/source-kysely.js.map +1 -0
- package/dist/source-next.d.ts +2 -0
- package/dist/source-next.d.ts.map +1 -0
- package/dist/source-next.js +2 -0
- package/dist/source-next.js.map +1 -0
- package/dist/source.d.ts +2 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +2 -0
- package/dist/source.js.map +1 -0
- package/dist/wire.d.ts +2 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +2 -0
- package/dist/wire.js.map +1 -0
- package/docs/agents.md +2 -2
- package/docs/api-keys.md +13 -12
- package/docs/api.md +15 -53
- package/docs/audit.md +4 -3
- package/docs/cli.md +11 -11
- package/docs/client-behavior.md +9 -9
- package/docs/concurrency-convention.md +28 -42
- package/docs/coordination.md +228 -86
- package/docs/data-sources.md +5 -5
- package/docs/debugging.md +34 -12
- package/docs/deployment.md +8 -8
- package/docs/examples/agent-human.md +4 -4
- package/docs/examples/ai-sdk-tool.md +1 -1
- package/docs/examples/existing-python-backend.md +15 -4
- package/docs/examples/nextjs.md +27 -6
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/examples/server-agent.md +2 -2
- package/docs/groups.md +57 -3
- package/docs/guarantees.md +37 -10
- package/docs/how-it-works.md +32 -8
- package/docs/idempotency.md +6 -6
- package/docs/identity.md +24 -24
- package/docs/index.md +8 -8
- package/docs/integration-guide.md +38 -16
- package/docs/internal/README.md +18 -0
- package/docs/internal/agent-fleet-coordination-design.md +171 -0
- package/docs/internal/agent-orchestration.md +58 -0
- package/docs/internal/commit-identifiers.md +91 -0
- package/docs/internal/concurrency-open-decisions.md +37 -0
- package/docs/internal/data-source-reverse-channel.md +150 -0
- package/docs/internal/per-field-conflict-detection.md +165 -0
- package/docs/internal/postgres-replication.md +64 -0
- package/docs/internal/serializable-schema.md +119 -0
- package/docs/internal/structure.md +32 -0
- package/docs/mcp.md +9 -9
- package/docs/migration.md +37 -18
- package/docs/projects.md +1 -1
- package/docs/quickstart.md +2 -2
- package/docs/react.md +24 -13
- package/docs/schema-contract.md +3 -3
- package/docs/sessions.md +91 -37
- package/docs/webhooks.md +9 -9
- package/examples/README.md +2 -2
- package/examples/data-source/README.md +1 -1
- package/examples/data-source/ablo-driver.ts +1 -1
- package/examples/data-source/customer-server.ts +1 -1
- package/examples/data-source/run.ts +1 -1
- package/examples/data-source/schema.ts +1 -1
- package/examples/quickstart.ts +2 -2
- package/llms.txt +12 -12
- package/package.json +64 -174
- package/dist/BaseSyncedStore.d.ts +0 -823
- package/dist/BaseSyncedStore.js +0 -1955
- package/dist/Database.d.ts +0 -335
- package/dist/Database.js +0 -1500
- package/dist/InstanceCache.d.ts +0 -233
- package/dist/InstanceCache.js +0 -1164
- package/dist/LazyReferenceCollection.d.ts +0 -177
- package/dist/LazyReferenceCollection.js +0 -461
- package/dist/Model.d.ts +0 -444
- package/dist/Model.js +0 -909
- package/dist/ModelRegistry.d.ts +0 -221
- package/dist/ModelRegistry.js +0 -537
- package/dist/NetworkMonitor.d.ts +0 -26
- package/dist/NetworkMonitor.js +0 -77
- package/dist/RuntimeContext.d.ts +0 -52
- package/dist/RuntimeContext.js +0 -80
- package/dist/SyncClient.d.ts +0 -551
- package/dist/SyncClient.js +0 -2199
- package/dist/adapters/alwaysOnline.d.ts +0 -14
- package/dist/adapters/alwaysOnline.js +0 -17
- package/dist/adapters/inMemoryStorage.d.ts +0 -31
- package/dist/adapters/inMemoryStorage.js +0 -110
- package/dist/ai-sdk/coordinatedTool.d.ts +0 -120
- package/dist/ai-sdk/coordinatedTool.js +0 -134
- package/dist/ai-sdk/coordinationContext.d.ts +0 -46
- package/dist/ai-sdk/coordinationContext.js +0 -106
- package/dist/ai-sdk/index.d.ts +0 -121
- package/dist/ai-sdk/index.js +0 -121
- package/dist/ai-sdk/wrap.d.ts +0 -65
- package/dist/ai-sdk/wrap.js +0 -39
- package/dist/auth/index.d.ts +0 -1
- package/dist/auth/index.js +0 -8
- package/dist/batching/index.d.ts +0 -55
- package/dist/batching/index.js +0 -147
- package/dist/cli.cjs +0 -288600
- package/dist/client/Ablo.d.ts +0 -231
- package/dist/client/Ablo.js +0 -149
- package/dist/client/abloClient.d.ts +0 -309
- package/dist/client/abloClient.js +0 -13
- package/dist/client/clientPrelude.d.ts +0 -52
- package/dist/client/clientPrelude.js +0 -60
- package/dist/client/consoleLogger.d.ts +0 -35
- package/dist/client/consoleLogger.js +0 -44
- package/dist/client/coreClient.d.ts +0 -60
- package/dist/client/coreClient.js +0 -118
- package/dist/client/createInternalComponents.d.ts +0 -46
- package/dist/client/createInternalComponents.js +0 -92
- package/dist/client/createModelProxy.d.ts +0 -228
- package/dist/client/createModelProxy.js +0 -818
- package/dist/client/humans.d.ts +0 -48
- package/dist/client/humans.js +0 -52
- package/dist/client/modelRegistration.d.ts +0 -10
- package/dist/client/modelRegistration.js +0 -312
- package/dist/client/options.d.ts +0 -461
- package/dist/client/options.js +0 -7
- package/dist/client/reactiveEngine.d.ts +0 -48
- package/dist/client/reactiveEngine.js +0 -910
- package/dist/client/resourceTypes.d.ts +0 -12
- package/dist/client/resourceTypes.js +0 -10
- package/dist/client/schemaConfig.d.ts +0 -44
- package/dist/client/schemaConfig.js +0 -185
- package/dist/client/validateAbloOptions.d.ts +0 -42
- package/dist/client/validateAbloOptions.js +0 -43
- package/dist/client/wsMutationExecutor.d.ts +0 -27
- package/dist/client/wsMutationExecutor.js +0 -72
- package/dist/context.d.ts +0 -29
- package/dist/context.js +0 -58
- package/dist/coordination/ClaimLog.d.ts +0 -26
- package/dist/coordination/ClaimLog.js +0 -32
- package/dist/coordination/index.d.ts +0 -1
- package/dist/coordination/index.js +0 -8
- package/dist/core/DatabaseManager.d.ts +0 -105
- package/dist/core/DatabaseManager.js +0 -387
- package/dist/core/QueryProcessor.d.ts +0 -75
- package/dist/core/QueryProcessor.js +0 -255
- package/dist/core/QueryView.d.ts +0 -79
- package/dist/core/QueryView.js +0 -218
- package/dist/core/StoreManager.d.ts +0 -112
- package/dist/core/StoreManager.js +0 -302
- package/dist/core/ViewRegistry.d.ts +0 -20
- package/dist/core/ViewRegistry.js +0 -55
- package/dist/core/index.d.ts +0 -33
- package/dist/core/index.js +0 -48
- package/dist/core/openIDBWithTimeout.d.ts +0 -65
- package/dist/core/openIDBWithTimeout.js +0 -153
- package/dist/core/queryUtils.d.ts +0 -45
- package/dist/core/queryUtils.js +0 -69
- package/dist/core/storeContract.d.ts +0 -145
- package/dist/core/storeContract.js +0 -12
- package/dist/docs/catalog.d.ts +0 -72
- package/dist/docs/catalog.js +0 -227
- package/dist/docs/index.d.ts +0 -10
- package/dist/docs/index.js +0 -10
- package/dist/environment.d.ts +0 -1
- package/dist/environment.js +0 -8
- package/dist/interfaces/index.d.ts +0 -311
- package/dist/interfaces/index.js +0 -9
- package/dist/keys/index.d.ts +0 -1
- package/dist/keys/index.js +0 -8
- package/dist/mutators/RecordingMutation.d.ts +0 -36
- package/dist/mutators/RecordingMutation.js +0 -182
- package/dist/mutators/Transaction.d.ts +0 -40
- package/dist/mutators/Transaction.js +0 -58
- package/dist/mutators/UndoManager.d.ts +0 -258
- package/dist/mutators/UndoManager.js +0 -658
- package/dist/mutators/defineMutators.d.ts +0 -60
- package/dist/mutators/defineMutators.js +0 -18
- package/dist/mutators/inverseOp.d.ts +0 -126
- package/dist/mutators/inverseOp.js +0 -71
- package/dist/mutators/mutateActions.d.ts +0 -45
- package/dist/mutators/mutateActions.js +0 -105
- package/dist/mutators/readerActions.d.ts +0 -33
- package/dist/mutators/readerActions.js +0 -57
- package/dist/mutators/undoApply.d.ts +0 -51
- package/dist/mutators/undoApply.js +0 -117
- package/dist/policy/index.d.ts +0 -21
- package/dist/policy/index.js +0 -20
- package/dist/query/client.d.ts +0 -61
- package/dist/query/client.js +0 -137
- package/dist/query/types.d.ts +0 -85
- package/dist/query/types.js +0 -16
- package/dist/react/AbloProvider.d.ts +0 -230
- package/dist/react/AbloProvider.js +0 -455
- package/dist/react/ClientSideSuspense.d.ts +0 -36
- package/dist/react/ClientSideSuspense.js +0 -17
- package/dist/react/DefaultFallback.d.ts +0 -24
- package/dist/react/DefaultFallback.js +0 -43
- package/dist/react/context.d.ts +0 -55
- package/dist/react/context.js +0 -29
- package/dist/react/index.d.ts +0 -61
- package/dist/react/index.js +0 -66
- package/dist/react/internalContext.d.ts +0 -33
- package/dist/react/internalContext.js +0 -3
- package/dist/react/useAblo.d.ts +0 -75
- package/dist/react/useAblo.js +0 -102
- package/dist/react/useCurrentUserId.d.ts +0 -22
- package/dist/react/useCurrentUserId.js +0 -34
- package/dist/react/useErrorListener.d.ts +0 -20
- package/dist/react/useErrorListener.js +0 -38
- package/dist/react/useMutationFailureListener.d.ts +0 -26
- package/dist/react/useMutationFailureListener.js +0 -38
- package/dist/react/useMutators.d.ts +0 -56
- package/dist/react/useMutators.js +0 -84
- package/dist/react/useReactive.d.ts +0 -35
- package/dist/react/useReactive.js +0 -123
- package/dist/react/useSyncStatus.d.ts +0 -59
- package/dist/react/useSyncStatus.js +0 -76
- package/dist/react/useUndoScope.d.ts +0 -34
- package/dist/react/useUndoScope.js +0 -81
- package/dist/schema/coordination.d.ts +0 -112
- package/dist/schema/coordination.js +0 -129
- package/dist/schema/ddl.d.ts +0 -97
- package/dist/schema/ddl.js +0 -491
- package/dist/schema/ddlLock.d.ts +0 -35
- package/dist/schema/ddlLock.js +0 -46
- package/dist/schema/diff.d.ts +0 -225
- package/dist/schema/diff.js +0 -289
- package/dist/schema/generate.d.ts +0 -19
- package/dist/schema/generate.js +0 -86
- package/dist/schema/index.d.ts +0 -41
- package/dist/schema/index.js +0 -76
- package/dist/schema/queries.d.ts +0 -201
- package/dist/schema/queries.js +0 -144
- package/dist/schema/select.d.ts +0 -40
- package/dist/schema/select.js +0 -87
- package/dist/schema/serialize.d.ts +0 -115
- package/dist/schema/serialize.js +0 -262
- package/dist/schema/sugar.d.ts +0 -109
- package/dist/schema/sugar.js +0 -83
- package/dist/schema/syncDeltaRow.d.ts +0 -6
- package/dist/schema/syncDeltaRow.js +0 -6
- package/dist/server/adapter.d.ts +0 -173
- package/dist/server/adapter.js +0 -18
- package/dist/server/commit.d.ts +0 -107
- package/dist/server/commit.js +0 -1
- package/dist/server/index.d.ts +0 -14
- package/dist/server/index.js +0 -2
- package/dist/server/readConfig.d.ts +0 -80
- package/dist/server/readConfig.js +0 -8
- package/dist/server/storageMode.d.ts +0 -23
- package/dist/server/storageMode.js +0 -17
- package/dist/source/adapter.d.ts +0 -81
- package/dist/source/adapter.js +0 -22
- package/dist/source/adapters/drizzle.d.ts +0 -48
- package/dist/source/adapters/drizzle.js +0 -219
- package/dist/source/adapters/kysely.d.ts +0 -42
- package/dist/source/adapters/kysely.js +0 -205
- package/dist/source/adapters/kyselyMutationCore.d.ts +0 -76
- package/dist/source/adapters/kyselyMutationCore.js +0 -125
- package/dist/source/adapters/memory.d.ts +0 -13
- package/dist/source/adapters/memory.js +0 -130
- package/dist/source/adapters/prisma.d.ts +0 -63
- package/dist/source/adapters/prisma.js +0 -202
- package/dist/source/conformance.d.ts +0 -37
- package/dist/source/conformance.js +0 -215
- package/dist/source/connector.d.ts +0 -95
- package/dist/source/connector.js +0 -266
- package/dist/source/connectorProtocol.d.ts +0 -154
- package/dist/source/connectorProtocol.js +0 -163
- package/dist/source/contract.d.ts +0 -195
- package/dist/source/contract.js +0 -164
- package/dist/source/factory.d.ts +0 -92
- package/dist/source/factory.js +0 -286
- package/dist/source/footprint.d.ts +0 -111
- package/dist/source/footprint.js +0 -0
- package/dist/source/idempotency.d.ts +0 -61
- package/dist/source/idempotency.js +0 -144
- package/dist/source/index.d.ts +0 -23
- package/dist/source/index.js +0 -30
- package/dist/source/migrations.d.ts +0 -21
- package/dist/source/migrations.js +0 -103
- package/dist/source/next.d.ts +0 -32
- package/dist/source/next.js +0 -25
- package/dist/source/pushQueue.d.ts +0 -134
- package/dist/source/pushQueue.js +0 -256
- package/dist/source/signing.d.ts +0 -92
- package/dist/source/signing.js +0 -162
- package/dist/source/types.d.ts +0 -401
- package/dist/source/types.js +0 -59
- package/dist/stores/ObjectStore.d.ts +0 -115
- package/dist/stores/ObjectStore.js +0 -393
- package/dist/stores/ObjectStoreContract.d.ts +0 -38
- package/dist/stores/ObjectStoreContract.js +0 -1
- package/dist/stores/SyncActionStore.d.ts +0 -97
- package/dist/stores/SyncActionStore.js +0 -504
- package/dist/stores/syncAction.d.ts +0 -26
- package/dist/stores/syncAction.js +0 -16
- package/dist/surface.d.ts +0 -36
- package/dist/surface.js +0 -75
- package/dist/sync/BootstrapFetcher.d.ts +0 -280
- package/dist/sync/BootstrapFetcher.js +0 -962
- package/dist/sync/ConnectionManager.d.ts +0 -8
- package/dist/sync/ConnectionManager.js +0 -8
- package/dist/sync/OnDemandLoader.d.ts +0 -228
- package/dist/sync/OnDemandLoader.js +0 -742
- package/dist/sync/SubscriptionManager.d.ts +0 -159
- package/dist/sync/SubscriptionManager.js +0 -243
- package/dist/sync/SyncWebSocket.d.ts +0 -173
- package/dist/sync/SyncWebSocket.js +0 -438
- package/dist/sync/awaitClaimGrant.d.ts +0 -6
- package/dist/sync/awaitClaimGrant.js +0 -6
- package/dist/sync/bootstrapApply.d.ts +0 -70
- package/dist/sync/bootstrapApply.js +0 -73
- package/dist/sync/commitFrames.d.ts +0 -8
- package/dist/sync/commitFrames.js +0 -8
- package/dist/sync/contextPorts.d.ts +0 -18
- package/dist/sync/contextPorts.js +0 -31
- package/dist/sync/createClaimStream.d.ts +0 -7
- package/dist/sync/createClaimStream.js +0 -7
- package/dist/sync/createPresenceStream.d.ts +0 -69
- package/dist/sync/createPresenceStream.js +0 -200
- package/dist/sync/createSnapshot.d.ts +0 -29
- package/dist/sync/createSnapshot.js +0 -118
- package/dist/sync/credentialLifecycle.d.ts +0 -7
- package/dist/sync/credentialLifecycle.js +0 -7
- package/dist/sync/deltaPipeline.d.ts +0 -113
- package/dist/sync/deltaPipeline.js +0 -261
- package/dist/sync/groupChange.d.ts +0 -113
- package/dist/sync/groupChange.js +0 -242
- package/dist/sync/participants.d.ts +0 -115
- package/dist/sync/participants.js +0 -344
- package/dist/sync/persistedPrefix.d.ts +0 -12
- package/dist/sync/persistedPrefix.js +0 -22
- package/dist/sync/schemaDrift.d.ts +0 -55
- package/dist/sync/schemaDrift.js +0 -53
- package/dist/sync/schemas.d.ts +0 -70
- package/dist/sync/schemas.js +0 -94
- package/dist/sync/syncCursor.d.ts +0 -40
- package/dist/sync/syncCursor.js +0 -55
- package/dist/sync/syncPlan.d.ts +0 -54
- package/dist/sync/syncPlan.js +0 -50
- package/dist/sync/wsFrameHandlers.d.ts +0 -8
- package/dist/sync/wsFrameHandlers.js +0 -8
- package/dist/testing/fixtures/bootstrap.d.ts +0 -49
- package/dist/testing/fixtures/bootstrap.js +0 -59
- package/dist/testing/fixtures/deltas.d.ts +0 -83
- package/dist/testing/fixtures/deltas.js +0 -136
- package/dist/testing/fixtures/httpResponses.d.ts +0 -70
- package/dist/testing/fixtures/httpResponses.js +0 -90
- package/dist/testing/fixtures/models.d.ts +0 -83
- package/dist/testing/fixtures/models.js +0 -272
- package/dist/testing/helpers/reactWrapper.d.ts +0 -69
- package/dist/testing/helpers/reactWrapper.js +0 -67
- package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
- package/dist/testing/helpers/syncEngineHarness.js +0 -73
- package/dist/testing/helpers/wait.d.ts +0 -30
- package/dist/testing/helpers/wait.js +0 -49
- package/dist/testing/index.d.ts +0 -23
- package/dist/testing/index.js +0 -33
- package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
- package/dist/testing/mocks/FakeDatabase.js +0 -10
- package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
- package/dist/testing/mocks/MockMutationExecutor.js +0 -186
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
- package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
- package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
- package/dist/testing/mocks/MockSyncContext.js +0 -72
- package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
- package/dist/testing/mocks/MockSyncStore.js +0 -171
- package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
- package/dist/testing/mocks/MockWebSocket.js +0 -118
- package/dist/transaction/ablo.d.ts +0 -88
- package/dist/transaction/ablo.js +0 -33
- package/dist/transaction/auth/apiKey.d.ts +0 -152
- package/dist/transaction/auth/apiKey.js +0 -419
- package/dist/transaction/auth/bootstrapScope.d.ts +0 -15
- package/dist/transaction/auth/bootstrapScope.js +0 -1
- package/dist/transaction/auth/capability.d.ts +0 -177
- package/dist/transaction/auth/capability.js +0 -199
- package/dist/transaction/auth/credentialEndpoint.d.ts +0 -61
- package/dist/transaction/auth/credentialEndpoint.js +0 -86
- package/dist/transaction/auth/credentialPolicy.d.ts +0 -148
- package/dist/transaction/auth/credentialPolicy.js +0 -125
- package/dist/transaction/auth/credentialSource.d.ts +0 -30
- package/dist/transaction/auth/credentialSource.js +0 -55
- package/dist/transaction/auth/hostedEndpoints.d.ts +0 -21
- package/dist/transaction/auth/hostedEndpoints.js +0 -21
- package/dist/transaction/auth/identity.d.ts +0 -55
- package/dist/transaction/auth/identity.js +0 -210
- package/dist/transaction/auth/index.d.ts +0 -162
- package/dist/transaction/auth/index.js +0 -304
- package/dist/transaction/auth/schemas.d.ts +0 -59
- package/dist/transaction/auth/schemas.js +0 -85
- package/dist/transaction/auth/sessionMint.d.ts +0 -28
- package/dist/transaction/auth/sessionMint.js +0 -85
- package/dist/transaction/coordination/awaitClaimGrant.d.ts +0 -49
- package/dist/transaction/coordination/awaitClaimGrant.js +0 -112
- package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +0 -50
- package/dist/transaction/coordination/claimHeartbeatLoop.js +0 -88
- package/dist/transaction/coordination/claimMeta.d.ts +0 -49
- package/dist/transaction/coordination/claimMeta.js +0 -52
- package/dist/transaction/coordination/createClaimStream.d.ts +0 -64
- package/dist/transaction/coordination/createClaimStream.js +0 -475
- package/dist/transaction/coordination/events.d.ts +0 -74
- package/dist/transaction/coordination/events.js +0 -7
- package/dist/transaction/coordination/index.d.ts +0 -19
- package/dist/transaction/coordination/index.js +0 -44
- package/dist/transaction/coordination/locator.d.ts +0 -83
- package/dist/transaction/coordination/locator.js +0 -82
- package/dist/transaction/coordination/schema.d.ts +0 -1473
- package/dist/transaction/coordination/schema.js +0 -1013
- package/dist/transaction/coordination/targetConflict.d.ts +0 -2
- package/dist/transaction/coordination/targetConflict.js +0 -103
- package/dist/transaction/coordination/trace.d.ts +0 -78
- package/dist/transaction/coordination/trace.js +0 -138
- package/dist/transaction/durableWrites.d.ts +0 -62
- package/dist/transaction/durableWrites.js +0 -71
- package/dist/transaction/environment.d.ts +0 -105
- package/dist/transaction/environment.js +0 -108
- package/dist/transaction/errorCodes.d.ts +0 -403
- package/dist/transaction/errorCodes.js +0 -480
- package/dist/transaction/errors.d.ts +0 -428
- package/dist/transaction/errors.js +0 -686
- package/dist/transaction/index.d.ts +0 -20
- package/dist/transaction/index.js +0 -20
- package/dist/transaction/keys/index.d.ts +0 -87
- package/dist/transaction/keys/index.js +0 -207
- package/dist/transaction/log/syncDeltaRow.d.ts +0 -158
- package/dist/transaction/log/syncDeltaRow.js +0 -95
- package/dist/transaction/logPosition.d.ts +0 -97
- package/dist/transaction/logPosition.js +0 -125
- package/dist/transaction/logger.d.ts +0 -16
- package/dist/transaction/logger.js +0 -7
- package/dist/transaction/observability.d.ts +0 -53
- package/dist/transaction/observability.js +0 -19
- package/dist/transaction/persistence.d.ts +0 -12
- package/dist/transaction/persistence.js +0 -11
- package/dist/transaction/plugin.d.ts +0 -192
- package/dist/transaction/plugin.js +0 -87
- package/dist/transaction/policy/types.d.ts +0 -217
- package/dist/transaction/policy/types.js +0 -126
- package/dist/transaction/resources/functionalUpdate.d.ts +0 -79
- package/dist/transaction/resources/functionalUpdate.js +0 -87
- package/dist/transaction/resources/httpResources.d.ts +0 -266
- package/dist/transaction/resources/httpResources.js +0 -7
- package/dist/transaction/resources/modelOperations.d.ts +0 -319
- package/dist/transaction/resources/modelOperations.js +0 -12
- package/dist/transaction/resources/mutationOptions.d.ts +0 -66
- package/dist/transaction/resources/mutationOptions.js +0 -9
- package/dist/transaction/resources/where.d.ts +0 -85
- package/dist/transaction/resources/where.js +0 -70
- package/dist/transaction/resources/writeOptionsSchema.d.ts +0 -47
- package/dist/transaction/resources/writeOptionsSchema.js +0 -73
- package/dist/transaction/schema/field.d.ts +0 -126
- package/dist/transaction/schema/field.js +0 -265
- package/dist/transaction/schema/loadStrategy.d.ts +0 -45
- package/dist/transaction/schema/loadStrategy.js +0 -46
- package/dist/transaction/schema/model.d.ts +0 -379
- package/dist/transaction/schema/model.js +0 -123
- package/dist/transaction/schema/openapi.d.ts +0 -57
- package/dist/transaction/schema/openapi.js +0 -340
- package/dist/transaction/schema/relation.d.ts +0 -199
- package/dist/transaction/schema/relation.js +0 -104
- package/dist/transaction/schema/residency.d.ts +0 -29
- package/dist/transaction/schema/residency.js +0 -25
- package/dist/transaction/schema/roles.d.ts +0 -249
- package/dist/transaction/schema/roles.js +0 -230
- package/dist/transaction/schema/schema.d.ts +0 -324
- package/dist/transaction/schema/schema.js +0 -305
- package/dist/transaction/schema/tenancy.d.ts +0 -139
- package/dist/transaction/schema/tenancy.js +0 -190
- package/dist/transaction/transactionLayer.d.ts +0 -82
- package/dist/transaction/transactionLayer.js +0 -24
- package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +0 -143
- package/dist/transaction/transactions/settlement/commitEnvelope.js +0 -161
- package/dist/transaction/transactions/settlement/httpCommitEnvelope.d.ts +0 -53
- package/dist/transaction/transactions/settlement/httpCommitEnvelope.js +0 -207
- package/dist/transaction/transactions/settlement/idempotencyKey.d.ts +0 -10
- package/dist/transaction/transactions/settlement/idempotencyKey.js +0 -9
- package/dist/transaction/transactions/settlement/pendingWrite.d.ts +0 -112
- package/dist/transaction/transactions/settlement/pendingWrite.js +0 -20
- package/dist/transaction/transport/commitFrames.d.ts +0 -90
- package/dist/transaction/transport/commitFrames.js +0 -134
- package/dist/transaction/transport/connectionManager.d.ts +0 -215
- package/dist/transaction/transport/connectionManager.js +0 -673
- package/dist/transaction/transport/credentialLifecycle.d.ts +0 -177
- package/dist/transaction/transport/credentialLifecycle.js +0 -324
- package/dist/transaction/transport/heartbeat.d.ts +0 -65
- package/dist/transaction/transport/heartbeat.js +0 -93
- package/dist/transaction/transport/httpClient.d.ts +0 -123
- package/dist/transaction/transport/httpClient.js +0 -145
- package/dist/transaction/transport/httpOptions.d.ts +0 -33
- package/dist/transaction/transport/httpOptions.js +0 -12
- package/dist/transaction/transport/httpTransport.d.ts +0 -8
- package/dist/transaction/transport/httpTransport.js +0 -1276
- package/dist/transaction/transport/networkProbe.d.ts +0 -84
- package/dist/transaction/transport/networkProbe.js +0 -207
- package/dist/transaction/transport/wsFrameHandlers.d.ts +0 -128
- package/dist/transaction/transport/wsFrameHandlers.js +0 -429
- package/dist/transaction/transport/wsTransport.d.ts +0 -576
- package/dist/transaction/transport/wsTransport.js +0 -1017
- package/dist/transaction/types/assertExact.d.ts +0 -17
- package/dist/transaction/types/assertExact.js +0 -1
- package/dist/transaction/types/global.d.ts +0 -107
- package/dist/transaction/types/global.js +0 -40
- package/dist/transaction/types/index.d.ts +0 -205
- package/dist/transaction/types/index.js +0 -56
- package/dist/transaction/types/modelData.d.ts +0 -10
- package/dist/transaction/types/modelData.js +0 -9
- package/dist/transaction/types/participant.d.ts +0 -20
- package/dist/transaction/types/participant.js +0 -10
- package/dist/transaction/types/streams.d.ts +0 -540
- package/dist/transaction/types/streams.js +0 -11
- package/dist/transaction/utils/asyncIterator.d.ts +0 -34
- package/dist/transaction/utils/asyncIterator.js +0 -135
- package/dist/transaction/utils/duration.d.ts +0 -25
- package/dist/transaction/utils/duration.js +0 -45
- package/dist/transaction/utils/json.d.ts +0 -57
- package/dist/transaction/utils/json.js +0 -276
- package/dist/transaction/wire/accountResponses.d.ts +0 -351
- package/dist/transaction/wire/accountResponses.js +0 -255
- package/dist/transaction/wire/auth.d.ts +0 -49
- package/dist/transaction/wire/auth.js +0 -57
- package/dist/transaction/wire/bootstrapReason.d.ts +0 -9
- package/dist/transaction/wire/bootstrapReason.js +0 -8
- package/dist/transaction/wire/claimEvent.d.ts +0 -76
- package/dist/transaction/wire/claimEvent.js +0 -73
- package/dist/transaction/wire/claims.d.ts +0 -463
- package/dist/transaction/wire/claims.js +0 -229
- package/dist/transaction/wire/commit.d.ts +0 -603
- package/dist/transaction/wire/commit.js +0 -321
- package/dist/transaction/wire/delta.d.ts +0 -250
- package/dist/transaction/wire/delta.js +0 -147
- package/dist/transaction/wire/errorEnvelope.d.ts +0 -72
- package/dist/transaction/wire/errorEnvelope.js +0 -123
- package/dist/transaction/wire/feedCursor.d.ts +0 -60
- package/dist/transaction/wire/feedCursor.js +0 -82
- package/dist/transaction/wire/feedEvent.d.ts +0 -177
- package/dist/transaction/wire/feedEvent.js +0 -39
- package/dist/transaction/wire/frames.d.ts +0 -194
- package/dist/transaction/wire/frames.js +0 -50
- package/dist/transaction/wire/inboundFrames.d.ts +0 -552
- package/dist/transaction/wire/inboundFrames.js +0 -116
- package/dist/transaction/wire/index.d.ts +0 -50
- package/dist/transaction/wire/index.js +0 -74
- package/dist/transaction/wire/listEnvelope.d.ts +0 -37
- package/dist/transaction/wire/listEnvelope.js +0 -42
- package/dist/transaction/wire/modelResponses.d.ts +0 -85
- package/dist/transaction/wire/modelResponses.js +0 -43
- package/dist/transaction/wire/protocol.d.ts +0 -38
- package/dist/transaction/wire/protocol.js +0 -38
- package/dist/transaction/wire/protocolVersion.d.ts +0 -73
- package/dist/transaction/wire/protocolVersion.js +0 -83
- package/dist/transactions/mutations/MutationQueue.d.ts +0 -655
- package/dist/transactions/mutations/MutationQueue.js +0 -2797
- package/dist/transactions/mutations/MutationStore.d.ts +0 -20
- package/dist/transactions/mutations/MutationStore.js +0 -53
- package/dist/transactions/mutations/UnconfirmedWrites.d.ts +0 -82
- package/dist/transactions/mutations/UnconfirmedWrites.js +0 -104
- package/dist/transactions/mutations/coalesceRules.d.ts +0 -58
- package/dist/transactions/mutations/coalesceRules.js +0 -140
- package/dist/transactions/mutations/commitLatency.d.ts +0 -52
- package/dist/transactions/mutations/commitLatency.js +0 -130
- package/dist/transactions/mutations/commitOutboxStore.d.ts +0 -28
- package/dist/transactions/mutations/commitOutboxStore.js +0 -26
- package/dist/transactions/mutations/commitPayload.d.ts +0 -164
- package/dist/transactions/mutations/commitPayload.js +0 -152
- package/dist/transactions/mutations/deltaConfirmation.d.ts +0 -59
- package/dist/transactions/mutations/deltaConfirmation.js +0 -233
- package/dist/transactions/mutations/durableWriteStore.d.ts +0 -14
- package/dist/transactions/mutations/durableWriteStore.js +0 -12
- package/dist/transactions/mutations/optimisticApply.d.ts +0 -49
- package/dist/transactions/mutations/optimisticApply.js +0 -65
- package/dist/transactions/mutations/replayValidation.d.ts +0 -186
- package/dist/transactions/mutations/replayValidation.js +0 -163
- package/dist/utils/mobxSetup.d.ts +0 -53
- package/dist/utils/mobxSetup.js +0 -330
- package/dist/webhooks/events.d.ts +0 -43
- package/dist/webhooks/events.js +0 -42
- package/dist/webhooks/index.d.ts +0 -8
- package/dist/webhooks/index.js +0 -8
- package/dist/wire/index.d.ts +0 -1
- package/dist/wire/index.js +0 -8
- package/docs/interaction-model.md +0 -99
package/dist/InstanceCache.js
DELETED
|
@@ -1,1164 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* InstanceCache is the in-memory cache of live model instances, keyed by id and
|
|
3
|
-
* deduplicated so each entity has a single instance. It maintains type and
|
|
4
|
-
* foreign-key indexes for fast lookups, evicts entries under a size cap, and
|
|
5
|
-
* notifies subscribers and query views as models are added, updated, or
|
|
6
|
-
* removed. It holds a {@link ModelRegistry} to map between model names and
|
|
7
|
-
* constructor classes, but performs no persistence of its own.
|
|
8
|
-
*/
|
|
9
|
-
import { makeObservable, observable, action, computed, runInAction } from 'mobx';
|
|
10
|
-
import { Model } from './Model.js';
|
|
11
|
-
import { ModelRegistry } from './ModelRegistry.js';
|
|
12
|
-
import { getContext } from './context.js';
|
|
13
|
-
import { AbloValidationError } from './transaction/errors.js';
|
|
14
|
-
import { ModelScope } from './transaction/types/index.js';
|
|
15
|
-
import { ViewRegistry } from './core/ViewRegistry.js';
|
|
16
|
-
import { QueryView } from './core/QueryView.js';
|
|
17
|
-
// Re-exported so `import { ModelScope } from './InstanceCache.js'` resolves
|
|
18
|
-
export { ModelScope };
|
|
19
|
-
/**
|
|
20
|
-
* The in-memory cache of model instances, keyed by id and deduplicated so each
|
|
21
|
-
* entity resolves to a single instance.
|
|
22
|
-
*/
|
|
23
|
-
export class InstanceCache {
|
|
24
|
-
// Single source of truth for all models (observable for reactivity)
|
|
25
|
-
entries = observable.map();
|
|
26
|
-
typeIndex = observable.map();
|
|
27
|
-
// Non-observable access time tracking — kept outside observable.map so that
|
|
28
|
-
// updating timestamps in get() during React render does NOT trigger MobX
|
|
29
|
-
// reactions (which would cause infinite re-render loops).
|
|
30
|
-
accessTimes = new Map();
|
|
31
|
-
// Deduplication tracking
|
|
32
|
-
recentAdditions = new Map(); // "modelType:modelId" -> timestamp
|
|
33
|
-
deltaHistory = new Map();
|
|
34
|
-
// No intermediate cache layer — getByType() reads typeIndex and entries
|
|
35
|
-
// directly. Both are observable, so the data structures are themselves the
|
|
36
|
-
// reactivity source; there are no computed getters with conditional cache
|
|
37
|
-
// invalidation to get wrong.
|
|
38
|
-
// Foreign key indexes: Map<"ModelType:fieldName", Map<fieldValue, ObservableSet<modelId>>>
|
|
39
|
-
// Enables O(1) lookups like "all Block models where sectionId = X"
|
|
40
|
-
// instead of scanning all models of a type and filtering.
|
|
41
|
-
foreignKeyIndexes = new Map();
|
|
42
|
-
// Registry of which fields to index: Map<modelName, fieldName[]>
|
|
43
|
-
foreignKeyConfig = new Map();
|
|
44
|
-
// Performance tracking
|
|
45
|
-
metrics = {
|
|
46
|
-
hits: 0,
|
|
47
|
-
misses: 0,
|
|
48
|
-
evictions: 0,
|
|
49
|
-
additions: 0,
|
|
50
|
-
duplicatesSkipped: 0,
|
|
51
|
-
};
|
|
52
|
-
// Configuration
|
|
53
|
-
config;
|
|
54
|
-
gcTimer;
|
|
55
|
-
// ModelRegistry instance — single source of truth for model metadata
|
|
56
|
-
registry;
|
|
57
|
-
// ViewRegistry — tracks active QueryViews for incremental view maintenance
|
|
58
|
-
viewRegistry = new ViewRegistry();
|
|
59
|
-
// Subscription registry
|
|
60
|
-
subscriptions = new Map();
|
|
61
|
-
/**
|
|
62
|
-
* Subscribe to updates for a specific model type.
|
|
63
|
-
*/
|
|
64
|
-
subscribe(modelClass, callback) {
|
|
65
|
-
const modelName = this.registry.getModelNameFromConstructor(modelClass);
|
|
66
|
-
if (!modelName) {
|
|
67
|
-
throw new AbloValidationError(`Model class not registered: ${modelClass.name}`, {
|
|
68
|
-
code: 'pool_subscribe_unregistered',
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
let subs = this.subscriptions.get(modelName);
|
|
72
|
-
if (!subs) {
|
|
73
|
-
subs = new Set();
|
|
74
|
-
this.subscriptions.set(modelName, subs);
|
|
75
|
-
}
|
|
76
|
-
const erased = callback;
|
|
77
|
-
subs.add(erased);
|
|
78
|
-
return () => subs.delete(erased);
|
|
79
|
-
}
|
|
80
|
-
notifySubscribers(model) {
|
|
81
|
-
const modelName = model.getModelName();
|
|
82
|
-
const subs = this.subscriptions.get(modelName);
|
|
83
|
-
if (subs) {
|
|
84
|
-
for (const callback of subs) {
|
|
85
|
-
callback(model);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
constructor(config = {}, modelRegistry) {
|
|
90
|
-
this.config = {
|
|
91
|
-
maxSize: config.maxSize ?? 10000,
|
|
92
|
-
// Idle-eviction disabled by default. The 5-minute default used to
|
|
93
|
-
// live here, but with schema-driven dynamic classes not
|
|
94
|
-
// registering `LazyReferenceCollection`s, the
|
|
95
|
-
// `hasObservedCollections()` guard in gc() didn't fire for most
|
|
96
|
-
// actively-rendered models — and they'd evict out from under a
|
|
97
|
-
// user whose tab sat for 10 minutes. Memory pressure relief is
|
|
98
|
-
// handled by the `maxSize` LRU cap (see `evictOldest`), which is
|
|
99
|
-
// the bound that actually matches usage: "keep the most recent N
|
|
100
|
-
// entities, not the entities touched in the last N minutes."
|
|
101
|
-
//
|
|
102
|
-
// Callers who genuinely want time-based eviction can pass an
|
|
103
|
-
// explicit `maxAge`. Leaving the default at Infinity keeps
|
|
104
|
-
// correctness as the default and makes aggressive GC an opt-in.
|
|
105
|
-
maxAge: config.maxAge ?? Number.POSITIVE_INFINITY,
|
|
106
|
-
gcInterval: config.gcInterval ?? 60000, // 1 minute
|
|
107
|
-
useWeakRefs: config.useWeakRefs ?? true,
|
|
108
|
-
};
|
|
109
|
-
// Store the model registry reference
|
|
110
|
-
if (!modelRegistry) {
|
|
111
|
-
throw new AbloValidationError('InstanceCache requires ModelRegistry for production-safe model name lookup', { code: 'pool_registry_missing' });
|
|
112
|
-
}
|
|
113
|
-
this.registry = modelRegistry;
|
|
114
|
-
// Type indexes are initialized on first use, so models can be registered
|
|
115
|
-
// after the InstanceCache is created; the first getByType call builds them.
|
|
116
|
-
// No computed cache layer: entries and typeIndex are both observable, and
|
|
117
|
-
// getByType() reads them directly, so MobX always tracks the dependency.
|
|
118
|
-
makeObservable(this, {
|
|
119
|
-
add: action,
|
|
120
|
-
addBatch: action,
|
|
121
|
-
upsertBatch: action,
|
|
122
|
-
removeBatch: action,
|
|
123
|
-
addToArchive: action,
|
|
124
|
-
remove: action,
|
|
125
|
-
removeFromArchive: action,
|
|
126
|
-
clear: action,
|
|
127
|
-
updateScope: action,
|
|
128
|
-
size: computed,
|
|
129
|
-
});
|
|
130
|
-
this.startGC();
|
|
131
|
-
}
|
|
132
|
-
// No computed getters — getByType() reads typeIndex and entries directly.
|
|
133
|
-
// Both are observable, so MobX always tracks the dependency; there is no
|
|
134
|
-
// conditional cache path that could silently drop a dependency.
|
|
135
|
-
// There is no cache layer to manage: typeIndex and entries are observable
|
|
136
|
-
// and read directly by getByType().
|
|
137
|
-
resolveModel(entry, id) {
|
|
138
|
-
if (entry.model)
|
|
139
|
-
return entry.model;
|
|
140
|
-
if (entry.weakRef) {
|
|
141
|
-
const model = entry.weakRef.deref();
|
|
142
|
-
if (model) {
|
|
143
|
-
entry.model = model;
|
|
144
|
-
if (id)
|
|
145
|
-
this.accessTimes.set(id, Date.now());
|
|
146
|
-
return model;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
// The type parameter appears only in the return position on purpose: this is
|
|
152
|
-
// an ergonomic typed accessor (like Map<K, V>.get) that centralizes what would
|
|
153
|
-
// otherwise be an `as T` cast at every call site.
|
|
154
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
155
|
-
get(id) {
|
|
156
|
-
const entry = this.entries.get(id);
|
|
157
|
-
if (!entry) {
|
|
158
|
-
runInAction(() => {
|
|
159
|
-
this.metrics.misses++;
|
|
160
|
-
});
|
|
161
|
-
return undefined;
|
|
162
|
-
}
|
|
163
|
-
let model = entry.model;
|
|
164
|
-
if (!model && entry.weakRef) {
|
|
165
|
-
const restoredModel = entry.weakRef.deref();
|
|
166
|
-
if (!restoredModel) {
|
|
167
|
-
runInAction(() => {
|
|
168
|
-
this.entries.delete(id);
|
|
169
|
-
this.removeFromTypeIndex(id, entry.model?.getModelName());
|
|
170
|
-
this.metrics.misses++;
|
|
171
|
-
});
|
|
172
|
-
return undefined;
|
|
173
|
-
}
|
|
174
|
-
model = restoredModel;
|
|
175
|
-
runInAction(() => {
|
|
176
|
-
entry.model = restoredModel;
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
// Never return disposed models — they are logically removed and may have
|
|
180
|
-
// torn-down internal state. Callers (e.g. flushPendingDeltas) must not
|
|
181
|
-
// receive a disposed reference that will throw on updateFromData().
|
|
182
|
-
if (model?.disposed) {
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
// Update access time in non-observable map — prevents MobX reactions during render
|
|
186
|
-
this.accessTimes.set(id, Date.now());
|
|
187
|
-
this.metrics.hits++;
|
|
188
|
-
return model ?? undefined;
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Look a row up **within one model**.
|
|
192
|
-
*
|
|
193
|
-
* The pool is a single id space: `get(id)` returns whatever row carries that
|
|
194
|
-
* id, whatever model it belongs to. That is the correct storage shape — ids
|
|
195
|
-
* are globally unique, the same premise as Relay's Global Object
|
|
196
|
-
* Identification — but it means an *untyped* lookup cannot stand in for a
|
|
197
|
-
* typed one. Apollo and EmberData avoid the question by keying their identity
|
|
198
|
-
* maps on `Type:id`; with unique ids the equivalent guarantee comes from
|
|
199
|
-
* stating the expected model at the lookup instead.
|
|
200
|
-
*
|
|
201
|
-
* Returns `undefined` for a row belonging to another model: from the asking
|
|
202
|
-
* model's perspective that id is simply absent. Callers that must tell "not
|
|
203
|
-
* here" apart from "here, but another model's" should compare against
|
|
204
|
-
* {@link get}.
|
|
205
|
-
*
|
|
206
|
-
* Prefer this over `get()` anywhere the caller knows which model it wants —
|
|
207
|
-
* `get()` returning another model's row has caused three product bugs, most
|
|
208
|
-
* recently a resize gesture that reverted after every commit.
|
|
209
|
-
*/
|
|
210
|
-
// `T` appears only in the return position, which is normally a caller-chosen
|
|
211
|
-
// cast in disguise. It is sound here precisely because `modelName` is checked
|
|
212
|
-
// at runtime below before the row is handed back, so the caller's expected
|
|
213
|
-
// type and the row's registered identity cannot disagree.
|
|
214
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
215
|
-
getOfType(id, modelName) {
|
|
216
|
-
const model = this.get(id);
|
|
217
|
-
if (!model)
|
|
218
|
-
return undefined;
|
|
219
|
-
// Checked, so the assertion below is sound: `typeIndex` and
|
|
220
|
-
// `getModelName()` are the same registered-name identity.
|
|
221
|
-
return model.getModelName() === modelName ? model : undefined;
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Add model with deduplication support
|
|
225
|
-
*/
|
|
226
|
-
add(model, scope = ModelScope.live, deltaInfo) {
|
|
227
|
-
const id = model.id;
|
|
228
|
-
const modelType = model.getModelName();
|
|
229
|
-
const addKey = `${modelType}:${id}`;
|
|
230
|
-
// Ensure type index exists for this model type
|
|
231
|
-
if (!this.typeIndex.has(modelType)) {
|
|
232
|
-
this.typeIndex.set(modelType, observable.set());
|
|
233
|
-
}
|
|
234
|
-
// Check if model already exists to prevent duplicates
|
|
235
|
-
const existingEntry = this.entries.get(id);
|
|
236
|
-
if (existingEntry?.model && !existingEntry.model.disposed) {
|
|
237
|
-
// Model already exists and is valid, update its scope if needed
|
|
238
|
-
if (existingEntry.scope !== scope) {
|
|
239
|
-
runInAction(() => {
|
|
240
|
-
this.entries.set(id, { ...existingEntry, scope });
|
|
241
|
-
});
|
|
242
|
-
this.accessTimes.set(id, Date.now());
|
|
243
|
-
}
|
|
244
|
-
this.metrics.duplicatesSkipped++;
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
// Check rapid additions (within 50ms) for better deduplication
|
|
248
|
-
const lastAdded = this.recentAdditions.get(addKey);
|
|
249
|
-
if (lastAdded && Date.now() - lastAdded < 50) {
|
|
250
|
-
this.metrics.duplicatesSkipped++;
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
// Check delta history for duplicate processing
|
|
254
|
-
if (deltaInfo?.syncId) {
|
|
255
|
-
const history = this.deltaHistory.get(addKey);
|
|
256
|
-
if (history) {
|
|
257
|
-
// Skip if we've already processed a newer or equal sync ID
|
|
258
|
-
if (history.lastSyncId >= deltaInfo.syncId) {
|
|
259
|
-
this.metrics.duplicatesSkipped++;
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
// Warn about suspicious patterns
|
|
263
|
-
if (deltaInfo.action === 'I' &&
|
|
264
|
-
(history.lastAction === 'U' || history.lastAction === 'D')) {
|
|
265
|
-
// Internal delta-ordering anomaly that reconciles on the next
|
|
266
|
-
// catch-up — forensic, not consumer-actionable → debug.
|
|
267
|
-
getContext().logger.debug(`InstanceCache.add() SUSPICIOUS: INSERT after ${history.lastAction}`, { modelType, id, syncId: deltaInfo.syncId });
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
// Update delta history
|
|
271
|
-
this.deltaHistory.set(addKey, {
|
|
272
|
-
lastAction: deltaInfo.action ?? 'U',
|
|
273
|
-
lastSyncId: deltaInfo.syncId,
|
|
274
|
-
timestamp: Date.now(),
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
// Track this addition
|
|
278
|
-
this.recentAdditions.set(addKey, Date.now());
|
|
279
|
-
// Clean old tracking entries periodically
|
|
280
|
-
if (this.recentAdditions.size > 100) {
|
|
281
|
-
this.cleanupTracking();
|
|
282
|
-
}
|
|
283
|
-
// Note: existingEntry check is now done earlier for better deduplication
|
|
284
|
-
if (this.entries.size >= this.config.maxSize) {
|
|
285
|
-
this.evictOldest();
|
|
286
|
-
}
|
|
287
|
-
const entry = {
|
|
288
|
-
model,
|
|
289
|
-
scope,
|
|
290
|
-
};
|
|
291
|
-
if (this.config.useWeakRefs && this.isLargeModel(model)) {
|
|
292
|
-
entry.weakRef = new WeakRef(model);
|
|
293
|
-
}
|
|
294
|
-
this.accessTimes.set(id, Date.now());
|
|
295
|
-
runInAction(() => {
|
|
296
|
-
this.entries.set(id, entry);
|
|
297
|
-
this.addToTypeIndex(id, model.getModelName());
|
|
298
|
-
this.metrics.additions++;
|
|
299
|
-
});
|
|
300
|
-
// No cache to invalidate — typeIndex + entries are directly observable
|
|
301
|
-
// Notify views of the addition
|
|
302
|
-
this.notifySubscribers(model);
|
|
303
|
-
this.viewRegistry.notifyAdded(modelType, model);
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* Upsert a model - INSERT if new, UPDATE if exists
|
|
307
|
-
*/
|
|
308
|
-
upsert(model, scope = ModelScope.live) {
|
|
309
|
-
const id = model.id;
|
|
310
|
-
const existingEntry = this.entries.get(id);
|
|
311
|
-
if (existingEntry?.model && !existingEntry.model.disposed) {
|
|
312
|
-
// Model exists - update it in-place
|
|
313
|
-
const existingModel = existingEntry.model;
|
|
314
|
-
// Skip updateFromData if same instance - preserves _local changes for client mutations
|
|
315
|
-
if (model !== existingModel) {
|
|
316
|
-
existingModel.updateFromData(model.toJSON());
|
|
317
|
-
}
|
|
318
|
-
// Update scope if different
|
|
319
|
-
if (existingEntry.scope !== scope) {
|
|
320
|
-
runInAction(() => {
|
|
321
|
-
this.entries.set(id, { ...existingEntry, scope });
|
|
322
|
-
});
|
|
323
|
-
this.accessTimes.set(id, Date.now());
|
|
324
|
-
}
|
|
325
|
-
this.notifySubscribers(existingModel);
|
|
326
|
-
// Notify views of the update
|
|
327
|
-
this.viewRegistry.notifyUpdated(existingModel.getModelName(), existingModel);
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
// Model doesn't exist - add it (add() already notifies views)
|
|
331
|
-
this.add(model, scope);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Batch add models - optimized for hydration
|
|
336
|
-
* All models are added in a single MobX action to minimize reactivity overhead
|
|
337
|
-
*/
|
|
338
|
-
addBatch(models, scope = ModelScope.live) {
|
|
339
|
-
if (models.length === 0)
|
|
340
|
-
return 0;
|
|
341
|
-
let addedCount = 0;
|
|
342
|
-
const now = Date.now();
|
|
343
|
-
// Process all models in a single action to avoid per-item reaction cycles
|
|
344
|
-
for (const model of models) {
|
|
345
|
-
const id = model.id;
|
|
346
|
-
const modelType = model.getModelName();
|
|
347
|
-
// Ensure type index exists
|
|
348
|
-
if (!this.typeIndex.has(modelType)) {
|
|
349
|
-
this.typeIndex.set(modelType, observable.set());
|
|
350
|
-
}
|
|
351
|
-
// Skip if model already exists and is valid
|
|
352
|
-
const existingEntry = this.entries.get(id);
|
|
353
|
-
if (existingEntry?.model && !existingEntry.model.disposed) {
|
|
354
|
-
if (existingEntry.scope !== scope) {
|
|
355
|
-
this.entries.set(id, { ...existingEntry, scope });
|
|
356
|
-
this.accessTimes.set(id, now);
|
|
357
|
-
}
|
|
358
|
-
this.metrics.duplicatesSkipped++;
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
// Evict if at capacity
|
|
362
|
-
if (this.entries.size >= this.config.maxSize) {
|
|
363
|
-
this.evictOldest();
|
|
364
|
-
}
|
|
365
|
-
const entry = {
|
|
366
|
-
model,
|
|
367
|
-
scope,
|
|
368
|
-
};
|
|
369
|
-
this.accessTimes.set(id, now);
|
|
370
|
-
if (this.config.useWeakRefs && this.isLargeModel(model)) {
|
|
371
|
-
entry.weakRef = new WeakRef(model);
|
|
372
|
-
}
|
|
373
|
-
this.entries.set(id, entry);
|
|
374
|
-
this.addToTypeIndex(id, modelType);
|
|
375
|
-
// Populate the foreign-key indexes. The single-item `add()` path
|
|
376
|
-
// does this; `addBatch()` used to skip it, which meant every
|
|
377
|
-
// block / ledger cell / message that came in through a bulk
|
|
378
|
-
// loader (`ensureReportBlocks`, `prefetchSectionBlocks`, bootstrap
|
|
379
|
-
// hydration) was in the pool but invisible to `hasMany` lookups
|
|
380
|
-
// — `section.blocks` returned `[]` until the user clicked a block
|
|
381
|
-
// and something else ran a non-batch `add` that happened to
|
|
382
|
-
// populate the FK index as a side effect. The UX symptom was
|
|
383
|
-
// "sections show empty until you click on one." Adding this one
|
|
384
|
-
// line closes the gap.
|
|
385
|
-
this.addToForeignKeyIndex(id, model, modelType);
|
|
386
|
-
this.metrics.additions++;
|
|
387
|
-
addedCount++;
|
|
388
|
-
this.notifySubscribers(model);
|
|
389
|
-
// Notify views of the addition
|
|
390
|
-
this.viewRegistry.notifyAdded(modelType, model);
|
|
391
|
-
}
|
|
392
|
-
// No cache to invalidate — typeIndex + entries are directly observable
|
|
393
|
-
return addedCount;
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Batch upsert models - optimized for delta processing.
|
|
397
|
-
* All upserts happen in a single MobX action to minimize reactivity overhead.
|
|
398
|
-
*/
|
|
399
|
-
upsertBatch(models, scope = ModelScope.live) {
|
|
400
|
-
if (models.length === 0)
|
|
401
|
-
return;
|
|
402
|
-
for (const model of models) {
|
|
403
|
-
const id = model.id;
|
|
404
|
-
const existingEntry = this.entries.get(id);
|
|
405
|
-
if (existingEntry?.model && !existingEntry.model.disposed) {
|
|
406
|
-
if (model !== existingEntry.model) {
|
|
407
|
-
existingEntry.model.updateFromData(model.toJSON());
|
|
408
|
-
}
|
|
409
|
-
if (existingEntry.scope !== scope) {
|
|
410
|
-
this.entries.set(id, { ...existingEntry, scope });
|
|
411
|
-
this.accessTimes.set(id, Date.now());
|
|
412
|
-
}
|
|
413
|
-
this.notifySubscribers(existingEntry.model);
|
|
414
|
-
// Notify views of the update
|
|
415
|
-
this.viewRegistry.notifyUpdated(existingEntry.model.getModelName(), existingEntry.model);
|
|
416
|
-
}
|
|
417
|
-
else {
|
|
418
|
-
// Delegate to inline add logic (same as addBatch internals)
|
|
419
|
-
const modelType = model.getModelName();
|
|
420
|
-
if (!this.typeIndex.has(modelType)) {
|
|
421
|
-
this.typeIndex.set(modelType, observable.set());
|
|
422
|
-
}
|
|
423
|
-
if (this.entries.size >= this.config.maxSize) {
|
|
424
|
-
this.evictOldest();
|
|
425
|
-
}
|
|
426
|
-
const entry = { model, scope };
|
|
427
|
-
this.accessTimes.set(id, Date.now());
|
|
428
|
-
if (this.config.useWeakRefs && this.isLargeModel(model)) {
|
|
429
|
-
entry.weakRef = new WeakRef(model);
|
|
430
|
-
}
|
|
431
|
-
this.entries.set(id, entry);
|
|
432
|
-
this.addToTypeIndex(id, modelType);
|
|
433
|
-
this.metrics.additions++;
|
|
434
|
-
this.notifySubscribers(model);
|
|
435
|
-
// Notify views of the addition
|
|
436
|
-
this.viewRegistry.notifyAdded(modelType, model);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
// No cache to invalidate — typeIndex + entries are directly observable
|
|
440
|
-
}
|
|
441
|
-
/**
|
|
442
|
-
* Batch remove models by ID - optimized for delta processing.
|
|
443
|
-
* All removals happen in a single MobX action to minimize reactivity overhead.
|
|
444
|
-
* Returns the number of models actually removed.
|
|
445
|
-
*/
|
|
446
|
-
removeBatch(ids) {
|
|
447
|
-
if (ids.length === 0)
|
|
448
|
-
return 0;
|
|
449
|
-
let removedCount = 0;
|
|
450
|
-
for (const id of ids) {
|
|
451
|
-
const entry = this.entries.get(id);
|
|
452
|
-
if (!entry)
|
|
453
|
-
continue;
|
|
454
|
-
const modelName = entry.model?.getModelName() ?? entry.weakRef?.deref()?.getModelName();
|
|
455
|
-
// FK/type cleanup must run before entries.delete — see `remove()`
|
|
456
|
-
// for the full explanation. Same bug, same fix.
|
|
457
|
-
this.removeFromTypeIndex(id, modelName);
|
|
458
|
-
this.entries.delete(id);
|
|
459
|
-
// Notify views of the removal before disposing
|
|
460
|
-
if (modelName) {
|
|
461
|
-
this.viewRegistry.notifyRemoved(modelName, id);
|
|
462
|
-
}
|
|
463
|
-
const model = entry.model ?? entry.weakRef?.deref();
|
|
464
|
-
// A non-Model object can reach the pool (see `clear`); only dispose a real one.
|
|
465
|
-
if (typeof model?.dispose === 'function')
|
|
466
|
-
model.dispose();
|
|
467
|
-
const addKey = modelName ? `${modelName}:${id}` : id;
|
|
468
|
-
this.recentAdditions.delete(addKey);
|
|
469
|
-
this.deltaHistory.delete(addKey);
|
|
470
|
-
this.accessTimes.delete(id);
|
|
471
|
-
removedCount++;
|
|
472
|
-
}
|
|
473
|
-
// No cache to invalidate — typeIndex + entries are directly observable
|
|
474
|
-
return removedCount;
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Read-only accessor for entity IDs by model type.
|
|
478
|
-
* Used by applyBootstrapToPool() and rehydrateFromDatabase() for ghost detection.
|
|
479
|
-
*/
|
|
480
|
-
getIdsByModelType(modelType) {
|
|
481
|
-
return this.typeIndex.get(modelType);
|
|
482
|
-
}
|
|
483
|
-
addToArchive(model) {
|
|
484
|
-
this.add(model, ModelScope.archived);
|
|
485
|
-
}
|
|
486
|
-
remove(id) {
|
|
487
|
-
const entry = this.entries.get(id);
|
|
488
|
-
if (!entry)
|
|
489
|
-
return false;
|
|
490
|
-
const modelName = entry.model?.getModelName() ?? entry.weakRef?.deref()?.getModelName();
|
|
491
|
-
// Order matters here: `removeFromTypeIndex` → `removeFromForeignKeyIndex`
|
|
492
|
-
// reads the FK field values off the model via `this.entries.get(id)`.
|
|
493
|
-
// If we `this.entries.delete(id)` first, the model is gone and the
|
|
494
|
-
// FK cleanup silently no-ops — leaving ghost ids in the FK index.
|
|
495
|
-
// That causes `getByForeignKey(..., parentId)` to report
|
|
496
|
-
// `matched > returned` (dropped-no-entry) and, on the UI, keeps the
|
|
497
|
-
// stale block visible until the next reload rebuilds the index
|
|
498
|
-
// from fresh data. Do the FK/type cleanup first, then delete the
|
|
499
|
-
// entry.
|
|
500
|
-
runInAction(() => {
|
|
501
|
-
this.removeFromTypeIndex(id, modelName);
|
|
502
|
-
this.entries.delete(id);
|
|
503
|
-
});
|
|
504
|
-
// No cache to invalidate — typeIndex + entries are directly observable
|
|
505
|
-
// Notify views of the removal before disposing
|
|
506
|
-
if (modelName) {
|
|
507
|
-
this.viewRegistry.notifyRemoved(modelName, id);
|
|
508
|
-
}
|
|
509
|
-
const model = entry.model ?? entry.weakRef?.deref();
|
|
510
|
-
// A non-Model object can reach the pool (see `clear`); only dispose a real one.
|
|
511
|
-
if (typeof model?.dispose === 'function')
|
|
512
|
-
model.dispose();
|
|
513
|
-
// Clean tracking
|
|
514
|
-
const addKey = modelName ? `${modelName}:${id}` : id;
|
|
515
|
-
this.recentAdditions.delete(addKey);
|
|
516
|
-
this.deltaHistory.delete(addKey);
|
|
517
|
-
this.accessTimes.delete(id);
|
|
518
|
-
return true;
|
|
519
|
-
}
|
|
520
|
-
removeFromArchive(id) {
|
|
521
|
-
const entry = this.entries.get(id);
|
|
522
|
-
if (entry?.scope !== ModelScope.archived) {
|
|
523
|
-
return false;
|
|
524
|
-
}
|
|
525
|
-
return this.remove(id);
|
|
526
|
-
}
|
|
527
|
-
getByType(modelClass, scope = ModelScope.all) {
|
|
528
|
-
// Read typeIndex and entries directly. Both are observable maps, so MobX
|
|
529
|
-
// always tracks the dependency — there is no conditional cache path.
|
|
530
|
-
let actualModelName = this.registry.getModelNameFromConstructor(modelClass);
|
|
531
|
-
if (!actualModelName) {
|
|
532
|
-
actualModelName = this.registry.getModelNameFromConstructor(modelClass);
|
|
533
|
-
if (!actualModelName) {
|
|
534
|
-
try {
|
|
535
|
-
const ConcreteClass = modelClass;
|
|
536
|
-
const tempInstance = new ConcreteClass({});
|
|
537
|
-
actualModelName = tempInstance.getModelName();
|
|
538
|
-
// Fallback resolved — hand-coded class not in registry but name matches.
|
|
539
|
-
// This is expected during migration from hand-coded → dynamic models.
|
|
540
|
-
}
|
|
541
|
-
catch (e) {
|
|
542
|
-
getContext().observability.breadcrumb(`Failed to create fallback instance for ${modelClass.name}`, 'sync.database', 'error', {
|
|
543
|
-
error: e instanceof Error ? e.message : String(e),
|
|
544
|
-
});
|
|
545
|
-
return [];
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
// Read from typeIndex (observable) to get IDs for this model type
|
|
550
|
-
const ids = this.typeIndex.get(actualModelName || '');
|
|
551
|
-
if (!ids || ids.size === 0) {
|
|
552
|
-
return [];
|
|
553
|
-
}
|
|
554
|
-
// Resolve each ID from entries (observable) with scope filtering.
|
|
555
|
-
// Note: we do not check `instanceof modelClass`, because schema-generated
|
|
556
|
-
// dynamic classes and hand-coded classes are different constructors that
|
|
557
|
-
// both represent the same model type. The typeIndex lookup by name is
|
|
558
|
-
// authoritative — if the name matched, the model belongs to this type.
|
|
559
|
-
const result = [];
|
|
560
|
-
for (const id of ids) {
|
|
561
|
-
const entry = this.entries.get(id);
|
|
562
|
-
if (!entry)
|
|
563
|
-
continue;
|
|
564
|
-
if (!this.matchesScope(entry.scope, scope))
|
|
565
|
-
continue;
|
|
566
|
-
const model = this.resolveModel(entry, id);
|
|
567
|
-
if (model && !model.disposed) {
|
|
568
|
-
result.push(model);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
return result;
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Get all models of a given type by string name.
|
|
575
|
-
* Used for custom entity types where multiple entity type names share
|
|
576
|
-
* the same CustomEntityModel constructor (getByType can't disambiguate).
|
|
577
|
-
* Reads from the same typeIndex as getByType — MobX tracks the dependency.
|
|
578
|
-
*/
|
|
579
|
-
getByTypeName(modelName, scope = ModelScope.all) {
|
|
580
|
-
const ids = this.typeIndex.get(modelName);
|
|
581
|
-
if (!ids || ids.size === 0) {
|
|
582
|
-
return [];
|
|
583
|
-
}
|
|
584
|
-
const result = [];
|
|
585
|
-
for (const id of ids) {
|
|
586
|
-
const entry = this.entries.get(id);
|
|
587
|
-
if (!entry)
|
|
588
|
-
continue;
|
|
589
|
-
if (!this.matchesScope(entry.scope, scope))
|
|
590
|
-
continue;
|
|
591
|
-
const model = this.resolveModel(entry, id);
|
|
592
|
-
if (model && !model.disposed) {
|
|
593
|
-
result.push(model);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
return result;
|
|
597
|
-
}
|
|
598
|
-
*iterateByType(modelClass, scope = ModelScope.all) {
|
|
599
|
-
const actualModelName = this.registry.getModelNameFromConstructor(modelClass);
|
|
600
|
-
if (!actualModelName) {
|
|
601
|
-
throw new AbloValidationError(`Model class ${modelClass.name} not registered in ModelRegistry`, { code: 'pool_model_class_not_registered' });
|
|
602
|
-
}
|
|
603
|
-
const ids = this.typeIndex.get(actualModelName);
|
|
604
|
-
if (!ids)
|
|
605
|
-
return;
|
|
606
|
-
for (const id of ids) {
|
|
607
|
-
const entry = this.entries.get(id);
|
|
608
|
-
if (!entry)
|
|
609
|
-
continue;
|
|
610
|
-
if (!this.matchesScope(entry.scope, scope))
|
|
611
|
-
continue;
|
|
612
|
-
const model = this.get(id);
|
|
613
|
-
if (model && model instanceof modelClass) {
|
|
614
|
-
yield model;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
updateScope(id, scope) {
|
|
619
|
-
const entry = this.entries.get(id);
|
|
620
|
-
if (entry && entry.scope !== scope) {
|
|
621
|
-
// Re-set the entry so ObservableMap notifies observers of the change.
|
|
622
|
-
// Mutating entry.scope in-place wouldn't trigger MobX (plain object property).
|
|
623
|
-
runInAction(() => {
|
|
624
|
-
this.entries.set(id, { ...entry, scope });
|
|
625
|
-
});
|
|
626
|
-
this.accessTimes.set(id, Date.now());
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Create (or update) a model instance locally, given a typename and raw
|
|
631
|
-
* data. Cleaner than `createFromData({ __typename, ...data })` — the
|
|
632
|
-
* typename lives in the arg list, not hidden inside the data object.
|
|
633
|
-
*
|
|
634
|
-
* Used for optimistic local writes: `pool.create('Section', { id, reportId, ... })`.
|
|
635
|
-
* For hydration from server deltas (where `__typename` already rides on
|
|
636
|
-
* the payload), use `createFromData(data)` directly — that path is kept
|
|
637
|
-
* because the wire format attaches the discriminator to the data itself.
|
|
638
|
-
*/
|
|
639
|
-
create(typename, data) {
|
|
640
|
-
return this.createFromData({ ...data, __typename: typename });
|
|
641
|
-
}
|
|
642
|
-
createFromData(data, ModelClass, opts) {
|
|
643
|
-
// Support multiple model identifier fields for backwards compatibility
|
|
644
|
-
const modelName = data.__typename ?? data.__class ?? data.modelName ?? 'Unknown';
|
|
645
|
-
const Constructor = ModelClass ?? this.registry.getModelByName(modelName);
|
|
646
|
-
if (!Constructor) {
|
|
647
|
-
if (modelName === 'Unknown') {
|
|
648
|
-
// Malformed row with no type marker — dropped, but nothing the consumer
|
|
649
|
-
// can act on (the actionable schema-drift case is handled below) → debug.
|
|
650
|
-
getContext().logger.debug('InstanceCache.createFromData: No model identifier found', { data });
|
|
651
|
-
getContext().modelDebugLogger?.logError('Unknown', 'CREATE', 'No model identifier found', data);
|
|
652
|
-
return null;
|
|
653
|
-
}
|
|
654
|
-
if (opts?.strict) {
|
|
655
|
-
const known = this.registry.getRegisteredModelNames();
|
|
656
|
-
throw new AbloValidationError(`Model "${modelName}" is not registered on this client` +
|
|
657
|
-
(known.length ? ` (known: ${known.join(', ')})` : '') +
|
|
658
|
-
`. The schema pushed to this org may differ from your local ` +
|
|
659
|
-
`schema — run \`ablo status\` to compare.`, { code: 'model_not_registered' });
|
|
660
|
-
}
|
|
661
|
-
// Genuinely actionable and NOT self-healing: a model the server is sending
|
|
662
|
-
// isn't in your schema, so these rows are silently skipped. Keep at warn,
|
|
663
|
-
// consumer register (their model name + the `ablo status` fix); forensics ride debug.
|
|
664
|
-
getContext().logger.warn(`Received data for "${modelName}", which isn't in your schema — these rows will be skipped. Run \`ablo status\` to compare your local schema with the server.`);
|
|
665
|
-
getContext().logger.debug(`InstanceCache.createFromData: No constructor found for model "${modelName}"`, { data });
|
|
666
|
-
getContext().modelDebugLogger?.logError(modelName, 'CREATE', `No constructor found for model "${modelName}"`, data);
|
|
667
|
-
return null;
|
|
668
|
-
}
|
|
669
|
-
// If the model already exists, update it in place instead of creating a
|
|
670
|
-
// duplicate. Keeping the existing instance alive preserves React's
|
|
671
|
-
// references and MobX's observation tracking.
|
|
672
|
-
if (data.id && this.entries.has(data.id)) {
|
|
673
|
-
const existing = this.get(data.id);
|
|
674
|
-
if (existing?.getModelName() === modelName) {
|
|
675
|
-
// Same ID and same type - update existing model with new data and return it
|
|
676
|
-
existing.updateFromData(data);
|
|
677
|
-
return existing;
|
|
678
|
-
}
|
|
679
|
-
// Different type with same ID - this is a shared PK scenario (e.g., two models sharing one row id)
|
|
680
|
-
// Don't return existing, create new model (will use composite key for storage)
|
|
681
|
-
}
|
|
682
|
-
// Log model creation attempt
|
|
683
|
-
getContext().modelDebugLogger?.logCreation(modelName, data, Constructor);
|
|
684
|
-
try {
|
|
685
|
-
// Pass data directly to constructor for Prisma-first models
|
|
686
|
-
const model = new Constructor(data);
|
|
687
|
-
return model;
|
|
688
|
-
}
|
|
689
|
-
catch (error) {
|
|
690
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
691
|
-
// Internal construction failure — captured via observability below and
|
|
692
|
-
// re-fetched on resync; the stack is forensic → debug.
|
|
693
|
-
getContext().logger.debug(`[InstanceCache.createFromData] FAILED ${modelName}`, { errorMessage, stack: error instanceof Error ? error.stack : undefined });
|
|
694
|
-
getContext().observability.captureMutationFailure({
|
|
695
|
-
context: 'createFromData',
|
|
696
|
-
modelName,
|
|
697
|
-
modelId: data.id,
|
|
698
|
-
error: errorMessage,
|
|
699
|
-
});
|
|
700
|
-
getContext().modelDebugLogger?.logError(modelName, 'CREATE', errorMessage, {
|
|
701
|
-
data,
|
|
702
|
-
constructor: Constructor.name,
|
|
703
|
-
});
|
|
704
|
-
return null;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
/**
|
|
708
|
-
* Clear the object pool
|
|
709
|
-
* @param options.preserveObserved - If true, keep models that are being observed by React
|
|
710
|
-
* This prevents React components from holding stale references
|
|
711
|
-
* after bootstrap/rehydration
|
|
712
|
-
*/
|
|
713
|
-
clear(options = {}) {
|
|
714
|
-
const preserveObserved = options.preserveObserved ?? false;
|
|
715
|
-
const preservedIds = [];
|
|
716
|
-
const preservedEntries = [];
|
|
717
|
-
for (const [id, entry] of this.entries) {
|
|
718
|
-
const model = entry.model ?? entry.weakRef?.deref();
|
|
719
|
-
// Check if this model should be preserved (has active React observers)
|
|
720
|
-
if (preserveObserved &&
|
|
721
|
-
model &&
|
|
722
|
-
typeof model.hasObservedCollections === 'function' &&
|
|
723
|
-
model.hasObservedCollections()) {
|
|
724
|
-
// Keep this model alive - React is still using it
|
|
725
|
-
preservedIds.push(id);
|
|
726
|
-
preservedEntries.push([id, entry]);
|
|
727
|
-
continue;
|
|
728
|
-
}
|
|
729
|
-
// `rowAsModel` only casts, so a non-Model object can reach the pool (see
|
|
730
|
-
// the SyncClient no-op UPDATE guard). Guard that `dispose` is actually
|
|
731
|
-
// callable — mirroring the `hasObservedCollections` typeof-check above —
|
|
732
|
-
// rather than assume every pooled entry is a real Model.
|
|
733
|
-
if (typeof model?.dispose === 'function')
|
|
734
|
-
model.dispose();
|
|
735
|
-
}
|
|
736
|
-
// Save access times for preserved entries before clearing
|
|
737
|
-
const preservedAccessTimes = new Map();
|
|
738
|
-
for (const [id] of preservedEntries) {
|
|
739
|
-
const time = this.accessTimes.get(id);
|
|
740
|
-
if (time)
|
|
741
|
-
preservedAccessTimes.set(id, time);
|
|
742
|
-
}
|
|
743
|
-
runInAction(() => {
|
|
744
|
-
this.entries.clear();
|
|
745
|
-
this.typeIndex.clear();
|
|
746
|
-
// Clear foreign key index data (preserves config/structure, just empties the value maps)
|
|
747
|
-
for (const index of this.foreignKeyIndexes.values()) {
|
|
748
|
-
index.clear();
|
|
749
|
-
}
|
|
750
|
-
this.recentAdditions.clear();
|
|
751
|
-
this.deltaHistory.clear();
|
|
752
|
-
this.metrics = {
|
|
753
|
-
hits: 0,
|
|
754
|
-
misses: 0,
|
|
755
|
-
evictions: 0,
|
|
756
|
-
additions: 0,
|
|
757
|
-
duplicatesSkipped: 0,
|
|
758
|
-
};
|
|
759
|
-
// Re-add preserved entries (also rebuilds foreign key indexes via addToTypeIndex)
|
|
760
|
-
for (const [id, entry] of preservedEntries) {
|
|
761
|
-
this.entries.set(id, entry);
|
|
762
|
-
const model = entry.model ?? entry.weakRef?.deref();
|
|
763
|
-
if (model) {
|
|
764
|
-
this.addToTypeIndex(id, model.getModelName());
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
});
|
|
768
|
-
// Restore access times: clear then re-add preserved
|
|
769
|
-
this.accessTimes.clear();
|
|
770
|
-
for (const [id, time] of preservedAccessTimes) {
|
|
771
|
-
this.accessTimes.set(id, time);
|
|
772
|
-
}
|
|
773
|
-
// No cache to invalidate — typeIndex + entries are directly observable
|
|
774
|
-
}
|
|
775
|
-
has(id) {
|
|
776
|
-
return this.entries.has(id);
|
|
777
|
-
}
|
|
778
|
-
/**
|
|
779
|
-
* Touch a model to update its access time (prevents premature GC)
|
|
780
|
-
* Used by LazyReferenceCollection to keep parent models alive during active usage
|
|
781
|
-
*/
|
|
782
|
-
touch(id) {
|
|
783
|
-
const entry = this.entries.get(id);
|
|
784
|
-
if (!entry) {
|
|
785
|
-
return false;
|
|
786
|
-
}
|
|
787
|
-
this.accessTimes.set(id, Date.now());
|
|
788
|
-
return true;
|
|
789
|
-
}
|
|
790
|
-
getAllIds() {
|
|
791
|
-
return Array.from(this.entries.keys());
|
|
792
|
-
}
|
|
793
|
-
getAllModels() {
|
|
794
|
-
const results = [];
|
|
795
|
-
for (const [id] of this.entries) {
|
|
796
|
-
const model = this.get(id);
|
|
797
|
-
if (model) {
|
|
798
|
-
results.push(model);
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
return results;
|
|
802
|
-
}
|
|
803
|
-
get size() {
|
|
804
|
-
return this.entries.size;
|
|
805
|
-
}
|
|
806
|
-
get hitRate() {
|
|
807
|
-
const total = this.metrics.hits + this.metrics.misses;
|
|
808
|
-
return total > 0 ? (this.metrics.hits / total) * 100 : 0;
|
|
809
|
-
}
|
|
810
|
-
getStats() {
|
|
811
|
-
const scopeCounts = { live: 0, archived: 0 };
|
|
812
|
-
const typeCounts = new Map();
|
|
813
|
-
for (const [, entry] of this.entries) {
|
|
814
|
-
if (entry.scope === ModelScope.live)
|
|
815
|
-
scopeCounts.live++;
|
|
816
|
-
else if (entry.scope === ModelScope.archived)
|
|
817
|
-
scopeCounts.archived++;
|
|
818
|
-
const modelName = entry.model?.getModelName() ?? entry.weakRef?.deref()?.getModelName();
|
|
819
|
-
if (modelName) {
|
|
820
|
-
typeCounts.set(modelName, (typeCounts.get(modelName) ?? 0) + 1);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
return {
|
|
824
|
-
size: this.size,
|
|
825
|
-
hitRate: this.hitRate,
|
|
826
|
-
metrics: { ...this.metrics },
|
|
827
|
-
scopeCounts,
|
|
828
|
-
typeCounts: Object.fromEntries(typeCounts),
|
|
829
|
-
deltaHistorySize: this.deltaHistory.size,
|
|
830
|
-
recentAdditionsSize: this.recentAdditions.size,
|
|
831
|
-
config: { ...this.config },
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
clearDeltaHistory(olderThanMs = 3600000) {
|
|
835
|
-
const now = Date.now();
|
|
836
|
-
const toDelete = [];
|
|
837
|
-
for (const [key, history] of this.deltaHistory) {
|
|
838
|
-
if (now - history.timestamp > olderThanMs) {
|
|
839
|
-
toDelete.push(key);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
toDelete.forEach((key) => this.deltaHistory.delete(key));
|
|
843
|
-
// Delta history entries cleared silently
|
|
844
|
-
}
|
|
845
|
-
cleanupTracking() {
|
|
846
|
-
const now = Date.now();
|
|
847
|
-
for (const [key, time] of this.recentAdditions) {
|
|
848
|
-
if (now - time > 1000) {
|
|
849
|
-
this.recentAdditions.delete(key);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
gc() {
|
|
854
|
-
return runInAction(() => {
|
|
855
|
-
const now = Date.now();
|
|
856
|
-
const toRemove = [];
|
|
857
|
-
let evicted = 0;
|
|
858
|
-
let skippedObserved = 0;
|
|
859
|
-
for (const [id, entry] of this.entries) {
|
|
860
|
-
// Check if model has expired based on last access time
|
|
861
|
-
const lastAccessed = this.accessTimes.get(id) ?? 0;
|
|
862
|
-
if (now - lastAccessed > this.config.maxAge) {
|
|
863
|
-
// Do not GC a model that has observed collections — disposing one
|
|
864
|
-
// React is still observing would break it (per MobX guidance).
|
|
865
|
-
// See: https://mobx.js.org/lazy-observables.html
|
|
866
|
-
const model = entry.model ?? entry.weakRef?.deref();
|
|
867
|
-
if (model &&
|
|
868
|
-
typeof model.hasObservedCollections === 'function' &&
|
|
869
|
-
model.hasObservedCollections()) {
|
|
870
|
-
// Model has active React observers - refresh access time and skip GC
|
|
871
|
-
this.accessTimes.set(id, now);
|
|
872
|
-
skippedObserved++;
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
|
-
toRemove.push(id);
|
|
876
|
-
continue;
|
|
877
|
-
}
|
|
878
|
-
// Strong-to-weak-ref demotion at `maxAge / 2` used to live here,
|
|
879
|
-
// in service of memory-pressure relief: idle entries would lose
|
|
880
|
-
// their strong reference, V8 would collect them, and the next
|
|
881
|
-
// access would re-hydrate from IDB/network. In practice it
|
|
882
|
-
// caused silent data loss — any model actively being rendered
|
|
883
|
-
// through a schema-driven dynamic class (i.e., most of them)
|
|
884
|
-
// would be demoted, collected, and the next render's
|
|
885
|
-
// `weakRef.deref()` returned undefined, so blocks / cells /
|
|
886
|
-
// messages "disappeared" after ~10 min of idle.
|
|
887
|
-
//
|
|
888
|
-
// The `hasObservedCollections()` guard used by the eviction
|
|
889
|
-
// branch above only protects models that explicitly register a
|
|
890
|
-
// LazyReferenceCollection; plain observer() components reading
|
|
891
|
-
// properties don't register, so for typical UI usage the guard
|
|
892
|
-
// didn't apply. Rather than try to make React-observation
|
|
893
|
-
// globally visible to the pool, we drop the demotion phase
|
|
894
|
-
// entirely — hard eviction at `maxAge` (with its own guard) is
|
|
895
|
-
// the only automated removal now. If memory-pressure relief is
|
|
896
|
-
// needed later, gate it on an explicit policy (e.g.,
|
|
897
|
-
// `documenthidden` + `performance.memory.usedJSHeapSize`) rather
|
|
898
|
-
// than a time-based tick.
|
|
899
|
-
}
|
|
900
|
-
for (const id of toRemove) {
|
|
901
|
-
if (this.remove(id)) {
|
|
902
|
-
evicted++;
|
|
903
|
-
this.metrics.evictions++;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
if (skippedObserved > 0) {
|
|
907
|
-
getContext().logger.debug(`[InstanceCache GC] Skipped ${skippedObserved} models with active React observers`);
|
|
908
|
-
}
|
|
909
|
-
// Also clean up old tracking data
|
|
910
|
-
this.clearDeltaHistory();
|
|
911
|
-
this.cleanupTracking();
|
|
912
|
-
return evicted;
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
startGC() {
|
|
916
|
-
if (this.gcTimer)
|
|
917
|
-
return;
|
|
918
|
-
this.gcTimer = setInterval(() => this.gc(), this.config.gcInterval);
|
|
919
|
-
// Don't hold a headless Node process open just for pool GC — without
|
|
920
|
-
// this, an agent that never calls disconnect() can never exit. No-op in
|
|
921
|
-
// browsers (where setInterval returns a number without `unref`), which is
|
|
922
|
-
// why the call is optional even though the Node timer type always has it.
|
|
923
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
924
|
-
this.gcTimer.unref?.();
|
|
925
|
-
}
|
|
926
|
-
stopGC() {
|
|
927
|
-
if (this.gcTimer) {
|
|
928
|
-
clearInterval(this.gcTimer);
|
|
929
|
-
this.gcTimer = undefined;
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
evictOldest() {
|
|
933
|
-
runInAction(() => {
|
|
934
|
-
let oldest;
|
|
935
|
-
let oldestTime = Date.now();
|
|
936
|
-
for (const [id, entry] of this.entries) {
|
|
937
|
-
// Skip models that are being observed by React - they must stay alive
|
|
938
|
-
const model = entry.model ?? entry.weakRef?.deref();
|
|
939
|
-
if (model &&
|
|
940
|
-
typeof model.hasObservedCollections === 'function' &&
|
|
941
|
-
model.hasObservedCollections()) {
|
|
942
|
-
continue;
|
|
943
|
-
}
|
|
944
|
-
const entryAccessTime = this.accessTimes.get(id) ?? 0;
|
|
945
|
-
if (entryAccessTime < oldestTime) {
|
|
946
|
-
oldest = [id, entry];
|
|
947
|
-
oldestTime = entryAccessTime;
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
if (oldest) {
|
|
951
|
-
this.remove(oldest[0]);
|
|
952
|
-
this.metrics.evictions++;
|
|
953
|
-
}
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
isLargeModel(model) {
|
|
957
|
-
try {
|
|
958
|
-
const size = JSON.stringify(model).length;
|
|
959
|
-
return size > 10240;
|
|
960
|
-
}
|
|
961
|
-
catch {
|
|
962
|
-
return false;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
// ========== FOREIGN KEY INDEX ==========
|
|
966
|
-
/**
|
|
967
|
-
* Register a foreign key field for indexing on a model type.
|
|
968
|
-
* Call once during app initialization (e.g., after model registration).
|
|
969
|
-
*
|
|
970
|
-
* Example: registerForeignKey('Block', 'sectionId')
|
|
971
|
-
* This enables getByForeignKey('Block', 'sectionId', someSectionId) → O(1) lookup
|
|
972
|
-
*/
|
|
973
|
-
registerForeignKey(modelName, fieldName) {
|
|
974
|
-
const fields = this.foreignKeyConfig.get(modelName) ?? [];
|
|
975
|
-
if (!fields.includes(fieldName)) {
|
|
976
|
-
fields.push(fieldName);
|
|
977
|
-
this.foreignKeyConfig.set(modelName, fields);
|
|
978
|
-
}
|
|
979
|
-
// Initialize the index map
|
|
980
|
-
const indexKey = `${modelName}:${fieldName}`;
|
|
981
|
-
if (!this.foreignKeyIndexes.has(indexKey)) {
|
|
982
|
-
this.foreignKeyIndexes.set(indexKey, observable.map());
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Check whether a foreign key index exists for a given typename + field.
|
|
987
|
-
* Used by QueryView to decide whether to use FK-index for initial scan.
|
|
988
|
-
*/
|
|
989
|
-
hasForeignKeyIndex(typename, fieldName) {
|
|
990
|
-
const indexKey = `${typename}:${fieldName}`;
|
|
991
|
-
return this.foreignKeyIndexes.has(indexKey);
|
|
992
|
-
}
|
|
993
|
-
/**
|
|
994
|
-
* Create a QueryView — an incrementally maintained materialized view.
|
|
995
|
-
* The view registers itself with the ViewRegistry and receives
|
|
996
|
-
* incremental updates when models of the given typename change.
|
|
997
|
-
*/
|
|
998
|
-
createView(typename, options) {
|
|
999
|
-
return new QueryView(typename, this, this.viewRegistry, options);
|
|
1000
|
-
}
|
|
1001
|
-
/**
|
|
1002
|
-
* O(1) lookup of models by foreign key value.
|
|
1003
|
-
* Returns model instances, filtered to live scope by default.
|
|
1004
|
-
*/
|
|
1005
|
-
getByForeignKey(modelName, fieldName, fieldValue) {
|
|
1006
|
-
const indexKey = `${modelName}:${fieldName}`;
|
|
1007
|
-
const index = this.foreignKeyIndexes.get(indexKey);
|
|
1008
|
-
// Both empty-path early-returns below are normal states, not errors:
|
|
1009
|
-
// a model with no FK index yet (not populated), or an index with no
|
|
1010
|
-
// entry for this specific parent id (entity genuinely has no
|
|
1011
|
-
// children). These used to `console.warn` diagnostic dumps on every
|
|
1012
|
-
// call, which turned into hundreds of log lines per second during
|
|
1013
|
-
// cursor hover / rapid re-renders on a busy page. If a caller
|
|
1014
|
-
// needs visibility into "why is this empty," wire an opt-in
|
|
1015
|
-
// `logger.debug` at the specific call site rather than re-adding
|
|
1016
|
-
// a blanket warn here.
|
|
1017
|
-
if (!index)
|
|
1018
|
-
return [];
|
|
1019
|
-
const ids = index.get(fieldValue);
|
|
1020
|
-
if (!ids || ids.size === 0)
|
|
1021
|
-
return [];
|
|
1022
|
-
const result = [];
|
|
1023
|
-
let droppedNoEntry = 0;
|
|
1024
|
-
let droppedScope = 0;
|
|
1025
|
-
let droppedDisposed = 0;
|
|
1026
|
-
for (const id of ids) {
|
|
1027
|
-
const entry = this.entries.get(id);
|
|
1028
|
-
if (!entry) {
|
|
1029
|
-
droppedNoEntry++;
|
|
1030
|
-
continue;
|
|
1031
|
-
}
|
|
1032
|
-
if (!this.matchesScope(entry.scope, ModelScope.live)) {
|
|
1033
|
-
droppedScope++;
|
|
1034
|
-
continue;
|
|
1035
|
-
}
|
|
1036
|
-
const model = this.resolveModel(entry, id);
|
|
1037
|
-
if (model && !model.disposed) {
|
|
1038
|
-
result.push(model);
|
|
1039
|
-
}
|
|
1040
|
-
else if (model?.disposed) {
|
|
1041
|
-
droppedDisposed++;
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
if (droppedNoEntry || droppedScope || droppedDisposed) {
|
|
1045
|
-
// Debug-level: happens on every render when a foreign-key index
|
|
1046
|
-
// has dangling refs (legacy orphan deltas, pending CREATE
|
|
1047
|
-
// transactions, etc.). Noisy at warn level, useful during
|
|
1048
|
-
// investigation.
|
|
1049
|
-
getContext().logger.debug('[InstanceCache.getByForeignKey] ROWS DROPPED', {
|
|
1050
|
-
modelName,
|
|
1051
|
-
fieldName,
|
|
1052
|
-
fieldValue,
|
|
1053
|
-
matched: ids.size,
|
|
1054
|
-
returned: result.length,
|
|
1055
|
-
droppedNoEntry,
|
|
1056
|
-
droppedScope,
|
|
1057
|
-
droppedDisposed,
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
return result;
|
|
1061
|
-
}
|
|
1062
|
-
/**
|
|
1063
|
-
* Add a model to foreign key indexes (called from addToTypeIndex path)
|
|
1064
|
-
*/
|
|
1065
|
-
addToForeignKeyIndex(id, model, modelName) {
|
|
1066
|
-
// Silent no-ops for "no config / non-string value / missing index"
|
|
1067
|
-
// — all three are legitimate states (non-indexed model, optional
|
|
1068
|
-
// nullable FK, index not yet registered because the batch ran
|
|
1069
|
-
// before schema registration completed). Diagnostic warns that
|
|
1070
|
-
// used to live here spammed the console on every hot-path load.
|
|
1071
|
-
const fields = this.foreignKeyConfig.get(modelName);
|
|
1072
|
-
if (!fields)
|
|
1073
|
-
return;
|
|
1074
|
-
for (const fieldName of fields) {
|
|
1075
|
-
const fieldValue = model.getField(fieldName);
|
|
1076
|
-
if (typeof fieldValue !== 'string')
|
|
1077
|
-
continue;
|
|
1078
|
-
const indexKey = `${modelName}:${fieldName}`;
|
|
1079
|
-
const index = this.foreignKeyIndexes.get(indexKey);
|
|
1080
|
-
if (!index)
|
|
1081
|
-
continue;
|
|
1082
|
-
let ids = index.get(fieldValue);
|
|
1083
|
-
if (!ids) {
|
|
1084
|
-
ids = observable.set();
|
|
1085
|
-
index.set(fieldValue, ids);
|
|
1086
|
-
}
|
|
1087
|
-
ids.add(id);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
/**
|
|
1091
|
-
* Remove a model from foreign key indexes (called from removeFromTypeIndex path)
|
|
1092
|
-
*/
|
|
1093
|
-
removeFromForeignKeyIndex(id, modelName) {
|
|
1094
|
-
if (!modelName)
|
|
1095
|
-
return;
|
|
1096
|
-
const fields = this.foreignKeyConfig.get(modelName);
|
|
1097
|
-
if (!fields)
|
|
1098
|
-
return;
|
|
1099
|
-
// We need the model to read the foreign key value
|
|
1100
|
-
const entry = this.entries.get(id);
|
|
1101
|
-
const model = entry?.model ?? entry?.weakRef?.deref();
|
|
1102
|
-
if (!model)
|
|
1103
|
-
return;
|
|
1104
|
-
for (const fieldName of fields) {
|
|
1105
|
-
const fieldValue = model.getField(fieldName);
|
|
1106
|
-
if (typeof fieldValue !== 'string')
|
|
1107
|
-
continue;
|
|
1108
|
-
const indexKey = `${modelName}:${fieldName}`;
|
|
1109
|
-
const index = this.foreignKeyIndexes.get(indexKey);
|
|
1110
|
-
if (!index)
|
|
1111
|
-
continue;
|
|
1112
|
-
const ids = index.get(fieldValue);
|
|
1113
|
-
if (ids) {
|
|
1114
|
-
ids.delete(id);
|
|
1115
|
-
if (ids.size === 0) {
|
|
1116
|
-
index.delete(fieldValue);
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
addToTypeIndex(id, modelName) {
|
|
1122
|
-
if (!modelName)
|
|
1123
|
-
return;
|
|
1124
|
-
let ids = this.typeIndex.get(modelName);
|
|
1125
|
-
if (!ids) {
|
|
1126
|
-
ids = observable.set();
|
|
1127
|
-
this.typeIndex.set(modelName, ids);
|
|
1128
|
-
}
|
|
1129
|
-
ids.add(id);
|
|
1130
|
-
// Update foreign key indexes. If we can't reach the model object,
|
|
1131
|
-
// the FK index will be (re)populated on the first lookup that
|
|
1132
|
-
// resolves the entry — no need to warn here.
|
|
1133
|
-
const entry = this.entries.get(id);
|
|
1134
|
-
const model = entry?.model ?? entry?.weakRef?.deref();
|
|
1135
|
-
if (model) {
|
|
1136
|
-
this.addToForeignKeyIndex(id, model, modelName);
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
removeFromTypeIndex(id, modelName) {
|
|
1140
|
-
if (!modelName)
|
|
1141
|
-
return;
|
|
1142
|
-
// Remove from foreign key indexes BEFORE removing from entries
|
|
1143
|
-
this.removeFromForeignKeyIndex(id, modelName);
|
|
1144
|
-
const ids = this.typeIndex.get(modelName);
|
|
1145
|
-
if (ids) {
|
|
1146
|
-
ids.delete(id);
|
|
1147
|
-
if (ids.size === 0) {
|
|
1148
|
-
this.typeIndex.delete(modelName);
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
matchesScope(entryScope, queryScope) {
|
|
1153
|
-
switch (queryScope) {
|
|
1154
|
-
case ModelScope.all:
|
|
1155
|
-
return true;
|
|
1156
|
-
case ModelScope.live:
|
|
1157
|
-
return entryScope === ModelScope.live;
|
|
1158
|
-
case ModelScope.archived:
|
|
1159
|
-
return entryScope === ModelScope.archived;
|
|
1160
|
-
default:
|
|
1161
|
-
return entryScope === queryScope;
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
}
|