@goatlab/node-backend 0.0.15 → 0.1.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 +54 -38
- package/dist/container/Container.d.ts +441 -0
- package/dist/container/Container.js +895 -0
- package/dist/container/Container.js.map +1 -0
- package/dist/container/DistributedCacheInvalidator.d.ts +84 -0
- package/dist/container/DistributedCacheInvalidator.js +213 -0
- package/dist/container/DistributedCacheInvalidator.js.map +1 -0
- package/dist/container/LruCache.d.ts +14 -0
- package/dist/container/LruCache.js +23 -0
- package/dist/container/LruCache.js.map +1 -0
- package/dist/container/types.d.ts +128 -0
- package/dist/container/types.js +6 -0
- package/dist/container/types.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +53 -1
- package/dist/index.js.map +1 -1
- package/dist/server/bootstraps/getExpressTrpcApp.d.ts +17 -0
- package/dist/server/bootstraps/getExpressTrpcApp.js +98 -0
- package/dist/server/bootstraps/getExpressTrpcApp.js.map +1 -0
- package/dist/server/consts.d.ts +35 -0
- package/dist/server/consts.js +33 -0
- package/dist/server/consts.js.map +1 -0
- package/dist/server/context/context.model.d.ts +13 -0
- package/dist/server/context/context.model.js +3 -0
- package/dist/server/context/context.model.js.map +1 -0
- package/dist/server/context/request.context.d.ts +40 -0
- package/dist/server/context/request.context.js +65 -0
- package/dist/server/context/request.context.js.map +1 -0
- package/dist/server/context/trpc.context.d.ts +11 -0
- package/dist/server/context/trpc.context.js +67 -0
- package/dist/server/context/trpc.context.js.map +1 -0
- package/dist/server/initOpenApiDocs.d.ts +9 -0
- package/dist/server/initOpenApiDocs.js +18 -0
- package/dist/server/initOpenApiDocs.js.map +1 -0
- package/dist/server/middleware/cloudTaskDecrypt.middleware.d.ts +6 -0
- package/dist/server/middleware/cloudTaskDecrypt.middleware.js +44 -0
- package/dist/server/middleware/cloudTaskDecrypt.middleware.js.map +1 -0
- package/dist/server/middleware/error.middleware.d.ts +17 -0
- package/dist/server/middleware/error.middleware.js +66 -0
- package/dist/server/middleware/error.middleware.js.map +1 -0
- package/dist/server/middleware/handleRequest.middleware.d.ts +7 -0
- package/dist/server/middleware/handleRequest.middleware.js +40 -0
- package/dist/server/middleware/handleRequest.middleware.js.map +1 -0
- package/dist/server/middleware/logger/cloudRun.logger.d.ts +27 -0
- package/dist/server/middleware/logger/cloudRun.logger.js +87 -0
- package/dist/server/middleware/logger/cloudRun.logger.js.map +1 -0
- package/dist/server/middleware/logger/logger.service.d.ts +6 -0
- package/dist/server/middleware/logger/logger.service.js +17 -0
- package/dist/server/middleware/logger/logger.service.js.map +1 -0
- package/dist/server/middleware/logs.middleware.d.ts +7 -0
- package/dist/server/middleware/logs.middleware.js +130 -0
- package/dist/server/middleware/logs.middleware.js.map +1 -0
- package/dist/server/middleware/requireAuthenticated.d.ts +2 -0
- package/dist/server/middleware/requireAuthenticated.js +13 -0
- package/dist/server/middleware/requireAuthenticated.js.map +1 -0
- package/dist/server/middleware/trpcError.middleware.d.ts +4 -0
- package/dist/server/middleware/trpcError.middleware.js +38 -0
- package/dist/server/middleware/trpcError.middleware.js.map +1 -0
- package/dist/server/schemas/user.schema.d.ts +109 -0
- package/dist/server/schemas/user.schema.js +28 -0
- package/dist/server/schemas/user.schema.js.map +1 -0
- package/dist/server/sentry/getSentry.d.ts +6 -0
- package/dist/server/sentry/getSentry.js +45 -0
- package/dist/server/sentry/getSentry.js.map +1 -0
- package/dist/server/sentry/sentry.service.d.ts +34 -0
- package/dist/server/sentry/sentry.service.js +110 -0
- package/dist/server/sentry/sentry.service.js.map +1 -0
- package/dist/server/services/email/email.model.d.ts +84 -0
- package/dist/server/services/email/email.model.js +62 -0
- package/dist/server/services/email/email.model.js.map +1 -0
- package/dist/server/services/email/email.service.d.ts +23 -0
- package/dist/server/services/email/email.service.js +139 -0
- package/dist/server/services/email/email.service.js.map +1 -0
- package/dist/server/services/gcp/getGcpServiceAccountFromBase64.d.ts +15 -0
- package/dist/server/services/gcp/getGcpServiceAccountFromBase64.js +9 -0
- package/dist/server/services/gcp/getGcpServiceAccountFromBase64.js.map +1 -0
- package/dist/server/services/secrets/secret.service.d.ts +31 -0
- package/dist/server/services/secrets/secret.service.js +172 -0
- package/dist/server/services/secrets/secret.service.js.map +1 -0
- package/dist/server/services/sendgrid/sendgrid.model.d.ts +118 -0
- package/dist/server/services/sendgrid/sendgrid.model.js +3 -0
- package/dist/server/services/sendgrid/sendgrid.model.js.map +1 -0
- package/dist/server/services/sendgrid/sendgridApi.service.d.ts +13 -0
- package/dist/server/services/sendgrid/sendgridApi.service.js +79 -0
- package/dist/server/services/sendgrid/sendgridApi.service.js.map +1 -0
- package/dist/server/services/sendgrid/sendgridHooks.model.d.ts +27 -0
- package/dist/server/services/sendgrid/sendgridHooks.model.js +19 -0
- package/dist/server/services/sendgrid/sendgridHooks.model.js.map +1 -0
- package/dist/server/services/translations/template.util.d.ts +7 -0
- package/dist/server/services/translations/template.util.js +11 -0
- package/dist/server/services/translations/template.util.js.map +1 -0
- package/dist/server/services/translations/translation.model.d.ts +4 -0
- package/dist/server/services/translations/translation.model.js +6 -0
- package/dist/server/services/translations/translation.model.js.map +1 -0
- package/dist/server/services/translations/translation.service.d.ts +25 -0
- package/dist/server/services/translations/translation.service.js +97 -0
- package/dist/server/services/translations/translation.service.js.map +1 -0
- package/dist/server/services/util/benchmarker.d.ts +13 -0
- package/dist/server/services/util/benchmarker.js +34 -0
- package/dist/server/services/util/benchmarker.js.map +1 -0
- package/dist/server/services/util/pagination.d.ts +50 -0
- package/dist/server/services/util/pagination.js +57 -0
- package/dist/server/services/util/pagination.js.map +1 -0
- package/dist/server/services/util/url.service.d.ts +75 -0
- package/dist/server/services/util/url.service.js +139 -0
- package/dist/server/services/util/url.service.js.map +1 -0
- package/dist/server/test/express.mock.d.ts +6 -0
- package/dist/server/test/express.mock.js +49 -0
- package/dist/server/test/express.mock.js.map +1 -0
- package/dist/server/test/firebase.mock.d.ts +4 -0
- package/dist/server/test/firebase.mock.js +30 -0
- package/dist/server/test/firebase.mock.js.map +1 -0
- package/dist/server/test/mock.model.d.ts +5 -0
- package/dist/server/test/mock.model.js +3 -0
- package/dist/server/test/mock.model.js.map +1 -0
- package/dist/server/test/trpc.mock.d.ts +6 -0
- package/dist/server/test/trpc.mock.js +14 -0
- package/dist/server/test/trpc.mock.js.map +1 -0
- package/dist/server/trpc.d.ts +364 -0
- package/dist/server/trpc.js +87 -0
- package/dist/server/trpc.js.map +1 -0
- package/dist/server/types/Envinronment.d.ts +1 -0
- package/dist/server/types/Envinronment.js +3 -0
- package/dist/server/types/Envinronment.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +33 -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
|