@goatlab/node-backend 0.0.16 → 0.1.1

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.
Files changed (129) hide show
  1. package/README.md +54 -38
  2. package/dist/container/Container.d.ts +441 -0
  3. package/dist/container/Container.js +895 -0
  4. package/dist/container/Container.js.map +1 -0
  5. package/dist/container/DistributedCacheInvalidator.d.ts +84 -0
  6. package/dist/container/DistributedCacheInvalidator.js +213 -0
  7. package/dist/container/DistributedCacheInvalidator.js.map +1 -0
  8. package/dist/container/LruCache.d.ts +14 -0
  9. package/dist/container/LruCache.js +23 -0
  10. package/dist/container/LruCache.js.map +1 -0
  11. package/dist/container/types.d.ts +128 -0
  12. package/dist/container/types.js +6 -0
  13. package/dist/container/types.js.map +1 -0
  14. package/dist/index.d.ts +31 -0
  15. package/dist/index.js +53 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/server/bootstraps/getExpressTrpcApp.d.ts +17 -0
  18. package/dist/server/bootstraps/getExpressTrpcApp.js +98 -0
  19. package/dist/server/bootstraps/getExpressTrpcApp.js.map +1 -0
  20. package/dist/server/consts.d.ts +35 -0
  21. package/dist/server/consts.js +33 -0
  22. package/dist/server/consts.js.map +1 -0
  23. package/dist/server/context/context.model.d.ts +13 -0
  24. package/dist/server/context/context.model.js +3 -0
  25. package/dist/server/context/context.model.js.map +1 -0
  26. package/dist/server/context/request.context.d.ts +40 -0
  27. package/dist/server/context/request.context.js +91 -0
  28. package/dist/server/context/request.context.js.map +1 -0
  29. package/dist/server/context/trpc.context.d.ts +11 -0
  30. package/dist/server/context/trpc.context.js +67 -0
  31. package/dist/server/context/trpc.context.js.map +1 -0
  32. package/dist/server/initOpenApiDocs.d.ts +9 -0
  33. package/dist/server/initOpenApiDocs.js +18 -0
  34. package/dist/server/initOpenApiDocs.js.map +1 -0
  35. package/dist/server/middleware/cloudTaskDecrypt.middleware.d.ts +6 -0
  36. package/dist/server/middleware/cloudTaskDecrypt.middleware.js +44 -0
  37. package/dist/server/middleware/cloudTaskDecrypt.middleware.js.map +1 -0
  38. package/dist/server/middleware/error.middleware.d.ts +17 -0
  39. package/dist/server/middleware/error.middleware.js +66 -0
  40. package/dist/server/middleware/error.middleware.js.map +1 -0
  41. package/dist/server/middleware/handleRequest.middleware.d.ts +7 -0
  42. package/dist/server/middleware/handleRequest.middleware.js +40 -0
  43. package/dist/server/middleware/handleRequest.middleware.js.map +1 -0
  44. package/dist/server/middleware/logger/cloudRun.logger.d.ts +27 -0
  45. package/dist/server/middleware/logger/cloudRun.logger.js +87 -0
  46. package/dist/server/middleware/logger/cloudRun.logger.js.map +1 -0
  47. package/dist/server/middleware/logger/logger.service.d.ts +6 -0
  48. package/dist/server/middleware/logger/logger.service.js +17 -0
  49. package/dist/server/middleware/logger/logger.service.js.map +1 -0
  50. package/dist/server/middleware/logs.middleware.d.ts +7 -0
  51. package/dist/server/middleware/logs.middleware.js +130 -0
  52. package/dist/server/middleware/logs.middleware.js.map +1 -0
  53. package/dist/server/middleware/requireAuthenticated.d.ts +2 -0
  54. package/dist/server/middleware/requireAuthenticated.js +13 -0
  55. package/dist/server/middleware/requireAuthenticated.js.map +1 -0
  56. package/dist/server/middleware/trpcError.middleware.d.ts +4 -0
  57. package/dist/server/middleware/trpcError.middleware.js +38 -0
  58. package/dist/server/middleware/trpcError.middleware.js.map +1 -0
  59. package/dist/server/schemas/user.schema.d.ts +109 -0
  60. package/dist/server/schemas/user.schema.js +28 -0
  61. package/dist/server/schemas/user.schema.js.map +1 -0
  62. package/dist/server/sentry/getSentry.d.ts +6 -0
  63. package/dist/server/sentry/getSentry.js +45 -0
  64. package/dist/server/sentry/getSentry.js.map +1 -0
  65. package/dist/server/sentry/sentry.service.d.ts +34 -0
  66. package/dist/server/sentry/sentry.service.js +110 -0
  67. package/dist/server/sentry/sentry.service.js.map +1 -0
  68. package/dist/server/services/email/email.model.d.ts +84 -0
  69. package/dist/server/services/email/email.model.js +62 -0
  70. package/dist/server/services/email/email.model.js.map +1 -0
  71. package/dist/server/services/email/email.service.d.ts +23 -0
  72. package/dist/server/services/email/email.service.js +139 -0
  73. package/dist/server/services/email/email.service.js.map +1 -0
  74. package/dist/server/services/gcp/getGcpServiceAccountFromBase64.d.ts +15 -0
  75. package/dist/server/services/gcp/getGcpServiceAccountFromBase64.js +9 -0
  76. package/dist/server/services/gcp/getGcpServiceAccountFromBase64.js.map +1 -0
  77. package/dist/server/services/secrets/secret.service.d.ts +32 -0
  78. package/dist/server/services/secrets/secret.service.js +220 -0
  79. package/dist/server/services/secrets/secret.service.js.map +1 -0
  80. package/dist/server/services/sendgrid/sendgrid.model.d.ts +118 -0
  81. package/dist/server/services/sendgrid/sendgrid.model.js +3 -0
  82. package/dist/server/services/sendgrid/sendgrid.model.js.map +1 -0
  83. package/dist/server/services/sendgrid/sendgridApi.service.d.ts +13 -0
  84. package/dist/server/services/sendgrid/sendgridApi.service.js +79 -0
  85. package/dist/server/services/sendgrid/sendgridApi.service.js.map +1 -0
  86. package/dist/server/services/sendgrid/sendgridHooks.model.d.ts +27 -0
  87. package/dist/server/services/sendgrid/sendgridHooks.model.js +19 -0
  88. package/dist/server/services/sendgrid/sendgridHooks.model.js.map +1 -0
  89. package/dist/server/services/translations/template.util.d.ts +7 -0
  90. package/dist/server/services/translations/template.util.js +11 -0
  91. package/dist/server/services/translations/template.util.js.map +1 -0
  92. package/dist/server/services/translations/translation.model.d.ts +4 -0
  93. package/dist/server/services/translations/translation.model.js +6 -0
  94. package/dist/server/services/translations/translation.model.js.map +1 -0
  95. package/dist/server/services/translations/translation.service.d.ts +25 -0
  96. package/dist/server/services/translations/translation.service.js +97 -0
  97. package/dist/server/services/translations/translation.service.js.map +1 -0
  98. package/dist/server/services/util/benchmarker.d.ts +13 -0
  99. package/dist/server/services/util/benchmarker.js +34 -0
  100. package/dist/server/services/util/benchmarker.js.map +1 -0
  101. package/dist/server/services/util/pagination.d.ts +50 -0
  102. package/dist/server/services/util/pagination.js +57 -0
  103. package/dist/server/services/util/pagination.js.map +1 -0
  104. package/dist/server/services/util/url.service.d.ts +75 -0
  105. package/dist/server/services/util/url.service.js +139 -0
  106. package/dist/server/services/util/url.service.js.map +1 -0
  107. package/dist/server/test/express.mock.d.ts +6 -0
  108. package/dist/server/test/express.mock.js +49 -0
  109. package/dist/server/test/express.mock.js.map +1 -0
  110. package/dist/server/test/firebase.mock.d.ts +4 -0
  111. package/dist/server/test/firebase.mock.js +30 -0
  112. package/dist/server/test/firebase.mock.js.map +1 -0
  113. package/dist/server/test/mock.model.d.ts +5 -0
  114. package/dist/server/test/mock.model.js +3 -0
  115. package/dist/server/test/mock.model.js.map +1 -0
  116. package/dist/server/test/trpc.mock.d.ts +6 -0
  117. package/dist/server/test/trpc.mock.js +14 -0
  118. package/dist/server/test/trpc.mock.js.map +1 -0
  119. package/dist/server/trpc.d.ts +364 -0
  120. package/dist/server/trpc.js +87 -0
  121. package/dist/server/trpc.js.map +1 -0
  122. package/dist/server/types/Envinronment.d.ts +1 -0
  123. package/dist/server/types/Envinronment.js +3 -0
  124. package/dist/server/types/Envinronment.js.map +1 -0
  125. package/dist/test/const.d.ts +4 -0
  126. package/dist/test/const.js +11 -1
  127. package/dist/test/const.js.map +1 -1
  128. package/dist/tsconfig.tsbuildinfo +1 -1
  129. package/package.json +34 -3
