@abloatai/ablo 0.3.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 (278) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +12 -0
  4. package/README.md +230 -0
  5. package/dist/BaseSyncedStore.d.ts +709 -0
  6. package/dist/BaseSyncedStore.js +1843 -0
  7. package/dist/Database.d.ts +344 -0
  8. package/dist/Database.js +1259 -0
  9. package/dist/LazyReferenceCollection.d.ts +181 -0
  10. package/dist/LazyReferenceCollection.js +460 -0
  11. package/dist/Model.d.ts +339 -0
  12. package/dist/Model.js +715 -0
  13. package/dist/ModelRegistry.d.ts +200 -0
  14. package/dist/ModelRegistry.js +535 -0
  15. package/dist/NetworkMonitor.d.ts +27 -0
  16. package/dist/NetworkMonitor.js +73 -0
  17. package/dist/ObjectPool.d.ts +202 -0
  18. package/dist/ObjectPool.js +1106 -0
  19. package/dist/SyncClient.d.ts +489 -0
  20. package/dist/SyncClient.js +1555 -0
  21. package/dist/SyncEngineContext.d.ts +46 -0
  22. package/dist/SyncEngineContext.js +74 -0
  23. package/dist/adapters/alwaysOnline.d.ts +16 -0
  24. package/dist/adapters/alwaysOnline.js +19 -0
  25. package/dist/adapters/inMemoryStorage.d.ts +30 -0
  26. package/dist/adapters/inMemoryStorage.js +94 -0
  27. package/dist/agent/Agent.d.ts +358 -0
  28. package/dist/agent/Agent.js +500 -0
  29. package/dist/agent/index.d.ts +115 -0
  30. package/dist/agent/index.js +128 -0
  31. package/dist/agent/session.d.ts +90 -0
  32. package/dist/agent/session.js +156 -0
  33. package/dist/agent/types.d.ts +73 -0
  34. package/dist/agent/types.js +10 -0
  35. package/dist/ai-sdk/coordination-context.d.ts +51 -0
  36. package/dist/ai-sdk/coordination-context.js +107 -0
  37. package/dist/ai-sdk/index.d.ts +68 -0
  38. package/dist/ai-sdk/index.js +68 -0
  39. package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
  40. package/dist/ai-sdk/intent-broadcast.js +72 -0
  41. package/dist/ai-sdk/wrap.d.ts +67 -0
  42. package/dist/ai-sdk/wrap.js +45 -0
  43. package/dist/api/index.d.ts +10 -0
  44. package/dist/api/index.js +9 -0
  45. package/dist/auth/index.d.ts +137 -0
  46. package/dist/auth/index.js +246 -0
  47. package/dist/client/Ablo.d.ts +835 -0
  48. package/dist/client/Ablo.js +1440 -0
  49. package/dist/client/ApiClient.d.ts +200 -0
  50. package/dist/client/ApiClient.js +659 -0
  51. package/dist/client/auth.d.ts +79 -0
  52. package/dist/client/auth.js +81 -0
  53. package/dist/client/createInternalComponents.d.ts +44 -0
  54. package/dist/client/createInternalComponents.js +88 -0
  55. package/dist/client/createModelProxy.d.ts +152 -0
  56. package/dist/client/createModelProxy.js +199 -0
  57. package/dist/client/identity.d.ts +63 -0
  58. package/dist/client/identity.js +156 -0
  59. package/dist/client/index.d.ts +36 -0
  60. package/dist/client/index.js +33 -0
  61. package/dist/client/persistence.d.ts +7 -0
  62. package/dist/client/persistence.js +11 -0
  63. package/dist/client/validateAbloOptions.d.ts +42 -0
  64. package/dist/client/validateAbloOptions.js +43 -0
  65. package/dist/config/index.d.ts +10 -0
  66. package/dist/config/index.js +12 -0
  67. package/dist/context.d.ts +27 -0
  68. package/dist/context.js +58 -0
  69. package/dist/core/DatabaseManager.d.ts +108 -0
  70. package/dist/core/DatabaseManager.js +361 -0
  71. package/dist/core/QueryProcessor.d.ts +77 -0
  72. package/dist/core/QueryProcessor.js +262 -0
  73. package/dist/core/QueryView.d.ts +64 -0
  74. package/dist/core/QueryView.js +219 -0
  75. package/dist/core/StoreManager.d.ts +131 -0
  76. package/dist/core/StoreManager.js +334 -0
  77. package/dist/core/ViewRegistry.d.ts +20 -0
  78. package/dist/core/ViewRegistry.js +55 -0
  79. package/dist/core/index.d.ts +34 -0
  80. package/dist/core/index.js +59 -0
  81. package/dist/core/openIDBWithTimeout.d.ts +27 -0
  82. package/dist/core/openIDBWithTimeout.js +63 -0
  83. package/dist/core/query-utils.d.ts +37 -0
  84. package/dist/core/query-utils.js +60 -0
  85. package/dist/errors.d.ts +235 -0
  86. package/dist/errors.js +243 -0
  87. package/dist/index.d.ts +41 -0
  88. package/dist/index.js +82 -0
  89. package/dist/interfaces/headless.d.ts +95 -0
  90. package/dist/interfaces/headless.js +41 -0
  91. package/dist/interfaces/index.d.ts +321 -0
  92. package/dist/interfaces/index.js +8 -0
  93. package/dist/mutators/RecordingTransaction.d.ts +36 -0
  94. package/dist/mutators/RecordingTransaction.js +216 -0
  95. package/dist/mutators/Transaction.d.ts +48 -0
  96. package/dist/mutators/Transaction.js +64 -0
  97. package/dist/mutators/UndoManager.d.ts +114 -0
  98. package/dist/mutators/UndoManager.js +143 -0
  99. package/dist/mutators/defineMutators.d.ts +55 -0
  100. package/dist/mutators/defineMutators.js +28 -0
  101. package/dist/policy/index.d.ts +19 -0
  102. package/dist/policy/index.js +18 -0
  103. package/dist/policy/types.d.ts +74 -0
  104. package/dist/policy/types.js +17 -0
  105. package/dist/principal.d.ts +44 -0
  106. package/dist/principal.js +49 -0
  107. package/dist/query/client.d.ts +43 -0
  108. package/dist/query/client.js +84 -0
  109. package/dist/query/index.d.ts +6 -0
  110. package/dist/query/index.js +5 -0
  111. package/dist/query/types.d.ts +143 -0
  112. package/dist/query/types.js +36 -0
  113. package/dist/react/AbloProvider.d.ts +205 -0
  114. package/dist/react/AbloProvider.js +398 -0
  115. package/dist/react/ClientSideSuspense.d.ts +36 -0
  116. package/dist/react/ClientSideSuspense.js +17 -0
  117. package/dist/react/DefaultFallback.d.ts +24 -0
  118. package/dist/react/DefaultFallback.js +43 -0
  119. package/dist/react/SyncGroupProvider.d.ts +19 -0
  120. package/dist/react/SyncGroupProvider.js +44 -0
  121. package/dist/react/context.d.ts +161 -0
  122. package/dist/react/context.js +35 -0
  123. package/dist/react/index.d.ts +64 -0
  124. package/dist/react/index.js +73 -0
  125. package/dist/react/internalContext.d.ts +35 -0
  126. package/dist/react/internalContext.js +3 -0
  127. package/dist/react/useAblo.d.ts +72 -0
  128. package/dist/react/useAblo.js +63 -0
  129. package/dist/react/useCurrentUserId.d.ts +21 -0
  130. package/dist/react/useCurrentUserId.js +33 -0
  131. package/dist/react/useErrorListener.d.ts +20 -0
  132. package/dist/react/useErrorListener.js +39 -0
  133. package/dist/react/useIntent.d.ts +29 -0
  134. package/dist/react/useIntent.js +42 -0
  135. package/dist/react/useMutate.d.ts +83 -0
  136. package/dist/react/useMutate.js +122 -0
  137. package/dist/react/useMutationFailureListener.d.ts +26 -0
  138. package/dist/react/useMutationFailureListener.js +38 -0
  139. package/dist/react/useMutators.d.ts +56 -0
  140. package/dist/react/useMutators.js +66 -0
  141. package/dist/react/usePresence.d.ts +32 -0
  142. package/dist/react/usePresence.js +41 -0
  143. package/dist/react/useQuery.d.ts +123 -0
  144. package/dist/react/useQuery.js +145 -0
  145. package/dist/react/useReactive.d.ts +35 -0
  146. package/dist/react/useReactive.js +111 -0
  147. package/dist/react/useReader.d.ts +69 -0
  148. package/dist/react/useReader.js +73 -0
  149. package/dist/react/useSyncStatus.d.ts +61 -0
  150. package/dist/react/useSyncStatus.js +76 -0
  151. package/dist/react/useUndoScope.d.ts +36 -0
  152. package/dist/react/useUndoScope.js +73 -0
  153. package/dist/realtime/index.d.ts +10 -0
  154. package/dist/realtime/index.js +9 -0
  155. package/dist/schema/field.d.ts +134 -0
  156. package/dist/schema/field.js +264 -0
  157. package/dist/schema/index.d.ts +29 -0
  158. package/dist/schema/index.js +38 -0
  159. package/dist/schema/model.d.ts +326 -0
  160. package/dist/schema/model.js +89 -0
  161. package/dist/schema/queries.d.ts +203 -0
  162. package/dist/schema/queries.js +145 -0
  163. package/dist/schema/relation.d.ts +172 -0
  164. package/dist/schema/relation.js +104 -0
  165. package/dist/schema/schema.d.ts +259 -0
  166. package/dist/schema/schema.js +188 -0
  167. package/dist/schema/sugar.d.ts +129 -0
  168. package/dist/schema/sugar.js +94 -0
  169. package/dist/source/index.d.ts +423 -0
  170. package/dist/source/index.js +320 -0
  171. package/dist/source/pushQueue.d.ts +112 -0
  172. package/dist/source/pushQueue.js +249 -0
  173. package/dist/stores/ObjectStore.d.ts +103 -0
  174. package/dist/stores/ObjectStore.js +371 -0
  175. package/dist/stores/ObjectStoreContract.d.ts +39 -0
  176. package/dist/stores/ObjectStoreContract.js +1 -0
  177. package/dist/stores/SyncActionStore.d.ts +101 -0
  178. package/dist/stores/SyncActionStore.js +481 -0
  179. package/dist/sync/BootstrapHelper.d.ts +127 -0
  180. package/dist/sync/BootstrapHelper.js +434 -0
  181. package/dist/sync/ConnectionManager.d.ts +136 -0
  182. package/dist/sync/ConnectionManager.js +465 -0
  183. package/dist/sync/HydrationCoordinator.d.ts +137 -0
  184. package/dist/sync/HydrationCoordinator.js +468 -0
  185. package/dist/sync/NetworkProbe.d.ts +43 -0
  186. package/dist/sync/NetworkProbe.js +113 -0
  187. package/dist/sync/OfflineFlush.d.ts +9 -0
  188. package/dist/sync/OfflineFlush.js +22 -0
  189. package/dist/sync/OfflineTransactionStore.d.ts +37 -0
  190. package/dist/sync/OfflineTransactionStore.js +263 -0
  191. package/dist/sync/SyncWebSocket.d.ts +663 -0
  192. package/dist/sync/SyncWebSocket.js +1336 -0
  193. package/dist/sync/createIntentStream.d.ts +33 -0
  194. package/dist/sync/createIntentStream.js +243 -0
  195. package/dist/sync/createPresenceStream.d.ts +46 -0
  196. package/dist/sync/createPresenceStream.js +192 -0
  197. package/dist/sync/createSnapshot.d.ts +33 -0
  198. package/dist/sync/createSnapshot.js +124 -0
  199. package/dist/sync/participants.d.ts +114 -0
  200. package/dist/sync/participants.js +336 -0
  201. package/dist/sync/schemas.d.ts +79 -0
  202. package/dist/sync/schemas.js +78 -0
  203. package/dist/testing/fixtures/bootstrap.d.ts +45 -0
  204. package/dist/testing/fixtures/bootstrap.js +53 -0
  205. package/dist/testing/fixtures/deltas.d.ts +86 -0
  206. package/dist/testing/fixtures/deltas.js +139 -0
  207. package/dist/testing/fixtures/models.d.ts +82 -0
  208. package/dist/testing/fixtures/models.js +270 -0
  209. package/dist/testing/helpers/react-wrapper.d.ts +66 -0
  210. package/dist/testing/helpers/react-wrapper.js +64 -0
  211. package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
  212. package/dist/testing/helpers/sync-engine-harness.js +70 -0
  213. package/dist/testing/helpers/wait.d.ts +25 -0
  214. package/dist/testing/helpers/wait.js +44 -0
  215. package/dist/testing/index.d.ts +21 -0
  216. package/dist/testing/index.js +32 -0
  217. package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
  218. package/dist/testing/mocks/MockMutationExecutor.js +139 -0
  219. package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
  220. package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
  221. package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
  222. package/dist/testing/mocks/MockSyncContext.js +100 -0
  223. package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
  224. package/dist/testing/mocks/MockSyncStore.js +171 -0
  225. package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
  226. package/dist/testing/mocks/MockWebSocket.js +117 -0
  227. package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
  228. package/dist/transactions/OptimisticEchoTracker.js +104 -0
  229. package/dist/transactions/TransactionQueue.d.ts +499 -0
  230. package/dist/transactions/TransactionQueue.js +1895 -0
  231. package/dist/transactions/index.d.ts +16 -0
  232. package/dist/transactions/index.js +7 -0
  233. package/dist/transactions/mutation-error-handler.d.ts +5 -0
  234. package/dist/transactions/mutation-error-handler.js +39 -0
  235. package/dist/types/global.d.ts +107 -0
  236. package/dist/types/global.js +38 -0
  237. package/dist/types/index.d.ts +241 -0
  238. package/dist/types/index.js +70 -0
  239. package/dist/types/streams.d.ts +495 -0
  240. package/dist/types/streams.js +11 -0
  241. package/dist/utils/asyncIterator.d.ts +41 -0
  242. package/dist/utils/asyncIterator.js +142 -0
  243. package/dist/utils/duration.d.ts +28 -0
  244. package/dist/utils/duration.js +47 -0
  245. package/dist/utils/mobx-setup.d.ts +42 -0
  246. package/dist/utils/mobx-setup.js +381 -0
  247. package/docs/api-keys.md +24 -0
  248. package/docs/api.md +230 -0
  249. package/docs/audit.md +81 -0
  250. package/docs/capabilities.md +163 -0
  251. package/docs/client-behavior.md +202 -0
  252. package/docs/data-sources.md +214 -0
  253. package/docs/examples/agent-human.md +84 -0
  254. package/docs/examples/ai-sdk-tool.md +92 -0
  255. package/docs/examples/existing-python-backend.md +249 -0
  256. package/docs/examples/nextjs.md +88 -0
  257. package/docs/examples/server-agent.md +86 -0
  258. package/docs/guarantees.md +148 -0
  259. package/docs/index.md +97 -0
  260. package/docs/integration-guide.md +493 -0
  261. package/docs/interaction-model.md +140 -0
  262. package/docs/mcp/claude-code.md +43 -0
  263. package/docs/mcp/cursor.md +53 -0
  264. package/docs/mcp/windsurf.md +46 -0
  265. package/docs/mcp.md +59 -0
  266. package/docs/quickstart.md +152 -0
  267. package/docs/react.md +115 -0
  268. package/docs/roadmap.md +45 -0
  269. package/examples/README.md +54 -0
  270. package/examples/data-source/README.md +102 -0
  271. package/examples/data-source/ablo-driver.ts +89 -0
  272. package/examples/data-source/customer-server.ts +208 -0
  273. package/examples/data-source/run.ts +101 -0
  274. package/examples/data-source/schema.ts +25 -0
  275. package/examples/quickstart.ts +54 -0
  276. package/examples/tsconfig.json +16 -0
  277. package/llms.txt +143 -0
  278. package/package.json +147 -0
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Linear Sync Engine - Lazy Reference Collection
3
+ *
4
+ * Efficient implementation of one-to-many relationships that loads
5
+ * data on-demand with intelligent caching and batching.
6
+ */
7
+ import type { Model } from './Model.js';
8
+ import { Database } from './Database.js';
9
+ import { ObjectPool } from './ObjectPool.js';
10
+ /**
11
+ * Options for LazyReferenceCollection behavior
12
+ */
13
+ export interface LazyCollectionOptions {
14
+ /** Skip network hydration if local data exists */
15
+ canSkipNetworkHydration?: () => boolean;
16
+ /** Custom filter for loaded items */
17
+ filter?: (item: any) => boolean;
18
+ /** Custom sort function */
19
+ sort?: (a: any, b: any) => number;
20
+ /** Maximum items to load */
21
+ limit?: number;
22
+ /** Enable automatic refresh on parent changes */
23
+ autoRefresh?: boolean;
24
+ }
25
+ /**
26
+ * LazyReferenceCollection - Lazy-loaded one-to-many relationships
27
+ *
28
+ * Key features:
29
+ * - Loads from IndexedDB first, then network if needed
30
+ * - Automatic batching to prevent N+1 queries
31
+ * - Observable for React integration
32
+ * - Memory efficient with intelligent caching
33
+ * - Support for filtering and sorting
34
+ */
35
+ export declare class LazyReferenceCollection<T extends Model> {
36
+ private modelName;
37
+ private parent;
38
+ private foreignKey;
39
+ private customQuery?;
40
+ private options;
41
+ /** Static dependencies - shared across all instances */
42
+ private static _database;
43
+ private static _objectPool;
44
+ /**
45
+ * Set global dependencies for all LazyReferenceCollection instances
46
+ * Called once during SyncedStore initialization
47
+ */
48
+ static setDependencies(database: Database, objectPool: ObjectPool): void;
49
+ /**
50
+ * Clear dependencies (e.g., on logout/store disposal)
51
+ */
52
+ static clearDependencies(): void;
53
+ /** Loaded items (null = not loaded, [] = loaded but empty) */
54
+ items: T[] | null;
55
+ /** Loading state */
56
+ isLoading: boolean;
57
+ /** Error state */
58
+ loadError: Error | null;
59
+ /**
60
+ * MobX observation tracking - prevents GC while React is observing this collection
61
+ * Following MobX best practice: https://mobx.js.org/lazy-observables.html
62
+ */
63
+ _isBeingObserved: boolean;
64
+ /** Promise for ongoing hydration */
65
+ private hydrationPromise;
66
+ /** Disposer for observation lifecycle hooks */
67
+ private observationDisposer;
68
+ /** Get database from static dependencies */
69
+ private get database();
70
+ /** Get objectPool from static dependencies */
71
+ private get objectPool();
72
+ constructor(modelName: string, parent: Model, foreignKey: string, customQuery?: any | undefined, options?: LazyCollectionOptions);
73
+ /**
74
+ * Set up MobX observation lifecycle hooks
75
+ * When React components observe this collection, we prevent GC of the parent model
76
+ */
77
+ private _setupObservationTracking;
78
+ /**
79
+ * Check if this collection is currently being observed by React/MobX
80
+ */
81
+ get isBeingObserved(): boolean;
82
+ /**
83
+ * Get the collection value (triggers hydration if needed).
84
+ *
85
+ * Filters out items whose id is no longer in the ObjectPool. The
86
+ * local `items` array isn't auto-synced with `pool.remove()` — a
87
+ * deleted entity would linger here until hydrate() re-runs on
88
+ * reload. Reading `pool.has(item.id)` inside this computed getter
89
+ * makes MobX track both `this.items` AND the pool's entries map,
90
+ * so any pool.remove invalidates the computed and re-renders the
91
+ * consumer with the deleted item gone.
92
+ *
93
+ * Without this, deleting a slide layer would pool.remove() cleanly
94
+ * but the canvas — which reads `slide.layers.value` — would keep
95
+ * showing the deleted layer until a full reload rebuilt the
96
+ * collection.
97
+ */
98
+ get value(): T[];
99
+ /**
100
+ * Check if collection has been loaded
101
+ */
102
+ get loaded(): boolean;
103
+ /**
104
+ * Check if collection is empty (only meaningful after loading)
105
+ */
106
+ get empty(): boolean;
107
+ /**
108
+ * Check if currently loading
109
+ */
110
+ get loading(): boolean;
111
+ /**
112
+ * Get load error if any
113
+ */
114
+ get error(): Error | null;
115
+ /**
116
+ * Get collection size
117
+ */
118
+ get size(): number;
119
+ /**
120
+ * Hydrate the collection from local storage and/or network
121
+ */
122
+ hydrate(): Promise<void>;
123
+ /**
124
+ * Internal hydration implementation
125
+ */
126
+ private _performHydration;
127
+ /**
128
+ * Load items from IndexedDB
129
+ */
130
+ private _loadFromLocal;
131
+ /**
132
+ * Refresh the collection (reload from network)
133
+ */
134
+ refresh(): Promise<void>;
135
+ /**
136
+ * Add an item to the collection
137
+ */
138
+ add(item: T): void;
139
+ /**
140
+ * Remove an item from the collection
141
+ */
142
+ remove(itemOrId: T | string): boolean;
143
+ /**
144
+ * Find an item in the collection
145
+ */
146
+ find(predicate: (item: T) => boolean): T | undefined;
147
+ /**
148
+ * Filter items in the collection
149
+ */
150
+ filter(predicate: (item: T) => boolean): T[];
151
+ /**
152
+ * Check if collection contains an item
153
+ */
154
+ contains(itemOrId: T | string): boolean;
155
+ /**
156
+ * Convert to array (triggers hydration)
157
+ */
158
+ toArray(): T[];
159
+ /**
160
+ * Set items directly (internal use)
161
+ */
162
+ private _setItems;
163
+ /**
164
+ * Set loading state (internal use)
165
+ */
166
+ private _setLoading;
167
+ /**
168
+ * Set error state (internal use)
169
+ */
170
+ private _setError;
171
+ /**
172
+ * Clear the collection
173
+ */
174
+ clear(): void;
175
+ /**
176
+ * Dispose of the collection (cleanup)
177
+ * Following MobX best practice: always clean up observation hooks
178
+ * See: https://github.com/mobxjs/mobx/issues/2047
179
+ */
180
+ dispose(): void;
181
+ }
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Linear Sync Engine - Lazy Reference Collection
3
+ *
4
+ * Efficient implementation of one-to-many relationships that loads
5
+ * data on-demand with intelligent caching and batching.
6
+ */
7
+ import { makeObservable, observable, action, computed, onBecomeObserved, onBecomeUnobserved, } from 'mobx';
8
+ import { getActiveRegistry } from './ModelRegistry.js';
9
+ import { AbloValidationError } from './errors.js';
10
+ /**
11
+ * LazyReferenceCollection - Lazy-loaded one-to-many relationships
12
+ *
13
+ * Key features:
14
+ * - Loads from IndexedDB first, then network if needed
15
+ * - Automatic batching to prevent N+1 queries
16
+ * - Observable for React integration
17
+ * - Memory efficient with intelligent caching
18
+ * - Support for filtering and sorting
19
+ */
20
+ export class LazyReferenceCollection {
21
+ modelName;
22
+ parent;
23
+ foreignKey;
24
+ customQuery;
25
+ options;
26
+ /** Static dependencies - shared across all instances */
27
+ static _database = null;
28
+ static _objectPool = null;
29
+ /**
30
+ * Set global dependencies for all LazyReferenceCollection instances
31
+ * Called once during SyncedStore initialization
32
+ */
33
+ static setDependencies(database, objectPool) {
34
+ LazyReferenceCollection._database = database;
35
+ LazyReferenceCollection._objectPool = objectPool;
36
+ getContext().logger.debug('LazyReferenceCollection dependencies set');
37
+ }
38
+ /**
39
+ * Clear dependencies (e.g., on logout/store disposal)
40
+ */
41
+ static clearDependencies() {
42
+ LazyReferenceCollection._database = null;
43
+ LazyReferenceCollection._objectPool = null;
44
+ }
45
+ /** Loaded items (null = not loaded, [] = loaded but empty) */
46
+ items = null;
47
+ /** Loading state */
48
+ isLoading = false;
49
+ /** Error state */
50
+ loadError = null;
51
+ /**
52
+ * MobX observation tracking - prevents GC while React is observing this collection
53
+ * Following MobX best practice: https://mobx.js.org/lazy-observables.html
54
+ */
55
+ _isBeingObserved = false;
56
+ /** Promise for ongoing hydration */
57
+ hydrationPromise = null;
58
+ /** Disposer for observation lifecycle hooks */
59
+ observationDisposer = null;
60
+ /** Get database from static dependencies */
61
+ get database() {
62
+ return LazyReferenceCollection._database;
63
+ }
64
+ /** Get objectPool from static dependencies */
65
+ get objectPool() {
66
+ return LazyReferenceCollection._objectPool;
67
+ }
68
+ constructor(modelName, parent, foreignKey, customQuery, options = {}) {
69
+ this.modelName = modelName;
70
+ this.parent = parent;
71
+ this.foreignKey = foreignKey;
72
+ this.customQuery = customQuery;
73
+ this.options = options;
74
+ makeObservable(this, {
75
+ items: observable,
76
+ isLoading: observable,
77
+ loadError: observable,
78
+ _isBeingObserved: observable,
79
+ hydrate: action,
80
+ refresh: action,
81
+ value: computed,
82
+ loaded: computed,
83
+ empty: computed,
84
+ loading: computed,
85
+ error: computed,
86
+ isBeingObserved: computed,
87
+ });
88
+ // Set up MobX observation lifecycle hooks
89
+ // This follows the official MobX pattern for lazy observables
90
+ // See: https://mobx.js.org/lazy-observables.html
91
+ this._setupObservationTracking();
92
+ }
93
+ /**
94
+ * Set up MobX observation lifecycle hooks
95
+ * When React components observe this collection, we prevent GC of the parent model
96
+ */
97
+ _setupObservationTracking() {
98
+ // Track when 'items' becomes observed (React component is rendering)
99
+ const disposeOnObserved = onBecomeObserved(this, 'items', () => {
100
+ this._isBeingObserved = true;
101
+ // Touch parent model to prevent GC while we're being observed
102
+ if (this.objectPool && this.parent?.id) {
103
+ this.objectPool.touch(this.parent.id);
104
+ }
105
+ // Register this collection with parent for observation tracking
106
+ if (this.parent) {
107
+ this.parent._registerObservedCollection(this);
108
+ }
109
+ });
110
+ // Track when 'items' stops being observed (component unmounted)
111
+ const disposeOnUnobserved = onBecomeUnobserved(this, 'items', () => {
112
+ this._isBeingObserved = false;
113
+ // Unregister from parent
114
+ if (this.parent) {
115
+ this.parent._unregisterObservedCollection(this);
116
+ }
117
+ });
118
+ // Store combined disposer for cleanup
119
+ this.observationDisposer = () => {
120
+ disposeOnObserved();
121
+ disposeOnUnobserved();
122
+ };
123
+ }
124
+ /**
125
+ * Check if this collection is currently being observed by React/MobX
126
+ */
127
+ get isBeingObserved() {
128
+ return this._isBeingObserved;
129
+ }
130
+ /**
131
+ * Get the collection value (triggers hydration if needed).
132
+ *
133
+ * Filters out items whose id is no longer in the ObjectPool. The
134
+ * local `items` array isn't auto-synced with `pool.remove()` — a
135
+ * deleted entity would linger here until hydrate() re-runs on
136
+ * reload. Reading `pool.has(item.id)` inside this computed getter
137
+ * makes MobX track both `this.items` AND the pool's entries map,
138
+ * so any pool.remove invalidates the computed and re-renders the
139
+ * consumer with the deleted item gone.
140
+ *
141
+ * Without this, deleting a slide layer would pool.remove() cleanly
142
+ * but the canvas — which reads `slide.layers.value` — would keep
143
+ * showing the deleted layer until a full reload rebuilt the
144
+ * collection.
145
+ */
146
+ get value() {
147
+ // Touch parent model to prevent GC during active collection usage
148
+ if (this.objectPool && this.parent?.id) {
149
+ this.objectPool.touch(this.parent.id);
150
+ }
151
+ if (this.items === null && !this.isLoading) {
152
+ // Auto-hydrate on first access
153
+ this.hydrate().catch((error) => {
154
+ getContext().observability.breadcrumb('Auto-hydration failed', 'sync.database', 'warning', {
155
+ error: error instanceof Error ? error.message : String(error),
156
+ });
157
+ });
158
+ return []; // Return empty array while loading
159
+ }
160
+ const raw = this.items || [];
161
+ const pool = this.objectPool;
162
+ if (!pool || raw.length === 0)
163
+ return raw;
164
+ // Filter items still present in the pool. `pool.has(id)` reads the
165
+ // observable `entries` map — MobX tracks the dependency, so a
166
+ // subsequent `pool.remove(id)` re-runs this computed.
167
+ return raw.filter((item) => pool.has(item.id));
168
+ }
169
+ /**
170
+ * Check if collection has been loaded
171
+ */
172
+ get loaded() {
173
+ return this.items !== null;
174
+ }
175
+ /**
176
+ * Check if collection is empty (only meaningful after loading)
177
+ */
178
+ get empty() {
179
+ return this.loaded && this.items.length === 0;
180
+ }
181
+ /**
182
+ * Check if currently loading
183
+ */
184
+ get loading() {
185
+ return this.isLoading;
186
+ }
187
+ /**
188
+ * Get load error if any
189
+ */
190
+ get error() {
191
+ return this.loadError;
192
+ }
193
+ /**
194
+ * Get collection size
195
+ */
196
+ get size() {
197
+ // Touch parent model when accessing collection size
198
+ if (this.objectPool && this.parent?.id) {
199
+ this.objectPool.touch(this.parent.id);
200
+ }
201
+ return this.items?.length || 0;
202
+ }
203
+ /**
204
+ * Hydrate the collection from local storage and/or network
205
+ */
206
+ async hydrate() {
207
+ // Return existing hydration promise if already in progress
208
+ if (this.hydrationPromise) {
209
+ return this.hydrationPromise;
210
+ }
211
+ // Skip if already loaded
212
+ if (this.items !== null) {
213
+ return;
214
+ }
215
+ this.hydrationPromise = this._performHydration();
216
+ try {
217
+ await this.hydrationPromise;
218
+ }
219
+ finally {
220
+ this.hydrationPromise = null;
221
+ }
222
+ }
223
+ /**
224
+ * Internal hydration implementation
225
+ */
226
+ async _performHydration() {
227
+ this._setLoading(true);
228
+ this._setError(null);
229
+ try {
230
+ // Step 1: Try loading from IndexedDB
231
+ const localData = await this._loadFromLocal();
232
+ if (localData.length > 0) {
233
+ this._setItems(localData);
234
+ // Check if we can skip network hydration
235
+ if (this.options.canSkipNetworkHydration?.()) {
236
+ this._setLoading(false);
237
+ return;
238
+ }
239
+ }
240
+ }
241
+ catch (error) {
242
+ this._setError(error);
243
+ getContext().observability.breadcrumb('Failed to hydrate collection', 'sync.database', 'warning', {
244
+ parent: this.parent.getModelName(),
245
+ parentId: this.parent.id,
246
+ foreignKey: this.foreignKey,
247
+ error: error instanceof Error ? error.message : String(error),
248
+ });
249
+ }
250
+ finally {
251
+ this._setLoading(false);
252
+ }
253
+ }
254
+ /**
255
+ * Load items from IndexedDB
256
+ */
257
+ async _loadFromLocal() {
258
+ try {
259
+ if (!this.database) {
260
+ throw new AbloValidationError(`Database dependency not provided to LazyReferenceCollection for ${this.modelName}`, { code: 'lazy_ref_db_missing' });
261
+ }
262
+ if (!this.objectPool) {
263
+ throw new AbloValidationError(`ObjectPool dependency not provided to LazyReferenceCollection for ${this.modelName}`, { code: 'lazy_ref_pool_missing' });
264
+ }
265
+ const store = this.database.getStore(this.modelName);
266
+ const rawData = store ? await store.getAllFromIndex(this.foreignKey, this.parent.id) : [];
267
+ // Get model class from registry
268
+ const ModelClass = getActiveRegistry().getModelByName(this.modelName);
269
+ if (!ModelClass) {
270
+ getContext().observability.breadcrumb(`Model '${this.modelName}' not found in registry`, 'sync.database', 'error');
271
+ return [];
272
+ }
273
+ // Convert raw data to model instances
274
+ const models = [];
275
+ for (const data of rawData) {
276
+ const id = data.id;
277
+ // Skip malformed rows. Records from IDB are typed as
278
+ // `Record<string, unknown>` (the centralized
279
+ // `ObjectStoreContract` shape) so the `id` field is
280
+ // narrow-checked here rather than assumed.
281
+ if (typeof id !== 'string')
282
+ continue;
283
+ // Check if already in ObjectPool
284
+ let model = this.objectPool.get(id);
285
+ if (!model) {
286
+ // Create new model instance
287
+ model = new ModelClass();
288
+ model.updateFromData(data);
289
+ this.objectPool.add(model);
290
+ }
291
+ if (model) {
292
+ models.push(model);
293
+ }
294
+ }
295
+ // Apply filtering if specified
296
+ let filteredModels = models;
297
+ if (this.options.filter) {
298
+ filteredModels = models.filter(this.options.filter);
299
+ }
300
+ // Apply sorting if specified
301
+ if (this.options.sort) {
302
+ filteredModels.sort(this.options.sort);
303
+ }
304
+ // Apply limit if specified
305
+ if (this.options.limit) {
306
+ filteredModels = filteredModels.slice(0, this.options.limit);
307
+ }
308
+ getContext().logger.debug('Loaded local items for collection', {
309
+ count: filteredModels.length,
310
+ parent: this.parent.getModelName(),
311
+ id: this.parent.id,
312
+ fk: this.foreignKey,
313
+ });
314
+ return filteredModels;
315
+ }
316
+ catch (error) {
317
+ getContext().observability.breadcrumb('Failed to load from local', 'sync.database', 'warning', {
318
+ error: error instanceof Error ? error.message : String(error),
319
+ });
320
+ return [];
321
+ }
322
+ }
323
+ /**
324
+ * Refresh the collection (reload from network)
325
+ */
326
+ async refresh() {
327
+ this.items = null;
328
+ this.hydrationPromise = null;
329
+ await this.hydrate();
330
+ getContext().logger.debug('Refreshed collection', {
331
+ parent: this.parent.getModelName(),
332
+ id: this.parent.id,
333
+ fk: this.foreignKey,
334
+ });
335
+ }
336
+ /**
337
+ * Add an item to the collection
338
+ */
339
+ add(item) {
340
+ if (this.items === null) {
341
+ this.items = [];
342
+ }
343
+ // Check if item already exists
344
+ const existingIndex = this.items.findIndex((existing) => existing.id === item.id);
345
+ if (existingIndex >= 0) {
346
+ // Replace existing item
347
+ this.items[existingIndex] = item;
348
+ }
349
+ else {
350
+ // Add new item
351
+ this.items.push(item);
352
+ // Apply sorting if specified
353
+ if (this.options.sort) {
354
+ this.items.sort(this.options.sort);
355
+ }
356
+ }
357
+ getContext().logger.debug('Added item to collection', { model: item.getModelName(), id: item.id });
358
+ }
359
+ /**
360
+ * Remove an item from the collection
361
+ */
362
+ remove(itemOrId) {
363
+ if (this.items === null)
364
+ return false;
365
+ const id = typeof itemOrId === 'string' ? itemOrId : itemOrId.id;
366
+ const index = this.items.findIndex((item) => item.id === id);
367
+ if (index >= 0) {
368
+ this.items.splice(index, 1);
369
+ getContext().logger.debug('Removed item from collection', { id });
370
+ return true;
371
+ }
372
+ return false;
373
+ }
374
+ /**
375
+ * Find an item in the collection
376
+ */
377
+ find(predicate) {
378
+ // Touch parent model when searching collection
379
+ if (this.objectPool && this.parent?.id) {
380
+ this.objectPool.touch(this.parent.id);
381
+ }
382
+ if (this.items === null)
383
+ return undefined;
384
+ return this.items.find(predicate);
385
+ }
386
+ /**
387
+ * Filter items in the collection
388
+ */
389
+ filter(predicate) {
390
+ // Touch parent model when filtering collection
391
+ if (this.objectPool && this.parent?.id) {
392
+ this.objectPool.touch(this.parent.id);
393
+ }
394
+ if (this.items === null)
395
+ return [];
396
+ return this.items.filter(predicate);
397
+ }
398
+ /**
399
+ * Check if collection contains an item
400
+ */
401
+ contains(itemOrId) {
402
+ if (this.items === null)
403
+ return false;
404
+ const id = typeof itemOrId === 'string' ? itemOrId : itemOrId.id;
405
+ return this.items.some((item) => item.id === id);
406
+ }
407
+ /**
408
+ * Convert to array (triggers hydration)
409
+ */
410
+ toArray() {
411
+ return this.value;
412
+ }
413
+ /**
414
+ * Set items directly (internal use)
415
+ */
416
+ _setItems(items) {
417
+ this.items = items;
418
+ }
419
+ /**
420
+ * Set loading state (internal use)
421
+ */
422
+ _setLoading(loading) {
423
+ this.isLoading = loading;
424
+ }
425
+ /**
426
+ * Set error state (internal use)
427
+ */
428
+ _setError(error) {
429
+ this.loadError = error;
430
+ }
431
+ /**
432
+ * Clear the collection
433
+ */
434
+ clear() {
435
+ this.items = [];
436
+ this.loadError = null;
437
+ }
438
+ /**
439
+ * Dispose of the collection (cleanup)
440
+ * Following MobX best practice: always clean up observation hooks
441
+ * See: https://github.com/mobxjs/mobx/issues/2047
442
+ */
443
+ dispose() {
444
+ // Clean up MobX observation hooks first
445
+ if (this.observationDisposer) {
446
+ this.observationDisposer();
447
+ this.observationDisposer = null;
448
+ }
449
+ // Unregister from parent if still registered
450
+ if (this._isBeingObserved && this.parent) {
451
+ this.parent._unregisterObservedCollection(this);
452
+ }
453
+ this._isBeingObserved = false;
454
+ this.items = null;
455
+ this.hydrationPromise = null;
456
+ this.loadError = null;
457
+ this.isLoading = false;
458
+ }
459
+ }
460
+ import { getContext } from './context.js';