@goatlab/node-backend 0.2.6 → 0.4.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/README.md +146 -14
- package/dist/container/Container.d.ts +86 -2
- package/dist/container/Container.js +260 -10
- package/dist/container/Container.js.map +1 -1
- package/dist/container/examples/batch-operations.example.d.ts +1 -0
- package/dist/container/examples/batch-operations.example.js +165 -0
- package/dist/container/examples/batch-operations.example.js.map +1 -0
- package/dist/container/types.d.ts +50 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server/bootstraps/getExpressTrpcApp.js +69 -7
- package/dist/server/bootstraps/getExpressTrpcApp.js.map +1 -1
- package/dist/server/middleware/memoryMonitor.example.d.ts +1 -0
- package/dist/server/middleware/memoryMonitor.example.js +109 -0
- package/dist/server/middleware/memoryMonitor.example.js.map +1 -0
- package/dist/server/middleware/memoryMonitor.middleware.d.ts +42 -0
- package/dist/server/middleware/memoryMonitor.middleware.js +134 -0
- package/dist/server/middleware/memoryMonitor.middleware.js.map +1 -0
- package/dist/server/services/secrets/examples/container-preload.example.d.ts +1 -0
- package/dist/server/services/secrets/examples/container-preload.example.js +148 -0
- package/dist/server/services/secrets/examples/container-preload.example.js.map +1 -0
- package/dist/server/services/secrets/index.d.ts +1 -0
- package/dist/server/services/secrets/index.js +6 -0
- package/dist/server/services/secrets/index.js.map +1 -0
- package/dist/server/services/secrets/secret.service.d.ts +48 -6
- package/dist/server/services/secrets/secret.service.js +280 -28
- package/dist/server/services/secrets/secret.service.js.map +1 -1
- package/dist/server/services/translations/translation.model.js +2 -1
- package/dist/server/services/translations/translation.model.js.map +1 -1
- package/dist/server/services/translations/translation.service.d.ts +8 -1
- package/dist/server/services/translations/translation.service.js +123 -13
- package/dist/server/services/translations/translation.service.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,38 +62,88 @@ const result = await cache.remember('expensive-op', 300000, async () => {
|
|
|
62
62
|
|
|
63
63
|
## Secret Management
|
|
64
64
|
|
|
65
|
-
The `SecretService` provides secure secret management with support for multiple backends:
|
|
65
|
+
The `SecretService` provides secure secret management with support for multiple backends and preloading:
|
|
66
66
|
|
|
67
67
|
```typescript
|
|
68
68
|
import { SecretService } from '@goatlab/node-backend'
|
|
69
69
|
|
|
70
|
-
// File-based encrypted secrets
|
|
71
|
-
const fileSecrets = new SecretService(
|
|
70
|
+
// File-based encrypted secrets with TTL caching
|
|
71
|
+
const fileSecrets = new SecretService({
|
|
72
|
+
provider: 'FILE',
|
|
73
|
+
location: '/path/to/secrets.json',
|
|
74
|
+
encryptionKey: process.env.ENCRYPTION_KEY,
|
|
75
|
+
cacheTTL: 300000 // 5 minutes (optional, default: 5 minutes)
|
|
76
|
+
})
|
|
72
77
|
|
|
73
78
|
// HashiCorp Vault integration
|
|
74
|
-
const vaultSecrets = new SecretService(
|
|
79
|
+
const vaultSecrets = new SecretService({
|
|
80
|
+
provider: 'VAULT',
|
|
81
|
+
location: 'my-app/secrets',
|
|
82
|
+
encryptionKey: process.env.ENCRYPTION_KEY
|
|
83
|
+
})
|
|
75
84
|
|
|
76
|
-
// Environment variables
|
|
77
|
-
const envSecrets = new SecretService(
|
|
78
|
-
|
|
85
|
+
// Environment variables
|
|
86
|
+
const envSecrets = new SecretService({
|
|
87
|
+
provider: 'ENV',
|
|
88
|
+
location: 'APP', // Loads APP_* env vars
|
|
89
|
+
encryptionKey: process.env.ENCRYPTION_KEY // Now supports encryption for all providers
|
|
90
|
+
})
|
|
79
91
|
|
|
80
|
-
//
|
|
81
|
-
await fileSecrets.
|
|
82
|
-
const apiKey =
|
|
83
|
-
const config =
|
|
92
|
+
// Preload secrets for synchronous access (new!)
|
|
93
|
+
await fileSecrets.preload()
|
|
94
|
+
const apiKey = fileSecrets.getSecretSync('API_KEY') // Synchronous!
|
|
95
|
+
const config = fileSecrets.getSecretJsonSync('CONFIG') // Synchronous!
|
|
96
|
+
|
|
97
|
+
// Async operations still available
|
|
98
|
+
const apiKeyAsync = await fileSecrets.getSecret('API_KEY')
|
|
99
|
+
const configAsync = await fileSecrets.getSecretJson('CONFIG')
|
|
84
100
|
|
|
85
101
|
// Store secrets (FILE and VAULT providers)
|
|
86
102
|
await fileSecrets.storeSecrets({ API_KEY: 'secret-value' })
|
|
103
|
+
|
|
104
|
+
// Manual cache cleanup
|
|
105
|
+
SecretService.cleanupExpiredCache()
|
|
106
|
+
|
|
107
|
+
// Dispose when done (stops file watching)
|
|
108
|
+
fileSecrets.dispose()
|
|
87
109
|
```
|
|
88
110
|
|
|
89
111
|
### Secret Provider Features
|
|
90
112
|
|
|
91
|
-
- **FILE**: Encrypted local file storage using AES encryption
|
|
113
|
+
- **FILE**: Encrypted local file storage using AES encryption with file watching
|
|
92
114
|
- **VAULT**: HashiCorp Vault integration with automatic token management
|
|
93
|
-
- **ENV**: Runtime environment variable access with
|
|
94
|
-
- **
|
|
115
|
+
- **ENV**: Runtime environment variable access with encryption support
|
|
116
|
+
- **Preloading**: Load secrets once async, access synchronously afterward
|
|
117
|
+
- **Per-Tenant Encryption**: Each tenant can have its own encryption key
|
|
118
|
+
- **Automatic Invalidation**: File changes trigger automatic reload (FILE provider)
|
|
119
|
+
- **TTL Caching**: Configurable time-to-live caching with automatic expiration
|
|
95
120
|
- **Type Safety**: Generic type support for JSON secrets
|
|
96
121
|
|
|
122
|
+
### Preloading Pattern with Container
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const container = new Container(factories, async (preload, meta) => {
|
|
126
|
+
// Create and preload secret service
|
|
127
|
+
const secretService = preload.secrets(meta.tenantId, {
|
|
128
|
+
provider: 'FILE',
|
|
129
|
+
location: meta.secretsLocation,
|
|
130
|
+
encryptionKey: meta.encryptionKey
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
await secretService.preload() // Load once async
|
|
134
|
+
|
|
135
|
+
// Use sync methods for instant access
|
|
136
|
+
const dbUrl = secretService.getSecretSync('DATABASE_URL')
|
|
137
|
+
const apiKey = secretService.getSecretSync('API_KEY')
|
|
138
|
+
|
|
139
|
+
// Create other services with preloaded secrets
|
|
140
|
+
const database = preload.database(meta.tenantId, { url: dbUrl })
|
|
141
|
+
const api = preload.api(meta.tenantId, { apiKey })
|
|
142
|
+
|
|
143
|
+
return { secrets: secretService, database, api }
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
97
147
|
## Express + tRPC Integration
|
|
98
148
|
|
|
99
149
|
Helper for creating Express applications with tRPC integration:
|
|
@@ -117,6 +167,88 @@ const app = getExpressTrpcApp({
|
|
|
117
167
|
})
|
|
118
168
|
```
|
|
119
169
|
|
|
170
|
+
### Performance Features (New!)
|
|
171
|
+
|
|
172
|
+
- **Optimized Compression**: Automatically skips compression for small responses (<1KB), SSE, WebSocket upgrades, and pre-compressed content
|
|
173
|
+
- **Memory Monitoring**: Built-in middleware tracks heap usage and triggers garbage collection when needed
|
|
174
|
+
- **Smart Rate Limiting**: Different limits for auth endpoints, API endpoints, and general routes
|
|
175
|
+
|
|
176
|
+
## Container - Dependency Injection
|
|
177
|
+
|
|
178
|
+
Multi-tenant dependency injection container with performance optimizations:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { Container } from '@goatlab/node-backend'
|
|
182
|
+
|
|
183
|
+
// Define your service factories
|
|
184
|
+
const factories = {
|
|
185
|
+
database: DatabaseService,
|
|
186
|
+
api: ApiService,
|
|
187
|
+
cache: CacheService
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Create container with initializer
|
|
191
|
+
const container = new Container(factories, async (preload, tenantMeta) => {
|
|
192
|
+
const db = preload.database(tenantMeta.id, tenantMeta.dbConfig)
|
|
193
|
+
const cache = preload.cache(tenantMeta.id)
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
database: db,
|
|
197
|
+
api: preload.api(tenantMeta.id, db),
|
|
198
|
+
cache
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// Bootstrap for a tenant
|
|
203
|
+
await container.bootstrap(tenantMeta, async () => {
|
|
204
|
+
const { database, api } = container.context
|
|
205
|
+
// Use services...
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Batch operations (new!)
|
|
209
|
+
const results = await container.bootstrapBatch([
|
|
210
|
+
{ metadata: tenant1, fn: processTenant1 },
|
|
211
|
+
{ metadata: tenant2, fn: processTenant2 }
|
|
212
|
+
], {
|
|
213
|
+
concurrency: 10,
|
|
214
|
+
continueOnError: true,
|
|
215
|
+
onProgress: (completed, total) => console.log(`${completed}/${total}`)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// Batch cache invalidation (new!)
|
|
219
|
+
await container.invalidateTenantBatch(['tenant1', 'tenant2', 'tenant3'])
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Container Features
|
|
223
|
+
|
|
224
|
+
- **Multi-tenancy**: Isolated service instances per tenant
|
|
225
|
+
- **Batch Operations**: Process multiple tenants in parallel with concurrency control
|
|
226
|
+
- **Performance Metrics**: Built-in performance tracking and statistics
|
|
227
|
+
- **Cache Management**: Efficient caching with batch invalidation support
|
|
228
|
+
- **Type Safety**: Full TypeScript support with inference
|
|
229
|
+
|
|
230
|
+
## Translation Service
|
|
231
|
+
|
|
232
|
+
High-performance translation service with template caching:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { translationService } from '@goatlab/node-backend'
|
|
236
|
+
|
|
237
|
+
// Translations are automatically loaded and cached
|
|
238
|
+
const greeting = translationService.translate('welcome', { language: 'es' })
|
|
239
|
+
|
|
240
|
+
// With template parameters
|
|
241
|
+
const message = translationService.translate('user.greeting',
|
|
242
|
+
{ language: 'en' },
|
|
243
|
+
{ name: 'John' }
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
// Performance optimized with:
|
|
247
|
+
// - Compiled template caching
|
|
248
|
+
// - Locale preloading at startup
|
|
249
|
+
// - In-memory locale storage
|
|
250
|
+
```
|
|
251
|
+
|
|
120
252
|
## Testing Utilities
|
|
121
253
|
|
|
122
254
|
Comprehensive testing setup with testcontainers support:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContainerOptions, InstancesStructure, PreloadStructure } from './types';
|
|
1
|
+
import { ContainerOptions, InstancesStructure, PreloadStructure, BatchBootstrapOptions, BatchBootstrapResult, BatchInvalidationResult } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
4
4
|
* 🏗️ MULTI-TENANT DEPENDENCY INJECTION CONTAINER
|
|
@@ -128,7 +128,7 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
128
128
|
private readonly initializerCache;
|
|
129
129
|
/**
|
|
130
130
|
* High-performance metrics using Uint32Array for better JIT optimization
|
|
131
|
-
* Indices: [hits, misses, creates, ctx, proxy, initHits, resets]
|
|
131
|
+
* Indices: [hits, misses, creates, ctx, proxy, initHits, resets, batchOps, batchErrors]
|
|
132
132
|
* Auto-wraps at 2^32 without overflow checks for maximum performance
|
|
133
133
|
*/
|
|
134
134
|
private readonly metrics;
|
|
@@ -285,6 +285,85 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
285
285
|
instances: InstancesStructure<Defs>;
|
|
286
286
|
result?: T;
|
|
287
287
|
}>;
|
|
288
|
+
/**
|
|
289
|
+
* Bootstrap multiple tenants in parallel with controlled concurrency
|
|
290
|
+
*
|
|
291
|
+
* This method enables efficient initialization of multiple tenants while:
|
|
292
|
+
* - Controlling concurrency to avoid overwhelming the system
|
|
293
|
+
* - Isolating errors so one failure doesn't affect others
|
|
294
|
+
* - Providing progress tracking for long-running operations
|
|
295
|
+
* - Collecting performance metrics for each operation
|
|
296
|
+
*
|
|
297
|
+
* @param tenantBatch - Array of tenant metadata and optional functions to execute
|
|
298
|
+
* @param options - Options for controlling the batch operation
|
|
299
|
+
* @returns Array of results for each tenant, including successes and failures
|
|
300
|
+
*
|
|
301
|
+
* ```typescript
|
|
302
|
+
* const results = await container.bootstrapBatch([
|
|
303
|
+
* { metadata: tenant1Meta, fn: async () => processТenant1() },
|
|
304
|
+
* { metadata: tenant2Meta, fn: async () => processTenant2() },
|
|
305
|
+
* { metadata: tenant3Meta } // No function, just bootstrap
|
|
306
|
+
* ], {
|
|
307
|
+
* concurrency: 5,
|
|
308
|
+
* continueOnError: true,
|
|
309
|
+
* onProgress: (completed, total) => console.log(`${completed}/${total}`)
|
|
310
|
+
* })
|
|
311
|
+
*
|
|
312
|
+
* // Process results
|
|
313
|
+
* for (const result of results) {
|
|
314
|
+
* if (result.status === 'success') {
|
|
315
|
+
* console.log(`Tenant ${result.metadata.id} initialized in ${result.metrics.duration}ms`)
|
|
316
|
+
* } else {
|
|
317
|
+
* console.error(`Tenant ${result.metadata.id} failed:`, result.error)
|
|
318
|
+
* }
|
|
319
|
+
* }
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
bootstrapBatch<TMetadata = unknown, T = unknown>(tenantBatch: Array<{
|
|
323
|
+
metadata: TMetadata;
|
|
324
|
+
fn?: () => Promise<T>;
|
|
325
|
+
}>, options?: BatchBootstrapOptions<TMetadata>): Promise<Array<BatchBootstrapResult<Defs, TMetadata, T>>>;
|
|
326
|
+
/**
|
|
327
|
+
* Invalidate multiple tenant caches in batch
|
|
328
|
+
*
|
|
329
|
+
* Efficiently invalidates caches for multiple tenants with proper disposal
|
|
330
|
+
* and error handling. Useful for bulk updates or maintenance operations.
|
|
331
|
+
*
|
|
332
|
+
* @param tenantIds - Array of tenant IDs to invalidate
|
|
333
|
+
* @param reason - Optional reason for invalidation (for logging)
|
|
334
|
+
* @param distributed - Whether to propagate invalidation to other instances
|
|
335
|
+
* @returns Summary of the batch invalidation operation
|
|
336
|
+
*
|
|
337
|
+
* ```typescript
|
|
338
|
+
* const result = await container.invalidateTenantBatch(
|
|
339
|
+
* ['tenant1', 'tenant2', 'tenant3'],
|
|
340
|
+
* 'Bulk configuration update',
|
|
341
|
+
* true // Distribute to other instances
|
|
342
|
+
* )
|
|
343
|
+
*
|
|
344
|
+
* console.log(`Invalidated ${result.succeeded}/${result.total} tenants`)
|
|
345
|
+
* if (result.failed > 0) {
|
|
346
|
+
* console.error('Failed invalidations:', result.errors)
|
|
347
|
+
* }
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
invalidateTenantBatch(tenantIds: string[], reason?: string, distributed?: boolean): Promise<BatchInvalidationResult>;
|
|
351
|
+
/**
|
|
352
|
+
* Invalidate multiple service caches in batch
|
|
353
|
+
*
|
|
354
|
+
* @param serviceTypes - Array of service types to invalidate
|
|
355
|
+
* @param reason - Optional reason for invalidation
|
|
356
|
+
* @param distributed - Whether to propagate invalidation
|
|
357
|
+
* @returns Summary of the batch invalidation operation
|
|
358
|
+
*
|
|
359
|
+
* ```typescript
|
|
360
|
+
* const result = await container.invalidateServiceBatch(
|
|
361
|
+
* ['database', 'api.users', 'api.auth'],
|
|
362
|
+
* 'Service configuration update'
|
|
363
|
+
* )
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
invalidateServiceBatch(serviceTypes: string[], reason?: string, distributed?: boolean): Promise<BatchInvalidationResult>;
|
|
288
367
|
/**
|
|
289
368
|
* Get current performance metrics
|
|
290
369
|
* Converts Uint32Array back to object format for compatibility
|
|
@@ -296,6 +375,8 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
296
375
|
contextAccesses: number;
|
|
297
376
|
proxyCacheHits: number;
|
|
298
377
|
initializerCacheHits: number;
|
|
378
|
+
batchOperations: number;
|
|
379
|
+
batchErrors: number;
|
|
299
380
|
};
|
|
300
381
|
/**
|
|
301
382
|
* Reset all performance metrics to zero
|
|
@@ -374,12 +455,15 @@ export declare class Container<Defs extends Record<string, unknown>, TenantMetad
|
|
|
374
455
|
initializerCacheSize: number;
|
|
375
456
|
initializerPromisesSize: number;
|
|
376
457
|
cacheHitRatio: number;
|
|
458
|
+
batchSuccessRatio: number;
|
|
377
459
|
cacheHits: number;
|
|
378
460
|
cacheMisses: number;
|
|
379
461
|
instanceCreations: number;
|
|
380
462
|
contextAccesses: number;
|
|
381
463
|
proxyCacheHits: number;
|
|
382
464
|
initializerCacheHits: number;
|
|
465
|
+
batchOperations: number;
|
|
466
|
+
batchErrors: number;
|
|
383
467
|
};
|
|
384
468
|
/**
|
|
385
469
|
* Check if there's an active tenant context
|
|
@@ -147,10 +147,10 @@ class Container {
|
|
|
147
147
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
148
148
|
/**
|
|
149
149
|
* High-performance metrics using Uint32Array for better JIT optimization
|
|
150
|
-
* Indices: [hits, misses, creates, ctx, proxy, initHits, resets]
|
|
150
|
+
* Indices: [hits, misses, creates, ctx, proxy, initHits, resets, batchOps, batchErrors]
|
|
151
151
|
* Auto-wraps at 2^32 without overflow checks for maximum performance
|
|
152
152
|
*/
|
|
153
|
-
metrics = new Uint32Array(
|
|
153
|
+
metrics = new Uint32Array(9);
|
|
154
154
|
/**
|
|
155
155
|
* Metric indices for Uint32Array
|
|
156
156
|
*/
|
|
@@ -162,6 +162,8 @@ class Container {
|
|
|
162
162
|
PROXIES: 4,
|
|
163
163
|
INIT_HITS: 5,
|
|
164
164
|
RESETS: 6,
|
|
165
|
+
BATCH_OPS: 7,
|
|
166
|
+
BATCH_ERRORS: 8
|
|
165
167
|
};
|
|
166
168
|
/**
|
|
167
169
|
* Legacy overflow threshold for test compatibility
|
|
@@ -176,11 +178,20 @@ class Container {
|
|
|
176
178
|
if (!this.options.enableMetrics)
|
|
177
179
|
return;
|
|
178
180
|
// Check for test mock of MAX_METRIC_VALUE (legacy compatibility)
|
|
179
|
-
if (this.MAX_METRIC_VALUE < 1000 &&
|
|
181
|
+
if (this.MAX_METRIC_VALUE < 1000 &&
|
|
182
|
+
this.metrics[idx] >= this.MAX_METRIC_VALUE) {
|
|
180
183
|
// Legacy test behavior - reset metrics when mock threshold reached
|
|
181
184
|
this.resetMetrics();
|
|
182
185
|
if (this.options.enableDiagnostics) {
|
|
183
|
-
const metricNames = [
|
|
186
|
+
const metricNames = [
|
|
187
|
+
'cacheHits',
|
|
188
|
+
'cacheMisses',
|
|
189
|
+
'instanceCreations',
|
|
190
|
+
'contextAccesses',
|
|
191
|
+
'proxyCacheHits',
|
|
192
|
+
'initializerCacheHits',
|
|
193
|
+
'resets'
|
|
194
|
+
];
|
|
184
195
|
console.warn(`Container metrics reset due to overflow protection. Metric '${metricNames[idx] || 'unknown'}' reached ${this.metrics[idx]}`);
|
|
185
196
|
}
|
|
186
197
|
}
|
|
@@ -213,7 +224,7 @@ class Container {
|
|
|
213
224
|
enableDiagnostics: false,
|
|
214
225
|
enableDistributedInvalidation: false,
|
|
215
226
|
distributedInvalidator: undefined,
|
|
216
|
-
...options
|
|
227
|
+
...options
|
|
217
228
|
};
|
|
218
229
|
// Pre-cache factory lookups for better performance
|
|
219
230
|
this.preloadFactoryCache();
|
|
@@ -323,7 +334,7 @@ class Container {
|
|
|
323
334
|
}
|
|
324
335
|
// No factory found - must be a nested path, return another proxy
|
|
325
336
|
return this.createPreloadProxy(newPath);
|
|
326
|
-
}
|
|
337
|
+
}
|
|
327
338
|
});
|
|
328
339
|
this.proxyCache.set(path, proxy);
|
|
329
340
|
return proxy;
|
|
@@ -408,7 +419,7 @@ class Container {
|
|
|
408
419
|
// Property exists but is undefined - this is valid (e.g., optional services)
|
|
409
420
|
return undefined;
|
|
410
421
|
}
|
|
411
|
-
// For symbols, especially well-known symbols like Symbol.iterator,
|
|
422
|
+
// For symbols, especially well-known symbols like Symbol.iterator,
|
|
412
423
|
// just return undefined instead of throwing an error
|
|
413
424
|
if (typeof prop === 'symbol') {
|
|
414
425
|
return undefined;
|
|
@@ -445,7 +456,7 @@ class Container {
|
|
|
445
456
|
}
|
|
446
457
|
// Return value as-is (primitives, functions, Promises, arrays)
|
|
447
458
|
return value;
|
|
448
|
-
}
|
|
459
|
+
}
|
|
449
460
|
});
|
|
450
461
|
// Cache using WeakMap for automatic garbage collection
|
|
451
462
|
this.contextProxyCache.set(obj, proxy);
|
|
@@ -463,7 +474,7 @@ class Container {
|
|
|
463
474
|
simpleHash(str) {
|
|
464
475
|
let hash = 5381;
|
|
465
476
|
for (let i = 0; i < str.length; i++) {
|
|
466
|
-
hash = (
|
|
477
|
+
hash = (hash << 5) + hash + str.charCodeAt(i);
|
|
467
478
|
}
|
|
468
479
|
return (hash >>> 0).toString(36);
|
|
469
480
|
}
|
|
@@ -561,6 +572,240 @@ class Container {
|
|
|
561
572
|
}
|
|
562
573
|
}
|
|
563
574
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
575
|
+
// BATCH OPERATIONS
|
|
576
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
577
|
+
/**
|
|
578
|
+
* Bootstrap multiple tenants in parallel with controlled concurrency
|
|
579
|
+
*
|
|
580
|
+
* This method enables efficient initialization of multiple tenants while:
|
|
581
|
+
* - Controlling concurrency to avoid overwhelming the system
|
|
582
|
+
* - Isolating errors so one failure doesn't affect others
|
|
583
|
+
* - Providing progress tracking for long-running operations
|
|
584
|
+
* - Collecting performance metrics for each operation
|
|
585
|
+
*
|
|
586
|
+
* @param tenantBatch - Array of tenant metadata and optional functions to execute
|
|
587
|
+
* @param options - Options for controlling the batch operation
|
|
588
|
+
* @returns Array of results for each tenant, including successes and failures
|
|
589
|
+
*
|
|
590
|
+
* ```typescript
|
|
591
|
+
* const results = await container.bootstrapBatch([
|
|
592
|
+
* { metadata: tenant1Meta, fn: async () => processТenant1() },
|
|
593
|
+
* { metadata: tenant2Meta, fn: async () => processTenant2() },
|
|
594
|
+
* { metadata: tenant3Meta } // No function, just bootstrap
|
|
595
|
+
* ], {
|
|
596
|
+
* concurrency: 5,
|
|
597
|
+
* continueOnError: true,
|
|
598
|
+
* onProgress: (completed, total) => console.log(`${completed}/${total}`)
|
|
599
|
+
* })
|
|
600
|
+
*
|
|
601
|
+
* // Process results
|
|
602
|
+
* for (const result of results) {
|
|
603
|
+
* if (result.status === 'success') {
|
|
604
|
+
* console.log(`Tenant ${result.metadata.id} initialized in ${result.metrics.duration}ms`)
|
|
605
|
+
* } else {
|
|
606
|
+
* console.error(`Tenant ${result.metadata.id} failed:`, result.error)
|
|
607
|
+
* }
|
|
608
|
+
* }
|
|
609
|
+
* ```
|
|
610
|
+
*/
|
|
611
|
+
async bootstrapBatch(tenantBatch, options = {}) {
|
|
612
|
+
const { concurrency = 10, continueOnError = true, timeout, onProgress } = options;
|
|
613
|
+
const results = [];
|
|
614
|
+
const total = tenantBatch.length;
|
|
615
|
+
let completed = 0;
|
|
616
|
+
let shouldAbort = false;
|
|
617
|
+
// Process tenants in chunks based on concurrency limit
|
|
618
|
+
for (let i = 0; i < total; i += concurrency) {
|
|
619
|
+
// Check if we should abort due to previous error in fail-fast mode
|
|
620
|
+
if (shouldAbort) {
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
const chunk = tenantBatch.slice(i, i + concurrency);
|
|
624
|
+
const chunkPromises = chunk.map(async ({ metadata, fn }) => {
|
|
625
|
+
const startTime = Date.now();
|
|
626
|
+
try {
|
|
627
|
+
// Apply timeout if specified
|
|
628
|
+
let bootstrapPromise = this.bootstrap(metadata, fn);
|
|
629
|
+
if (timeout) {
|
|
630
|
+
bootstrapPromise = Promise.race([
|
|
631
|
+
bootstrapPromise,
|
|
632
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Bootstrap timeout after ${timeout}ms`)), timeout))
|
|
633
|
+
]);
|
|
634
|
+
}
|
|
635
|
+
const { instances, result } = await bootstrapPromise;
|
|
636
|
+
const endTime = Date.now();
|
|
637
|
+
this.inc(Container.METRIC.BATCH_OPS);
|
|
638
|
+
return {
|
|
639
|
+
metadata,
|
|
640
|
+
status: 'success',
|
|
641
|
+
instances,
|
|
642
|
+
result,
|
|
643
|
+
metrics: {
|
|
644
|
+
startTime,
|
|
645
|
+
endTime,
|
|
646
|
+
duration: endTime - startTime
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
const endTime = Date.now();
|
|
652
|
+
this.inc(Container.METRIC.BATCH_ERRORS);
|
|
653
|
+
if (this.options.enableDiagnostics) {
|
|
654
|
+
console.error(`Batch bootstrap failed for tenant:`, metadata, error);
|
|
655
|
+
}
|
|
656
|
+
const result = {
|
|
657
|
+
metadata,
|
|
658
|
+
status: 'error',
|
|
659
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
660
|
+
metrics: {
|
|
661
|
+
startTime,
|
|
662
|
+
endTime,
|
|
663
|
+
duration: endTime - startTime
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
if (!continueOnError) {
|
|
667
|
+
// Mark that we should abort processing
|
|
668
|
+
shouldAbort = true;
|
|
669
|
+
}
|
|
670
|
+
return result;
|
|
671
|
+
}
|
|
672
|
+
finally {
|
|
673
|
+
completed++;
|
|
674
|
+
onProgress?.(completed, total, metadata);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
// Wait for current chunk to complete before starting next
|
|
678
|
+
const chunkResults = await Promise.allSettled(chunkPromises);
|
|
679
|
+
// Extract results from Promise.allSettled
|
|
680
|
+
for (const settledResult of chunkResults) {
|
|
681
|
+
if (settledResult.status === 'fulfilled') {
|
|
682
|
+
results.push(settledResult.value);
|
|
683
|
+
}
|
|
684
|
+
else if (continueOnError) {
|
|
685
|
+
// This shouldn't happen as we handle errors above, but just in case
|
|
686
|
+
results.push({
|
|
687
|
+
metadata: tenantBatch[results.length].metadata,
|
|
688
|
+
status: 'error',
|
|
689
|
+
error: settledResult.reason,
|
|
690
|
+
metrics: {
|
|
691
|
+
startTime: Date.now(),
|
|
692
|
+
endTime: Date.now(),
|
|
693
|
+
duration: 0
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Check if we had any errors and should fail fast
|
|
699
|
+
if (!continueOnError && results.some(r => r.status === 'error')) {
|
|
700
|
+
const errorResult = results.find(r => r.status === 'error');
|
|
701
|
+
throw errorResult?.error || new Error('Batch operation failed');
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return results;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Invalidate multiple tenant caches in batch
|
|
708
|
+
*
|
|
709
|
+
* Efficiently invalidates caches for multiple tenants with proper disposal
|
|
710
|
+
* and error handling. Useful for bulk updates or maintenance operations.
|
|
711
|
+
*
|
|
712
|
+
* @param tenantIds - Array of tenant IDs to invalidate
|
|
713
|
+
* @param reason - Optional reason for invalidation (for logging)
|
|
714
|
+
* @param distributed - Whether to propagate invalidation to other instances
|
|
715
|
+
* @returns Summary of the batch invalidation operation
|
|
716
|
+
*
|
|
717
|
+
* ```typescript
|
|
718
|
+
* const result = await container.invalidateTenantBatch(
|
|
719
|
+
* ['tenant1', 'tenant2', 'tenant3'],
|
|
720
|
+
* 'Bulk configuration update',
|
|
721
|
+
* true // Distribute to other instances
|
|
722
|
+
* )
|
|
723
|
+
*
|
|
724
|
+
* console.log(`Invalidated ${result.succeeded}/${result.total} tenants`)
|
|
725
|
+
* if (result.failed > 0) {
|
|
726
|
+
* console.error('Failed invalidations:', result.errors)
|
|
727
|
+
* }
|
|
728
|
+
* ```
|
|
729
|
+
*/
|
|
730
|
+
async invalidateTenantBatch(tenantIds, reason, distributed = false) {
|
|
731
|
+
const result = {
|
|
732
|
+
total: tenantIds.length,
|
|
733
|
+
succeeded: 0,
|
|
734
|
+
failed: 0,
|
|
735
|
+
errors: []
|
|
736
|
+
};
|
|
737
|
+
// Process invalidations in parallel with error isolation
|
|
738
|
+
const invalidationPromises = tenantIds.map(async (tenantId) => {
|
|
739
|
+
try {
|
|
740
|
+
if (distributed) {
|
|
741
|
+
await this.invalidateTenantDistributed(tenantId, reason);
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
this.invalidateTenantLocally(tenantId, reason);
|
|
745
|
+
}
|
|
746
|
+
result.succeeded++;
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
result.failed++;
|
|
750
|
+
result.errors.push({
|
|
751
|
+
key: tenantId,
|
|
752
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
753
|
+
});
|
|
754
|
+
if (this.options.enableDiagnostics) {
|
|
755
|
+
console.error(`Failed to invalidate tenant ${tenantId}:`, error);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
await Promise.allSettled(invalidationPromises);
|
|
760
|
+
return result;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Invalidate multiple service caches in batch
|
|
764
|
+
*
|
|
765
|
+
* @param serviceTypes - Array of service types to invalidate
|
|
766
|
+
* @param reason - Optional reason for invalidation
|
|
767
|
+
* @param distributed - Whether to propagate invalidation
|
|
768
|
+
* @returns Summary of the batch invalidation operation
|
|
769
|
+
*
|
|
770
|
+
* ```typescript
|
|
771
|
+
* const result = await container.invalidateServiceBatch(
|
|
772
|
+
* ['database', 'api.users', 'api.auth'],
|
|
773
|
+
* 'Service configuration update'
|
|
774
|
+
* )
|
|
775
|
+
* ```
|
|
776
|
+
*/
|
|
777
|
+
async invalidateServiceBatch(serviceTypes, reason, distributed = false) {
|
|
778
|
+
const result = {
|
|
779
|
+
total: serviceTypes.length,
|
|
780
|
+
succeeded: 0,
|
|
781
|
+
failed: 0,
|
|
782
|
+
errors: []
|
|
783
|
+
};
|
|
784
|
+
const invalidationPromises = serviceTypes.map(async (serviceType) => {
|
|
785
|
+
try {
|
|
786
|
+
if (distributed) {
|
|
787
|
+
await this.invalidateServiceDistributed(serviceType, reason);
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
this.invalidateServiceLocally(serviceType, reason);
|
|
791
|
+
}
|
|
792
|
+
result.succeeded++;
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
result.failed++;
|
|
796
|
+
result.errors.push({
|
|
797
|
+
key: serviceType,
|
|
798
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
799
|
+
});
|
|
800
|
+
if (this.options.enableDiagnostics) {
|
|
801
|
+
console.error(`Failed to invalidate service ${serviceType}:`, error);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
await Promise.allSettled(invalidationPromises);
|
|
806
|
+
return result;
|
|
807
|
+
}
|
|
808
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
564
809
|
// 📊 OBSERVABILITY & DEBUGGING
|
|
565
810
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
566
811
|
/**
|
|
@@ -575,6 +820,8 @@ class Container {
|
|
|
575
820
|
contextAccesses: this.metrics[Container.METRIC.CONTEXTS],
|
|
576
821
|
proxyCacheHits: this.metrics[Container.METRIC.PROXIES],
|
|
577
822
|
initializerCacheHits: this.metrics[Container.METRIC.INIT_HITS],
|
|
823
|
+
batchOperations: this.metrics[Container.METRIC.BATCH_OPS],
|
|
824
|
+
batchErrors: this.metrics[Container.METRIC.BATCH_ERRORS]
|
|
578
825
|
};
|
|
579
826
|
}
|
|
580
827
|
/**
|
|
@@ -780,7 +1027,7 @@ class Container {
|
|
|
780
1027
|
: managerAny.size || 0;
|
|
781
1028
|
stats[key] = {
|
|
782
1029
|
size,
|
|
783
|
-
maxSize: this.options.cacheSize
|
|
1030
|
+
maxSize: this.options.cacheSize
|
|
784
1031
|
};
|
|
785
1032
|
}
|
|
786
1033
|
return stats;
|
|
@@ -794,6 +1041,8 @@ class Container {
|
|
|
794
1041
|
const totalCacheSize = Object.values(cacheStats).reduce((sum, stat) => sum + stat.size, 0);
|
|
795
1042
|
const hits = this.metrics[Container.METRIC.HITS];
|
|
796
1043
|
const misses = this.metrics[Container.METRIC.MISSES];
|
|
1044
|
+
const batchOps = this.metrics[Container.METRIC.BATCH_OPS];
|
|
1045
|
+
const batchErrors = this.metrics[Container.METRIC.BATCH_ERRORS];
|
|
797
1046
|
return {
|
|
798
1047
|
...this.getMetrics(),
|
|
799
1048
|
cacheStats,
|
|
@@ -804,6 +1053,7 @@ class Container {
|
|
|
804
1053
|
initializerCacheSize: this.initializerCache.size,
|
|
805
1054
|
initializerPromisesSize: this.initializerPromises.size,
|
|
806
1055
|
cacheHitRatio: hits + misses > 0 ? hits / (hits + misses) : 0,
|
|
1056
|
+
batchSuccessRatio: batchOps > 0 ? (batchOps - batchErrors) / batchOps : 0
|
|
807
1057
|
};
|
|
808
1058
|
}
|
|
809
1059
|
// ═══════════════════════════════════════════════════════════════════════════
|