@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
@@ -14,7 +14,7 @@ import { Model } from './Model.js';
14
14
  import { snapshotJsonValue } from './transaction/utils/json.js';
15
15
  // ModelRegistry instance accessed via this.objectPool.registry
16
16
  import { LoadStrategy } from './transaction/types/index.js';
17
- import { getContext } from './context.js';
17
+ import { globalRuntime } from './context.js';
18
18
  import { AbloAuthenticationError, AbloError, AbloValidationError } from './transaction/errors.js';
19
19
  import { EventEmitter } from 'events';
20
20
  import { NetworkMonitor } from './NetworkMonitor.js';
@@ -67,9 +67,10 @@ function toEpochMs(value) {
67
67
  return 0;
68
68
  }
69
69
  export class SyncClient extends EventEmitter {
70
+ runtime;
70
71
  objectPool;
71
72
  database;
72
- get mutationExecutor() { return getContext().mutationExecutor; }
73
+ get mutationExecutor() { return this.runtime.mutationExecutor; }
73
74
  networkMonitor;
74
75
  /**
75
76
  * @internal — test seam, stripped from the published declarations by
@@ -111,15 +112,17 @@ export class SyncClient extends EventEmitter {
111
112
  * responses, and snapshots and claims read `readFloor`.
112
113
  */
113
114
  position = new LogPosition();
114
- constructor(objectPool, database, commitOutbox = new DatabaseCommitOutboxStore(database), commitOutboxNamespace = 'default') {
115
+ constructor(objectPool, database, commitOutbox = new DatabaseCommitOutboxStore(database), commitOutboxNamespace = 'default', runtime = globalRuntime) {
115
116
  super();
117
+ this.runtime = runtime;
116
118
  this.objectPool = objectPool;
117
119
  this.database = database;
118
120
  this.commitOutboxNamespace = commitOutboxNamespace;
119
- this.networkMonitor = new NetworkMonitor();
121
+ this.networkMonitor = new NetworkMonitor(this.runtime);
120
122
  // Initialize MutationQueue with proper configuration
121
123
  this.mutationQueue = new MutationQueue({
122
124
  position: this.position,
125
+ runtime: this.runtime,
123
126
  maxBatchSize: 50, // Larger batches keep the batch count low for bulk operations
124
127
  // A short delay keeps writes responsive; coalescing still groups them
125
128
  batchDelay: 150,
@@ -203,7 +206,7 @@ export class SyncClient extends EventEmitter {
203
206
  // losing a failed reconnect flush silently.
204
207
  this.networkMonitor.on('online', () => {
205
208
  void this.handleReconnection().catch((error) => {
206
- getContext().observability.captureMutationFailure({
209
+ this.runtime.observability.captureMutationFailure({
207
210
  context: 'network-online-reconnection',
208
211
  error: error instanceof Error ? error : new Error(String(error)),
209
212
  });
@@ -211,7 +214,7 @@ export class SyncClient extends EventEmitter {
211
214
  });
212
215
  this.networkMonitor.on('offline', () => {
213
216
  void this.handleDisconnection().catch((error) => {
214
- getContext().observability.captureMutationFailure({
217
+ this.runtime.observability.captureMutationFailure({
215
218
  context: 'network-offline-handler',
216
219
  error: error instanceof Error ? error : new Error(String(error)),
217
220
  });
@@ -261,7 +264,7 @@ export class SyncClient extends EventEmitter {
261
264
  // rejected write — keep it at `debug` so the rollback mechanics are
262
265
  // available when debugging but don't double the console noise.
263
266
  const abloErr = error instanceof AbloError ? error : undefined;
264
- getContext().logger.debug('[SyncClient.rollback]', {
267
+ this.runtime.logger.debug('[SyncClient.rollback]', {
265
268
  txType: transaction.type,
266
269
  modelName: transaction.modelName,
267
270
  modelId: transaction.modelId.slice(0, 12),
@@ -272,7 +275,7 @@ export class SyncClient extends EventEmitter {
272
275
  requestId: abloErr?.requestId,
273
276
  message: error?.message,
274
277
  });
275
- getContext().observability.captureRollback({
278
+ this.runtime.observability.captureRollback({
276
279
  transactionType: transaction.type,
277
280
  modelName: transaction.modelName,
278
281
  modelId: transaction.modelId,
@@ -291,7 +294,7 @@ export class SyncClient extends EventEmitter {
291
294
  // DELETE "not found" rollback: the entity doesn't exist on the server.
292
295
  // Instead of restoring a ghost entity, remove it locally too.
293
296
  // Both sides agree: this entity should not exist.
294
- getContext().observability.breadcrumb('DELETE rolled back with "not found" - removing ghost entity', 'sync.conflict', 'info', {
297
+ this.runtime.observability.breadcrumb('DELETE rolled back with "not found" - removing ghost entity', 'sync.conflict', 'info', {
295
298
  modelId: transaction.modelId,
296
299
  modelName: transaction.modelName,
297
300
  });
@@ -309,7 +312,7 @@ export class SyncClient extends EventEmitter {
309
312
  // design (can't revive the private isDisposed flag), so keep this
310
313
  // at `debug` instead of emitting a second `warn` that reads as a
311
314
  // distinct failure in the console.
312
- getContext().logger.debug('[SyncClient] Rollback skipped restore (model already disposed)', {
315
+ this.runtime.logger.debug('[SyncClient] Rollback skipped restore (model already disposed)', {
313
316
  modelId: transaction.modelId,
314
317
  modelName: transaction.modelName,
315
318
  reason,
@@ -336,7 +339,7 @@ export class SyncClient extends EventEmitter {
336
339
  });
337
340
  }
338
341
  catch (error) {
339
- getContext().observability.captureMutationFailure({
342
+ this.runtime.observability.captureMutationFailure({
340
343
  context: 'rollback-failed',
341
344
  transactionId: transaction.id,
342
345
  modelName: transaction.modelName,
@@ -354,7 +357,7 @@ export class SyncClient extends EventEmitter {
354
357
  */
355
358
  setupReconciliationForwarding() {
356
359
  this.mutationQueue.on('reconciliation:needed', (event) => {
357
- getContext().observability.captureReconciliation({
360
+ this.runtime.observability.captureReconciliation({
358
361
  reason: event.reason,
359
362
  model: event.model,
360
363
  modelId: event.modelId,
@@ -415,14 +418,14 @@ export class SyncClient extends EventEmitter {
415
418
  operationType: event.operationType,
416
419
  },
417
420
  });
418
- getContext().observability.breadcrumb('Persisted unconfirmed transaction to IDB', 'sync.transaction', 'info', {
421
+ this.runtime.observability.breadcrumb('Persisted unconfirmed transaction to IDB', 'sync.transaction', 'info', {
419
422
  txId: event.txId,
420
423
  model: event.model,
421
424
  modelId: event.modelId,
422
425
  });
423
426
  }
424
427
  catch (error) {
425
- getContext().observability.captureMutationFailure({
428
+ this.runtime.observability.captureMutationFailure({
426
429
  context: 'persist-awaiting-transaction',
427
430
  modelName: event.model,
428
431
  modelId: event.modelId,
@@ -447,7 +450,7 @@ export class SyncClient extends EventEmitter {
447
450
  async initialize(userId, organizationId) {
448
451
  this.userId = userId;
449
452
  this.organizationId = organizationId;
450
- getContext().observability.setContext(userId, organizationId);
453
+ this.runtime.observability.setContext(userId, organizationId);
451
454
  this.mutationQueue.setCommitOutboxScope({
452
455
  organizationId,
453
456
  participantId: userId,
@@ -470,7 +473,7 @@ export class SyncClient extends EventEmitter {
470
473
  // reports online by default. NetworkMonitor drives the ongoing
471
474
  // online/offline transitions below — this read is only the initial
472
475
  // snapshot taken when identity is set.
473
- if (getContext().onlineStatus.isOnline()) {
476
+ if (this.runtime.onlineStatus.isOnline()) {
474
477
  this.setConnectionState('connected');
475
478
  }
476
479
  else {
@@ -523,7 +526,7 @@ export class SyncClient extends EventEmitter {
523
526
  const replacement = rule.from === 'organizationId' ? this.organizationId : this.userId;
524
527
  if (!replacement)
525
528
  continue;
526
- getContext().observability.captureSelfHealing({
529
+ this.runtime.observability.captureSelfHealing({
527
530
  modelName: modelType,
528
531
  modelId: idPrefix,
529
532
  field: rule.field,
@@ -537,7 +540,7 @@ export class SyncClient extends EventEmitter {
537
540
  for (const field of meta.requiredFields) {
538
541
  if (result[field])
539
542
  continue;
540
- getContext().observability.captureSelfHealing({
543
+ this.runtime.observability.captureSelfHealing({
541
544
  modelName: modelType,
542
545
  modelId: idPrefix,
543
546
  field,
@@ -600,7 +603,7 @@ export class SyncClient extends EventEmitter {
600
603
  allModelsToAdd.push(...modelsForType);
601
604
  // Persist healed records back to IndexedDB (fire-and-forget, non-blocking)
602
605
  if (recordsToHeal.length > 0 && this.database) {
603
- getContext().logger.info(`[SyncClient.hydrate] Persisting ${recordsToHeal.length} healed ${modelType} records to IndexedDB`);
606
+ this.runtime.logger.info(`[SyncClient.hydrate] Persisting ${recordsToHeal.length} healed ${modelType} records to IndexedDB`);
604
607
  // Use fire-and-forget to not block hydration.
605
608
  // void is safe: the handler's body is fully try/catch'd.
606
609
  void Promise.resolve().then(async () => {
@@ -608,10 +611,10 @@ export class SyncClient extends EventEmitter {
608
611
  for (const { id, data } of recordsToHeal) {
609
612
  await this.database.putRecord(modelType, id, data);
610
613
  }
611
- getContext().logger.info(`[SyncClient.hydrate] Successfully healed ${recordsToHeal.length} ${modelType} records`);
614
+ this.runtime.logger.info(`[SyncClient.hydrate] Successfully healed ${recordsToHeal.length} ${modelType} records`);
612
615
  }
613
616
  catch (err) {
614
- getContext().observability.captureMutationFailure({
617
+ this.runtime.observability.captureMutationFailure({
615
618
  context: 'persist-healed-records',
616
619
  modelName: modelType,
617
620
  error: err instanceof Error ? err : new Error(String(err)),
@@ -629,7 +632,7 @@ export class SyncClient extends EventEmitter {
629
632
  });
630
633
  }
631
634
  catch (error) {
632
- getContext().observability.captureBootstrapFailure(error, { type: `hydrate-${modelType}` });
635
+ this.runtime.observability.captureBootstrapFailure(error, { type: `hydrate-${modelType}` });
633
636
  }
634
637
  }
635
638
  // Phase 2: Single MobX action — add ALL models across all types at once.
@@ -638,7 +641,7 @@ export class SyncClient extends EventEmitter {
638
641
  const addEnd = typeof performance !== 'undefined' ? performance.now() : Date.now();
639
642
  // Log per-type perf after the batched add (so logs still show per-type breakdown)
640
643
  for (const entry of perTypePerfLogs) {
641
- getContext().logger.debug('hydrate:type', parseFloat(entry.fetchMs) + parseFloat(entry.createMs), {
644
+ this.runtime.logger.debug('hydrate:type', parseFloat(entry.fetchMs) + parseFloat(entry.createMs), {
642
645
  type: entry.type,
643
646
  fetched: entry.fetched,
644
647
  added: entry.added,
@@ -647,7 +650,7 @@ export class SyncClient extends EventEmitter {
647
650
  });
648
651
  }
649
652
  const totalEnd = typeof performance !== 'undefined' ? performance.now() : Date.now();
650
- getContext().logger.debug('hydrate:total', totalEnd - totalStart, {
653
+ this.runtime.logger.debug('hydrate:total', totalEnd - totalStart, {
651
654
  totalModels: totalAdded,
652
655
  addBatchMs: (addEnd - addStart).toFixed(2),
653
656
  });
@@ -655,7 +658,7 @@ export class SyncClient extends EventEmitter {
655
658
  try {
656
659
  const preseededTypes = this.objectPool.registry.getRegisteredModelNames();
657
660
  const stats = this.objectPool.getStats();
658
- getContext().logger.info('startup_summary', {
661
+ this.runtime.logger.info('startup_summary', {
659
662
  typesPreseeded: preseededTypes.length,
660
663
  poolSize: stats.size,
661
664
  typeCounts: stats.typeCounts,
@@ -736,7 +739,7 @@ export class SyncClient extends EventEmitter {
736
739
  }
737
740
  }
738
741
  catch (error) {
739
- getContext().observability.breadcrumb('Model creation failed during rehydration', 'sync.bootstrap', 'warning', {
742
+ this.runtime.observability.breadcrumb('Model creation failed during rehydration', 'sync.bootstrap', 'warning', {
740
743
  modelType,
741
744
  modelId: recordId?.slice(0, 8) ?? 'unknown',
742
745
  error: error instanceof Error ? error.message : String(error),
@@ -746,7 +749,7 @@ export class SyncClient extends EventEmitter {
746
749
  }
747
750
  }
748
751
  catch (error) {
749
- getContext().observability.captureBootstrapFailure(error, { type: `rehydrate-${modelType}` });
752
+ this.runtime.observability.captureBootstrapFailure(error, { type: `rehydrate-${modelType}` });
750
753
  }
751
754
  }
752
755
  // ── Phase 2: Upsert batch (single MobX action) ──
@@ -785,12 +788,12 @@ export class SyncClient extends EventEmitter {
785
788
  healed: healedCount,
786
789
  elapsedMs,
787
790
  };
788
- getContext().logger.info('[SyncClient.rehydrate] Complete', {
791
+ this.runtime.logger.info('[SyncClient.rehydrate] Complete', {
789
792
  ...stats,
790
793
  poolSize: this.objectPool.size,
791
794
  ghostIds: ghostIds.length > 0 ? ghostIds.slice(0, 5).map((id) => id.slice(0, 8)) : [],
792
795
  });
793
- getContext().observability.breadcrumb('Rehydration complete', 'sync.bootstrap', 'info', {
796
+ this.runtime.observability.breadcrumb('Rehydration complete', 'sync.bootstrap', 'info', {
794
797
  added: stats.added,
795
798
  updated: stats.updated,
796
799
  removed: stats.removed,
@@ -882,7 +885,7 @@ export class SyncClient extends EventEmitter {
882
885
  * (e.g. freshly constructed cell models from a bulk document decomposition).
883
886
  */
884
887
  updateWithChanges(model, changes) {
885
- getContext().logger.debug(`SyncClient.updateWithChanges`, {
888
+ this.runtime.logger.debug(`SyncClient.updateWithChanges`, {
886
889
  modelId: model.id,
887
890
  modelType: model.getModelName(),
888
891
  });
@@ -928,7 +931,7 @@ export class SyncClient extends EventEmitter {
928
931
  this.pendingMutations = this.pendingMutations.filter((m) => m.model.id !== modelId);
929
932
  const afterCount = this.pendingMutations.length;
930
933
  if (beforeCount !== afterCount) {
931
- getContext().logger.debug('[SyncClient.clearPendingMutationsForModel] Cleared pending mutations', {
934
+ this.runtime.logger.debug('[SyncClient.clearPendingMutationsForModel] Cleared pending mutations', {
932
935
  modelId,
933
936
  clearedCount: beforeCount - afterCount,
934
937
  remainingCount: afterCount,
@@ -988,7 +991,7 @@ export class SyncClient extends EventEmitter {
988
991
  return null;
989
992
  }
990
993
  catch (error) {
991
- getContext().observability.captureMutationFailure({
994
+ this.runtime.observability.captureMutationFailure({
992
995
  context: 'file-upload',
993
996
  error: error instanceof Error ? error : new Error(String(error)),
994
997
  });
@@ -1187,9 +1190,9 @@ export class SyncClient extends EventEmitter {
1187
1190
  : (cb) => Promise.resolve().then(cb);
1188
1191
  schedule(() => {
1189
1192
  this.syncScheduled = false;
1190
- if (getContext().onlineStatus.isOnline()) {
1193
+ if (this.runtime.onlineStatus.isOnline()) {
1191
1194
  this.processPendingMutations().catch((err) => {
1192
- getContext().observability.breadcrumb('Background sync failed', 'sync.transaction', 'warning', { error: err instanceof Error ? err.message : String(err) });
1195
+ this.runtime.observability.breadcrumb('Background sync failed', 'sync.transaction', 'warning', { error: err instanceof Error ? err.message : String(err) });
1193
1196
  });
1194
1197
  }
1195
1198
  });
@@ -1248,7 +1251,7 @@ export class SyncClient extends EventEmitter {
1248
1251
  const restore = async (mutation, migrateLegacy, legacyMutationId) => {
1249
1252
  const parsed = persistedMutationSchema.safeParse(mutation);
1250
1253
  if (!parsed.success) {
1251
- getContext().logger.debug('[SyncClient] Dropping malformed persisted mutation', {
1254
+ this.runtime.logger.debug('[SyncClient] Dropping malformed persisted mutation', {
1252
1255
  issues: parsed.error.issues.map((i) => i.path.join('.')).join(', '),
1253
1256
  });
1254
1257
  return;
@@ -1260,7 +1263,7 @@ export class SyncClient extends EventEmitter {
1260
1263
  const age = Date.now() - writtenAt;
1261
1264
  if (!(age < PENDING_MUTATION_REPLAY_WINDOW_MS)) {
1262
1265
  heldForReview += 1;
1263
- getContext().logger.warn('A saved local write is older than the server idempotency window and was held for review.');
1266
+ this.runtime.logger.warn('A saved local write is older than the server idempotency window and was held for review.');
1264
1267
  return;
1265
1268
  }
1266
1269
  const mutationId = parsed.data.mutationId ?? legacyMutationId ?? `mutation_${uuid()}`;
@@ -1304,7 +1307,7 @@ export class SyncClient extends EventEmitter {
1304
1307
  await restore(legacy.data.mutation, true);
1305
1308
  continue;
1306
1309
  }
1307
- getContext().logger.debug('[SyncClient] Dropping malformed pending mutation record', {
1310
+ this.runtime.logger.debug('[SyncClient] Dropping malformed pending mutation record', {
1308
1311
  rowId: row.id,
1309
1312
  });
1310
1313
  continue;
@@ -1312,7 +1315,7 @@ export class SyncClient extends EventEmitter {
1312
1315
  if (parsed.data.scope.organizationId !== this.organizationId ||
1313
1316
  parsed.data.scope.participantId !== this.userId ||
1314
1317
  parsed.data.scope.namespace !== this.commitOutboxNamespace) {
1315
- getContext().logger.warn('A saved local write belongs to a different account or server and was held for review.');
1318
+ this.runtime.logger.warn('A saved local write belongs to a different account or server and was held for review.');
1316
1319
  continue;
1317
1320
  }
1318
1321
  await restore(parsed.data.mutation, false);
@@ -1334,10 +1337,10 @@ export class SyncClient extends EventEmitter {
1334
1337
  // A restore failure means queued offline writes did NOT rehydrate.
1335
1338
  // Self-healing is impossible here (the record may be unreadable), but
1336
1339
  // the failure must be visible for diagnosis instead of silent loss.
1337
- getContext().logger.debug('[SyncClient] Failed to restore offline mutation queue', {
1340
+ this.runtime.logger.debug('[SyncClient] Failed to restore offline mutation queue', {
1338
1341
  error: error instanceof Error ? error.message : String(error),
1339
1342
  });
1340
- getContext().observability.captureMutationFailure({
1343
+ this.runtime.observability.captureMutationFailure({
1341
1344
  context: 'restore-mutation-queue',
1342
1345
  error: error instanceof Error ? error : String(error),
1343
1346
  });
@@ -1367,14 +1370,14 @@ export class SyncClient extends EventEmitter {
1367
1370
  // identity, which surfaces downstream as "writes never confirm"; we do
1368
1371
  // NOT name internal wiring (`SyncClient.initialize`) here because that
1369
1372
  // method isn't part of the @abloatai/ablo surface a reader could act on.
1370
- getContext().logger.debug('[sync] writes waiting for identity (user/org not set yet) — queued, will retry', {
1373
+ this.runtime.logger.debug('[sync] writes waiting for identity (user/org not set yet) — queued, will retry', {
1371
1374
  pending: this.pendingMutations.length,
1372
1375
  userId: this.userId,
1373
1376
  organizationId: this.organizationId,
1374
1377
  });
1375
1378
  return;
1376
1379
  }
1377
- if (!getContext().onlineStatus.isOnline())
1380
+ if (!this.runtime.onlineStatus.isOnline())
1378
1381
  return; // Skip if offline
1379
1382
  if (this.isDisposed)
1380
1383
  return; // Skip if disposed
@@ -1419,7 +1422,7 @@ export class SyncClient extends EventEmitter {
1419
1422
  this.stagedMutationIds.delete(mutation.mutationId);
1420
1423
  this.pendingMutations = this.pendingMutations.filter((pending) => pending.mutationId !== mutation.mutationId);
1421
1424
  mutation.rejectStaged?.(outcome.reason);
1422
- getContext().observability.captureMutationFailure({
1425
+ this.runtime.observability.captureMutationFailure({
1423
1426
  context: 'persist-pending-mutation',
1424
1427
  error: outcome.reason instanceof Error
1425
1428
  ? outcome.reason
@@ -1451,7 +1454,7 @@ export class SyncClient extends EventEmitter {
1451
1454
  // apply, store add). That means the write never entered the queue, so
1452
1455
  // capture it instead of dropping it silently.
1453
1456
  const captureStagingFailure = (error) => {
1454
- getContext().observability.captureMutationFailure({
1457
+ this.runtime.observability.captureMutationFailure({
1455
1458
  context: `stage-mutation-${mutation.type}`,
1456
1459
  modelName: mutation.model.getModelName(),
1457
1460
  modelId: mutation.model.id,
@@ -1484,7 +1487,7 @@ export class SyncClient extends EventEmitter {
1484
1487
  : new Date(localModel.updatedAt).getTime()
1485
1488
  : 0;
1486
1489
  const serverUpdatedAt = toEpochMs(serverData.updatedAt);
1487
- getContext().logger.debug('Conflict resolution', {
1490
+ this.runtime.logger.debug('Conflict resolution', {
1488
1491
  modelId: localModel.id,
1489
1492
  modelType: localModel.getModelName(),
1490
1493
  hasLocalChanges,
@@ -1498,7 +1501,7 @@ export class SyncClient extends EventEmitter {
1498
1501
  const criticalServerStates = this.extractCriticalState(serverData);
1499
1502
  const shouldForceAcceptServer = this.hasCriticalStateChange(criticalServerStates);
1500
1503
  if (shouldForceAcceptServer) {
1501
- getContext().logger.debug('Accepting server update - critical state change detected', {
1504
+ this.runtime.logger.debug('Accepting server update - critical state change detected', {
1502
1505
  modelId: localModel.id,
1503
1506
  criticalStates: criticalServerStates,
1504
1507
  });
@@ -1512,7 +1515,7 @@ export class SyncClient extends EventEmitter {
1512
1515
  // Keep locally changed fields; apply server for the rest.
1513
1516
  if (hasLocalChanges) {
1514
1517
  const localChanges = localModel.getChanges();
1515
- getContext().logger.debug('Merging server update with local dirty fields', {
1518
+ this.runtime.logger.debug('Merging server update with local dirty fields', {
1516
1519
  modelId: localModel.id,
1517
1520
  keptFields: Object.keys(localChanges || {}),
1518
1521
  });
@@ -1531,7 +1534,7 @@ export class SyncClient extends EventEmitter {
1531
1534
  // No local changes: fall back to LWW to converge
1532
1535
  // Accept server regardless of timestamp equality to stay in sync
1533
1536
  const acceptReason = serverUpdatedAt > localUpdatedAt ? 'server is newer' : 'no local changes';
1534
- getContext().logger.debug(`Accepting server update - ${acceptReason}`);
1537
+ this.runtime.logger.debug(`Accepting server update - ${acceptReason}`);
1535
1538
  localModel.updateFromData(serverData);
1536
1539
  localModel.clearChanges();
1537
1540
  localModel.markAsSynced();
@@ -1575,7 +1578,7 @@ export class SyncClient extends EventEmitter {
1575
1578
  * Handle network reconnection
1576
1579
  */
1577
1580
  async handleReconnection() {
1578
- getContext().observability.breadcrumb('Network reconnected', 'sync.offline');
1581
+ this.runtime.observability.breadcrumb('Network reconnected', 'sync.offline');
1579
1582
  this.emit('sync:reconnecting');
1580
1583
  try {
1581
1584
  // Prefer a single batch flush for pending mutations (fast path)
@@ -1589,7 +1592,7 @@ export class SyncClient extends EventEmitter {
1589
1592
  this.emit('sync:reconnected');
1590
1593
  }
1591
1594
  catch (error) {
1592
- getContext().observability.captureMutationFailure({
1595
+ this.runtime.observability.captureMutationFailure({
1593
1596
  context: 'reconnection-sync',
1594
1597
  error: error instanceof Error ? error : new Error(String(error)),
1595
1598
  });
@@ -1600,7 +1603,7 @@ export class SyncClient extends EventEmitter {
1600
1603
  * Handle network disconnection
1601
1604
  */
1602
1605
  async handleDisconnection() {
1603
- getContext().observability.breadcrumb('Network disconnected', 'sync.offline');
1606
+ this.runtime.observability.breadcrumb('Network disconnected', 'sync.offline');
1604
1607
  this.setConnectionState('disconnected');
1605
1608
  this.emit('sync:offline');
1606
1609
  }
@@ -1622,8 +1625,8 @@ export class SyncClient extends EventEmitter {
1622
1625
  const oldState = this.connectionState;
1623
1626
  this.connectionState = state;
1624
1627
  if (oldState !== state) {
1625
- getContext().observability.setConnectionState(state);
1626
- getContext().observability.breadcrumb(`Connection: ${oldState} → ${state}`, 'sync.websocket');
1628
+ this.runtime.observability.setConnectionState(state);
1629
+ this.runtime.observability.breadcrumb(`Connection: ${oldState} → ${state}`, 'sync.websocket');
1627
1630
  if (state === 'connected') {
1628
1631
  this.emit('connection:established');
1629
1632
  this.mutationQueue.setConnectionState('connected');
@@ -1666,7 +1669,7 @@ export class SyncClient extends EventEmitter {
1666
1669
  observer.onSync(event);
1667
1670
  }
1668
1671
  catch (error) {
1669
- getContext().observability.breadcrumb('Observer error', 'sync.transaction', 'error', {
1672
+ this.runtime.observability.breadcrumb('Observer error', 'sync.transaction', 'error', {
1670
1673
  error: error instanceof Error ? error.message : String(error),
1671
1674
  });
1672
1675
  }
@@ -1689,7 +1692,7 @@ export class SyncClient extends EventEmitter {
1689
1692
  // WebSocket itself was ready. Always kick both durable lanes on the real
1690
1693
  // socket event, even when the high-level state did not change.
1691
1694
  void this.mutationQueue.flushOfflineQueue().catch((error) => {
1692
- getContext().observability.captureMutationFailure({
1695
+ this.runtime.observability.captureMutationFailure({
1693
1696
  context: 'restore-commit-outbox',
1694
1697
  error: error instanceof Error ? error : new Error(String(error)),
1695
1698
  });
@@ -1720,7 +1723,7 @@ export class SyncClient extends EventEmitter {
1720
1723
  this.mutationQueue.onDeltaReceived(syncId, transactionId, correlationId);
1721
1724
  }
1722
1725
  catch (e) {
1723
- getContext().observability.breadcrumb('Failed to notify delta received', 'sync.transaction', 'warning', {
1726
+ this.runtime.observability.breadcrumb('Failed to notify delta received', 'sync.transaction', 'warning', {
1724
1727
  syncId,
1725
1728
  transactionId,
1726
1729
  correlationId,
@@ -2071,7 +2074,7 @@ export class SyncClient extends EventEmitter {
2071
2074
  // fall-through.
2072
2075
  // Self-healing: the next catch-up poll / reconnect re-fetches and
2073
2076
  // re-applies this delta, so it's forensic, not consumer-actionable → debug.
2074
- getContext().logger.debug('[SyncClient.applyDeltaBatchToPool] skipping pool op for unpersisted delta', {
2077
+ this.runtime.logger.debug('[SyncClient.applyDeltaBatchToPool] skipping pool op for unpersisted delta', {
2075
2078
  modelName,
2076
2079
  modelId: modelId.slice(0, 12),
2077
2080
  });
@@ -35,10 +35,10 @@ import { createAbloHttpClient, } from '../transaction/transport/httpClient.js';
35
35
  import { layerPluginSurface, resolvePlugins, } from '../transaction/plugin.js';
36
36
  import { noopLogger } from '../transaction/logger.js';
37
37
  import { humans } from './humans.js';
38
+ import { kStoreCluster } from './storeCluster.js';
38
39
  import { createCoreClient } from './coreClient.js';
39
40
  import { buildReactiveEngine } from './reactiveEngine.js';
40
41
  import { resolveClientPrelude } from './clientPrelude.js';
41
- import { createDefaultMutationExecutor } from './wsMutationExecutor.js';
42
42
  export function Ablo(options) {
43
43
  if (options.transport === 'http') {
44
44
  // Capabilities are declared above transport (ADR 0016): the plugin list
@@ -63,11 +63,15 @@ export function Ablo(options) {
63
63
  // `connect()`, and `deferConnect` keeps even that closed until the store
64
64
  // has seeded identity and read scope during `ready()` — the late-bound
65
65
  // values (`kind`, the credential, `syncGroups`, the resume cursor) are
66
- // seeded there, not here. Built only for the reactive path: the core
67
- // client (`plugins: []`) constructs its own feed, and no plugin runs on
68
- // that path to read this one.
66
+ // seeded there, not here. Whether to build it is read off what the
67
+ // listed plugins DECLARE a plugin that requires a duplex transport
68
+ // gets one to attach to — never off a plugin's name; interrogating the
69
+ // assembly by id is the context's `hasPlugin`, a capability for
70
+ // plugins, not a dispatch key for this root. The empty list builds
71
+ // nothing here: the core client (`plugins: []`) constructs its own
72
+ // feed, and no plugin runs on that path to read this one.
69
73
  const pluginList = options.plugins ?? [humans()];
70
- const transport = pluginList.some((plugin) => plugin.id === 'humans')
74
+ const transport = pluginList.some((plugin) => plugin.requires?.duplex === true)
71
75
  ? new SyncWebSocket({
72
76
  baseUrl: url,
73
77
  kind,
@@ -77,19 +81,15 @@ export function Ablo(options) {
77
81
  deferConnect: true,
78
82
  })
79
83
  : null;
80
- // The default mutation executor sends `{ type: 'commit', ... }` over
81
- // that connection; before it opens, sends reject with the diagnosed
82
- // not-ready error and the MutationQueue owns the retry. Caller-supplied
83
- // executors are still honored for advanced cases (test mocks,
84
- // alternative transports).
85
- const executor = internalOptions.mutationExecutor ?? createDefaultMutationExecutor(() => transport);
86
84
  // Resolve the capability list before anything heavy is constructed
87
85
  // (ADR 0016). The two configuration gates — a duplicate id, a transport
88
86
  // mismatch — fire here, while the stack still points at the caller's own
89
87
  // setup. With no list given, the reactive materialiser is installed:
90
88
  // today's default client. (Flipping the bare default to the stateless core
91
89
  // is the mirror-flip step, decided with the published version identity —
92
- // not here.)
90
+ // not here.) The context carries the host's resolved values — url,
91
+ // credential source, identity — so `humans().init` constructs the store
92
+ // cluster right here, during resolution.
93
93
  const installedPlugins = resolvePlugins(pluginList, { duplex: true }, {
94
94
  logger,
95
95
  observability: internalOptions.observability,
@@ -99,6 +99,8 @@ export function Ablo(options) {
99
99
  ...(transport ? { transport } : {}),
100
100
  participant: { id: participantId, kind },
101
101
  syncGroups: internalOptions.syncGroups ?? [],
102
+ url,
103
+ auth: authCredentials,
102
104
  });
103
105
  const humansSurface = installedPlugins.humans;
104
106
  if (!humansSurface) {
@@ -132,18 +134,27 @@ export function Ablo(options) {
132
134
  // 'humans', which is exactly the condition the transport was built under.
133
135
  throw new AbloValidationError('The reactive client was selected but no connection was constructed.', { code: 'invalid_options', param: 'plugins' });
134
136
  }
135
- // The reactive engine is the humans() capability: everything from here on
136
- // runtime context, store, streams, model proxies, resources — builds in
137
- // `./reactiveEngine.ts`, fed the prelude plus the plugin's contribution
138
- // (the presence stream). The host owns construction; plugins declare and
139
- // contribute. A plugin surface that hands back a whole-client constructor
140
- // inverts that relationship tried and rejected (ADR 0016, follow-up 3b).
137
+ const cluster = humansSurface[kStoreCluster];
138
+ if (!cluster) {
139
+ // Unreachable on this path: the context above carries everything the
140
+ // cluster needs (connection, url, credential source, schema). A missing
141
+ // cluster means the context and the plugin disagree say so rather
142
+ // than building a client with no store.
143
+ throw new AbloValidationError('The reactive client was selected but no store was constructed.', { code: 'invalid_options', param: 'plugins' });
144
+ }
145
+ // The reactive engine is the humans() capability: `init` constructed the
146
+ // store cluster (runtime, component graph, store) from the widened
147
+ // context; what remains here builds in `./reactiveEngine.ts`, fed the
148
+ // prelude plus the plugin's contributions. The host owns assembly;
149
+ // plugins declare and construct their own parts. A plugin surface that
150
+ // hands back a whole-client constructor inverts that relationship —
151
+ // tried and rejected (ADR 0016, follow-up 3b).
141
152
  return layerPluginSurface(buildReactiveEngine({
142
153
  ...prelude,
143
154
  options,
144
- executor,
145
155
  transport,
146
156
  presence: humansSurface.presence,
157
+ cluster,
147
158
  createSibling: (siblingOptions) => Ablo(siblingOptions),
148
159
  }), installedPlugins);
149
160
  }
@@ -17,6 +17,7 @@ import type { AuthCredentialSource } from '../transaction/auth/credentialSource.
17
17
  import type { Schema, SchemaRecord } from '../transaction/schema/schema.js';
18
18
  import { type AbloPersistence } from '../transaction/persistence.js';
19
19
  import type { DurableWriteStore, DurableWritesConfig } from '../transactions/mutations/durableWriteStore.js';
20
+ import type { RuntimeContext } from '../RuntimeContext.js';
20
21
  export interface InternalComponentsInput<S extends SchemaRecord> {
21
22
  readonly schema: Schema<S>;
22
23
  /** The WebSocket URL. Used to derive the bootstrap HTTP base URL when the
@@ -34,6 +35,9 @@ export interface InternalComponentsInput<S extends SchemaRecord> {
34
35
  readonly commitOutbox?: DurableWriteStore;
35
36
  };
36
37
  readonly auth?: AuthCredentialSource;
38
+ /** The owning client's runtime, threaded into every component built here.
39
+ * Defaults to the module-global bridge for direct construction. */
40
+ readonly runtime?: RuntimeContext;
37
41
  }
38
42
  export interface InternalComponents {
39
43
  readonly modelRegistry: ModelRegistry;
@@ -16,17 +16,20 @@ import { BootstrapFetcher } from '../sync/BootstrapFetcher.js';
16
16
  import { loadsAtBootstrap } from '../transaction/schema/loadStrategy.js';
17
17
  import { resolveBootstrapBaseUrl } from '../transaction/auth/apiKey.js';
18
18
  import { shouldUseInMemoryPersistence } from '../transaction/persistence.js';
19
+ import { globalRuntime } from '../context.js';
19
20
  import { resolveDurableWrites } from '../transaction/durableWrites.js';
20
21
  export function createInternalComponents(input) {
21
22
  const { schema, url, options, auth } = input;
23
+ const runtime = input.runtime ?? globalRuntime;
22
24
  // The registry is created here, but model registration happens in the caller,
23
25
  // which owns the schema-to-class translation.
24
26
  const modelRegistry = new ModelRegistry({
25
27
  validateOnRegister: false,
26
28
  allowLateReferences: true,
29
+ runtime,
27
30
  });
28
31
  setActiveRegistry(modelRegistry);
29
- const objectPool = new InstanceCache({ maxSize: options.maxPoolSize ?? 10000 }, modelRegistry);
32
+ const objectPool = new InstanceCache({ maxSize: options.maxPoolSize ?? 10000, runtime }, modelRegistry);
30
33
  const bootstrapBaseUrl = resolveBootstrapBaseUrl({
31
34
  url,
32
35
  bootstrapBaseUrl: options.bootstrapBaseUrl,
@@ -36,15 +39,17 @@ export function createInternalComponents(input) {
36
39
  syncGroups: options.syncGroups,
37
40
  instantModels: deriveInstantModels(schema),
38
41
  getAuthToken: auth?.getAuthToken,
42
+ runtime,
39
43
  });
40
44
  const database = new Database(modelRegistry, bootstrapHelper, {
41
45
  // By default there is no browser-local durable store unless the caller asks
42
46
  // for one. Node and edge runtimes always use the in-memory store because
43
47
  // IndexedDB is unavailable there.
44
48
  inMemory: shouldUseInMemoryPersistence(options),
49
+ runtime,
45
50
  });
46
51
  const durableWrites = resolveDurableWrites(options);
47
- const syncClient = new SyncClient(objectPool, database, durableWrites.store, durableWrites.namespace ?? url);
52
+ const syncClient = new SyncClient(objectPool, database, durableWrites.store, durableWrites.namespace ?? url, runtime);
48
53
  // Lazy-load lane: hydrates the object pool and IndexedDB on demand for
49
54
  // entities not in scope at bootstrap (`load: 'lazy'` models, or an entity
50
55
  // reached by deep link before the pool warmed up). Single-flight, with
@@ -56,6 +61,7 @@ export function createInternalComponents(input) {
56
61
  schema,
57
62
  baseUrl: bootstrapBaseUrl,
58
63
  getAuthToken: auth?.getAuthToken,
64
+ runtime,
59
65
  });
60
66
  // Drop the lazy-lane hydration ledger on reconnect. While connected, the
61
67
  // WebSocket delta stream keeps hydrated rows fresh so repeat reads serve