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