@abloatai/ablo 0.35.0 → 0.36.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 (215) hide show
  1. package/CHANGELOG.md +713 -629
  2. package/README.md +56 -519
  3. package/bin/ablo.cjs +39 -0
  4. package/dist/BaseSyncedStore.d.ts +24 -4
  5. package/dist/BaseSyncedStore.js +53 -37
  6. package/dist/Database.d.ts +8 -20
  7. package/dist/Database.js +61 -59
  8. package/dist/InstanceCache.d.ts +6 -2
  9. package/dist/InstanceCache.js +18 -16
  10. package/dist/Model.d.ts +17 -7
  11. package/dist/Model.js +17 -7
  12. package/dist/ModelRegistry.d.ts +4 -0
  13. package/dist/ModelRegistry.js +17 -15
  14. package/dist/NetworkMonitor.d.ts +3 -1
  15. package/dist/NetworkMonitor.js +7 -5
  16. package/dist/SyncClient.d.ts +5 -15
  17. package/dist/SyncClient.js +60 -57
  18. package/dist/client/Ablo.js +30 -19
  19. package/dist/client/createInternalComponents.d.ts +4 -0
  20. package/dist/client/createInternalComponents.js +8 -2
  21. package/dist/client/createModelProxy.d.ts +21 -1
  22. package/dist/client/createModelProxy.js +123 -57
  23. package/dist/client/humans.d.ts +30 -9
  24. package/dist/client/humans.js +45 -19
  25. package/dist/client/reactiveEngine.d.ts +16 -11
  26. package/dist/client/reactiveEngine.js +113 -335
  27. package/dist/client/storeCluster.d.ts +47 -0
  28. package/dist/client/storeCluster.js +118 -0
  29. package/dist/client/storeLifecycle.d.ts +61 -0
  30. package/dist/client/storeLifecycle.js +231 -0
  31. package/dist/context.d.ts +13 -0
  32. package/dist/context.js +23 -0
  33. package/dist/core/index.d.ts +1 -1
  34. package/dist/docs/catalog.js +6 -3
  35. package/dist/index.d.ts +4 -2
  36. package/dist/index.js +4 -2
  37. package/dist/query/client.d.ts +3 -0
  38. package/dist/query/client.js +6 -5
  39. package/dist/react/AbloProvider.d.ts +13 -1
  40. package/dist/react/AbloProvider.js +5 -2
  41. package/dist/react/context.d.ts +2 -2
  42. package/dist/react/createAbloReact.d.ts +56 -0
  43. package/dist/react/createAbloReact.js +51 -0
  44. package/dist/react/index.d.ts +1 -0
  45. package/dist/react/index.js +3 -0
  46. package/dist/react/useAblo.d.ts +9 -2
  47. package/dist/react/useAblo.js +25 -7
  48. package/dist/schema/coordination.js +5 -1
  49. package/dist/schema/index.d.ts +1 -0
  50. package/dist/schema/index.js +4 -0
  51. package/dist/schema/select.js +3 -0
  52. package/dist/schema/serialize.js +3 -0
  53. package/dist/source/adapter.d.ts +7 -5
  54. package/dist/source/adapter.js +7 -5
  55. package/dist/source/index.d.ts +1 -1
  56. package/dist/source/index.js +1 -1
  57. package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
  58. package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
  59. package/dist/{core → stores}/DatabaseManager.js +13 -12
  60. package/dist/{core → stores}/StoreManager.d.ts +5 -3
  61. package/dist/{core → stores}/StoreManager.js +24 -22
  62. package/dist/stores/SyncActionStore.d.ts +3 -1
  63. package/dist/stores/SyncActionStore.js +9 -7
  64. package/dist/sync/BootstrapFetcher.d.ts +4 -0
  65. package/dist/sync/BootstrapFetcher.js +28 -26
  66. package/dist/sync/OnDemandLoader.d.ts +3 -0
  67. package/dist/sync/OnDemandLoader.js +1 -0
  68. package/dist/sync/bootstrapApply.d.ts +3 -0
  69. package/dist/sync/bootstrapApply.js +2 -2
  70. package/dist/sync/deltaPipeline.d.ts +13 -12
  71. package/dist/sync/deltaPipeline.js +21 -4
  72. package/dist/sync/groupChange.d.ts +3 -0
  73. package/dist/sync/groupChange.js +16 -14
  74. package/dist/sync/participants.d.ts +19 -2
  75. package/dist/sync/participants.js +3 -1
  76. package/dist/sync/schemas.d.ts +2 -1
  77. package/dist/sync/schemas.js +3 -3
  78. package/dist/syncLog/contract.d.ts +20 -0
  79. package/dist/syncLog/contract.js +19 -0
  80. package/dist/syncLog/index.d.ts +1 -0
  81. package/dist/syncLog/index.js +1 -0
  82. package/dist/transaction/auth/capability.d.ts +35 -0
  83. package/dist/transaction/auth/capability.js +25 -0
  84. package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
  85. package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
  86. package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
  87. package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
  88. package/dist/transaction/coordination/index.d.ts +4 -4
  89. package/dist/transaction/coordination/index.js +4 -3
  90. package/dist/transaction/coordination/locator.d.ts +23 -2
  91. package/dist/transaction/coordination/locator.js +22 -2
  92. package/dist/transaction/coordination/schema.d.ts +125 -62
  93. package/dist/transaction/coordination/schema.js +228 -64
  94. package/dist/transaction/coordination/targetConflict.js +32 -28
  95. package/dist/transaction/errorCodes.d.ts +2 -2
  96. package/dist/transaction/errorCodes.js +13 -9
  97. package/dist/transaction/plugin.d.ts +95 -2
  98. package/dist/transaction/plugin.js +21 -2
  99. package/dist/transaction/resources/httpResources.d.ts +57 -2
  100. package/dist/transaction/resources/modelOperations.d.ts +148 -40
  101. package/dist/transaction/resources/where.d.ts +16 -0
  102. package/dist/transaction/resources/where.js +45 -0
  103. package/dist/transaction/schema/field.d.ts +12 -18
  104. package/dist/transaction/schema/fieldRef.d.ts +38 -0
  105. package/dist/transaction/schema/fieldRef.js +11 -0
  106. package/dist/transaction/schema/openapi.d.ts +16 -15
  107. package/dist/transaction/schema/openapi.js +186 -25
  108. package/dist/transaction/schema/relation.d.ts +7 -2
  109. package/dist/transaction/schema/schema.d.ts +27 -0
  110. package/dist/transaction/schema/schema.js +20 -0
  111. package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
  112. package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
  113. package/dist/transaction/transport/httpClient.d.ts +9 -1
  114. package/dist/transaction/transport/httpClient.js +1 -0
  115. package/dist/transaction/transport/httpTransport.js +156 -44
  116. package/dist/transaction/transport/wsTransport.d.ts +2 -4
  117. package/dist/transaction/transport/wsTransport.js +16 -10
  118. package/dist/transaction/types/streams.d.ts +10 -0
  119. package/dist/transaction/utils/duration.d.ts +25 -0
  120. package/dist/transaction/utils/duration.js +32 -0
  121. package/dist/transaction/wire/accountResponses.d.ts +69 -0
  122. package/dist/transaction/wire/accountResponses.js +36 -1
  123. package/dist/transaction/wire/auth.d.ts +9 -2
  124. package/dist/transaction/wire/auth.js +7 -1
  125. package/dist/transaction/wire/claims.d.ts +164 -97
  126. package/dist/transaction/wire/claims.js +126 -28
  127. package/dist/transaction/wire/commit.d.ts +1 -1
  128. package/dist/transaction/wire/feedEvent.d.ts +27 -0
  129. package/dist/transaction/wire/feedEvent.js +27 -1
  130. package/dist/transaction/wire/frames.d.ts +2 -2
  131. package/dist/transaction/wire/inboundFrames.d.ts +12 -2
  132. package/dist/transaction/wire/index.d.ts +10 -6
  133. package/dist/transaction/wire/index.js +12 -3
  134. package/dist/transaction/wire/modelMutations.d.ts +31 -0
  135. package/dist/transaction/wire/modelMutations.js +52 -0
  136. package/dist/transaction/wire/modelShape.d.ts +78 -0
  137. package/dist/transaction/wire/modelShape.js +74 -0
  138. package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
  139. package/dist/transactions/mutations/MutationQueue.js +55 -45
  140. package/dist/transactions/mutations/commitPayload.d.ts +3 -2
  141. package/dist/transactions/mutations/commitPayload.js +5 -5
  142. package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
  143. package/dist/transactions/mutations/deltaConfirmation.js +10 -8
  144. package/dist/transactions/mutations/replayValidation.d.ts +2 -1
  145. package/dist/transactions/mutations/replayValidation.js +3 -2
  146. package/dist/{core → views}/QueryView.d.ts +1 -1
  147. package/dist/{core → views}/QueryView.js +1 -1
  148. package/dist/{core → views}/ViewRegistry.d.ts +1 -1
  149. package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
  150. package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
  151. package/docs/agents.md +1 -1
  152. package/docs/api-keys.md +6 -6
  153. package/docs/api.md +5 -43
  154. package/docs/audit.md +4 -3
  155. package/docs/cli.md +11 -11
  156. package/docs/client-behavior.md +4 -4
  157. package/docs/concurrency-convention.md +28 -42
  158. package/docs/coordination.md +235 -83
  159. package/docs/data-sources.md +4 -4
  160. package/docs/debugging.md +34 -12
  161. package/docs/deployment.md +8 -8
  162. package/docs/examples/scoped-agent.md +3 -3
  163. package/docs/groups.md +57 -3
  164. package/docs/guarantees.md +37 -10
  165. package/docs/how-it-works.md +29 -5
  166. package/docs/idempotency.md +6 -6
  167. package/docs/identity.md +22 -23
  168. package/docs/index.md +8 -8
  169. package/docs/integration-guide.md +14 -3
  170. package/docs/mcp.md +7 -7
  171. package/docs/migration.md +34 -15
  172. package/docs/projects.md +1 -1
  173. package/docs/react.md +19 -8
  174. package/docs/sessions.md +1 -1
  175. package/docs/webhooks.md +9 -9
  176. package/llms.txt +4 -4
  177. package/package.json +13 -20
  178. package/dist/cli.cjs +0 -288600
  179. package/dist/testing/fixtures/bootstrap.d.ts +0 -49
  180. package/dist/testing/fixtures/bootstrap.js +0 -59
  181. package/dist/testing/fixtures/deltas.d.ts +0 -83
  182. package/dist/testing/fixtures/deltas.js +0 -136
  183. package/dist/testing/fixtures/httpResponses.d.ts +0 -70
  184. package/dist/testing/fixtures/httpResponses.js +0 -90
  185. package/dist/testing/fixtures/models.d.ts +0 -83
  186. package/dist/testing/fixtures/models.js +0 -272
  187. package/dist/testing/helpers/reactWrapper.d.ts +0 -69
  188. package/dist/testing/helpers/reactWrapper.js +0 -67
  189. package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
  190. package/dist/testing/helpers/syncEngineHarness.js +0 -73
  191. package/dist/testing/helpers/wait.d.ts +0 -30
  192. package/dist/testing/helpers/wait.js +0 -49
  193. package/dist/testing/index.d.ts +0 -23
  194. package/dist/testing/index.js +0 -33
  195. package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
  196. package/dist/testing/mocks/FakeDatabase.js +0 -10
  197. package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
  198. package/dist/testing/mocks/MockMutationExecutor.js +0 -186
  199. package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
  200. package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
  201. package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
  202. package/dist/testing/mocks/MockSyncContext.js +0 -72
  203. package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
  204. package/dist/testing/mocks/MockSyncStore.js +0 -171
  205. package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
  206. package/dist/testing/mocks/MockWebSocket.js +0 -118
  207. package/docs/interaction-model.md +0 -99
  208. /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
  209. /package/dist/{core → query}/QueryProcessor.js +0 -0
  210. /package/dist/{core/storeContract.js → storeContract.js} +0 -0
  211. /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
  212. /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
  213. /package/dist/{source → transaction}/footprint.d.ts +0 -0
  214. /package/dist/{source → transaction}/footprint.js +0 -0
  215. /package/dist/{core → views}/ViewRegistry.js +0 -0
