@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.
@@ -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
- * Key is a serialized version of tenant metadata, value is the initialized instances
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
- initializerAccessOrder = new Map();
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, Prisma clients, or Redis clients
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
- // Avoid wrapping Prisma clients (they have complex internal properties)
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
- // Avoid wrapping Redis clients (they have ioredis-specific properties)
494
+ // Redis/ioredis clients
463
495
  !('options' in value && 'status' in value && 'connector' in value) &&
464
- // Avoid wrapping cache service objects (Keyv instances with opts property)
496
+ // Keyv instances
465
497
  !('opts' in value) &&
466
- // Avoid wrapping cache store objects
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
- // Last resort for circular refs
511
- return `tenant:ts:${Date.now()}`;
512
- }
513
- }
514
- /**
515
- * Evict the least recently used entry from initializerCache if at capacity
516
- * Called before adding a new entry when cache is at maxInitializerCacheSize
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
- // Evict LRU entry if cache is at capacity
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
- // Track access order for LRU
566
- this.initializerAccessOrder.set(cacheKey, ++this.initializerAccessCounter);
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 tenants in chunks based on concurrency limit
661
- for (let i = 0; i < total; i += concurrency) {
662
- // Check if we should abort due to previous error in fail-fast mode
663
- if (shouldAbort) {
664
- break;
665
- }
666
- const chunk = tenantBatch.slice(i, i + concurrency);
667
- const chunkPromises = chunk.map(async ({ metadata, fn }) => {
668
- const startTime = Date.now();
669
- try {
670
- // Apply timeout if specified
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
- // Wait for current chunk to complete before starting next
721
- const chunkResults = await Promise.allSettled(chunkPromises);
722
- // Extract results from Promise.allSettled
723
- for (const settledResult of chunkResults) {
724
- if (settledResult.status === 'fulfilled') {
725
- results.push(settledResult.value);
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
- else if (continueOnError) {
728
- // This shouldn't happen as we handle errors above, but just in case
729
- results.push({
730
- metadata: tenantBatch[results.length].metadata,
731
- status: 'error',
732
- error: settledResult.reason,
733
- metrics: {
734
- startTime: Date.now(),
735
- endTime: Date.now(),
736
- duration: 0,
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
- // Check if we had any errors and should fail fast
742
- if (!continueOnError && results.some(r => r.status === 'error')) {
743
- const errorResult = results.find(r => r.status === 'error');
744
- throw errorResult?.error || new Error('Batch operation failed');
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
- return results;
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
- // Clear initializer cache for this tenant
1041
- for (const [cacheKey] of this.initializerCache.entries()) {
1042
- if (cacheKey.includes(tenantId)) {
1043
- this.initializerCache.delete(cacheKey);
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