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