@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
@@ -19,11 +19,13 @@ import type { Database, BootstrapResult } from './Database.js';
19
19
  import type { InstanceCache } from './InstanceCache.js';
20
20
  import { ModelRegistry } from './ModelRegistry.js';
21
21
  import { SyncWebSocket, type SyncDelta, type SyncGroupChangePayload, type GroupAddedPayload, type GroupRemovedPayload, type BootstrapHint, type BootstrapDataEvent, type PresenceUpdate, type EventMap, type DefaultCollaborationEvents, type SyncWebSocketEventMap } from './sync/SyncWebSocket.js';
22
- import { QueryProcessor } from './core/QueryProcessor.js';
22
+ import { QueryProcessor } from './query/QueryProcessor.js';
23
23
  import { Model } from './Model.js';
24
+ import type { RuntimeContext } from './RuntimeContext.js';
25
+ import type { AbloPlugin, AppliedChange } from './transaction/plugin.js';
24
26
  import { ModelScope } from './InstanceCache.js';
25
27
  import type { Schema } from './transaction/schema/schema.js';
26
- import type { SyncStatus, LocalMutation } from './core/storeContract.js';
28
+ import type { SyncStatus, LocalMutation } from './storeContract.js';
27
29
  import type { AuthCredentialSource } from './transaction/auth/credentialSource.js';
28
30
  import type { ModelData } from './transaction/types/modelData.js';
29
31
  import type { EnrichmentPlanEntry, ForeignKeyIndexSpec } from './sync/syncPlan.js';
@@ -75,7 +77,7 @@ export interface SyncedStoreConfig {
75
77
  */
76
78
  foreignKeyIndexes?: readonly ForeignKeyIndexSpec[];
77
79
  }
78
- export type { SyncStatus } from './core/storeContract.js';
80
+ export type { SyncStatus } from './storeContract.js';
79
81
  /** User context for initialization */
80
82
  export interface UserContext {
81
83
  userId: string;
@@ -162,6 +164,10 @@ export { deriveSyncPlanFromSchema } from './sync/syncPlan.js';
162
164
  */
163
165
  export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaboration> = DefaultCollaborationEvents, TSchema extends Schema = Schema> {
164
166
  syncStatus: SyncStatus;
167
+ /** The owning client's runtime; the module-global bridge when constructed directly. */
168
+ protected readonly runtime: RuntimeContext;
169
+ /** The installed plugins the delta pipeline dispatches stage handlers to. */
170
+ protected readonly stagePlugins: readonly AbloPlugin[];
165
171
  protected readonly syncClient: SyncClient;
166
172
  protected readonly database: Database;
167
173
  protected readonly objectPool: InstanceCache;
@@ -307,6 +313,14 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
307
313
  url?: string;
308
314
  /** Shared bearer credential source for every auth-aware transport. */
309
315
  auth?: AuthCredentialSource;
316
+ /** The owning client's runtime. Defaults to the module-global bridge. */
317
+ runtime?: RuntimeContext;
318
+ /**
319
+ * The installed plugins, whose declared stage handlers the delta
320
+ * pipeline dispatches. Empty on direct construction — the store's own
321
+ * apply is then the whole pipeline.
322
+ */
323
+ stagePlugins?: readonly AbloPlugin[];
310
324
  }, config?: SyncedStoreConfig);
311
325
  /**
312
326
  * Register foreign-key indexes for constant-time lookups.
@@ -411,7 +425,7 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
411
425
  waitForConfirmation(modelName: string, modelId: string): Promise<void>;
412
426
  /**
413
427
  * Observe the LOCAL mutation stream for undo recording (see
414
- * {@link import('./core/storeContract.js').LocalMutation}). Taps the
428
+ * {@link import('./storeContract.js').LocalMutation}). Taps the
415
429
  * MutationQueue's `transaction:created` event — fired once per local
416
430
  * create/update/delete/archive with `previousData` already captured.
417
431
  * Remote/collaborator deltas apply via `applyDeltaBatchToPool` and never
@@ -649,6 +663,12 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
649
663
  * read the live host fields). */
650
664
  private _deltaPipelineContext;
651
665
  private get deltaPipelineContext();
