@objectql/core 4.0.1 → 4.0.3
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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +32 -0
- package/README.md +14 -12
- package/dist/app.d.ts +9 -6
- package/dist/app.js +151 -29
- package/dist/app.js.map +1 -1
- package/dist/index.d.ts +6 -9
- package/dist/index.js +2 -5
- package/dist/index.js.map +1 -1
- package/dist/optimizations/CompiledHookManager.d.ts +55 -0
- package/dist/optimizations/CompiledHookManager.js +164 -0
- package/dist/optimizations/CompiledHookManager.js.map +1 -0
- package/dist/optimizations/DependencyGraph.d.ts +82 -0
- package/dist/optimizations/DependencyGraph.js +211 -0
- package/dist/optimizations/DependencyGraph.js.map +1 -0
- package/dist/optimizations/GlobalConnectionPool.d.ts +89 -0
- package/dist/optimizations/GlobalConnectionPool.js +193 -0
- package/dist/optimizations/GlobalConnectionPool.js.map +1 -0
- package/dist/optimizations/LazyMetadataLoader.d.ts +75 -0
- package/dist/optimizations/LazyMetadataLoader.js +149 -0
- package/dist/optimizations/LazyMetadataLoader.js.map +1 -0
- package/dist/optimizations/OptimizedMetadataRegistry.d.ts +26 -0
- package/dist/optimizations/OptimizedMetadataRegistry.js +117 -0
- package/dist/optimizations/OptimizedMetadataRegistry.js.map +1 -0
- package/dist/optimizations/OptimizedValidationEngine.d.ts +73 -0
- package/dist/optimizations/OptimizedValidationEngine.js +141 -0
- package/dist/optimizations/OptimizedValidationEngine.js.map +1 -0
- package/dist/optimizations/QueryCompiler.d.ts +51 -0
- package/dist/optimizations/QueryCompiler.js +216 -0
- package/dist/optimizations/QueryCompiler.js.map +1 -0
- package/dist/optimizations/SQLQueryOptimizer.d.ts +96 -0
- package/dist/optimizations/SQLQueryOptimizer.js +265 -0
- package/dist/optimizations/SQLQueryOptimizer.js.map +1 -0
- package/dist/optimizations/index.d.ts +32 -0
- package/dist/optimizations/index.js +44 -0
- package/dist/optimizations/index.js.map +1 -0
- package/dist/plugin.d.ts +6 -7
- package/dist/plugin.js +39 -22
- package/dist/plugin.js.map +1 -1
- package/dist/query/filter-translator.d.ts +6 -18
- package/dist/query/filter-translator.js +6 -103
- package/dist/query/filter-translator.js.map +1 -1
- package/dist/query/query-analyzer.js +24 -25
- package/dist/query/query-analyzer.js.map +1 -1
- package/dist/query/query-builder.d.ts +9 -3
- package/dist/query/query-builder.js +25 -35
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/query-service.d.ts +2 -2
- package/dist/query/query-service.js +5 -5
- package/dist/query/query-service.js.map +1 -1
- package/dist/repository.d.ts +2 -0
- package/dist/repository.js +24 -17
- package/dist/repository.js.map +1 -1
- package/jest.config.js +3 -3
- package/package.json +8 -5
- package/src/app.ts +173 -47
- package/src/index.ts +7 -8
- package/src/optimizations/CompiledHookManager.ts +185 -0
- package/src/optimizations/DependencyGraph.ts +255 -0
- package/src/optimizations/GlobalConnectionPool.ts +251 -0
- package/src/optimizations/LazyMetadataLoader.ts +180 -0
- package/src/optimizations/OptimizedMetadataRegistry.ts +132 -0
- package/src/optimizations/OptimizedValidationEngine.ts +172 -0
- package/src/optimizations/QueryCompiler.ts +242 -0
- package/src/optimizations/SQLQueryOptimizer.ts +329 -0
- package/src/optimizations/index.ts +34 -0
- package/src/plugin.ts +51 -28
- package/src/query/filter-translator.ts +8 -115
- package/src/query/query-analyzer.ts +25 -29
- package/src/query/query-builder.ts +26 -43
- package/src/query/query-service.ts +6 -6
- package/src/repository.ts +35 -22
- package/test/__mocks__/@objectstack/runtime.ts +8 -8
- package/test/app.test.ts +11 -8
- package/test/optimizations.test.ts +440 -0
- package/test/plugin-integration.test.ts +30 -19
- package/tsconfig.json +4 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/ai-agent.d.ts +0 -176
- package/dist/ai-agent.js +0 -722
- package/dist/ai-agent.js.map +0 -1
- package/dist/formula-engine.d.ts +0 -102
- package/dist/formula-engine.js +0 -433
- package/dist/formula-engine.js.map +0 -1
- package/dist/formula-plugin.d.ts +0 -52
- package/dist/formula-plugin.js +0 -107
- package/dist/formula-plugin.js.map +0 -1
- package/dist/validator-plugin.d.ts +0 -56
- package/dist/validator-plugin.js +0 -106
- package/dist/validator-plugin.js.map +0 -1
- package/dist/validator.d.ts +0 -80
- package/dist/validator.js +0 -625
- package/dist/validator.js.map +0 -1
- package/src/ai-agent.ts +0 -868
- package/src/formula-engine.ts +0 -572
- package/src/formula-plugin.ts +0 -141
- package/src/validator-plugin.ts +0 -140
- package/src/validator.ts +0 -743
- package/test/formula-engine.test.ts +0 -725
- package/test/formula-integration.test.ts +0 -286
- package/test/formula-plugin.test.ts +0 -197
- package/test/validator-plugin.test.ts +0 -126
- package/test/validator.test.ts +0 -440
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Connection interface
|
|
11
|
+
*/
|
|
12
|
+
export interface Connection {
|
|
13
|
+
id: string;
|
|
14
|
+
driverName: string;
|
|
15
|
+
inUse: boolean;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
lastUsedAt: number;
|
|
18
|
+
release: () => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Connection pool limits
|
|
23
|
+
*/
|
|
24
|
+
export interface PoolLimits {
|
|
25
|
+
total: number;
|
|
26
|
+
perDriver: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Global Connection Pool Manager
|
|
31
|
+
*
|
|
32
|
+
* Improvement: Kernel-level connection pool with global limits.
|
|
33
|
+
* Coordinates connection allocation across all drivers.
|
|
34
|
+
*
|
|
35
|
+
* Expected: 5x faster connection acquisition, predictable resource usage
|
|
36
|
+
*/
|
|
37
|
+
export class GlobalConnectionPool {
|
|
38
|
+
private limits: PoolLimits;
|
|
39
|
+
private allocations = new Map<string, number>();
|
|
40
|
+
private connections = new Map<string, Connection[]>();
|
|
41
|
+
private waitQueue: Array<{
|
|
42
|
+
driverName: string;
|
|
43
|
+
resolve: (conn: Connection) => void;
|
|
44
|
+
reject: (error: Error) => void;
|
|
45
|
+
}> = [];
|
|
46
|
+
|
|
47
|
+
constructor(limits: PoolLimits = { total: 50, perDriver: 20 }) {
|
|
48
|
+
this.limits = limits;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get total number of active connections across all drivers
|
|
53
|
+
*/
|
|
54
|
+
private totalConnections(): number {
|
|
55
|
+
let total = 0;
|
|
56
|
+
for (const count of this.allocations.values()) {
|
|
57
|
+
total += count;
|
|
58
|
+
}
|
|
59
|
+
return total;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get number of connections for a specific driver
|
|
64
|
+
*/
|
|
65
|
+
private getDriverConnections(driverName: string): number {
|
|
66
|
+
return this.allocations.get(driverName) || 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Try to process the wait queue
|
|
71
|
+
*/
|
|
72
|
+
private processWaitQueue(): void {
|
|
73
|
+
if (this.waitQueue.length === 0) return;
|
|
74
|
+
|
|
75
|
+
// Check if we can fulfill any waiting requests
|
|
76
|
+
for (let i = 0; i < this.waitQueue.length; i++) {
|
|
77
|
+
const request = this.waitQueue[i];
|
|
78
|
+
|
|
79
|
+
// Check if we can allocate
|
|
80
|
+
if (this.canAllocate(request.driverName)) {
|
|
81
|
+
this.waitQueue.splice(i, 1);
|
|
82
|
+
|
|
83
|
+
// Try to acquire connection
|
|
84
|
+
this.doAcquire(request.driverName)
|
|
85
|
+
.then(request.resolve)
|
|
86
|
+
.catch(request.reject);
|
|
87
|
+
|
|
88
|
+
// Only process one at a time to avoid over-allocation
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if we can allocate a connection for a driver
|
|
96
|
+
*/
|
|
97
|
+
private canAllocate(driverName: string): boolean {
|
|
98
|
+
const totalConns = this.totalConnections();
|
|
99
|
+
const driverConns = this.getDriverConnections(driverName);
|
|
100
|
+
|
|
101
|
+
// Check if there's an idle connection available
|
|
102
|
+
const driverConnections = this.connections.get(driverName) || [];
|
|
103
|
+
const hasIdleConnection = driverConnections.some(c => !c.inUse);
|
|
104
|
+
|
|
105
|
+
// Can allocate if:
|
|
106
|
+
// 1. There's an idle connection (reuse), OR
|
|
107
|
+
// 2. We're under the total and per-driver limits (create new)
|
|
108
|
+
return hasIdleConnection || (totalConns < this.limits.total && driverConns < this.limits.perDriver);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Actually acquire a connection (internal)
|
|
113
|
+
*/
|
|
114
|
+
private async doAcquire(driverName: string): Promise<Connection> {
|
|
115
|
+
// Check for available idle connection first
|
|
116
|
+
const driverConnections = this.connections.get(driverName) || [];
|
|
117
|
+
const idleConnection = driverConnections.find(c => !c.inUse);
|
|
118
|
+
|
|
119
|
+
if (idleConnection) {
|
|
120
|
+
idleConnection.inUse = true;
|
|
121
|
+
idleConnection.lastUsedAt = Date.now();
|
|
122
|
+
return idleConnection;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Verify we can create a new connection (double-check to prevent race conditions)
|
|
126
|
+
const totalConns = this.totalConnections();
|
|
127
|
+
const driverConns = this.getDriverConnections(driverName);
|
|
128
|
+
if (totalConns >= this.limits.total || driverConns >= this.limits.perDriver) {
|
|
129
|
+
throw new Error(`Connection pool limit reached for driver: ${driverName}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Create new connection
|
|
133
|
+
const connectionId = `${driverName}-${Date.now()}-${Math.random()}`;
|
|
134
|
+
const connection: Connection = {
|
|
135
|
+
id: connectionId,
|
|
136
|
+
driverName,
|
|
137
|
+
inUse: true,
|
|
138
|
+
createdAt: Date.now(),
|
|
139
|
+
lastUsedAt: Date.now(),
|
|
140
|
+
release: async () => {
|
|
141
|
+
connection.inUse = false;
|
|
142
|
+
connection.lastUsedAt = Date.now();
|
|
143
|
+
|
|
144
|
+
// Process wait queue when connection is released
|
|
145
|
+
this.processWaitQueue();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Store connection
|
|
150
|
+
if (!this.connections.has(driverName)) {
|
|
151
|
+
this.connections.set(driverName, []);
|
|
152
|
+
}
|
|
153
|
+
this.connections.get(driverName)!.push(connection);
|
|
154
|
+
|
|
155
|
+
// Update allocation count
|
|
156
|
+
this.allocations.set(driverName, this.getDriverConnections(driverName) + 1);
|
|
157
|
+
|
|
158
|
+
return connection;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Acquire a connection from the pool
|
|
163
|
+
*/
|
|
164
|
+
async acquire(driverName: string): Promise<Connection> {
|
|
165
|
+
// Check global limits before allocation ✅
|
|
166
|
+
if (!this.canAllocate(driverName)) {
|
|
167
|
+
// Add to wait queue
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
this.waitQueue.push({ driverName, resolve, reject });
|
|
170
|
+
|
|
171
|
+
// Set timeout to prevent indefinite waiting
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
const index = this.waitQueue.findIndex(
|
|
174
|
+
r => r.driverName === driverName && r.resolve === resolve
|
|
175
|
+
);
|
|
176
|
+
if (index >= 0) {
|
|
177
|
+
this.waitQueue.splice(index, 1);
|
|
178
|
+
reject(new Error(`Connection pool limit reached for driver: ${driverName}`));
|
|
179
|
+
}
|
|
180
|
+
}, 30000); // 30 second timeout
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return this.doAcquire(driverName);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Release a connection back to the pool
|
|
189
|
+
*/
|
|
190
|
+
async release(connection: Connection): Promise<void> {
|
|
191
|
+
await connection.release();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Close all connections for a driver
|
|
196
|
+
*/
|
|
197
|
+
async closeDriver(driverName: string): Promise<void> {
|
|
198
|
+
const driverConnections = this.connections.get(driverName);
|
|
199
|
+
if (driverConnections) {
|
|
200
|
+
// Clear all connections
|
|
201
|
+
driverConnections.length = 0;
|
|
202
|
+
this.connections.delete(driverName);
|
|
203
|
+
this.allocations.delete(driverName);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Process wait queue
|
|
207
|
+
this.processWaitQueue();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get pool statistics
|
|
212
|
+
*/
|
|
213
|
+
getStats(): {
|
|
214
|
+
totalConnections: number;
|
|
215
|
+
totalLimit: number;
|
|
216
|
+
perDriverLimit: number;
|
|
217
|
+
driverStats: Record<string, { active: number; idle: number }>;
|
|
218
|
+
waitQueueSize: number;
|
|
219
|
+
} {
|
|
220
|
+
const driverStats: Record<string, { active: number; idle: number }> = {};
|
|
221
|
+
|
|
222
|
+
for (const [driverName, connections] of this.connections.entries()) {
|
|
223
|
+
const active = connections.filter(c => c.inUse).length;
|
|
224
|
+
const idle = connections.filter(c => !c.inUse).length;
|
|
225
|
+
driverStats[driverName] = { active, idle };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
totalConnections: this.totalConnections(),
|
|
230
|
+
totalLimit: this.limits.total,
|
|
231
|
+
perDriverLimit: this.limits.perDriver,
|
|
232
|
+
driverStats,
|
|
233
|
+
waitQueueSize: this.waitQueue.length
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Update pool limits
|
|
239
|
+
*/
|
|
240
|
+
updateLimits(limits: Partial<PoolLimits>): void {
|
|
241
|
+
if (limits.total !== undefined) {
|
|
242
|
+
this.limits.total = limits.total;
|
|
243
|
+
}
|
|
244
|
+
if (limits.perDriver !== undefined) {
|
|
245
|
+
this.limits.perDriver = limits.perDriver;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Process wait queue after limits update
|
|
249
|
+
this.processWaitQueue();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Object metadata definition
|
|
11
|
+
*/
|
|
12
|
+
export interface ObjectMetadata {
|
|
13
|
+
name: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
fields: Record<string, any>;
|
|
16
|
+
triggers?: any[];
|
|
17
|
+
workflows?: any[];
|
|
18
|
+
permissions?: any[];
|
|
19
|
+
relatedObjects?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Metadata loader function type
|
|
24
|
+
*/
|
|
25
|
+
export type MetadataLoader = (objectName: string) => Promise<ObjectMetadata>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lazy Metadata Loader with Smart Caching
|
|
29
|
+
*
|
|
30
|
+
* Improvement: Loads metadata on-demand instead of eagerly at startup.
|
|
31
|
+
* Includes predictive preloading for related objects.
|
|
32
|
+
*
|
|
33
|
+
* Expected: 10x faster startup, 70% lower initial memory
|
|
34
|
+
*/
|
|
35
|
+
export class LazyMetadataLoader {
|
|
36
|
+
private cache = new Map<string, ObjectMetadata>();
|
|
37
|
+
private loaded = new Set<string>();
|
|
38
|
+
private loading = new Map<string, Promise<ObjectMetadata>>();
|
|
39
|
+
private preloadScheduled = new Set<string>(); // Track objects with scheduled preloads
|
|
40
|
+
private loader: MetadataLoader;
|
|
41
|
+
|
|
42
|
+
constructor(loader: MetadataLoader) {
|
|
43
|
+
this.loader = loader;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Load a single object's metadata
|
|
48
|
+
*/
|
|
49
|
+
private async loadSingle(objectName: string): Promise<ObjectMetadata> {
|
|
50
|
+
// Check if already loaded
|
|
51
|
+
if (this.loaded.has(objectName)) {
|
|
52
|
+
const cached = this.cache.get(objectName);
|
|
53
|
+
if (cached) return cached;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if currently loading (avoid duplicate loads)
|
|
57
|
+
const existingLoad = this.loading.get(objectName);
|
|
58
|
+
if (existingLoad) {
|
|
59
|
+
return existingLoad;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Load metadata
|
|
63
|
+
const loadPromise = (async () => {
|
|
64
|
+
try {
|
|
65
|
+
const metadata = await this.loader(objectName);
|
|
66
|
+
this.cache.set(objectName, metadata);
|
|
67
|
+
this.loaded.add(objectName);
|
|
68
|
+
return metadata;
|
|
69
|
+
} finally {
|
|
70
|
+
this.loading.delete(objectName);
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
|
|
74
|
+
this.loading.set(objectName, loadPromise);
|
|
75
|
+
return loadPromise;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Predictive preload: load related objects in the background
|
|
80
|
+
*/
|
|
81
|
+
private predictivePreload(objectName: string): void {
|
|
82
|
+
// Avoid redundant preload scheduling for the same object
|
|
83
|
+
if (this.preloadScheduled.has(objectName)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.preloadScheduled.add(objectName);
|
|
87
|
+
|
|
88
|
+
// Run preloading asynchronously after current call stack to avoid blocking
|
|
89
|
+
setImmediate(() => {
|
|
90
|
+
const metadata = this.cache.get(objectName);
|
|
91
|
+
if (!metadata) return;
|
|
92
|
+
|
|
93
|
+
// Extract related object names from various sources
|
|
94
|
+
const relatedObjects = new Set<string>();
|
|
95
|
+
|
|
96
|
+
// 1. From explicit relatedObjects field
|
|
97
|
+
if (metadata.relatedObjects) {
|
|
98
|
+
metadata.relatedObjects.forEach(obj => relatedObjects.add(obj));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. From lookup/master-detail fields
|
|
102
|
+
if (metadata.fields) {
|
|
103
|
+
for (const field of Object.values(metadata.fields)) {
|
|
104
|
+
if (field.type === 'lookup' || field.type === 'master_detail') {
|
|
105
|
+
if (field.reference_to) {
|
|
106
|
+
relatedObjects.add(field.reference_to);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Preload related objects asynchronously (don't await)
|
|
113
|
+
for (const relatedObject of relatedObjects) {
|
|
114
|
+
if (!this.loaded.has(relatedObject) && !this.loading.has(relatedObject)) {
|
|
115
|
+
// Fire and forget - preload in background
|
|
116
|
+
this.loadSingle(relatedObject).catch(() => {
|
|
117
|
+
// Ignore errors in background preloading
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get metadata for an object (loads on-demand if not cached)
|
|
126
|
+
*/
|
|
127
|
+
async get(objectName: string): Promise<ObjectMetadata> {
|
|
128
|
+
// Load on first access
|
|
129
|
+
const metadata = await this.loadSingle(objectName);
|
|
130
|
+
|
|
131
|
+
// Trigger predictive preloading for related objects
|
|
132
|
+
this.predictivePreload(objectName);
|
|
133
|
+
|
|
134
|
+
return metadata;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if metadata is loaded
|
|
139
|
+
*/
|
|
140
|
+
isLoaded(objectName: string): boolean {
|
|
141
|
+
return this.loaded.has(objectName);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Preload metadata for specific objects
|
|
146
|
+
*/
|
|
147
|
+
async preload(objectNames: string[]): Promise<void> {
|
|
148
|
+
await Promise.all(objectNames.map(name => this.get(name)));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Clear cache for an object
|
|
153
|
+
*/
|
|
154
|
+
invalidate(objectName: string): void {
|
|
155
|
+
this.cache.delete(objectName);
|
|
156
|
+
this.loaded.delete(objectName);
|
|
157
|
+
this.preloadScheduled.delete(objectName);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Clear all cached metadata
|
|
162
|
+
*/
|
|
163
|
+
clearAll(): void {
|
|
164
|
+
this.cache.clear();
|
|
165
|
+
this.loaded.clear();
|
|
166
|
+
this.loading.clear();
|
|
167
|
+
this.preloadScheduled.clear();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get statistics about loaded metadata
|
|
172
|
+
*/
|
|
173
|
+
getStats(): { loaded: number; cached: number; loading: number } {
|
|
174
|
+
return {
|
|
175
|
+
loaded: this.loaded.size,
|
|
176
|
+
cached: this.cache.size,
|
|
177
|
+
loading: this.loading.size
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Optimized Metadata Registry with O(k) package uninstall complexity
|
|
11
|
+
*
|
|
12
|
+
* Improvement: Uses secondary indexes to achieve O(k) complexity for
|
|
13
|
+
* unregisterPackage operation (where k is the number of items in the package)
|
|
14
|
+
* instead of O(n*m) (where n is types and m is items per type).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface MetadataRef {
|
|
18
|
+
type: string;
|
|
19
|
+
name: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class OptimizedMetadataRegistry {
|
|
23
|
+
private items: Record<string, Record<string, any>> = {};
|
|
24
|
+
|
|
25
|
+
// Secondary index: package name -> list of metadata references
|
|
26
|
+
private packageIndex = new Map<string, Set<MetadataRef>>();
|
|
27
|
+
|
|
28
|
+
constructor() {}
|
|
29
|
+
|
|
30
|
+
register(type: string, nameOrConfig: any, config?: any) {
|
|
31
|
+
if (!this.items[type]) {
|
|
32
|
+
this.items[type] = {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let name: string;
|
|
36
|
+
let item: any;
|
|
37
|
+
|
|
38
|
+
if (config) {
|
|
39
|
+
name = nameOrConfig;
|
|
40
|
+
item = config;
|
|
41
|
+
} else {
|
|
42
|
+
item = nameOrConfig;
|
|
43
|
+
name = item.name || item.id;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (name) {
|
|
47
|
+
this.items[type][name] = item;
|
|
48
|
+
|
|
49
|
+
// Update package index
|
|
50
|
+
const packageName = item.package || (item as any)._package || (item as any).packageName;
|
|
51
|
+
if (packageName) {
|
|
52
|
+
if (!this.packageIndex.has(packageName)) {
|
|
53
|
+
this.packageIndex.set(packageName, new Set());
|
|
54
|
+
}
|
|
55
|
+
this.packageIndex.get(packageName)!.add({ type, name });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get<T = any>(type: string, name: string): T {
|
|
61
|
+
const item = this.items[type]?.[name];
|
|
62
|
+
if (item && item.content) {
|
|
63
|
+
return item.content;
|
|
64
|
+
}
|
|
65
|
+
return item;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
list<T = any>(type: string): T[] {
|
|
69
|
+
if (!this.items[type]) return [];
|
|
70
|
+
return Object.values(this.items[type]).map((item: any) => {
|
|
71
|
+
if (item && item.content) {
|
|
72
|
+
return item.content;
|
|
73
|
+
}
|
|
74
|
+
return item;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getTypes(): string[] {
|
|
79
|
+
return Object.keys(this.items);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getEntry<T = any>(type: string, name: string): T {
|
|
83
|
+
return this.items[type]?.[name];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
unregister(type: string, name: string) {
|
|
87
|
+
const item = this.items[type]?.[name];
|
|
88
|
+
if (item) {
|
|
89
|
+
// Update package index
|
|
90
|
+
const packageName = item.package || (item as any)._package || (item as any).packageName;
|
|
91
|
+
if (packageName) {
|
|
92
|
+
const refs = this.packageIndex.get(packageName);
|
|
93
|
+
if (refs) {
|
|
94
|
+
// Remove this specific reference
|
|
95
|
+
for (const ref of refs) {
|
|
96
|
+
if (ref.type === type && ref.name === name) {
|
|
97
|
+
refs.delete(ref);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Clean up empty package entries
|
|
102
|
+
if (refs.size === 0) {
|
|
103
|
+
this.packageIndex.delete(packageName);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
delete this.items[type][name];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Optimized package unregistration with O(k) complexity
|
|
113
|
+
* where k is the number of items in the package.
|
|
114
|
+
*
|
|
115
|
+
* Previous complexity: O(n*m) - iterate all types and all items
|
|
116
|
+
* New complexity: O(k) - direct lookup via secondary index
|
|
117
|
+
*/
|
|
118
|
+
unregisterPackage(packageName: string) {
|
|
119
|
+
// Direct lookup via secondary index ✅
|
|
120
|
+
const refs = this.packageIndex.get(packageName);
|
|
121
|
+
if (refs) {
|
|
122
|
+
// Delete each item referenced by this package
|
|
123
|
+
for (const ref of refs) {
|
|
124
|
+
if (this.items[ref.type]?.[ref.name]) {
|
|
125
|
+
delete this.items[ref.type][ref.name];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Remove package from index
|
|
129
|
+
this.packageIndex.delete(packageName);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|