@goatlab/node-backend 1.2.0 → 1.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/dist/cache/LazyRedisStore.d.ts +0 -5
- package/dist/cache/LazyRedisStore.js +0 -7
- package/dist/cache/LazyRedisStore.js.map +1 -1
- package/dist/container/Container.d.ts +45 -13
- package/dist/container/Container.js +260 -140
- package/dist/container/Container.js.map +1 -1
- package/dist/container/types.d.ts +70 -0
- package/dist/container/types.js +20 -0
- package/dist/container/types.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/server/bootstraps/ExpressTrpcAppConfig.d.ts +12 -0
- package/dist/server/bootstraps/ExpressTrpcAppConfig.js +1 -0
- package/dist/server/bootstraps/ExpressTrpcAppConfig.js.map +1 -1
- package/dist/server/bootstraps/getExpressTrpcApp.js +2 -2
- package/dist/server/bootstraps/getExpressTrpcApp.js.map +1 -1
- package/dist/server/middleware/logs.middleware.d.ts +6 -1
- package/dist/server/middleware/logs.middleware.js +10 -4
- package/dist/server/middleware/logs.middleware.js.map +1 -1
- package/dist/server/trpc.d.ts +49 -8
- package/dist/server/trpc.js +1 -0
- package/dist/server/trpc.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ exports.Container = void 0;
|
|
|
4
4
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
5
5
|
const helpers_1 = require("./helpers");
|
|
6
6
|
const LruCache_1 = require("./LruCache");
|
|
7
|
+
const types_1 = require("./types");
|
|
7
8
|
// Instantiation helper moved to helpers.ts for better performance
|
|
8
9
|
/**
|
|
9
10
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -98,6 +99,22 @@ class Container {
|
|
|
98
99
|
* Lazy-allocated to save memory for unused services
|
|
99
100
|
*/
|
|
100
101
|
managers = {};
|
|
102
|
+
/**
|
|
103
|
+
* Kill switch: Set of blocked tenant IDs
|
|
104
|
+
* Blocked tenants are rejected immediately at bootstrap without initialization
|
|
105
|
+
*/
|
|
106
|
+
blockedTenants = new Set();
|
|
107
|
+
/**
|
|
108
|
+
* Cooldown tracker: tenants whose initializer recently failed
|
|
109
|
+
* Maps tenant cache key -> expiry timestamp (Date.now() + cooldown)
|
|
110
|
+
* Prevents retry storms when a tenant's initializer is broken
|
|
111
|
+
*/
|
|
112
|
+
initializerCooldowns = new Map();
|
|
113
|
+
/**
|
|
114
|
+
* Bootstrap call counter for sampled heap checks
|
|
115
|
+
* Only check memory every N calls to minimize overhead
|
|
116
|
+
*/
|
|
117
|
+
bootstrapCounter = 0;
|
|
101
118
|
/**
|
|
102
119
|
* AsyncLocalStorage provides automatic tenant context isolation
|
|
103
120
|
* Each async call tree gets its own isolated service instances
|
|
@@ -122,6 +139,12 @@ class Container {
|
|
|
122
139
|
* Prevents concurrent bootstrap for same tenant from running initializer twice
|
|
123
140
|
*/
|
|
124
141
|
initializerPromises = new Map();
|
|
142
|
+
/**
|
|
143
|
+
* Tracks in-flight disposal operations per tenant
|
|
144
|
+
* Bootstrap waits for pending disposal to complete before re-initializing
|
|
145
|
+
* Prevents duplicate live instances when invalidation overlaps with re-bootstrap
|
|
146
|
+
*/
|
|
147
|
+
disposalPromises = new Map();
|
|
125
148
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
126
149
|
// ⚡ PERFORMANCE OPTIMIZATION CACHES
|
|
127
150
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -139,16 +162,9 @@ class Container {
|
|
|
139
162
|
/**
|
|
140
163
|
* Initializer cache: stores initialized instances per tenant with LRU eviction
|
|
141
164
|
* Avoids re-running the expensive initializer function for the same tenant
|
|
142
|
-
*
|
|
143
|
-
* Uses accessOrder to track LRU for eviction when cache exceeds maxInitializerCacheSize
|
|
144
|
-
*/
|
|
145
|
-
initializerCache = new Map();
|
|
146
|
-
/**
|
|
147
|
-
* Tracks access order for LRU eviction of initializerCache entries
|
|
148
|
-
* Key is cache key, value is access timestamp (incrementing counter)
|
|
165
|
+
* Uses tiny-lru for O(1) eviction instead of hand-rolled linear scan
|
|
149
166
|
*/
|
|
150
|
-
|
|
151
|
-
initializerAccessCounter = 0;
|
|
167
|
+
initializerCache;
|
|
152
168
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
153
169
|
// 📊 PERFORMANCE METRICS
|
|
154
170
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -205,6 +221,13 @@ class Container {
|
|
|
205
221
|
}
|
|
206
222
|
++this.metrics[idx];
|
|
207
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Emit a structured event if an event handler is configured
|
|
226
|
+
* No-op if onEvent is not set, keeping zero overhead when unused
|
|
227
|
+
*/
|
|
228
|
+
emit(event) {
|
|
229
|
+
this.options.onEvent?.(event);
|
|
230
|
+
}
|
|
208
231
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
209
232
|
// 🏗️ CONSTRUCTOR & INITIALIZATION
|
|
210
233
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -234,8 +257,14 @@ class Container {
|
|
|
234
257
|
enableDiagnostics: false,
|
|
235
258
|
enableDistributedInvalidation: false,
|
|
236
259
|
distributedInvalidator: undefined,
|
|
260
|
+
initializerCooldownMs: 0,
|
|
261
|
+
onEvent: undefined,
|
|
262
|
+
maxHeapUsageRatio: 0,
|
|
263
|
+
heapCheckInterval: 10,
|
|
237
264
|
...options,
|
|
238
265
|
};
|
|
266
|
+
// Initialize LRU cache for initializer results (O(1) eviction)
|
|
267
|
+
this.initializerCache = (0, LruCache_1.createServiceCache)(this.options.maxInitializerCacheSize);
|
|
239
268
|
// Pre-cache factory lookups for better performance
|
|
240
269
|
this.preloadFactoryCache();
|
|
241
270
|
// Setup distributed cache invalidation if enabled
|
|
@@ -449,21 +478,24 @@ class Container {
|
|
|
449
478
|
`Available services: ${available}`);
|
|
450
479
|
}
|
|
451
480
|
// Only wrap objects that are safe to wrap
|
|
452
|
-
// Avoid wrapping Promises, arrays, thenable objects,
|
|
481
|
+
// Avoid wrapping Promises, arrays, thenable objects, or opted-out services
|
|
453
482
|
if (typeof value === 'object' &&
|
|
454
483
|
value !== null &&
|
|
455
484
|
!Array.isArray(value) &&
|
|
456
485
|
!(value instanceof Promise) &&
|
|
457
486
|
typeof value.then !== 'function' &&
|
|
458
|
-
//
|
|
487
|
+
// Symbol-based opt-out: preferred mechanism for any service
|
|
488
|
+
!(types_1.NO_CONTAINER_PROXY in value && value[types_1.NO_CONTAINER_PROXY]) &&
|
|
489
|
+
// Legacy duck-typing checks (kept for backwards compatibility)
|
|
490
|
+
// Prisma clients
|
|
459
491
|
!('_engine' in value &&
|
|
460
492
|
'_extensions' in value &&
|
|
461
493
|
'$connect' in value) &&
|
|
462
|
-
//
|
|
494
|
+
// Redis/ioredis clients
|
|
463
495
|
!('options' in value && 'status' in value && 'connector' in value) &&
|
|
464
|
-
//
|
|
496
|
+
// Keyv instances
|
|
465
497
|
!('opts' in value) &&
|
|
466
|
-
//
|
|
498
|
+
// Cache store objects
|
|
467
499
|
!('store' in value && 'namespace' in value)) {
|
|
468
500
|
// Safe to wrap - create nested proxy
|
|
469
501
|
return this.createContextProxy(value, newPath);
|
|
@@ -507,30 +539,14 @@ class Container {
|
|
|
507
539
|
return `tenant:hash:${this.simpleHash(json)}`;
|
|
508
540
|
}
|
|
509
541
|
catch {
|
|
510
|
-
//
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
evictLruFromInitializerCache() {
|
|
519
|
-
if (this.initializerCache.size < this.options.maxInitializerCacheSize) {
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
// Find the LRU entry (lowest access counter)
|
|
523
|
-
let lruKey = null;
|
|
524
|
-
let lruTime = Number.POSITIVE_INFINITY;
|
|
525
|
-
for (const [key, time] of this.initializerAccessOrder) {
|
|
526
|
-
if (time < lruTime) {
|
|
527
|
-
lruTime = time;
|
|
528
|
-
lruKey = key;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
if (lruKey) {
|
|
532
|
-
this.initializerCache.delete(lruKey);
|
|
533
|
-
this.initializerAccessOrder.delete(lruKey);
|
|
542
|
+
// For circular refs, build a stable key from sorted own-property keys + values
|
|
543
|
+
// This is deterministic unlike Date.now() which guarantees cache misses
|
|
544
|
+
const keys = Object.keys(m).sort();
|
|
545
|
+
const parts = keys.map(k => {
|
|
546
|
+
const v = m[k];
|
|
547
|
+
return `${k}=${typeof v === 'object' ? typeof v : v}`;
|
|
548
|
+
});
|
|
549
|
+
return `tenant:keys:${this.simpleHash(parts.join('|'))}`;
|
|
534
550
|
}
|
|
535
551
|
}
|
|
536
552
|
/**
|
|
@@ -540,12 +556,27 @@ class Container {
|
|
|
540
556
|
*/
|
|
541
557
|
async getOrCreateInstances(meta) {
|
|
542
558
|
const cacheKey = this.createTenantCacheKey(meta);
|
|
559
|
+
// Wait for any pending disposal before re-initializing this tenant
|
|
560
|
+
const m = meta;
|
|
561
|
+
const tenantId = m.id ?? m.tenantId ?? m.name;
|
|
562
|
+
if (tenantId) {
|
|
563
|
+
const pendingDisposal = this.disposalPromises.get(String(tenantId));
|
|
564
|
+
if (pendingDisposal) {
|
|
565
|
+
await pendingDisposal;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Check cooldown: reject if initializer recently failed for this tenant
|
|
569
|
+
if (this.options.initializerCooldownMs > 0) {
|
|
570
|
+
const cooldownExpiry = this.initializerCooldowns.get(cacheKey);
|
|
571
|
+
if (cooldownExpiry && Date.now() < cooldownExpiry) {
|
|
572
|
+
const remainingMs = cooldownExpiry - Date.now();
|
|
573
|
+
throw new Error(`Tenant initializer is in cooldown (${remainingMs}ms remaining). Previous initialization failed.`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
543
576
|
// Check if we already have initialized instances for this tenant
|
|
544
577
|
const cachedInstances = this.initializerCache.get(cacheKey);
|
|
545
578
|
if (cachedInstances) {
|
|
546
579
|
this.inc(Container.METRIC.INIT_HITS);
|
|
547
|
-
// Update access order for LRU tracking
|
|
548
|
-
this.initializerAccessOrder.set(cacheKey, ++this.initializerAccessCounter);
|
|
549
580
|
return cachedInstances;
|
|
550
581
|
}
|
|
551
582
|
// Check if initialization is already in progress for this tenant
|
|
@@ -558,14 +589,19 @@ class Container {
|
|
|
558
589
|
this.initializerPromises.set(cacheKey, initPromise);
|
|
559
590
|
try {
|
|
560
591
|
const instances = await initPromise;
|
|
561
|
-
//
|
|
562
|
-
this.evictLruFromInitializerCache();
|
|
563
|
-
// Cache the result for future use
|
|
592
|
+
// Cache the result for future use (LRU eviction handled by tiny-lru)
|
|
564
593
|
this.initializerCache.set(cacheKey, instances);
|
|
565
|
-
//
|
|
566
|
-
this.
|
|
594
|
+
// Clear any previous cooldown on success
|
|
595
|
+
this.initializerCooldowns.delete(cacheKey);
|
|
567
596
|
return instances;
|
|
568
597
|
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
// Set cooldown on failure to prevent retry storms
|
|
600
|
+
if (this.options.initializerCooldownMs > 0) {
|
|
601
|
+
this.initializerCooldowns.set(cacheKey, Date.now() + this.options.initializerCooldownMs);
|
|
602
|
+
}
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
569
605
|
finally {
|
|
570
606
|
// Clean up inflight promise tracking
|
|
571
607
|
this.initializerPromises.delete(cacheKey);
|
|
@@ -598,16 +634,60 @@ class Container {
|
|
|
598
634
|
* 4. Returns both the instances and your function's result
|
|
599
635
|
*/
|
|
600
636
|
async bootstrap(meta, fn) {
|
|
637
|
+
// Kill switch: reject blocked tenants immediately
|
|
638
|
+
const m = meta;
|
|
639
|
+
const tenantId = m.id ?? m.tenantId ?? m.name;
|
|
640
|
+
const tenantIdStr = tenantId ? String(tenantId) : undefined;
|
|
641
|
+
if (tenantIdStr && this.blockedTenants.has(tenantIdStr)) {
|
|
642
|
+
this.emit({
|
|
643
|
+
type: 'tenant:blocked',
|
|
644
|
+
tenantId: tenantIdStr,
|
|
645
|
+
timestamp: Date.now(),
|
|
646
|
+
});
|
|
647
|
+
throw new Error(`Tenant '${tenantIdStr}' is blocked. Use container.unblockTenant() to restore access.`);
|
|
648
|
+
}
|
|
649
|
+
// Memory pressure check (sampled to avoid overhead)
|
|
650
|
+
if (this.options.maxHeapUsageRatio > 0) {
|
|
651
|
+
this.bootstrapCounter++;
|
|
652
|
+
if (this.bootstrapCounter % this.options.heapCheckInterval === 0) {
|
|
653
|
+
const { heapUsed, heapTotal } = process.memoryUsage();
|
|
654
|
+
const ratio = heapUsed / heapTotal;
|
|
655
|
+
if (ratio > this.options.maxHeapUsageRatio) {
|
|
656
|
+
throw new Error(`Memory pressure: heap usage ${(ratio * 100).toFixed(1)}% exceeds limit ${(this.options.maxHeapUsageRatio * 100).toFixed(1)}%. Rejecting new bootstrap.`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const startTime = Date.now();
|
|
661
|
+
this.emit({
|
|
662
|
+
type: 'bootstrap:start',
|
|
663
|
+
tenantId: tenantIdStr,
|
|
664
|
+
timestamp: startTime,
|
|
665
|
+
});
|
|
601
666
|
try {
|
|
602
667
|
// Phase 1: Get or create services for this tenant (with caching)
|
|
603
668
|
const instances = await this.getOrCreateInstances(meta);
|
|
669
|
+
const cached = this.initializerCache.get(this.createTenantCacheKey(meta)) !== undefined;
|
|
604
670
|
// Phase 2: Run user function within tenant context
|
|
605
671
|
const result = await this.runWithContext(instances, meta, fn || (async () => undefined));
|
|
672
|
+
this.emit({
|
|
673
|
+
type: 'bootstrap:complete',
|
|
674
|
+
tenantId: tenantIdStr,
|
|
675
|
+
durationMs: Date.now() - startTime,
|
|
676
|
+
cached,
|
|
677
|
+
timestamp: Date.now(),
|
|
678
|
+
});
|
|
606
679
|
// Type assertion: we trust that initializer provides all required services
|
|
607
680
|
// In practice, this is validated at runtime by the context proxy
|
|
608
681
|
return { instances: instances, result };
|
|
609
682
|
}
|
|
610
683
|
catch (err) {
|
|
684
|
+
this.emit({
|
|
685
|
+
type: 'bootstrap:error',
|
|
686
|
+
tenantId: tenantIdStr,
|
|
687
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
688
|
+
durationMs: Date.now() - startTime,
|
|
689
|
+
timestamp: Date.now(),
|
|
690
|
+
});
|
|
611
691
|
if (this.options.enableDiagnostics) {
|
|
612
692
|
console.error('Container bootstrap failed:', err);
|
|
613
693
|
}
|
|
@@ -653,98 +733,86 @@ class Container {
|
|
|
653
733
|
*/
|
|
654
734
|
async bootstrapBatch(tenantBatch, options = {}) {
|
|
655
735
|
const { concurrency = 10, continueOnError = true, timeout, onProgress, } = options;
|
|
656
|
-
const results = [];
|
|
657
736
|
const total = tenantBatch.length;
|
|
737
|
+
const results = new Array(total);
|
|
658
738
|
let completed = 0;
|
|
739
|
+
let nextIndex = 0;
|
|
659
740
|
let shouldAbort = false;
|
|
660
|
-
// Process
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
let bootstrapPromise = this.bootstrap(metadata, fn);
|
|
672
|
-
if (timeout) {
|
|
673
|
-
bootstrapPromise = Promise.race([
|
|
674
|
-
bootstrapPromise,
|
|
675
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(`Bootstrap timeout after ${timeout}ms`)), timeout)),
|
|
676
|
-
]);
|
|
677
|
-
}
|
|
678
|
-
const { instances, result } = await bootstrapPromise;
|
|
679
|
-
const endTime = Date.now();
|
|
680
|
-
this.inc(Container.METRIC.BATCH_OPS);
|
|
681
|
-
return {
|
|
682
|
-
metadata,
|
|
683
|
-
status: 'success',
|
|
684
|
-
instances,
|
|
685
|
-
result,
|
|
686
|
-
metrics: {
|
|
687
|
-
startTime,
|
|
688
|
-
endTime,
|
|
689
|
-
duration: endTime - startTime,
|
|
690
|
-
},
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
catch (error) {
|
|
694
|
-
const endTime = Date.now();
|
|
695
|
-
this.inc(Container.METRIC.BATCH_ERRORS);
|
|
696
|
-
if (this.options.enableDiagnostics) {
|
|
697
|
-
console.error(`Batch bootstrap failed for tenant:`, metadata, error);
|
|
698
|
-
}
|
|
699
|
-
const result = {
|
|
700
|
-
metadata,
|
|
701
|
-
status: 'error',
|
|
702
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
703
|
-
metrics: {
|
|
704
|
-
startTime,
|
|
705
|
-
endTime,
|
|
706
|
-
duration: endTime - startTime,
|
|
707
|
-
},
|
|
708
|
-
};
|
|
709
|
-
if (!continueOnError) {
|
|
710
|
-
// Mark that we should abort processing
|
|
711
|
-
shouldAbort = true;
|
|
712
|
-
}
|
|
713
|
-
return result;
|
|
714
|
-
}
|
|
715
|
-
finally {
|
|
716
|
-
completed++;
|
|
717
|
-
onProgress?.(completed, total, metadata);
|
|
741
|
+
// Process a single tenant and return its result at the correct index
|
|
742
|
+
const processTenant = async (index) => {
|
|
743
|
+
const { metadata, fn } = tenantBatch[index];
|
|
744
|
+
const startTime = Date.now();
|
|
745
|
+
try {
|
|
746
|
+
let bootstrapPromise = this.bootstrap(metadata, fn);
|
|
747
|
+
if (timeout) {
|
|
748
|
+
bootstrapPromise = Promise.race([
|
|
749
|
+
bootstrapPromise,
|
|
750
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Bootstrap timeout after ${timeout}ms`)), timeout)),
|
|
751
|
+
]);
|
|
718
752
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
753
|
+
const { instances, result } = await bootstrapPromise;
|
|
754
|
+
const endTime = Date.now();
|
|
755
|
+
this.inc(Container.METRIC.BATCH_OPS);
|
|
756
|
+
results[index] = {
|
|
757
|
+
metadata,
|
|
758
|
+
status: 'success',
|
|
759
|
+
instances,
|
|
760
|
+
result,
|
|
761
|
+
metrics: {
|
|
762
|
+
startTime,
|
|
763
|
+
endTime,
|
|
764
|
+
duration: endTime - startTime,
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
const endTime = Date.now();
|
|
770
|
+
this.inc(Container.METRIC.BATCH_ERRORS);
|
|
771
|
+
if (this.options.enableDiagnostics) {
|
|
772
|
+
console.error(`Batch bootstrap failed for tenant:`, metadata, error);
|
|
726
773
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
774
|
+
results[index] = {
|
|
775
|
+
metadata,
|
|
776
|
+
status: 'error',
|
|
777
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
778
|
+
metrics: {
|
|
779
|
+
startTime,
|
|
780
|
+
endTime,
|
|
781
|
+
duration: endTime - startTime,
|
|
782
|
+
},
|
|
783
|
+
};
|
|
784
|
+
if (!continueOnError) {
|
|
785
|
+
shouldAbort = true;
|
|
739
786
|
}
|
|
740
787
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
788
|
+
finally {
|
|
789
|
+
completed++;
|
|
790
|
+
onProgress?.(completed, total, metadata);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
// Pool-based concurrency: start new work as soon as any slot frees up
|
|
794
|
+
const workers = [];
|
|
795
|
+
const runWorker = async () => {
|
|
796
|
+
while (!shouldAbort && nextIndex < total) {
|
|
797
|
+
const index = nextIndex++;
|
|
798
|
+
await processTenant(index);
|
|
745
799
|
}
|
|
800
|
+
};
|
|
801
|
+
// Launch workers up to the concurrency limit
|
|
802
|
+
const workerCount = Math.min(concurrency, total);
|
|
803
|
+
for (let i = 0; i < workerCount; i++) {
|
|
804
|
+
workers.push(runWorker());
|
|
746
805
|
}
|
|
747
|
-
|
|
806
|
+
await Promise.allSettled(workers);
|
|
807
|
+
// In fail-fast mode, throw the first error found
|
|
808
|
+
if (!continueOnError) {
|
|
809
|
+
const errorResult = results.find(r => r?.status === 'error');
|
|
810
|
+
if (errorResult?.error) {
|
|
811
|
+
throw errorResult.error;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// Filter out any undefined slots (from aborted processing)
|
|
815
|
+
return results.filter(Boolean);
|
|
748
816
|
}
|
|
749
817
|
/**
|
|
750
818
|
* Invalidate multiple tenant caches in batch
|
|
@@ -898,8 +966,9 @@ class Container {
|
|
|
898
966
|
// Clear optimization caches as well
|
|
899
967
|
this.proxyCache.clear();
|
|
900
968
|
this.initializerCache.clear();
|
|
901
|
-
this.initializerAccessOrder.clear();
|
|
902
969
|
this.initializerPromises.clear();
|
|
970
|
+
this.disposalPromises.clear();
|
|
971
|
+
this.initializerCooldowns.clear();
|
|
903
972
|
// Note: contextProxyCache is a WeakMap and will be garbage collected automatically
|
|
904
973
|
}
|
|
905
974
|
/**
|
|
@@ -945,8 +1014,9 @@ class Container {
|
|
|
945
1014
|
// Clear optimization caches as well
|
|
946
1015
|
this.proxyCache.clear();
|
|
947
1016
|
this.initializerCache.clear();
|
|
948
|
-
this.initializerAccessOrder.clear();
|
|
949
1017
|
this.initializerPromises.clear();
|
|
1018
|
+
this.disposalPromises.clear();
|
|
1019
|
+
this.initializerCooldowns.clear();
|
|
950
1020
|
// Note: contextProxyCache is a WeakMap and will be garbage collected automatically
|
|
951
1021
|
return result;
|
|
952
1022
|
}
|
|
@@ -1022,32 +1092,51 @@ class Container {
|
|
|
1022
1092
|
* This only affects the current instance
|
|
1023
1093
|
*/
|
|
1024
1094
|
invalidateTenantLocally(tenantId, reason) {
|
|
1095
|
+
this.emit({
|
|
1096
|
+
type: 'tenant:invalidated',
|
|
1097
|
+
tenantId,
|
|
1098
|
+
reason,
|
|
1099
|
+
timestamp: Date.now(),
|
|
1100
|
+
});
|
|
1025
1101
|
if (this.options.enableDiagnostics) {
|
|
1026
1102
|
console.log(`Invalidating tenant cache locally: ${tenantId}`, reason ? `(${reason})` : '');
|
|
1027
1103
|
}
|
|
1028
|
-
// Clear service instance caches for this tenant with disposal
|
|
1104
|
+
// Clear service instance caches for this tenant with tracked disposal
|
|
1105
|
+
const disposalTasks = [];
|
|
1029
1106
|
for (const manager of Object.values(this.managers)) {
|
|
1030
1107
|
const instance = manager.get(tenantId);
|
|
1031
1108
|
if (instance) {
|
|
1032
|
-
(0, helpers_1.safeDispose)(instance).catch(err => {
|
|
1109
|
+
disposalTasks.push((0, helpers_1.safeDispose)(instance).catch(err => {
|
|
1033
1110
|
if (this.options.enableDiagnostics) {
|
|
1034
1111
|
console.warn('Error disposing tenant instance:', err);
|
|
1035
1112
|
}
|
|
1036
|
-
});
|
|
1113
|
+
}));
|
|
1037
1114
|
}
|
|
1038
1115
|
manager.delete(tenantId);
|
|
1039
1116
|
}
|
|
1040
|
-
//
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
this.
|
|
1044
|
-
}
|
|
1117
|
+
// Track disposal so bootstrap can wait for it before re-initializing
|
|
1118
|
+
if (disposalTasks.length > 0) {
|
|
1119
|
+
const disposalPromise = Promise.all(disposalTasks).then(() => {
|
|
1120
|
+
this.disposalPromises.delete(tenantId);
|
|
1121
|
+
});
|
|
1122
|
+
this.disposalPromises.set(tenantId, disposalPromise);
|
|
1123
|
+
}
|
|
1124
|
+
// Clear initializer cache for this tenant (exact match, not substring)
|
|
1125
|
+
const exactKey = `tenant:${tenantId}`;
|
|
1126
|
+
if (this.initializerCache.get(exactKey) !== undefined) {
|
|
1127
|
+
this.initializerCache.delete(exactKey);
|
|
1045
1128
|
}
|
|
1046
1129
|
}
|
|
1047
1130
|
/**
|
|
1048
1131
|
* Invalidate cached data for a specific service type (local only) with disposal support
|
|
1049
1132
|
*/
|
|
1050
1133
|
invalidateServiceLocally(serviceType, reason) {
|
|
1134
|
+
this.emit({
|
|
1135
|
+
type: 'service:invalidated',
|
|
1136
|
+
serviceType,
|
|
1137
|
+
reason,
|
|
1138
|
+
timestamp: Date.now(),
|
|
1139
|
+
});
|
|
1051
1140
|
if (this.options.enableDiagnostics) {
|
|
1052
1141
|
console.log(`Invalidating service cache locally: ${serviceType}`, reason ? `(${reason})` : '');
|
|
1053
1142
|
}
|
|
@@ -1119,8 +1208,9 @@ class Container {
|
|
|
1119
1208
|
}
|
|
1120
1209
|
this.proxyCache.clear();
|
|
1121
1210
|
this.initializerCache.clear();
|
|
1122
|
-
this.initializerAccessOrder.clear();
|
|
1123
1211
|
this.initializerPromises.clear();
|
|
1212
|
+
this.disposalPromises.clear();
|
|
1213
|
+
this.initializerCooldowns.clear();
|
|
1124
1214
|
return result;
|
|
1125
1215
|
}
|
|
1126
1216
|
/**
|
|
@@ -1159,7 +1249,9 @@ class Container {
|
|
|
1159
1249
|
pathCacheSize: 0, // removed - no longer tracked
|
|
1160
1250
|
proxyCacheSize: this.proxyCache.size,
|
|
1161
1251
|
factoryCacheSize: this.factoryCache.size,
|
|
1162
|
-
initializerCacheSize: this.initializerCache.size
|
|
1252
|
+
initializerCacheSize: typeof this.initializerCache.size === 'function'
|
|
1253
|
+
? this.initializerCache.size()
|
|
1254
|
+
: (this.initializerCache.size ?? 0),
|
|
1163
1255
|
initializerPromisesSize: this.initializerPromises.size,
|
|
1164
1256
|
cacheHitRatio: hits + misses > 0 ? hits / (hits + misses) : 0,
|
|
1165
1257
|
batchSuccessRatio: batchOps > 0 ? (batchOps - batchErrors) / batchOps : 0,
|
|
@@ -1299,6 +1391,34 @@ class Container {
|
|
|
1299
1391
|
}
|
|
1300
1392
|
}
|
|
1301
1393
|
}
|
|
1394
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1395
|
+
// 🛑 KILL SWITCH
|
|
1396
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1397
|
+
/**
|
|
1398
|
+
* Block a tenant from bootstrapping. Blocked tenants are rejected immediately.
|
|
1399
|
+
* Use this for: runaway crons, infinite loops, compromised keys, legal holds.
|
|
1400
|
+
*/
|
|
1401
|
+
blockTenant(tenantId) {
|
|
1402
|
+
this.blockedTenants.add(tenantId);
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Unblock a previously blocked tenant, restoring normal bootstrap behavior.
|
|
1406
|
+
*/
|
|
1407
|
+
unblockTenant(tenantId) {
|
|
1408
|
+
this.blockedTenants.delete(tenantId);
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Check if a tenant is currently blocked.
|
|
1412
|
+
*/
|
|
1413
|
+
isTenantBlocked(tenantId) {
|
|
1414
|
+
return this.blockedTenants.has(tenantId);
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Get the set of all currently blocked tenant IDs.
|
|
1418
|
+
*/
|
|
1419
|
+
getBlockedTenants() {
|
|
1420
|
+
return this.blockedTenants;
|
|
1421
|
+
}
|
|
1302
1422
|
}
|
|
1303
1423
|
exports.Container = Container;
|
|
1304
1424
|
//# sourceMappingURL=Container.js.map
|