package/dist/Database.js CHANGED
@@ -5,11 +5,11 @@
5
5
  * server. {@link BaseSyncedStore} drives it, and {@link InstanceCache} holds the
6
6
  * in-memory mirror of what this class persists.
7
7
  */
8
- import { DatabaseManager } from './core/DatabaseManager.js';
9
- import { StoreManager } from './core/StoreManager.js';
8
+ import { DatabaseManager } from './stores/DatabaseManager.js';
9
+ import { StoreManager } from './stores/StoreManager.js';
10
10
  import { ModelRegistry } from './ModelRegistry.js';
11
11
  import { LoadStrategy } from './transaction/types/index.js';
12
- import { getContext } from './context.js';
12
+ import { globalRuntime } from './context.js';
13
13
  import { AbloConnectionError, AbloValidationError } from './transaction/errors.js';
14
14
  import { InMemoryObjectStore } from './adapters/inMemoryStorage.js';
15
15
  import { logPositionSchema } from './transaction/logPosition.js';
@@ -110,7 +110,7 @@ export class Database {
110
110
  /** Essential fields that must be preserved during partial UPDATE merges.
111
111
  * Sourced from SyncEngineConfig.essentialFields — consumers define their own. */
112
112
  get essentialFields() {
113
- return getContext().config.essentialFields;
113
+ return this.runtime.config.essentialFields;
114
114
  }
115
115
  /**
116
116
  * When true, all IndexedDB operations are replaced with in-memory Maps.
@@ -121,13 +121,15 @@ export class Database {
121
121
  * new Database(registry, bootstrap, { inMemory: true })
122
122
  */
123
123
  inMemory;
124
+ runtime;
124
125
  /** In-memory stores used when inMemory=true. Keyed by model name. */
125
126
  inMemoryStores = new Map();
126
127
  /** In-memory workspace metadata when inMemory=true. */
127
128
  inMemoryMetadata = null;
128
129
  constructor(modelRegistry, bootstrapHelper, options) {
129
- this.databaseManager = new DatabaseManager();
130
- this.storeManager = new StoreManager(modelRegistry);
130
+ this.runtime = options?.runtime ?? globalRuntime;
131
+ this.databaseManager = new DatabaseManager(this.runtime);
132
+ this.storeManager = new StoreManager(modelRegistry, this.runtime);
131
133
  this.modelRegistry = modelRegistry;
132
134
  this.bootstrapHelper = bootstrapHelper;
133
135
  this.inMemory = options?.inMemory ?? false;
@@ -149,7 +151,7 @@ export class Database {
149
151
  ? this.inMemoryStores.get(modelName)
150
152
  : this.storeManager.getStore(modelName);
151
153
  if (!store && context) {
152
- getContext().observability.breadcrumb(`Store not found for model: ${modelName}`, 'sync.database', 'warning', { context });
154
+ this.runtime.observability.breadcrumb(`Store not found for model: ${modelName}`, 'sync.database', 'warning', { context });
153
155
  }
154
156
  return store;
155
157
  }
@@ -170,7 +172,7 @@ export class Database {
170
172
  const requiredFields = this.essentialFields[modelName] ?? [];
171
173
  const preserved = requiredFields.filter((field) => existing[field] !== undefined && delta[field] === undefined);
172
174
  if (preserved.length > 0) {
173
- getContext().logger.debug('[Database] UPDATE merged - preserved fields', {
175
+ this.runtime.logger.debug('[Database] UPDATE merged - preserved fields', {
174
176
  modelName,
175
177
  modelId: modelId.slice(0, 12),
176
178
  deltaFields: Object.keys(delta),
@@ -188,7 +190,7 @@ export class Database {
188
190
  // Creates InMemoryObjectStore instances for all registered models.
189
191
  // Bootstrap via HTTP still works; only local persistence is skipped.
190
192
  if (this.inMemory) {
191
- getContext().logger.debug('Opening in-memory database (headless mode)');
193
+ this.runtime.logger.debug('Opening in-memory database (headless mode)');
192
194
  const allModels = this.modelRegistry.getRegisteredModelNames();
193
195
  for (const modelName of allModels) {
194
196
  const storeName = `store_${modelName.toLowerCase()}`;
@@ -196,11 +198,11 @@ export class Database {
196
198
  }
197
199
  // Create a __transactions store for the offline queue
198
200
  this.inMemoryStores.set('__transactions', new InMemoryObjectStore('__transactions', '__transactions'));
199
- getContext().logger.info(`In-memory database opened: ${this.inMemoryStores.size} stores`);
201
+ this.runtime.logger.info(`In-memory database opened: ${this.inMemoryStores.size} stores`);
200
202
  return;
201
203
  }
202
204
  // ── Browser mode: IndexedDB (existing behavior, unchanged) ───
203
- getContext().logger.debug('Opening IndexedDB database');
205
+ this.runtime.logger.debug('Opening IndexedDB database');
204
206
  // Initialize meta database
205
207
  await this.databaseManager.initializeMetaDatabase();
206
208
  // Calculate database info
@@ -214,7 +216,7 @@ export class Database {
214
216
  // Initialize stores
215
217
  await this.storeManager.initializeStores(this.workspaceDb);
216
218
  const readiness = await this.storeManager.checkReadinessOfStores();
217
- getContext().logger.info(`Database opened: ${this.currentDbInfo.name} (${readiness.readyStores.length}/${readiness.totalStores} stores ready)`);
219
+ this.runtime.logger.info(`Database opened: ${this.currentDbInfo.name} (${readiness.readyStores.length}/${readiness.totalStores} stores ready)`);
218
220
  }
219
221
  /**
220
222
  * Shrink a record before persisting it. Drops `undefined` fields, empty
@@ -277,7 +279,7 @@ export class Database {
277
279
  */
278
280
  markRequiresFullBootstrap() {
279
281
  this._forceFullBootstrap = true;
280
- getContext().logger.info('[Database] Marked for forced full bootstrap (sync group change)');
282
+ this.runtime.logger.info('[Database] Marked for forced full bootstrap (sync group change)');
281
283
  }
282
284
  /**
283
285
  * Smart bootstrap requirements based on data freshness
@@ -309,7 +311,7 @@ export class Database {
309
311
  this._forceFullBootstrap = false;
310
312
  const instantModels = this.modelRegistry.getModelsByLoadStrategy(LoadStrategy.instant);
311
313
  const lazyModels = this.modelRegistry.getModelsByLoadStrategy(LoadStrategy.lazy);
312
- getContext().logger.info('[Database.requiredBootstrap] Forced FULL bootstrap (sync group change)');
314
+ this.runtime.logger.info('[Database.requiredBootstrap] Forced FULL bootstrap (sync group change)');
313
315
  return {
314
316
  type: 'full',
315
317
  modelsToLoad: [...instantModels, ...lazyModels],
@@ -348,7 +350,7 @@ export class Database {
348
350
  // we've confirmed the data it refers to actually exists in the stores.
349
351
  const lastSyncId = dataExists ? metadataLastSyncId : 0;
350
352
  // Log the resolved database state for diagnostics.
351
- getContext().logger.debug('[Database.requiredBootstrap] State check', {
353
+ this.runtime.logger.debug('[Database.requiredBootstrap] State check', {
352
354
  readinessReady: readiness.ready,
353
355
  hasMetadata: !!metadata,
354
356
  metadataLastSyncId,
@@ -365,13 +367,13 @@ export class Database {
365
367
  if (offline && hasLocalData) {
366
368
  // Offline with data - use local bootstrap (only option when offline)
367
369
  type = 'local';
368
- getContext().logger.info('Offline detected with local data - using local bootstrap');
370
+ this.runtime.logger.info('Offline detected with local data - using local bootstrap');
369
371
  }
370
372
  else {
371
373
  // The server is the source of truth: always use a full bootstrap
372
374
  // when online.
373
375
  type = 'full';
374
- getContext().logger.info('Full bootstrap - server is source of truth', {
376
+ this.runtime.logger.info('Full bootstrap - server is source of truth', {
375
377
  reason: offline ? 'offline_no_data' : 'server_authoritative',
376
378
  hasLocalData,
377
379
  lastSyncId,
@@ -396,12 +398,12 @@ export class Database {
396
398
  * shrinkage check. Caller supplies the complete list, not just
397
399
  * team-derived groups. */
398
400
  syncGroups, onProgress) {
399
- getContext().logger.debug('Starting bootstrap fetch', {
401
+ this.runtime.logger.debug('Starting bootstrap fetch', {
400
402
  type: requirements.type,
401
403
  lastSyncId: requirements.lastSyncId,
402
404
  modelsToLoad: requirements.modelsToLoad,
403
405
  });
404
- getContext().logger.info('Database: Starting bootstrap from Go server', {
406
+ this.runtime.logger.info('Database: Starting bootstrap from Go server', {
405
407
  type: requirements.type,
406
408
  syncGroups,
407
409
  modelsToLoad: requirements.modelsToLoad,
@@ -410,12 +412,12 @@ export class Database {
410
412
  // Fetch before any destructive operation, so a failed network
411
413
  // request can't leave the local store empty.
412
414
  const startTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
413
- getContext().logger.info('Fetching bootstrap data from server (before clearing local data)', {
415
+ this.runtime.logger.info('Fetching bootstrap data from server (before clearing local data)', {
414
416
  type: requirements.type,
415
417
  lastSyncId: requirements.lastSyncId,
416
418
  });
417
419
  const bootstrapData = await this.bootstrapHelper.fetchBootstrap(requirements.lastSyncId);
418
- getContext().logger.debug('Received bootstrap response', {
420
+ this.runtime.logger.debug('Received bootstrap response', {
419
421
  type: bootstrapData.type,
420
422
  lastSyncId: bootstrapData.lastSyncId,
421
423
  hasModels: !!bootstrapData.models,
@@ -431,7 +433,7 @@ export class Database {
431
433
  // Handle partial bootstrap (delta batch)
432
434
  if (bootstrapData.type === 'partial') {
433
435
  const deltas = bootstrapData.deltas ?? [];
434
- getContext().logger.info('Processing partial bootstrap with delta batch', {
436
+ this.runtime.logger.info('Processing partial bootstrap with delta batch', {
435
437
  deltaCount: deltas.length,
436
438
  fromSyncId: requirements.lastSyncId,
437
439
  toSyncId: bootstrapData.lastSyncId,
@@ -471,7 +473,7 @@ export class Database {
471
473
  updatedAt: new Date(),
472
474
  });
473
475
  const elapsed = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - startTime;
474
- getContext().logger.info(`Partial bootstrap complete in ${elapsed.toFixed(2)}ms`, {
476
+ this.runtime.logger.info(`Partial bootstrap complete in ${elapsed.toFixed(2)}ms`, {
475
477
  deltasApplied,
476
478
  lastSyncId: bootstrapData.lastSyncId,
477
479
  });
@@ -488,21 +490,21 @@ export class Database {
488
490
  for (const [modelName, modelData] of Object.entries(bootstrapData.models)) {
489
491
  // Handle null, undefined, or non-array data
490
492
  if (!modelData) {
491
- getContext().observability.breadcrumb(`No data received for ${modelName}`, 'sync.bootstrap', 'warning');
493
+ this.runtime.observability.breadcrumb(`No data received for ${modelName}`, 'sync.bootstrap', 'warning');
492
494
  continue;
493
495
  }
494
496
  if (!Array.isArray(modelData)) {
495
- getContext().observability.breadcrumb(`Skipping non-array data for ${modelName}`, 'sync.bootstrap', 'warning');
497
+ this.runtime.observability.breadcrumb(`Skipping non-array data for ${modelName}`, 'sync.bootstrap', 'warning');
496
498
  continue;
497
499
  }
498
500
  // Skip empty arrays silently (expected for some models)
499
501
  if (modelData.length === 0) {
500
- getContext().logger.debug(`No ${modelName} items to store (empty array)`);
502
+ this.runtime.logger.debug(`No ${modelName} items to store (empty array)`);
501
503
  continue;
502
504
  }
503
505
  const store = this.getStore(modelName, 'bootstrap');
504
506
  if (!store) {
505
- getContext().logger.debug(`[Bootstrap] NO IDB STORE for ${modelName} — ${modelData.length} items DROPPED`);
507
+ this.runtime.logger.debug(`[Bootstrap] NO IDB STORE for ${modelName} — ${modelData.length} items DROPPED`);
506
508
  continue;
507
509
  }
508
510
  let writeErrors = 0;
@@ -520,7 +522,7 @@ export class Database {
520
522
  }
521
523
  catch (error) {
522
524
  writeErrors++;
523
- getContext().observability.breadcrumb(`Failed to store ${modelName} item`, 'sync.database', 'error', {
525
+ this.runtime.observability.breadcrumb(`Failed to store ${modelName} item`, 'sync.database', 'error', {
524
526
  error: error instanceof Error ? error.message : String(error),
525
527
  });
526
528
  }
@@ -530,7 +532,7 @@ export class Database {
530
532
  // against. Counted and surfaced here so a partial does not read as a
531
533
  // clean bootstrap.
532
534
  if (writeErrors > 0) {
533
- getContext().observability.breadcrumb(`Stored ${modelName} with ${writeErrors} of ${modelData.length} items dropped`, 'sync.database', 'warning');
535
+ this.runtime.observability.breadcrumb(`Stored ${modelName} with ${writeErrors} of ${modelData.length} items dropped`, 'sync.database', 'warning');
534
536
  }
535
537
  // Mark model as persisted after successful write
536
538
  try {
@@ -548,8 +550,8 @@ export class Database {
548
550
  updatedAt: new Date(),
549
551
  });
550
552
  const elapsed = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - startTime;
551
- getContext().logger.info(`Bootstrap complete: ${modelsLoaded} items loaded, ${modelsStored} stored to IndexedDB in ${elapsed.toFixed(2)}ms`);
552
- getContext().analytics?.capture('bootstrap_success', {
553
+ this.runtime.logger.info(`Bootstrap complete: ${modelsLoaded} items loaded, ${modelsStored} stored to IndexedDB in ${elapsed.toFixed(2)}ms`);
554
+ this.runtime.analytics?.capture('bootstrap_success', {
553
555
  responseTime: elapsed,
554
556
  modelsLoaded,
555
557
  });
@@ -557,12 +559,12 @@ export class Database {
557
559
  }
558
560
  catch (error) {
559
561
  // Comprehensive error logging for bootstrap failures
560
- getContext().observability.captureBootstrapFailure(error, {
562
+ this.runtime.observability.captureBootstrapFailure(error, {
561
563
  type: requirements.type,
562
564
  navigatorOnline: typeof navigator !== 'undefined' ? navigator.onLine : undefined,
563
565
  });
564
566
  // Track bootstrap failure telemetry
565
- getContext().analytics?.capture('bootstrap_failed', {
567
+ this.runtime.analytics?.capture('bootstrap_failed', {
566
568
  bootstrapType: requirements.type,
567
569
  lastSyncId: requirements.lastSyncId,
568
570
  errorMessage: error instanceof Error ? error.message : String(error),
@@ -617,7 +619,7 @@ export class Database {
617
619
  // newly created, it was newly visible. We fall through to the 'I' case
618
620
  // after a debug trace so the two can be disambiguated in logs.
619
621
  case 'C':
620
- getContext().observability.breadcrumb('Applying covering delta (gained permission)', 'sync.database', 'info', { modelName, modelId: modelId.slice(0, 12) });
622
+ this.runtime.observability.breadcrumb('Applying covering delta (gained permission)', 'sync.database', 'info', { modelName, modelId: modelId.slice(0, 12) });
621
623
  // falls through
622
624
  case 'I': {
623
625
  // Skip when the delta payload was empty/null. IDB rejects
@@ -635,7 +637,7 @@ export class Database {
635
637
  }
636
638
  }
637
639
  catch (err) {
638
- getContext().observability.breadcrumb(`IndexedDB put failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
640
+ this.runtime.observability.breadcrumb(`IndexedDB put failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
639
641
  error: err instanceof Error ? err.message : String(err),
640
642
  });
641
643
  throw err; // Re-throw to see the actual error
@@ -650,7 +652,7 @@ export class Database {
650
652
  // building a record from partial update data would corrupt it
651
653
  // (missing reportId, and so on).
652
654
  if (!existing) {
653
- getContext().observability.breadcrumb('Skipping UPDATE delta - no existing record to merge with', 'sync.database', 'warning', {
655
+ this.runtime.observability.breadcrumb('Skipping UPDATE delta - no existing record to merge with', 'sync.database', 'warning', {
654
656
  modelName,
655
657
  modelId: modelId.slice(0, 12),
656
658
  });
@@ -671,7 +673,7 @@ export class Database {
671
673
  }
672
674
  }
673
675
  catch (err) {
674
- getContext().observability.breadcrumb(`IndexedDB put failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
676
+ this.runtime.observability.breadcrumb(`IndexedDB put failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
675
677
  error: err instanceof Error ? err.message : String(err),
676
678
  });
677
679
  throw err;
@@ -688,7 +690,7 @@ export class Database {
688
690
  }
689
691
  }
690
692
  catch (err) {
691
- getContext().observability.breadcrumb(`IndexedDB delete failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
693
+ this.runtime.observability.breadcrumb(`IndexedDB delete failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
692
694
  error: err instanceof Error ? err.message : String(err),
693
695
  });
694
696
  // Surface failure so caller does not mutate InstanceCache inconsistently
@@ -706,7 +708,7 @@ export class Database {
706
708
  }
707
709
  }
708
710
  catch (err) {
709
- getContext().observability.breadcrumb(`IndexedDB archive put failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
711
+ this.runtime.observability.breadcrumb(`IndexedDB archive put failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
710
712
  error: err instanceof Error ? err.message : String(err),
711
713
  });
712
714
  throw err;
@@ -722,7 +724,7 @@ export class Database {
722
724
  // queue), we return a no-op verify rather than crashing the engine.
723
725
  case 'G':
724
726
  case 'S':
725
- getContext().observability.breadcrumb(`Group membership delta (${actionType}) reached processDelta — should be handled upstream`, 'sync.database', 'warning', { modelName, modelId: modelId.slice(0, 12), actionType });
727
+ this.runtime.observability.breadcrumb(`Group membership delta (${actionType}) reached processDelta — should be handled upstream`, 'sync.database', 'warning', { modelName, modelId: modelId.slice(0, 12), actionType });
726
728
  return { action: 'verify', modelName, modelId, data: null };
727
729
  default: {
728
730
  // The switch above is exhaustive over the declared action types, so
@@ -808,7 +810,7 @@ export class Database {
808
810
  }
809
811
  }
810
812
  if (deleteSyncIds.size > 0) {
811
- getContext().logger.debug('[Database.processDeltaBatch] Built DELETE index for conflict resolution', {
813
+ this.runtime.logger.debug('[Database.processDeltaBatch] Built DELETE index for conflict resolution', {
812
814
  deleteCount: deleteSyncIds.size,
813
815
  totalDeltas: deltas.length,
814
816
  });
@@ -850,7 +852,7 @@ export class Database {
850
852
  const deltaSyncId = delta.syncId ?? 0;
851
853
  if (deleteSyncId >= deltaSyncId) {
852
854
  // DELETE has equal or higher syncId - skip this UPDATE/INSERT
853
- getContext().logger.debug('[Database.processDeltaBatch] Skipping stale delta (DELETE wins)', {
855
+ this.runtime.logger.debug('[Database.processDeltaBatch] Skipping stale delta (DELETE wins)', {
854
856
  modelName: delta.modelName,
855
857
  modelId: delta.modelId.slice(0, 12),
856
858
  actionType: delta.actionType,
@@ -877,7 +879,7 @@ export class Database {
877
879
  deltasByStore.get(delta.modelName).push({ idx, delta });
878
880
  });
879
881
  if (skippedDueToConflict > 0) {
880
- getContext().logger.info('[Database.processDeltaBatch] Conflict resolution summary', {
882
+ this.runtime.logger.info('[Database.processDeltaBatch] Conflict resolution summary', {
881
883
  skippedDueToConflict,
882
884
  totalDeltas: deltas.length,
883
885
  deleteCount: deleteSyncIds.size,
@@ -908,7 +910,7 @@ export class Database {
908
910
  }
909
911
  }
910
912
  catch {
911
- getContext().observability.breadcrumb(`Batch read failed for ${modelName}, falling back to individual reads`, 'sync.database', 'warning');
913
+ this.runtime.observability.breadcrumb(`Batch read failed for ${modelName}, falling back to individual reads`, 'sync.database', 'warning');
912
914
  // Fallback: mark all as missing for self-healing
913
915
  for (const id of updateIds) {
914
916
  missingIds.add(id);
@@ -920,7 +922,7 @@ export class Database {
920
922
  // so its delta is skipped).
921
923
  const failedToFetch = new Set();
922
924
  if (missingIds.size > 0) {
923
- getContext().logger.info(`[Database.processDeltaBatch] Found ${missingIds.size} missing records for ${modelName}, fetching from server...`);
925
+ this.runtime.logger.info(`[Database.processDeltaBatch] Found ${missingIds.size} missing records for ${modelName}, fetching from server...`);
924
926
  // Fetch sequentially to avoid overwhelming server
925
927
  for (const id of missingIds) {
926
928
  try {
@@ -928,24 +930,24 @@ export class Database {
928
930
  if (fetchedRecord) {
929
931
  const compacted = this.compactRecord(modelName, fetchedRecord);
930
932
  existingRecords.set(id, compacted);
931
- getContext().logger.debug(`[Database.processDeltaBatch] Successfully fetched missing record: ${modelName}:${id}`);
933
+ this.runtime.logger.debug(`[Database.processDeltaBatch] Successfully fetched missing record: ${modelName}:${id}`);
932
934
  }
933
935
  else {
934
936
  // fetchEntity returns null for 404 — entity was deleted, skip the delta
935
937
  failedToFetch.add(id);
936
- getContext().logger.debug(`[Database.processDeltaBatch] Entity not found (deleted): ${modelName}:${id}`);
938
+ this.runtime.logger.debug(`[Database.processDeltaBatch] Entity not found (deleted): ${modelName}:${id}`);
937
939
  }
938
940
  }
939
941
  catch (error) {
940
942
  // Unexpected error (5xx, network failure) — mark for skipping and report
941
943
  failedToFetch.add(id);
942
- getContext().observability.breadcrumb(`Failed to fetch missing record ${modelName}:${id}`, 'sync.database', 'warning', {
944
+ this.runtime.observability.breadcrumb(`Failed to fetch missing record ${modelName}:${id}`, 'sync.database', 'warning', {
943
945
  error: error instanceof Error ? error.message : String(error),
944
946
  });
945
947
  }
946
948
  }
947
949
  if (failedToFetch.size > 0) {
948
- getContext().logger.info(`[Database.processDeltaBatch] Skipping ${failedToFetch.size} stale UPDATE deltas for deleted entities`, {
950
+ this.runtime.logger.info(`[Database.processDeltaBatch] Skipping ${failedToFetch.size} stale UPDATE deltas for deleted entities`, {
949
951
  modelName,
950
952
  failedCount: failedToFetch.size,
951
953
  totalMissing: missingIds.size,
@@ -997,7 +999,7 @@ export class Database {
997
999
  // store nor fetchable from the server (a 404), it was deleted,
998
1000
  // so skip it rather than create an incomplete record.
999
1001
  if (!existing && failedToFetch.has(modelId)) {
1000
- getContext().logger.debug('[Database.processDeltaBatch] Skipping UPDATE for deleted entity', {
1002
+ this.runtime.logger.debug('[Database.processDeltaBatch] Skipping UPDATE for deleted entity', {
1001
1003
  modelName,
1002
1004
  modelId: modelId.slice(0, 12),
1003
1005
  });
@@ -1008,7 +1010,7 @@ export class Database {
1008
1010
  // building a record from partial update data would corrupt it
1009
1011
  // (missing reportId, and so on).
1010
1012
  if (!existing) {
1011
- getContext().observability.breadcrumb('Batch: Skipping UPDATE delta - no existing record', 'sync.database', 'warning', {
1013
+ this.runtime.observability.breadcrumb('Batch: Skipping UPDATE delta - no existing record', 'sync.database', 'warning', {
1012
1014
  modelName,
1013
1015
  modelId: modelId.slice(0, 12),
1014
1016
  });
@@ -1084,7 +1086,7 @@ export class Database {
1084
1086
  // `DataError`, `AbortError`) so we can find what's wrong with
1085
1087
  // the `compacted` payload shape or store schema.
1086
1088
  const idbErr = err instanceof Error ? err : new Error(String(err));
1087
- getContext().logger.debug('[Database.processDeltaBatch] store tx FAILED', {
1089
+ this.runtime.logger.debug('[Database.processDeltaBatch] store tx FAILED', {
1088
1090
  modelName,
1089
1091
  storeDeltasCount: storeDeltas.length,
1090
1092
  errorName: idbErr.name,
@@ -1097,7 +1099,7 @@ export class Database {
1097
1099
  : typeof delta.data,
1098
1100
  })),
1099
1101
  });
1100
- getContext().observability.captureMutationFailure({
1102
+ this.runtime.observability.captureMutationFailure({
1101
1103
  context: 'batch-indexeddb-operation',
1102
1104
  modelName,
1103
1105
  error: idbErr,
@@ -1123,7 +1125,7 @@ export class Database {
1123
1125
  await this.updateWorkspaceMetadata({ lastSyncId: highestPersistedSyncId });
1124
1126
  }
1125
1127
  catch (err) {
1126
- getContext().observability.breadcrumb('Failed to update metadata after batch', 'sync.database', 'error', {
1128
+ this.runtime.observability.breadcrumb('Failed to update metadata after batch', 'sync.database', 'error', {
1127
1129
  error: err instanceof Error ? err.message : String(err),
1128
1130
  });
1129
1131
  }
@@ -1133,7 +1135,7 @@ export class Database {
1133
1135
  // persisted" signal loud when it actually happens. If this fires
1134
1136
  // repeatedly on the same sync IDs, a specific row is un-writable
1135
1137
  // (validation? compact issue?) and needs fixing at that layer.
1136
- getContext().logger.debug('[Database.processDeltaBatch] cursor withheld due to failed store tx', {
1138
+ this.runtime.logger.debug('[Database.processDeltaBatch] cursor withheld due to failed store tx', {
1137
1139
  seen: highestSyncId,
1138
1140
  persisted: highestPersistedSyncId,
1139
1141
  gap: highestSyncId - highestPersistedSyncId,
@@ -1153,7 +1155,7 @@ export class Database {
1153
1155
  async putRecord(modelName, id, data) {
1154
1156
  const store = this.getStore(modelName, 'putRecord');
1155
1157
  if (!store) {
1156
- getContext().observability.breadcrumb(`Store not found for putRecord: ${modelName}`, 'sync.database', 'warning');
1158
+ this.runtime.observability.breadcrumb(`Store not found for putRecord: ${modelName}`, 'sync.database', 'warning');
1157
1159
  return;
1158
1160
  }
1159
1161
  const compacted = this.compactRecord(modelName, data);
@@ -1196,7 +1198,7 @@ export class Database {
1196
1198
  // Graceful degradation: skip if database is closing or not open
1197
1199
  // This prevents "Database not opened" errors during React Strict Mode cleanup
1198
1200
  if (!this.workspaceDb || this.isClosing) {
1199
- getContext().observability.breadcrumb('updateWorkspaceMetadata: Database not open or closing', 'sync.database', 'warning', {
1201
+ this.runtime.observability.breadcrumb('updateWorkspaceMetadata: Database not open or closing', 'sync.database', 'warning', {
1200
1202
  hasDb: !!this.workspaceDb,
1201
1203
  isClosing: this.isClosing,
1202
1204
  });
@@ -1488,13 +1490,13 @@ export class Database {
1488
1490
  }
1489
1491
  await this.databaseManager.close();
1490
1492
  this.currentDbInfo = null;
1491
- getContext().logger.debug('Database closed');
1493
+ this.runtime.logger.debug('Database closed');
1492
1494
  }
1493
1495
  async clear(options = {}) {
1494
1496
  await this.storeManager.clearAllStores();
1495
1497
  if (options.includeWriteJournal) {
1496
1498
  await this.transactionStore.clear();
1497
1499
  }
1498
- getContext().logger.info('All stores cleared');
1500
+ this.runtime.logger.info('All stores cleared');
1499
1501
  }
1500
1502
  }
@@ -8,9 +8,10 @@
8
8
  */
9
9
  import { Model } from './Model.js';
10
10
  import { ModelRegistry } from './ModelRegistry.js';
11
+ import type { RuntimeContext } from './RuntimeContext.js';
11
12
  import { ModelScope } from './transaction/types/index.js';
12
- import { ViewRegistry } from './core/ViewRegistry.js';
13
- import { QueryView, type QueryViewOptions } from './core/QueryView.js';
13
+ import { ViewRegistry } from './views/ViewRegistry.js';
14
+ import { QueryView, type QueryViewOptions } from './views/QueryView.js';
14
15
  /** Constructor type for Model subclasses - uses abstract to handle variance */
15
16
  type ModelConstructor<T extends Model> = abstract new (...args: never[]) => T;
16
17
  export { ModelScope };
@@ -19,6 +20,8 @@ interface PoolConfig {
19
20
  maxAge?: number;
20
21
  gcInterval?: number;
21
22
  useWeakRefs?: boolean;
23
+ /** The owning client's runtime. Defaults to the module-global bridge. */
24
+ runtime?: RuntimeContext;
22
25
  }
23
26
  interface DeltaInfo {
24
27
  action?: string;
@@ -38,6 +41,7 @@ export declare class InstanceCache {
38
41
  private foreignKeyConfig;
39
42
  private metrics;
40
43
  private config;
44
+ private readonly runtime;
41
45
  private gcTimer?;
42
46
  readonly registry: ModelRegistry;
43
47
  readonly viewRegistry: ViewRegistry;
@@ -9,11 +9,11 @@
9
9
  import { makeObservable, observable, action, computed, runInAction } from 'mobx';
10
10
  import { Model } from './Model.js';
11
11
  import { ModelRegistry } from './ModelRegistry.js';
12
- import { getContext } from './context.js';
12
+ import { globalRuntime } from './context.js';
13
13
  import { AbloValidationError } from './transaction/errors.js';
14
14
  import { ModelScope } from './transaction/types/index.js';
15
- import { ViewRegistry } from './core/ViewRegistry.js';
16
- import { QueryView } from './core/QueryView.js';
15
+ import { ViewRegistry } from './views/ViewRegistry.js';
16
+ import { QueryView } from './views/QueryView.js';
17
17
  // Re-exported so `import { ModelScope } from './InstanceCache.js'` resolves
18
18
  export { ModelScope };
19
19
  /**
@@ -51,6 +51,7 @@ export class InstanceCache {
51
51
  };
52
52
  // Configuration
53
53
  config;
54
+ runtime;
54
55
  gcTimer;
55
56
  // ModelRegistry instance — single source of truth for model metadata
56
57
  registry;
@@ -106,6 +107,7 @@ export class InstanceCache {
106
107
  gcInterval: config.gcInterval ?? 60000, // 1 minute
107
108
  useWeakRefs: config.useWeakRefs ?? true,
108
109
  };
110
+ this.runtime = config.runtime ?? globalRuntime;
109
111
  // Store the model registry reference
110
112
  if (!modelRegistry) {
111
113
  throw new AbloValidationError('InstanceCache requires ModelRegistry for production-safe model name lookup', { code: 'pool_registry_missing' });
@@ -264,7 +266,7 @@ export class InstanceCache {
264
266
  (history.lastAction === 'U' || history.lastAction === 'D')) {
265
267
  // Internal delta-ordering anomaly that reconciles on the next
266
268
  // catch-up — forensic, not consumer-actionable → debug.
267
- getContext().logger.debug(`InstanceCache.add() SUSPICIOUS: INSERT after ${history.lastAction}`, { modelType, id, syncId: deltaInfo.syncId });
269
+ this.runtime.logger.debug(`InstanceCache.add() SUSPICIOUS: INSERT after ${history.lastAction}`, { modelType, id, syncId: deltaInfo.syncId });
268
270
  }
269
271
  }
270
272
  // Update delta history
@@ -539,7 +541,7 @@ export class InstanceCache {
539
541
  // This is expected during migration from hand-coded → dynamic models.
540
542
  }
541
543
  catch (e) {
542
- getContext().observability.breadcrumb(`Failed to create fallback instance for ${modelClass.name}`, 'sync.database', 'error', {
544
+ this.runtime.observability.breadcrumb(`Failed to create fallback instance for ${modelClass.name}`, 'sync.database', 'error', {
543
545
  error: e instanceof Error ? e.message : String(e),
544
546
  });
545
547
  return [];
@@ -647,8 +649,8 @@ export class InstanceCache {
647
649
  if (modelName === 'Unknown') {
648
650
  // Malformed row with no type marker — dropped, but nothing the consumer
649
651
  // 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
+ this.runtime.logger.debug('InstanceCache.createFromData: No model identifier found', { data });
653
+ this.runtime.modelDebugLogger?.logError('Unknown', 'CREATE', 'No model identifier found', data);
652
654
  return null;
653
655
  }
654
656
  if (opts?.strict) {
@@ -661,9 +663,9 @@ export class InstanceCache {
661
663
  // Genuinely actionable and NOT self-healing: a model the server is sending
662
664
  // isn't in your schema, so these rows are silently skipped. Keep at warn,
663
665
  // 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);
666
+ this.runtime.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.`);
667
+ this.runtime.logger.debug(`InstanceCache.createFromData: No constructor found for model "${modelName}"`, { data });
668
+ this.runtime.modelDebugLogger?.logError(modelName, 'CREATE', `No constructor found for model "${modelName}"`, data);
667
669
  return null;
668
670
  }
669
671
  // If the model already exists, update it in place instead of creating a
@@ -680,7 +682,7 @@ export class InstanceCache {
680
682
  // Don't return existing, create new model (will use composite key for storage)
681
683
  }
682
684
  // Log model creation attempt
683
- getContext().modelDebugLogger?.logCreation(modelName, data, Constructor);
685
+ this.runtime.modelDebugLogger?.logCreation(modelName, data, Constructor);
684
686
  try {
685
687
  // Pass data directly to constructor for Prisma-first models
686
688
  const model = new Constructor(data);
@@ -690,14 +692,14 @@ export class InstanceCache {
690
692
  const errorMessage = error instanceof Error ? error.message : String(error);
691
693
  // Internal construction failure — captured via observability below and
692
694
  // 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
+ this.runtime.logger.debug(`[InstanceCache.createFromData] FAILED ${modelName}`, { errorMessage, stack: error instanceof Error ? error.stack : undefined });
696
+ this.runtime.observability.captureMutationFailure({
695
697
  context: 'createFromData',
696
698
  modelName,
697
699
  modelId: data.id,
698
700
  error: errorMessage,
699
701
  });
700
- getContext().modelDebugLogger?.logError(modelName, 'CREATE', errorMessage, {
702
+ this.runtime.modelDebugLogger?.logError(modelName, 'CREATE', errorMessage, {
701
703
  data,
702
704
  constructor: Constructor.name,
703
705
  });
@@ -904,7 +906,7 @@ export class InstanceCache {
904
906
  }
905
907
  }
906
908
  if (skippedObserved > 0) {
907
- getContext().logger.debug(`[InstanceCache GC] Skipped ${skippedObserved} models with active React observers`);
909
+ this.runtime.logger.debug(`[InstanceCache GC] Skipped ${skippedObserved} models with active React observers`);
908
910
  }
909
911
  // Also clean up old tracking data
910
912
  this.clearDeltaHistory();
@@ -1046,7 +1048,7 @@ export class InstanceCache {
1046
1048
  // has dangling refs (legacy orphan deltas, pending CREATE
1047
1049
  // transactions, etc.). Noisy at warn level, useful during
1048
1050
  // investigation.
1049
- getContext().logger.debug('[InstanceCache.getByForeignKey] ROWS DROPPED', {
1051
+ this.runtime.logger.debug('[InstanceCache.getByForeignKey] ROWS DROPPED', {
1050
1052
  modelName,
1051
1053
  fieldName,
1052
1054
  fieldValue,