@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.
- package/CHANGELOG.md +208 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/dist/BaseSyncedStore.d.ts +709 -0
- package/dist/BaseSyncedStore.js +1843 -0
- package/dist/Database.d.ts +344 -0
- package/dist/Database.js +1259 -0
- package/dist/LazyReferenceCollection.d.ts +181 -0
- package/dist/LazyReferenceCollection.js +460 -0
- package/dist/Model.d.ts +339 -0
- package/dist/Model.js +715 -0
- package/dist/ModelRegistry.d.ts +200 -0
- package/dist/ModelRegistry.js +535 -0
- package/dist/NetworkMonitor.d.ts +27 -0
- package/dist/NetworkMonitor.js +73 -0
- package/dist/ObjectPool.d.ts +202 -0
- package/dist/ObjectPool.js +1106 -0
- package/dist/SyncClient.d.ts +489 -0
- package/dist/SyncClient.js +1555 -0
- package/dist/SyncEngineContext.d.ts +46 -0
- package/dist/SyncEngineContext.js +74 -0
- package/dist/adapters/alwaysOnline.d.ts +16 -0
- package/dist/adapters/alwaysOnline.js +19 -0
- package/dist/adapters/inMemoryStorage.d.ts +30 -0
- package/dist/adapters/inMemoryStorage.js +94 -0
- package/dist/agent/Agent.d.ts +358 -0
- package/dist/agent/Agent.js +500 -0
- package/dist/agent/index.d.ts +115 -0
- package/dist/agent/index.js +128 -0
- package/dist/agent/session.d.ts +90 -0
- package/dist/agent/session.js +156 -0
- package/dist/agent/types.d.ts +73 -0
- package/dist/agent/types.js +10 -0
- package/dist/ai-sdk/coordination-context.d.ts +51 -0
- package/dist/ai-sdk/coordination-context.js +107 -0
- package/dist/ai-sdk/index.d.ts +68 -0
- package/dist/ai-sdk/index.js +68 -0
- package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
- package/dist/ai-sdk/intent-broadcast.js +72 -0
- package/dist/ai-sdk/wrap.d.ts +67 -0
- package/dist/ai-sdk/wrap.js +45 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +9 -0
- package/dist/auth/index.d.ts +137 -0
- package/dist/auth/index.js +246 -0
- package/dist/client/Ablo.d.ts +835 -0
- package/dist/client/Ablo.js +1440 -0
- package/dist/client/ApiClient.d.ts +200 -0
- package/dist/client/ApiClient.js +659 -0
- package/dist/client/auth.d.ts +79 -0
- package/dist/client/auth.js +81 -0
- package/dist/client/createInternalComponents.d.ts +44 -0
- package/dist/client/createInternalComponents.js +88 -0
- package/dist/client/createModelProxy.d.ts +152 -0
- package/dist/client/createModelProxy.js +199 -0
- package/dist/client/identity.d.ts +63 -0
- package/dist/client/identity.js +156 -0
- package/dist/client/index.d.ts +36 -0
- package/dist/client/index.js +33 -0
- package/dist/client/persistence.d.ts +7 -0
- package/dist/client/persistence.js +11 -0
- package/dist/client/validateAbloOptions.d.ts +42 -0
- package/dist/client/validateAbloOptions.js +43 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +12 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.js +58 -0
- package/dist/core/DatabaseManager.d.ts +108 -0
- package/dist/core/DatabaseManager.js +361 -0
- package/dist/core/QueryProcessor.d.ts +77 -0
- package/dist/core/QueryProcessor.js +262 -0
- package/dist/core/QueryView.d.ts +64 -0
- package/dist/core/QueryView.js +219 -0
- package/dist/core/StoreManager.d.ts +131 -0
- package/dist/core/StoreManager.js +334 -0
- package/dist/core/ViewRegistry.d.ts +20 -0
- package/dist/core/ViewRegistry.js +55 -0
- package/dist/core/index.d.ts +34 -0
- package/dist/core/index.js +59 -0
- package/dist/core/openIDBWithTimeout.d.ts +27 -0
- package/dist/core/openIDBWithTimeout.js +63 -0
- package/dist/core/query-utils.d.ts +37 -0
- package/dist/core/query-utils.js +60 -0
- package/dist/errors.d.ts +235 -0
- package/dist/errors.js +243 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +82 -0
- package/dist/interfaces/headless.d.ts +95 -0
- package/dist/interfaces/headless.js +41 -0
- package/dist/interfaces/index.d.ts +321 -0
- package/dist/interfaces/index.js +8 -0
- package/dist/mutators/RecordingTransaction.d.ts +36 -0
- package/dist/mutators/RecordingTransaction.js +216 -0
- package/dist/mutators/Transaction.d.ts +48 -0
- package/dist/mutators/Transaction.js +64 -0
- package/dist/mutators/UndoManager.d.ts +114 -0
- package/dist/mutators/UndoManager.js +143 -0
- package/dist/mutators/defineMutators.d.ts +55 -0
- package/dist/mutators/defineMutators.js +28 -0
- package/dist/policy/index.d.ts +19 -0
- package/dist/policy/index.js +18 -0
- package/dist/policy/types.d.ts +74 -0
- package/dist/policy/types.js +17 -0
- package/dist/principal.d.ts +44 -0
- package/dist/principal.js +49 -0
- package/dist/query/client.d.ts +43 -0
- package/dist/query/client.js +84 -0
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +5 -0
- package/dist/query/types.d.ts +143 -0
- package/dist/query/types.js +36 -0
- package/dist/react/AbloProvider.d.ts +205 -0
- package/dist/react/AbloProvider.js +398 -0
- package/dist/react/ClientSideSuspense.d.ts +36 -0
- package/dist/react/ClientSideSuspense.js +17 -0
- package/dist/react/DefaultFallback.d.ts +24 -0
- package/dist/react/DefaultFallback.js +43 -0
- package/dist/react/SyncGroupProvider.d.ts +19 -0
- package/dist/react/SyncGroupProvider.js +44 -0
- package/dist/react/context.d.ts +161 -0
- package/dist/react/context.js +35 -0
- package/dist/react/index.d.ts +64 -0
- package/dist/react/index.js +73 -0
- package/dist/react/internalContext.d.ts +35 -0
- package/dist/react/internalContext.js +3 -0
- package/dist/react/useAblo.d.ts +72 -0
- package/dist/react/useAblo.js +63 -0
- package/dist/react/useCurrentUserId.d.ts +21 -0
- package/dist/react/useCurrentUserId.js +33 -0
- package/dist/react/useErrorListener.d.ts +20 -0
- package/dist/react/useErrorListener.js +39 -0
- package/dist/react/useIntent.d.ts +29 -0
- package/dist/react/useIntent.js +42 -0
- package/dist/react/useMutate.d.ts +83 -0
- package/dist/react/useMutate.js +122 -0
- package/dist/react/useMutationFailureListener.d.ts +26 -0
- package/dist/react/useMutationFailureListener.js +38 -0
- package/dist/react/useMutators.d.ts +56 -0
- package/dist/react/useMutators.js +66 -0
- package/dist/react/usePresence.d.ts +32 -0
- package/dist/react/usePresence.js +41 -0
- package/dist/react/useQuery.d.ts +123 -0
- package/dist/react/useQuery.js +145 -0
- package/dist/react/useReactive.d.ts +35 -0
- package/dist/react/useReactive.js +111 -0
- package/dist/react/useReader.d.ts +69 -0
- package/dist/react/useReader.js +73 -0
- package/dist/react/useSyncStatus.d.ts +61 -0
- package/dist/react/useSyncStatus.js +76 -0
- package/dist/react/useUndoScope.d.ts +36 -0
- package/dist/react/useUndoScope.js +73 -0
- package/dist/realtime/index.d.ts +10 -0
- package/dist/realtime/index.js +9 -0
- package/dist/schema/field.d.ts +134 -0
- package/dist/schema/field.js +264 -0
- package/dist/schema/index.d.ts +29 -0
- package/dist/schema/index.js +38 -0
- package/dist/schema/model.d.ts +326 -0
- package/dist/schema/model.js +89 -0
- package/dist/schema/queries.d.ts +203 -0
- package/dist/schema/queries.js +145 -0
- package/dist/schema/relation.d.ts +172 -0
- package/dist/schema/relation.js +104 -0
- package/dist/schema/schema.d.ts +259 -0
- package/dist/schema/schema.js +188 -0
- package/dist/schema/sugar.d.ts +129 -0
- package/dist/schema/sugar.js +94 -0
- package/dist/source/index.d.ts +423 -0
- package/dist/source/index.js +320 -0
- package/dist/source/pushQueue.d.ts +112 -0
- package/dist/source/pushQueue.js +249 -0
- package/dist/stores/ObjectStore.d.ts +103 -0
- package/dist/stores/ObjectStore.js +371 -0
- package/dist/stores/ObjectStoreContract.d.ts +39 -0
- package/dist/stores/ObjectStoreContract.js +1 -0
- package/dist/stores/SyncActionStore.d.ts +101 -0
- package/dist/stores/SyncActionStore.js +481 -0
- package/dist/sync/BootstrapHelper.d.ts +127 -0
- package/dist/sync/BootstrapHelper.js +434 -0
- package/dist/sync/ConnectionManager.d.ts +136 -0
- package/dist/sync/ConnectionManager.js +465 -0
- package/dist/sync/HydrationCoordinator.d.ts +137 -0
- package/dist/sync/HydrationCoordinator.js +468 -0
- package/dist/sync/NetworkProbe.d.ts +43 -0
- package/dist/sync/NetworkProbe.js +113 -0
- package/dist/sync/OfflineFlush.d.ts +9 -0
- package/dist/sync/OfflineFlush.js +22 -0
- package/dist/sync/OfflineTransactionStore.d.ts +37 -0
- package/dist/sync/OfflineTransactionStore.js +263 -0
- package/dist/sync/SyncWebSocket.d.ts +663 -0
- package/dist/sync/SyncWebSocket.js +1336 -0
- package/dist/sync/createIntentStream.d.ts +33 -0
- package/dist/sync/createIntentStream.js +243 -0
- package/dist/sync/createPresenceStream.d.ts +46 -0
- package/dist/sync/createPresenceStream.js +192 -0
- package/dist/sync/createSnapshot.d.ts +33 -0
- package/dist/sync/createSnapshot.js +124 -0
- package/dist/sync/participants.d.ts +114 -0
- package/dist/sync/participants.js +336 -0
- package/dist/sync/schemas.d.ts +79 -0
- package/dist/sync/schemas.js +78 -0
- package/dist/testing/fixtures/bootstrap.d.ts +45 -0
- package/dist/testing/fixtures/bootstrap.js +53 -0
- package/dist/testing/fixtures/deltas.d.ts +86 -0
- package/dist/testing/fixtures/deltas.js +139 -0
- package/dist/testing/fixtures/models.d.ts +82 -0
- package/dist/testing/fixtures/models.js +270 -0
- package/dist/testing/helpers/react-wrapper.d.ts +66 -0
- package/dist/testing/helpers/react-wrapper.js +64 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
- package/dist/testing/helpers/sync-engine-harness.js +70 -0
- package/dist/testing/helpers/wait.d.ts +25 -0
- package/dist/testing/helpers/wait.js +44 -0
- package/dist/testing/index.d.ts +21 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
- package/dist/testing/mocks/MockMutationExecutor.js +139 -0
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
- package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
- package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
- package/dist/testing/mocks/MockSyncContext.js +100 -0
- package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
- package/dist/testing/mocks/MockSyncStore.js +171 -0
- package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
- package/dist/testing/mocks/MockWebSocket.js +117 -0
- package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
- package/dist/transactions/OptimisticEchoTracker.js +104 -0
- package/dist/transactions/TransactionQueue.d.ts +499 -0
- package/dist/transactions/TransactionQueue.js +1895 -0
- package/dist/transactions/index.d.ts +16 -0
- package/dist/transactions/index.js +7 -0
- package/dist/transactions/mutation-error-handler.d.ts +5 -0
- package/dist/transactions/mutation-error-handler.js +39 -0
- package/dist/types/global.d.ts +107 -0
- package/dist/types/global.js +38 -0
- package/dist/types/index.d.ts +241 -0
- package/dist/types/index.js +70 -0
- package/dist/types/streams.d.ts +495 -0
- package/dist/types/streams.js +11 -0
- package/dist/utils/asyncIterator.d.ts +41 -0
- package/dist/utils/asyncIterator.js +142 -0
- package/dist/utils/duration.d.ts +28 -0
- package/dist/utils/duration.js +47 -0
- package/dist/utils/mobx-setup.d.ts +42 -0
- package/dist/utils/mobx-setup.js +381 -0
- package/docs/api-keys.md +24 -0
- package/docs/api.md +230 -0
- package/docs/audit.md +81 -0
- package/docs/capabilities.md +163 -0
- package/docs/client-behavior.md +202 -0
- package/docs/data-sources.md +214 -0
- package/docs/examples/agent-human.md +84 -0
- package/docs/examples/ai-sdk-tool.md +92 -0
- package/docs/examples/existing-python-backend.md +249 -0
- package/docs/examples/nextjs.md +88 -0
- package/docs/examples/server-agent.md +86 -0
- package/docs/guarantees.md +148 -0
- package/docs/index.md +97 -0
- package/docs/integration-guide.md +493 -0
- package/docs/interaction-model.md +140 -0
- package/docs/mcp/claude-code.md +43 -0
- package/docs/mcp/cursor.md +53 -0
- package/docs/mcp/windsurf.md +46 -0
- package/docs/mcp.md +59 -0
- package/docs/quickstart.md +152 -0
- package/docs/react.md +115 -0
- package/docs/roadmap.md +45 -0
- package/examples/README.md +54 -0
- package/examples/data-source/README.md +102 -0
- package/examples/data-source/ablo-driver.ts +89 -0
- package/examples/data-source/customer-server.ts +208 -0
- package/examples/data-source/run.ts +101 -0
- package/examples/data-source/schema.ts +25 -0
- package/examples/quickstart.ts +54 -0
- package/examples/tsconfig.json +16 -0
- package/llms.txt +143 -0
- 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
|
+
}
|