@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/dist/bootstrap/app.d.ts +7 -1
  4. package/dist/bootstrap/config.d.ts +1 -1
  5. package/dist/bootstrap/context.d.ts +8 -2
  6. package/dist/bootstrap/contracts.d.ts +1 -1
  7. package/dist/bootstrap/dependencies.d.ts +1 -1
  8. package/dist/bootstrap/discoverJobs.d.ts +11 -3
  9. package/dist/bootstrap/discoverListeners.d.ts +7 -3
  10. package/dist/bootstrap/discoverModels.d.ts +8 -0
  11. package/dist/bootstrap/discoverModules.d.ts +4 -1
  12. package/dist/bootstrap/httpKernel.d.ts +25 -20
  13. package/dist/bootstrap/inProcessCron.d.ts +2 -1
  14. package/dist/bootstrap/infrastructureDiscovery.d.ts +9 -0
  15. package/dist/bootstrap/modules.d.ts +1 -1
  16. package/dist/bootstrap/providers/tracing.d.ts +3 -0
  17. package/dist/bootstrap/public-api.d.ts +9 -4
  18. package/dist/bootstrap/web/index.d.ts +1 -1
  19. package/dist/bootstrap/web/routing.d.ts +2 -2
  20. package/dist/bootstrap/web/server.d.ts +15 -7
  21. package/dist/entries/buildModuleRoutes.js +212 -67
  22. package/dist/entries/buildWebModuleRoutes.js +212 -67
  23. package/dist/entries/cache/modelCacheTags.js +157 -51
  24. package/dist/entries/config.js +6 -0
  25. package/dist/entries/context.js +427 -188
  26. package/dist/entries/contracts.js +2 -0
  27. package/dist/entries/createWebRoutes.js +214 -69
  28. package/dist/entries/dependencies.js +429 -190
  29. package/dist/entries/discoverJobs.js +132 -50
  30. package/dist/entries/discoverListeners.js +85 -37
  31. package/dist/entries/discoverModels.js +39 -0
  32. package/dist/entries/discoverModules.js +158 -51
  33. package/dist/entries/health.js +3 -0
  34. package/dist/entries/httpKernel.js +52 -13
  35. package/dist/entries/listeners/invalidateCacheOnModelWrite.js +157 -51
  36. package/dist/entries/providers.js +307 -164
  37. package/dist/entries/web/routing.js +52 -13
  38. package/dist/entries/web/server.js +69 -14
  39. package/dist/index.js +591 -216
  40. package/package.json +8 -3
@@ -1,4 +1,8 @@
1
1
  // @bun
2
+ // ../../src/bootstrap/context.ts
3
+ import { CORE_HTTP_CLEANUP_TOKEN as CORE_HTTP_CLEANUP_TOKEN2 } from "@getstrata/core/contracts/serviceTokens";
4
+ import { clearActiveApplicationContext } from "@getstrata/core/runtime/applicationRegistry";
5
+
2
6
  // ../../src/bootstrap/applicationRegistry.ts
