@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.
Files changed (622) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +71 -1929
  3. package/NOTICE +2 -2
  4. package/README.md +23 -532
  5. package/assets/banner.png +0 -0
  6. package/dist/auth.d.ts +2 -0
  7. package/dist/auth.d.ts.map +1 -0
  8. package/dist/auth.js +2 -0
  9. package/dist/auth.js.map +1 -0
  10. package/dist/client.d.ts +3 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +3 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/coordination.d.ts +2 -0
  15. package/dist/coordination.d.ts.map +1 -0
  16. package/dist/coordination.js +2 -0
  17. package/dist/coordination.js.map +1 -0
  18. package/dist/index.d.ts +3 -112
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +3 -161
  21. package/dist/index.js.map +1 -0
  22. package/dist/react.d.ts +4 -0
  23. package/dist/react.d.ts.map +1 -0
  24. package/dist/react.js +3 -0
  25. package/dist/react.js.map +1 -0
  26. package/dist/schema.d.ts +2 -0
  27. package/dist/schema.d.ts.map +1 -0
  28. package/dist/schema.js +2 -0
  29. package/dist/schema.js.map +1 -0
  30. package/dist/server.d.ts +2 -0
  31. package/dist/server.d.ts.map +1 -0
  32. package/dist/server.js +2 -0
  33. package/dist/server.js.map +1 -0
  34. package/dist/source-conformance.d.ts +2 -0
  35. package/dist/source-conformance.d.ts.map +1 -0
  36. package/dist/source-conformance.js +2 -0
  37. package/dist/source-conformance.js.map +1 -0
  38. package/dist/source-drizzle.d.ts +2 -0
  39. package/dist/source-drizzle.d.ts.map +1 -0
  40. package/dist/source-drizzle.js +2 -0
  41. package/dist/source-drizzle.js.map +1 -0
  42. package/dist/source-kysely.d.ts +2 -0
  43. package/dist/source-kysely.d.ts.map +1 -0
  44. package/dist/source-kysely.js +2 -0
  45. package/dist/source-kysely.js.map +1 -0
  46. package/dist/source-next.d.ts +2 -0
  47. package/dist/source-next.d.ts.map +1 -0
  48. package/dist/source-next.js +2 -0
  49. package/dist/source-next.js.map +1 -0
  50. package/dist/source.d.ts +2 -0
  51. package/dist/source.d.ts.map +1 -0
  52. package/dist/source.js +2 -0
  53. package/dist/source.js.map +1 -0
  54. package/dist/wire.d.ts +2 -0
  55. package/dist/wire.d.ts.map +1 -0
  56. package/dist/wire.js +2 -0
  57. package/dist/wire.js.map +1 -0
  58. package/docs/agents.md +2 -2
  59. package/docs/api-keys.md +13 -12
  60. package/docs/api.md +15 -53
  61. package/docs/audit.md +4 -3
  62. package/docs/cli.md +11 -11
  63. package/docs/client-behavior.md +9 -9
  64. package/docs/concurrency-convention.md +28 -42
  65. package/docs/coordination.md +228 -86
  66. package/docs/data-sources.md +5 -5
  67. package/docs/debugging.md +34 -12
  68. package/docs/deployment.md +8 -8
  69. package/docs/examples/agent-human.md +4 -4
  70. package/docs/examples/ai-sdk-tool.md +1 -1
  71. package/docs/examples/existing-python-backend.md +15 -4
  72. package/docs/examples/nextjs.md +27 -6
  73. package/docs/examples/scoped-agent.md +3 -3
  74. package/docs/examples/server-agent.md +2 -2
  75. package/docs/groups.md +57 -3
  76. package/docs/guarantees.md +37 -10
  77. package/docs/how-it-works.md +32 -8
  78. package/docs/idempotency.md +6 -6
  79. package/docs/identity.md +24 -24
  80. package/docs/index.md +8 -8
  81. package/docs/integration-guide.md +38 -16
  82. package/docs/internal/README.md +18 -0
  83. package/docs/internal/agent-fleet-coordination-design.md +171 -0
  84. package/docs/internal/agent-orchestration.md +58 -0
  85. package/docs/internal/commit-identifiers.md +91 -0
  86. package/docs/internal/concurrency-open-decisions.md +37 -0
  87. package/docs/internal/data-source-reverse-channel.md +150 -0
  88. package/docs/internal/per-field-conflict-detection.md +165 -0
  89. package/docs/internal/postgres-replication.md +64 -0
  90. package/docs/internal/serializable-schema.md +119 -0
  91. package/docs/internal/structure.md +32 -0
  92. package/docs/mcp.md +9 -9
  93. package/docs/migration.md +37 -18
  94. package/docs/projects.md +1 -1
  95. package/docs/quickstart.md +2 -2
  96. package/docs/react.md +24 -13
  97. package/docs/schema-contract.md +3 -3
  98. package/docs/sessions.md +91 -37
  99. package/docs/webhooks.md +9 -9
  100. package/examples/README.md +2 -2
  101. package/examples/data-source/README.md +1 -1
  102. package/examples/data-source/ablo-driver.ts +1 -1
  103. package/examples/data-source/customer-server.ts +1 -1
  104. package/examples/data-source/run.ts +1 -1
  105. package/examples/data-source/schema.ts +1 -1
  106. package/examples/quickstart.ts +2 -2
  107. package/llms.txt +12 -12
  108. package/package.json +64 -174
  109. package/dist/BaseSyncedStore.d.ts +0 -823
  110. package/dist/BaseSyncedStore.js +0 -1955
  111. package/dist/Database.d.ts +0 -335
  112. package/dist/Database.js +0 -1500
  113. package/dist/InstanceCache.d.ts +0 -233
  114. package/dist/InstanceCache.js +0 -1164
  115. package/dist/LazyReferenceCollection.d.ts +0 -177
  116. package/dist/LazyReferenceCollection.js +0 -461
  117. package/dist/Model.d.ts +0 -444
  118. package/dist/Model.js +0 -909
  119. package/dist/ModelRegistry.d.ts +0 -221
  120. package/dist/ModelRegistry.js +0 -537
  121. package/dist/NetworkMonitor.d.ts +0 -26
  122. package/dist/NetworkMonitor.js +0 -77
  123. package/dist/RuntimeContext.d.ts +0 -52
  124. package/dist/RuntimeContext.js +0 -80
  125. package/dist/SyncClient.d.ts +0 -551
  126. package/dist/SyncClient.js +0 -2199
  127. package/dist/adapters/alwaysOnline.d.ts +0 -14
  128. package/dist/adapters/alwaysOnline.js +0 -17
  129. package/dist/adapters/inMemoryStorage.d.ts +0 -31
  130. package/dist/adapters/inMemoryStorage.js +0 -110
  131. package/dist/ai-sdk/coordinatedTool.d.ts +0 -120
  132. package/dist/ai-sdk/coordinatedTool.js +0 -134
  133. package/dist/ai-sdk/coordinationContext.d.ts +0 -46
  134. package/dist/ai-sdk/coordinationContext.js +0 -106
  135. package/dist/ai-sdk/index.d.ts +0 -121
  136. package/dist/ai-sdk/index.js +0 -121
  137. package/dist/ai-sdk/wrap.d.ts +0 -65
  138. package/dist/ai-sdk/wrap.js +0 -39
  139. package/dist/auth/index.d.ts +0 -1
  140. package/dist/auth/index.js +0 -8
  141. package/dist/batching/index.d.ts +0 -55
  142. package/dist/batching/index.js +0 -147
  143. package/dist/cli.cjs +0 -288600
  144. package/dist/client/Ablo.d.ts +0 -231
  145. package/dist/client/Ablo.js +0 -149
  146. package/dist/client/abloClient.d.ts +0 -309
  147. package/dist/client/abloClient.js +0 -13
  148. package/dist/client/clientPrelude.d.ts +0 -52
  149. package/dist/client/clientPrelude.js +0 -60
  150. package/dist/client/consoleLogger.d.ts +0 -35
  151. package/dist/client/consoleLogger.js +0 -44
  152. package/dist/client/coreClient.d.ts +0 -60
  153. package/dist/client/coreClient.js +0 -118
  154. package/dist/client/createInternalComponents.d.ts +0 -46
  155. package/dist/client/createInternalComponents.js +0 -92
  156. package/dist/client/createModelProxy.d.ts +0 -228
  157. package/dist/client/createModelProxy.js +0 -818
  158. package/dist/client/humans.d.ts +0 -48
  159. package/dist/client/humans.js +0 -52
  160. package/dist/client/modelRegistration.d.ts +0 -10
  161. package/dist/client/modelRegistration.js +0 -312
  162. package/dist/client/options.d.ts +0 -461
  163. package/dist/client/options.js +0 -7
  164. package/dist/client/reactiveEngine.d.ts +0 -48
  165. package/dist/client/reactiveEngine.js +0 -910
  166. package/dist/client/resourceTypes.d.ts +0 -12
  167. package/dist/client/resourceTypes.js +0 -10
  168. package/dist/client/schemaConfig.d.ts +0 -44
  169. package/dist/client/schemaConfig.js +0 -185
  170. package/dist/client/validateAbloOptions.d.ts +0 -42
  171. package/dist/client/validateAbloOptions.js +0 -43
  172. package/dist/client/wsMutationExecutor.d.ts +0 -27
  173. package/dist/client/wsMutationExecutor.js +0 -72
  174. package/dist/context.d.ts +0 -29
  175. package/dist/context.js +0 -58
  176. package/dist/coordination/ClaimLog.d.ts +0 -26
  177. package/dist/coordination/ClaimLog.js +0 -32
  178. package/dist/coordination/index.d.ts +0 -1
  179. package/dist/coordination/index.js +0 -8
  180. package/dist/core/DatabaseManager.d.ts +0 -105
  181. package/dist/core/DatabaseManager.js +0 -387
  182. package/dist/core/QueryProcessor.d.ts +0 -75
  183. package/dist/core/QueryProcessor.js +0 -255
  184. package/dist/core/QueryView.d.ts +0 -79
  185. package/dist/core/QueryView.js +0 -218
  186. package/dist/core/StoreManager.d.ts +0 -112
  187. package/dist/core/StoreManager.js +0 -302
  188. package/dist/core/ViewRegistry.d.ts +0 -20
  189. package/dist/core/ViewRegistry.js +0 -55
  190. package/dist/core/index.d.ts +0 -33
  191. package/dist/core/index.js +0 -48
  192. package/dist/core/openIDBWithTimeout.d.ts +0 -65
  193. package/dist/core/openIDBWithTimeout.js +0 -153
  194. package/dist/core/queryUtils.d.ts +0 -45
  195. package/dist/core/queryUtils.js +0 -69
  196. package/dist/core/storeContract.d.ts +0 -145
  197. package/dist/core/storeContract.js +0 -12
  198. package/dist/docs/catalog.d.ts +0 -72
  199. package/dist/docs/catalog.js +0 -227
  200. package/dist/docs/index.d.ts +0 -10
  201. package/dist/docs/index.js +0 -10
  202. package/dist/environment.d.ts +0 -1
  203. package/dist/environment.js +0 -8
  204. package/dist/interfaces/index.d.ts +0 -311
  205. package/dist/interfaces/index.js +0 -9
  206. package/dist/keys/index.d.ts +0 -1
  207. package/dist/keys/index.js +0 -8
  208. package/dist/mutators/RecordingMutation.d.ts +0 -36
  209. package/dist/mutators/RecordingMutation.js +0 -182
  210. package/dist/mutators/Transaction.d.ts +0 -40
  211. package/dist/mutators/Transaction.js +0 -58
  212. package/dist/mutators/UndoManager.d.ts +0 -258
  213. package/dist/mutators/UndoManager.js +0 -658
  214. package/dist/mutators/defineMutators.d.ts +0 -60
  215. package/dist/mutators/defineMutators.js +0 -18
  216. package/dist/mutators/inverseOp.d.ts +0 -126
  217. package/dist/mutators/inverseOp.js +0 -71
  218. package/dist/mutators/mutateActions.d.ts +0 -45
  219. package/dist/mutators/mutateActions.js +0 -105
  220. package/dist/mutators/readerActions.d.ts +0 -33
  221. package/dist/mutators/readerActions.js +0 -57
  222. package/dist/mutators/undoApply.d.ts +0 -51
  223. package/dist/mutators/undoApply.js +0 -117
  224. package/dist/policy/index.d.ts +0 -21
  225. package/dist/policy/index.js +0 -20
  226. package/dist/query/client.d.ts +0 -61
  227. package/dist/query/client.js +0 -137
  228. package/dist/query/types.d.ts +0 -85
  229. package/dist/query/types.js +0 -16
  230. package/dist/react/AbloProvider.d.ts +0 -230
  231. package/dist/react/AbloProvider.js +0 -455
  232. package/dist/react/ClientSideSuspense.d.ts +0 -36
  233. package/dist/react/ClientSideSuspense.js +0 -17
  234. package/dist/react/DefaultFallback.d.ts +0 -24
  235. package/dist/react/DefaultFallback.js +0 -43
  236. package/dist/react/context.d.ts +0 -55
  237. package/dist/react/context.js +0 -29
  238. package/dist/react/index.d.ts +0 -61
  239. package/dist/react/index.js +0 -66
  240. package/dist/react/internalContext.d.ts +0 -33
  241. package/dist/react/internalContext.js +0 -3
  242. package/dist/react/useAblo.d.ts +0 -75
  243. package/dist/react/useAblo.js +0 -102
  244. package/dist/react/useCurrentUserId.d.ts +0 -22
  245. package/dist/react/useCurrentUserId.js +0 -34
  246. package/dist/react/useErrorListener.d.ts +0 -20
  247. package/dist/react/useErrorListener.js +0 -38
  248. package/dist/react/useMutationFailureListener.d.ts +0 -26
  249. package/dist/react/useMutationFailureListener.js +0 -38
  250. package/dist/react/useMutators.d.ts +0 -56
  251. package/dist/react/useMutators.js +0 -84
  252. package/dist/react/useReactive.d.ts +0 -35
  253. package/dist/react/useReactive.js +0 -123
  254. package/dist/react/useSyncStatus.d.ts +0 -59
  255. package/dist/react/useSyncStatus.js +0 -76
  256. package/dist/react/useUndoScope.d.ts +0 -34
  257. package/dist/react/useUndoScope.js +0 -81
  258. package/dist/schema/coordination.d.ts +0 -112
  259. package/dist/schema/coordination.js +0 -129
  260. package/dist/schema/ddl.d.ts +0 -97
  261. package/dist/schema/ddl.js +0 -491
  262. package/dist/schema/ddlLock.d.ts +0 -35
  263. package/dist/schema/ddlLock.js +0 -46
  264. package/dist/schema/diff.d.ts +0 -225
  265. package/dist/schema/diff.js +0 -289
  266. package/dist/schema/generate.d.ts +0 -19
  267. package/dist/schema/generate.js +0 -86
  268. package/dist/schema/index.d.ts +0 -41
  269. package/dist/schema/index.js +0 -76
  270. package/dist/schema/queries.d.ts +0 -201
  271. package/dist/schema/queries.js +0 -144
  272. package/dist/schema/select.d.ts +0 -40
  273. package/dist/schema/select.js +0 -87
  274. package/dist/schema/serialize.d.ts +0 -115
  275. package/dist/schema/serialize.js +0 -262
  276. package/dist/schema/sugar.d.ts +0 -109
  277. package/dist/schema/sugar.js +0 -83
  278. package/dist/schema/syncDeltaRow.d.ts +0 -6
  279. package/dist/schema/syncDeltaRow.js +0 -6
  280. package/dist/server/adapter.d.ts +0 -173
  281. package/dist/server/adapter.js +0 -18
  282. package/dist/server/commit.d.ts +0 -107
  283. package/dist/server/commit.js +0 -1
  284. package/dist/server/index.d.ts +0 -14
  285. package/dist/server/index.js +0 -2
  286. package/dist/server/readConfig.d.ts +0 -80
  287. package/dist/server/readConfig.js +0 -8
  288. package/dist/server/storageMode.d.ts +0 -23
  289. package/dist/server/storageMode.js +0 -17
  290. package/dist/source/adapter.d.ts +0 -81
  291. package/dist/source/adapter.js +0 -22
  292. package/dist/source/adapters/drizzle.d.ts +0 -48
  293. package/dist/source/adapters/drizzle.js +0 -219
  294. package/dist/source/adapters/kysely.d.ts +0 -42
  295. package/dist/source/adapters/kysely.js +0 -205
  296. package/dist/source/adapters/kyselyMutationCore.d.ts +0 -76
  297. package/dist/source/adapters/kyselyMutationCore.js +0 -125
  298. package/dist/source/adapters/memory.d.ts +0 -13
  299. package/dist/source/adapters/memory.js +0 -130
  300. package/dist/source/adapters/prisma.d.ts +0 -63
  301. package/dist/source/adapters/prisma.js +0 -202
  302. package/dist/source/conformance.d.ts +0 -37
  303. package/dist/source/conformance.js +0 -215
  304. package/dist/source/connector.d.ts +0 -95
  305. package/dist/source/connector.js +0 -266
  306. package/dist/source/connectorProtocol.d.ts +0 -154
  307. package/dist/source/connectorProtocol.js +0 -163
  308. package/dist/source/contract.d.ts +0 -195
  309. package/dist/source/contract.js +0 -164
  310. package/dist/source/factory.d.ts +0 -92
  311. package/dist/source/factory.js +0 -286
  312. package/dist/source/footprint.d.ts +0 -111
  313. package/dist/source/footprint.js +0 -0
  314. package/dist/source/idempotency.d.ts +0 -61
  315. package/dist/source/idempotency.js +0 -144
  316. package/dist/source/index.d.ts +0 -23
  317. package/dist/source/index.js +0 -30
  318. package/dist/source/migrations.d.ts +0 -21
  319. package/dist/source/migrations.js +0 -103
  320. package/dist/source/next.d.ts +0 -32
  321. package/dist/source/next.js +0 -25
  322. package/dist/source/pushQueue.d.ts +0 -134
  323. package/dist/source/pushQueue.js +0 -256
  324. package/dist/source/signing.d.ts +0 -92
  325. package/dist/source/signing.js +0 -162
  326. package/dist/source/types.d.ts +0 -401
  327. package/dist/source/types.js +0 -59
  328. package/dist/stores/ObjectStore.d.ts +0 -115
  329. package/dist/stores/ObjectStore.js +0 -393
  330. package/dist/stores/ObjectStoreContract.d.ts +0 -38
  331. package/dist/stores/ObjectStoreContract.js +0 -1
  332. package/dist/stores/SyncActionStore.d.ts +0 -97
  333. package/dist/stores/SyncActionStore.js +0 -504
  334. package/dist/stores/syncAction.d.ts +0 -26
  335. package/dist/stores/syncAction.js +0 -16
  336. package/dist/surface.d.ts +0 -36
  337. package/dist/surface.js +0 -75
  338. package/dist/sync/BootstrapFetcher.d.ts +0 -280
  339. package/dist/sync/BootstrapFetcher.js +0 -962
  340. package/dist/sync/ConnectionManager.d.ts +0 -8
  341. package/dist/sync/ConnectionManager.js +0 -8
  342. package/dist/sync/OnDemandLoader.d.ts +0 -228
  343. package/dist/sync/OnDemandLoader.js +0 -742
  344. package/dist/sync/SubscriptionManager.d.ts +0 -159
  345. package/dist/sync/SubscriptionManager.js +0 -243
  346. package/dist/sync/SyncWebSocket.d.ts +0 -173
  347. package/dist/sync/SyncWebSocket.js +0 -438
  348. package/dist/sync/awaitClaimGrant.d.ts +0 -6
  349. package/dist/sync/awaitClaimGrant.js +0 -6
  350. package/dist/sync/bootstrapApply.d.ts +0 -70
  351. package/dist/sync/bootstrapApply.js +0 -73
  352. package/dist/sync/commitFrames.d.ts +0 -8
  353. package/dist/sync/commitFrames.js +0 -8
  354. package/dist/sync/contextPorts.d.ts +0 -18
  355. package/dist/sync/contextPorts.js +0 -31
  356. package/dist/sync/createClaimStream.d.ts +0 -7
  357. package/dist/sync/createClaimStream.js +0 -7
  358. package/dist/sync/createPresenceStream.d.ts +0 -69
  359. package/dist/sync/createPresenceStream.js +0 -200
  360. package/dist/sync/createSnapshot.d.ts +0 -29
  361. package/dist/sync/createSnapshot.js +0 -118
  362. package/dist/sync/credentialLifecycle.d.ts +0 -7
  363. package/dist/sync/credentialLifecycle.js +0 -7
  364. package/dist/sync/deltaPipeline.d.ts +0 -113
  365. package/dist/sync/deltaPipeline.js +0 -261
  366. package/dist/sync/groupChange.d.ts +0 -113
  367. package/dist/sync/groupChange.js +0 -242
  368. package/dist/sync/participants.d.ts +0 -115
  369. package/dist/sync/participants.js +0 -344
  370. package/dist/sync/persistedPrefix.d.ts +0 -12
  371. package/dist/sync/persistedPrefix.js +0 -22
  372. package/dist/sync/schemaDrift.d.ts +0 -55
  373. package/dist/sync/schemaDrift.js +0 -53
  374. package/dist/sync/schemas.d.ts +0 -70
  375. package/dist/sync/schemas.js +0 -94
  376. package/dist/sync/syncCursor.d.ts +0 -40
  377. package/dist/sync/syncCursor.js +0 -55
  378. package/dist/sync/syncPlan.d.ts +0 -54
  379. package/dist/sync/syncPlan.js +0 -50
  380. package/dist/sync/wsFrameHandlers.d.ts +0 -8
  381. package/dist/sync/wsFrameHandlers.js +0 -8
  382. package/dist/testing/fixtures/bootstrap.d.ts +0 -49
  383. package/dist/testing/fixtures/bootstrap.js +0 -59
  384. package/dist/testing/fixtures/deltas.d.ts +0 -83
  385. package/dist/testing/fixtures/deltas.js +0 -136
  386. package/dist/testing/fixtures/httpResponses.d.ts +0 -70
  387. package/dist/testing/fixtures/httpResponses.js +0 -90
  388. package/dist/testing/fixtures/models.d.ts +0 -83
  389. package/dist/testing/fixtures/models.js +0 -272
  390. package/dist/testing/helpers/reactWrapper.d.ts +0 -69
  391. package/dist/testing/helpers/reactWrapper.js +0 -67
  392. package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
  393. package/dist/testing/helpers/syncEngineHarness.js +0 -73
  394. package/dist/testing/helpers/wait.d.ts +0 -30
  395. package/dist/testing/helpers/wait.js +0 -49
  396. package/dist/testing/index.d.ts +0 -23
  397. package/dist/testing/index.js +0 -33
  398. package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
  399. package/dist/testing/mocks/FakeDatabase.js +0 -10
  400. package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
  401. package/dist/testing/mocks/MockMutationExecutor.js +0 -186
  402. package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
  403. package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
  404. package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
  405. package/dist/testing/mocks/MockSyncContext.js +0 -72
  406. package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
  407. package/dist/testing/mocks/MockSyncStore.js +0 -171
  408. package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
  409. package/dist/testing/mocks/MockWebSocket.js +0 -118
  410. package/dist/transaction/ablo.d.ts +0 -88
  411. package/dist/transaction/ablo.js +0 -33
  412. package/dist/transaction/auth/apiKey.d.ts +0 -152
  413. package/dist/transaction/auth/apiKey.js +0 -419
  414. package/dist/transaction/auth/bootstrapScope.d.ts +0 -15
  415. package/dist/transaction/auth/bootstrapScope.js +0 -1
  416. package/dist/transaction/auth/capability.d.ts +0 -177
  417. package/dist/transaction/auth/capability.js +0 -199
  418. package/dist/transaction/auth/credentialEndpoint.d.ts +0 -61
  419. package/dist/transaction/auth/credentialEndpoint.js +0 -86
  420. package/dist/transaction/auth/credentialPolicy.d.ts +0 -148
  421. package/dist/transaction/auth/credentialPolicy.js +0 -125
  422. package/dist/transaction/auth/credentialSource.d.ts +0 -30
  423. package/dist/transaction/auth/credentialSource.js +0 -55
  424. package/dist/transaction/auth/hostedEndpoints.d.ts +0 -21
  425. package/dist/transaction/auth/hostedEndpoints.js +0 -21
  426. package/dist/transaction/auth/identity.d.ts +0 -55
  427. package/dist/transaction/auth/identity.js +0 -210
  428. package/dist/transaction/auth/index.d.ts +0 -162
  429. package/dist/transaction/auth/index.js +0 -304
  430. package/dist/transaction/auth/schemas.d.ts +0 -59
  431. package/dist/transaction/auth/schemas.js +0 -85
  432. package/dist/transaction/auth/sessionMint.d.ts +0 -28
  433. package/dist/transaction/auth/sessionMint.js +0 -85
  434. package/dist/transaction/coordination/awaitClaimGrant.d.ts +0 -49
  435. package/dist/transaction/coordination/awaitClaimGrant.js +0 -112
  436. package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +0 -50
  437. package/dist/transaction/coordination/claimHeartbeatLoop.js +0 -88
  438. package/dist/transaction/coordination/claimMeta.d.ts +0 -49
  439. package/dist/transaction/coordination/claimMeta.js +0 -52
  440. package/dist/transaction/coordination/createClaimStream.d.ts +0 -64
  441. package/dist/transaction/coordination/createClaimStream.js +0 -475
  442. package/dist/transaction/coordination/events.d.ts +0 -74
  443. package/dist/transaction/coordination/events.js +0 -7
  444. package/dist/transaction/coordination/index.d.ts +0 -19
  445. package/dist/transaction/coordination/index.js +0 -44
  446. package/dist/transaction/coordination/locator.d.ts +0 -83
  447. package/dist/transaction/coordination/locator.js +0 -82
  448. package/dist/transaction/coordination/schema.d.ts +0 -1473
  449. package/dist/transaction/coordination/schema.js +0 -1013
  450. package/dist/transaction/coordination/targetConflict.d.ts +0 -2
  451. package/dist/transaction/coordination/targetConflict.js +0 -103
  452. package/dist/transaction/coordination/trace.d.ts +0 -78
  453. package/dist/transaction/coordination/trace.js +0 -138
  454. package/dist/transaction/durableWrites.d.ts +0 -62
  455. package/dist/transaction/durableWrites.js +0 -71
  456. package/dist/transaction/environment.d.ts +0 -105
  457. package/dist/transaction/environment.js +0 -108
  458. package/dist/transaction/errorCodes.d.ts +0 -403
  459. package/dist/transaction/errorCodes.js +0 -480
  460. package/dist/transaction/errors.d.ts +0 -428
  461. package/dist/transaction/errors.js +0 -686
  462. package/dist/transaction/index.d.ts +0 -20
  463. package/dist/transaction/index.js +0 -20
  464. package/dist/transaction/keys/index.d.ts +0 -87
  465. package/dist/transaction/keys/index.js +0 -207
  466. package/dist/transaction/log/syncDeltaRow.d.ts +0 -158
  467. package/dist/transaction/log/syncDeltaRow.js +0 -95
  468. package/dist/transaction/logPosition.d.ts +0 -97
  469. package/dist/transaction/logPosition.js +0 -125
  470. package/dist/transaction/logger.d.ts +0 -16
  471. package/dist/transaction/logger.js +0 -7
  472. package/dist/transaction/observability.d.ts +0 -53
  473. package/dist/transaction/observability.js +0 -19
  474. package/dist/transaction/persistence.d.ts +0 -12
  475. package/dist/transaction/persistence.js +0 -11
  476. package/dist/transaction/plugin.d.ts +0 -192
  477. package/dist/transaction/plugin.js +0 -87
  478. package/dist/transaction/policy/types.d.ts +0 -217
  479. package/dist/transaction/policy/types.js +0 -126
  480. package/dist/transaction/resources/functionalUpdate.d.ts +0 -79
  481. package/dist/transaction/resources/functionalUpdate.js +0 -87
  482. package/dist/transaction/resources/httpResources.d.ts +0 -266
  483. package/dist/transaction/resources/httpResources.js +0 -7
  484. package/dist/transaction/resources/modelOperations.d.ts +0 -319
  485. package/dist/transaction/resources/modelOperations.js +0 -12
  486. package/dist/transaction/resources/mutationOptions.d.ts +0 -66
  487. package/dist/transaction/resources/mutationOptions.js +0 -9
  488. package/dist/transaction/resources/where.d.ts +0 -85
  489. package/dist/transaction/resources/where.js +0 -70
  490. package/dist/transaction/resources/writeOptionsSchema.d.ts +0 -47
  491. package/dist/transaction/resources/writeOptionsSchema.js +0 -73
  492. package/dist/transaction/schema/field.d.ts +0 -126
  493. package/dist/transaction/schema/field.js +0 -265
  494. package/dist/transaction/schema/loadStrategy.d.ts +0 -45
  495. package/dist/transaction/schema/loadStrategy.js +0 -46
  496. package/dist/transaction/schema/model.d.ts +0 -379
  497. package/dist/transaction/schema/model.js +0 -123
  498. package/dist/transaction/schema/openapi.d.ts +0 -57
  499. package/dist/transaction/schema/openapi.js +0 -340
  500. package/dist/transaction/schema/relation.d.ts +0 -199
  501. package/dist/transaction/schema/relation.js +0 -104
  502. package/dist/transaction/schema/residency.d.ts +0 -29
  503. package/dist/transaction/schema/residency.js +0 -25
  504. package/dist/transaction/schema/roles.d.ts +0 -249
  505. package/dist/transaction/schema/roles.js +0 -230
  506. package/dist/transaction/schema/schema.d.ts +0 -324
  507. package/dist/transaction/schema/schema.js +0 -305
  508. package/dist/transaction/schema/tenancy.d.ts +0 -139
  509. package/dist/transaction/schema/tenancy.js +0 -190
  510. package/dist/transaction/transactionLayer.d.ts +0 -82
  511. package/dist/transaction/transactionLayer.js +0 -24
  512. package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +0 -143
  513. package/dist/transaction/transactions/settlement/commitEnvelope.js +0 -161
  514. package/dist/transaction/transactions/settlement/httpCommitEnvelope.d.ts +0 -53
  515. package/dist/transaction/transactions/settlement/httpCommitEnvelope.js +0 -207
  516. package/dist/transaction/transactions/settlement/idempotencyKey.d.ts +0 -10
  517. package/dist/transaction/transactions/settlement/idempotencyKey.js +0 -9
  518. package/dist/transaction/transactions/settlement/pendingWrite.d.ts +0 -112
  519. package/dist/transaction/transactions/settlement/pendingWrite.js +0 -20
  520. package/dist/transaction/transport/commitFrames.d.ts +0 -90
  521. package/dist/transaction/transport/commitFrames.js +0 -134
  522. package/dist/transaction/transport/connectionManager.d.ts +0 -215
  523. package/dist/transaction/transport/connectionManager.js +0 -673
  524. package/dist/transaction/transport/credentialLifecycle.d.ts +0 -177
  525. package/dist/transaction/transport/credentialLifecycle.js +0 -324
  526. package/dist/transaction/transport/heartbeat.d.ts +0 -65
  527. package/dist/transaction/transport/heartbeat.js +0 -93
  528. package/dist/transaction/transport/httpClient.d.ts +0 -123
  529. package/dist/transaction/transport/httpClient.js +0 -145
  530. package/dist/transaction/transport/httpOptions.d.ts +0 -33
  531. package/dist/transaction/transport/httpOptions.js +0 -12
  532. package/dist/transaction/transport/httpTransport.d.ts +0 -8
  533. package/dist/transaction/transport/httpTransport.js +0 -1276
  534. package/dist/transaction/transport/networkProbe.d.ts +0 -84
  535. package/dist/transaction/transport/networkProbe.js +0 -207
  536. package/dist/transaction/transport/wsFrameHandlers.d.ts +0 -128
  537. package/dist/transaction/transport/wsFrameHandlers.js +0 -429
  538. package/dist/transaction/transport/wsTransport.d.ts +0 -576
  539. package/dist/transaction/transport/wsTransport.js +0 -1017
  540. package/dist/transaction/types/assertExact.d.ts +0 -17
  541. package/dist/transaction/types/assertExact.js +0 -1
  542. package/dist/transaction/types/global.d.ts +0 -107
  543. package/dist/transaction/types/global.js +0 -40
  544. package/dist/transaction/types/index.d.ts +0 -205
  545. package/dist/transaction/types/index.js +0 -56
  546. package/dist/transaction/types/modelData.d.ts +0 -10
  547. package/dist/transaction/types/modelData.js +0 -9
  548. package/dist/transaction/types/participant.d.ts +0 -20
  549. package/dist/transaction/types/participant.js +0 -10
  550. package/dist/transaction/types/streams.d.ts +0 -540
  551. package/dist/transaction/types/streams.js +0 -11
  552. package/dist/transaction/utils/asyncIterator.d.ts +0 -34
  553. package/dist/transaction/utils/asyncIterator.js +0 -135
  554. package/dist/transaction/utils/duration.d.ts +0 -25
  555. package/dist/transaction/utils/duration.js +0 -45
  556. package/dist/transaction/utils/json.d.ts +0 -57
  557. package/dist/transaction/utils/json.js +0 -276
  558. package/dist/transaction/wire/accountResponses.d.ts +0 -351
  559. package/dist/transaction/wire/accountResponses.js +0 -255
  560. package/dist/transaction/wire/auth.d.ts +0 -49
  561. package/dist/transaction/wire/auth.js +0 -57
  562. package/dist/transaction/wire/bootstrapReason.d.ts +0 -9
  563. package/dist/transaction/wire/bootstrapReason.js +0 -8
  564. package/dist/transaction/wire/claimEvent.d.ts +0 -76
  565. package/dist/transaction/wire/claimEvent.js +0 -73
  566. package/dist/transaction/wire/claims.d.ts +0 -463
  567. package/dist/transaction/wire/claims.js +0 -229
  568. package/dist/transaction/wire/commit.d.ts +0 -603
  569. package/dist/transaction/wire/commit.js +0 -321
  570. package/dist/transaction/wire/delta.d.ts +0 -250
  571. package/dist/transaction/wire/delta.js +0 -147
  572. package/dist/transaction/wire/errorEnvelope.d.ts +0 -72
  573. package/dist/transaction/wire/errorEnvelope.js +0 -123
  574. package/dist/transaction/wire/feedCursor.d.ts +0 -60
  575. package/dist/transaction/wire/feedCursor.js +0 -82
  576. package/dist/transaction/wire/feedEvent.d.ts +0 -177
  577. package/dist/transaction/wire/feedEvent.js +0 -39
  578. package/dist/transaction/wire/frames.d.ts +0 -194
  579. package/dist/transaction/wire/frames.js +0 -50
  580. package/dist/transaction/wire/inboundFrames.d.ts +0 -552
  581. package/dist/transaction/wire/inboundFrames.js +0 -116
  582. package/dist/transaction/wire/index.d.ts +0 -50
  583. package/dist/transaction/wire/index.js +0 -74
  584. package/dist/transaction/wire/listEnvelope.d.ts +0 -37
  585. package/dist/transaction/wire/listEnvelope.js +0 -42
  586. package/dist/transaction/wire/modelResponses.d.ts +0 -85
  587. package/dist/transaction/wire/modelResponses.js +0 -43
  588. package/dist/transaction/wire/protocol.d.ts +0 -38
  589. package/dist/transaction/wire/protocol.js +0 -38
  590. package/dist/transaction/wire/protocolVersion.d.ts +0 -73
  591. package/dist/transaction/wire/protocolVersion.js +0 -83
  592. package/dist/transactions/mutations/MutationQueue.d.ts +0 -655
  593. package/dist/transactions/mutations/MutationQueue.js +0 -2797
  594. package/dist/transactions/mutations/MutationStore.d.ts +0 -20
  595. package/dist/transactions/mutations/MutationStore.js +0 -53
  596. package/dist/transactions/mutations/UnconfirmedWrites.d.ts +0 -82
  597. package/dist/transactions/mutations/UnconfirmedWrites.js +0 -104
  598. package/dist/transactions/mutations/coalesceRules.d.ts +0 -58
  599. package/dist/transactions/mutations/coalesceRules.js +0 -140
  600. package/dist/transactions/mutations/commitLatency.d.ts +0 -52
  601. package/dist/transactions/mutations/commitLatency.js +0 -130
  602. package/dist/transactions/mutations/commitOutboxStore.d.ts +0 -28
  603. package/dist/transactions/mutations/commitOutboxStore.js +0 -26
  604. package/dist/transactions/mutations/commitPayload.d.ts +0 -164
  605. package/dist/transactions/mutations/commitPayload.js +0 -152
  606. package/dist/transactions/mutations/deltaConfirmation.d.ts +0 -59
  607. package/dist/transactions/mutations/deltaConfirmation.js +0 -233
  608. package/dist/transactions/mutations/durableWriteStore.d.ts +0 -14
  609. package/dist/transactions/mutations/durableWriteStore.js +0 -12
  610. package/dist/transactions/mutations/optimisticApply.d.ts +0 -49
  611. package/dist/transactions/mutations/optimisticApply.js +0 -65
  612. package/dist/transactions/mutations/replayValidation.d.ts +0 -186
  613. package/dist/transactions/mutations/replayValidation.js +0 -163
  614. package/dist/utils/mobxSetup.d.ts +0 -53
  615. package/dist/utils/mobxSetup.js +0 -330
  616. package/dist/webhooks/events.d.ts +0 -43
  617. package/dist/webhooks/events.js +0 -42
  618. package/dist/webhooks/index.d.ts +0 -8
  619. package/dist/webhooks/index.js +0 -8
  620. package/dist/wire/index.d.ts +0 -1
  621. package/dist/wire/index.js +0 -8
  622. package/docs/interaction-model.md +0 -99
@@ -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
- }