@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,361 @@
1
+ /**
2
+ * Ablo Sync Engine - Database Manager
3
+ *
4
+ * Manages the two-tier database architecture:
5
+ * 1. ablo_databases - Metadata about workspace databases
6
+ * 2. ablo_(hash) - Workspace-specific data storage
7
+ *
8
+ * Follows Ablo's architecture for database management.
9
+ */
10
+ import { getContext } from '../context.js';
11
+ import { openIDBWithTimeout } from './openIDBWithTimeout.js';
12
+ import { AbloConnectionError } from '../errors.js';
13
+ import { getActiveRegistry, hasActiveRegistry } from '../ModelRegistry.js';
14
+ /**
15
+ * DatabaseManager - Manages Ablo's two-tier database architecture
16
+ *
17
+ * Key responsibilities:
18
+ * - Manages ablo_databases (database registry)
19
+ * - Creates workspace-specific databases (ablo_hash)
20
+ * - Handles database migration and versioning
21
+ * - Provides database info and metadata management
22
+ */
23
+ export class DatabaseManager {
24
+ metaDb = null;
25
+ metaDbName = 'ablo_databases';
26
+ constructor() {
27
+ // Singleton-like behavior
28
+ }
29
+ /**
30
+ * Initialize the meta database (ablo_databases)
31
+ */
32
+ async initializeMetaDatabase() {
33
+ this.metaDb = await openIDBWithTimeout(this.metaDbName, 1, {
34
+ onUpgrade: (request) => {
35
+ const db = request.result;
36
+ if (!db.objectStoreNames.contains('databases')) {
37
+ const store = db.createObjectStore('databases', { keyPath: 'name' });
38
+ store.createIndex('userId', 'userId');
39
+ store.createIndex('workspaceId', 'workspaceId');
40
+ store.createIndex('schemaHash', 'schemaHash');
41
+ store.createIndex('updatedAt', 'updatedAt');
42
+ }
43
+ },
44
+ });
45
+ }
46
+ /**
47
+ * Calculate database info for a user/workspace combination
48
+ */
49
+ async calculateDatabaseInfo(userId, workspaceId, userVersion = 1) {
50
+ // Get schema hash from the active ModelRegistry
51
+ const schemaHash = hasActiveRegistry()
52
+ ? getActiveRegistry().getSchemaHash()
53
+ : 'no-registry-hash';
54
+ // Generate database name from userId, workspaceId, and versions
55
+ const dbName = this.generateDatabaseName(userId, workspaceId, userVersion);
56
+ // Check if we need to increment schema version
57
+ const existingInfo = await this.getDatabaseInfo(dbName);
58
+ let schemaVersion = 1;
59
+ if (existingInfo && existingInfo.schemaHash !== schemaHash) {
60
+ schemaVersion = (existingInfo.schemaVersion || 1) + 1;
61
+ }
62
+ else if (existingInfo) {
63
+ schemaVersion = existingInfo.schemaVersion || 1;
64
+ }
65
+ // DEBUG: Log all existing databases for this user to detect duplicates
66
+ const allUserDatabases = await this.getDatabasesForUser(userId);
67
+ const allIndexedDBs = (await indexedDB.databases?.()) || [];
68
+ const abloDatabases = allIndexedDBs.filter((db) => db.name?.startsWith('ablo_'));
69
+ getContext().observability.breadcrumb('Database info calculated', 'sync.database', 'info', {
70
+ dbName,
71
+ schemaVersion,
72
+ existingDbCount: allUserDatabases.length,
73
+ abloDbCount: abloDatabases.length,
74
+ });
75
+ return {
76
+ name: dbName,
77
+ userId,
78
+ workspaceId,
79
+ schemaHash,
80
+ schemaVersion,
81
+ userVersion,
82
+ createdAt: existingInfo?.createdAt || new Date(),
83
+ updatedAt: new Date(),
84
+ };
85
+ }
86
+ /**
87
+ * Generate deterministic database name
88
+ */
89
+ generateDatabaseName(userId, workspaceId, userVersion = 1) {
90
+ // Combine userId, workspaceId, and userVersion for unique database
91
+ const combined = `${userId}:${workspaceId}:${userVersion}`;
92
+ // Generate hash similar to Linear's approach
93
+ let hash = 0;
94
+ for (let i = 0; i < combined.length; i++) {
95
+ hash = (hash << 5) - hash + combined.charCodeAt(i);
96
+ hash = hash & hash; // Convert to 32bit integer
97
+ }
98
+ // Convert to hex and create Ablo-style name
99
+ const hexHash = Math.abs(hash).toString(16).padStart(8, '0');
100
+ return `ablo_${hexHash}`;
101
+ }
102
+ /**
103
+ * Register database info in ablo_databases
104
+ */
105
+ async registerDatabase(info) {
106
+ if (!this.metaDb) {
107
+ throw new AbloConnectionError('Meta database not initialized', {
108
+ code: 'meta_db_not_initialized',
109
+ });
110
+ }
111
+ return new Promise((resolve, reject) => {
112
+ const tx = this.metaDb.transaction(['databases'], 'readwrite');
113
+ const store = tx.objectStore('databases');
114
+ const request = store.put(info);
115
+ tx.oncomplete = () => {
116
+ resolve();
117
+ };
118
+ tx.onerror = () => reject(tx.error);
119
+ request.onerror = () => reject(request.error);
120
+ });
121
+ }
122
+ /**
123
+ * Get database info by name
124
+ */
125
+ async getDatabaseInfo(name) {
126
+ if (!this.metaDb)
127
+ return null;
128
+ return new Promise((resolve, reject) => {
129
+ const tx = this.metaDb.transaction(['databases'], 'readonly');
130
+ const store = tx.objectStore('databases');
131
+ const request = store.get(name);
132
+ request.onsuccess = () => resolve(request.result || null);
133
+ request.onerror = () => reject(request.error);
134
+ });
135
+ }
136
+ /**
137
+ * Get all databases for a user
138
+ */
139
+ async getDatabasesForUser(userId) {
140
+ if (!this.metaDb)
141
+ return [];
142
+ return new Promise((resolve, reject) => {
143
+ const tx = this.metaDb.transaction(['databases'], 'readonly');
144
+ const store = tx.objectStore('databases');
145
+ const index = store.index('userId');
146
+ const request = index.getAll(userId);
147
+ request.onsuccess = () => resolve(request.result || []);
148
+ request.onerror = () => reject(request.error);
149
+ });
150
+ }
151
+ /**
152
+ * Open workspace-specific database
153
+ */
154
+ async openWorkspaceDatabase(dbInfo, createStoresFn) {
155
+ try {
156
+ return await openIDBWithTimeout(dbInfo.name, dbInfo.schemaVersion, {
157
+ onUpgrade: (request, event) => {
158
+ const db = request.result;
159
+ const tx = event.target.transaction;
160
+ // Per jakearchibald/idb's "Transaction Lifetime Management":
161
+ // only IDB-request awaits keep an upgrade transaction alive; any
162
+ // non-IDB await (fetch, timer, etc.) commits it prematurely and
163
+ // later ops throw `TransactionInactiveError`. StoreManager.createStores
164
+ // (src/core/StoreManager.ts:93) is only synchronous createObjectStore
165
+ // / createIndex calls wrapped in an `async` keyword, so firing it
166
+ // without awaiting is safe and matches the VCS-slot semantics.
167
+ if (createStoresFn && tx) {
168
+ try {
169
+ void createStoresFn(db, tx).catch((err) => {
170
+ getContext().observability.captureBootstrapFailure(err, {
171
+ type: 'store-creation',
172
+ });
173
+ });
174
+ }
175
+ catch (err) {
176
+ getContext().observability.captureBootstrapFailure(err, {
177
+ type: 'store-creation',
178
+ });
179
+ }
180
+ }
181
+ },
182
+ });
183
+ }
184
+ catch (error) {
185
+ getContext().observability.captureBootstrapFailure(error, {
186
+ type: 'database-open',
187
+ });
188
+ throw error;
189
+ }
190
+ }
191
+ /**
192
+ * Read workspace metadata from __meta table
193
+ */
194
+ async getWorkspaceMetadata(db) {
195
+ return new Promise((resolve, reject) => {
196
+ const tx = db.transaction(['__meta'], 'readonly');
197
+ const store = tx.objectStore('__meta');
198
+ const request = store.get('metadata');
199
+ request.onsuccess = () => {
200
+ const data = request.result;
201
+ if (!data) {
202
+ resolve(null);
203
+ return;
204
+ }
205
+ const meta = {
206
+ lastSyncId: data.lastSyncId || 0,
207
+ firstSyncId: data.firstSyncId || 0,
208
+ backendDatabaseVersion: data.backendDatabaseVersion || 1,
209
+ subscribedSyncGroups: data.subscribedSyncGroups || [],
210
+ updatedAt: data.updatedAt ? new Date(data.updatedAt) : new Date(),
211
+ schemaHash: data.schemaHash,
212
+ syncGroups: data.syncGroups,
213
+ versions: data.versions || undefined,
214
+ };
215
+ resolve(meta);
216
+ };
217
+ request.onerror = () => reject(request.error);
218
+ });
219
+ }
220
+ /**
221
+ * Write workspace metadata to __meta table
222
+ */
223
+ async setWorkspaceMetadata(db, metadata) {
224
+ return new Promise((resolve, reject) => {
225
+ const tx = db.transaction(['__meta'], 'readwrite');
226
+ const store = tx.objectStore('__meta');
227
+ const request = store.put(metadata, 'metadata');
228
+ tx.oncomplete = () => resolve();
229
+ tx.onerror = () => reject(tx.error);
230
+ request.onerror = () => reject(request.error);
231
+ });
232
+ }
233
+ /**
234
+ * Check if a model is persisted (all instances loaded)
235
+ */
236
+ async isModelPersisted(db, modelName) {
237
+ return new Promise((resolve, reject) => {
238
+ const tx = db.transaction(['__meta'], 'readonly');
239
+ const store = tx.objectStore('__meta');
240
+ const request = store.get(modelName);
241
+ request.onsuccess = () => {
242
+ const data = request.result;
243
+ resolve(data?.persisted === true);
244
+ };
245
+ request.onerror = () => reject(request.error);
246
+ });
247
+ }
248
+ /**
249
+ * Mark a model as persisted
250
+ */
251
+ async setModelPersisted(db, modelName, persisted) {
252
+ return new Promise((resolve, reject) => {
253
+ const tx = db.transaction(['__meta'], 'readwrite');
254
+ const store = tx.objectStore('__meta');
255
+ const persistenceData = {
256
+ persisted,
257
+ modelName,
258
+ timestamp: Date.now(),
259
+ updatedAt: new Date().toISOString(),
260
+ };
261
+ const request = store.put(persistenceData, modelName);
262
+ tx.oncomplete = () => resolve();
263
+ tx.onerror = () => reject(tx.error);
264
+ request.onerror = () => reject(request.error);
265
+ });
266
+ }
267
+ /**
268
+ * Get all model persistence states
269
+ */
270
+ async getAllModelPersistenceStates(db) {
271
+ return new Promise((resolve, reject) => {
272
+ const tx = db.transaction(['__meta'], 'readonly');
273
+ const store = tx.objectStore('__meta');
274
+ const request = store.getAll();
275
+ request.onsuccess = () => {
276
+ const states = {};
277
+ for (const item of request.result) {
278
+ // Skip metadata entry
279
+ if (item.key === 'metadata')
280
+ continue;
281
+ if (item.modelName) {
282
+ states[item.modelName] = item.persisted === true;
283
+ }
284
+ }
285
+ resolve(states);
286
+ };
287
+ request.onerror = () => reject(request.error);
288
+ });
289
+ }
290
+ /**
291
+ * Delete a workspace database
292
+ */
293
+ async deleteWorkspaceDatabase(dbInfo) {
294
+ return new Promise((resolve, reject) => {
295
+ const deleteRequest = indexedDB.deleteDatabase(dbInfo.name);
296
+ deleteRequest.onsuccess = async () => {
297
+ // Remove from registry
298
+ if (this.metaDb) {
299
+ const tx = this.metaDb.transaction(['databases'], 'readwrite');
300
+ const store = tx.objectStore('databases');
301
+ store.delete(dbInfo.name);
302
+ }
303
+ resolve();
304
+ };
305
+ deleteRequest.onerror = () => {
306
+ getContext().observability.breadcrumb(`Failed to delete workspace database: ${dbInfo.name}`, 'sync.database', 'error');
307
+ reject(deleteRequest.error);
308
+ };
309
+ deleteRequest.onblocked = () => {
310
+ getContext().observability.breadcrumb(`Database deletion blocked: ${dbInfo.name}`, 'sync.database', 'warning');
311
+ // Could implement retry logic or user notification
312
+ };
313
+ });
314
+ }
315
+ /**
316
+ * Get comprehensive database statistics
317
+ */
318
+ async getDatabaseStatistics() {
319
+ if (!this.metaDb) {
320
+ return {
321
+ metaDatabaseSize: 0,
322
+ totalWorkspaceDatabases: 0,
323
+ databasesByUser: {},
324
+ schemaVersions: {},
325
+ };
326
+ }
327
+ return new Promise((resolve, reject) => {
328
+ const tx = this.metaDb.transaction(['databases'], 'readonly');
329
+ const store = tx.objectStore('databases');
330
+ const request = store.getAll();
331
+ request.onsuccess = () => {
332
+ const databases = request.result;
333
+ const databasesByUser = {};
334
+ const schemaVersions = {};
335
+ for (const db of databases) {
336
+ // Count by user
337
+ databasesByUser[db.userId] = (databasesByUser[db.userId] || 0) + 1;
338
+ // Count schema versions
339
+ const versionKey = `v${db.schemaVersion}`;
340
+ schemaVersions[versionKey] = (schemaVersions[versionKey] || 0) + 1;
341
+ }
342
+ resolve({
343
+ metaDatabaseSize: databases.length,
344
+ totalWorkspaceDatabases: databases.length,
345
+ databasesByUser,
346
+ schemaVersions,
347
+ });
348
+ };
349
+ request.onerror = () => reject(request.error);
350
+ });
351
+ }
352
+ /**
353
+ * Close all database connections
354
+ */
355
+ async close() {
356
+ if (this.metaDb) {
357
+ this.metaDb.close();
358
+ this.metaDb = null;
359
+ }
360
+ }
361
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * QueryProcessor - Centralized query processing for the sync engine
3
+ *
4
+ * Responsibilities:
5
+ * - Complex filtering, sorting, and pagination logic
6
+ * - Query optimization and caching strategies
7
+ * - Predicate evaluation and result processing
8
+ *
9
+ * This extracts query processing logic from SyncedStore for proper separation of concerns
10
+ */
11
+ import type { Model } from '../Model.js';
12
+ import type { ModelScope } from '../ObjectPool.js';
13
+ export interface QueryOptions<T extends Model> {
14
+ predicate?: (model: T) => boolean;
15
+ /** Stable key to distinguish different predicates for the same model type.
16
+ * Required when multiple predicate queries exist for the same model — without this,
17
+ * they share a cache key and thrash each other's cached result every render. */
18
+ predicateKey?: string;
19
+ scope?: ModelScope;
20
+ orderBy?: keyof T;
21
+ order?: 'asc' | 'desc';
22
+ limit?: number;
23
+ offset?: number;
24
+ skipCache?: boolean;
25
+ }
26
+ export interface QueryResult<T extends Model> {
27
+ data: T[];
28
+ total: number;
29
+ hasMore: boolean;
30
+ fromCache?: boolean;
31
+ }
32
+ export declare class QueryProcessor {
33
+ private cache;
34
+ private enableCache;
35
+ private predicateResultCache;
36
+ constructor(config?: {
37
+ enableCache?: boolean;
38
+ });
39
+ /**
40
+ * Process query with filtering, sorting, and pagination
41
+ */
42
+ processQuery<T extends Model>(models: T[], modelName: string, options?: QueryOptions<T>): QueryResult<T>;
43
+ /**
44
+ * Sort models by field
45
+ */
46
+ private sortModels;
47
+ /**
48
+ * Find first matching model with predicate
49
+ */
50
+ findFirst<T extends Model>(models: Generator<T, void, unknown>, predicate: (model: T) => boolean): T | undefined;
51
+ /**
52
+ * Count models with optional predicate
53
+ */
54
+ countModels<T extends Model>(models: Generator<T, void, unknown>, predicate?: (model: T) => boolean): number;
55
+ /**
56
+ * Generate a deterministic cache key from query parameters.
57
+ * Functions (predicates) are excluded from the key — predicate queries use
58
+ * the structural identity cache (predicateResultCache) instead of the
59
+ * string-based cache.
60
+ */
61
+ private generateCacheKey;
62
+ /**
63
+ * Invalidate cache by pattern
64
+ */
65
+ invalidateCache(pattern?: string): void;
66
+ /**
67
+ * Clear all cache
68
+ */
69
+ clearCache(): void;
70
+ /**
71
+ * Get cache stats for debugging
72
+ */
73
+ getCacheStats(): {
74
+ size: number;
75
+ enabled: boolean;
76
+ };
77
+ }
@@ -0,0 +1,262 @@
1
+ /**
2
+ * QueryProcessor - Centralized query processing for the sync engine
3
+ *
4
+ * Responsibilities:
5
+ * - Complex filtering, sorting, and pagination logic
6
+ * - Query optimization and caching strategies
7
+ * - Predicate evaluation and result processing
8
+ *
9
+ * This extracts query processing logic from SyncedStore for proper separation of concerns
10
+ */
11
+ /**
12
+ * Optimized in-memory cache implementation
13
+ *
14
+ * 2025 Best Practice: O(1) invalidation by model type instead of O(n) regex matching
15
+ * - Maintains a reverse index from model type to cache keys
16
+ * - Invalidation by model type is O(k) where k = keys for that model type
17
+ * - No regex compilation or full cache iteration needed
18
+ */
19
+ class BasicQueryCache {
20
+ cache = new Map();
21
+ // Reverse index: model type -> set of cache keys for that type
22
+ modelTypeIndex = new Map();
23
+ get(key) {
24
+ return this.cache.get(key);
25
+ }
26
+ get size() {
27
+ return this.cache.size;
28
+ }
29
+ set(key, data) {
30
+ this.cache.set(key, data);
31
+ // Extract model type from cache key (format: "operation:ModelType:options")
32
+ const modelType = this.extractModelType(key);
33
+ if (modelType) {
34
+ if (!this.modelTypeIndex.has(modelType)) {
35
+ this.modelTypeIndex.set(modelType, new Set());
36
+ }
37
+ this.modelTypeIndex.get(modelType).add(key);
38
+ }
39
+ }
40
+ /**
41
+ * Optimized invalidation - O(k) where k = keys for model type
42
+ * Supports both exact model type names and regex patterns (fallback)
43
+ */
44
+ invalidate(pattern) {
45
+ if (!pattern) {
46
+ this.cache.clear();
47
+ this.modelTypeIndex.clear();
48
+ return;
49
+ }
50
+ // Fast path: Check if pattern is a simple model type match like ".*ModelType.*"
51
+ const simpleMatch = pattern.match(/^\.\*(\w+)\.\*$/);
52
+ if (simpleMatch) {
53
+ const modelType = simpleMatch[1];
54
+ const keysToDelete = this.modelTypeIndex.get(modelType);
55
+ if (keysToDelete) {
56
+ for (const key of keysToDelete) {
57
+ this.cache.delete(key);
58
+ }
59
+ this.modelTypeIndex.delete(modelType);
60
+ }
61
+ return;
62
+ }
63
+ // Slow path fallback: regex matching for complex patterns
64
+ // This should rarely be needed with proper model type patterns
65
+ const regex = new RegExp(pattern);
66
+ const keysToDelete = [];
67
+ for (const key of this.cache.keys()) {
68
+ if (regex.test(key)) {
69
+ keysToDelete.push(key);
70
+ }
71
+ }
72
+ // Batch delete to avoid iterator invalidation
73
+ for (const key of keysToDelete) {
74
+ this.cache.delete(key);
75
+ // Clean up index
76
+ const modelType = this.extractModelType(key);
77
+ if (modelType) {
78
+ this.modelTypeIndex.get(modelType)?.delete(key);
79
+ }
80
+ }
81
+ }
82
+ clear() {
83
+ this.cache.clear();
84
+ this.modelTypeIndex.clear();
85
+ }
86
+ /**
87
+ * Extract model type from cache key
88
+ * Cache key format: "operation:ModelType:options"
89
+ */
90
+ extractModelType(key) {
91
+ const parts = key.split(':');
92
+ return parts.length >= 2 ? parts[1] : null;
93
+ }
94
+ }
95
+ export class QueryProcessor {
96
+ cache;
97
+ enableCache;
98
+ // Stable-reference cache for predicate queries.
99
+ // String-based cache keys can't represent closures, so we use a separate
100
+ // identity-based cache that compares result model IDs. This follows the same
101
+ // principle as MobX's comparer.structural — return the previous reference
102
+ // when the structural content hasn't changed.
103
+ // Key: deterministic portion of query (modelName + serializable options)
104
+ // Value: previous result + its ID fingerprint
105
+ predicateResultCache = new Map();
106
+ constructor(config = {}) {
107
+ this.enableCache = config.enableCache ?? true;
108
+ this.cache = new BasicQueryCache();
109
+ }
110
+ /**
111
+ * Process query with filtering, sorting, and pagination
112
+ */
113
+ processQuery(models, modelName, options = {}) {
114
+ // Generate cache key
115
+ const cacheKey = this.generateCacheKey('query', modelName, options);
116
+ // Check string-based cache (non-predicate queries only)
117
+ if (!options.predicate && !options.skipCache && this.enableCache) {
118
+ const cached = this.cache.get(cacheKey);
119
+ if (cached) {
120
+ return { ...cached, fromCache: true };
121
+ }
122
+ }
123
+ // Apply predicate filter
124
+ let filtered = options.predicate ? models.filter(options.predicate) : models;
125
+ // Sort models
126
+ if (options.orderBy) {
127
+ filtered = this.sortModels(filtered, options.orderBy, options.order);
128
+ }
129
+ const total = filtered.length;
130
+ // Apply pagination
131
+ const offset = options.offset || 0;
132
+ const limit = options.limit || filtered.length;
133
+ const data = filtered.slice(offset, offset + limit);
134
+ const result = {
135
+ data,
136
+ total,
137
+ hasMore: offset + limit < total,
138
+ fromCache: false,
139
+ };
140
+ // For predicate queries: use structural identity comparison
141
+ // Return the previous array reference if model IDs haven't changed.
142
+ // This is the query-layer equivalent of MobX's comparer.structural —
143
+ // observers won't re-render when the result is structurally identical.
144
+ if (options.predicate && this.enableCache) {
145
+ const ids = data.map((m) => m.id).join(',');
146
+ const cached = this.predicateResultCache.get(cacheKey);
147
+ if (cached && cached.ids === ids) {
148
+ // Structural match — return previous reference for stability
149
+ return { ...cached.result, fromCache: true };
150
+ }
151
+ // New result — store for future comparison
152
+ this.predicateResultCache.set(cacheKey, { ids, result });
153
+ }
154
+ // For non-predicate queries: use string-based cache as before
155
+ if (!options.predicate && this.enableCache) {
156
+ this.cache.set(cacheKey, result);
157
+ }
158
+ return result;
159
+ }
160
+ /**
161
+ * Sort models by field
162
+ */
163
+ sortModels(models, field, order = 'asc') {
164
+ return [...models].sort((a, b) => {
165
+ const aVal = a[field];
166
+ const bVal = b[field];
167
+ if (aVal === bVal)
168
+ return 0;
169
+ if (aVal === null || aVal === undefined)
170
+ return 1;
171
+ if (bVal === null || bVal === undefined)
172
+ return -1;
173
+ const comparison = aVal < bVal ? -1 : 1;
174
+ return order === 'asc' ? comparison : -comparison;
175
+ });
176
+ }
177
+ /**
178
+ * Find first matching model with predicate
179
+ */
180
+ findFirst(models, predicate) {
181
+ for (const model of models) {
182
+ if (predicate(model)) {
183
+ return model;
184
+ }
185
+ }
186
+ return undefined;
187
+ }
188
+ /**
189
+ * Count models with optional predicate
190
+ */
191
+ countModels(models, predicate) {
192
+ let count = 0;
193
+ for (const model of models) {
194
+ if (!predicate || predicate(model)) {
195
+ count++;
196
+ }
197
+ }
198
+ return count;
199
+ }
200
+ /**
201
+ * Generate a deterministic cache key from query parameters.
202
+ * Functions (predicates) are excluded from the key — predicate queries use
203
+ * the structural identity cache (predicateResultCache) instead of the
204
+ * string-based cache.
205
+ */
206
+ generateCacheKey(operation, modelName, options) {
207
+ const serializableOptions = {};
208
+ let hasPredicate = false;
209
+ for (const [key, value] of Object.entries(options)) {
210
+ if (typeof value === 'function') {
211
+ hasPredicate = true;
212
+ // Mark that this query has a predicate, but use a stable marker
213
+ // (not Math.random). The actual caching for predicate queries
214
+ // is handled by predicateResultCache using ID comparison.
215
+ serializableOptions[key] = `__predicate__`;
216
+ }
217
+ else {
218
+ serializableOptions[key] = value;
219
+ }
220
+ }
221
+ const sortedOptions = JSON.stringify(serializableOptions, Object.keys(serializableOptions).sort());
222
+ const key = `${operation}:${modelName}:${sortedOptions}`;
223
+ return hasPredicate ? `pred:${key}` : key;
224
+ }
225
+ /**
226
+ * Invalidate cache by pattern
227
+ */
228
+ invalidateCache(pattern) {
229
+ this.cache.invalidate(pattern);
230
+ // Also invalidate predicate result cache for this model type
231
+ if (pattern) {
232
+ const simpleMatch = pattern.match(/^\.\*(\w+)\.\*$/);
233
+ if (simpleMatch) {
234
+ const modelType = simpleMatch[1];
235
+ for (const key of this.predicateResultCache.keys()) {
236
+ if (key.includes(modelType)) {
237
+ this.predicateResultCache.delete(key);
238
+ }
239
+ }
240
+ }
241
+ }
242
+ else {
243
+ this.predicateResultCache.clear();
244
+ }
245
+ }
246
+ /**
247
+ * Clear all cache
248
+ */
249
+ clearCache() {
250
+ this.cache.clear();
251
+ this.predicateResultCache.clear();
252
+ }
253
+ /**
254
+ * Get cache stats for debugging
255
+ */
256
+ getCacheStats() {
257
+ return {
258
+ size: this.cache.size,
259
+ enabled: this.enableCache,
260
+ };
261
+ }
262
+ }