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