@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
package/dist/Model.js ADDED
@@ -0,0 +1,715 @@
1
+ /**
2
+ * Model - Clean base class for domain models
3
+ *
4
+ * Models are pure domain objects that:
5
+ * - Hold data and business logic
6
+ * - Track their own changes
7
+ * - Validate themselves
8
+ * - Return updates/changes (not perform them)
9
+ *
10
+ * Models do NOT:
11
+ * - Access stores or singletons
12
+ * - Perform side effects (saving, notifications)
13
+ * - Know about sync infrastructure
14
+ */
15
+ import { runInAction, isComputedProp } from 'mobx';
16
+ import { v4 as uuid } from 'uuid';
17
+ import { M1 } from './utils/mobx-setup.js';
18
+ import { getActiveRegistry, hasActiveRegistry } from './ModelRegistry.js';
19
+ import { getContext } from './context.js';
20
+ import { AbloValidationError } from './errors.js';
21
+ /**
22
+ * Validation error for model validation failures
23
+ */
24
+ export class ValidationError extends Error {
25
+ errors;
26
+ constructor(errors) {
27
+ super(`Validation failed: ${errors.join(', ')}`);
28
+ this.errors = errors;
29
+ this.name = 'ValidationError';
30
+ }
31
+ }
32
+ /**
33
+ * Abstract Model - Base class for all domain models
34
+ *
35
+ * Pure domain object with no external dependencies
36
+ */
37
+ export class Model {
38
+ /** Static reference to active SyncedStore for reactive queries */
39
+ static store = null;
40
+ /** Unique identifier - always permanent UUID */
41
+ id;
42
+ /** Client ID - always equals id, kept for compatibility */
43
+ clientId;
44
+ /** MobX observable properties storage */
45
+ _mobxProperties = {};
46
+ /** Referenced models cache */
47
+ _referencedModels = {};
48
+ /** Track property changes */
49
+ modifiedProperties = new Map();
50
+ /** Track if this is a new model */
51
+ _isNew = true;
52
+ /** Original data snapshot */
53
+ _originalData;
54
+ /** Sync status */
55
+ syncStatus = 'pending';
56
+ /** Timestamps */
57
+ createdAt;
58
+ updatedAt;
59
+ archivedAt;
60
+ /** Validation rules */
61
+ validationRules = {};
62
+ /** Lifecycle state */
63
+ isDisposed = false;
64
+ disposers = [];
65
+ /**
66
+ * Track observed LazyReferenceCollections for GC prevention
67
+ * When any collection is being observed by React, the model should not be GC'd
68
+ * Following MobX best practice: https://mobx.js.org/lazy-observables.html
69
+ */
70
+ _observedCollections = new Set();
71
+ constructor(data = {}) {
72
+ // Always generate permanent UUID on client
73
+ this.id = data.id || Model.generateId();
74
+ this.clientId = this.id; // No more temp IDs!
75
+ // Ensure dates are Date objects, not strings
76
+ this.createdAt = data.createdAt
77
+ ? data.createdAt instanceof Date
78
+ ? data.createdAt
79
+ : new Date(data.createdAt)
80
+ : new Date();
81
+ this.updatedAt = data.updatedAt
82
+ ? data.updatedAt instanceof Date
83
+ ? data.updatedAt
84
+ : new Date(data.updatedAt)
85
+ : new Date();
86
+ this.syncStatus = data.syncStatus || 'pending';
87
+ }
88
+ /**
89
+ * Generate unique ID
90
+ */
91
+ static generateId() {
92
+ return uuid();
93
+ }
94
+ /**
95
+ * Set the active SyncedStore reference for reactive queries.
96
+ * Called once at engine initialization.
97
+ */
98
+ static setStore(store) {
99
+ Model.store = store;
100
+ }
101
+ /**
102
+ * Get the active SyncedStore reference for reactive queries.
103
+ *
104
+ * Returns `null` if no store has been registered yet (e.g. during
105
+ * bootstrap before the engine is ready). Subclasses should use this
106
+ * instead of reaching into the private static field via bracket
107
+ * notation — the generic parameter lets app-side Model subclasses
108
+ * narrow the return to their concrete store type.
109
+ *
110
+ * @example
111
+ * // In a Slide model getter
112
+ * const store = Slide.getStore();
113
+ * if (!store) return [];
114
+ * return store.getByForeignKey<SlideLayer>('SlideLayer', 'slideId', this.id);
115
+ */
116
+ static getStore() {
117
+ return Model.store;
118
+ }
119
+ /**
120
+ * Initialize MobX observability
121
+ */
122
+ makeObservable() {
123
+ const modelName = this.getModelName();
124
+ // Get metadata from static ModelRegistry
125
+ const propertyMetadata = getActiveRegistry().getProperties(modelName);
126
+ const referenceMetadata = getActiveRegistry().getReferences(modelName);
127
+ // Use M1 for observability setup
128
+ M1(this, propertyMetadata, referenceMetadata);
129
+ }
130
+ /**
131
+ * Track property changes
132
+ */
133
+ propertyChanged(propertyName, oldValue, newValue) {
134
+ if (oldValue === newValue)
135
+ return;
136
+ runInAction(() => {
137
+ // Preserve the earliest captured `old` for this field until the entry
138
+ // is cleared (by `clearChanges` on sync-ack or by a mutator consuming
139
+ // it). Consecutive in-place mutations between mutator invocations —
140
+ // e.g. a drag loop writing `layer.position = ...` on every frame —
141
+ // would otherwise overwrite `.old` with each frame's predecessor,
142
+ // destroying the pre-session baseline that `RecordingTransaction`
143
+ // relies on to record a correct undo inverse. `.new` always reflects
144
+ // the latest value so the transaction queue's `getChanges()` keeps
145
+ // sending the right payload to the server.
146
+ const existing = this.modifiedProperties.get(propertyName);
147
+ this.modifiedProperties.set(propertyName, {
148
+ old: existing ? existing.old : oldValue,
149
+ new: newValue,
150
+ });
151
+ this.updatedAt = new Date();
152
+ });
153
+ }
154
+ /**
155
+ * Get changes as object
156
+ */
157
+ getChanges() {
158
+ const changes = {};
159
+ for (const [propertyName, change] of this.modifiedProperties) {
160
+ changes[propertyName] = change.new;
161
+ }
162
+ return changes;
163
+ }
164
+ /**
165
+ * Check if model has changes
166
+ */
167
+ get hasChanges() {
168
+ return this.modifiedProperties.size > 0;
169
+ }
170
+ /**
171
+ * Mark model as persisted (not new)
172
+ */
173
+ markAsPersisted() {
174
+ this._isNew = false;
175
+ this._originalData = this.captureSnapshot();
176
+ }
177
+ /**
178
+ * Check if this is a new model
179
+ */
180
+ isNew() {
181
+ return this._isNew;
182
+ }
183
+ /**
184
+ * Read-only view of the snapshot taken at `markAsPersisted()` /
185
+ * load. Used by recording-transaction undo to derive a pre-session
186
+ * baseline for fields that weren't yet pre-mutated (so
187
+ * `modifiedProperties` has no entry for them). Returns the same
188
+ * underlying object — callers must not mutate it.
189
+ *
190
+ * Architectural note: this method exists because we allow direct
191
+ * property writes (`slide.title = 'foo'`) AND mutator-recorded
192
+ * writes to coexist. Zero / Replicache structurally avoids this:
193
+ * every mutation MUST go through a registered mutator function,
194
+ * mutator args are serialized, and on server pull all unacked
195
+ * mutations are dropped and the mutator functions are replayed on
196
+ * the new basis (rebase). That makes per-instance baselines
197
+ * unnecessary because the b-tree at the new basis IS the
198
+ * authoritative pre-session state.
199
+ *
200
+ * If we ever migrate to "mutators are the only write path," this
201
+ * snapshot field, `_originalData`, and most of
202
+ * `RecordingTransaction.snapshotFields` become dead code. See
203
+ * `packages/replicache/src/db/rebase.ts` (rocicorp/mono) for the
204
+ * pattern.
205
+ */
206
+ getOriginalSnapshot() {
207
+ return this._originalData;
208
+ }
209
+ /**
210
+ * Clear tracked changes
211
+ */
212
+ clearChanges() {
213
+ runInAction(() => {
214
+ this.modifiedProperties.clear();
215
+ this._originalData = this.captureSnapshot();
216
+ });
217
+ }
218
+ /**
219
+ * Validate model
220
+ */
221
+ validate() {
222
+ if (this.isDisposed) {
223
+ throw new AbloValidationError('Cannot validate disposed model', {
224
+ code: 'model_disposed',
225
+ });
226
+ }
227
+ const errors = [];
228
+ const modelName = this.getModelName();
229
+ const properties = getActiveRegistry().getProperties(modelName);
230
+ if (properties) {
231
+ const json = this.toJSON();
232
+ for (const [propName, metadata] of properties) {
233
+ // Check required fields
234
+ if (!metadata.nullable && !metadata.optional) {
235
+ const value = json[propName];
236
+ if (value == null || value === '') {
237
+ errors.push(`${propName} is required`);
238
+ }
239
+ }
240
+ // Run custom validation rules
241
+ const rules = this.validationRules[propName];
242
+ if (rules) {
243
+ const value = json[propName];
244
+ for (const rule of rules) {
245
+ const error = rule(value);
246
+ if (error)
247
+ errors.push(error);
248
+ }
249
+ }
250
+ }
251
+ }
252
+ // Run model-specific validation
253
+ const customErrors = this.customValidate();
254
+ errors.push(...customErrors);
255
+ return errors;
256
+ }
257
+ /**
258
+ * Override for custom validation
259
+ */
260
+ customValidate() {
261
+ return [];
262
+ }
263
+ /**
264
+ * Add validation rule
265
+ */
266
+ addValidationRule(propName, rule) {
267
+ if (!this.validationRules[propName]) {
268
+ this.validationRules[propName] = [];
269
+ }
270
+ this.validationRules[propName].push(rule);
271
+ }
272
+ /**
273
+ * Prepare save operation
274
+ * Returns the changes to be saved without side effects
275
+ */
276
+ prepareSave() {
277
+ if (this.isDisposed) {
278
+ throw new AbloValidationError('Cannot prepare save for disposed model', {
279
+ code: 'model_disposed',
280
+ });
281
+ }
282
+ // Validate first
283
+ const errors = this.validate();
284
+ if (errors.length > 0) {
285
+ throw new ValidationError(errors);
286
+ }
287
+ if (this._isNew) {
288
+ // New model - return create operation
289
+ return {
290
+ type: 'create',
291
+ modelName: this.getModelName(), // Use Prisma model name
292
+ modelId: this.id,
293
+ timestamp: new Date(),
294
+ };
295
+ }
296
+ else if (this.hasChanges) {
297
+ // Existing model with changes - return update operation
298
+ return {
299
+ type: 'update',
300
+ modelName: this.getModelName(), // Use Prisma model name
301
+ modelId: this.id,
302
+ changes: new Map(this.modifiedProperties),
303
+ timestamp: new Date(),
304
+ };
305
+ }
306
+ // No changes
307
+ return null;
308
+ }
309
+ /**
310
+ * Prepare delete operation
311
+ */
312
+ prepareDelete() {
313
+ if (this.isDisposed) {
314
+ throw new AbloValidationError('Cannot prepare delete for disposed model', {
315
+ code: 'model_disposed',
316
+ });
317
+ }
318
+ this.willDelete();
319
+ return {
320
+ type: 'delete',
321
+ modelName: this.getModelName(), // Use Prisma model name
322
+ modelId: this.id,
323
+ timestamp: new Date(),
324
+ };
325
+ }
326
+ /**
327
+ * Prepare archive operation
328
+ */
329
+ prepareArchive() {
330
+ if (this.isDisposed) {
331
+ throw new AbloValidationError('Cannot prepare archive for disposed model', {
332
+ code: 'model_disposed',
333
+ });
334
+ }
335
+ this.archivedAt = new Date();
336
+ return {
337
+ type: 'archive',
338
+ modelName: this.getModelName(), // Use Prisma model name
339
+ modelId: this.id,
340
+ timestamp: new Date(),
341
+ };
342
+ }
343
+ /**
344
+ * Prepare unarchive operation
345
+ */
346
+ prepareUnarchive() {
347
+ if (this.isDisposed) {
348
+ throw new AbloValidationError('Cannot prepare unarchive for disposed model', {
349
+ code: 'model_disposed',
350
+ });
351
+ }
352
+ this.archivedAt = null;
353
+ return {
354
+ type: 'unarchive',
355
+ modelName: this.getModelName(), // Use Prisma model name
356
+ modelId: this.id,
357
+ timestamp: new Date(),
358
+ };
359
+ }
360
+ /**
361
+ * Update from raw data (hydration)
362
+ */
363
+ updateFromData(data) {
364
+ if (this.isDisposed) {
365
+ throw new AbloValidationError('Cannot update disposed model', {
366
+ code: 'model_disposed',
367
+ });
368
+ }
369
+ runInAction(() => {
370
+ // Temporarily disable change tracking
371
+ const originalTracking = this.modifiedProperties;
372
+ this.modifiedProperties = new Map();
373
+ // Update properties with safety checks for read-only/computed accessors
374
+ for (const [key, raw] of Object.entries(data)) {
375
+ if (key === 'id')
376
+ continue;
377
+ // Only attempt to set if the property exists on instance or prototype
378
+ if (!(this.hasOwnProperty(key) || key in this))
379
+ continue;
380
+ // Never assign to MobX computed properties (they may expose a setter that throws)
381
+ try {
382
+ if (isComputedProp(this, key)) {
383
+ continue;
384
+ }
385
+ }
386
+ catch {
387
+ // If MobX internals are unavailable for some reason, fall back to descriptor checks below
388
+ }
389
+ // Resolve property descriptor from own or prototype chain
390
+ const ownDesc = Object.getOwnPropertyDescriptor(this, key);
391
+ let desc = ownDesc;
392
+ if (!desc) {
393
+ let proto = Object.getPrototypeOf(this);
394
+ while (proto && proto !== Object.prototype && !desc) {
395
+ desc = Object.getOwnPropertyDescriptor(proto, key);
396
+ proto = Object.getPrototypeOf(proto);
397
+ }
398
+ }
399
+ // Determine writability: allow if data descriptor writable, or accessor with setter
400
+ const writable = desc
401
+ ? ('writable' in desc && !!desc.writable) ||
402
+ ('set' in desc && typeof desc.set === 'function')
403
+ : true;
404
+ if (!writable) {
405
+ // Skip read-only accessor properties (getter-only)
406
+ continue;
407
+ }
408
+ // Handle date conversions
409
+ const value = (key === 'createdAt' || key === 'updatedAt' || key === 'archivedAt') && raw
410
+ ? new Date(raw)
411
+ : raw;
412
+ // Dynamic property assignment for hydration - use indexed access
413
+ this[key] = value;
414
+ }
415
+ // Restore change tracking
416
+ this.modifiedProperties = originalTracking;
417
+ });
418
+ // Mark as persisted if updating existing model
419
+ if (!this._isNew) {
420
+ this._originalData = this.captureSnapshot();
421
+ }
422
+ this.didUpdate();
423
+ }
424
+ /**
425
+ * Serialize to JSON
426
+ * This method should not trigger MobX reactions since it's used for serialization
427
+ * Returns Record<string, any> to allow subclass specialization with more specific return types
428
+ */
429
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
430
+ toJSON() {
431
+ const modelName = this.getModelName();
432
+ const properties = getActiveRegistry().getProperties(modelName);
433
+ const result = {
434
+ __class: this.getModelName(), // Use Prisma model name for consistency
435
+ __typename: this.getModelName(), // Also add __typename for GraphQL compatibility
436
+ id: this.id,
437
+ createdAt: this.createdAt?.toISOString(),
438
+ updatedAt: this.updatedAt?.toISOString(),
439
+ clientId: this.clientId,
440
+ syncStatus: this.syncStatus,
441
+ };
442
+ if (this.archivedAt !== undefined) {
443
+ result.archivedAt = this.archivedAt?.toISOString() || null;
444
+ }
445
+ if (properties) {
446
+ const self = this;
447
+ for (const [propName, metadata] of properties) {
448
+ // Skip certain types
449
+ if (metadata.type === 'ephemeralProperty')
450
+ continue;
451
+ if (metadata.type === 'referenceModel')
452
+ continue;
453
+ if (metadata.type === 'referenceCollection')
454
+ continue;
455
+ const value = self[propName];
456
+ if (value !== undefined) {
457
+ result[propName] = value;
458
+ }
459
+ }
460
+ }
461
+ return result;
462
+ }
463
+ /**
464
+ * Clone this model
465
+ */
466
+ clone() {
467
+ const Constructor = this.constructor;
468
+ const clone = new Constructor();
469
+ const data = this.toJSON();
470
+ delete data.id; // New ID for clone
471
+ delete data.createdAt;
472
+ delete data.updatedAt;
473
+ clone.updateFromData(data);
474
+ return clone;
475
+ }
476
+ getModelName() {
477
+ const registeredName = getActiveRegistry().getModelNameFromConstructor(this.constructor);
478
+ if (registeredName) {
479
+ return registeredName;
480
+ }
481
+ const className = this.constructor.name;
482
+ // Use consumer-provided fallback map from config (replaces hardcoded Prisma name map)
483
+ const fallbackMap = getContext().config.classNameFallbackMap;
484
+ return fallbackMap[className] || className.replace(/Model$/, '');
485
+ }
486
+ /**
487
+ * Read a field value by name. Runtime-safe dynamic field access —
488
+ * schema-generated models store all declared fields as instance properties.
489
+ * Use this for generic code (sort comparators, filter predicates that work
490
+ * across model types) that reads fields by name string.
491
+ */
492
+ getField(name) {
493
+ return Reflect.get(this, name);
494
+ }
495
+ /**
496
+ * Check equality
497
+ */
498
+ equals(other) {
499
+ return this.id === other.id && this.constructor === other.constructor;
500
+ }
501
+ /**
502
+ * String representation
503
+ */
504
+ toString() {
505
+ return `${this.constructor.name}[${this.id}]`;
506
+ }
507
+ // ==========================================
508
+ // MobX Observation Tracking (for GC prevention)
509
+ // ==========================================
510
+ /**
511
+ * Register a LazyReferenceCollection as being observed
512
+ * Called by LazyReferenceCollection when onBecomeObserved fires
513
+ */
514
+ _registerObservedCollection(collection) {
515
+ this._observedCollections.add(collection);
516
+ }
517
+ /**
518
+ * Unregister a LazyReferenceCollection that's no longer observed
519
+ * Called by LazyReferenceCollection when onBecomeUnobserved fires
520
+ */
521
+ _unregisterObservedCollection(collection) {
522
+ this._observedCollections.delete(collection);
523
+ }
524
+ /**
525
+ * Check if any collection on this model is currently being observed by React
526
+ * Used by ObjectPool GC to prevent disposing models in active use
527
+ */
528
+ hasObservedCollections() {
529
+ return this._observedCollections.size > 0;
530
+ }
531
+ /**
532
+ * Get count of observed collections (for debugging)
533
+ */
534
+ get observedCollectionCount() {
535
+ return this._observedCollections.size;
536
+ }
537
+ /**
538
+ * Dispose model
539
+ */
540
+ dispose() {
541
+ if (this.isDisposed)
542
+ return;
543
+ // Clean up
544
+ for (const disposer of this.disposers) {
545
+ disposer();
546
+ }
547
+ this.disposers = [];
548
+ this._referencedModels = {};
549
+ this.modifiedProperties.clear();
550
+ this._observedCollections.clear();
551
+ // Dispose collections. Gracefully skip when no active registry
552
+ // exists — `dispose()` is a cleanup path and must not crash when a
553
+ // test (or a teardown during engine shutdown) calls it after the
554
+ // registry is gone. Production flows always have one set, so the
555
+ // collection-disposal branch still runs there.
556
+ if (hasActiveRegistry()) {
557
+ const modelName = this.getModelName();
558
+ const properties = getActiveRegistry().getProperties(modelName);
559
+ if (properties) {
560
+ const self = this;
561
+ for (const [propName, metadata] of properties) {
562
+ if (metadata.type === 'referenceCollection') {
563
+ const collection = self[propName];
564
+ if (collection?.dispose) {
565
+ collection.dispose();
566
+ }
567
+ }
568
+ }
569
+ }
570
+ }
571
+ this.isDisposed = true;
572
+ }
573
+ /**
574
+ * Check if disposed
575
+ */
576
+ get disposed() {
577
+ return this.isDisposed;
578
+ }
579
+ /**
580
+ * Lifecycle hooks - override in subclasses
581
+ */
582
+ didUpdate() { }
583
+ willDelete() { }
584
+ /**
585
+ * Capture snapshot for change detection
586
+ */
587
+ captureSnapshot() {
588
+ const snapshot = {};
589
+ const modelName = this.getModelName();
590
+ const properties = getActiveRegistry().getProperties(modelName);
591
+ if (properties) {
592
+ const json = this.toJSON();
593
+ for (const [propName] of properties) {
594
+ snapshot[propName] = json[propName];
595
+ }
596
+ }
597
+ return snapshot;
598
+ }
599
+ /**
600
+ * Get field changes for activity tracking
601
+ */
602
+ getFieldChanges() {
603
+ const changes = [];
604
+ for (const [field, change] of this.modifiedProperties) {
605
+ changes.push({
606
+ field,
607
+ oldValue: change.old,
608
+ newValue: change.new,
609
+ fieldType: this.getFieldType(change.new),
610
+ });
611
+ }
612
+ return changes;
613
+ }
614
+ getFieldType(value) {
615
+ if (value === null || value === undefined)
616
+ return 'string';
617
+ if (typeof value === 'number')
618
+ return 'number';
619
+ if (value instanceof Date)
620
+ return 'date';
621
+ if (Array.isArray(value))
622
+ return 'array';
623
+ if (typeof value === 'string' && /^[a-fA-F0-9-]{36}$/.test(value))
624
+ return 'reference';
625
+ return 'string';
626
+ }
627
+ /**
628
+ * Create model from JSON
629
+ */
630
+ static fromJSON(data) {
631
+ // Support both __class and __typename, and handle both old and new naming
632
+ const modelIdentifier = data.__typename || data.__class || data.modelName;
633
+ if (!modelIdentifier) {
634
+ throw new AbloValidationError('Model identifier (__typename, __class, or modelName) not found in data', { code: 'model_identifier_missing' });
635
+ }
636
+ // Try to get model class by identifier
637
+ let ModelClass = getActiveRegistry().getModelByName(modelIdentifier);
638
+ // If not found with Prisma name, try mapping to class name
639
+ if (!ModelClass) {
640
+ const classNameMap = {
641
+ Task: 'TaskModel',
642
+ Project: 'Project',
643
+ Comment: 'CommentModel',
644
+ User: 'UserModel',
645
+ Organization: 'OrganizationModel',
646
+ StatusGroup: 'StatusGroupModel',
647
+ Team: 'TeamModel',
648
+ Member: 'MemberModel',
649
+ Role: 'RoleModel',
650
+ };
651
+ const className = classNameMap[modelIdentifier];
652
+ if (className) {
653
+ ModelClass = getActiveRegistry().getModelByName(className);
654
+ }
655
+ }
656
+ if (!ModelClass) {
657
+ throw new AbloValidationError(`Model class not found for: ${modelIdentifier}`, { code: 'model_class_not_registered' });
658
+ }
659
+ const instance = new ModelClass(data);
660
+ instance.markAsPersisted();
661
+ return instance;
662
+ }
663
+ /**
664
+ * Get sync status
665
+ */
666
+ getSyncStatus() {
667
+ return this.syncStatus;
668
+ }
669
+ /**
670
+ * Mark model as synced
671
+ */
672
+ markAsSynced() {
673
+ this.syncStatus = 'synced';
674
+ }
675
+ /**
676
+ * Mark model as pending sync
677
+ */
678
+ markAsPending() {
679
+ this.syncStatus = 'pending';
680
+ }
681
+ }
682
+ /**
683
+ * Project a dynamic-class `Model` instance to the schema row shape `T`.
684
+ *
685
+ * The runtime invariant: `createDynamicModelClass(...)` attaches every
686
+ * field of `T` directly onto the Model prototype/instance via
687
+ * `Object.defineProperty` and the M1 observable bridge, so a Model
688
+ * instance structurally satisfies `T` at runtime. The static type
689
+ * system can't see this because `T` is a free generic — there's no
690
+ * common ancestor between `Model` (base class) and the schema row
691
+ * interface produced by `defineSchema`.
692
+ *
693
+ * This is a typed boundary, not a bypass: every call site is the
694
+ * dynamic-class duality where Model-with-extras-and-T-fields is being
695
+ * returned to a consumer that only sees `T`. Concentrating the cast
696
+ * here means there's one place to look when the boundary changes.
697
+ */
698
+ export function modelAsRow(model) {
699
+ return model;
700
+ }
701
+ /**
702
+ * Inverse of `modelAsRow`: accept a row-shaped value (schema-derived
703
+ * `T` with at minimum an `id`) and surface it as a `Model`. Used by
704
+ * `BaseSyncedStore.save / delete / archive / unarchive` so consumers
705
+ * can pass either a typed schema row or a Model instance and the
706
+ * SDK's persistence path sees a uniform Model surface.
707
+ *
708
+ * Same runtime invariant as `modelAsRow`: dynamic-class instances
709
+ * carry both the row fields and the Model methods on the same object
710
+ * — one structural identity, two static views. The helper does no
711
+ * runtime conversion (no allocation, no copy) — it's a pure type cast.
712
+ */
713
+ export function rowAsModel(entity) {
714
+ return entity;
715
+ }