666
+ /**
667
+ * Lands persisted changes in the in-memory pool, with this store's
668
+ * relation enrichment bound. The one apply path: the pipeline's bridge
669
+ * (no plugins installed) and the `humans()` apply handler both call it.
670
+ */
671
+ applyChangesToPool(changes: readonly AppliedChange[]): void;
652
672
  /** Get fields that represent meaningful state for deduplication. Override for model-specific fields. */
653
673
  protected getStateFields(_modelName: string): string[];
654
674
  /** Deduplicate deltas to the same entity — keep meaningful state transitions only */
@@ -19,9 +19,9 @@ import { resolveParticipantSyncGroups, } from './sync/participants.js';
19
19
  import { ModelRegistry } from './ModelRegistry.js';
20
20
  import { PropertyType } from './transaction/types/index.js';
21
21
  import { SyncWebSocket, } from './sync/SyncWebSocket.js';
22
- import { QueryProcessor } from './core/QueryProcessor.js';
22
+ import { QueryProcessor } from './query/QueryProcessor.js';
23
23
  import { Model, rowAsModel } from './Model.js';
24
- import { getContext } from './context.js';
24
+ import { globalRuntime } from './context.js';
25
25
  import { AbloSessionError, isAccessCredentialExpiryCloseReason } from './transaction/errors.js';
26
26
  import { ModelScope } from './InstanceCache.js';
27
27
  import { LazyReferenceCollection } from './LazyReferenceCollection.js';
@@ -84,6 +84,10 @@ export class BaseSyncedStore {
84
84
  isSessionError: false,
85
85
  };
86
86
  // ── Injected dependencies ──
87
+ /** The owning client's runtime; the module-global bridge when constructed directly. */
88
+ runtime;
89
+ /** The installed plugins the delta pipeline dispatches stage handlers to. */
90
+ stagePlugins;
87
91
  syncClient;
88
92
  database;
89
93
  objectPool;
@@ -203,7 +207,7 @@ export class BaseSyncedStore {
203
207
  this.hydratedGroups.add(g);
204
208
  }