3
7
  import {
4
8
  resolveApplicationAuth,
@@ -15,6 +19,7 @@ import {
15
19
  // ../../src/bootstrap/contracts.ts
16
20
  import {
17
21
  ConfigStore,
22
+ createServiceToken,
18
23
  ServiceContainer
19
24
  } from "@getstrata/core/contracts/container";
20
25
  import {
@@ -24,74 +29,180 @@ import {
24
29
  } from "@getstrata/core/contracts/di";
25
30
 
26
31
  // ../../src/bootstrap/discoverModules.ts
27
- import { readdirSync } from "fs";
28
- import { join } from "path";
32
+ import { readdir as readdir2 } from "fs/promises";
33
+ import { join as join2, resolve } from "path";
34
+
35
+ // ../../src/bootstrap/infrastructureDiscovery.ts
36
+ import { readdir } from "fs/promises";
37
+ import { join, relative } from "path";
29
38
  import { pathToFileURL } from "url";
30
- var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
31
- function readDiscoverModulesState() {
32
- const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
33
- if (existing) {
34
- return existing;
39
+ async function infrastructureFiles(directory, exclude = []) {
40
+ const excluded = new Set([
41
+ "index.ts",
42
+ "index.js",
43
+ "index.mts",
44
+ "index.mjs",
45
+ "register.ts",
46
+ "register.js",
47
+ "register.mts",
48
+ "register.mjs",
49
+ ...exclude
50
+ ]);
51
+ const files = [];
52
+ async function visit(path) {
53
+ const entries = await readdir(path, { withFileTypes: true });
54
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
55
+ for (const entry of entries) {
56
+ const file = join(path, entry.name);
57
+ const key = relative(directory, file).replaceAll("\\", "/");
58
+ if (excluded.has(entry.name) || excluded.has(key))
59
+ continue;
60
+ if (entry.isDirectory())
61
+ await visit(file);
62
+ else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
63
+ files.push(file);
64
+ }
35
65
  }
36
- const state = { appModules: [] };
37
- globalThis[DISCOVER_MODULES_STATE_KEY] = state;
38
- return state;
66
+ try {
67
+ await visit(directory);
68
+ } catch (error) {
69
+ if (error.code !== "ENOENT")
70
+ throw error;
71
+ if (error.path !== directory)
72
+ throw error;
73
+ }
74
+ return files;
75
+ }
76
+ async function importInfrastructure(file) {
77
+ try {
78
+ return await import(pathToFileURL(file).href);
79
+ } catch (error) {
80
+ throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
81
+ }
82
+ }
83
+
84
+ // ../../src/bootstrap/discoverModules.ts
85
+ var KEY = Symbol.for("@getstrata/discoverModulesState");
86
+ function state() {
87
+ const globals = globalThis;
88
+ globals[KEY] ??= { appModules: [] };
89
+ return globals[KEY];
39
90
  }
40
91
  function configureModulesDirectory(modulesDir) {
41
- const state = readDiscoverModulesState();
42
- if (state.configuredModulesDir !== modulesDir) {
43
- state.appModules.length = 0;
44
- state.modulesReady = undefined;
92
+ const current = state();
93
+ const directory = resolve(modulesDir);
94
+ if (current.configuredModulesDir !== directory || current.configuredManifest) {
95
+ current.appModules.length = 0;
96
+ current.modulesReady = undefined;
97
+ current.source = undefined;
45
98
  }
46
- state.configuredModulesDir = modulesDir;
99
+ current.configuredManifest = undefined;
100
+ current.configuredModulesDir = directory;
47
101
  }
48
- function resolveModulesDirectory(options) {
49
- const state = readDiscoverModulesState();
50
- if (options?.modulesDir) {
51
- return options.modulesDir;
102
+ function configureModulesManifest(manifest) {
103
+ const current = state();
104
+ if (current.configuredManifest !== manifest) {
105
+ current.appModules.length = 0;
106
+ current.modulesReady = undefined;
107
+ current.source = undefined;
52
108
  }
53
- if (state.configuredModulesDir) {
54
- return state.configuredModulesDir;
109
+ current.configuredManifest = manifest;
110
+ }
111
+ function validateModule(value, location) {
112
+ if (!value || typeof value !== "object" || Array.isArray(value))
113
+ throw new TypeError(`Invalid application module at ${location}.`);
114
+ const module = value;
115
+ if (typeof module.name !== "string" || !module.name.trim())
116
+ throw new TypeError(`Invalid module name at ${location}.`);
117
+ if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
118
+ throw new TypeError(`Invalid module order at ${location}.`);
119
+ for (const hook of ["routes", "webRoutes"])
120
+ if (module[hook] !== undefined && typeof module[hook] !== "function")
121
+ throw new TypeError(`Invalid ${hook} at ${location}.`);
122
+ if (module.providers !== undefined) {
123
+ if (!Array.isArray(module.providers))
124
+ throw new TypeError(`Invalid providers at ${location}.`);
125
+ for (const provider of module.providers) {
126
+ if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
127
+ throw new TypeError(`Invalid provider at ${location}.`);
128
+ }
55
129
  }
56
- throw new Error("configureModulesDirectory() must be called before discovering modules. The app preload (or starter) should call it.");
57
130
  }
58
- async function loadDiscoveredModules(options) {
59
- const modulesDirectory = resolveModulesDirectory(options);
60
- let moduleNames;
61
- try {
62
- moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
63
- } catch (error) {
64
- if (error.code === "ENOENT") {
65
- return [];
131
+ async function load(options) {
132
+ const manifest = options.manifest ?? (options.modulesDir === undefined ? state().configuredManifest : undefined);
133
+ const entries = [];
134
+ if (manifest)
135
+ for (const [index, value] of manifest.entries())
136
+ entries.push({ value, location: `module manifest[${index}]` });
137
+ else {
138
+ const directory = options.modulesDir ?? state().configuredModulesDir;
139
+ if (!directory)
140
+ throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
141
+ let folders;
142
+ try {
143
+ folders = await readdir2(directory, { withFileTypes: true });
144
+ } catch (error) {
145
+ if (error.code === "ENOENT")
146
+ return [];
147
+ throw error;
148
+ }
149
+ const excluded = new Set(options.exclude ?? []);
150
+ 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)) {
151
+ const path = join2(directory, folder.name);
152
+ const files = await readdir2(path);
153
+ const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
154
+ if (candidates.length !== 1)
155
+ throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
156
+ const file = join2(path, candidates[0]);
157
+ const exports = await importInfrastructure(file);
158
+ entries.push({ value: exports.default, location: file });
66
159
  }
67
- throw error;
68
160
  }
69
- const modules = await Promise.all(moduleNames.map(async (moduleName) => {
70
- const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
71
- const loaded = await import(moduleUrl);
72
- return loaded.default;
73
- }));
74
- return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
75
- }
76
- async function ensureModulesLoaded(options) {
77
- const state = readDiscoverModulesState();
78
- if (state.appModules.length > 0) {
79
- return state.appModules;
161
+ const names = new Map;
162
+ const modules = [];
163
+ for (const { value, location } of entries) {
164
+ validateModule(value, location);
165
+ const previous = names.get(value.name);
166
+ if (previous)
167
+ throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
168
+ names.set(value.name, location);
169
+ modules.push(value);
80
170
  }
81
- state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
82
- state.appModules.splice(0, state.appModules.length, ...modules);
83
- return state.appModules;
171
+ return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
172
+ }
173
+ function ensureModulesLoaded(options = {}) {
174
+ const current = state();
175
+ const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
176
+ options.modulesDir ? resolve(options.modulesDir) : current.configuredModulesDir,
177
+ [...options.exclude ?? []].sort()
178
+ ]);
179
+ if (current.modulesReady && current.source === key)
180
+ return current.modulesReady;
181
+ current.appModules.length = 0;
182
+ current.source = key;
183
+ const pending = load(options).then((modules) => {
184
+ if (current.modulesReady !== pending)
185
+ return modules;
186
+ current.appModules.splice(0, current.appModules.length, ...modules);
187
+ return current.appModules;
188
+ }).catch((error) => {
189
+ if (current.modulesReady === pending)
190
+ current.modulesReady = undefined;
191
+ throw error;
84
192
  });
85
- return state.modulesReady;
193
+ current.modulesReady = pending;
194
+ return pending;
86
195
  }
87
196
  function discoverModules() {
88
- return readDiscoverModulesState().appModules;
197
+ return state().appModules;
89
198
  }
90
199
  function resetDiscoverModulesForTests() {
91
- const state = readDiscoverModulesState();
92
- state.configuredModulesDir = undefined;
93
- state.appModules.length = 0;
94
- state.modulesReady = undefined;
200
+ const current = state();
201
+ current.configuredModulesDir = undefined;
202
+ current.configuredManifest = undefined;
203
+ current.appModules.length = 0;
204
+ current.modulesReady = undefined;
205
+ current.source = undefined;
95
206
  }
96
207
  // ../../src/bootstrap/providers/auth.ts
97
208
  import { BasicAuthGuard } from "@getstrata/core/auth/basicAuthGuard";
@@ -119,8 +230,11 @@ import {
119
230
  CORE_CACHE_TOKEN,
120
231
  CORE_CONFIG_TOKEN,
121
232
  CORE_EVENT_BUS_TOKEN,
233
+ CORE_HTTP_CLEANUP_TOKEN,
122
234
  CORE_POLICY_GATE_TOKEN,
123
235
  CORE_QUEUE_TOKEN,
236
+ CORE_TENANT_RESOLVER_TOKEN,
237
+ CORE_THROTTLE_QUOTA_POLICY_TOKEN,
124
238
  CORE_TOKEN_SERVICE_TOKEN
125
239
  } from "@getstrata/core/contracts/serviceTokens";
126
240
  var APP_PORT_CONFIG_KEY = "app.port";
@@ -167,7 +281,7 @@ import { createCacheStore } from "@getstrata/core/cache/createCacheStore";
167
281
  import { CacheRepository } from "@getstrata/core/cache/repository";
168
282
  var cacheProvider = {
169
283
  name: "core.cache",
170
- register({ container, config, dependencies }) {
284
+ register({ container, config, dependencies, onCleanup }) {
171
285
  container.singleton(CORE_CACHE_TOKEN, () => {
172
286
  const store = createCacheStore({
173
287
  driver: config.require(CACHE_DRIVER_CONFIG_KEY),
@@ -175,7 +289,9 @@ var cacheProvider = {
175
289
  maxEntries: config.require(CACHE_MAX_ENTRIES_CONFIG_KEY),
176
290
  redisUrl: config.get(REDIS_URL_CONFIG_KEY) || undefined
177
291
  });
178
- return new CacheRepository(store);
292
+ const cache = new CacheRepository(store);
293
+ onCleanup(() => cache.close());
294
+ return cache;
179
295
  });
180
296
  dependencies.cache = container.resolve(CORE_CACHE_TOKEN);
181
297
  }
@@ -372,53 +488,50 @@ var eventsProvider = {
372
488
  var events_default = eventsProvider;
373
489
 
374
490
  // ../../src/bootstrap/discoverListeners.ts
375
- import { existsSync, readdirSync as readdirSync2 } from "fs";
376
- import { createRequire } from "module";
377
- import { join as join2 } from "path";
378
- var requireListener = createRequire(import.meta.url);
379
- var DISCOVER_LISTENERS_STATE_KEY = Symbol.for("@getstrata/discoverListenersState");
380
- function readDiscoverListenersState() {
381
- const existing = globalThis[DISCOVER_LISTENERS_STATE_KEY];
382
- if (existing) {
383
- return existing;
384
- }
385
- const state = {};
386
- globalThis[DISCOVER_LISTENERS_STATE_KEY] = state;
387
- return state;
388
- }
389
- function resolveListenersDirectory() {
390
- const fromCwd = join2(process.cwd(), "src", "listeners");
391
- if (existsSync(fromCwd)) {
392
- return fromCwd;
393
- }
394
- return join2(import.meta.dir, "../listeners");
491
+ import { join as join3, resolve as resolve2 } from "path";
492
+ var KEY2 = Symbol.for("@getstrata/discoverListenersState");
493
+ function state2() {
494
+ const globals = globalThis;
495
+ globals[KEY2] ??= { loads: new Map };
496
+ return globals[KEY2];
395
497
  }
396
- function loadDiscoveredListeners() {
397
- const listenersDirectory = resolveListenersDirectory();
398
- let entries;
399
- try {
400
- entries = readdirSync2(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.(ts|js)$/.test(entry.name)).map((entry) => entry.name);
401
- } catch (error) {
402
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
403
- return [];
498
+ async function load2(options) {
499
+ const listeners = new Set;
500
+ const add = (value, location) => {
501
+ if (typeof value !== "function")
502
+ throw new TypeError(`Invalid listener registrar at ${location}: expected a default function.`);
503
+ listeners.add(value);
504
+ };
505
+ if (options.manifest)
506
+ for (const [index, value] of options.manifest.entries())
507
+ add(value, `listener manifest[${index}]`);
508
+ else
509
+ for (const file of await infrastructureFiles(options.directory ?? join3(process.cwd(), "src", "listeners"), options.exclude)) {
510
+ const exports = await importInfrastructure(file);
511
+ const value = exports.default;
512
+ add(value && typeof value === "object" && "default" in value ? value.default : value, file);
404
513
  }
514
+ return [...listeners];
515
+ }
516
+ function discoverListeners(options = {}) {
517
+ const cache = state2().loads;
518
+ const key = options.manifest ?? JSON.stringify([
519
+ resolve2(options.directory ?? join3(process.cwd(), "src", "listeners")),
520
+ [...options.exclude ?? []].sort()
521
+ ]);
522
+ const existing = cache.get(key);
523
+ if (existing)
524
+ return existing;
525
+ const pending = load2(options).catch((error) => {
526
+ if (cache.get(key) === pending)
527
+ cache.delete(key);
405
528
  throw error;
406
- }
407
- const listeners = entries.map((fileName) => {
408
- const filePath = join2(listenersDirectory, fileName);
409
- const loaded = requireListener(filePath);
410
- return loaded.default;
411
529
  });
412
- return listeners.filter((listener) => typeof listener === "function");
413
- }
414
- function discoverListeners() {
415
- const state = readDiscoverListenersState();
416
- state.appListeners ??= loadDiscoveredListeners();
417
- return state.appListeners;
530
+ cache.set(key, pending);
531
+ return pending;
418
532
  }
419
533
  function resetDiscoverListenersForTests() {
420
- const state = readDiscoverListenersState();
421
- state.appListeners = undefined;
534
+ state2().loads = new Map;
422
535
  }
423
536
 
424
537
  // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
@@ -540,22 +653,12 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options =
540
653
  }
541
654
 
542
655
  // ../../src/bootstrap/providers/listeners.ts
543
- var registeredListenerGroups = new Set;
544
- function registerListenerGroup(name, register) {
545
- if (registeredListenerGroups.has(name)) {
546
- return;
547
- }
548
- registeredListenerGroups.add(name);
549
- register();
550
- }
551
656
  var listenersProvider = {
552
657
  name: "core.listeners",
553
- boot() {
554
- registerListenerGroup("cache.invalidate-on-model-write", () => {
555
- registerInvalidateCacheOnModelWriteListeners();
556
- });
557
- for (const registerListener of discoverListeners()) {
558
- registerListener();
658
+ async boot({ onCleanup }) {
659
+ onCleanup(registerInvalidateCacheOnModelWriteListeners());
660
+ for (const registerListener of await discoverListeners()) {
661
+ await registerListener();
559
662
  }
560
663
  }
561
664
  };
@@ -579,68 +682,99 @@ import {
579
682
  } from "@getstrata/core/queue/createAppQueue";
580
683
 
581
684
  // ../../src/bootstrap/discoverJobs.ts
582
- import { existsSync as existsSync2, readdirSync as readdirSync3 } from "fs";
583
- import { createRequire as createRequire2 } from "module";
584
- import { join as join3 } from "path";
685
+ import { join as join4, resolve as resolve3 } from "path";
686
+ import { Job } from "@getstrata/core/queue";
585
687
  import { jobRegistry } from "@getstrata/core/queue/jobRegistry";
586
- var requireJob = createRequire2(import.meta.url);
587
- var DISCOVER_JOBS_STATE_KEY = Symbol.for("@getstrata/discoverJobsState");
588
- function readDiscoverJobsState() {
589
- const existing = globalThis[DISCOVER_JOBS_STATE_KEY];
590
- if (existing) {
591
- return existing;
592
- }
593
- const state = {};
594
- globalThis[DISCOVER_JOBS_STATE_KEY] = state;
595
- return state;
596
- }
597
- function resolveJobsDirectory() {
598
- const fromCwd = join3(process.cwd(), "src", "jobs");
599
- if (existsSync2(fromCwd)) {
600
- return fromCwd;
601
- }
602
- return join3(import.meta.dir, "../jobs");
688
+ var KEY3 = Symbol.for("@getstrata/discoverJobsState");
689
+ function state3() {
690
+ const globals = globalThis;
691
+ globals[KEY3] ??= { loads: new Map, registered: new Map };
692
+ return globals[KEY3];
603
693
  }
604
- function isDiscoveredJobClass(value) {
605
- if (typeof value !== "function") {
606
- return false;
607
- }
608
- const jobName = value.jobName;
609
- return typeof jobName === "string" && jobName.length > 0;
694
+ function validateClass(value, location) {
695
+ if (typeof value !== "function" || typeof value.jobName !== "string" || !value.jobName.trim() || !(value.prototype instanceof Job) && typeof value.prototype?.handle !== "function")
696
+ throw new TypeError(`Invalid job at ${location}: expected a class with nonempty static jobName and handle().`);
610
697
  }
611
- function loadDiscoveredJobClasses() {
612
- const jobsDirectory = resolveJobsDirectory();
613
- let entries;
614
- try {
615
- entries = readdirSync3(jobsDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.(ts|js)$/.test(entry.name)).map((entry) => entry.name);
616
- } catch (error) {
617
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
618
- return [];
698
+ async function load3(options, current) {
699
+ const jobs = new Map;
700
+ const add = (name, identity, create, location) => {
701
+ const previous = jobs.get(name);
702
+ if (previous && previous.identity !== identity)
703
+ throw new Error(`Duplicate job "${name}" at ${location}; first declared at ${previous.location}.`);
704
+ if (!previous)
705
+ jobs.set(name, { identity, create, location });
706
+ };
707
+ if (options.manifest) {
708
+ for (const [index, entry] of options.manifest.entries()) {
709
+ const location = `job manifest[${index}]`;
710
+ if (typeof entry === "function") {
711
+ validateClass(entry, location);
712
+ add(entry.jobName, entry, () => new entry, location);
713
+ } else {
714
+ if (!entry || typeof entry.name !== "string" || !entry.name.trim() || typeof entry.create !== "function")
715
+ throw new TypeError(`Invalid job factory at ${location}.`);
716
+ add(entry.name, entry.create, entry.create, location);
717
+ }
718
+ }
719
+ } else {
720
+ for (const file of await infrastructureFiles(options.directory ?? join4(process.cwd(), "src", "jobs"), options.exclude)) {
721
+ const exports = await importInfrastructure(file);
722
+ const defaultValue = exports.default;
723
+ const legacy = defaultValue && typeof defaultValue === "object" && "default" in defaultValue ? defaultValue.default : defaultValue;
724
+ if (legacy !== undefined)
725
+ validateClass(legacy, `${file}:default`);
726
+ let found = false;
727
+ for (const [name, value] of Object.entries({
728
+ ...exports,
729
+ ...legacy === undefined ? {} : { default: legacy }
730
+ })) {
731
+ if (name !== "default" && !(typeof value === "function" && ("jobName" in value)))
732
+ continue;
733
+ validateClass(value, `${file}:${name}`);
734
+ found = true;
735
+ add(value.jobName, value, () => new value, `${file}:${name}`);
736
+ }
737
+ if (!found)
738
+ throw new TypeError(`No job exports at ${file}; exclude helper modules explicitly.`);
619
739
  }
620
- throw error;
621
740
  }
622
- return entries.flatMap((fileName) => {
623
- const filePath = join3(jobsDirectory, fileName);
624
- const loaded = requireJob(filePath);
625
- return isDiscoveredJobClass(loaded.default) ? [loaded.default] : [];
626
- });
627
- }
628
- function discoverJobs() {
629
- const state = readDiscoverJobsState();
630
- if (state.names) {
631
- return state.names;
741
+ if (!current())
742
+ return [...jobs.keys()];
743
+ const registered = state3().registered;
744
+ for (const [name, entry] of jobs) {
745
+ const previous = registered.get(name);
746
+ if (previous && previous.identity !== entry.identity)
747
+ throw new Error(`Duplicate discovered job "${name}" at ${entry.location}; first declared at ${previous.location}.`);
632
748
  }
633
- const names = [];
634
- for (const JobClass of loadDiscoveredJobClasses()) {
635
- jobRegistry.register(JobClass.jobName, () => new JobClass);
636
- names.push(JobClass.jobName);
749
+ const existing = new Set(jobRegistry.names());
750
+ for (const [name, entry] of jobs) {
751
+ if (!existing.has(name)) {
752
+ jobRegistry.register(name, entry.create);
753
+ registered.set(name, entry);
754
+ }
637
755
  }
638
- state.names = names;
639
- return names;
756
+ return [...jobs.keys()];
757
+ }
758
+ function discoverJobs(options = {}) {
759
+ const cache = state3().loads;
760
+ const key = options.manifest ?? JSON.stringify([
761
+ resolve3(options.directory ?? join4(process.cwd(), "src", "jobs")),
762
+ [...options.exclude ?? []].sort()
763
+ ]);
764
+ const existing = cache.get(key);
765
+ if (existing)
766
+ return existing;
767
+ const pending = load3(options, () => state3().loads === cache).catch((error) => {
768
+ if (cache.get(key) === pending)
769
+ cache.delete(key);
770
+ throw error;
771
+ });
772
+ cache.set(key, pending);
773
+ return pending;
640
774
  }
641
775
  function resetDiscoverJobsForTests() {
642
- const state = readDiscoverJobsState();
643
- state.names = undefined;
776
+ state3().loads = new Map;
777
+ state3().registered = new Map;
644
778
  }
645
779
 
646
780
  // ../../src/bootstrap/queue/defaultJobs.ts
@@ -658,16 +792,17 @@ function registerDefaultJobs() {
658
792
  // ../../src/bootstrap/providers/queue.ts
659
793
  var queueProvider = {
660
794
  name: "core.queue",
661
- register({ container, config }) {
795
+ async register({ container, config, onCleanup }) {
662
796
  const configuredDriver = process.env.QUEUE_DRIVER ?? DEFAULT_QUEUE_DRIVER;
663
797
  const driver = configuredDriver === "async" || configuredDriver === "redis" || configuredDriver === "sync" ? configuredDriver : DEFAULT_QUEUE_DRIVER;
664
798
  config.set("queue.driver", driver);
665
799
  const failedJobs = createFailedJobService();
666
800
  container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
667
- container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, () => {
668
- registerDefaultJobs();
669
- discoverJobs();
670
- }));
801
+ registerDefaultJobs();
802
+ await discoverJobs();
803
+ const queue = createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs);
804
+ onCleanup(() => queue.close?.(), driver === "async" ? "drain" : "close");
805
+ container.set(CORE_QUEUE_TOKEN, queue);
671
806
  }
672
807
  };
673
808
  var queue_default = queueProvider;
@@ -682,6 +817,18 @@ var storageProvider = {
682
817
  };
683
818
  var storage_default = storageProvider;
684
819
 
820
+ // ../../src/bootstrap/providers/tracing.ts
821
+ import { acquireTracingRuntime } from "@getstrata/core/tracing/tracingMiddleware";
822
+ var tracingProvider = {
823
+ name: "core.tracing",
824
+ register({ onCleanup }) {
825
+ const ownership = acquireTracingRuntime();
826
+ onCleanup(() => ownership.flush(), "flush");
827
+ onCleanup(() => ownership.release(), "close");
828
+ }
829
+ };
830
+ var tracing_default = tracingProvider;
831
+
685
832
  // ../../src/bootstrap/providers/view.ts
686
833
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
687
834
  import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
@@ -726,6 +873,7 @@ var viewProvider = {
726
873
  // ../../src/bootstrap/providers/index.ts
727
874
  var coreProviders = [
728
875
  config_default,
876
+ tracing_default,
729
877
  cache_default,
730
878
  storage_default,
731
879
  auth_default,
@@ -740,38 +888,129 @@ var coreProviders = [
740
888
  function collectProviders(modules = discoverModules()) {
741
889
  return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
742
890
  }
743
- function runProviderPhase(providers, phase, context) {
744
- for (const provider of providers) {
745
- provider[phase]?.(context);
746
- }
891
+ async function runProviderPhase(providers, phase, context) {
892
+ for (const provider of providers)
893
+ await provider[phase]?.(context);
747
894
  }
748
- function createAppContext() {
895
+ var readyContext;
896
+ async function createAppContext(providers) {
897
+ if (!providers)
898
+ await ensureModulesLoaded();
899
+ const configured = [...providers ?? collectProviders()];
749
900
  const container = new ServiceContainer;
750
901
  const config = new ConfigStore;
751
- const dependencies = {
752
- container
902
+ const dependencies = { container };
903
+ const cleanups = [];
904
+ const drains = [];
905
+ let draining;
906
+ const drain = () => {
907
+ draining ??= (async () => {
908
+ const errors = [];
909
+ for (const handler of drains.splice(0).reverse()) {
910
+ try {
911
+ await handler();
912
+ } catch (error) {
913
+ errors.push(error);
914
+ }
915
+ }
916
+ if (errors.length)
917
+ throw new AggregateError(errors, "Provider drain failed.");
918
+ })();
919
+ return draining;
753
920
  };
754
- const context = {
755
- container,
756
- config,
757
- dependencies
921
+ const flushes = [];
922
+ let flushing;
923
+ const flush = () => {
924
+ flushing ??= (async () => {
925
+ const errors = [];
926
+ for (const handler of flushes.splice(0).reverse()) {
927
+ try {
928
+ await handler();
929
+ } catch (error) {
930
+ errors.push(error);
931
+ }
932
+ }
933
+ if (errors.length)
934
+ throw new AggregateError(errors, "Provider flush failed.");
935
+ })();
936
+ return flushing;
758
937
  };
759
- const providers = collectProviders();
760
- runProviderPhase(providers, "register", context);
761
- runProviderPhase(providers, "boot", context);
762
- assertAppDependenciesComplete(dependencies);
763
- const appContext = {
938
+ let disposal;
939
+ let disposed = false;
940
+ let initialized;
941
+ const dispose = () => {
942
+ disposed = true;
943
+ disposal ??= (async () => {
944
+ await drain();
945
+ const errors = [];
946
+ try {
947
+ await flush();
948
+ } catch (error) {
949
+ errors.push(error);
950
+ }
951
+ if (initialized) {
952
+ clearActiveApplicationContext(initialized);
953
+ if (readyContext === initialized)
954
+ readyContext = undefined;
955
+ }
956
+ for (const handler of cleanups.splice(0).reverse()) {
957
+ try {
958
+ await handler();
959
+ } catch (error) {
960
+ errors.push(error);
961
+ }
962
+ }
963
+ if (errors.length)
964
+ throw new AggregateError(errors, "Provider cleanup failed.");
965
+ })();
966
+ return disposal;
967
+ };
968
+ const context = {
764
969
  container,
765
970
  config,
766
- dependencies
971
+ dependencies,
972
+ onCleanup(handler, phase = "close") {
973
+ if (disposed)
974
+ throw new Error("Cannot register cleanup on a disposed application context.");
975
+ if (phase !== "drain" && phase !== "flush" && phase !== "close")
976
+ throw new TypeError("Unknown provider cleanup phase.");
977
+ if (phase === "drain" && draining || phase === "flush" && flushing)
978
+ throw new Error("Cannot register cleanup after its phase started.");
979
+ (phase === "drain" ? drains : phase === "flush" ? flushes : cleanups).push(handler);
980
+ }
767
981
  };
768
- setActiveApplicationContext(appContext);
769
- return appContext;
982
+ container.set(CORE_HTTP_CLEANUP_TOKEN2, (handler) => context.onCleanup(handler));
983
+ try {
984
+ await runProviderPhase(configured, "register", context);
985
+ await runProviderPhase(configured, "boot", context);
986
+ assertAppDependenciesComplete(dependencies);
987
+ const appContext = {
988
+ container,
989
+ config,
990
+ dependencies,
991
+ drain,
992
+ flush,
993
+ dispose
994
+ };
995
+ initialized = appContext;
996
+ setActiveApplicationContext(appContext);
997
+ readyContext = appContext;
998
+ return appContext;
999
+ } catch (error) {
1000
+ try {
1001
+ await dispose();
1002
+ } catch (cleanupError) {
1003
+ throw new AggregateError([error, cleanupError], "Application startup and cleanup failed.", {
1004
+ cause: error
1005
+ });
1006
+ }
1007
+ throw error;
1008
+ }
770
1009
  }
771
- var cachedAppContext;
772
1010
  function getAppContext() {
773
- cachedAppContext ??= createAppContext();
774
- return cachedAppContext;
1011
+ if (!readyContext)
1012
+ throw new Error("Application context is not ready. Await createAppContext() before using appContext.");
1013
+ return readyContext;
775
1014
  }
776
1015
  var appContext = {
777
1016
  get container() {