@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
@@ -7,8 +7,8 @@
7
7
  * when its data is loaded.
8
8
  */
9
9
  import { ModelRegistry } from '../ModelRegistry.js';
10
- import { ObjectStore } from '../stores/ObjectStore.js';
11
- import { SyncActionStore } from '../stores/SyncActionStore.js';
10
+ import { ObjectStore } from './ObjectStore.js';
11
+ import { SyncActionStore } from './SyncActionStore.js';
12
12
  import { LoadStrategy } from '../transaction/types/index.js';
13
13
  import { AbloValidationError } from '../transaction/errors.js';
14
14
  /**
@@ -21,6 +21,7 @@ import { AbloValidationError } from '../transaction/errors.js';
21
21
  * - Handles store-specific optimizations based on load strategies
22
22
  */
23
23
  export class StoreManager {
24
+ runtime;
24
25
  stores = new Map();
25
26
  /** Strict-durability wrapper for the client write journal and commit outbox. */
26
27
  transactionStore = null;
@@ -28,7 +29,8 @@ export class StoreManager {
28
29
  db = null;
29
30
  isInitialized = false;
30
31
  modelRegistry;
31
- constructor(modelRegistry) {
32
+ constructor(modelRegistry, runtime = globalRuntime) {
33
+ this.runtime = runtime;
32
34
  this.modelRegistry = modelRegistry;
33
35
  }
34
36
  /**
@@ -38,10 +40,10 @@ export class StoreManager {
38
40
  this.db = db;
39
41
  if (this.isInitialized) {
40
42
  // Idempotent re-entry — harmless, nothing for the consumer to act on → debug.
41
- getContext().logger.debug('StoreManager already initialized');
43
+ this.runtime.logger.debug('StoreManager already initialized');
42
44
  return;
43
45
  }
44
- getContext().logger.info('Initializing ObjectStore instances for all models');
46
+ this.runtime.logger.info('Initializing ObjectStore instances for all models');
45
47
  const startTime = performance.now();
46
48
  // Get all registered models
47
49
  const allModels = this.modelRegistry.getRegisteredModelNames();
@@ -55,11 +57,11 @@ export class StoreManager {
55
57
  // mode.
56
58
  this.transactionStore = new ObjectStore(this.db, '__transactions', '__transactions', { loadStrategy: LoadStrategy.instant }, 'strict');
57
59
  // Initialize SyncactionStore
58
- this.syncactionStore = new SyncActionStore(this.db);
60
+ this.syncactionStore = new SyncActionStore(this.db, this.runtime);
59
61
  await this.syncactionStore.initialize();
60
62
  this.isInitialized = true;
61
63
  const duration = performance.now() - startTime;
62
- getContext().logger.info('Initialized ObjectStores and SyncactionStore', {
64
+ this.runtime.logger.info('Initialized ObjectStores and SyncactionStore', {
63
65
  count: this.stores.size,
64
66
  ms: duration.toFixed(2),
65
67
  });
@@ -84,14 +86,14 @@ export class StoreManager {
84
86
  * Create stores (tables) in IndexedDB
85
87
  */
86
88
  async createStores(db) {
87
- getContext().logger.info('Creating tables for all registered models');
89
+ this.runtime.logger.info('Creating tables for all registered models');
88
90
  for (const modelName of this.modelRegistry.getRegisteredModelNames()) {
89
91
  const storeName = modelName;
90
92
  // Skip if store already exists
91
93
  if (db.objectStoreNames.contains(storeName)) {
92
94
  continue;
93
95
  }
94
- getContext().logger.debug('Creating table', { storeName, modelName });
96
+ this.runtime.logger.debug('Creating table', { storeName, modelName });
95
97
  // Create object store with id as keyPath
96
98
  const store = db.createObjectStore(storeName, { keyPath: 'id' });
97
99
  // Create indexes for indexed properties
@@ -99,12 +101,12 @@ export class StoreManager {
99
101
  for (const propName of indexedProperties) {
100
102
  try {
101
103
  store.createIndex(propName, propName, { unique: false });
102
- getContext().logger.debug('Created index', { store: storeName, prop: propName });
104
+ this.runtime.logger.debug('Created index', { store: storeName, prop: propName });
103
105
  }
104
106
  catch (error) {
105
107
  // Internal IndexedDB index setup — a miss only affects local query
106
108
  // speed and isn't consumer-actionable → debug.
107
- getContext().logger.debug('Failed to create index', { store: storeName, prop: propName, error });
109
+ this.runtime.logger.debug('Failed to create index', { store: storeName, prop: propName, error });
108
110
  }
109
111
  }
110
112
  }
@@ -119,12 +121,12 @@ export class StoreManager {
119
121
  if (!db.objectStoreNames.contains('sync_action_table')) {
120
122
  const syncActionStore = db.createObjectStore('sync_action_table', { keyPath: 'id' });
121
123
  syncActionStore.createIndex('syncId', 'id');
122
- getContext().logger.debug('Created sync_action_table');
124
+ this.runtime.logger.debug('Created sync_action_table');
123
125
  }
124
126
  // Create __meta table for model persistence state and database metadata
125
127
  if (!db.objectStoreNames.contains('__meta')) {
126
128
  db.createObjectStore('__meta');
127
- getContext().logger.debug('Created __meta table');
129
+ this.runtime.logger.debug('Created __meta table');
128
130
  }
129
131
  // Create __transactions table for unsent transactions
130
132
  if (!db.objectStoreNames.contains('__transactions')) {
@@ -135,7 +137,7 @@ export class StoreManager {
135
137
  // Create indexes for transaction queries
136
138
  transactionStore.createIndex('timestamp', 'timestamp');
137
139
  transactionStore.createIndex('status', 'status');
138
- getContext().logger.debug('Created __transactions table');
140
+ this.runtime.logger.debug('Created __transactions table');
139
141
  }
140
142
  }
141
143
  /**
@@ -175,7 +177,7 @@ export class StoreManager {
175
177
  }
176
178
  }
177
179
  const allReady = notReadyStores.length === 0;
178
- getContext().logger.debug('Store readiness', {
180
+ this.runtime.logger.debug('Store readiness', {
179
181
  ready: readyStores.length,
180
182
  total: this.stores.size,
181
183
  notReady: notReadyStores,
@@ -232,10 +234,10 @@ export class StoreManager {
232
234
  * Perform maintenance on all stores
233
235
  */
234
236
  async performMaintenance() {
235
- getContext().logger.info('Performing maintenance on all stores');
237
+ this.runtime.logger.info('Performing maintenance on all stores');
236
238
  const promises = Array.from(this.stores.values()).map((store) => store.performMaintenance());
237
239
  await Promise.all(promises);
238
- getContext().logger.info('Store maintenance completed');
240
+ this.runtime.logger.info('Store maintenance completed');
239
241
  }
240
242
  /**
241
243
  * Clear all stores
@@ -243,17 +245,17 @@ export class StoreManager {
243
245
  async clearAllStores() {
244
246
  // Lifecycle chatter (logout / identity switch / reset), not a warning.
245
247
  // Logged at `debug` so it stays silent under the default `warn` threshold.
246
- getContext().logger.debug('Clearing all stores');
248
+ this.runtime.logger.debug('Clearing all stores');
247
249
  const promises = Array.from(this.stores.values()).map((store) => store.clear());
248
250
  await Promise.all(promises);
249
- getContext().logger.info('All stores cleared');
251
+ this.runtime.logger.info('All stores cleared');
250
252
  }
251
253
  /**
252
254
  * Mark all stores as closing to prevent new operations
253
255
  * Called before database connection is closed
254
256
  */
255
257
  markAllStoresAsClosing() {
256
- getContext().logger.debug('Marking all stores as closing');
258
+ this.runtime.logger.debug('Marking all stores as closing');
257
259
  for (const store of this.stores.values()) {
258
260
  store.markAsClosing();
259
261
  }
@@ -263,7 +265,7 @@ export class StoreManager {
263
265
  // If closing-state coordination is ever needed for sync actions, add it
264
266
  // explicitly to SyncActionStore rather than casting to reach a method that
265
267
  // may not exist.
266
- getContext().logger.debug('All stores marked as closing');
268
+ this.runtime.logger.debug('All stores marked as closing');
267
269
  }
268
270
  /**
269
271
  * Get comprehensive statistics
@@ -299,4 +301,4 @@ export class StoreManager {
299
301
  };
300
302
  }
301
303
  }
302
- import { getContext } from '../context.js';
304
+ import { globalRuntime } from '../context.js';
@@ -2,6 +2,7 @@
2
2
  * A local, IndexedDB-backed store for the sync actions (change deltas) the
3
3
  * server sends. See {@link SyncActionStore}.
4
4
  */
5
+ import type { RuntimeContext } from '../RuntimeContext.js';
5
6
  import { type SyncAction } from './syncAction.js';
6
7
  /**
7
8
  * Stores the sync actions (the change deltas) the server sends, keyed by
@@ -11,11 +12,12 @@ import { type SyncAction } from './syncAction.js';
11
12
  * replayed while resolving a conflict.
12
13
  */
13
14
  export declare class SyncActionStore {
15
+ private readonly runtime;
14
16
  private db;
15
17
  private storeName;
16
18
  private lastAppliedSyncId;
17
19
  private pendingActions;
18
- constructor(db: IDBDatabase);
20
+ constructor(db: IDBDatabase, runtime?: RuntimeContext);
19
21
  /**
20
22
  * Initialize store (create if needed)
21
23
  */
@@ -3,7 +3,7 @@
3
3
  * server sends. See {@link SyncActionStore}.
4
4
  */
5
5
  // Uses native IndexedDB for maximum performance
6
- import { getContext } from '../context.js';
6
+ import { globalRuntime } from '../context.js';
7
7
  import { syncActionSchema } from './syncAction.js';
8
8
  /**
9
9
  * The validation boundary for rows read back from the store. A row may have
@@ -14,10 +14,10 @@ import { syncActionSchema } from './syncAction.js';
14
14
  * than replayed as a malformed delta.
15
15
  */
16
16
  /** Parse one stored row into a SyncAction, or `null` (dropped + logged). */
17
- function toSyncAction(row) {
17
+ function toSyncAction(row, runtime) {
18
18
  const parsed = syncActionSchema.safeParse(row);
19
19
  if (!parsed.success) {
20
- getContext().logger.debug('[SyncActionStore] Dropping malformed stored sync action', {
20
+ runtime.logger.debug('[SyncActionStore] Dropping malformed stored sync action', {
21
21
  issues: parsed.error.issues.map((i) => i.path.join('.')).join(', '),
22
22
  });
23
23
  return null;
@@ -32,11 +32,13 @@ function toSyncAction(row) {
32
32
  * replayed while resolving a conflict.
33
33
  */
34
34
  export class SyncActionStore {
35
+ runtime;
35
36
  db;
36
37
  storeName = 'sync_action_table';
37
38
  lastAppliedSyncId = 0;
38
39
  pendingActions = new Map();
39
- constructor(db) {
40
+ constructor(db, runtime = globalRuntime) {
41
+ this.runtime = runtime;
40
42
  this.db = db;
41
43
  }
42
44
  /**
@@ -121,7 +123,7 @@ export class SyncActionStore {
121
123
  resolve(undefined);
122
124
  return;
123
125
  }
124
- resolve(toSyncAction(data) ?? undefined);
126
+ resolve(toSyncAction(data, this.runtime) ?? undefined);
125
127
  };
126
128
  request.onerror = () => { reject(request.error); };
127
129
  });
@@ -139,7 +141,7 @@ export class SyncActionStore {
139
141
  request.onsuccess = () => {
140
142
  const allData = request.result;
141
143
  const actions = allData
142
- .map(toSyncAction)
144
+ .map((row) => toSyncAction(row, this.runtime))
143
145
  .filter((action) => action !== null);
144
146
  resolve(actions);
145
147
  };
@@ -488,7 +490,7 @@ export class SyncActionStore {
488
490
  try {
489
491
  await this.updateLastSyncId(this.lastAppliedSyncId);
490
492
  const result = actionsToRewind
491
- .map(toSyncAction)
493
+ .map((row) => toSyncAction(row, this.runtime))
492
494
  .filter((action) => action !== null);
493
495
  resolve(result);
494
496
  }
@@ -94,11 +94,15 @@ export interface BootstrapOptions {
94
94
  * {@link BootstrapFetcher.setAuthToken}.
95
95
  */
96
96
  getAuthToken?: AuthTokenGetter;
97
+ /** The owning client's runtime. Defaults to the module-global bridge. */
98
+ runtime?: RuntimeContext;
97
99
  }
100
+ import type { RuntimeContext } from '../RuntimeContext.js';
98
101
  import { type AuthTokenGetter } from '../transaction/auth/credentialSource.js';
99
102
  import { type ValidatedServerDelta } from './schemas.js';
100
103
  export declare class BootstrapFetcher {
101
104
  private options;
105
+ private readonly runtime;
102
106
  /**
103
107
  * Every in-flight request's controller, tagged with the lane it belongs to. A
104
108
  * registry rather than a single field because a chunked cold start runs
@@ -6,11 +6,11 @@
6
6
  * cached snapshot when the device is offline. {@link BootstrapData} is the
7
7
  * shape it returns; {@link BootstrapOptions} configures it.
8
8
  */
9
- import { getContext } from '../context.js';
9
+ import { globalRuntime } from '../context.js';
10
10
  import { AbloError, AbloSessionError, AbloConnectionError, translateHttpError, toAbloError, isRetryableCode } from '../transaction/errors.js';
11
11
  import { withAuthHeaders } from '../transaction/auth/credentialSource.js';
12
12
  import { classifySchemaDrift, describeSchemaDrift, } from './schemaDrift.js';
13
- // SyncObservability replaced by getContext().observability
13
+ // SyncObservability replaced by this.runtime.observability
14
14
  import { parseBootstrapResponse } from './schemas.js';
15
15
  /**
16
16
  * Rows per page for the chunked cold-start bootstrap. Matches the server's
@@ -67,6 +67,7 @@ function classifyRequestFailure(error, controller, diedMessage) {
67
67
  }
68
68
  export class BootstrapFetcher {
69
69
  options;
70
+ runtime;
70
71
  /**
71
72
  * Every in-flight request's controller, tagged with the lane it belongs to. A
72
73
  * registry rather than a single field because a chunked cold start runs
@@ -139,14 +140,14 @@ export class BootstrapFetcher {
139
140
  warnOnSchemaDrift(serverHash) {
140
141
  if (this.schemaDriftWarned || !serverHash)
141
142
  return;
142
- const clientHash = getContext().config.expectedSchemaHash;
143
+ const clientHash = this.runtime.config.expectedSchemaHash;
143
144
  if (!clientHash || clientHash === serverHash)
144
145
  return;
145
146
  // A projection (`selectModels`/`omitModels`) hashes its subset, which never
146
147
  // equals the full schema a server runs — so it also carries the source
147
148
  // schema's hash. Matching that means the client is a faithful subset of the
148
149
  // deployed schema: current, not drifted. Only warn when neither matches.
149
- const sourceHash = getContext().config.expectedSourceSchemaHash;
150
+ const sourceHash = this.runtime.config.expectedSourceSchemaHash;
150
151
  if (sourceHash && sourceHash === serverHash)
151
152
  return;
152
153
  this.schemaDriftWarned = true;
@@ -158,7 +159,7 @@ export class BootstrapFetcher {
158
159
  // from the server's per-model surface before speaking; fall back to the
159
160
  // hash message only when that surface is unavailable (older server,
160
161
  // network hiccup). Fire-and-forget: never blocks or fails the bootstrap.
161
- const clientModels = getContext().config.expectedModelHashes;
162
+ const clientModels = this.runtime.config.expectedModelHashes;
162
163
  if (clientModels && Object.keys(clientModels).length > 0) {
163
164
  void this.resolveSemanticDrift(clientModels, clientHash, serverHash, where);
164
165
  return;
@@ -188,7 +189,7 @@ export class BootstrapFetcher {
188
189
  if (finding.kind === 'aligned')
189
190
  return; // additive server lead — not this client's concern
190
191
  if (finding.kind !== 'unknown') {
191
- getContext().logger.warn(describeSchemaDrift(finding, where), {
192
+ this.runtime.logger.warn(describeSchemaDrift(finding, where), {
192
193
  clientSchemaHash: clientHash,
193
194
  serverSchemaHash: serverHash,
194
195
  serverUrl: this.baseUrl,
@@ -210,7 +211,7 @@ export class BootstrapFetcher {
210
211
  // `[Ablo]` namespace — consumers wiring their own logger (pino, etc.) lose
211
212
  // that prefix, and a drift warning that reads like the app's own log is
212
213
  // worse than none. The brand tells them at a glance who is talking.
213
- getContext().logger.warn(`Ablo: Schema drift — the schema this client was built with (${clientHash}) is not the ` +
214
+ this.runtime.logger.warn(`Ablo: Schema drift — the schema this client was built with (${clientHash}) is not the ` +
214
215
  `one active on the server it connected to (${serverHash} at ${where}). Until they match, ` +
215
216
  `operations that depend on the difference will fail later with an opaque database error. ` +
216
217
  `This is usually one of three things. The schema may have been pushed to a different ` +
@@ -227,6 +228,7 @@ export class BootstrapFetcher {
227
228
  });
228
229
  }
229
230
  constructor(options) {
231
+ this.runtime = options.runtime ?? globalRuntime;
230
232
  // Defaults are spread first; the explicit `baseUrl` then takes precedence,
231
233
  // resolved from `options.baseUrl` or the localhost fallback. Callers pass
232
234
  // the full base URL, including the `/api` prefix.
@@ -299,7 +301,7 @@ export class BootstrapFetcher {
299
301
  const key = this.flightKey(lastSyncId);
300
302
  const joined = this.flights.get(key);
301
303
  if (joined) {
302
- getContext().logger.debug('Joining the bootstrap already in flight', { key });
304
+ this.runtime.logger.debug('Joining the bootstrap already in flight', { key });
303
305
  return joined;
304
306
  }
305
307
  // A request for something else genuinely does supersede: the running one is
@@ -372,14 +374,14 @@ export class BootstrapFetcher {
372
374
  ? this.loadCachedBootstrap(this.options.cacheScope)
373
375
  : null;
374
376
  if (cached) {
375
- getContext().logger.info('Using cached bootstrap (offline)');
377
+ this.runtime.logger.info('Using cached bootstrap (offline)');
376
378
  return cached;
377
379
  }
378
380
  throw new AbloConnectionError('Offline and no cached bootstrap available', {
379
381
  code: 'bootstrap_offline_no_cache',
380
382
  });
381
383
  }
382
- getContext().logger.info('Fetching fresh bootstrap data', { url });
384
+ this.runtime.logger.info('Fetching fresh bootstrap data', { url });
383
385
  const lane = syncGroupsOverride ? 'scoped' : 'bootstrap';
384
386
  // Chunk a COLD start by model: each instant model is its own request, so
385
387
  // one giant model can't make the whole snapshot undeliverable, and a
@@ -397,7 +399,7 @@ export class BootstrapFetcher {
397
399
  const data = chunked
398
400
  ? await this.fetchChunkedBootstrap(instantModels, this.options.syncGroups)
399
401
  : await this.fetchWithRetries(url, lane);
400
- getContext().logger.info('Bootstrap data fetched', {
402
+ this.runtime.logger.info('Bootstrap data fetched', {
401
403
  type: data.type,
402
404
  lastSyncId: data.lastSyncId,
403
405
  chunked,
@@ -429,7 +431,7 @@ export class BootstrapFetcher {
429
431
  ? this.loadCachedBootstrap(this.options.cacheScope)
430
432
  : null;
431
433
  if (cached) {
432
- getContext().observability.breadcrumb('Bootstrap cache fallback', 'sync.bootstrap', 'warning', {
434
+ this.runtime.observability.breadcrumb('Bootstrap cache fallback', 'sync.bootstrap', 'warning', {
433
435
  error: ablo.message,
434
436
  });
435
437
  return cached;
@@ -452,7 +454,7 @@ export class BootstrapFetcher {
452
454
  catch (error) {
453
455
  // SessionError should NOT be retried - the session is invalid and needs re-authentication
454
456
  if (AbloSessionError.isSessionError(error)) {
455
- getContext().observability.breadcrumb('Bootstrap session error - redirecting to sign-in', 'sync.bootstrap', 'warning', {
457
+ this.runtime.observability.breadcrumb('Bootstrap session error - redirecting to sign-in', 'sync.bootstrap', 'warning', {
456
458
  statusCode: (error).statusCode,
457
459
  });
458
460
  throw error;
@@ -465,11 +467,11 @@ export class BootstrapFetcher {
465
467
  // unclassified error with no code) flow through to the retry/backoff.
466
468
  const ablo = toAbloError(error);
467
469
  if (ablo.code && !isRetryableCode(ablo.code)) {
468
- getContext().observability.breadcrumb('Bootstrap non-retryable error — failing fast', 'sync.bootstrap', 'warning', { code: ablo.code, httpStatus: ablo.httpStatus });
470
+ this.runtime.observability.breadcrumb('Bootstrap non-retryable error — failing fast', 'sync.bootstrap', 'warning', { code: ablo.code, httpStatus: ablo.httpStatus });
469
471
  throw ablo;
470
472
  }
471
473
  lastError = error;
472
- getContext().observability.breadcrumb('Bootstrap fetch failed', 'sync.bootstrap', 'warning', {
474
+ this.runtime.observability.breadcrumb('Bootstrap fetch failed', 'sync.bootstrap', 'warning', {
473
475
  attempt: attempt + 1,
474
476
  });
475
477
  if (attempt < this.options.maxRetries - 1) {
@@ -490,7 +492,7 @@ export class BootstrapFetcher {
490
492
  * and cancels its siblings.
491
493
  */
492
494
  async fetchChunkedBootstrap(models, syncGroups) {
493
- getContext().logger.info('Bootstrap chunked by model', {
495
+ this.runtime.logger.info('Bootstrap chunked by model', {
494
496
  models: models.length,
495
497
  });
496
498
  const queue = [...models];
@@ -587,7 +589,7 @@ export class BootstrapFetcher {
587
589
  const etag = res.headers.get('ETag');
588
590
  if (res.status === 304) {
589
591
  // Log for telemetry
590
- getContext().logger.info('[Bootstrap] 304 Not Modified - using cached data');
592
+ this.runtime.logger.info('[Bootstrap] 304 Not Modified - using cached data');
591
593
  return { notModified: true, etag };
592
594
  }
593
595
  if (!res.ok) {
@@ -621,7 +623,7 @@ export class BootstrapFetcher {
621
623
  }
622
624
  throw translated;
623
625
  }
624
- const data = parseBootstrapResponse(await this.readJsonWithStallGuard(res, controller));
626
+ const data = parseBootstrapResponse(await this.readJsonWithStallGuard(res, controller), this.runtime);
625
627
  this.warnOnSchemaDrift(data.schemaHash);
626
628
  // Persist payload for offline
627
629
  try {
@@ -633,7 +635,7 @@ export class BootstrapFetcher {
633
635
  // Offline persistence is best-effort; a failed cache write must not
634
636
  // block returning the freshly fetched data.
635
637
  }
636
- getContext().logger.info('[Bootstrap] 200 OK - received new data');
638
+ this.runtime.logger.info('[Bootstrap] 200 OK - received new data');
637
639
  return { notModified: false, data, etag };
638
640
  }
639
641
  /**
@@ -667,7 +669,7 @@ export class BootstrapFetcher {
667
669
  const armStallTimer = () => {
668
670
  clearTimeout(stallTimer);
669
671
  stallTimer = setTimeout(() => {
670
- getContext().observability.breadcrumb('Bootstrap download stalled', 'sync.bootstrap', 'warning', { receivedBytes, stallTimeoutMs: this.options.stallTimeout });
672
+ this.runtime.observability.breadcrumb('Bootstrap download stalled', 'sync.bootstrap', 'warning', { receivedBytes, stallTimeoutMs: this.options.stallTimeout });
671
673
  const stallError = new AbloConnectionError(`Bootstrap download stalled: no data received for ${this.options.stallTimeout}ms (${receivedBytes} bytes arrived before the stream went quiet)`, { code: 'bootstrap_fetch_timeout' });
672
674
  stallReject?.(stallError);
673
675
  // Then tear the transfer down: abort frees the socket under real
@@ -723,7 +725,7 @@ export class BootstrapFetcher {
723
725
  }
724
726
  async fetchOnceWith(url, controller) {
725
727
  const timeoutId = setTimeout(() => {
726
- getContext().observability.breadcrumb('Bootstrap fetch timeout', 'sync.bootstrap', 'warning', {
728
+ this.runtime.observability.breadcrumb('Bootstrap fetch timeout', 'sync.bootstrap', 'warning', {
727
729
  timeoutMs: this.options.fetchTimeout,
728
730
  });
729
731
  controller.abort(new AbloConnectionError(`Bootstrap fetch timed out after ${this.options.fetchTimeout}ms waiting for the server to respond`, { code: 'bootstrap_fetch_timeout' }));
@@ -771,7 +773,7 @@ export class BootstrapFetcher {
771
773
  }
772
774
  throw translated;
773
775
  }
774
- const data = parseBootstrapResponse(await this.readJsonWithStallGuard(response, controller));
776
+ const data = parseBootstrapResponse(await this.readJsonWithStallGuard(response, controller), this.runtime);
775
777
  this.warnOnSchemaDrift(data.schemaHash);
776
778
  // Offline caching happens in `fetchBootstrap` on the assembled result —
777
779
  // caching here would let a single-model chunk overwrite the full snapshot.
@@ -848,11 +850,11 @@ export class BootstrapFetcher {
848
850
  }
849
851
  keysToRemove.forEach((key) => {
850
852
  localStorage.removeItem(key);
851
- getContext().logger.debug('Cleared cache key', { key });
853
+ this.runtime.logger.debug('Cleared cache key', { key });
852
854
  });
853
855
  }
854
856
  catch (error) {
855
- getContext().logger.debug('Failed to clear cache', { error });
857
+ this.runtime.logger.debug('Failed to clear cache', { error });
856
858
  }
857
859
  }
858
860
  // Cache helpers for offline bootstrap
@@ -866,7 +868,7 @@ export class BootstrapFetcher {
866
868
  localStorage.setItem(this.getBootstrapCacheKey(orgId), JSON.stringify(data));
867
869
  }
868
870
  catch (e) {
869
- getContext().logger.debug('Failed to cache bootstrap payload', {
871
+ this.runtime.logger.debug('Failed to cache bootstrap payload', {
870
872
  error: e instanceof Error ? e.message : String(e),
871
873
  });
872
874
  }
@@ -918,7 +920,7 @@ export class BootstrapFetcher {
918
920
  return body.status === 'healthy';
919
921
  }
920
922
  catch {
921
- getContext().observability.breadcrumb('Health check failed', 'sync.bootstrap', 'warning');
923
+ this.runtime.observability.breadcrumb('Health check failed', 'sync.bootstrap', 'warning');
922
924
  return false;
923
925
  }
924
926
  }
@@ -24,6 +24,7 @@ import type { InstanceCache } from '../InstanceCache.js';
24
24
  import type { Database } from '../Database.js';
25
25
  import type { Model } from '../Model.js';
26
26
  import type { ModelRegistry } from '../ModelRegistry.js';
27
+ import type { RuntimeContext } from '../RuntimeContext.js';
27
28
  import type { RecoveryClass } from '../transaction/errorCodes.js';
28
29
  import type { LoadWhere, WhereClause } from '../query/types.js';
29
30
  import type { Schema } from '../transaction/schema/schema.js';
@@ -41,6 +42,8 @@ export interface OnDemandLoaderOptions {
41
42
  readonly getAuthToken?: () => string | null;
42
43
  /** @deprecated Use `getAuthToken`. */
43
44
  readonly getCapabilityToken?: () => string | null;
45
+ /** The owning client's runtime. Defaults to the module-global bridge. */
46
+ readonly runtime?: RuntimeContext;
44
47
  }
45
48
  export interface FetchOptions<T> {
46
49
  /**
@@ -432,6 +432,7 @@ export class OnDemandLoader {
432
432
  baseUrl: this.opts.baseUrl,
433
433
  getAuthToken: this.authTokenProvider ?? undefined,
434
434
  recoverCredential: this.credentialRecovery ?? undefined,
435
+ runtime: this.opts.runtime,
435
436
  }, { queries: [query] });
436
437
  const rows = Array.isArray(result.results[0]) ? result.results[0] : [];
437
438
  // Normalize: wire rows lack `__typename` when the server elides it.
@@ -13,6 +13,7 @@
13
13
  * stale local copies the server no longer reports — are performed by the sync
14
14
  * client behind that interface.
15
15
  */
16
+ import type { RuntimeContext } from '../RuntimeContext.js';
16
17
  import type { BootstrapResult } from '../Database.js';
17
18
  import type { SyncDelta } from './SyncWebSocket.js';
18
19
  /** Counts describing what applying a bootstrap changed in the pool: entities
@@ -32,6 +33,8 @@ export interface RehydrationStats {
32
33
  * takes effect through this interface.
33
34
  */
34
35
  export interface PoolContext {
36
+ /** The owning client's runtime. Defaults to the module-global bridge. */
37
+ readonly runtime?: RuntimeContext;
35
38
  /** Applies persisted delta results to the in-memory pool, with the host's relation enrichment bound. */
36
39
  applyDeltaBatchToPool(results: NonNullable<BootstrapResult['deltaResults']>): void;
37
40
  /** Writes bootstrap data into the pool: creates models, heals partial rows, upserts, and removes stale local copies the server no longer reports. */
@@ -13,7 +13,7 @@
13
13
  * stale local copies the server no longer reports — are performed by the sync
14
14
  * client behind that interface.
15
15
  */
16
- import { getContext } from '../context.js';
16
+ import { globalRuntime } from '../context.js';
17
17
  /**
18
18
  * Applies a bootstrap result to the in-memory pool and returns what changed.
19
19
  * A full bootstrap creates, heals, and upserts models and removes stale local
@@ -42,7 +42,7 @@ export function applyBootstrapToPool(ctx, bootstrapResult, protectedIds) {
42
42
  // Creates models, heals partial rows, upserts, and removes stale local copies.
43
43
  const stats = ctx.applyBootstrapDataToPool(bootstrapData, protectedIds);
44
44
  const elapsedMs = Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - start);
45
- getContext().logger.info('[BaseSyncedStore] Bootstrap applied', {
45
+ (ctx.runtime ?? globalRuntime).logger.info('[BaseSyncedStore] Bootstrap applied', {
46
46
  ...stats, elapsedMs, poolSize: ctx.getPoolSize(),
47
47
  });
48
48
  return { ...stats, elapsedMs };
@@ -10,18 +10,12 @@
10
10
  * point lives on the store and drives this pipeline through {@link enqueueDelta}
11
11
  * and {@link flushPendingDeltas}.
12
12
  */
13
+ import type { RuntimeContext } from '../RuntimeContext.js';
13
14
  import { ModelScope } from '../InstanceCache.js';
14
15
  import type { Model } from '../Model.js';
15
- import type { ModelData } from '../transaction/types/modelData.js';
16
16
  import type { SyncDelta } from './SyncWebSocket.js';
17
- /** One applied-delta result from the persistence layer's batch write, forwarded to the pool. */
18
- interface DeltaDbResult {
19
- action: 'add' | 'update' | 'remove' | 'archive' | 'verify';
20
- modelName: string;
21
- modelId: string;
22
- data?: ModelData | null;
23
- transactionId?: string;
24
- }
17
+ import type { ModelData } from '../transaction/types/modelData.js';
18
+ import { type AbloPlugin, type AppliedChange } from '../transaction/plugin.js';
25
19
  /**
26
20
  * What the pipeline needs back from the surrounding store: the shared mutable
27
21
  * pipeline state (backed by the store's own fields through accessors), narrow
@@ -29,6 +23,14 @@ interface DeltaDbResult {
29
23
  * subclass's overrides continue to take effect.
30
24
  */
31
25
  export interface DeltaPipelineContext {
26
+ /** The owning client's runtime. Defaults to the module-global bridge. */
27
+ readonly runtime?: RuntimeContext;
28
+ /**
29
+ * The installed plugins, whose declared stage handlers this pipeline
30
+ * dispatches at each boundary. Empty (or absent) on directly-constructed
31
+ * stores, where the store's own apply is the whole pipeline.
32
+ */
33
+ readonly stagePlugins?: readonly AbloPlugin[];
32
34
  pendingDeltas: SyncDelta[];
33
35
  batchTimer: ReturnType<typeof setTimeout> | null;
34
36
  /** Queue for deltas arriving during an active bootstrap; null when none. */
@@ -52,11 +54,11 @@ export interface DeltaPipelineContext {
52
54
  data: ModelData | null;
53
55
  transactionId?: string;
54
56
  }[]): Promise<{
55
- results: DeltaDbResult[];
57
+ results: AppliedChange[];
56
58
  persistedSyncId: number;
57
59
  }>;
58
60
  /** Applies persisted delta results to the in-memory pool, with the host's relation enrichment bound. */
59
- applyDeltaBatchToPool(results: DeltaDbResult[]): void;
61
+ applyDeltaBatchToPool(results: AppliedChange[]): void;
60
62
  /** Acknowledges a sync id back to the server; a no-op when the socket is down. */
61
63
  acknowledge(syncId: number): void;
62
64
  readonly objectPool: {
@@ -110,4 +112,3 @@ export declare function scheduleDeltaFlush(ctx: DeltaPipelineContext): void;
110
112
  * and advances the acknowledgement cursor once the store write has committed.
111
113
  */
112
114
  export declare function flushPendingDeltas(ctx: DeltaPipelineContext): Promise<void>;
113
- export {};