205
209
  catch (err) {
206
- getContext().logger.debug('[BaseSyncedStore] scoped hydrate failed', {
210
+ this.runtime.logger.debug('[BaseSyncedStore] scoped hydrate failed', {
207
211
  syncGroups: need,
208
212
  error: err instanceof Error ? err.message : String(err),
209
213
  });
@@ -280,6 +284,8 @@ export class BaseSyncedStore {
280
284
  modelTypesHydrated = new Set();
281
285
  modelTypeHydrationInFlight = new Map();
282
286
  constructor(dependencies, config = {}) {
287
+ this.runtime = dependencies.runtime ?? globalRuntime;
288
+ this.stagePlugins = dependencies.stagePlugins ?? [];
283
289
  this.syncClient = dependencies.syncClient;
284
290
  this.database = dependencies.database;
285
291
  this.objectPool = dependencies.objectPool;
@@ -516,7 +522,7 @@ export class BaseSyncedStore {
516
522
  }
517
523
  /**
518
524
  * Observe the LOCAL mutation stream for undo recording (see
519
- * {@link import('./core/storeContract.js').LocalMutation}). Taps the
525
+ * {@link import('./storeContract.js').LocalMutation}). Taps the
520
526
  * MutationQueue's `transaction:created` event — fired once per local
521
527
  * create/update/delete/archive with `previousData` already captured.
522
528
  * Remote/collaborator deltas apply via `applyDeltaBatchToPool` and never
@@ -563,18 +569,18 @@ export class BaseSyncedStore {
563
569
  // negation false-positives every server-side bootstrap (e.g. the
564
570
  // server-side agent.run dispatch path through `connectAgent`).
565
571
  if (typeof navigator !== 'undefined' && navigator.onLine === false) {
566
- getContext().observability.breadcrumb(`Bootstrap attempt ${attempt} skipped - offline`, 'sync.bootstrap', 'warning');
572
+ this.runtime.observability.breadcrumb(`Bootstrap attempt ${attempt} skipped - offline`, 'sync.bootstrap', 'warning');
567
573
  throw new AbloConnectionError('Bootstrap skipped - device is offline', {
568
574
  code: 'bootstrap_offline',
569
575
  });
570
576
  }
571
577
  try {
572
- getContext().logger.info(`[BaseSyncedStore] Bootstrap attempt ${attempt}/${BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS}`);
578
+ this.runtime.logger.info(`[BaseSyncedStore] Bootstrap attempt ${attempt}/${BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS}`);
573
579
  const result = (await Promise.race([
574
580
  bootstrapFn(),
575
581
  this.createBootstrapTimeout(attempt),
576
582
  ]));
577
- getContext().logger.info('[BaseSyncedStore] Bootstrap completed successfully', { attempt });
583
+ this.runtime.logger.info('[BaseSyncedStore] Bootstrap completed successfully', { attempt });
578
584
  return result;
579
585
  }
580
586
  catch (error) {
@@ -587,12 +593,12 @@ export class BaseSyncedStore {
587
593
  if (AbloSessionError.isSessionError(error))
588
594
  throw error;
589
595
  if (isNetworkError && typeof navigator !== 'undefined' && navigator.onLine === false) {
590
- getContext().observability.captureBootstrapFailure(error, { type: 'network-offline' });
596
+ this.runtime.observability.captureBootstrapFailure(error, { type: 'network-offline' });
591
597
  throw error;
592
598
  }
593
- getContext().observability.breadcrumb(`Bootstrap attempt ${attempt} failed`, 'sync.bootstrap', 'warning', { isTimeout, isNetworkError, willRetry: attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS });
599
+ this.runtime.observability.breadcrumb(`Bootstrap attempt ${attempt} failed`, 'sync.bootstrap', 'warning', { isTimeout, isNetworkError, willRetry: attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS });
594
600
  if (isTimeout && attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS) {
595
- getContext().logger.info('[BaseSyncedStore] Resetting state before bootstrap retry');
601
+ this.runtime.logger.info('[BaseSyncedStore] Resetting state before bootstrap retry');
596
602
  this.resetBootstrapState();
597
603
  await new Promise((resolve) => setTimeout(resolve, BOOTSTRAP_CONFIG.RETRY_DELAY_MS));
598
604
  }
@@ -666,10 +672,10 @@ export class BaseSyncedStore {
666
672
  // is stale — clear it so re-entered groups backfill again.
667
673
  this.hydratedGroups.clear();
668
674
  this.hydratingGroups.clear();
669
- getContext().logger.info('[BaseSyncedStore] Bootstrap state reset complete');
675
+ this.runtime.logger.info('[BaseSyncedStore] Bootstrap state reset complete');
670
676
  }
671
677
  catch {
672
- getContext().observability.breadcrumb('Error resetting bootstrap state', 'sync.bootstrap', 'warning');
678
+ this.runtime.observability.breadcrumb('Error resetting bootstrap state', 'sync.bootstrap', 'warning');
673
679
  }
674
680
  }
675
681
  // ── Reconnection ─────────────────────────────────────────────────────────
@@ -698,7 +704,7 @@ export class BaseSyncedStore {
698
704
  return 'success';
699
705
  }
700
706
  catch (error) {
701
- getContext().observability.captureBootstrapFailure(error, { type: 'connection-store-reconnect' });
707
+ this.runtime.observability.captureBootstrapFailure(error, { type: 'connection-store-reconnect' });
702
708
  if (AbloSessionError.isSessionError(error)) {
703
709
  this.syncWebSocket.setSessionErrorDetected();
704
710
  this.syncWebSocket.disconnect();
@@ -717,13 +723,13 @@ export class BaseSyncedStore {
717
723
  const hydratedSize = this.objectPool.size;
718
724
  if (hydratedSize > 0) {
719
725
  this.dataReady = true;
720
- getContext().logger.info('[BaseSyncedStore] Hydrated from local fallback', {
726
+ this.runtime.logger.info('[BaseSyncedStore] Hydrated from local fallback', {
721
727
  objectPoolSize: hydratedSize,
722
728
  });
723
729
  }
724
730
  }
725
731
  catch (fallbackError) {
726
- getContext().logger.debug('[BaseSyncedStore] Local fallback failed', {
732
+ this.runtime.logger.debug('[BaseSyncedStore] Local fallback failed', {
727
733
  error: fallbackError.message,
728
734
  });
729
735
  }
@@ -792,6 +798,7 @@ export class BaseSyncedStore {
792
798
  /** Narrow context the group-change leaf talks back through. */
793
799
  groupChangeContext() {
794
800
  return {
801
+ runtime: this.runtime,
795
802
  database: this.database,
796
803
  objectPool: this.objectPool,
797
804
  getSubscribedSyncGroups: () => this.syncWebSocket.getSyncGroups(),
@@ -861,6 +868,7 @@ export class BaseSyncedStore {
861
868
  poolContext() {
862
869
  const store = this;
863
870
  return {
871
+ runtime: this.runtime,
864
872
  applyDeltaBatchToPool: (results) => {
865
873
  this.syncClient.applyDeltaBatchToPool(results, (name, data) => this.enrichRelations(name, data));
866
874
  },
@@ -901,8 +909,8 @@ export class BaseSyncedStore {
901
909
  hasLocalData = this.objectPool.size > 0;
902
910
  }
903
911
  catch (hydrateError) {
904
- getContext().logger.debug('[sync-engine] IDB hydration failed', { error: hydrateError });
905
- getContext().observability.captureBootstrapFailure(hydrateError, { type: 'hydration-from-idb' });
912
+ this.runtime.logger.debug('[sync-engine] IDB hydration failed', { error: hydrateError });
913
+ this.runtime.observability.captureBootstrapFailure(hydrateError, { type: 'hydration-from-idb' });
906
914
  }
907
915
  // Get sync baseline for WebSocket
908
916
  const lastSyncId = (yield this.database.getLastSyncId());
@@ -925,7 +933,7 @@ export class BaseSyncedStore {
925
933
  // delta flow works regardless of this branch.
926
934
  const requirements = (yield this.database.requiredBootstrap());
927
935
  if (context.bootstrapMode === 'none') {
928
- getContext().logger.info('[BaseSyncedStore] Bootstrap skipped (bootstrapMode=none)', { kind: context.kind ?? 'user' });
936
+ this.runtime.logger.info('[BaseSyncedStore] Bootstrap skipped (bootstrapMode=none)', { kind: context.kind ?? 'user' });
929
937
  // `setupWebSocketSync` above creates the SyncWebSocket and
930
938
  // initiates the upgrade, but it does NOT await the 'connected'
931
939
  // event — it returns synchronously after wiring listeners.
@@ -952,7 +960,7 @@ export class BaseSyncedStore {
952
960
  // delta-queue replay in withDeltaQueuing's finally (and the error
953
961
  // handler itself), which would otherwise vanish unhandled.
954
962
  void this.performBackgroundBootstrap(requirements, context, signal).catch((error) => {
955
- getContext().observability.captureBootstrapFailure(error, {
963
+ this.runtime.observability.captureBootstrapFailure(error, {
956
964
  type: 'background-orchestration',
957
965
  });
958
966
  });
@@ -984,7 +992,7 @@ export class BaseSyncedStore {
984
992
  return { success: false, error: error };
985
993
  }
986
994
  const isSession = AbloSessionError.isSessionError(error);
987
- getContext().observability.captureBootstrapFailure(error, { type: 'initialize' });
995
+ this.runtime.observability.captureBootstrapFailure(error, { type: 'initialize' });
988
996
  if (isSession) {
989
997
  this.syncWebSocket.setSessionErrorDetected();
990
998
  this.syncWebSocket.disconnect();
@@ -1021,11 +1029,11 @@ export class BaseSyncedStore {
1021
1029
  this.updateSyncStatus({ state: 'idle', progress: 100 });
1022
1030
  }
1023
1031
  catch (error) {
1024
- getContext().logger.debug('[sync-engine] Background bootstrap failed', {
1032
+ this.runtime.logger.debug('[sync-engine] Background bootstrap failed', {
1025
1033
  error: error instanceof Error ? error.message : String(error),
1026
1034
  cause: error,
1027
1035
  });
1028
- getContext().observability.captureBootstrapFailure(error, { type: 'background' });
1036
+ this.runtime.observability.captureBootstrapFailure(error, { type: 'background' });
1029
1037
  if (AbloSessionError.isSessionError(error)) {
1030
1038
  this.syncWebSocket.setSessionErrorDetected();
1031
1039
  this.syncWebSocket.disconnect();
@@ -1210,7 +1218,7 @@ export class BaseSyncedStore {
1210
1218
  return;
1211
1219
  resolved = true;
1212
1220
  unsubscribe();
1213
- getContext().logger.debug(`[BaseSyncedStore] waitForWebSocketConnected timed out after ${timeoutMs}ms — initialize() will return but the next mutation may race the upgrade.`);
1221
+ this.runtime.logger.debug(`[BaseSyncedStore] waitForWebSocketConnected timed out after ${timeoutMs}ms — initialize() will return but the next mutation may race the upgrade.`);
1214
1222
  resolve(false);
1215
1223
  }, timeoutMs);
1216
1224
  });
@@ -1227,7 +1235,7 @@ export class BaseSyncedStore {
1227
1235
  */
1228
1236
  setupWebSocketSync(context, lastSyncId) {
1229
1237
  if (!context.userId || !context.organizationId) {
1230
- getContext().observability.breadcrumb('Cannot setup WebSocket sync without user context', 'sync.websocket', 'warning');
1238
+ this.runtime.observability.breadcrumb('Cannot setup WebSocket sync without user context', 'sync.websocket', 'warning');
1231
1239
  return;
1232
1240
  }
1233
1241
  if (context.kind)
@@ -1280,7 +1288,7 @@ export class BaseSyncedStore {
1280
1288
  this.updateSyncStatus({ state: 'offline', offlineSince: new Date() });
1281
1289
  });
1282
1290
  const onReconnecting = this.syncWebSocket.subscribe('reconnecting', ({ attempt, delay }) => {
1283
- getContext().logger.info('[BaseSyncedStore] WebSocket reconnecting', { attempt, delay });
1291
+ this.runtime.logger.info('[BaseSyncedStore] WebSocket reconnecting', { attempt, delay });
1284
1292
  this.updateSyncStatus({ state: 'reconnecting' });
1285
1293
  });
1286
1294
  // Delta events → feed into processing pipeline
@@ -1313,7 +1321,7 @@ export class BaseSyncedStore {
1313
1321
  // Terminal session loss (revocation / the login itself is gone): notify,
1314
1322
  // route the FSM to its terminal state, and clear local data.
1315
1323
  const handleTerminalSessionError = (error) => {
1316
- getContext().observability.captureWebSocketError({ context: 'session-error', error: error.message });
1324
+ this.runtime.observability.captureWebSocketError({ context: 'session-error', error: error.message });
1317
1325
  this.onConnectionEvent?.('WS_SESSION_ERROR');
1318
1326
  for (const listener of this.sessionErrorListeners) {
1319
1327
  try {
@@ -1326,8 +1334,8 @@ export class BaseSyncedStore {
1326
1334
  // When auth is revoked, locally cached data must not persist on disk.
1327
1335
  this.database.clear({ includeWriteJournal: true }).catch((clearErr) => {
1328
1336
  // consumer register: session ended, but cached data may remain on disk
1329
- getContext().logger.error('Your session ended, but some locally cached data could not be cleared from this device.');
1330
- getContext().logger.debug('[BaseSyncedStore] Failed to clear database on session error', clearErr);
1337
+ this.runtime.logger.error('Your session ended, but some locally cached data could not be cleared from this device.');
1338
+ this.runtime.logger.debug('[BaseSyncedStore] Failed to clear database on session error', clearErr);
1331
1339
  });
1332
1340
  this.objectPool.clear();
1333
1341
  };
@@ -1342,7 +1350,7 @@ export class BaseSyncedStore {
1342
1350
  // through to the terminal path. Without this branch, every credential
1343
1351
  // TTL elapse wedged the socket behind the write-once session latch.
1344
1352
  if (AbloSessionError.isSessionError(error) && isAccessCredentialExpiryCloseReason(error.message)) {
1345
- getContext().observability.breadcrumb('WebSocket closed for expired access credential — re-minting', 'sync.websocket', 'warning');
1353
+ this.runtime.observability.breadcrumb('WebSocket closed for expired access credential — re-minting', 'sync.websocket', 'warning');
1346
1354
  // Un-latch BEFORE the async mint so the FSM's own recovery
1347
1355
  // (probe → refreshing_credential → reconnect) is never blocked on
1348
1356
  // our .then() ordering.
@@ -1390,8 +1398,8 @@ export class BaseSyncedStore {
1390
1398
  });
1391
1399
  const onReconnectFailed = this.syncWebSocket.subscribe('reconnect_failed', ({ attempts }) => {
1392
1400
  // consumer register: reconnection exhausted — the app is now offline
1393
- getContext().logger.warn('Lost connection to the sync service and could not reconnect. Your app is now offline; changes will sync once the connection is restored.');
1394
- getContext().logger.debug('[BaseSyncedStore] WebSocket reconnection gave up', { attempts });
1401
+ this.runtime.logger.warn('Lost connection to the sync service and could not reconnect. Your app is now offline; changes will sync once the connection is restored.');
1402
+ this.runtime.logger.debug('[BaseSyncedStore] WebSocket reconnection gave up', { attempts });
1395
1403
  this.updateSyncStatus({ state: 'reconnecting' });
1396
1404
  });
1397
1405
  this.disposers.push(onConnected, onDisconnected, onReconnecting, onDelta, onDeltaBatch, onBootstrapRequired, onBootstrapData, onPresenceUpdate, onError, onSessionError, onHandshakeFailed, onReconnectFailed, () => { this.areaOfInterest.dispose(); });
@@ -1513,6 +1521,8 @@ export class BaseSyncedStore {
1513
1521
  return this._deltaPipelineContext;
1514
1522
  const store = this;
1515
1523
  this._deltaPipelineContext = {
1524
+ runtime: this.runtime,
1525
+ stagePlugins: this.stagePlugins,
1516
1526
  // Shared pipeline state, backed by the host fields.
1517
1527
  get pendingDeltas() { return store.pendingDeltas; },
1518
1528
  set pendingDeltas(deltas) { store.pendingDeltas = deltas; },
@@ -1530,9 +1540,7 @@ export class BaseSyncedStore {
1530
1540
  advancePersisted: (syncId) => { this.syncClient.position.advancePersisted(syncId); },
1531
1541
  // Persistence + pool writes.
1532
1542
  processDeltaBatch: (deltas) => this.database.processDeltaBatch(deltas),
1533
- applyDeltaBatchToPool: (results) => {
1534
- this.syncClient.applyDeltaBatchToPool(results, (name, data) => this.enrichRelations(name, data));
1535
- },
1543
+ applyDeltaBatchToPool: (results) => { this.applyChangesToPool(results); },
1536
1544
  acknowledge: (syncId) => { this.syncWebSocket.acknowledge(syncId); },
1537
1545
  get objectPool() { return store.objectPool; },
1538
1546
  // Dynamic-dispatch hooks — protected override points on this class.
@@ -1551,6 +1559,14 @@ export class BaseSyncedStore {
1551
1559
  };
1552
1560
  return this._deltaPipelineContext;
1553
1561
  }
1562
+ /**
1563
+ * Lands persisted changes in the in-memory pool, with this store's
1564
+ * relation enrichment bound. The one apply path: the pipeline's bridge
1565
+ * (no plugins installed) and the `humans()` apply handler both call it.
1566
+ */
1567
+ applyChangesToPool(changes) {
1568
+ this.syncClient.applyDeltaBatchToPool(changes, (name, data) => this.enrichRelations(name, data));
1569
+ }
1554
1570
  /** Get fields that represent meaningful state for deduplication. Override for model-specific fields. */
1555
1571
  getStateFields(_modelName) {
1556
1572
  return ['status', 'state', 'isActive'];
@@ -1663,7 +1679,7 @@ export class BaseSyncedStore {
1663
1679
  }
1664
1680
  }
1665
1681
  if (totalCancelled > 0) {
1666
- getContext().logger.info('[BaseSyncedStore] Cascade cancelled orphaned transactions', {
1682
+ this.runtime.logger.info('[BaseSyncedStore] Cascade cancelled orphaned transactions', {
1667
1683
  parentModel: parentModelName,
1668
1684
  parentId: parentId.slice(0, 12),
1669
1685
  totalCancelled,
@@ -1843,13 +1859,13 @@ export class BaseSyncedStore {
1843
1859
  }
1844
1860
  /** Error handler for fire-and-forget flushPendingDeltas calls */
1845
1861
  handleFlushError = (error) => {
1846
- getContext().observability.captureMutationFailure({
1862
+ this.runtime.observability.captureMutationFailure({
1847
1863
  context: 'flush-pending-deltas',
1848
1864
  modelName: 'batch',
1849
1865
  modelId: 'batch',
1850
1866
  error: error instanceof Error ? error : new Error(String(error)),
1851
1867
  });
1852
- getContext().logger.debug('[BaseSyncedStore] Delta flush error', {
1868
+ this.runtime.logger.debug('[BaseSyncedStore] Delta flush error', {
1853
1869
  error: error instanceof Error ? error.message : String(error),
1854
1870
  });
1855
1871
  };
@@ -5,9 +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 { type DatabaseInfo, type WorkspaceMetadata } from './core/DatabaseManager.js';
8
+ import { type DatabaseInfo, type WorkspaceMetadata } from './stores/DatabaseManager.js';
9
9
  import { ModelRegistry } from './ModelRegistry.js';
10
10
  import { LoadStrategy } from './transaction/types/index.js';
11
+ import type { RuntimeContext } from './RuntimeContext.js';
12
+ import type { AppliedChange } from './transaction/plugin.js';
11
13
  import type { BootstrapFetcher, BootstrapData } from './sync/BootstrapFetcher.js';
12
14
  import { InMemoryObjectStore } from './adapters/inMemoryStorage.js';
13
15
  import type { SyncDeltaAction } from './transaction/wire/delta.js';
@@ -68,12 +70,7 @@ export interface BootstrapResult {
68
70
  * was disconnected — without this, DELETE deltas persist to IDB but
69
71
  * ghost entities linger in the pool until a full reload.
70
72
  */
71
- deltaResults?: {
72
- action: 'add' | 'update' | 'remove' | 'archive' | 'verify';
73
- modelName: string;
74
- modelId: string;
75
- data?: ModelData | null;
76
- }[];
73
+ deltaResults?: AppliedChange[];
77
74
  }
78
75
  export declare class Database {
79
76
  private databaseManager;
@@ -115,12 +112,14 @@ export declare class Database {
115
112
  * new Database(registry, bootstrap, { inMemory: true })
116
113
  */
117
114
  private readonly inMemory;
115
+ private readonly runtime;
118
116
  /** In-memory stores used when inMemory=true. Keyed by model name. */
119
117
  private inMemoryStores;
120
118
  /** In-memory workspace metadata when inMemory=true. */
121
119
  private inMemoryMetadata;
122
120
  constructor(modelRegistry: ModelRegistry, bootstrapHelper: BootstrapFetcher, options?: {
123
121
  inMemory?: boolean;
122
+ runtime?: RuntimeContext;
124
123
  });
125
124
  /**
126
125
  * Get store for a model, or `undefined` if no store exists.
@@ -196,12 +195,7 @@ export declare class Database {
196
195
  modelName: string;
197
196
  modelId: string;
198
197
  data: ModelData | null;
199
- }): Promise<{
200
- action: 'add' | 'update' | 'remove' | 'archive' | 'verify';
201
- modelName: string;
202
- modelId: string;
203
- data?: ModelData | null;
204
- }>;
198
+ }): Promise<AppliedChange>;
205
199
  /**
206
200
  * Apply many deltas to the local store in as few IndexedDB transactions as
207
201
  * possible. Deltas are grouped by store, and each store's writes commit in a
@@ -240,13 +234,7 @@ export declare class Database {
240
234
  */
241
235
  transactionId?: string;
242
236
  }[]): Promise<{
243
- results: {
244
- action: 'add' | 'update' | 'remove' | 'archive' | 'verify';
245
- modelName: string;
246
- modelId: string;
247
- data?: ModelData | null;
248
- transactionId?: string;
249
- }[];
237
+ results: AppliedChange[];
250
238
  /**
251
239
  * Highest syncId whose IDB store transaction actually committed in this
252
240
  * batch. The runtime delta cursor (WS `lastSyncId`, server-side