@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,535 @@
1
+ /**
2
+ * ModelRegistry - Type-safe model metadata management
3
+ *
4
+ * Key improvements:
5
+ * - Instance-based for better testing
6
+ * - Validation at registration
7
+ * - Lazy reference resolution
8
+ * - Crypto-based schema hashing
9
+ * - Comprehensive error reporting
10
+ * - Best practices from Linear Sync Engine
11
+ */
12
+ // Removed Node.js crypto import for browser compatibility
13
+ import { PropertyType, LoadStrategy, } from './types/index.js';
14
+ import { getContext } from './context.js';
15
+ import { AbloValidationError } from './errors.js';
16
+ /**
17
+ * Module-level active registry. Set by createSyncEngine so that Model instances
18
+ * (which don't receive DI) can look up metadata without static maps.
19
+ */
20
+ let _activeRegistry = null;
21
+ /** Set the active ModelRegistry instance (called by createSyncEngine) */
22
+ export function setActiveRegistry(registry) {
23
+ _activeRegistry = registry;
24
+ }
25
+ /** Get the active ModelRegistry. Throws if none set. */
26
+ export function getActiveRegistry() {
27
+ if (!_activeRegistry) {
28
+ throw new AbloValidationError('No active ModelRegistry — call createSyncEngine() first', { code: 'registry_not_initialized' });
29
+ }
30
+ return _activeRegistry;
31
+ }
32
+ /** Whether an active ModelRegistry has been set. */
33
+ export function hasActiveRegistry() {
34
+ return _activeRegistry !== null;
35
+ }
36
+ /** Clear the active ModelRegistry (tests only). */
37
+ export function clearActiveRegistry() {
38
+ _activeRegistry = null;
39
+ }
40
+ export class ModelRegistry {
41
+ models = new Map();
42
+ modelMetadata = new Map();
43
+ properties = new Map();
44
+ references = new Map();
45
+ pendingReferences = new Map();
46
+ // 🔧 PROPER FIX: Static mapping from constructor to model name
47
+ constructorToModelName = new Map();
48
+ // LINEAR PATTERN: BackReferences for cascade-aware transaction handling.
49
+ // Maps childModelName → BackReferenceMetadata[] (which parent models
50
+ // own this child). The inverse direction (parentModelName → children)
51
+ // is derived on demand by `getChildModels`, populated only here.
52
+ backReferences = new Map();
53
+ schemaHash;
54
+ config;
55
+ registeredModels = new Set();
56
+ batchMode = false;
57
+ pendingHashUpdate = false;
58
+ constructor(config = {}) {
59
+ this.config = {
60
+ validateOnRegister: config.validateOnRegister ?? true,
61
+ allowLateReferences: config.allowLateReferences ?? true,
62
+ };
63
+ }
64
+ validateModelConstructor(name, constructor) {
65
+ if (typeof constructor !== 'function') {
66
+ throw new AbloValidationError(`Model ${name} constructor must be a function`, { code: 'registry_invalid_constructor' });
67
+ }
68
+ if (!constructor.prototype) {
69
+ throw new AbloValidationError(`Model ${name} constructor must have a prototype`, { code: 'registry_invalid_constructor' });
70
+ }
71
+ // Check for required methods
72
+ const required = ['updateFromData', 'toJSON', 'getModelName'];
73
+ for (const method of required) {
74
+ if (typeof constructor.prototype[method] !== 'function') {
75
+ getContext().logger.debug('Model missing required method', name, { method });
76
+ }
77
+ }
78
+ }
79
+ arePropertiesCompatible(existing, incoming) {
80
+ // For reference-generated ID properties, be more lenient
81
+ // Only check core compatibility, not all metadata fields
82
+ return (existing.type === incoming.type &&
83
+ // For indexed, treat undefined as false for comparison
84
+ (existing.indexed ?? false) === (incoming.indexed ?? false) &&
85
+ // For optional, treat undefined as false for comparison
86
+ (existing.optional ?? false) === (incoming.optional ?? false));
87
+ }
88
+ addPendingReference(modelName, propertyName, metadata) {
89
+ // Get target model name
90
+ let targetName;
91
+ try {
92
+ targetName = metadata.referencedModel()?.name || 'Unknown';
93
+ }
94
+ catch {
95
+ targetName = 'Unknown';
96
+ }
97
+ let pending = this.pendingReferences.get(targetName);
98
+ if (!pending) {
99
+ pending = [];
100
+ this.pendingReferences.set(targetName, pending);
101
+ }
102
+ pending.push({ modelName, propertyName, metadata });
103
+ getContext().logger.debug('Reference deferred', `${modelName}.${propertyName}`, { targetModel: targetName });
104
+ }
105
+ resolvePendingReferences(targetModelName) {
106
+ const pending = this.pendingReferences.get(targetModelName);
107
+ if (!pending)
108
+ return;
109
+ for (const ref of pending) {
110
+ try {
111
+ this.completeReferenceRegistration(ref.modelName, ref.propertyName, ref.metadata);
112
+ getContext().logger.debug('Reference resolved', `${ref.modelName}.${ref.propertyName}`, {
113
+ targetModel: targetModelName,
114
+ });
115
+ }
116
+ catch (error) {
117
+ getContext().observability.breadcrumb(`Failed to resolve reference ${ref.modelName}.${ref.propertyName}`, 'sync.database', 'error', {
118
+ error: error instanceof Error ? error.message : String(error),
119
+ });
120
+ }
121
+ }
122
+ this.pendingReferences.delete(targetModelName);
123
+ }
124
+ completeReferenceRegistration(modelName, propertyName, metadata) {
125
+ // Store reference
126
+ let refs = this.references.get(modelName);
127
+ if (!refs) {
128
+ refs = new Map();
129
+ this.references.set(modelName, refs);
130
+ }
131
+ refs.set(propertyName, metadata);
132
+ // Register ID property (skip organizationId as it's handled by models themselves)
133
+ const idPropName = propertyName.endsWith('Id') ? propertyName : `${propertyName}Id`;
134
+ if (idPropName !== 'organizationId') {
135
+ this.registerProperty(modelName, idPropName, {
136
+ type: PropertyType.reference,
137
+ indexed: metadata.indexed || false,
138
+ optional: metadata.nullable || false,
139
+ });
140
+ }
141
+ // Register model property
142
+ this.registerProperty(modelName, propertyName, {
143
+ type: PropertyType.referenceModel,
144
+ optional: metadata.nullable || false,
145
+ });
146
+ this.schemaHash = undefined;
147
+ }
148
+ /**
149
+ * Register a model with validation
150
+ */
151
+ registerModel(name, constructor, metadata = { loadStrategy: LoadStrategy.instant }) {
152
+ // Validate
153
+ if (this.config.validateOnRegister) {
154
+ this.validateModelConstructor(name, constructor);
155
+ }
156
+ // Check for duplicate
157
+ if (this.models.has(name)) {
158
+ getContext().logger.debug('Model already registered, skipping', name);
159
+ return;
160
+ }
161
+ getContext().logger.debug('Registering model', name);
162
+ // Register
163
+ this.models.set(name, constructor);
164
+ this.modelMetadata.set(name, metadata);
165
+ // 🔧 PROPER FIX: Create reverse mapping from constructor to model name
166
+ this.constructorToModelName.set(constructor, name);
167
+ // Initialize property maps
168
+ if (!this.properties.has(name)) {
169
+ this.properties.set(name, new Map());
170
+ }
171
+ if (!this.references.has(name)) {
172
+ this.references.set(name, new Map());
173
+ }
174
+ // Mark as registered
175
+ this.registeredModels.add(name);
176
+ // Resolve pending references to this model
177
+ this.resolvePendingReferences(name);
178
+ // Invalidate schema hash
179
+ this.schemaHash = undefined;
180
+ getContext().logger.debug('Model registered', name, metadata);
181
+ }
182
+ /**
183
+ * Register property with validation
184
+ */
185
+ registerProperty(modelName, propertyName, metadata) {
186
+ // Validate model exists
187
+ if (!this.models.has(modelName) && this.config.validateOnRegister) {
188
+ throw new AbloValidationError(`Cannot register property for unknown model: ${modelName}`, { code: 'registry_unknown_model' });
189
+ }
190
+ // Get or create property map
191
+ let props = this.properties.get(modelName);
192
+ if (!props) {
193
+ props = new Map();
194
+ this.properties.set(modelName, props);
195
+ }
196
+ // Check for conflicts
197
+ const existing = props.get(propertyName);
198
+ if (existing) {
199
+ if (this.arePropertiesCompatible(existing, metadata)) {
200
+ // Properties are compatible, skip re-registration
201
+ getContext().logger.debug('Property already registered (compatible)', `${modelName}.${propertyName}`);
202
+ return;
203
+ }
204
+ else {
205
+ throw new AbloValidationError(`Property ${modelName}.${propertyName} already registered with incompatible metadata`, { code: 'registry_property_conflict' });
206
+ }
207
+ }
208
+ props.set(propertyName, metadata);
209
+ this.schemaHash = undefined;
210
+ getContext().logger.debug('Property registered', `${modelName}.${propertyName}`, metadata);
211
+ }
212
+ /**
213
+ * Register reference with lazy resolution
214
+ */
215
+ registerReference(modelName, propertyName, metadata) {
216
+ // Try to resolve target model
217
+ let targetModelName;
218
+ try {
219
+ const targetModel = metadata.referencedModel();
220
+ targetModelName = targetModel?.name;
221
+ }
222
+ catch {
223
+ // Defer resolution
224
+ if (this.config.allowLateReferences) {
225
+ this.addPendingReference(modelName, propertyName, metadata);
226
+ return;
227
+ }
228
+ throw new AbloValidationError(`Cannot resolve reference ${modelName}.${propertyName}`, { code: 'registry_reference_unresolved' });
229
+ }
230
+ // Validate target exists or defer
231
+ if (!this.models.has(targetModelName)) {
232
+ if (this.config.allowLateReferences) {
233
+ this.addPendingReference(modelName, propertyName, metadata);
234
+ return;
235
+ }
236
+ throw new AbloValidationError(`Reference ${modelName}.${propertyName} points to unknown model ${targetModelName}`, { code: 'registry_reference_unknown_target' });
237
+ }
238
+ // Complete registration
239
+ this.completeReferenceRegistration(modelName, propertyName, metadata);
240
+ }
241
+ /**
242
+ * LINEAR PATTERN: Register a back-reference for cascade-aware transaction handling
243
+ *
244
+ * When a parent model is deleted, the TransactionQueue will cancel pending
245
+ * transactions for all child models that have a backReference to that parent.
246
+ *
247
+ * @param childModelName - The model that has a FK to the parent (e.g., 'Slide')
248
+ * @param metadata - BackReference configuration
249
+ */
250
+ registerBackReference(childModelName, metadata) {
251
+ // Add to instance map
252
+ let refs = this.backReferences.get(childModelName);
253
+ if (!refs) {
254
+ refs = [];
255
+ this.backReferences.set(childModelName, refs);
256
+ }
257
+ // Avoid duplicates
258
+ const exists = refs.some((r) => r.parentModel === metadata.parentModel && r.foreignKey === metadata.foreignKey);
259
+ if (!exists) {
260
+ refs.push(metadata);
261
+ }
262
+ // Reverse lookup (parent → children) is derived on demand by
263
+ // `getChildModels`, which scans this map.
264
+ getContext().logger.debug('BackReference registered', `${childModelName} -> ${metadata.parentModel}`, {
265
+ foreignKey: metadata.foreignKey,
266
+ cascadeDelete: metadata.cascadeDelete,
267
+ });
268
+ }
269
+ /** Get all models with specific load strategy. */
270
+ getModelsByLoadStrategy(strategy) {
271
+ const models = [];
272
+ for (const [modelName, metadata] of this.modelMetadata) {
273
+ if (metadata.loadStrategy === strategy) {
274
+ models.push(modelName);
275
+ }
276
+ }
277
+ return models;
278
+ }
279
+ /** Get model name from constructor (production-safe). */
280
+ getModelNameFromConstructor(constructor) {
281
+ return this.constructorToModelName.get(constructor);
282
+ }
283
+ /** Get properties for a model. */
284
+ getPropertiesForModel(modelName) {
285
+ return this.properties.get(modelName) || new Map();
286
+ }
287
+ /**
288
+ * Get all registered model names for this instance
289
+ */
290
+ getRegisteredModelNames() {
291
+ return Array.from(this.models.keys());
292
+ }
293
+ /** Get model constructor by name */
294
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
295
+ getModelByName(name) {
296
+ return this.models.get(name);
297
+ }
298
+ /** Check if model is registered */
299
+ hasModel(name) {
300
+ return this.models.has(name);
301
+ }
302
+ /** Get model metadata by name */
303
+ getMetadata(name) {
304
+ return this.modelMetadata.get(name);
305
+ }
306
+ /** Get properties for a model */
307
+ getProperties(name) {
308
+ return this.properties.get(name) || new Map();
309
+ }
310
+ /** Get references for a model */
311
+ getReferences(name) {
312
+ return this.references.get(name) || new Map();
313
+ }
314
+ /** Get indexed properties for a model */
315
+ getIndexedProperties(modelName) {
316
+ const properties = this.getProperties(modelName);
317
+ const indexed = [];
318
+ for (const [propName, metadata] of properties) {
319
+ if (metadata.indexed)
320
+ indexed.push(propName);
321
+ }
322
+ return indexed;
323
+ }
324
+ /** Get back-references for a child model */
325
+ getBackReferences(childModelName) {
326
+ return this.backReferences.get(childModelName) || [];
327
+ }
328
+ /** Get child models for a parent */
329
+ getChildModels(parentModelName) {
330
+ // Derive from backReferences
331
+ const children = [];
332
+ for (const [childModel, refs] of this.backReferences) {
333
+ for (const ref of refs) {
334
+ if (ref.parentModel === parentModelName) {
335
+ children.push({ childModel, foreignKey: ref.foreignKey });
336
+ }
337
+ }
338
+ }
339
+ return children;
340
+ }
341
+ /**
342
+ * Calculate schema hash using crypto
343
+ */
344
+ getSchemaHash() {
345
+ if (this.schemaHash)
346
+ return this.schemaHash;
347
+ const schema = {};
348
+ // Build schema object
349
+ for (const [modelName, props] of this.properties) {
350
+ schema[modelName] = {};
351
+ for (const [propName, meta] of props) {
352
+ schema[modelName][propName] = {
353
+ type: meta.type,
354
+ indexed: meta.indexed || false,
355
+ optional: meta.optional || false,
356
+ };
357
+ }
358
+ }
359
+ // Sort and stringify
360
+ const sorted = JSON.stringify(schema, Object.keys(schema).sort());
361
+ // Create hash - browser-compatible simple hash
362
+ this.schemaHash = this.simpleHash(sorted);
363
+ getContext().logger.debug('Schema hash updated', this.schemaHash);
364
+ return this.schemaHash;
365
+ }
366
+ /**
367
+ * Browser-compatible hash function
368
+ */
369
+ simpleHash(str) {
370
+ let hash = 0;
371
+ for (let i = 0; i < str.length; i++) {
372
+ const char = str.charCodeAt(i);
373
+ hash = (hash << 5) - hash + char;
374
+ hash = hash & hash; // Convert to 32bit integer
375
+ }
376
+ return Math.abs(hash).toString(16).padStart(8, '0');
377
+ }
378
+ /**
379
+ * Static wrapper for backward compatibility
380
+ */
381
+ /**
382
+ * Validate all references
383
+ */
384
+ validateReferences() {
385
+ const errors = [];
386
+ // Check pending references
387
+ for (const [target, pending] of this.pendingReferences) {
388
+ for (const ref of pending) {
389
+ errors.push(`Unresolved reference: ${ref.modelName}.${ref.propertyName} -> ${target}`);
390
+ }
391
+ }
392
+ // Check resolved references
393
+ for (const [modelName, refs] of this.references) {
394
+ for (const [propName, meta] of refs) {
395
+ try {
396
+ const target = meta.referencedModel();
397
+ if (!this.models.has(target.name)) {
398
+ errors.push(`Invalid reference: ${modelName}.${propName} -> ${target.name}`);
399
+ }
400
+ }
401
+ catch (error) {
402
+ errors.push(`Cannot resolve reference: ${modelName}.${propName}`);
403
+ }
404
+ }
405
+ }
406
+ const isValid = errors.length === 0;
407
+ if (isValid) {
408
+ getContext().logger.info('All model references are valid');
409
+ }
410
+ else {
411
+ getContext().observability.breadcrumb('Reference validation failed', 'sync.database', 'error');
412
+ }
413
+ return {
414
+ valid: isValid,
415
+ errors,
416
+ };
417
+ }
418
+ /**
419
+ * Static wrapper for backward compatibility
420
+ */
421
+ /**
422
+ * Start batch registration mode to optimize performance
423
+ */
424
+ startBatch() {
425
+ this.batchMode = true;
426
+ this.pendingHashUpdate = false;
427
+ }
428
+ /**
429
+ * Static wrapper for backward compatibility
430
+ */
431
+ /**
432
+ * End batch registration mode and update schema hash if needed
433
+ */
434
+ endBatch() {
435
+ this.batchMode = false;
436
+ if (this.pendingHashUpdate) {
437
+ this.getSchemaHash(); // This will recalculate if needed
438
+ this.pendingHashUpdate = false;
439
+ }
440
+ }
441
+ /**
442
+ * Static wrapper for backward compatibility
443
+ */
444
+ /**
445
+ * Clear registry
446
+ */
447
+ clear() {
448
+ this.models.clear();
449
+ this.modelMetadata.clear();
450
+ this.properties.clear();
451
+ this.references.clear();
452
+ this.pendingReferences.clear();
453
+ this.registeredModels.clear();
454
+ this.backReferences.clear();
455
+ this.constructorToModelName.clear();
456
+ this.schemaHash = undefined;
457
+ this.batchMode = false;
458
+ this.pendingHashUpdate = false;
459
+ getContext().logger.info('ModelRegistry cleared');
460
+ }
461
+ /**
462
+ * Static wrapper for backward compatibility
463
+ */
464
+ /**
465
+ * Export for debugging
466
+ */
467
+ export() {
468
+ return {
469
+ models: Array.from(this.models.keys()),
470
+ metadata: Object.fromEntries(this.modelMetadata),
471
+ properties: Object.fromEntries(Array.from(this.properties.entries()).map(([name, props]) => [
472
+ name,
473
+ Object.fromEntries(props),
474
+ ])),
475
+ references: Object.fromEntries(Array.from(this.references.entries()).map(([name, refs]) => [
476
+ name,
477
+ Object.fromEntries(refs),
478
+ ])),
479
+ pending: Object.fromEntries(Array.from(this.pendingReferences.entries()).map(([name, refs]) => [
480
+ name,
481
+ refs.map((r) => `${r.modelName}.${r.propertyName}`),
482
+ ])),
483
+ schemaHash: this.getSchemaHash(),
484
+ };
485
+ }
486
+ /**
487
+ * Export registry data for debugging (backward compatibility)
488
+ */
489
+ exportRegistryData() {
490
+ const models = {};
491
+ const properties = {};
492
+ const references = {};
493
+ const metadata = {};
494
+ // Export models
495
+ for (const [name, constructor] of this.models) {
496
+ models[name] = constructor.name;
497
+ }
498
+ // Export properties
499
+ for (const [modelName, propertyMap] of this.properties) {
500
+ properties[modelName] = {};
501
+ for (const [propName, propMetadata] of propertyMap) {
502
+ properties[modelName][propName] = propMetadata;
503
+ }
504
+ }
505
+ // Export references
506
+ for (const [modelName, referenceMap] of this.references) {
507
+ references[modelName] = {};
508
+ for (const [refName, refMetadata] of referenceMap) {
509
+ try {
510
+ references[modelName][refName] = {
511
+ ...refMetadata,
512
+ referencedModel: refMetadata.referencedModel().name,
513
+ };
514
+ }
515
+ catch {
516
+ references[modelName][refName] = {
517
+ ...refMetadata,
518
+ referencedModel: 'Unresolved',
519
+ };
520
+ }
521
+ }
522
+ }
523
+ // Export metadata
524
+ for (const [modelName, modelMetadata] of this.modelMetadata) {
525
+ metadata[modelName] = modelMetadata;
526
+ }
527
+ return {
528
+ models,
529
+ properties,
530
+ references,
531
+ metadata,
532
+ schemaHash: this.getSchemaHash(),
533
+ };
534
+ }
535
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * NetworkMonitor - Network connectivity tracking with visibility awareness
3
+ *
4
+ * Monitors online/offline state using browser events AND visibility changes.
5
+ * When a tab becomes visible after being hidden (e.g., laptop sleep/wake),
6
+ * the WebSocket may have silently died without triggering online/offline events.
7
+ * The visibility handler detects this and emits 'online' to trigger recovery.
8
+ */
9
+ import { EventEmitter } from 'events';
10
+ export declare class NetworkMonitor extends EventEmitter {
11
+ private isOnline;
12
+ private lastOnlineCheck;
13
+ constructor();
14
+ private handleOnline;
15
+ private handleOffline;
16
+ /**
17
+ * When the tab becomes visible, the WebSocket may have silently died
18
+ * (e.g., laptop sleep/wake, long background). Browser online/offline events
19
+ * don't fire in this case because the network itself didn't change.
20
+ * Emit 'visibility_online' so SyncedStore can check and recover.
21
+ */
22
+ private handleVisibilityChange;
23
+ private setupListeners;
24
+ getStatus(): boolean;
25
+ getLastOnlineTime(): Date;
26
+ dispose(): void;
27
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * NetworkMonitor - Network connectivity tracking with visibility awareness
3
+ *
4
+ * Monitors online/offline state using browser events AND visibility changes.
5
+ * When a tab becomes visible after being hidden (e.g., laptop sleep/wake),
6
+ * the WebSocket may have silently died without triggering online/offline events.
7
+ * The visibility handler detects this and emits 'online' to trigger recovery.
8
+ */
9
+ import { EventEmitter } from 'events';
10
+ import { getContext } from './context.js';
11
+ export class NetworkMonitor extends EventEmitter {
12
+ isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
13
+ lastOnlineCheck = new Date();
14
+ constructor() {
15
+ super();
16
+ this.setupListeners();
17
+ }
18
+ handleOnline = async () => {
19
+ const wasOffline = !this.isOnline;
20
+ this.isOnline = true;
21
+ this.lastOnlineCheck = new Date();
22
+ if (wasOffline) {
23
+ getContext().logger.info('Network connection restored');
24
+ this.emit('online');
25
+ }
26
+ };
27
+ handleOffline = () => {
28
+ const wasOnline = this.isOnline;
29
+ this.isOnline = false;
30
+ if (wasOnline) {
31
+ getContext().logger.warn('Network connection lost');
32
+ this.emit('offline');
33
+ }
34
+ };
35
+ /**
36
+ * When the tab becomes visible, the WebSocket may have silently died
37
+ * (e.g., laptop sleep/wake, long background). Browser online/offline events
38
+ * don't fire in this case because the network itself didn't change.
39
+ * Emit 'visibility_online' so SyncedStore can check and recover.
40
+ */
41
+ handleVisibilityChange = () => {
42
+ if (document.visibilityState !== 'visible')
43
+ return;
44
+ // Update navigator.onLine state — it may have changed while hidden
45
+ this.isOnline = navigator.onLine;
46
+ this.lastOnlineCheck = new Date();
47
+ if (this.isOnline) {
48
+ getContext().logger.info('Tab became visible with network available — emitting visibility_online');
49
+ this.emit('visibility_online');
50
+ }
51
+ };
52
+ setupListeners() {
53
+ if (typeof window === 'undefined')
54
+ return;
55
+ window.addEventListener('online', this.handleOnline);
56
+ window.addEventListener('offline', this.handleOffline);
57
+ document.addEventListener('visibilitychange', this.handleVisibilityChange);
58
+ }
59
+ getStatus() {
60
+ return this.isOnline;
61
+ }
62
+ getLastOnlineTime() {
63
+ return this.lastOnlineCheck;
64
+ }
65
+ dispose() {
66
+ if (typeof window !== 'undefined') {
67
+ window.removeEventListener('online', this.handleOnline);
68
+ window.removeEventListener('offline', this.handleOffline);
69
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange);
70
+ }
71
+ this.removeAllListeners();
72
+ }
73
+ }