@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
|
@@ -25,8 +25,12 @@ import {
|
|
|
25
25
|
CORE_CACHE_TOKEN,
|
|
26
26
|
CORE_CONFIG_TOKEN,
|
|
27
27
|
CORE_EVENT_BUS_TOKEN,
|
|
28
|
+
CORE_HTTP_CLEANUP_TOKEN,
|
|
28
29
|
CORE_POLICY_GATE_TOKEN,
|
|
30
|
+
CORE_PUBLIC_TENANCY_TOKEN,
|
|
29
31
|
CORE_QUEUE_TOKEN,
|
|
32
|
+
CORE_TENANT_RESOLVER_TOKEN,
|
|
33
|
+
CORE_THROTTLE_QUOTA_POLICY_TOKEN,
|
|
30
34
|
CORE_TOKEN_SERVICE_TOKEN
|
|
31
35
|
} from "@getstrata/core/contracts/serviceTokens";
|
|
32
36
|
var APP_PORT_CONFIG_KEY = "app.port";
|
|
@@ -73,7 +77,7 @@ import { createCacheStore } from "@getstrata/core/cache/createCacheStore";
|
|
|
73
77
|
import { CacheRepository } from "@getstrata/core/cache/repository";
|
|
74
78
|
var cacheProvider = {
|
|
75
79
|
name: "core.cache",
|
|
76
|
-
register({ container, config, dependencies }) {
|
|
80
|
+
register({ container, config, dependencies, onCleanup }) {
|
|
77
81
|
container.singleton(CORE_CACHE_TOKEN, () => {
|
|
78
82
|
const store = createCacheStore({
|
|
79
83
|
driver: config.require(CACHE_DRIVER_CONFIG_KEY),
|
|
@@ -81,7 +85,9 @@ var cacheProvider = {
|
|
|
81
85
|
maxEntries: config.require(CACHE_MAX_ENTRIES_CONFIG_KEY),
|
|
82
86
|
redisUrl: config.get(REDIS_URL_CONFIG_KEY) || undefined
|
|
83
87
|
});
|
|
84
|
-
|
|
88
|
+
const cache = new CacheRepository(store);
|
|
89
|
+
onCleanup(() => cache.close());
|
|
90
|
+
return cache;
|
|
85
91
|
});
|
|
86
92
|
dependencies.cache = container.resolve(CORE_CACHE_TOKEN);
|
|
87
93
|
}
|
|
@@ -278,53 +284,101 @@ var eventsProvider = {
|
|
|
278
284
|
var events_default = eventsProvider;
|
|
279
285
|
|
|
280
286
|
// ../../src/bootstrap/discoverListeners.ts
|
|
281
|
-
import {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
import { join as join2, resolve } from "path";
|
|
288
|
+
|
|
289
|
+
// ../../src/bootstrap/infrastructureDiscovery.ts
|
|
290
|
+
import { readdir } from "fs/promises";
|
|
291
|
+
import { join, relative } from "path";
|
|
292
|
+
import { pathToFileURL } from "url";
|
|
293
|
+
async function infrastructureFiles(directory, exclude = []) {
|
|
294
|
+
const excluded = new Set([
|
|
295
|
+
"index.ts",
|
|
296
|
+
"index.js",
|
|
297
|
+
"index.mts",
|
|
298
|
+
"index.mjs",
|
|
299
|
+
"register.ts",
|
|
300
|
+
"register.js",
|
|
301
|
+
"register.mts",
|
|
302
|
+
"register.mjs",
|
|
303
|
+
...exclude
|
|
304
|
+
]);
|
|
305
|
+
const files = [];
|
|
306
|
+
async function visit(path) {
|
|
307
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
308
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
309
|
+
for (const entry of entries) {
|
|
310
|
+
const file = join(path, entry.name);
|
|
311
|
+
const key = relative(directory, file).replaceAll("\\", "/");
|
|
312
|
+
if (excluded.has(entry.name) || excluded.has(key))
|
|
313
|
+
continue;
|
|
314
|
+
if (entry.isDirectory())
|
|
315
|
+
await visit(file);
|
|
316
|
+
else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
|
|
317
|
+
files.push(file);
|
|
318
|
+
}
|
|
290
319
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return fromCwd;
|
|
320
|
+
try {
|
|
321
|
+
await visit(directory);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (error.code !== "ENOENT")
|
|
324
|
+
throw error;
|
|
325
|
+
if (error.path !== directory)
|
|
326
|
+
throw error;
|
|
299
327
|
}
|
|
300
|
-
return
|
|
328
|
+
return files;
|
|
301
329
|
}
|
|
302
|
-
function
|
|
303
|
-
const listenersDirectory = resolveListenersDirectory();
|
|
304
|
-
let entries;
|
|
330
|
+
async function importInfrastructure(file) {
|
|
305
331
|
try {
|
|
306
|
-
|
|
332
|
+
return await import(pathToFileURL(file).href);
|
|
307
333
|
} catch (error) {
|
|
308
|
-
|
|
309
|
-
|
|
334
|
+
throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ../../src/bootstrap/discoverListeners.ts
|
|
339
|
+
var KEY = Symbol.for("@getstrata/discoverListenersState");
|
|
340
|
+
function state() {
|
|
341
|
+
const globals = globalThis;
|
|
342
|
+
globals[KEY] ??= { loads: new Map };
|
|
343
|
+
return globals[KEY];
|
|
344
|
+
}
|
|
345
|
+
async function load(options) {
|
|
346
|
+
const listeners = new Set;
|
|
347
|
+
const add = (value, location) => {
|
|
348
|
+
if (typeof value !== "function")
|
|
349
|
+
throw new TypeError(`Invalid listener registrar at ${location}: expected a default function.`);
|
|
350
|
+
listeners.add(value);
|
|
351
|
+
};
|
|
352
|
+
if (options.manifest)
|
|
353
|
+
for (const [index, value] of options.manifest.entries())
|
|
354
|
+
add(value, `listener manifest[${index}]`);
|
|
355
|
+
else
|
|
356
|
+
for (const file of await infrastructureFiles(options.directory ?? join2(process.cwd(), "src", "listeners"), options.exclude)) {
|
|
357
|
+
const exports = await importInfrastructure(file);
|
|
358
|
+
const value = exports.default;
|
|
359
|
+
add(value && typeof value === "object" && "default" in value ? value.default : value, file);
|
|
310
360
|
}
|
|
361
|
+
return [...listeners];
|
|
362
|
+
}
|
|
363
|
+
function discoverListeners(options = {}) {
|
|
364
|
+
const cache = state().loads;
|
|
365
|
+
const key = options.manifest ?? JSON.stringify([
|
|
366
|
+
resolve(options.directory ?? join2(process.cwd(), "src", "listeners")),
|
|
367
|
+
[...options.exclude ?? []].sort()
|
|
368
|
+
]);
|
|
369
|
+
const existing = cache.get(key);
|
|
370
|
+
if (existing)
|
|
371
|
+
return existing;
|
|
372
|
+
const pending = load(options).catch((error) => {
|
|
373
|
+
if (cache.get(key) === pending)
|
|
374
|
+
cache.delete(key);
|
|
311
375
|
throw error;
|
|
312
|
-
}
|
|
313
|
-
const listeners = entries.map((fileName) => {
|
|
314
|
-
const filePath = join(listenersDirectory, fileName);
|
|
315
|
-
const loaded = requireListener(filePath);
|
|
316
|
-
return loaded.default;
|
|
317
376
|
});
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
function discoverListeners() {
|
|
321
|
-
const state = readDiscoverListenersState();
|
|
322
|
-
state.appListeners ??= loadDiscoveredListeners();
|
|
323
|
-
return state.appListeners;
|
|
377
|
+
cache.set(key, pending);
|
|
378
|
+
return pending;
|
|
324
379
|
}
|
|
325
380
|
function resetDiscoverListenersForTests() {
|
|
326
|
-
|
|
327
|
-
state.appListeners = undefined;
|
|
381
|
+
state().loads = new Map;
|
|
328
382
|
}
|
|
329
383
|
|
|
330
384
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
@@ -346,74 +400,129 @@ import {
|
|
|
346
400
|
} from "@getstrata/core/runtime/applicationRegistry";
|
|
347
401
|
|
|
348
402
|
// ../../src/bootstrap/discoverModules.ts
|
|
349
|
-
import {
|
|
350
|
-
import { join as
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
return existing;
|
|
357
|
-
}
|
|
358
|
-
const state = { appModules: [] };
|
|
359
|
-
globalThis[DISCOVER_MODULES_STATE_KEY] = state;
|
|
360
|
-
return state;
|
|
403
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
404
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
405
|
+
var KEY2 = Symbol.for("@getstrata/discoverModulesState");
|
|
406
|
+
function state2() {
|
|
407
|
+
const globals = globalThis;
|
|
408
|
+
globals[KEY2] ??= { appModules: [] };
|
|
409
|
+
return globals[KEY2];
|
|
361
410
|
}
|
|
362
411
|
function configureModulesDirectory(modulesDir) {
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
412
|
+
const current = state2();
|
|
413
|
+
const directory = resolve2(modulesDir);
|
|
414
|
+
if (current.configuredModulesDir !== directory || current.configuredManifest) {
|
|
415
|
+
current.appModules.length = 0;
|
|
416
|
+
current.modulesReady = undefined;
|
|
417
|
+
current.source = undefined;
|
|
367
418
|
}
|
|
368
|
-
|
|
419
|
+
current.configuredManifest = undefined;
|
|
420
|
+
current.configuredModulesDir = directory;
|
|
369
421
|
}
|
|
370
|
-
function
|
|
371
|
-
const
|
|
372
|
-
if (
|
|
373
|
-
|
|
422
|
+
function configureModulesManifest(manifest) {
|
|
423
|
+
const current = state2();
|
|
424
|
+
if (current.configuredManifest !== manifest) {
|
|
425
|
+
current.appModules.length = 0;
|
|
426
|
+
current.modulesReady = undefined;
|
|
427
|
+
current.source = undefined;
|
|
374
428
|
}
|
|
375
|
-
|
|
376
|
-
return state.configuredModulesDir;
|
|
377
|
-
}
|
|
378
|
-
throw new Error("configureModulesDirectory() must be called before discovering modules. The app preload (or starter) should call it.");
|
|
429
|
+
current.configuredManifest = manifest;
|
|
379
430
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
431
|
+
function validateModule(value, location) {
|
|
432
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
433
|
+
throw new TypeError(`Invalid application module at ${location}.`);
|
|
434
|
+
const module = value;
|
|
435
|
+
if (typeof module.name !== "string" || !module.name.trim())
|
|
436
|
+
throw new TypeError(`Invalid module name at ${location}.`);
|
|
437
|
+
if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
|
|
438
|
+
throw new TypeError(`Invalid module order at ${location}.`);
|
|
439
|
+
for (const hook of ["routes", "webRoutes"])
|
|
440
|
+
if (module[hook] !== undefined && typeof module[hook] !== "function")
|
|
441
|
+
throw new TypeError(`Invalid ${hook} at ${location}.`);
|
|
442
|
+
if (module.providers !== undefined) {
|
|
443
|
+
if (!Array.isArray(module.providers))
|
|
444
|
+
throw new TypeError(`Invalid providers at ${location}.`);
|
|
445
|
+
for (const provider of module.providers) {
|
|
446
|
+
if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
|
|
447
|
+
throw new TypeError(`Invalid provider at ${location}.`);
|
|
388
448
|
}
|
|
389
|
-
throw error;
|
|
390
449
|
}
|
|
391
|
-
const modules = await Promise.all(moduleNames.map(async (moduleName) => {
|
|
392
|
-
const moduleUrl = pathToFileURL(join2(modulesDirectory, moduleName, "index.ts")).href;
|
|
393
|
-
const loaded = await import(moduleUrl);
|
|
394
|
-
return loaded.default;
|
|
395
|
-
}));
|
|
396
|
-
return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
397
450
|
}
|
|
398
|
-
async function
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
451
|
+
async function load2(options) {
|
|
452
|
+
const manifest = options.manifest ?? (options.modulesDir === undefined ? state2().configuredManifest : undefined);
|
|
453
|
+
const entries = [];
|
|
454
|
+
if (manifest)
|
|
455
|
+
for (const [index, value] of manifest.entries())
|
|
456
|
+
entries.push({ value, location: `module manifest[${index}]` });
|
|
457
|
+
else {
|
|
458
|
+
const directory = options.modulesDir ?? state2().configuredModulesDir;
|
|
459
|
+
if (!directory)
|
|
460
|
+
throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
|
|
461
|
+
let folders;
|
|
462
|
+
try {
|
|
463
|
+
folders = await readdir2(directory, { withFileTypes: true });
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (error.code === "ENOENT")
|
|
466
|
+
return [];
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
const excluded = new Set(options.exclude ?? []);
|
|
470
|
+
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)) {
|
|
471
|
+
const path = join3(directory, folder.name);
|
|
472
|
+
const files = await readdir2(path);
|
|
473
|
+
const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
|
|
474
|
+
if (candidates.length !== 1)
|
|
475
|
+
throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
|
|
476
|
+
const file = join3(path, candidates[0]);
|
|
477
|
+
const exports = await importInfrastructure(file);
|
|
478
|
+
entries.push({ value: exports.default, location: file });
|
|
479
|
+
}
|
|
402
480
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
481
|
+
const names = new Map;
|
|
482
|
+
const modules = [];
|
|
483
|
+
for (const { value, location } of entries) {
|
|
484
|
+
validateModule(value, location);
|
|
485
|
+
const previous = names.get(value.name);
|
|
486
|
+
if (previous)
|
|
487
|
+
throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
|
|
488
|
+
names.set(value.name, location);
|
|
489
|
+
modules.push(value);
|
|
490
|
+
}
|
|
491
|
+
return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
492
|
+
}
|
|
493
|
+
function ensureModulesLoaded(options = {}) {
|
|
494
|
+
const current = state2();
|
|
495
|
+
const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
|
|
496
|
+
options.modulesDir ? resolve2(options.modulesDir) : current.configuredModulesDir,
|
|
497
|
+
[...options.exclude ?? []].sort()
|
|
498
|
+
]);
|
|
499
|
+
if (current.modulesReady && current.source === key)
|
|
500
|
+
return current.modulesReady;
|
|
501
|
+
current.appModules.length = 0;
|
|
502
|
+
current.source = key;
|
|
503
|
+
const pending = load2(options).then((modules) => {
|
|
504
|
+
if (current.modulesReady !== pending)
|
|
505
|
+
return modules;
|
|
506
|
+
current.appModules.splice(0, current.appModules.length, ...modules);
|
|
507
|
+
return current.appModules;
|
|
508
|
+
}).catch((error) => {
|
|
509
|
+
if (current.modulesReady === pending)
|
|
510
|
+
current.modulesReady = undefined;
|
|
511
|
+
throw error;
|
|
406
512
|
});
|
|
407
|
-
|
|
513
|
+
current.modulesReady = pending;
|
|
514
|
+
return pending;
|
|
408
515
|
}
|
|
409
516
|
function discoverModules() {
|
|
410
|
-
return
|
|
517
|
+
return state2().appModules;
|
|
411
518
|
}
|
|
412
519
|
function resetDiscoverModulesForTests() {
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
520
|
+
const current = state2();
|
|
521
|
+
current.configuredModulesDir = undefined;
|
|
522
|
+
current.configuredManifest = undefined;
|
|
523
|
+
current.appModules.length = 0;
|
|
524
|
+
current.modulesReady = undefined;
|
|
525
|
+
current.source = undefined;
|
|
417
526
|
}
|
|
418
527
|
|
|
419
528
|
// ../../src/bootstrap/cache/modelCacheTags.ts
|
|
@@ -530,22 +639,12 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options =
|
|
|
530
639
|
}
|
|
531
640
|
|
|
532
641
|
// ../../src/bootstrap/providers/listeners.ts
|
|
533
|
-
var registeredListenerGroups = new Set;
|
|
534
|
-
function registerListenerGroup(name, register) {
|
|
535
|
-
if (registeredListenerGroups.has(name)) {
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
registeredListenerGroups.add(name);
|
|
539
|
-
register();
|
|
540
|
-
}
|
|
541
642
|
var listenersProvider = {
|
|
542
643
|
name: "core.listeners",
|
|
543
|
-
boot() {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
for (const registerListener of discoverListeners()) {
|
|
548
|
-
registerListener();
|
|
644
|
+
async boot({ onCleanup }) {
|
|
645
|
+
onCleanup(registerInvalidateCacheOnModelWriteListeners());
|
|
646
|
+
for (const registerListener of await discoverListeners()) {
|
|
647
|
+
await registerListener();
|
|
549
648
|
}
|
|
550
649
|
}
|
|
551
650
|
};
|
|
@@ -569,68 +668,99 @@ import {
|
|
|
569
668
|
} from "@getstrata/core/queue/createAppQueue";
|
|
570
669
|
|
|
571
670
|
// ../../src/bootstrap/discoverJobs.ts
|
|
572
|
-
import {
|
|
573
|
-
import {
|
|
574
|
-
import { join as join3 } from "path";
|
|
671
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
672
|
+
import { Job } from "@getstrata/core/queue";
|
|
575
673
|
import { jobRegistry } from "@getstrata/core/queue/jobRegistry";
|
|
576
|
-
var
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
return existing;
|
|
582
|
-
}
|
|
583
|
-
const state = {};
|
|
584
|
-
globalThis[DISCOVER_JOBS_STATE_KEY] = state;
|
|
585
|
-
return state;
|
|
586
|
-
}
|
|
587
|
-
function resolveJobsDirectory() {
|
|
588
|
-
const fromCwd = join3(process.cwd(), "src", "jobs");
|
|
589
|
-
if (existsSync2(fromCwd)) {
|
|
590
|
-
return fromCwd;
|
|
591
|
-
}
|
|
592
|
-
return join3(import.meta.dir, "../jobs");
|
|
674
|
+
var KEY3 = Symbol.for("@getstrata/discoverJobsState");
|
|
675
|
+
function state3() {
|
|
676
|
+
const globals = globalThis;
|
|
677
|
+
globals[KEY3] ??= { loads: new Map, registered: new Map };
|
|
678
|
+
return globals[KEY3];
|
|
593
679
|
}
|
|
594
|
-
function
|
|
595
|
-
if (typeof value !== "function")
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
const jobName = value.jobName;
|
|
599
|
-
return typeof jobName === "string" && jobName.length > 0;
|
|
680
|
+
function validateClass(value, location) {
|
|
681
|
+
if (typeof value !== "function" || typeof value.jobName !== "string" || !value.jobName.trim() || !(value.prototype instanceof Job) && typeof value.prototype?.handle !== "function")
|
|
682
|
+
throw new TypeError(`Invalid job at ${location}: expected a class with nonempty static jobName and handle().`);
|
|
600
683
|
}
|
|
601
|
-
function
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (
|
|
608
|
-
|
|
684
|
+
async function load3(options, current) {
|
|
685
|
+
const jobs = new Map;
|
|
686
|
+
const add = (name, identity, create, location) => {
|
|
687
|
+
const previous = jobs.get(name);
|
|
688
|
+
if (previous && previous.identity !== identity)
|
|
689
|
+
throw new Error(`Duplicate job "${name}" at ${location}; first declared at ${previous.location}.`);
|
|
690
|
+
if (!previous)
|
|
691
|
+
jobs.set(name, { identity, create, location });
|
|
692
|
+
};
|
|
693
|
+
if (options.manifest) {
|
|
694
|
+
for (const [index, entry] of options.manifest.entries()) {
|
|
695
|
+
const location = `job manifest[${index}]`;
|
|
696
|
+
if (typeof entry === "function") {
|
|
697
|
+
validateClass(entry, location);
|
|
698
|
+
add(entry.jobName, entry, () => new entry, location);
|
|
699
|
+
} else {
|
|
700
|
+
if (!entry || typeof entry.name !== "string" || !entry.name.trim() || typeof entry.create !== "function")
|
|
701
|
+
throw new TypeError(`Invalid job factory at ${location}.`);
|
|
702
|
+
add(entry.name, entry.create, entry.create, location);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
} else {
|
|
706
|
+
for (const file of await infrastructureFiles(options.directory ?? join4(process.cwd(), "src", "jobs"), options.exclude)) {
|
|
707
|
+
const exports = await importInfrastructure(file);
|
|
708
|
+
const defaultValue = exports.default;
|
|
709
|
+
const legacy = defaultValue && typeof defaultValue === "object" && "default" in defaultValue ? defaultValue.default : defaultValue;
|
|
710
|
+
if (legacy !== undefined)
|
|
711
|
+
validateClass(legacy, `${file}:default`);
|
|
712
|
+
let found = false;
|
|
713
|
+
for (const [name, value] of Object.entries({
|
|
714
|
+
...exports,
|
|
715
|
+
...legacy === undefined ? {} : { default: legacy }
|
|
716
|
+
})) {
|
|
717
|
+
if (name !== "default" && !(typeof value === "function" && ("jobName" in value)))
|
|
718
|
+
continue;
|
|
719
|
+
validateClass(value, `${file}:${name}`);
|
|
720
|
+
found = true;
|
|
721
|
+
add(value.jobName, value, () => new value, `${file}:${name}`);
|
|
722
|
+
}
|
|
723
|
+
if (!found)
|
|
724
|
+
throw new TypeError(`No job exports at ${file}; exclude helper modules explicitly.`);
|
|
609
725
|
}
|
|
610
|
-
throw error;
|
|
611
726
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const state = readDiscoverJobsState();
|
|
620
|
-
if (state.names) {
|
|
621
|
-
return state.names;
|
|
727
|
+
if (!current())
|
|
728
|
+
return [...jobs.keys()];
|
|
729
|
+
const registered = state3().registered;
|
|
730
|
+
for (const [name, entry] of jobs) {
|
|
731
|
+
const previous = registered.get(name);
|
|
732
|
+
if (previous && previous.identity !== entry.identity)
|
|
733
|
+
throw new Error(`Duplicate discovered job "${name}" at ${entry.location}; first declared at ${previous.location}.`);
|
|
622
734
|
}
|
|
623
|
-
const
|
|
624
|
-
for (const
|
|
625
|
-
|
|
626
|
-
|
|
735
|
+
const existing = new Set(jobRegistry.names());
|
|
736
|
+
for (const [name, entry] of jobs) {
|
|
737
|
+
if (!existing.has(name)) {
|
|
738
|
+
jobRegistry.register(name, entry.create);
|
|
739
|
+
registered.set(name, entry);
|
|
740
|
+
}
|
|
627
741
|
}
|
|
628
|
-
|
|
629
|
-
|
|
742
|
+
return [...jobs.keys()];
|
|
743
|
+
}
|
|
744
|
+
function discoverJobs(options = {}) {
|
|
745
|
+
const cache = state3().loads;
|
|
746
|
+
const key = options.manifest ?? JSON.stringify([
|
|
747
|
+
resolve3(options.directory ?? join4(process.cwd(), "src", "jobs")),
|
|
748
|
+
[...options.exclude ?? []].sort()
|
|
749
|
+
]);
|
|
750
|
+
const existing = cache.get(key);
|
|
751
|
+
if (existing)
|
|
752
|
+
return existing;
|
|
753
|
+
const pending = load3(options, () => state3().loads === cache).catch((error) => {
|
|
754
|
+
if (cache.get(key) === pending)
|
|
755
|
+
cache.delete(key);
|
|
756
|
+
throw error;
|
|
757
|
+
});
|
|
758
|
+
cache.set(key, pending);
|
|
759
|
+
return pending;
|
|
630
760
|
}
|
|
631
761
|
function resetDiscoverJobsForTests() {
|
|
632
|
-
|
|
633
|
-
|
|
762
|
+
state3().loads = new Map;
|
|
763
|
+
state3().registered = new Map;
|
|
634
764
|
}
|
|
635
765
|
|
|
636
766
|
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
@@ -648,16 +778,17 @@ function registerDefaultJobs() {
|
|
|
648
778
|
// ../../src/bootstrap/providers/queue.ts
|
|
649
779
|
var queueProvider = {
|
|
650
780
|
name: "core.queue",
|
|
651
|
-
register({ container, config }) {
|
|
781
|
+
async register({ container, config, onCleanup }) {
|
|
652
782
|
const configuredDriver = process.env.QUEUE_DRIVER ?? DEFAULT_QUEUE_DRIVER;
|
|
653
783
|
const driver = configuredDriver === "async" || configuredDriver === "redis" || configuredDriver === "sync" ? configuredDriver : DEFAULT_QUEUE_DRIVER;
|
|
654
784
|
config.set("queue.driver", driver);
|
|
655
785
|
const failedJobs = createFailedJobService();
|
|
656
786
|
container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
787
|
+
registerDefaultJobs();
|
|
788
|
+
await discoverJobs();
|
|
789
|
+
const queue = createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs);
|
|
790
|
+
onCleanup(() => queue.close?.(), driver === "async" ? "drain" : "close");
|
|
791
|
+
container.set(CORE_QUEUE_TOKEN, queue);
|
|
661
792
|
}
|
|
662
793
|
};
|
|
663
794
|
var queue_default = queueProvider;
|
|
@@ -672,6 +803,18 @@ var storageProvider = {
|
|
|
672
803
|
};
|
|
673
804
|
var storage_default = storageProvider;
|
|
674
805
|
|
|
806
|
+
// ../../src/bootstrap/providers/tracing.ts
|
|
807
|
+
import { acquireTracingRuntime } from "@getstrata/core/tracing/tracingMiddleware";
|
|
808
|
+
var tracingProvider = {
|
|
809
|
+
name: "core.tracing",
|
|
810
|
+
register({ onCleanup }) {
|
|
811
|
+
const ownership = acquireTracingRuntime();
|
|
812
|
+
onCleanup(() => ownership.flush(), "flush");
|
|
813
|
+
onCleanup(() => ownership.release(), "close");
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
var tracing_default = tracingProvider;
|
|
817
|
+
|
|
675
818
|
// ../../src/bootstrap/providers/view.ts
|
|
676
819
|
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
677
820
|
import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
|
|
@@ -716,6 +859,7 @@ var viewProvider = {
|
|
|
716
859
|
// ../../src/bootstrap/providers/index.ts
|
|
717
860
|
var coreProviders = [
|
|
718
861
|
config_default,
|
|
862
|
+
tracing_default,
|
|
719
863
|
cache_default,
|
|
720
864
|
storage_default,
|
|
721
865
|
auth_default,
|
|
@@ -126,8 +126,8 @@ function assertFeatureProductionSecrets(env) {
|
|
|
126
126
|
throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
-
if (envFlagEnabled(env.
|
|
130
|
-
throw new Error("Production startup blocked: set
|
|
129
|
+
if (envFlagEnabled(env.TENANT_DEV_HEADERS)) {
|
|
130
|
+
throw new Error("Production startup blocked: set TENANT_DEV_HEADERS=false.");
|
|
131
131
|
}
|
|
132
132
|
if (!env.SIEM_EXPORT_URL?.trim() && envFlagEnabled(env.FEATURE_SIEM_EXPORT)) {
|
|
133
133
|
console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
|