@getstrata/bootstrap 2.1.0 → 2.2.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.
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/dist/bootstrap/app.d.ts +7 -1
- package/dist/bootstrap/config.d.ts +1 -1
- package/dist/bootstrap/context.d.ts +8 -2
- package/dist/bootstrap/dependencies.d.ts +1 -1
- package/dist/bootstrap/discoverJobs.d.ts +11 -3
- package/dist/bootstrap/discoverListeners.d.ts +7 -3
- package/dist/bootstrap/discoverModels.d.ts +8 -0
- package/dist/bootstrap/discoverModules.d.ts +4 -1
- package/dist/bootstrap/httpKernel.d.ts +11 -2
- package/dist/bootstrap/inProcessCron.d.ts +2 -1
- package/dist/bootstrap/infrastructureDiscovery.d.ts +9 -0
- package/dist/bootstrap/modules.d.ts +1 -1
- package/dist/bootstrap/providers/tracing.d.ts +3 -0
- package/dist/bootstrap/public-api.d.ts +9 -4
- package/dist/bootstrap/web/index.d.ts +1 -1
- package/dist/bootstrap/web/server.d.ts +15 -7
- package/dist/entries/buildModuleRoutes.js +218 -71
- package/dist/entries/buildWebModuleRoutes.js +218 -71
- package/dist/entries/cache/modelCacheTags.js +157 -51
- package/dist/entries/config.js +8 -0
- package/dist/entries/context.js +427 -188
- package/dist/entries/createWebRoutes.js +220 -73
- package/dist/entries/dependencies.js +429 -190
- package/dist/entries/discoverJobs.js +132 -50
- package/dist/entries/discoverListeners.js +85 -37
- package/dist/entries/discoverModels.js +39 -0
- package/dist/entries/discoverModules.js +158 -51
- package/dist/entries/health.js +4 -0
- package/dist/entries/httpKernel.js +58 -17
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +157 -51
- package/dist/entries/providers.js +308 -164
- package/dist/entries/secretsGuard.js +2 -2
- package/dist/entries/web/routing.js +58 -17
- package/dist/entries/web/server.js +71 -14
- package/dist/index.js +600 -222
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/bootstrap/public-api.ts
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
appSchedule,
|
|
5
|
+
RedisSchedulerLeaseStore,
|
|
6
|
+
runDueScheduledTasks,
|
|
7
|
+
Schedule,
|
|
8
|
+
SchedulerLeaseLostError
|
|
9
|
+
} from "@getstrata/core/scheduler/schedule";
|
|
4
10
|
|
|
5
11
|
// ../../src/bootstrap/applicationRegistry.ts
|
|
6
12
|
import {
|
|
@@ -65,8 +71,12 @@ import {
|
|
|
65
71
|
CORE_CACHE_TOKEN,
|
|
66
72
|
CORE_CONFIG_TOKEN,
|
|
67
73
|
CORE_EVENT_BUS_TOKEN,
|
|
74
|
+
CORE_HTTP_CLEANUP_TOKEN,
|
|
68
75
|
CORE_POLICY_GATE_TOKEN,
|
|
76
|
+
CORE_PUBLIC_TENANCY_TOKEN,
|
|
69
77
|
CORE_QUEUE_TOKEN,
|
|
78
|
+
CORE_TENANT_RESOLVER_TOKEN,
|
|
79
|
+
CORE_THROTTLE_QUOTA_POLICY_TOKEN,
|
|
70
80
|
CORE_TOKEN_SERVICE_TOKEN
|
|
71
81
|
} from "@getstrata/core/contracts/serviceTokens";
|
|
72
82
|
var APP_PORT_CONFIG_KEY = "app.port";
|
|
@@ -131,8 +141,39 @@ function resolveRegisterRateLimit() {
|
|
|
131
141
|
// ../../src/bootstrap/httpKernel.ts
|
|
132
142
|
class HttpKernel {
|
|
133
143
|
dependencies;
|
|
144
|
+
ownedThrottles = new Set;
|
|
145
|
+
disposed = false;
|
|
134
146
|
constructor(dependencies) {
|
|
135
147
|
this.dependencies = dependencies;
|
|
148
|
+
if (dependencies.container.has(CORE_HTTP_CLEANUP_TOKEN)) {
|
|
149
|
+
dependencies.container.resolve(CORE_HTTP_CLEANUP_TOKEN)(() => this.dispose());
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
dispose() {
|
|
153
|
+
this.disposed = true;
|
|
154
|
+
const owned = [...this.ownedThrottles];
|
|
155
|
+
this.ownedThrottles.clear();
|
|
156
|
+
const errors = [];
|
|
157
|
+
for (const throttle of owned.reverse()) {
|
|
158
|
+
try {
|
|
159
|
+
throttle.dispose();
|
|
160
|
+
} catch (error) {
|
|
161
|
+
errors.push(error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (errors.length)
|
|
165
|
+
throw new AggregateError(errors, "HTTP throttle cleanup failed.");
|
|
166
|
+
}
|
|
167
|
+
ownThrottle(throttle) {
|
|
168
|
+
if (this.disposed) {
|
|
169
|
+
if ("dispose" in throttle && typeof throttle.dispose === "function")
|
|
170
|
+
throttle.dispose();
|
|
171
|
+
throw new Error("HTTP kernel is disposed.");
|
|
172
|
+
}
|
|
173
|
+
if ("dispose" in throttle && typeof throttle.dispose === "function") {
|
|
174
|
+
this.ownedThrottles.add(throttle);
|
|
175
|
+
}
|
|
176
|
+
return throttle;
|
|
136
177
|
}
|
|
137
178
|
globalMiddleware(errorFormat = "json") {
|
|
138
179
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
@@ -147,7 +188,10 @@ class HttpKernel {
|
|
|
147
188
|
errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
|
|
148
189
|
createAuthMiddleware(auth),
|
|
149
190
|
createMembershipMiddleware(),
|
|
150
|
-
createTenantMiddleware(
|
|
191
|
+
createTenantMiddleware({
|
|
192
|
+
publicTenancy: this.dependencies.container.has(CORE_PUBLIC_TENANCY_TOKEN) ? this.dependencies.container.resolve(CORE_PUBLIC_TENANCY_TOKEN) : undefined,
|
|
193
|
+
resolveTenant: this.dependencies.container.has(CORE_TENANT_RESOLVER_TOKEN) ? this.dependencies.container.resolve(CORE_TENANT_RESOLVER_TOKEN) : undefined
|
|
194
|
+
})
|
|
151
195
|
];
|
|
152
196
|
}
|
|
153
197
|
group(name) {
|
|
@@ -162,25 +206,28 @@ class HttpKernel {
|
|
|
162
206
|
if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
|
|
163
207
|
return [csrf];
|
|
164
208
|
}
|
|
209
|
+
const quotaPolicy = this.dependencies.container.has(CORE_THROTTLE_QUOTA_POLICY_TOKEN) ? this.dependencies.container.resolve(CORE_THROTTLE_QUOTA_POLICY_TOKEN) : undefined;
|
|
165
210
|
const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
|
|
166
211
|
const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
|
|
167
212
|
if (!redisUrl) {
|
|
168
213
|
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
169
214
|
return [
|
|
170
|
-
createMemoryThrottleMiddleware({
|
|
215
|
+
this.ownThrottle(createMemoryThrottleMiddleware({
|
|
171
216
|
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
172
|
-
decaySeconds: 60
|
|
173
|
-
|
|
217
|
+
decaySeconds: 60,
|
|
218
|
+
quotaPolicy
|
|
219
|
+
})),
|
|
174
220
|
csrf
|
|
175
221
|
];
|
|
176
222
|
}
|
|
177
223
|
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
178
224
|
return [
|
|
179
|
-
createThrottleMiddleware({
|
|
225
|
+
this.ownThrottle(createThrottleMiddleware({
|
|
180
226
|
redisUrl,
|
|
181
227
|
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
182
|
-
decaySeconds: 60
|
|
183
|
-
|
|
228
|
+
decaySeconds: 60,
|
|
229
|
+
quotaPolicy
|
|
230
|
+
})),
|
|
184
231
|
csrf
|
|
185
232
|
];
|
|
186
233
|
}
|
|
@@ -217,8 +264,8 @@ class HttpKernel {
|
|
|
217
264
|
return handler(request);
|
|
218
265
|
});
|
|
219
266
|
}
|
|
220
|
-
wrapWebPublicRead(handler) {
|
|
221
|
-
if (isPublicReadsEnabled()) {
|
|
267
|
+
wrapWebPublicRead(handler, options = {}) {
|
|
268
|
+
if (options.allowAnonymous ?? isPublicReadsEnabled()) {
|
|
222
269
|
return this.wrapWeb(handler);
|
|
223
270
|
}
|
|
224
271
|
return this.wrapWebAuthenticated(handler);
|
|
@@ -264,8 +311,8 @@ class HttpKernel {
|
|
|
264
311
|
const middleware = [...this.group("authenticated"), ...this.verifiedMiddleware()];
|
|
265
312
|
return withMiddleware(...middleware)(handler);
|
|
266
313
|
}
|
|
267
|
-
wrapPublicRead(handler) {
|
|
268
|
-
if (isPublicReadsEnabled()) {
|
|
314
|
+
wrapPublicRead(handler, options = {}) {
|
|
315
|
+
if (options.allowAnonymous ?? isPublicReadsEnabled()) {
|
|
269
316
|
return handler;
|
|
270
317
|
}
|
|
271
318
|
return this.wrapAuthenticated(handler);
|
|
@@ -310,25 +357,25 @@ class HttpKernel {
|
|
|
310
357
|
const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
|
|
311
358
|
const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
|
|
312
359
|
if (scope === "login") {
|
|
313
|
-
return withMiddleware(createLoginThrottleMiddleware({
|
|
360
|
+
return withMiddleware(this.ownThrottle(createLoginThrottleMiddleware({
|
|
314
361
|
...redisUrl ? { redisUrl } : {},
|
|
315
362
|
maxAttempts: rateLimit.maxAttempts,
|
|
316
363
|
decaySeconds: rateLimit.decaySeconds
|
|
317
|
-
}))(handler);
|
|
364
|
+
})))(handler);
|
|
318
365
|
}
|
|
319
366
|
if (redisUrl) {
|
|
320
|
-
return withMiddleware(createThrottleMiddleware({
|
|
367
|
+
return withMiddleware(this.ownThrottle(createThrottleMiddleware({
|
|
321
368
|
redisUrl,
|
|
322
369
|
maxAttempts: rateLimit.maxAttempts,
|
|
323
370
|
decaySeconds: rateLimit.decaySeconds,
|
|
324
371
|
keyPrefix: memoryKeyPrefix
|
|
325
|
-
}))(handler);
|
|
372
|
+
})))(handler);
|
|
326
373
|
}
|
|
327
|
-
return withMiddleware(createMemoryThrottleMiddleware({
|
|
374
|
+
return withMiddleware(this.ownThrottle(createMemoryThrottleMiddleware({
|
|
328
375
|
maxAttempts: rateLimit.maxAttempts,
|
|
329
376
|
decaySeconds: rateLimit.decaySeconds,
|
|
330
377
|
keyPrefix: memoryKeyPrefix
|
|
331
|
-
}))(handler);
|
|
378
|
+
})))(handler);
|
|
332
379
|
}
|
|
333
380
|
}
|
|
334
381
|
function createHttpKernel(dependencies) {
|
|
@@ -336,68 +383,172 @@ function createHttpKernel(dependencies) {
|
|
|
336
383
|
}
|
|
337
384
|
|
|
338
385
|
// ../../src/bootstrap/discoverModules.ts
|
|
339
|
-
import {
|
|
340
|
-
import { join } from "path";
|
|
386
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
387
|
+
import { join as join2, resolve } from "path";
|
|
388
|
+
|
|
389
|
+
// ../../src/bootstrap/infrastructureDiscovery.ts
|
|
390
|
+
import { readdir } from "fs/promises";
|
|
391
|
+
import { join, relative } from "path";
|
|
341
392
|
import { pathToFileURL } from "url";
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
393
|
+
async function infrastructureFiles(directory, exclude = []) {
|
|
394
|
+
const excluded = new Set([
|
|
395
|
+
"index.ts",
|
|
396
|
+
"index.js",
|
|
397
|
+
"index.mts",
|
|
398
|
+
"index.mjs",
|
|
399
|
+
"register.ts",
|
|
400
|
+
"register.js",
|
|
401
|
+
"register.mts",
|
|
402
|
+
"register.mjs",
|
|
403
|
+
...exclude
|
|
404
|
+
]);
|
|
405
|
+
const files = [];
|
|
406
|
+
async function visit(path) {
|
|
407
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
408
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
409
|
+
for (const entry of entries) {
|
|
410
|
+
const file = join(path, entry.name);
|
|
411
|
+
const key = relative(directory, file).replaceAll("\\", "/");
|
|
412
|
+
if (excluded.has(entry.name) || excluded.has(key))
|
|
413
|
+
continue;
|
|
414
|
+
if (entry.isDirectory())
|
|
415
|
+
await visit(file);
|
|
416
|
+
else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
|
|
417
|
+
files.push(file);
|
|
418
|
+
}
|
|
347
419
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
state.appModules.length = 0;
|
|
356
|
-
state.modulesReady = undefined;
|
|
420
|
+
try {
|
|
421
|
+
await visit(directory);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (error.code !== "ENOENT")
|
|
424
|
+
throw error;
|
|
425
|
+
if (error.path !== directory)
|
|
426
|
+
throw error;
|
|
357
427
|
}
|
|
358
|
-
|
|
428
|
+
return files;
|
|
359
429
|
}
|
|
360
|
-
function
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
430
|
+
async function importInfrastructure(file) {
|
|
431
|
+
try {
|
|
432
|
+
return await import(pathToFileURL(file).href);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
|
|
364
435
|
}
|
|
365
|
-
|
|
366
|
-
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ../../src/bootstrap/discoverModules.ts
|
|
439
|
+
var KEY = Symbol.for("@getstrata/discoverModulesState");
|
|
440
|
+
function state() {
|
|
441
|
+
const globals = globalThis;
|
|
442
|
+
globals[KEY] ??= { appModules: [] };
|
|
443
|
+
return globals[KEY];
|
|
444
|
+
}
|
|
445
|
+
function configureModulesDirectory(modulesDir) {
|
|
446
|
+
const current = state();
|
|
447
|
+
const directory = resolve(modulesDir);
|
|
448
|
+
if (current.configuredModulesDir !== directory || current.configuredManifest) {
|
|
449
|
+
current.appModules.length = 0;
|
|
450
|
+
current.modulesReady = undefined;
|
|
451
|
+
current.source = undefined;
|
|
452
|
+
}
|
|
453
|
+
current.configuredManifest = undefined;
|
|
454
|
+
current.configuredModulesDir = directory;
|
|
455
|
+
}
|
|
456
|
+
function configureModulesManifest(manifest) {
|
|
457
|
+
const current = state();
|
|
458
|
+
if (current.configuredManifest !== manifest) {
|
|
459
|
+
current.appModules.length = 0;
|
|
460
|
+
current.modulesReady = undefined;
|
|
461
|
+
current.source = undefined;
|
|
462
|
+
}
|
|
463
|
+
current.configuredManifest = manifest;
|
|
464
|
+
}
|
|
465
|
+
function validateModule(value, location) {
|
|
466
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
467
|
+
throw new TypeError(`Invalid application module at ${location}.`);
|
|
468
|
+
const module = value;
|
|
469
|
+
if (typeof module.name !== "string" || !module.name.trim())
|
|
470
|
+
throw new TypeError(`Invalid module name at ${location}.`);
|
|
471
|
+
if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
|
|
472
|
+
throw new TypeError(`Invalid module order at ${location}.`);
|
|
473
|
+
for (const hook of ["routes", "webRoutes"])
|
|
474
|
+
if (module[hook] !== undefined && typeof module[hook] !== "function")
|
|
475
|
+
throw new TypeError(`Invalid ${hook} at ${location}.`);
|
|
476
|
+
if (module.providers !== undefined) {
|
|
477
|
+
if (!Array.isArray(module.providers))
|
|
478
|
+
throw new TypeError(`Invalid providers at ${location}.`);
|
|
479
|
+
for (const provider of module.providers) {
|
|
480
|
+
if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
|
|
481
|
+
throw new TypeError(`Invalid provider at ${location}.`);
|
|
482
|
+
}
|
|
367
483
|
}
|
|
368
|
-
throw new Error("configureModulesDirectory() must be called before discovering modules. The app preload (or starter) should call it.");
|
|
369
484
|
}
|
|
370
|
-
async function
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
485
|
+
async function load(options) {
|
|
486
|
+
const manifest = options.manifest ?? (options.modulesDir === undefined ? state().configuredManifest : undefined);
|
|
487
|
+
const entries = [];
|
|
488
|
+
if (manifest)
|
|
489
|
+
for (const [index, value] of manifest.entries())
|
|
490
|
+
entries.push({ value, location: `module manifest[${index}]` });
|
|
491
|
+
else {
|
|
492
|
+
const directory = options.modulesDir ?? state().configuredModulesDir;
|
|
493
|
+
if (!directory)
|
|
494
|
+
throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
|
|
495
|
+
let folders;
|
|
496
|
+
try {
|
|
497
|
+
folders = await readdir2(directory, { withFileTypes: true });
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if (error.code === "ENOENT")
|
|
500
|
+
return [];
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
const excluded = new Set(options.exclude ?? []);
|
|
504
|
+
for (const folder of folders.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
505
|
+
const path = join2(directory, folder.name);
|
|
506
|
+
const files = await readdir2(path);
|
|
507
|
+
const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
|
|
508
|
+
if (candidates.length !== 1)
|
|
509
|
+
throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
|
|
510
|
+
const file = join2(path, candidates[0]);
|
|
511
|
+
const exports = await importInfrastructure(file);
|
|
512
|
+
entries.push({ value: exports.default, location: file });
|
|
378
513
|
}
|
|
379
|
-
throw error;
|
|
380
514
|
}
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
515
|
+
const names = new Map;
|
|
516
|
+
const modules = [];
|
|
517
|
+
for (const { value, location } of entries) {
|
|
518
|
+
validateModule(value, location);
|
|
519
|
+
const previous = names.get(value.name);
|
|
520
|
+
if (previous)
|
|
521
|
+
throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
|
|
522
|
+
names.set(value.name, location);
|
|
523
|
+
modules.push(value);
|
|
524
|
+
}
|
|
525
|
+
return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
526
|
+
}
|
|
527
|
+
function ensureModulesLoaded(options = {}) {
|
|
528
|
+
const current = state();
|
|
529
|
+
const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
|
|
530
|
+
options.modulesDir ? resolve(options.modulesDir) : current.configuredModulesDir,
|
|
531
|
+
[...options.exclude ?? []].sort()
|
|
532
|
+
]);
|
|
533
|
+
if (current.modulesReady && current.source === key)
|
|
534
|
+
return current.modulesReady;
|
|
535
|
+
current.appModules.length = 0;
|
|
536
|
+
current.source = key;
|
|
537
|
+
const pending = load(options).then((modules) => {
|
|
538
|
+
if (current.modulesReady !== pending)
|
|
539
|
+
return modules;
|
|
540
|
+
current.appModules.splice(0, current.appModules.length, ...modules);
|
|
541
|
+
return current.appModules;
|
|
542
|
+
}).catch((error) => {
|
|
543
|
+
if (current.modulesReady === pending)
|
|
544
|
+
current.modulesReady = undefined;
|
|
545
|
+
throw error;
|
|
396
546
|
});
|
|
397
|
-
|
|
547
|
+
current.modulesReady = pending;
|
|
548
|
+
return pending;
|
|
398
549
|
}
|
|
399
550
|
function discoverModules() {
|
|
400
|
-
return
|
|
551
|
+
return state().appModules;
|
|
401
552
|
}
|
|
402
553
|
// ../../src/bootstrap/prefixRouteMap.ts
|
|
403
554
|
function prefixRouteMap(prefix, routes) {
|
|
@@ -527,6 +678,10 @@ function cacheTagsForModelWrite(tableName, action) {
|
|
|
527
678
|
function discoverModelTableNames() {
|
|
528
679
|
return discoverModules().map((module) => module.tableName).filter((tableName) => tableName !== undefined);
|
|
529
680
|
}
|
|
681
|
+
// ../../src/bootstrap/context.ts
|
|
682
|
+
import { CORE_HTTP_CLEANUP_TOKEN as CORE_HTTP_CLEANUP_TOKEN2 } from "@getstrata/core/contracts/serviceTokens";
|
|
683
|
+
import { clearActiveApplicationContext } from "@getstrata/core/runtime/applicationRegistry";
|
|
684
|
+
|
|
530
685
|
// ../../src/bootstrap/contracts.ts
|
|
531
686
|
import {
|
|
532
687
|
ConfigStore,
|
|
@@ -587,7 +742,7 @@ import { createCacheStore } from "@getstrata/core/cache/createCacheStore";
|
|
|
587
742
|
import { CacheRepository } from "@getstrata/core/cache/repository";
|
|
588
743
|
var cacheProvider = {
|
|
589
744
|
name: "core.cache",
|
|
590
|
-
register({ container, config, dependencies }) {
|
|
745
|
+
register({ container, config, dependencies, onCleanup }) {
|
|
591
746
|
container.singleton(CORE_CACHE_TOKEN, () => {
|
|
592
747
|
const store = createCacheStore({
|
|
593
748
|
driver: config.require(CACHE_DRIVER_CONFIG_KEY),
|
|
@@ -595,7 +750,9 @@ var cacheProvider = {
|
|
|
595
750
|
maxEntries: config.require(CACHE_MAX_ENTRIES_CONFIG_KEY),
|
|
596
751
|
redisUrl: config.get(REDIS_URL_CONFIG_KEY) || undefined
|
|
597
752
|
});
|
|
598
|
-
|
|
753
|
+
const cache = new CacheRepository(store);
|
|
754
|
+
onCleanup(() => cache.close());
|
|
755
|
+
return cache;
|
|
599
756
|
});
|
|
600
757
|
dependencies.cache = container.resolve(CORE_CACHE_TOKEN);
|
|
601
758
|
}
|
|
@@ -792,49 +949,47 @@ var eventsProvider = {
|
|
|
792
949
|
var events_default = eventsProvider;
|
|
793
950
|
|
|
794
951
|
// ../../src/bootstrap/discoverListeners.ts
|
|
795
|
-
import {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const listenersDirectory = resolveListenersDirectory();
|
|
818
|
-
let entries;
|
|
819
|
-
try {
|
|
820
|
-
entries = readdirSync2(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.(ts|js)$/.test(entry.name)).map((entry) => entry.name);
|
|
821
|
-
} catch (error) {
|
|
822
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
823
|
-
return [];
|
|
952
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
953
|
+
var KEY2 = Symbol.for("@getstrata/discoverListenersState");
|
|
954
|
+
function state2() {
|
|
955
|
+
const globals = globalThis;
|
|
956
|
+
globals[KEY2] ??= { loads: new Map };
|
|
957
|
+
return globals[KEY2];
|
|
958
|
+
}
|
|
959
|
+
async function load2(options) {
|
|
960
|
+
const listeners = new Set;
|
|
961
|
+
const add = (value, location) => {
|
|
962
|
+
if (typeof value !== "function")
|
|
963
|
+
throw new TypeError(`Invalid listener registrar at ${location}: expected a default function.`);
|
|
964
|
+
listeners.add(value);
|
|
965
|
+
};
|
|
966
|
+
if (options.manifest)
|
|
967
|
+
for (const [index, value] of options.manifest.entries())
|
|
968
|
+
add(value, `listener manifest[${index}]`);
|
|
969
|
+
else
|
|
970
|
+
for (const file of await infrastructureFiles(options.directory ?? join3(process.cwd(), "src", "listeners"), options.exclude)) {
|
|
971
|
+
const exports = await importInfrastructure(file);
|
|
972
|
+
const value = exports.default;
|
|
973
|
+
add(value && typeof value === "object" && "default" in value ? value.default : value, file);
|
|
824
974
|
}
|
|
975
|
+
return [...listeners];
|
|
976
|
+
}
|
|
977
|
+
function discoverListeners(options = {}) {
|
|
978
|
+
const cache = state2().loads;
|
|
979
|
+
const key = options.manifest ?? JSON.stringify([
|
|
980
|
+
resolve2(options.directory ?? join3(process.cwd(), "src", "listeners")),
|
|
981
|
+
[...options.exclude ?? []].sort()
|
|
982
|
+
]);
|
|
983
|
+
const existing = cache.get(key);
|
|
984
|
+
if (existing)
|
|
985
|
+
return existing;
|
|
986
|
+
const pending = load2(options).catch((error) => {
|
|
987
|
+
if (cache.get(key) === pending)
|
|
988
|
+
cache.delete(key);
|
|
825
989
|
throw error;
|
|
826
|
-
}
|
|
827
|
-
const listeners = entries.map((fileName) => {
|
|
828
|
-
const filePath = join2(listenersDirectory, fileName);
|
|
829
|
-
const loaded = requireListener(filePath);
|
|
830
|
-
return loaded.default;
|
|
831
990
|
});
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
function discoverListeners() {
|
|
835
|
-
const state = readDiscoverListenersState();
|
|
836
|
-
state.appListeners ??= loadDiscoveredListeners();
|
|
837
|
-
return state.appListeners;
|
|
991
|
+
cache.set(key, pending);
|
|
992
|
+
return pending;
|
|
838
993
|
}
|
|
839
994
|
|
|
840
995
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
@@ -927,22 +1082,12 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options =
|
|
|
927
1082
|
}
|
|
928
1083
|
|
|
929
1084
|
// ../../src/bootstrap/providers/listeners.ts
|
|
930
|
-
var registeredListenerGroups = new Set;
|
|
931
|
-
function registerListenerGroup(name, register) {
|
|
932
|
-
if (registeredListenerGroups.has(name)) {
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
registeredListenerGroups.add(name);
|
|
936
|
-
register();
|
|
937
|
-
}
|
|
938
1085
|
var listenersProvider = {
|
|
939
1086
|
name: "core.listeners",
|
|
940
|
-
boot() {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
for (const registerListener of discoverListeners()) {
|
|
945
|
-
registerListener();
|
|
1087
|
+
async boot({ onCleanup }) {
|
|
1088
|
+
onCleanup(registerInvalidateCacheOnModelWriteListeners());
|
|
1089
|
+
for (const registerListener of await discoverListeners()) {
|
|
1090
|
+
await registerListener();
|
|
946
1091
|
}
|
|
947
1092
|
}
|
|
948
1093
|
};
|
|
@@ -966,68 +1111,99 @@ import {
|
|
|
966
1111
|
} from "@getstrata/core/queue/createAppQueue";
|
|
967
1112
|
|
|
968
1113
|
// ../../src/bootstrap/discoverJobs.ts
|
|
969
|
-
import {
|
|
970
|
-
import {
|
|
971
|
-
import { join as join3 } from "path";
|
|
1114
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
1115
|
+
import { Job } from "@getstrata/core/queue";
|
|
972
1116
|
import { jobRegistry } from "@getstrata/core/queue/jobRegistry";
|
|
973
|
-
var
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
1117
|
+
var KEY3 = Symbol.for("@getstrata/discoverJobsState");
|
|
1118
|
+
function state3() {
|
|
1119
|
+
const globals = globalThis;
|
|
1120
|
+
globals[KEY3] ??= { loads: new Map, registered: new Map };
|
|
1121
|
+
return globals[KEY3];
|
|
1122
|
+
}
|
|
1123
|
+
function validateClass(value, location) {
|
|
1124
|
+
if (typeof value !== "function" || typeof value.jobName !== "string" || !value.jobName.trim() || !(value.prototype instanceof Job) && typeof value.prototype?.handle !== "function")
|
|
1125
|
+
throw new TypeError(`Invalid job at ${location}: expected a class with nonempty static jobName and handle().`);
|
|
1126
|
+
}
|
|
1127
|
+
async function load3(options, current) {
|
|
1128
|
+
const jobs = new Map;
|
|
1129
|
+
const add = (name, identity, create, location) => {
|
|
1130
|
+
const previous = jobs.get(name);
|
|
1131
|
+
if (previous && previous.identity !== identity)
|
|
1132
|
+
throw new Error(`Duplicate job "${name}" at ${location}; first declared at ${previous.location}.`);
|
|
1133
|
+
if (!previous)
|
|
1134
|
+
jobs.set(name, { identity, create, location });
|
|
1135
|
+
};
|
|
1136
|
+
if (options.manifest) {
|
|
1137
|
+
for (const [index, entry] of options.manifest.entries()) {
|
|
1138
|
+
const location = `job manifest[${index}]`;
|
|
1139
|
+
if (typeof entry === "function") {
|
|
1140
|
+
validateClass(entry, location);
|
|
1141
|
+
add(entry.jobName, entry, () => new entry, location);
|
|
1142
|
+
} else {
|
|
1143
|
+
if (!entry || typeof entry.name !== "string" || !entry.name.trim() || typeof entry.create !== "function")
|
|
1144
|
+
throw new TypeError(`Invalid job factory at ${location}.`);
|
|
1145
|
+
add(entry.name, entry.create, entry.create, location);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
} else {
|
|
1149
|
+
for (const file of await infrastructureFiles(options.directory ?? join4(process.cwd(), "src", "jobs"), options.exclude)) {
|
|
1150
|
+
const exports = await importInfrastructure(file);
|
|
1151
|
+
const defaultValue = exports.default;
|
|
1152
|
+
const legacy = defaultValue && typeof defaultValue === "object" && "default" in defaultValue ? defaultValue.default : defaultValue;
|
|
1153
|
+
if (legacy !== undefined)
|
|
1154
|
+
validateClass(legacy, `${file}:default`);
|
|
1155
|
+
let found = false;
|
|
1156
|
+
for (const [name, value] of Object.entries({
|
|
1157
|
+
...exports,
|
|
1158
|
+
...legacy === undefined ? {} : { default: legacy }
|
|
1159
|
+
})) {
|
|
1160
|
+
if (name !== "default" && !(typeof value === "function" && ("jobName" in value)))
|
|
1161
|
+
continue;
|
|
1162
|
+
validateClass(value, `${file}:${name}`);
|
|
1163
|
+
found = true;
|
|
1164
|
+
add(value.jobName, value, () => new value, `${file}:${name}`);
|
|
1165
|
+
}
|
|
1166
|
+
if (!found)
|
|
1167
|
+
throw new TypeError(`No job exports at ${file}; exclude helper modules explicitly.`);
|
|
1168
|
+
}
|
|
988
1169
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1170
|
+
if (!current())
|
|
1171
|
+
return [...jobs.keys()];
|
|
1172
|
+
const registered = state3().registered;
|
|
1173
|
+
for (const [name, entry] of jobs) {
|
|
1174
|
+
const previous = registered.get(name);
|
|
1175
|
+
if (previous && previous.identity !== entry.identity)
|
|
1176
|
+
throw new Error(`Duplicate discovered job "${name}" at ${entry.location}; first declared at ${previous.location}.`);
|
|
1177
|
+
}
|
|
1178
|
+
const existing = new Set(jobRegistry.names());
|
|
1179
|
+
for (const [name, entry] of jobs) {
|
|
1180
|
+
if (!existing.has(name)) {
|
|
1181
|
+
jobRegistry.register(name, entry.create);
|
|
1182
|
+
registered.set(name, entry);
|
|
1183
|
+
}
|
|
994
1184
|
}
|
|
995
|
-
|
|
996
|
-
return typeof jobName === "string" && jobName.length > 0;
|
|
1185
|
+
return [...jobs.keys()];
|
|
997
1186
|
}
|
|
998
|
-
function
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1187
|
+
function discoverJobs(options = {}) {
|
|
1188
|
+
const cache = state3().loads;
|
|
1189
|
+
const key = options.manifest ?? JSON.stringify([
|
|
1190
|
+
resolve3(options.directory ?? join4(process.cwd(), "src", "jobs")),
|
|
1191
|
+
[...options.exclude ?? []].sort()
|
|
1192
|
+
]);
|
|
1193
|
+
const existing = cache.get(key);
|
|
1194
|
+
if (existing)
|
|
1195
|
+
return existing;
|
|
1196
|
+
const pending = load3(options, () => state3().loads === cache).catch((error) => {
|
|
1197
|
+
if (cache.get(key) === pending)
|
|
1198
|
+
cache.delete(key);
|
|
1007
1199
|
throw error;
|
|
1008
|
-
}
|
|
1009
|
-
return entries.flatMap((fileName) => {
|
|
1010
|
-
const filePath = join3(jobsDirectory, fileName);
|
|
1011
|
-
const loaded = requireJob(filePath);
|
|
1012
|
-
return isDiscoveredJobClass(loaded.default) ? [loaded.default] : [];
|
|
1013
1200
|
});
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const state = readDiscoverJobsState();
|
|
1017
|
-
if (state.names) {
|
|
1018
|
-
return state.names;
|
|
1019
|
-
}
|
|
1020
|
-
const names = [];
|
|
1021
|
-
for (const JobClass of loadDiscoveredJobClasses()) {
|
|
1022
|
-
jobRegistry.register(JobClass.jobName, () => new JobClass);
|
|
1023
|
-
names.push(JobClass.jobName);
|
|
1024
|
-
}
|
|
1025
|
-
state.names = names;
|
|
1026
|
-
return names;
|
|
1201
|
+
cache.set(key, pending);
|
|
1202
|
+
return pending;
|
|
1027
1203
|
}
|
|
1028
1204
|
function resetDiscoverJobsForTests() {
|
|
1029
|
-
|
|
1030
|
-
|
|
1205
|
+
state3().loads = new Map;
|
|
1206
|
+
state3().registered = new Map;
|
|
1031
1207
|
}
|
|
1032
1208
|
|
|
1033
1209
|
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
@@ -1045,16 +1221,17 @@ function registerDefaultJobs() {
|
|
|
1045
1221
|
// ../../src/bootstrap/providers/queue.ts
|
|
1046
1222
|
var queueProvider = {
|
|
1047
1223
|
name: "core.queue",
|
|
1048
|
-
register({ container, config }) {
|
|
1224
|
+
async register({ container, config, onCleanup }) {
|
|
1049
1225
|
const configuredDriver = process.env.QUEUE_DRIVER ?? DEFAULT_QUEUE_DRIVER;
|
|
1050
1226
|
const driver = configuredDriver === "async" || configuredDriver === "redis" || configuredDriver === "sync" ? configuredDriver : DEFAULT_QUEUE_DRIVER;
|
|
1051
1227
|
config.set("queue.driver", driver);
|
|
1052
1228
|
const failedJobs = createFailedJobService();
|
|
1053
1229
|
container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1230
|
+
registerDefaultJobs();
|
|
1231
|
+
await discoverJobs();
|
|
1232
|
+
const queue = createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs);
|
|
1233
|
+
onCleanup(() => queue.close?.(), driver === "async" ? "drain" : "close");
|
|
1234
|
+
container.set(CORE_QUEUE_TOKEN, queue);
|
|
1058
1235
|
}
|
|
1059
1236
|
};
|
|
1060
1237
|
var queue_default = queueProvider;
|
|
@@ -1069,6 +1246,18 @@ var storageProvider = {
|
|
|
1069
1246
|
};
|
|
1070
1247
|
var storage_default = storageProvider;
|
|
1071
1248
|
|
|
1249
|
+
// ../../src/bootstrap/providers/tracing.ts
|
|
1250
|
+
import { acquireTracingRuntime } from "@getstrata/core/tracing/tracingMiddleware";
|
|
1251
|
+
var tracingProvider = {
|
|
1252
|
+
name: "core.tracing",
|
|
1253
|
+
register({ onCleanup }) {
|
|
1254
|
+
const ownership = acquireTracingRuntime();
|
|
1255
|
+
onCleanup(() => ownership.flush(), "flush");
|
|
1256
|
+
onCleanup(() => ownership.release(), "close");
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
var tracing_default = tracingProvider;
|
|
1260
|
+
|
|
1072
1261
|
// ../../src/bootstrap/providers/view.ts
|
|
1073
1262
|
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
1074
1263
|
import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
|
|
@@ -1113,6 +1302,7 @@ var viewProvider = {
|
|
|
1113
1302
|
// ../../src/bootstrap/providers/index.ts
|
|
1114
1303
|
var coreProviders = [
|
|
1115
1304
|
config_default,
|
|
1305
|
+
tracing_default,
|
|
1116
1306
|
cache_default,
|
|
1117
1307
|
storage_default,
|
|
1118
1308
|
auth_default,
|
|
@@ -1127,36 +1317,127 @@ var coreProviders = [
|
|
|
1127
1317
|
function collectProviders(modules = discoverModules()) {
|
|
1128
1318
|
return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
|
|
1129
1319
|
}
|
|
1130
|
-
function runProviderPhase(providers, phase, context) {
|
|
1131
|
-
for (const provider of providers)
|
|
1132
|
-
provider[phase]?.(context);
|
|
1133
|
-
}
|
|
1320
|
+
async function runProviderPhase(providers, phase, context) {
|
|
1321
|
+
for (const provider of providers)
|
|
1322
|
+
await provider[phase]?.(context);
|
|
1134
1323
|
}
|
|
1135
|
-
|
|
1324
|
+
var readyContext;
|
|
1325
|
+
async function createAppContext(providers) {
|
|
1326
|
+
if (!providers)
|
|
1327
|
+
await ensureModulesLoaded();
|
|
1328
|
+
const configured = [...providers ?? collectProviders()];
|
|
1136
1329
|
const container = new ServiceContainer;
|
|
1137
1330
|
const config = new ConfigStore;
|
|
1138
|
-
const dependencies = {
|
|
1139
|
-
|
|
1331
|
+
const dependencies = { container };
|
|
1332
|
+
const cleanups = [];
|
|
1333
|
+
const drains = [];
|
|
1334
|
+
let draining;
|
|
1335
|
+
const drain = () => {
|
|
1336
|
+
draining ??= (async () => {
|
|
1337
|
+
const errors = [];
|
|
1338
|
+
for (const handler of drains.splice(0).reverse()) {
|
|
1339
|
+
try {
|
|
1340
|
+
await handler();
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
errors.push(error);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
if (errors.length)
|
|
1346
|
+
throw new AggregateError(errors, "Provider drain failed.");
|
|
1347
|
+
})();
|
|
1348
|
+
return draining;
|
|
1140
1349
|
};
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1350
|
+
const flushes = [];
|
|
1351
|
+
let flushing;
|
|
1352
|
+
const flush = () => {
|
|
1353
|
+
flushing ??= (async () => {
|
|
1354
|
+
const errors = [];
|
|
1355
|
+
for (const handler of flushes.splice(0).reverse()) {
|
|
1356
|
+
try {
|
|
1357
|
+
await handler();
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
errors.push(error);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
if (errors.length)
|
|
1363
|
+
throw new AggregateError(errors, "Provider flush failed.");
|
|
1364
|
+
})();
|
|
1365
|
+
return flushing;
|
|
1366
|
+
};
|
|
1367
|
+
let disposal;
|
|
1368
|
+
let disposed = false;
|
|
1369
|
+
let initialized;
|
|
1370
|
+
const dispose = () => {
|
|
1371
|
+
disposed = true;
|
|
1372
|
+
disposal ??= (async () => {
|
|
1373
|
+
await drain();
|
|
1374
|
+
const errors = [];
|
|
1375
|
+
try {
|
|
1376
|
+
await flush();
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
errors.push(error);
|
|
1379
|
+
}
|
|
1380
|
+
if (initialized) {
|
|
1381
|
+
clearActiveApplicationContext(initialized);
|
|
1382
|
+
if (readyContext === initialized)
|
|
1383
|
+
readyContext = undefined;
|
|
1384
|
+
}
|
|
1385
|
+
for (const handler of cleanups.splice(0).reverse()) {
|
|
1386
|
+
try {
|
|
1387
|
+
await handler();
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
errors.push(error);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (errors.length)
|
|
1393
|
+
throw new AggregateError(errors, "Provider cleanup failed.");
|
|
1394
|
+
})();
|
|
1395
|
+
return disposal;
|
|
1145
1396
|
};
|
|
1146
|
-
const
|
|
1147
|
-
runProviderPhase(providers, "register", context);
|
|
1148
|
-
runProviderPhase(providers, "boot", context);
|
|
1149
|
-
assertAppDependenciesComplete(dependencies);
|
|
1150
|
-
const appContext = {
|
|
1397
|
+
const context = {
|
|
1151
1398
|
container,
|
|
1152
1399
|
config,
|
|
1153
|
-
dependencies
|
|
1400
|
+
dependencies,
|
|
1401
|
+
onCleanup(handler, phase = "close") {
|
|
1402
|
+
if (disposed)
|
|
1403
|
+
throw new Error("Cannot register cleanup on a disposed application context.");
|
|
1404
|
+
if (phase !== "drain" && phase !== "flush" && phase !== "close")
|
|
1405
|
+
throw new TypeError("Unknown provider cleanup phase.");
|
|
1406
|
+
if (phase === "drain" && draining || phase === "flush" && flushing)
|
|
1407
|
+
throw new Error("Cannot register cleanup after its phase started.");
|
|
1408
|
+
(phase === "drain" ? drains : phase === "flush" ? flushes : cleanups).push(handler);
|
|
1409
|
+
}
|
|
1154
1410
|
};
|
|
1155
|
-
|
|
1156
|
-
|
|
1411
|
+
container.set(CORE_HTTP_CLEANUP_TOKEN2, (handler) => context.onCleanup(handler));
|
|
1412
|
+
try {
|
|
1413
|
+
await runProviderPhase(configured, "register", context);
|
|
1414
|
+
await runProviderPhase(configured, "boot", context);
|
|
1415
|
+
assertAppDependenciesComplete(dependencies);
|
|
1416
|
+
const appContext = {
|
|
1417
|
+
container,
|
|
1418
|
+
config,
|
|
1419
|
+
dependencies,
|
|
1420
|
+
drain,
|
|
1421
|
+
flush,
|
|
1422
|
+
dispose
|
|
1423
|
+
};
|
|
1424
|
+
initialized = appContext;
|
|
1425
|
+
setActiveApplicationContext(appContext);
|
|
1426
|
+
readyContext = appContext;
|
|
1427
|
+
return appContext;
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
try {
|
|
1430
|
+
await dispose();
|
|
1431
|
+
} catch (cleanupError) {
|
|
1432
|
+
throw new AggregateError([error, cleanupError], "Application startup and cleanup failed.", {
|
|
1433
|
+
cause: error
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
throw error;
|
|
1437
|
+
}
|
|
1157
1438
|
}
|
|
1158
1439
|
// ../../src/bootstrap/createWebRoutes.ts
|
|
1159
|
-
import { join as
|
|
1440
|
+
import { join as join5 } from "path";
|
|
1160
1441
|
import { isViewsEnabled as isViewsEnabled3 } from "@getstrata/core/runtime/frontendMode";
|
|
1161
1442
|
import { assertUrlPathUnderRoot } from "@getstrata/core/security/safePath";
|
|
1162
1443
|
import { notFoundHtmlResponse } from "@getstrata/core/view";
|
|
@@ -1173,7 +1454,7 @@ function createWebRoutes(dependencies, options = {}) {
|
|
|
1173
1454
|
registerRoute("GET", "/assets/*", ["global", "web"]);
|
|
1174
1455
|
const pathname = new URL(request.url).pathname;
|
|
1175
1456
|
try {
|
|
1176
|
-
const filePath = assertUrlPathUnderRoot(
|
|
1457
|
+
const filePath = assertUrlPathUnderRoot(join5(process.cwd(), "public"), pathname);
|
|
1177
1458
|
const file = Bun.file(filePath);
|
|
1178
1459
|
if (!await file.exists()) {
|
|
1179
1460
|
return notFoundHtmlResponse();
|
|
@@ -1196,8 +1477,43 @@ function mergeWebRoutes(dependencies, routes, options = {}) {
|
|
|
1196
1477
|
};
|
|
1197
1478
|
}
|
|
1198
1479
|
// ../../src/bootstrap/dependencies.ts
|
|
1199
|
-
function createAppDependencies() {
|
|
1200
|
-
return createAppContext().dependencies;
|
|
1480
|
+
async function createAppDependencies() {
|
|
1481
|
+
return (await createAppContext()).dependencies;
|
|
1482
|
+
}
|
|
1483
|
+
// ../../src/bootstrap/discoverModels.ts
|
|
1484
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
1485
|
+
import { join as join6 } from "path";
|
|
1486
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1487
|
+
import { bootModels, Model } from "@getstrata/core/database/model";
|
|
1488
|
+
async function discoverModels(directory = join6(process.cwd(), "src", "models"), options = {}) {
|
|
1489
|
+
const excluded = new Set([
|
|
1490
|
+
"register.ts",
|
|
1491
|
+
"register.js",
|
|
1492
|
+
"index.ts",
|
|
1493
|
+
"index.js",
|
|
1494
|
+
...options.exclude ?? []
|
|
1495
|
+
]);
|
|
1496
|
+
const models = new Set;
|
|
1497
|
+
async function visit(path) {
|
|
1498
|
+
const entries = await readdir3(path, { withFileTypes: true });
|
|
1499
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1500
|
+
for (const entry of entries) {
|
|
1501
|
+
const file = join6(path, entry.name);
|
|
1502
|
+
if (entry.isDirectory())
|
|
1503
|
+
await visit(file);
|
|
1504
|
+
else if (!excluded.has(entry.name) && entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.d\.[cm]?ts$/.test(entry.name)) {
|
|
1505
|
+
const exports = await import(pathToFileURL2(file).href);
|
|
1506
|
+
for (const value of Object.values(exports)) {
|
|
1507
|
+
if (typeof value === "function" && value.prototype instanceof Model)
|
|
1508
|
+
models.add(value);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
await visit(directory);
|
|
1514
|
+
const result = [...models];
|
|
1515
|
+
bootModels(result);
|
|
1516
|
+
return result;
|
|
1201
1517
|
}
|
|
1202
1518
|
// ../../src/bootstrap/health.ts
|
|
1203
1519
|
import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
|
|
@@ -1429,8 +1745,8 @@ function assertFeatureProductionSecrets(env) {
|
|
|
1429
1745
|
throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
|
|
1430
1746
|
}
|
|
1431
1747
|
}
|
|
1432
|
-
if (envFlagEnabled3(env.
|
|
1433
|
-
throw new Error("Production startup blocked: set
|
|
1748
|
+
if (envFlagEnabled3(env.TENANT_DEV_HEADERS)) {
|
|
1749
|
+
throw new Error("Production startup blocked: set TENANT_DEV_HEADERS=false.");
|
|
1434
1750
|
}
|
|
1435
1751
|
if (!env.SIEM_EXPORT_URL?.trim() && envFlagEnabled3(env.FEATURE_SIEM_EXPORT)) {
|
|
1436
1752
|
console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
|
|
@@ -1580,6 +1896,7 @@ function createRouteKernel(dependencies) {
|
|
|
1580
1896
|
return createHttpKernel(dependencies);
|
|
1581
1897
|
}
|
|
1582
1898
|
// ../../src/bootstrap/web/server.ts
|
|
1899
|
+
import { resolveMaxBodyBytes } from "@getstrata/core/http/bodySizeLimitMiddleware";
|
|
1583
1900
|
import { createCorsMiddleware as createCorsMiddleware2 } from "@getstrata/core/http/corsMiddleware";
|
|
1584
1901
|
import { currentRequestMeta as currentRequestMeta2, runWithRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
1585
1902
|
import { assertUrlPathUnderRoot as assertUrlPathUnderRoot2 } from "@getstrata/core/security/safePath";
|
|
@@ -1591,29 +1908,33 @@ function socketAddress(server, request) {
|
|
|
1591
1908
|
async function missingHtmlResponse() {
|
|
1592
1909
|
return notFoundHtmlResponse2();
|
|
1593
1910
|
}
|
|
1594
|
-
function wrapRouteHandler(handler, path) {
|
|
1911
|
+
function wrapRouteHandler(handler, path, onRequest, track = (work) => work()) {
|
|
1595
1912
|
return async (request, server) => {
|
|
1596
|
-
return await runWithRequestMeta({
|
|
1913
|
+
return await track(async () => await runWithRequestMeta({
|
|
1597
1914
|
...currentRequestMeta2(),
|
|
1598
1915
|
request,
|
|
1599
1916
|
routeTemplate: path,
|
|
1600
1917
|
ipAddress: socketAddress(server, request),
|
|
1918
|
+
peerAddress: socketAddress(server, request),
|
|
1601
1919
|
userAgent: request.headers.get("user-agent")
|
|
1602
1920
|
}, async () => {
|
|
1921
|
+
const intercepted = await onRequest?.(request, server);
|
|
1922
|
+
if (intercepted instanceof Response)
|
|
1923
|
+
return intercepted;
|
|
1603
1924
|
const response = await handler(request);
|
|
1604
1925
|
return response ?? await missingHtmlResponse();
|
|
1605
|
-
});
|
|
1926
|
+
}));
|
|
1606
1927
|
};
|
|
1607
1928
|
}
|
|
1608
|
-
function convertAppRoutesToBunRoutes(routes, cors = {}) {
|
|
1929
|
+
function convertAppRoutesToBunRoutes(routes, cors = {}, onRequest, track) {
|
|
1609
1930
|
const bunRoutes = {};
|
|
1610
1931
|
const middleware = createCorsMiddleware2(cors);
|
|
1611
1932
|
const preflight = (request) => middleware(request, async () => new Response(null, { status: 405 }));
|
|
1612
1933
|
for (const [path, handler] of Object.entries(routes)) {
|
|
1613
1934
|
if (typeof handler === "function") {
|
|
1614
1935
|
bunRoutes[path] = {
|
|
1615
|
-
GET: wrapRouteHandler(handler, path),
|
|
1616
|
-
OPTIONS: wrapRouteHandler(preflight, path)
|
|
1936
|
+
GET: wrapRouteHandler(handler, path, onRequest, track),
|
|
1937
|
+
OPTIONS: wrapRouteHandler(preflight, path, onRequest, track)
|
|
1617
1938
|
};
|
|
1618
1939
|
continue;
|
|
1619
1940
|
}
|
|
@@ -1623,10 +1944,10 @@ function convertAppRoutesToBunRoutes(routes, cors = {}) {
|
|
|
1623
1944
|
if (typeof methodHandler !== "function") {
|
|
1624
1945
|
continue;
|
|
1625
1946
|
}
|
|
1626
|
-
methods[method.toUpperCase()] = wrapRouteHandler(methodHandler, path);
|
|
1947
|
+
methods[method.toUpperCase()] = wrapRouteHandler(methodHandler, path, onRequest, track);
|
|
1627
1948
|
}
|
|
1628
1949
|
if (Object.keys(methods).length > 0) {
|
|
1629
|
-
methods.OPTIONS ??= wrapRouteHandler(preflight, path);
|
|
1950
|
+
methods.OPTIONS ??= wrapRouteHandler(preflight, path, onRequest, track);
|
|
1630
1951
|
bunRoutes[path] = methods;
|
|
1631
1952
|
}
|
|
1632
1953
|
}
|
|
@@ -1634,19 +1955,48 @@ function convertAppRoutesToBunRoutes(routes, cors = {}) {
|
|
|
1634
1955
|
return bunRoutes;
|
|
1635
1956
|
}
|
|
1636
1957
|
function createWebServer(options) {
|
|
1958
|
+
if (options.lifecycle?.isShuttingDown)
|
|
1959
|
+
throw new Error("Cannot start HTTP during shutdown.");
|
|
1960
|
+
const onRequest = (request, server) => {
|
|
1961
|
+
if (options.lifecycle?.isShuttingDown)
|
|
1962
|
+
return new Response("Service Unavailable", { status: 503 });
|
|
1963
|
+
return options.onRequest?.(request, server);
|
|
1964
|
+
};
|
|
1965
|
+
for (const field of ["fetch", "routes", "port", "unix"])
|
|
1966
|
+
if (options.native && field in options.native)
|
|
1967
|
+
throw new TypeError(`native.${field} is owned by createWebServer.`);
|
|
1968
|
+
const maxRequestBodySize = options.native?.maxRequestBodySize ?? resolveMaxBodyBytes();
|
|
1969
|
+
if (!Number.isSafeInteger(maxRequestBodySize) || maxRequestBodySize <= 0)
|
|
1970
|
+
throw new RangeError("maxRequestBodySize must be a positive safe integer.");
|
|
1971
|
+
const active = new Set;
|
|
1972
|
+
const track = (work) => {
|
|
1973
|
+
if (!options.lifecycle)
|
|
1974
|
+
return work();
|
|
1975
|
+
const running = work();
|
|
1976
|
+
active.add(running);
|
|
1977
|
+
return running.finally(() => {
|
|
1978
|
+
active.delete(running);
|
|
1979
|
+
});
|
|
1980
|
+
};
|
|
1637
1981
|
const publicDir = options.publicDir ?? "./public";
|
|
1638
|
-
const bunRoutes = options.routes ? convertAppRoutesToBunRoutes(options.routes, options.cors) : undefined;
|
|
1639
|
-
|
|
1982
|
+
const bunRoutes = options.routes ? convertAppRoutesToBunRoutes(options.routes, options.cors, onRequest, track) : undefined;
|
|
1983
|
+
const { websocket, ...native } = options.native ?? {};
|
|
1984
|
+
const configuration = {
|
|
1985
|
+
...native,
|
|
1986
|
+
maxRequestBodySize,
|
|
1640
1987
|
port: options.port,
|
|
1641
1988
|
...bunRoutes ? { routes: bunRoutes } : {},
|
|
1642
1989
|
async fetch(request, server) {
|
|
1643
|
-
return await runWithRequestMeta({
|
|
1990
|
+
return await track(async () => await runWithRequestMeta({
|
|
1644
1991
|
...currentRequestMeta2(),
|
|
1645
1992
|
request,
|
|
1646
1993
|
ipAddress: socketAddress(server, request),
|
|
1994
|
+
peerAddress: socketAddress(server, request),
|
|
1647
1995
|
userAgent: request.headers.get("user-agent")
|
|
1648
1996
|
}, async () => {
|
|
1649
|
-
await
|
|
1997
|
+
const intercepted = await onRequest(request, server);
|
|
1998
|
+
if (intercepted instanceof Response)
|
|
1999
|
+
return intercepted;
|
|
1650
2000
|
const url = new URL(request.url);
|
|
1651
2001
|
if (url.pathname.startsWith("/assets/")) {
|
|
1652
2002
|
try {
|
|
@@ -1660,13 +2010,36 @@ function createWebServer(options) {
|
|
|
1660
2010
|
}
|
|
1661
2011
|
}
|
|
1662
2012
|
if (options.handle) {
|
|
1663
|
-
const response = await options.handle(request);
|
|
2013
|
+
const response = await options.handle(request, server);
|
|
2014
|
+
if (response === undefined && websocket)
|
|
2015
|
+
return;
|
|
1664
2016
|
return response ?? await missingHtmlResponse();
|
|
1665
2017
|
}
|
|
1666
2018
|
return await missingHtmlResponse();
|
|
1667
|
-
});
|
|
2019
|
+
}));
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
const server = websocket ? Bun.serve({ ...configuration, websocket }) : Bun.serve({
|
|
2023
|
+
...configuration,
|
|
2024
|
+
async fetch(request, server) {
|
|
2025
|
+
return await configuration.fetch(request, server) ?? await missingHtmlResponse();
|
|
1668
2026
|
}
|
|
1669
2027
|
});
|
|
2028
|
+
if (options.lifecycle) {
|
|
2029
|
+
const lifecycle = options.lifecycle;
|
|
2030
|
+
const name = `http:${crypto.randomUUID()}`;
|
|
2031
|
+
let draining;
|
|
2032
|
+
lifecycle.register(`${name}:stop`, () => {
|
|
2033
|
+
draining ??= server.stop(false);
|
|
2034
|
+
draining.catch(() => {});
|
|
2035
|
+
}, "stop");
|
|
2036
|
+
lifecycle.register(`${name}:drain`, async () => {
|
|
2037
|
+
await draining;
|
|
2038
|
+
await Promise.allSettled([...active]);
|
|
2039
|
+
}, "drain");
|
|
2040
|
+
lifecycle.register(`${name}:force`, () => server.stop(true), "force");
|
|
2041
|
+
}
|
|
2042
|
+
return server;
|
|
1670
2043
|
}
|
|
1671
2044
|
// ../../src/bootstrap/web/session.ts
|
|
1672
2045
|
import { createHmac, randomBytes } from "crypto";
|
|
@@ -1945,8 +2318,10 @@ export {
|
|
|
1945
2318
|
DATABASE_URL_CONFIG_KEY,
|
|
1946
2319
|
DEFAULT_APP_PORT,
|
|
1947
2320
|
REDIS_URL_CONFIG_KEY,
|
|
2321
|
+
RedisSchedulerLeaseStore,
|
|
1948
2322
|
RouteRegistry,
|
|
1949
2323
|
Schedule,
|
|
2324
|
+
SchedulerLeaseLostError,
|
|
1950
2325
|
ServiceContainer,
|
|
1951
2326
|
appSchedule,
|
|
1952
2327
|
assertAppDependenciesComplete,
|
|
@@ -1958,6 +2333,7 @@ export {
|
|
|
1958
2333
|
checkRedis,
|
|
1959
2334
|
collectProviders,
|
|
1960
2335
|
configureModulesDirectory,
|
|
2336
|
+
configureModulesManifest,
|
|
1961
2337
|
coreProviders,
|
|
1962
2338
|
createAppContext,
|
|
1963
2339
|
createAppDependencies,
|
|
@@ -1970,6 +2346,7 @@ export {
|
|
|
1970
2346
|
createWebServer,
|
|
1971
2347
|
discoverJobs,
|
|
1972
2348
|
discoverModelTableNames,
|
|
2349
|
+
discoverModels,
|
|
1973
2350
|
discoverModules,
|
|
1974
2351
|
ensureModulesLoaded,
|
|
1975
2352
|
getRequiredDependency,
|
|
@@ -1998,6 +2375,7 @@ export {
|
|
|
1998
2375
|
setActiveApplicationContext,
|
|
1999
2376
|
slugify,
|
|
2000
2377
|
toRouteRequest,
|
|
2378
|
+
tracing_default as tracingProvider,
|
|
2001
2379
|
wrapSecuredRouteModelByKey,
|
|
2002
2380
|
wrapWebLogin,
|
|
2003
2381
|
wrapWebRegister
|