@@ -0,0 +1,895 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Container = void 0;
4
+ const async_hooks_1 = require("async_hooks");
5
+ const LruCache_1 = require("./LruCache");
6
+ /**
7
+ * Smart instantiation helper that tries constructor first, then function
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
+ }
21
+ /**
22
+ * ═══════════════════════════════════════════════════════════════════════════════
23
+ * 🏗️ MULTI-TENANT DEPENDENCY INJECTION CONTAINER
24
+ * ═══════════════════════════════════════════════════════════════════════════════
25
+ *
26
+ * This Container provides a dependency injection system designed
27
+ * for multi-tenant applications. Each tenant gets their own isolated service
28
+ * instances while sharing the same factory definitions.
29
+ *
30
+ * Key Features:
31
+ * • 🔄 Tenant-isolated service instances using AsyncLocalStorage
32
+ * • ⚡ High-performance caching with LRU eviction
33
+ * • 🪞 Intelligent proxy system for lazy loading and error handling
34
+ * • 📊 Built-in performance metrics and debugging tools
35
+ * • 🛡️ Type-safe service resolution with full TypeScript support
36
+ * • 🔧 Support for both class constructors and factory functions
37
+ *
38
+ * Architecture Overview:
39
+ * ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
40
+ * │ Factories │ -> │ Container │ -> │ Instances │
41
+ * │ (Shared Defs) │ │ (Per-tenant) │ │ (Per-tenant) │
42
+ * └─────────────────┘ └─────────────────┘ └─────────────────┘
43
+ *
44
+ * Flow:
45
+ * 1. Define factories once (shared across all tenants)
46
+ * 2. Bootstrap container with tenant metadata
47
+ * 3. Services are lazy-loaded and cached per tenant
48
+ * 4. AsyncLocalStorage provides automatic context isolation
49
+ * ═══════════════════════════════════════════════════════════════════════════════
50
+ */
51
+ // ═══════════════════════════════════════════════════════════════════════════════
52
+ // 🏛️ MAIN CONTAINER CLASS
53
+ // ═══════════════════════════════════════════════════════════════════════════════
54
+ /**
55
+ * 🏗️ Multi-Tenant Dependency Injection Container
56
+ *
57
+ * The Container is the heart of the multi-tenant service architecture. It manages
58
+ * service instantiation, caching, and tenant isolation using AsyncLocalStorage.
59
+ *
60
+ * @template Defs - Factory definitions record (shared across tenants)
61
+ * @template TenantMetadata - Type of tenant-specific metadata (DB config, secrets, etc.)
62
+ *
63
+ * Key Responsibilities:
64
+ * 1. 🏭 **Factory Management**: Register and cache service factories
65
+ * 2. 🔄 **Tenant Isolation**: Each tenant gets isolated service instances
66
+ * 3. ⚡ **Performance**: Multi-level caching for optimal performance
67
+ * 4. 🪞 **Lazy Loading**: Services instantiated only when accessed
68
+ * 5. 🛡️ **Error Handling**: Clear error messages for missing services
69
+ * 6. 📊 **Observability**: Performance metrics and debugging tools
70
+ *
71
+ * Usage Pattern:
72
+ * ```typescript
73
+ * // 1. Define your services
74
+ * const factories = {
75
+ * database: DatabaseService,
76
+ * api: {
77
+ * users: UserApiService,
78
+ * auth: AuthApiService
79
+ * }
80
+ * }
81
+ *
82
+ * // 2. Create container with initializer
83
+ * const container = new Container(factories, async (preload, meta) => {
84
+ * const db = preload.database('main', meta.connectionString)
85
+ * return {
86
+ * database: db,
87
+ * api: {
88
+ * users: preload.api.users('users', db),
89
+ * auth: preload.api.auth('auth', db, meta.jwtSecret)
90
+ * }
91
+ * }
92
+ * })
93
+ *
94
+ * // 3. Bootstrap for a tenant and run code
95
+ * await container.bootstrap(tenantMeta, async () => {
96
+ * const { database, api } = container.context
97
+ * const users = await api.users.getAll()
98
+ * return users
99
+ * })
100
+ * ```
101
+ */
102
+ class Container {
103
+ factories;
104
+ initializer;
105
+ // ═══════════════════════════════════════════════════════════════════════════
106
+ // 💾 CORE STORAGE SYSTEMS
107
+ // ═══════════════════════════════════════════════════════════════════════════
108
+ /**
109
+ * Service instance cache managers - one per service type
110
+ * Each manager handles LRU caching for that specific service
111
+ */
112
+ managers;
113
+ /**
114
+ * AsyncLocalStorage provides automatic tenant context isolation
115
+ * Each async call tree gets its own isolated service instances
116
+ * Also stores tenant metadata for introspection
117
+ */
118
+ als = new async_hooks_1.AsyncLocalStorage();
119
+ /**
120
+ * Pre-resolved factory lookup cache for performance
121
+ * Avoids recursive object traversal on every service access
122
+ */
123
+ factoryCache = new Map();
124
+ /**
125
+ * Cached preload proxy to avoid recreating the same proxy structure
126
+ */
127
+ preloadProxy = null;
128
+ /**
129
+ * Container configuration with sensible defaults
130
+ */
131
+ options;
132
+ // ═══════════════════════════════════════════════════════════════════════════
133
+ // ⚡ PERFORMANCE OPTIMIZATION CACHES
134
+ // ═══════════════════════════════════════════════════════════════════════════
135
+ /**
136
+ * Path string cache: converts ['user', 'repo'] -> "user.repo"
137
+ * Avoids repeated string joining operations
138
+ */
139
+ pathCache = new Map();
140
+ /**
141
+ * Proxy object cache: reuses proxy objects for the same paths
142
+ * Reduces memory allocation and improves performance
143
+ */
144
+ proxyCache = new Map();
145
+ /**
146
+ * Context proxy cache: reuses proxies for the same object instances
147
+ * Uses WeakMap for automatic garbage collection when objects are freed
148
+ */
149
+ contextProxyCache = new WeakMap();
150
+ /**
151
+ * Initializer cache: stores initialized instances per tenant
152
+ * Avoids re-running the expensive initializer function for the same tenant
153
+ * Key is a serialized version of tenant metadata, value is the initialized instances
154
+ */
155
+ initializerCache = new Map();
156
+ // ═══════════════════════════════════════════════════════════════════════════
157
+ // 📊 PERFORMANCE METRICS
158
+ // ═══════════════════════════════════════════════════════════════════════════
159
+ /**
160
+ * Performance metrics for monitoring and optimization
161
+ * Only collected when enableMetrics is true
162
+ * Includes overflow protection for long-running services
163
+ */
164
+ metrics = {
165
+ cacheHits: 0, // How many times we found an instance in cache
166
+ cacheMisses: 0, // How many times we had to create a new instance
167
+ instanceCreations: 0, // Total number of service instances created
168
+ contextAccesses: 0, // How many times container.context was accessed
169
+ pathCacheHits: 0, // Path string cache effectiveness
170
+ proxyCacheHits: 0, // Proxy object cache effectiveness
171
+ initializerCacheHits: 0, // How many times we reused cached initializer results
172
+ };
173
+ /**
174
+ * Maximum safe value for metrics before overflow protection kicks in
175
+ * Set to 90% of MAX_SAFE_INTEGER to provide buffer
176
+ */
177
+ MAX_METRIC_VALUE = Math.floor(Number.MAX_SAFE_INTEGER * 0.9);
178
+ /**
179
+ * Safely increment a metric with overflow protection
180
+ * When approaching MAX_SAFE_INTEGER, resets all metrics to prevent overflow
181
+ */
182
+ safeIncrementMetric(metricName) {
183
+ if (!this.options.enableMetrics)
184
+ return;
185
+ const currentValue = this.metrics[metricName];
186
+ // Check if we're approaching the overflow threshold
187
+ if (currentValue >= this.MAX_METRIC_VALUE) {
188
+ // Reset all metrics to prevent overflow
189
+ this.resetMetrics();
190
+ // Log the reset for monitoring
191
+ if (this.options.enableDiagnostics) {
192
+ console.warn(`Container metrics reset due to overflow protection. Metric '${metricName}' reached ${currentValue}`);
193
+ }
194
+ }
195
+ this.metrics[metricName]++;
196
+ }
197
+ // ═══════════════════════════════════════════════════════════════════════════
198
+ // 🏗️ CONSTRUCTOR & INITIALIZATION
199
+ // ═══════════════════════════════════════════════════════════════════════════
200
+ /**
201
+ * Create a new Container instance
202
+ *
203
+ * @param factories - Service factory definitions (shared across all tenants)
204
+ * @param initializer - Function that creates tenant-specific service instances
205
+ * @param options - Configuration options for performance and debugging
206
+ *
207
+ * The initializer function receives:
208
+ * - preload: Proxy object for creating service instances with parameters
209
+ * - meta: Tenant-specific metadata (DB config, secrets, etc.)
210
+ *
211
+ * And should return a structure matching your factory definitions but with
212
+ * actual service instances instead of factory functions.
213
+ */
214
+ constructor(factories, initializer, options = {}) {
215
+ this.factories = factories;
216
+ this.initializer = initializer;
217
+ // Apply default options
218
+ this.options = {
219
+ cacheSize: 100,
220
+ enableMetrics: false,
221
+ enableDiagnostics: false,
222
+ enableDistributedInvalidation: false,
223
+ distributedInvalidator: undefined,
224
+ ...options,
225
+ };
226
+ // Initialize cache managers for each service
227
+ this.managers = this.createManagers(this.factories, this.options.cacheSize);
228
+ // Pre-cache factory lookups for better performance
229
+ this.preloadFactoryCache();
230
+ // Setup distributed cache invalidation if enabled
231
+ this.setupDistributedInvalidation();
232
+ }
233
+ // ═══════════════════════════════════════════════════════════════════════════
234
+ // 🏭 CACHE MANAGER SETUP
235
+ // ═══════════════════════════════════════════════════════════════════════════
236
+ /**
237
+ * Recursively create cache managers for all services in the factory tree
238
+ * Each service gets its own LRU cache with the configured size limit
239
+ */
240
+ createManagers(defs, cacheSize, path = []) {
241
+ const managers = {};
242
+ for (const [key, value] of Object.entries(defs)) {
243
+ const newPath = path.length === 0 ? [key] : [...path, key];
244
+ if (typeof value === 'function') {
245
+ // This is a factory function/constructor - create a cache for it
246
+ const flatKey = this.getOrCachePath(newPath);
247
+ managers[flatKey] = (0, LruCache_1.createServiceCache)(cacheSize);
248
+ }
249
+ else if (typeof value === 'object' && value !== null) {
250
+ // This is a nested object - recurse into it
251
+ const subManagers = this.createManagers(value, cacheSize, newPath);
252
+ Object.assign(managers, subManagers);
253
+ }
254
+ }
255
+ return managers;
256
+ }
257
+ // ═══════════════════════════════════════════════════════════════════════════
258
+ // ⚡ PERFORMANCE OPTIMIZATION HELPERS
259
+ // ═══════════════════════════════════════════════════════════════════════════
260
+ /**
261
+ * Efficiently cache path string conversions
262
+ * Converts ['user', 'service'] to "user.service" and caches the result
263
+ *
264
+ * Uses different separators for cache key vs final path to avoid conflicts
265
+ */
266
+ getOrCachePath(path) {
267
+ // Convert symbols to strings
268
+ const stringPath = path.map(p => typeof p === 'symbol' ? p.toString() : p);
269
+ const pathKey = stringPath.join('|'); // Cache key uses pipe separator
270
+ let cached = this.pathCache.get(pathKey);
271
+ if (!cached) {
272
+ cached = stringPath.join('.'); // Final path uses dot separator
273
+ this.pathCache.set(pathKey, cached);
274
+ }
275
+ else {
276
+ this.safeIncrementMetric('pathCacheHits');
277
+ }
278
+ return cached;
279
+ }
280
+ /**
281
+ * Pre-populate the factory cache by walking the entire factory tree
282
+ * This eliminates the need for recursive object traversal during runtime
283
+ */
284
+ preloadFactoryCache() {
285
+ this.walkFactories(this.factories, []);
286
+ }
287
+ /**
288
+ * Recursive factory tree walker that builds the flat factory cache
289
+ * Converts nested object structure to flat dot-notation keys
290
+ */
291
+ walkFactories(obj, path) {
292
+ for (const [key, value] of Object.entries(obj)) {
293
+ const newPath = path.length === 0 ? [key] : [...path, key];
294
+ if (typeof value === 'function') {
295
+ // Found a factory - cache it with its full path
296
+ const flatKey = this.getOrCachePath(newPath);
297
+ this.factoryCache.set(flatKey, value);
298
+ }
299
+ else if (typeof value === 'object' && value !== null) {
300
+ // Found a nested object - recurse deeper
301
+ this.walkFactories(value, newPath);
302
+ }
303
+ }
304
+ }
305
+ // ═══════════════════════════════════════════════════════════════════════════
306
+ // 🪞 PRELOAD PROXY SYSTEM
307
+ // ═══════════════════════════════════════════════════════════════════════════
308
+ /**
309
+ * Get the preload proxy for service instantiation
310
+ * The preload proxy allows you to create services with parameters:
311
+ *
312
+ * ```typescript
313
+ * const db = preload.database('main', connectionString)
314
+ * const userApi = preload.api.users('users', db, config)
315
+ * ```
316
+ *
317
+ * This is used during the initialization phase to wire up dependencies
318
+ */
319
+ get preload() {
320
+ // Cache the preload proxy since it's expensive to create and never changes
321
+ if (!this.preloadProxy) {
322
+ this.preloadProxy = this.createPreloadProxy();
323
+ }
324
+ return this.preloadProxy;
325
+ }
326
+ /**
327
+ * Create a proxy that intercepts property access and provides factory functions
328
+ *
329
+ * The proxy works by:
330
+ * 1. Intercepting property access (e.g., preload.database)
331
+ * 2. Looking up the factory for that path
332
+ * 3. Returning a function that creates and caches instances
333
+ * 4. For nested paths, returning another proxy
334
+ *
335
+ * This enables natural dot-notation access while maintaining lazy loading
336
+ */
337
+ createPreloadProxy(path = []) {
338
+ const pathKey = path.join('|');
339
+ // Check cache first for performance
340
+ if (this.proxyCache.has(pathKey)) {
341
+ this.safeIncrementMetric('proxyCacheHits');
342
+ return this.proxyCache.get(pathKey);
343
+ }
344
+ const proxy = new Proxy({}, // Empty target object - all access is intercepted
345
+ {
346
+ get: (_, prop) => {
347
+ const newPath = path.length === 0 ? [prop] : [...path, prop];
348
+ const flatKey = this.getOrCachePath(newPath);
349
+ const factory = this.factoryCache.get(flatKey);
350
+ if (factory) {
351
+ // Found a factory - return a function that creates/caches instances
352
+ return (id, ...params) => {
353
+ const mgr = this.managers[flatKey];
354
+ let inst = mgr.get(id);
355
+ if (!inst) {
356
+ // Cache miss - create new instance
357
+ this.safeIncrementMetric('cacheMisses');
358
+ this.safeIncrementMetric('instanceCreations');
359
+ inst = instantiate(factory, params);
360
+ mgr.set(id, inst);
361
+ }
362
+ else {
363
+ // Cache hit - reusing existing instance
364
+ this.safeIncrementMetric('cacheHits');
365
+ }
366
+ return inst;
367
+ };
368
+ }
369
+ else {
370
+ // No factory found - must be a nested path, return another proxy
371
+ return this.createPreloadProxy(newPath);
372
+ }
373
+ },
374
+ });
375
+ // Cache the proxy for reuse
376
+ this.proxyCache.set(pathKey, proxy);
377
+ return proxy;
378
+ }
379
+ // ═══════════════════════════════════════════════════════════════════════════
380
+ // 🔄 TENANT CONTEXT MANAGEMENT
381
+ // ═══════════════════════════════════════════════════════════════════════════
382
+ /**
383
+ * Run a function within a specific tenant context
384
+ * This is usually called internally by bootstrap, but can be used directly
385
+ * for testing or advanced use cases
386
+ */
387
+ async runWithContext(instances, tenantMetadata, fn) {
388
+ return await this.als.run({ instances, tenantMetadata }, fn);
389
+ }
390
+ /**
391
+ * Get the current tenant's service context
392
+ *
393
+ * This is the main way to access services within a tenant context:
394
+ * ```typescript
395
+ * const { database, api } = container.context
396
+ * const users = await api.users.getAll()
397
+ * ```
398
+ *
399
+ * Throws an error if called outside of a tenant context
400
+ */
401
+ get context() {
402
+ const store = this.als.getStore();
403
+ if (!store) {
404
+ throw new Error("No tenant context available. Make sure you're running within a container context.");
405
+ }
406
+ this.safeIncrementMetric('contextAccesses');
407
+ return this.createContextProxy(store.instances);
408
+ }
409
+ /**
410
+ * Create a proxy for the runtime context that provides intelligent error handling
411
+ *
412
+ * The context proxy:
413
+ * 1. Provides helpful error messages when services are missing
414
+ * 2. Handles nested object access gracefully
415
+ * 3. Avoids wrapping Promises or arrays (which would break them)
416
+ * 4. Uses WeakMap caching for automatic memory management
417
+ *
418
+ * This ensures that accessing container.context.someService either works
419
+ * or gives you a clear error message about what's available
420
+ */
421
+ createContextProxy(obj, path = []) {
422
+ // Use WeakMap cache to avoid memory leaks
423
+ if (this.contextProxyCache.has(obj)) {
424
+ return this.contextProxyCache.get(obj);
425
+ }
426
+ const proxy = new Proxy(obj, {
427
+ get: (target, prop) => {
428
+ const newPath = path.length === 0 ? [prop] : [...path, prop];
429
+ const value = target[prop];
430
+ if (value === undefined) {
431
+ // Check if property exists but is undefined (vs completely missing)
432
+ if (prop in target) {
433
+ // Property exists but is undefined - this is valid (e.g., optional services)
434
+ return undefined;
435
+ }
436
+ // For symbols, especially well-known symbols like Symbol.iterator,
437
+ // just return undefined instead of throwing an error
438
+ if (typeof prop === 'symbol') {
439
+ return undefined;
440
+ }
441
+ // Property doesn't exist - provide helpful error message
442
+ const servicePath = this.getOrCachePath(newPath);
443
+ const available = Object.keys(target).join(', ');
444
+ throw new Error(`Service '${servicePath}' not initialized. ` +
445
+ `Available services: ${available}`);
446
+ }
447
+ // Only wrap objects that are safe to wrap
448
+ // Avoid wrapping Promises, arrays, thenable objects, Prisma clients, or Redis clients
449
+ if (typeof value === 'object' &&
450
+ value !== null &&
451
+ !Array.isArray(value) &&
452
+ !(value instanceof Promise) &&
453
+ typeof value.then !== 'function' &&
454
+ // Avoid wrapping Prisma clients (they have complex internal properties)
455
+ !('_engine' in value &&
456
+ '_extensions' in value &&
457
+ '$connect' in value) &&
458
+ // Avoid wrapping Redis clients (they have ioredis-specific properties)
459
+ !('options' in value && 'status' in value && 'connector' in value) &&
460
+ // Avoid wrapping cache service objects (Keyv instances with opts property)
461
+ !('opts' in value) &&
462
+ // Avoid wrapping cache store objects
463
+ !('store' in value && 'namespace' in value)) {
464
+ // Safe to wrap - create nested proxy
465
+ return this.createContextProxy(value, newPath);
466
+ }
467
+ // Return value as-is (primitives, functions, Promises, arrays)
468
+ return value;
469
+ },
470
+ });
471
+ // Cache using WeakMap for automatic garbage collection
472
+ this.contextProxyCache.set(obj, proxy);
473
+ return proxy;
474
+ }
475
+ // ═══════════════════════════════════════════════════════════════════════════
476
+ // 🚀 BOOTSTRAP & LIFECYCLE
477
+ // ═══════════════════════════════════════════════════════════════════════════
478
+ /**
479
+ * Create a stable cache key from tenant metadata
480
+ * Uses a deterministic approach focusing on tenant identity
481
+ */
482
+ createTenantCacheKey(meta) {
483
+ // Try to extract a tenant ID from common properties
484
+ const metaObj = meta;
485
+ const tenantId = metaObj.id || metaObj.tenantId || metaObj.name || 'unknown';
486
+ // 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
+ try {
489
+ const sortedMeta = JSON.stringify(meta, Object.keys(meta).sort());
490
+ // Use a simple hash to keep cache keys manageable
491
+ const hash = this.simpleHash(sortedMeta);
492
+ return `tenant:${tenantId}:${hash}`;
493
+ }
494
+ catch {
495
+ // Fallback if JSON.stringify fails (circular refs, etc.)
496
+ return `tenant:${tenantId}:${Date.now()}`;
497
+ }
498
+ }
499
+ /**
500
+ * Simple hash function for cache keys
501
+ * Not cryptographically secure, but good enough for cache key generation
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
517
+ */
518
+ async getOrCreateInstances(meta) {
519
+ const cacheKey = this.createTenantCacheKey(meta);
520
+ // Check if we already have initialized instances for this tenant
521
+ const cachedInstances = this.initializerCache.get(cacheKey);
522
+ if (cachedInstances) {
523
+ this.safeIncrementMetric('initializerCacheHits');
524
+ return cachedInstances;
525
+ }
526
+ // No cached instances - run the initializer
527
+ const instances = await this.initializer(this.preload, meta);
528
+ // Cache the result for future use
529
+ this.initializerCache.set(cacheKey, instances);
530
+ return instances;
531
+ }
532
+ /**
533
+ * Bootstrap the container for a specific tenant and execute a function
534
+ *
535
+ * This is the main entry point for tenant-specific operations:
536
+ *
537
+ * @param meta - Tenant-specific metadata (DB config, secrets, etc.)
538
+ * @param fn - Function to execute within the tenant context (optional)
539
+ * @returns Object containing the initialized instances and function result
540
+ *
541
+ * ```typescript
542
+ * // Example: Process a user request for tenant "acme"
543
+ * const result = await container.bootstrap(acmeTenantMeta, async () => {
544
+ * const { api } = container.context
545
+ * return await api.users.getById(userId)
546
+ * })
547
+ *
548
+ * console.log(result.instances) // All initialized services
549
+ * console.log(result.result) // Return value from the function
550
+ * ```
551
+ *
552
+ * The bootstrap process:
553
+ * 1. Gets or creates initialized services for this tenant (with caching)
554
+ * 2. Sets up AsyncLocalStorage context with the service instances
555
+ * 3. Executes your function within that context
556
+ * 4. Returns both the instances and your function's result
557
+ */
558
+ async bootstrap(meta, fn) {
559
+ try {
560
+ // Phase 1: Get or create services for this tenant (with caching)
561
+ const instances = await this.getOrCreateInstances(meta);
562
+ // Phase 2: Run user function within tenant context
563
+ const result = await this.runWithContext(instances, meta, fn || (async () => undefined));
564
+ // Type assertion: we trust that initializer provides all required services
565
+ // In practice, this is validated at runtime by the context proxy
566
+ return { instances: instances, result };
567
+ }
568
+ catch (err) {
569
+ if (this.options.enableDiagnostics) {
570
+ console.error('Container bootstrap failed:', err);
571
+ }
572
+ throw err;
573
+ }
574
+ }
575
+ // ═══════════════════════════════════════════════════════════════════════════
576
+ // 📊 OBSERVABILITY & DEBUGGING
577
+ // ═══════════════════════════════════════════════════════════════════════════
578
+ /**
579
+ * Get current performance metrics
580
+ * Useful for monitoring cache effectiveness and performance tuning
581
+ */
582
+ getMetrics() {
583
+ return { ...this.metrics };
584
+ }
585
+ /**
586
+ * Reset all performance metrics to zero
587
+ * Useful for benchmarking specific operations
588
+ */
589
+ resetMetrics() {
590
+ this.metrics = {
591
+ cacheHits: 0,
592
+ cacheMisses: 0,
593
+ instanceCreations: 0,
594
+ contextAccesses: 0,
595
+ pathCacheHits: 0,
596
+ proxyCacheHits: 0,
597
+ initializerCacheHits: 0,
598
+ };
599
+ }
600
+ /**
601
+ * Clear all service instance caches
602
+ * Forces fresh instantiation of all services on next access
603
+ * Useful for testing or when you need to ensure clean state
604
+ */
605
+ clearCaches() {
606
+ for (const manager of Object.values(this.managers)) {
607
+ manager.clear?.();
608
+ }
609
+ // Clear optimization caches as well
610
+ this.pathCache.clear();
611
+ this.proxyCache.clear();
612
+ this.initializerCache.clear();
613
+ // Note: contextProxyCache is a WeakMap and will be garbage collected automatically
614
+ }
615
+ // ═══════════════════════════════════════════════════════════════════════════
616
+ // 🌐 DISTRIBUTED CACHE INVALIDATION
617
+ // ═══════════════════════════════════════════════════════════════════════════
618
+ /**
619
+ * Setup distributed cache invalidation system
620
+ * Connects to Redis pub/sub for coordinating cache invalidation across instances
621
+ */
622
+ setupDistributedInvalidation() {
623
+ if (!this.options.enableDistributedInvalidation ||
624
+ !this.options.distributedInvalidator) {
625
+ return;
626
+ }
627
+ const invalidator = this.options.distributedInvalidator;
628
+ // Listen for invalidation events from other instances
629
+ invalidator.on('invalidate-tenant', (tenantId, reason) => {
630
+ this.invalidateTenantLocally(tenantId, reason);
631
+ });
632
+ invalidator.on('invalidate-service', (serviceType, reason) => {
633
+ this.invalidateServiceLocally(serviceType, reason);
634
+ });
635
+ invalidator.on('invalidate-all', (reason) => {
636
+ this.invalidateAllLocally(reason);
637
+ });
638
+ // Handle Redis connection issues
639
+ invalidator.on('redis-error', (error) => {
640
+ if (this.options.enableDiagnostics) {
641
+ console.warn('Distributed cache invalidation Redis error:', error);
642
+ }
643
+ });
644
+ }
645
+ /**
646
+ * Invalidate all cached data for a specific tenant across all instances
647
+ * This sends a distributed invalidation message via Redis pub/sub
648
+ */
649
+ async invalidateTenantDistributed(tenantId, reason) {
650
+ // First invalidate locally
651
+ this.invalidateTenantLocally(tenantId, reason);
652
+ // Then invalidate on other instances
653
+ if (this.options.enableDistributedInvalidation &&
654
+ this.options.distributedInvalidator) {
655
+ await this.options.distributedInvalidator.invalidateTenant(tenantId, reason);
656
+ }
657
+ }
658
+ /**
659
+ * Invalidate all cached data for a specific service type across all instances
660
+ */
661
+ async invalidateServiceDistributed(serviceType, reason) {
662
+ // First invalidate locally
663
+ this.invalidateServiceLocally(serviceType, reason);
664
+ // Then invalidate on other instances
665
+ if (this.options.enableDistributedInvalidation &&
666
+ this.options.distributedInvalidator) {
667
+ await this.options.distributedInvalidator.invalidateService(serviceType, reason);
668
+ }
669
+ }
670
+ /**
671
+ * Invalidate all cached data across all instances
672
+ */
673
+ async invalidateAllDistributed(reason) {
674
+ // First invalidate locally
675
+ this.invalidateAllLocally(reason);
676
+ // Then invalidate on other instances
677
+ if (this.options.enableDistributedInvalidation &&
678
+ this.options.distributedInvalidator) {
679
+ await this.options.distributedInvalidator.invalidateAll(reason);
680
+ }
681
+ }
682
+ /**
683
+ * Invalidate cached data for a specific tenant (local only)
684
+ * This only affects the current instance
685
+ */
686
+ invalidateTenantLocally(tenantId, reason) {
687
+ if (this.options.enableDiagnostics) {
688
+ console.log(`Invalidating tenant cache locally: ${tenantId}`, reason ? `(${reason})` : '');
689
+ }
690
+ // Clear service instance caches for this tenant
691
+ for (const manager of Object.values(this.managers)) {
692
+ manager.delete(tenantId);
693
+ }
694
+ // Clear initializer cache for this tenant
695
+ for (const [cacheKey] of this.initializerCache.entries()) {
696
+ if (cacheKey.includes(tenantId)) {
697
+ this.initializerCache.delete(cacheKey);
698
+ }
699
+ }
700
+ }
701
+ /**
702
+ * Invalidate cached data for a specific service type (local only)
703
+ */
704
+ invalidateServiceLocally(serviceType, reason) {
705
+ if (this.options.enableDiagnostics) {
706
+ console.log(`Invalidating service cache locally: ${serviceType}`, reason ? `(${reason})` : '');
707
+ }
708
+ // Clear service instance cache for this service type
709
+ const manager = this.managers[serviceType];
710
+ if (manager) {
711
+ manager.clear();
712
+ }
713
+ }
714
+ /**
715
+ * Invalidate all cached data (local only)
716
+ */
717
+ invalidateAllLocally(reason) {
718
+ if (this.options.enableDiagnostics) {
719
+ console.log('Invalidating all caches locally', reason ? `(${reason})` : '');
720
+ }
721
+ this.clearCaches();
722
+ }
723
+ /**
724
+ * Get detailed cache statistics for each service
725
+ * Shows how many instances are cached and the cache limits
726
+ */
727
+ getCacheStats() {
728
+ const stats = {};
729
+ for (const [key, manager] of Object.entries(this.managers)) {
730
+ const managerAny = manager;
731
+ const size = typeof managerAny.size === 'function'
732
+ ? managerAny.size()
733
+ : managerAny.size || 0;
734
+ stats[key] = {
735
+ size,
736
+ maxSize: this.options.cacheSize,
737
+ };
738
+ }
739
+ return stats;
740
+ }
741
+ /**
742
+ * Get comprehensive performance statistics
743
+ * Combines metrics, cache stats, and computed ratios for full observability
744
+ */
745
+ getPerformanceStats() {
746
+ const cacheStats = this.getCacheStats();
747
+ const totalCacheSize = Object.values(cacheStats).reduce((sum, stat) => sum + stat.size, 0);
748
+ return {
749
+ ...this.getMetrics(),
750
+ cacheStats,
751
+ totalCacheSize,
752
+ pathCacheSize: this.pathCache.size,
753
+ proxyCacheSize: this.proxyCache.size,
754
+ factoryCacheSize: this.factoryCache.size,
755
+ initializerCacheSize: this.initializerCache.size,
756
+ cacheHitRatio: this.metrics.cacheHits + this.metrics.cacheMisses > 0
757
+ ? this.metrics.cacheHits /
758
+ (this.metrics.cacheHits + this.metrics.cacheMisses)
759
+ : 0,
760
+ };
761
+ }
762
+ // ═══════════════════════════════════════════════════════════════════════════
763
+ // 🔍 RUNTIME INTROSPECTION
764
+ // ═══════════════════════════════════════════════════════════════════════════
765
+ /**
766
+ * Check if there's an active tenant context
767
+ *
768
+ * @returns true if called within a tenant context, false otherwise
769
+ *
770
+ * ```typescript
771
+ * if (container.hasActiveContext()) {
772
+ * const services = container.context
773
+ * // Safe to access services
774
+ * }
775
+ * ```
776
+ */
777
+ hasActiveContext() {
778
+ return this.als.getStore() !== undefined;
779
+ }
780
+ /**
781
+ * Check if a service is available in the current tenant context
782
+ *
783
+ * @param servicePath - Dot-notation path to the service (e.g., "api.users")
784
+ * @returns true if the service exists and is initialized, false otherwise
785
+ *
786
+ * ```typescript
787
+ * if (container.hasService('api.users')) {
788
+ * const users = container.context.api.users
789
+ * // Safe to use users service
790
+ * }
791
+ * ```
792
+ */
793
+ hasService(servicePath) {
794
+ const store = this.als.getStore();
795
+ if (!store)
796
+ return false;
797
+ const parts = servicePath.split('.');
798
+ let current = store.instances;
799
+ for (const part of parts) {
800
+ if (typeof current !== 'object' ||
801
+ current === null ||
802
+ !(part in current)) {
803
+ return false;
804
+ }
805
+ current = current[part];
806
+ }
807
+ return current !== undefined;
808
+ }
809
+ /**
810
+ * Get the current tenant's metadata
811
+ *
812
+ * This allows access to tenant-specific configuration, credentials, and other
813
+ * metadata that was passed to the bootstrap method:
814
+ *
815
+ * ```typescript
816
+ * await container.bootstrap(tenantMeta, async () => {
817
+ * const meta = container.getCurrentTenantMetadata()
818
+ * console.log('Current tenant:', meta.id)
819
+ * console.log('DB URL:', meta.connectionString)
820
+ * })
821
+ * ```
822
+ *
823
+ * @returns The tenant metadata that was passed to bootstrap
824
+ * @throws Error if called outside of a tenant context
825
+ */
826
+ getCurrentTenantMetadata() {
827
+ const store = this.als.getStore();
828
+ if (!store) {
829
+ throw new Error("No tenant context available. Make sure you're running within a container context.");
830
+ }
831
+ return store.tenantMetadata;
832
+ }
833
+ /**
834
+ * Get the current tenant ID from metadata
835
+ *
836
+ * This is a convenience method that extracts the tenant ID from the metadata.
837
+ * It assumes the metadata has an 'id' property (common pattern).
838
+ *
839
+ * ```typescript
840
+ * await container.bootstrap(tenantMeta, async () => {
841
+ * const tenantId = container.getCurrentTenantId()
842
+ * console.log('Processing request for tenant:', tenantId)
843
+ * })
844
+ * ```
845
+ *
846
+ * @returns The tenant ID if metadata has an 'id' property, undefined otherwise
847
+ * @throws Error if called outside of a tenant context
848
+ */
849
+ getCurrentTenantId() {
850
+ const metadata = this.getCurrentTenantMetadata();
851
+ // Check if metadata has an 'id' property (common pattern)
852
+ if (metadata && typeof metadata === 'object' && 'id' in metadata) {
853
+ const id = metadata.id;
854
+ return typeof id === 'string' ? id : String(id);
855
+ }
856
+ return undefined;
857
+ }
858
+ /**
859
+ * Get a list of all available services in the current tenant context
860
+ * Useful for debugging, testing, or dynamic service discovery
861
+ *
862
+ * @returns Array of dot-notation service paths (e.g., ["database", "api.users", "api.auth"])
863
+ */
864
+ getAvailableServices() {
865
+ const store = this.als.getStore();
866
+ if (!store)
867
+ return [];
868
+ const services = [];
869
+ this.collectServices(store.instances, services);
870
+ return services;
871
+ }
872
+ /**
873
+ * Recursively collect all service paths from the current context
874
+ * Helper method for getAvailableServices()
875
+ */
876
+ collectServices(obj, services, path = []) {
877
+ for (const [key, value] of Object.entries(obj)) {
878
+ if (value === undefined)
879
+ continue; // Skip undefined services (partial structure)
880
+ const currentPath = [...path, key];
881
+ if (typeof value === 'object' &&
882
+ value !== null &&
883
+ !Array.isArray(value)) {
884
+ // Nested object - recurse deeper
885
+ this.collectServices(value, services, currentPath);
886
+ }
887
+ else {
888
+ // Leaf service - add to list
889
+ services.push(currentPath.join('.'));
890
+ }
891
+ }
892
+ }
893
+ }
894
+ exports.Container = Container;
895
+ //# sourceMappingURL=Container.js.map