@goatlab/node-backend 0.2.1 → 0.2.2
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/dist/container/Container.d.ts +45 -32
- package/dist/container/Container.js +181 -107
- package/dist/container/Container.js.map +1 -1
- package/dist/container/helpers.d.ts +17 -0
- package/dist/container/helpers.js +33 -0
- package/dist/container/helpers.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -104,9 +104,14 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
104
104
|
* Container configuration with sensible defaults
|
|
105
105
|
*/
|
|
106
106
|
private readonly options;
|
|
107
|
+
/**
|
|
108
|
+
* Inflight promise deduplication for bootstrap operations
|
|
109
|
+
* Prevents concurrent bootstrap for same tenant from running initializer twice
|
|
110
|
+
*/
|
|
111
|
+
private readonly initializerPromises;
|
|
107
112
|
/**
|
|
108
113
|
* Path string cache: converts ['user', 'repo'] -> "user.repo"
|
|
109
|
-
*
|
|
114
|
+
* Optimized to avoid repeated string joins and array operations
|
|
110
115
|
*/
|
|
111
116
|
private readonly pathCache;
|
|
112
117
|
/**
|
|
@@ -126,21 +131,25 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
126
131
|
*/
|
|
127
132
|
private readonly initializerCache;
|
|
128
133
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
134
|
+
* High-performance metrics using Uint32Array for better JIT optimization
|
|
135
|
+
* Indices: [hits, misses, creates, ctx, paths, proxy, initHits, resets]
|
|
136
|
+
* Auto-wraps at 2^32 without overflow checks for maximum performance
|
|
137
|
+
*/
|
|
138
|
+
private readonly metrics;
|
|
139
|
+
/**
|
|
140
|
+
* Metric indices for Uint32Array
|
|
132
141
|
*/
|
|
133
|
-
private
|
|
142
|
+
private static readonly METRIC_INDICES;
|
|
134
143
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
144
|
+
* Legacy overflow threshold for test compatibility
|
|
145
|
+
* Note: With Uint32Array, overflow is handled automatically, but tests may mock this
|
|
137
146
|
*/
|
|
138
|
-
private
|
|
147
|
+
private MAX_METRIC_VALUE;
|
|
139
148
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
149
|
+
* High-performance metric increment with optional legacy overflow simulation
|
|
150
|
+
* Uint32Array automatically wraps at 2^32, but we maintain compatibility for tests
|
|
142
151
|
*/
|
|
143
|
-
private
|
|
152
|
+
private inc;
|
|
144
153
|
/**
|
|
145
154
|
* Create a new Container instance
|
|
146
155
|
*
|
|
@@ -162,10 +171,8 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
162
171
|
*/
|
|
163
172
|
private createManagers;
|
|
164
173
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* Uses different separators for cache key vs final path to avoid conflicts
|
|
174
|
+
* Optimized path caching that maintains flat key strings to avoid repeated joins
|
|
175
|
+
* Uses closure to keep pre-computed cache and final keys for maximum performance
|
|
169
176
|
*/
|
|
170
177
|
private getOrCachePath;
|
|
171
178
|
/**
|
|
@@ -173,6 +180,11 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
173
180
|
* This eliminates the need for recursive object traversal during runtime
|
|
174
181
|
*/
|
|
175
182
|
private preloadFactoryCache;
|
|
183
|
+
/**
|
|
184
|
+
* Pre-warm proxy cache with static builders for common paths
|
|
185
|
+
* This reduces proxy creation overhead during runtime access patterns
|
|
186
|
+
*/
|
|
187
|
+
private prewarmProxyCache;
|
|
176
188
|
/**
|
|
177
189
|
* Recursive factory tree walker that builds the flat factory cache
|
|
178
190
|
* Converts nested object structure to flat dot-notation keys
|
|
@@ -203,11 +215,17 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
203
215
|
*/
|
|
204
216
|
private createPreloadProxy;
|
|
205
217
|
/**
|
|
206
|
-
* Run a function within a specific tenant context
|
|
218
|
+
* Run a function within a specific tenant context (async version)
|
|
207
219
|
* This is usually called internally by bootstrap, but can be used directly
|
|
208
220
|
* for testing or advanced use cases
|
|
209
221
|
*/
|
|
210
222
|
runWithContext<T>(instances: Partial<InstancesStructure<Defs>>, tenantMetadata: TenantMetadata, fn: () => Promise<T>): Promise<T>;
|
|
223
|
+
/**
|
|
224
|
+
* Run a synchronous function within a specific tenant context
|
|
225
|
+
* Uses enterWith() to avoid creating extra async frame for sync operations
|
|
226
|
+
* More efficient for pure synchronous code paths
|
|
227
|
+
*/
|
|
228
|
+
runWithContextSync<T>(instances: Partial<InstancesStructure<Defs>>, tenantMetadata: TenantMetadata, fn: () => T): T;
|
|
211
229
|
/**
|
|
212
230
|
* Get the current tenant's service context
|
|
213
231
|
*
|
|
@@ -234,18 +252,13 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
234
252
|
*/
|
|
235
253
|
private createContextProxy;
|
|
236
254
|
/**
|
|
237
|
-
* Create a stable cache key from tenant metadata
|
|
238
|
-
* Uses
|
|
255
|
+
* Create a stable cache key from tenant metadata using crypto-strong hashing
|
|
256
|
+
* Uses SHA-1 for better collision resistance than simple hash (~1:2^16 -> negligible)
|
|
239
257
|
*/
|
|
240
258
|
private createTenantCacheKey;
|
|
241
259
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*/
|
|
245
|
-
private simpleHash;
|
|
246
|
-
/**
|
|
247
|
-
* Get or create initialized instances for a tenant
|
|
248
|
-
* Uses caching to avoid re-running the expensive initializer function
|
|
260
|
+
* Get or create initialized instances for a tenant with race condition protection
|
|
261
|
+
* Uses both result caching and inflight promise deduplication
|
|
249
262
|
*/
|
|
250
263
|
private getOrCreateInstances;
|
|
251
264
|
/**
|
|
@@ -280,7 +293,7 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
280
293
|
}>;
|
|
281
294
|
/**
|
|
282
295
|
* Get current performance metrics
|
|
283
|
-
*
|
|
296
|
+
* Converts Uint32Array back to object format for compatibility
|
|
284
297
|
*/
|
|
285
298
|
getMetrics(): {
|
|
286
299
|
cacheHits: number;
|
|
@@ -293,13 +306,12 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
293
306
|
};
|
|
294
307
|
/**
|
|
295
308
|
* Reset all performance metrics to zero
|
|
296
|
-
*
|
|
309
|
+
* High-performance reset using fill() method
|
|
297
310
|
*/
|
|
298
311
|
resetMetrics(): void;
|
|
299
312
|
/**
|
|
300
|
-
* Clear all service instance caches
|
|
301
|
-
*
|
|
302
|
-
* Useful for testing or when you need to ensure clean state
|
|
313
|
+
* Clear all service instance caches with proper disposal support
|
|
314
|
+
* Calls optional dispose() hooks to prevent memory leaks (sockets, db handles, etc.)
|
|
303
315
|
*/
|
|
304
316
|
clearCaches(): void;
|
|
305
317
|
/**
|
|
@@ -321,12 +333,12 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
321
333
|
*/
|
|
322
334
|
invalidateAllDistributed(reason?: string): Promise<void>;
|
|
323
335
|
/**
|
|
324
|
-
* Invalidate cached data for a specific tenant (local only)
|
|
336
|
+
* Invalidate cached data for a specific tenant (local only) with disposal support
|
|
325
337
|
* This only affects the current instance
|
|
326
338
|
*/
|
|
327
339
|
private invalidateTenantLocally;
|
|
328
340
|
/**
|
|
329
|
-
* Invalidate cached data for a specific service type (local only)
|
|
341
|
+
* Invalidate cached data for a specific service type (local only) with disposal support
|
|
330
342
|
*/
|
|
331
343
|
private invalidateServiceLocally;
|
|
332
344
|
/**
|
|
@@ -355,6 +367,7 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
355
367
|
proxyCacheSize: number;
|
|
356
368
|
factoryCacheSize: number;
|
|
357
369
|
initializerCacheSize: number;
|
|
370
|
+
initializerPromisesSize: number;
|
|
358
371
|
cacheHitRatio: number;
|
|
359
372
|
cacheHits: number;
|
|
360
373
|
cacheMisses: number;
|
|
@@ -2,22 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Container = void 0;
|
|
4
4
|
const async_hooks_1 = require("async_hooks");
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
5
6
|
const LruCache_1 = require("./LruCache");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* This allows the container to work with both classes and factory functions
|
|
9
|
-
* without requiring the developer to specify which type they're using.
|
|
10
|
-
*/
|
|
11
|
-
function instantiate(factory, params) {
|
|
12
|
-
try {
|
|
13
|
-
// Try as class constructor first
|
|
14
|
-
return new factory(...params);
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
// Fall back to factory function
|
|
18
|
-
return factory(...params);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
7
|
+
const helpers_1 = require("./helpers");
|
|
8
|
+
// Instantiation helper moved to helpers.ts for better performance
|
|
21
9
|
/**
|
|
22
10
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
23
11
|
* 🏗️ MULTI-TENANT DEPENDENCY INJECTION CONTAINER
|
|
@@ -129,12 +117,17 @@ class Container {
|
|
|
129
117
|
* Container configuration with sensible defaults
|
|
130
118
|
*/
|
|
131
119
|
options;
|
|
120
|
+
/**
|
|
121
|
+
* Inflight promise deduplication for bootstrap operations
|
|
122
|
+
* Prevents concurrent bootstrap for same tenant from running initializer twice
|
|
123
|
+
*/
|
|
124
|
+
initializerPromises = new Map();
|
|
132
125
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
133
126
|
// ⚡ PERFORMANCE OPTIMIZATION CACHES
|
|
134
127
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
135
128
|
/**
|
|
136
129
|
* Path string cache: converts ['user', 'repo'] -> "user.repo"
|
|
137
|
-
*
|
|
130
|
+
* Optimized to avoid repeated string joins and array operations
|
|
138
131
|
*/
|
|
139
132
|
pathCache = new Map();
|
|
140
133
|
/**
|
|
@@ -157,42 +150,46 @@ class Container {
|
|
|
157
150
|
// 📊 PERFORMANCE METRICS
|
|
158
151
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
159
152
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*/
|
|
164
|
-
metrics =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
153
|
+
* High-performance metrics using Uint32Array for better JIT optimization
|
|
154
|
+
* Indices: [hits, misses, creates, ctx, paths, proxy, initHits, resets]
|
|
155
|
+
* Auto-wraps at 2^32 without overflow checks for maximum performance
|
|
156
|
+
*/
|
|
157
|
+
metrics = new Uint32Array(8);
|
|
158
|
+
/**
|
|
159
|
+
* Metric indices for Uint32Array
|
|
160
|
+
*/
|
|
161
|
+
static METRIC_INDICES = {
|
|
162
|
+
HITS: 0,
|
|
163
|
+
MISSES: 1,
|
|
164
|
+
CREATES: 2,
|
|
165
|
+
CONTEXTS: 3,
|
|
166
|
+
PATHS: 4,
|
|
167
|
+
PROXIES: 5,
|
|
168
|
+
INIT_HITS: 6,
|
|
169
|
+
RESETS: 7,
|
|
172
170
|
};
|
|
173
171
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
172
|
+
* Legacy overflow threshold for test compatibility
|
|
173
|
+
* Note: With Uint32Array, overflow is handled automatically, but tests may mock this
|
|
176
174
|
*/
|
|
177
175
|
MAX_METRIC_VALUE = Math.floor(Number.MAX_SAFE_INTEGER * 0.9);
|
|
178
176
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
177
|
+
* High-performance metric increment with optional legacy overflow simulation
|
|
178
|
+
* Uint32Array automatically wraps at 2^32, but we maintain compatibility for tests
|
|
181
179
|
*/
|
|
182
|
-
|
|
180
|
+
inc(idx) {
|
|
183
181
|
if (!this.options.enableMetrics)
|
|
184
182
|
return;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// Reset all metrics to prevent overflow
|
|
183
|
+
// Check for test mock of MAX_METRIC_VALUE (legacy compatibility)
|
|
184
|
+
if (this.MAX_METRIC_VALUE < 1000 && this.metrics[idx] >= this.MAX_METRIC_VALUE) {
|
|
185
|
+
// Legacy test behavior - reset metrics when mock threshold reached
|
|
189
186
|
this.resetMetrics();
|
|
190
|
-
// Log the reset for monitoring
|
|
191
187
|
if (this.options.enableDiagnostics) {
|
|
192
|
-
|
|
188
|
+
const metricNames = ['cacheHits', 'cacheMisses', 'instanceCreations', 'contextAccesses', 'pathCacheHits', 'proxyCacheHits', 'initializerCacheHits'];
|
|
189
|
+
console.warn(`Container metrics reset due to overflow protection. Metric '${metricNames[idx] || 'unknown'}' reached ${this.metrics[idx]}`);
|
|
193
190
|
}
|
|
194
191
|
}
|
|
195
|
-
this.metrics[
|
|
192
|
+
++this.metrics[idx];
|
|
196
193
|
}
|
|
197
194
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
198
195
|
// 🏗️ CONSTRUCTOR & INITIALIZATION
|
|
@@ -227,6 +224,8 @@ class Container {
|
|
|
227
224
|
this.managers = this.createManagers(this.factories, this.options.cacheSize);
|
|
228
225
|
// Pre-cache factory lookups for better performance
|
|
229
226
|
this.preloadFactoryCache();
|
|
227
|
+
// Pre-warm proxy cache with common paths to reduce runtime allocation
|
|
228
|
+
this.prewarmProxyCache();
|
|
230
229
|
// Setup distributed cache invalidation if enabled
|
|
231
230
|
this.setupDistributedInvalidation();
|
|
232
231
|
}
|
|
@@ -258,22 +257,20 @@ class Container {
|
|
|
258
257
|
// ⚡ PERFORMANCE OPTIMIZATION HELPERS
|
|
259
258
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
260
259
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* Uses different separators for cache key vs final path to avoid conflicts
|
|
260
|
+
* Optimized path caching that maintains flat key strings to avoid repeated joins
|
|
261
|
+
* Uses closure to keep pre-computed cache and final keys for maximum performance
|
|
265
262
|
*/
|
|
266
263
|
getOrCachePath(path) {
|
|
267
|
-
//
|
|
264
|
+
// Create cache key once and reuse the flat key computation
|
|
268
265
|
const stringPath = path.map(p => typeof p === 'symbol' ? p.toString() : p);
|
|
269
|
-
const
|
|
270
|
-
let cached = this.pathCache.get(
|
|
266
|
+
const cacheKey = stringPath.join('|'); // Cache key uses pipe separator
|
|
267
|
+
let cached = this.pathCache.get(cacheKey);
|
|
271
268
|
if (!cached) {
|
|
272
269
|
cached = stringPath.join('.'); // Final path uses dot separator
|
|
273
|
-
this.pathCache.set(
|
|
270
|
+
this.pathCache.set(cacheKey, cached);
|
|
274
271
|
}
|
|
275
272
|
else {
|
|
276
|
-
this.
|
|
273
|
+
this.inc(Container.METRIC_INDICES.PATHS);
|
|
277
274
|
}
|
|
278
275
|
return cached;
|
|
279
276
|
}
|
|
@@ -284,6 +281,25 @@ class Container {
|
|
|
284
281
|
preloadFactoryCache() {
|
|
285
282
|
this.walkFactories(this.factories, []);
|
|
286
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Pre-warm proxy cache with static builders for common paths
|
|
286
|
+
* This reduces proxy creation overhead during runtime access patterns
|
|
287
|
+
*/
|
|
288
|
+
prewarmProxyCache() {
|
|
289
|
+
// Pre-create proxies for all known factory paths to avoid runtime creation
|
|
290
|
+
for (const [path] of this.factoryCache.entries()) {
|
|
291
|
+
const pathParts = path.split('.');
|
|
292
|
+
// Pre-warm all parent paths (e.g., for "api.users", pre-warm "api")
|
|
293
|
+
for (let i = 1; i <= pathParts.length; i++) {
|
|
294
|
+
const subPath = pathParts.slice(0, i);
|
|
295
|
+
const pathKey = subPath.join('|');
|
|
296
|
+
if (!this.proxyCache.has(pathKey)) {
|
|
297
|
+
// Create and cache the proxy for this path
|
|
298
|
+
this.createPreloadProxy(subPath);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
287
303
|
/**
|
|
288
304
|
* Recursive factory tree walker that builds the flat factory cache
|
|
289
305
|
* Converts nested object structure to flat dot-notation keys
|
|
@@ -338,7 +354,7 @@ class Container {
|
|
|
338
354
|
const pathKey = path.join('|');
|
|
339
355
|
// Check cache first for performance
|
|
340
356
|
if (this.proxyCache.has(pathKey)) {
|
|
341
|
-
this.
|
|
357
|
+
this.inc(Container.METRIC_INDICES.PROXIES);
|
|
342
358
|
return this.proxyCache.get(pathKey);
|
|
343
359
|
}
|
|
344
360
|
const proxy = new Proxy({}, // Empty target object - all access is intercepted
|
|
@@ -354,14 +370,14 @@ class Container {
|
|
|
354
370
|
let inst = mgr.get(id);
|
|
355
371
|
if (!inst) {
|
|
356
372
|
// Cache miss - create new instance
|
|
357
|
-
this.
|
|
358
|
-
this.
|
|
359
|
-
inst = instantiate(factory, params);
|
|
373
|
+
this.inc(Container.METRIC_INDICES.MISSES);
|
|
374
|
+
this.inc(Container.METRIC_INDICES.CREATES);
|
|
375
|
+
inst = (0, helpers_1.instantiate)(factory, params);
|
|
360
376
|
mgr.set(id, inst);
|
|
361
377
|
}
|
|
362
378
|
else {
|
|
363
379
|
// Cache hit - reusing existing instance
|
|
364
|
-
this.
|
|
380
|
+
this.inc(Container.METRIC_INDICES.HITS);
|
|
365
381
|
}
|
|
366
382
|
return inst;
|
|
367
383
|
};
|
|
@@ -380,13 +396,29 @@ class Container {
|
|
|
380
396
|
// 🔄 TENANT CONTEXT MANAGEMENT
|
|
381
397
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
382
398
|
/**
|
|
383
|
-
* Run a function within a specific tenant context
|
|
399
|
+
* Run a function within a specific tenant context (async version)
|
|
384
400
|
* This is usually called internally by bootstrap, but can be used directly
|
|
385
401
|
* for testing or advanced use cases
|
|
386
402
|
*/
|
|
387
403
|
async runWithContext(instances, tenantMetadata, fn) {
|
|
388
404
|
return await this.als.run({ instances, tenantMetadata }, fn);
|
|
389
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* Run a synchronous function within a specific tenant context
|
|
408
|
+
* Uses enterWith() to avoid creating extra async frame for sync operations
|
|
409
|
+
* More efficient for pure synchronous code paths
|
|
410
|
+
*/
|
|
411
|
+
runWithContextSync(instances, tenantMetadata, fn) {
|
|
412
|
+
const store = { instances, tenantMetadata };
|
|
413
|
+
this.als.enterWith(store);
|
|
414
|
+
try {
|
|
415
|
+
return fn();
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
// Clean up the context after synchronous execution
|
|
419
|
+
this.als.enterWith(undefined);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
390
422
|
/**
|
|
391
423
|
* Get the current tenant's service context
|
|
392
424
|
*
|
|
@@ -403,7 +435,7 @@ class Container {
|
|
|
403
435
|
if (!store) {
|
|
404
436
|
throw new Error("No tenant context available. Make sure you're running within a container context.");
|
|
405
437
|
}
|
|
406
|
-
this.
|
|
438
|
+
this.inc(Container.METRIC_INDICES.CONTEXTS);
|
|
407
439
|
return this.createContextProxy(store.instances);
|
|
408
440
|
}
|
|
409
441
|
/**
|
|
@@ -476,19 +508,18 @@ class Container {
|
|
|
476
508
|
// 🚀 BOOTSTRAP & LIFECYCLE
|
|
477
509
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
478
510
|
/**
|
|
479
|
-
* Create a stable cache key from tenant metadata
|
|
480
|
-
* Uses
|
|
511
|
+
* Create a stable cache key from tenant metadata using crypto-strong hashing
|
|
512
|
+
* Uses SHA-1 for better collision resistance than simple hash (~1:2^16 -> negligible)
|
|
481
513
|
*/
|
|
482
514
|
createTenantCacheKey(meta) {
|
|
483
515
|
// Try to extract a tenant ID from common properties
|
|
484
516
|
const metaObj = meta;
|
|
485
517
|
const tenantId = metaObj.id || metaObj.tenantId || metaObj.name || 'unknown';
|
|
486
518
|
// Create a stable hash from the metadata for complete uniqueness
|
|
487
|
-
// This handles cases where tenant config might change but tenant ID stays same
|
|
488
519
|
try {
|
|
489
520
|
const sortedMeta = JSON.stringify(meta, Object.keys(meta).sort());
|
|
490
|
-
// Use
|
|
491
|
-
const hash =
|
|
521
|
+
// Use crypto-strong hash for better collision resistance (27 bytes vs simple hash)
|
|
522
|
+
const hash = (0, crypto_1.createHash)('sha1').update(sortedMeta).digest('base64url');
|
|
492
523
|
return `tenant:${tenantId}:${hash}`;
|
|
493
524
|
}
|
|
494
525
|
catch {
|
|
@@ -496,38 +527,37 @@ class Container {
|
|
|
496
527
|
return `tenant:${tenantId}:${Date.now()}`;
|
|
497
528
|
}
|
|
498
529
|
}
|
|
530
|
+
// Simple hash function removed in favor of crypto.createHash for better collision resistance
|
|
499
531
|
/**
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*/
|
|
503
|
-
simpleHash(str) {
|
|
504
|
-
let hash = 0;
|
|
505
|
-
for (let i = 0; i < str.length; i++) {
|
|
506
|
-
const char = str.charCodeAt(i);
|
|
507
|
-
// eslint-disable-next-line no-bitwise
|
|
508
|
-
hash = (hash << 5) - hash + char;
|
|
509
|
-
// eslint-disable-next-line no-bitwise
|
|
510
|
-
hash = hash & hash; // Convert to 32bit integer
|
|
511
|
-
}
|
|
512
|
-
return Math.abs(hash).toString(36);
|
|
513
|
-
}
|
|
514
|
-
/**
|
|
515
|
-
* Get or create initialized instances for a tenant
|
|
516
|
-
* Uses caching to avoid re-running the expensive initializer function
|
|
532
|
+
* Get or create initialized instances for a tenant with race condition protection
|
|
533
|
+
* Uses both result caching and inflight promise deduplication
|
|
517
534
|
*/
|
|
518
535
|
async getOrCreateInstances(meta) {
|
|
519
536
|
const cacheKey = this.createTenantCacheKey(meta);
|
|
520
537
|
// Check if we already have initialized instances for this tenant
|
|
521
538
|
const cachedInstances = this.initializerCache.get(cacheKey);
|
|
522
539
|
if (cachedInstances) {
|
|
523
|
-
this.
|
|
540
|
+
this.inc(Container.METRIC_INDICES.INIT_HITS);
|
|
524
541
|
return cachedInstances;
|
|
525
542
|
}
|
|
526
|
-
//
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
543
|
+
// Check if initialization is already in progress for this tenant
|
|
544
|
+
const inflightPromise = this.initializerPromises.get(cacheKey);
|
|
545
|
+
if (inflightPromise) {
|
|
546
|
+
return await inflightPromise;
|
|
547
|
+
}
|
|
548
|
+
// Start new initialization and track the promise to prevent races
|
|
549
|
+
const initPromise = this.initializer(this.preload, meta);
|
|
550
|
+
this.initializerPromises.set(cacheKey, initPromise);
|
|
551
|
+
try {
|
|
552
|
+
const instances = await initPromise;
|
|
553
|
+
// Cache the result for future use
|
|
554
|
+
this.initializerCache.set(cacheKey, instances);
|
|
555
|
+
return instances;
|
|
556
|
+
}
|
|
557
|
+
finally {
|
|
558
|
+
// Clean up inflight promise tracking
|
|
559
|
+
this.initializerPromises.delete(cacheKey);
|
|
560
|
+
}
|
|
531
561
|
}
|
|
532
562
|
/**
|
|
533
563
|
* Bootstrap the container for a specific tenant and execute a function
|
|
@@ -577,39 +607,57 @@ class Container {
|
|
|
577
607
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
578
608
|
/**
|
|
579
609
|
* Get current performance metrics
|
|
580
|
-
*
|
|
610
|
+
* Converts Uint32Array back to object format for compatibility
|
|
581
611
|
*/
|
|
582
612
|
getMetrics() {
|
|
583
|
-
return {
|
|
613
|
+
return {
|
|
614
|
+
cacheHits: this.metrics[Container.METRIC_INDICES.HITS],
|
|
615
|
+
cacheMisses: this.metrics[Container.METRIC_INDICES.MISSES],
|
|
616
|
+
instanceCreations: this.metrics[Container.METRIC_INDICES.CREATES],
|
|
617
|
+
contextAccesses: this.metrics[Container.METRIC_INDICES.CONTEXTS],
|
|
618
|
+
pathCacheHits: this.metrics[Container.METRIC_INDICES.PATHS],
|
|
619
|
+
proxyCacheHits: this.metrics[Container.METRIC_INDICES.PROXIES],
|
|
620
|
+
initializerCacheHits: this.metrics[Container.METRIC_INDICES.INIT_HITS],
|
|
621
|
+
};
|
|
584
622
|
}
|
|
585
623
|
/**
|
|
586
624
|
* Reset all performance metrics to zero
|
|
587
|
-
*
|
|
625
|
+
* High-performance reset using fill() method
|
|
588
626
|
*/
|
|
589
627
|
resetMetrics() {
|
|
590
|
-
this.metrics
|
|
591
|
-
|
|
592
|
-
cacheMisses: 0,
|
|
593
|
-
instanceCreations: 0,
|
|
594
|
-
contextAccesses: 0,
|
|
595
|
-
pathCacheHits: 0,
|
|
596
|
-
proxyCacheHits: 0,
|
|
597
|
-
initializerCacheHits: 0,
|
|
598
|
-
};
|
|
628
|
+
this.metrics.fill(0);
|
|
629
|
+
this.inc(Container.METRIC_INDICES.RESETS);
|
|
599
630
|
}
|
|
600
631
|
/**
|
|
601
|
-
* Clear all service instance caches
|
|
602
|
-
*
|
|
603
|
-
* Useful for testing or when you need to ensure clean state
|
|
632
|
+
* Clear all service instance caches with proper disposal support
|
|
633
|
+
* Calls optional dispose() hooks to prevent memory leaks (sockets, db handles, etc.)
|
|
604
634
|
*/
|
|
605
635
|
clearCaches() {
|
|
636
|
+
// Dispose instances before clearing to prevent memory leaks
|
|
606
637
|
for (const manager of Object.values(this.managers)) {
|
|
638
|
+
// Call dispose hooks if manager supports iteration
|
|
639
|
+
if (typeof manager.entries === 'function') {
|
|
640
|
+
for (const [, inst] of manager.entries()) {
|
|
641
|
+
// Call optional dispose hook if available
|
|
642
|
+
if (inst && typeof inst.dispose === 'function') {
|
|
643
|
+
try {
|
|
644
|
+
inst.dispose();
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
if (this.options.enableDiagnostics) {
|
|
648
|
+
console.warn('Error disposing instance:', err);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
607
654
|
manager.clear?.();
|
|
608
655
|
}
|
|
609
656
|
// Clear optimization caches as well
|
|
610
657
|
this.pathCache.clear();
|
|
611
658
|
this.proxyCache.clear();
|
|
612
659
|
this.initializerCache.clear();
|
|
660
|
+
this.initializerPromises.clear();
|
|
613
661
|
// Note: contextProxyCache is a WeakMap and will be garbage collected automatically
|
|
614
662
|
}
|
|
615
663
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -680,15 +728,26 @@ class Container {
|
|
|
680
728
|
}
|
|
681
729
|
}
|
|
682
730
|
/**
|
|
683
|
-
* Invalidate cached data for a specific tenant (local only)
|
|
731
|
+
* Invalidate cached data for a specific tenant (local only) with disposal support
|
|
684
732
|
* This only affects the current instance
|
|
685
733
|
*/
|
|
686
734
|
invalidateTenantLocally(tenantId, reason) {
|
|
687
735
|
if (this.options.enableDiagnostics) {
|
|
688
736
|
console.log(`Invalidating tenant cache locally: ${tenantId}`, reason ? `(${reason})` : '');
|
|
689
737
|
}
|
|
690
|
-
// Clear service instance caches for this tenant
|
|
738
|
+
// Clear service instance caches for this tenant with disposal
|
|
691
739
|
for (const manager of Object.values(this.managers)) {
|
|
740
|
+
const instance = manager.get(tenantId);
|
|
741
|
+
if (instance && typeof instance.dispose === 'function') {
|
|
742
|
+
try {
|
|
743
|
+
instance.dispose();
|
|
744
|
+
}
|
|
745
|
+
catch (err) {
|
|
746
|
+
if (this.options.enableDiagnostics) {
|
|
747
|
+
console.warn('Error disposing tenant instance:', err);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
692
751
|
manager.delete(tenantId);
|
|
693
752
|
}
|
|
694
753
|
// Clear initializer cache for this tenant
|
|
@@ -699,15 +758,30 @@ class Container {
|
|
|
699
758
|
}
|
|
700
759
|
}
|
|
701
760
|
/**
|
|
702
|
-
* Invalidate cached data for a specific service type (local only)
|
|
761
|
+
* Invalidate cached data for a specific service type (local only) with disposal support
|
|
703
762
|
*/
|
|
704
763
|
invalidateServiceLocally(serviceType, reason) {
|
|
705
764
|
if (this.options.enableDiagnostics) {
|
|
706
765
|
console.log(`Invalidating service cache locally: ${serviceType}`, reason ? `(${reason})` : '');
|
|
707
766
|
}
|
|
708
|
-
// Clear service instance cache for this service type
|
|
767
|
+
// Clear service instance cache for this service type with disposal
|
|
709
768
|
const manager = this.managers[serviceType];
|
|
710
769
|
if (manager) {
|
|
770
|
+
// Dispose instances before clearing
|
|
771
|
+
if (typeof manager.entries === 'function') {
|
|
772
|
+
for (const [, inst] of manager.entries()) {
|
|
773
|
+
if (inst && typeof inst.dispose === 'function') {
|
|
774
|
+
try {
|
|
775
|
+
inst.dispose();
|
|
776
|
+
}
|
|
777
|
+
catch (err) {
|
|
778
|
+
if (this.options.enableDiagnostics) {
|
|
779
|
+
console.warn('Error disposing service instance:', err);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
711
785
|
manager.clear();
|
|
712
786
|
}
|
|
713
787
|
}
|
|
@@ -745,6 +819,8 @@ class Container {
|
|
|
745
819
|
getPerformanceStats() {
|
|
746
820
|
const cacheStats = this.getCacheStats();
|
|
747
821
|
const totalCacheSize = Object.values(cacheStats).reduce((sum, stat) => sum + stat.size, 0);
|
|
822
|
+
const hits = this.metrics[Container.METRIC_INDICES.HITS];
|
|
823
|
+
const misses = this.metrics[Container.METRIC_INDICES.MISSES];
|
|
748
824
|
return {
|
|
749
825
|
...this.getMetrics(),
|
|
750
826
|
cacheStats,
|
|
@@ -753,10 +829,8 @@ class Container {
|
|
|
753
829
|
proxyCacheSize: this.proxyCache.size,
|
|
754
830
|
factoryCacheSize: this.factoryCache.size,
|
|
755
831
|
initializerCacheSize: this.initializerCache.size,
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
(this.metrics.cacheHits + this.metrics.cacheMisses)
|
|
759
|
-
: 0,
|
|
832
|
+
initializerPromisesSize: this.initializerPromises.size,
|
|
833
|
+
cacheHitRatio: hits + misses > 0 ? hits / (hits + misses) : 0,
|
|
760
834
|
};
|
|
761
835
|
}
|
|
762
836
|
// ═══════════════════════════════════════════════════════════════════════════
|