@getstrata/bootstrap 2.1.0 → 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 +25 -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 +5 -0
- 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 +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 +426 -188
- package/dist/entries/createWebRoutes.js +214 -69
- package/dist/entries/dependencies.js +428 -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 +590 -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,6 +676,10 @@ 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,
|
|
@@ -587,7 +740,7 @@ import { createCacheStore } from "@getstrata/core/cache/createCacheStore";
|
|
|
587
740
|
import { CacheRepository } from "@getstrata/core/cache/repository";
|
|
588
741
|
var cacheProvider = {
|
|
589
742
|
name: "core.cache",
|
|
590
|
-
register({ container, config, dependencies }) {
|
|
743
|
+
register({ container, config, dependencies, onCleanup }) {
|
|
591
744
|
container.singleton(CORE_CACHE_TOKEN, () => {
|
|
592
745
|
const store = createCacheStore({
|
|
593
746
|
driver: config.require(CACHE_DRIVER_CONFIG_KEY),
|
|
@@ -595,7 +748,9 @@ var cacheProvider = {
|
|
|
595
748
|
maxEntries: config.require(CACHE_MAX_ENTRIES_CONFIG_KEY),
|
|
596
749
|
redisUrl: config.get(REDIS_URL_CONFIG_KEY) || undefined
|
|
597
750
|
});
|
|
598
|
-
|
|
751
|
+
const cache = new CacheRepository(store);
|
|
752
|
+
onCleanup(() => cache.close());
|
|
753
|
+
return cache;
|
|
599
754
|
});
|
|
600
755
|
dependencies.cache = container.resolve(CORE_CACHE_TOKEN);
|
|
601
756
|
}
|
|
@@ -792,49 +947,47 @@ var eventsProvider = {
|
|
|
792
947
|
var events_default = eventsProvider;
|
|
793
948
|
|
|
794
949
|
// ../../src/bootstrap/discoverListeners.ts
|
|
795
|
-
import {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const listenersDirectory = resolveListenersDirectory();
|
|
818
|
-
let entries;
|
|
819
|
-
try {
|
|
820
|
-
entries = readdirSync2(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.(ts|js)$/.test(entry.name)).map((entry) => entry.name);
|
|
821
|
-
} catch (error) {
|
|
822
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
823
|
-
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);
|
|
824
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);
|
|
825
987
|
throw error;
|
|
826
|
-
}
|
|
827
|
-
const listeners = entries.map((fileName) => {
|
|
828
|
-
const filePath = join2(listenersDirectory, fileName);
|
|
829
|
-
const loaded = requireListener(filePath);
|
|
830
|
-
return loaded.default;
|
|
831
988
|
});
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
function discoverListeners() {
|
|
835
|
-
const state = readDiscoverListenersState();
|
|
836
|
-
state.appListeners ??= loadDiscoveredListeners();
|
|
837
|
-
return state.appListeners;
|
|
989
|
+
cache.set(key, pending);
|
|
990
|
+
return pending;
|
|
838
991
|
}
|
|
839
992
|
|
|
840
993
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
@@ -927,22 +1080,12 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options =
|
|
|
927
1080
|
}
|
|
928
1081
|
|
|
929
1082
|
// ../../src/bootstrap/providers/listeners.ts
|
|
930
|
-
var registeredListenerGroups = new Set;
|
|
931
|
-
function registerListenerGroup(name, register) {
|
|
932
|
-
if (registeredListenerGroups.has(name)) {
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
registeredListenerGroups.add(name);
|
|
936
|
-
register();
|
|
937
|
-
}
|
|
938
1083
|
var listenersProvider = {
|
|
939
1084
|
name: "core.listeners",
|
|
940
|
-
boot() {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
for (const registerListener of discoverListeners()) {
|
|
945
|
-
registerListener();
|
|
1085
|
+
async boot({ onCleanup }) {
|
|
1086
|
+
onCleanup(registerInvalidateCacheOnModelWriteListeners());
|
|
1087
|
+
for (const registerListener of await discoverListeners()) {
|
|
1088
|
+
await registerListener();
|
|
946
1089
|
}
|
|
947
1090
|
}
|
|
948
1091
|
};
|
|
@@ -966,68 +1109,99 @@ import {
|
|
|
966
1109
|
} from "@getstrata/core/queue/createAppQueue";
|
|
967
1110
|
|
|
968
1111
|
// ../../src/bootstrap/discoverJobs.ts
|
|
969
|
-
import {
|
|
970
|
-
import {
|
|
971
|
-
import { join as join3 } from "path";
|
|
1112
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
1113
|
+
import { Job } from "@getstrata/core/queue";
|
|
972
1114
|
import { jobRegistry } from "@getstrata/core/queue/jobRegistry";
|
|
973
|
-
var
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
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
|
+
}
|
|
988
1167
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
+
}
|
|
994
1182
|
}
|
|
995
|
-
|
|
996
|
-
return typeof jobName === "string" && jobName.length > 0;
|
|
1183
|
+
return [...jobs.keys()];
|
|
997
1184
|
}
|
|
998
|
-
function
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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);
|
|
1007
1197
|
throw error;
|
|
1008
|
-
}
|
|
1009
|
-
return entries.flatMap((fileName) => {
|
|
1010
|
-
const filePath = join3(jobsDirectory, fileName);
|
|
1011
|
-
const loaded = requireJob(filePath);
|
|
1012
|
-
return isDiscoveredJobClass(loaded.default) ? [loaded.default] : [];
|
|
1013
1198
|
});
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const state = readDiscoverJobsState();
|
|
1017
|
-
if (state.names) {
|
|
1018
|
-
return state.names;
|
|
1019
|
-
}
|
|
1020
|
-
const names = [];
|
|
1021
|
-
for (const JobClass of loadDiscoveredJobClasses()) {
|
|
1022
|
-
jobRegistry.register(JobClass.jobName, () => new JobClass);
|
|
1023
|
-
names.push(JobClass.jobName);
|
|
1024
|
-
}
|
|
1025
|
-
state.names = names;
|
|
1026
|
-
return names;
|
|
1199
|
+
cache.set(key, pending);
|
|
1200
|
+
return pending;
|
|
1027
1201
|
}
|
|
1028
1202
|
function resetDiscoverJobsForTests() {
|
|
1029
|
-
|
|
1030
|
-
|
|
1203
|
+
state3().loads = new Map;
|
|
1204
|
+
state3().registered = new Map;
|
|
1031
1205
|
}
|
|
1032
1206
|
|
|
1033
1207
|
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
@@ -1045,16 +1219,17 @@ function registerDefaultJobs() {
|
|
|
1045
1219
|
// ../../src/bootstrap/providers/queue.ts
|
|
1046
1220
|
var queueProvider = {
|
|
1047
1221
|
name: "core.queue",
|
|
1048
|
-
register({ container, config }) {
|
|
1222
|
+
async register({ container, config, onCleanup }) {
|
|
1049
1223
|
const configuredDriver = process.env.QUEUE_DRIVER ?? DEFAULT_QUEUE_DRIVER;
|
|
1050
1224
|
const driver = configuredDriver === "async" || configuredDriver === "redis" || configuredDriver === "sync" ? configuredDriver : DEFAULT_QUEUE_DRIVER;
|
|
1051
1225
|
config.set("queue.driver", driver);
|
|
1052
1226
|
const failedJobs = createFailedJobService();
|
|
1053
1227
|
container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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);
|
|
1058
1233
|
}
|
|
1059
1234
|
};
|
|
1060
1235
|
var queue_default = queueProvider;
|
|
@@ -1069,6 +1244,18 @@ var storageProvider = {
|
|
|
1069
1244
|
};
|
|
1070
1245
|
var storage_default = storageProvider;
|
|
1071
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
|
+
|
|
1072
1259
|
// ../../src/bootstrap/providers/view.ts
|
|
1073
1260
|
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
1074
1261
|
import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
|
|
@@ -1113,6 +1300,7 @@ var viewProvider = {
|
|
|
1113
1300
|
// ../../src/bootstrap/providers/index.ts
|
|
1114
1301
|
var coreProviders = [
|
|
1115
1302
|
config_default,
|
|
1303
|
+
tracing_default,
|
|
1116
1304
|
cache_default,
|
|
1117
1305
|
storage_default,
|
|
1118
1306
|
auth_default,
|
|
@@ -1127,36 +1315,127 @@ var coreProviders = [
|
|
|
1127
1315
|
function collectProviders(modules = discoverModules()) {
|
|
1128
1316
|
return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
|
|
1129
1317
|
}
|
|
1130
|
-
function runProviderPhase(providers, phase, context) {
|
|
1131
|
-
for (const provider of providers)
|
|
1132
|
-
provider[phase]?.(context);
|
|
1133
|
-
}
|
|
1318
|
+
async function runProviderPhase(providers, phase, context) {
|
|
1319
|
+
for (const provider of providers)
|
|
1320
|
+
await provider[phase]?.(context);
|
|
1134
1321
|
}
|
|
1135
|
-
|
|
1322
|
+
var readyContext;
|
|
1323
|
+
async function createAppContext(providers) {
|
|
1324
|
+
if (!providers)
|
|
1325
|
+
await ensureModulesLoaded();
|
|
1326
|
+
const configured = [...providers ?? collectProviders()];
|
|
1136
1327
|
const container = new ServiceContainer;
|
|
1137
1328
|
const config = new ConfigStore;
|
|
1138
|
-
const dependencies = {
|
|
1139
|
-
|
|
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;
|
|
1140
1347
|
};
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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;
|
|
1145
1394
|
};
|
|
1146
|
-
const
|
|
1147
|
-
runProviderPhase(providers, "register", context);
|
|
1148
|
-
runProviderPhase(providers, "boot", context);
|
|
1149
|
-
assertAppDependenciesComplete(dependencies);
|
|
1150
|
-
const appContext = {
|
|
1395
|
+
const context = {
|
|
1151
1396
|
container,
|
|
1152
1397
|
config,
|
|
1153
|
-
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
|
+
}
|
|
1154
1408
|
};
|
|
1155
|
-
|
|
1156
|
-
|
|
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
|
+
}
|
|
1157
1436
|
}
|
|
1158
1437
|
// ../../src/bootstrap/createWebRoutes.ts
|
|
1159
|
-
import { join as
|
|
1438
|
+
import { join as join5 } from "path";
|
|
1160
1439
|
import { isViewsEnabled as isViewsEnabled3 } from "@getstrata/core/runtime/frontendMode";
|
|
1161
1440
|
import { assertUrlPathUnderRoot } from "@getstrata/core/security/safePath";
|
|
1162
1441
|
import { notFoundHtmlResponse } from "@getstrata/core/view";
|
|
@@ -1173,7 +1452,7 @@ function createWebRoutes(dependencies, options = {}) {
|
|
|
1173
1452
|
registerRoute("GET", "/assets/*", ["global", "web"]);
|
|
1174
1453
|
const pathname = new URL(request.url).pathname;
|
|
1175
1454
|
try {
|
|
1176
|
-
const filePath = assertUrlPathUnderRoot(
|
|
1455
|
+
const filePath = assertUrlPathUnderRoot(join5(process.cwd(), "public"), pathname);
|
|
1177
1456
|
const file = Bun.file(filePath);
|
|
1178
1457
|
if (!await file.exists()) {
|
|
1179
1458
|
return notFoundHtmlResponse();
|
|
@@ -1196,8 +1475,43 @@ function mergeWebRoutes(dependencies, routes, options = {}) {
|
|
|
1196
1475
|
};
|
|
1197
1476
|
}
|
|
1198
1477
|
// ../../src/bootstrap/dependencies.ts
|
|
1199
|
-
function createAppDependencies() {
|
|
1200
|
-
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;
|
|
1201
1515
|
}
|
|
1202
1516
|
// ../../src/bootstrap/health.ts
|
|
1203
1517
|
import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
|
|
@@ -1580,6 +1894,7 @@ function createRouteKernel(dependencies) {
|
|
|
1580
1894
|
return createHttpKernel(dependencies);
|
|
1581
1895
|
}
|
|
1582
1896
|
// ../../src/bootstrap/web/server.ts
|
|
1897
|
+
import { resolveMaxBodyBytes } from "@getstrata/core/http/bodySizeLimitMiddleware";
|
|
1583
1898
|
import { createCorsMiddleware as createCorsMiddleware2 } from "@getstrata/core/http/corsMiddleware";
|
|
1584
1899
|
import { currentRequestMeta as currentRequestMeta2, runWithRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
1585
1900
|
import { assertUrlPathUnderRoot as assertUrlPathUnderRoot2 } from "@getstrata/core/security/safePath";
|
|
@@ -1591,29 +1906,32 @@ function socketAddress(server, request) {
|
|
|
1591
1906
|
async function missingHtmlResponse() {
|
|
1592
1907
|
return notFoundHtmlResponse2();
|
|
1593
1908
|
}
|
|
1594
|
-
function wrapRouteHandler(handler, path) {
|
|
1909
|
+
function wrapRouteHandler(handler, path, onRequest, track = (work) => work()) {
|
|
1595
1910
|
return async (request, server) => {
|
|
1596
|
-
return await runWithRequestMeta({
|
|
1911
|
+
return await track(async () => await runWithRequestMeta({
|
|
1597
1912
|
...currentRequestMeta2(),
|
|
1598
1913
|
request,
|
|
1599
1914
|
routeTemplate: path,
|
|
1600
1915
|
ipAddress: socketAddress(server, request),
|
|
1601
1916
|
userAgent: request.headers.get("user-agent")
|
|
1602
1917
|
}, async () => {
|
|
1918
|
+
const intercepted = await onRequest?.(request, server);
|
|
1919
|
+
if (intercepted instanceof Response)
|
|
1920
|
+
return intercepted;
|
|
1603
1921
|
const response = await handler(request);
|
|
1604
1922
|
return response ?? await missingHtmlResponse();
|
|
1605
|
-
});
|
|
1923
|
+
}));
|
|
1606
1924
|
};
|
|
1607
1925
|
}
|
|
1608
|
-
function convertAppRoutesToBunRoutes(routes, cors = {}) {
|
|
1926
|
+
function convertAppRoutesToBunRoutes(routes, cors = {}, onRequest, track) {
|
|
1609
1927
|
const bunRoutes = {};
|
|
1610
1928
|
const middleware = createCorsMiddleware2(cors);
|
|
1611
1929
|
const preflight = (request) => middleware(request, async () => new Response(null, { status: 405 }));
|
|
1612
1930
|
for (const [path, handler] of Object.entries(routes)) {
|
|
1613
1931
|
if (typeof handler === "function") {
|
|
1614
1932
|
bunRoutes[path] = {
|
|
1615
|
-
GET: wrapRouteHandler(handler, path),
|
|
1616
|
-
OPTIONS: wrapRouteHandler(preflight, path)
|
|
1933
|
+
GET: wrapRouteHandler(handler, path, onRequest, track),
|
|
1934
|
+
OPTIONS: wrapRouteHandler(preflight, path, onRequest, track)
|
|
1617
1935
|
};
|
|
1618
1936
|
continue;
|
|
1619
1937
|
}
|
|
@@ -1623,10 +1941,10 @@ function convertAppRoutesToBunRoutes(routes, cors = {}) {
|
|
|
1623
1941
|
if (typeof methodHandler !== "function") {
|
|
1624
1942
|
continue;
|
|
1625
1943
|
}
|
|
1626
|
-
methods[method.toUpperCase()] = wrapRouteHandler(methodHandler, path);
|
|
1944
|
+
methods[method.toUpperCase()] = wrapRouteHandler(methodHandler, path, onRequest, track);
|
|
1627
1945
|
}
|
|
1628
1946
|
if (Object.keys(methods).length > 0) {
|
|
1629
|
-
methods.OPTIONS ??= wrapRouteHandler(preflight, path);
|
|
1947
|
+
methods.OPTIONS ??= wrapRouteHandler(preflight, path, onRequest, track);
|
|
1630
1948
|
bunRoutes[path] = methods;
|
|
1631
1949
|
}
|
|
1632
1950
|
}
|
|
@@ -1634,19 +1952,47 @@ function convertAppRoutesToBunRoutes(routes, cors = {}) {
|
|
|
1634
1952
|
return bunRoutes;
|
|
1635
1953
|
}
|
|
1636
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
|
+
};
|
|
1637
1978
|
const publicDir = options.publicDir ?? "./public";
|
|
1638
|
-
const bunRoutes = options.routes ? convertAppRoutesToBunRoutes(options.routes, options.cors) : undefined;
|
|
1639
|
-
|
|
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,
|
|
1640
1984
|
port: options.port,
|
|
1641
1985
|
...bunRoutes ? { routes: bunRoutes } : {},
|
|
1642
1986
|
async fetch(request, server) {
|
|
1643
|
-
return await runWithRequestMeta({
|
|
1987
|
+
return await track(async () => await runWithRequestMeta({
|
|
1644
1988
|
...currentRequestMeta2(),
|
|
1645
1989
|
request,
|
|
1646
1990
|
ipAddress: socketAddress(server, request),
|
|
1647
1991
|
userAgent: request.headers.get("user-agent")
|
|
1648
1992
|
}, async () => {
|
|
1649
|
-
await
|
|
1993
|
+
const intercepted = await onRequest(request, server);
|
|
1994
|
+
if (intercepted instanceof Response)
|
|
1995
|
+
return intercepted;
|
|
1650
1996
|
const url = new URL(request.url);
|
|
1651
1997
|
if (url.pathname.startsWith("/assets/")) {
|
|
1652
1998
|
try {
|
|
@@ -1660,13 +2006,36 @@ function createWebServer(options) {
|
|
|
1660
2006
|
}
|
|
1661
2007
|
}
|
|
1662
2008
|
if (options.handle) {
|
|
1663
|
-
const response = await options.handle(request);
|
|
2009
|
+
const response = await options.handle(request, server);
|
|
2010
|
+
if (response === undefined && websocket)
|
|
2011
|
+
return;
|
|
1664
2012
|
return response ?? await missingHtmlResponse();
|
|
1665
2013
|
}
|
|
1666
2014
|
return await missingHtmlResponse();
|
|
1667
|
-
});
|
|
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();
|
|
1668
2022
|
}
|
|
1669
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;
|
|
1670
2039
|
}
|
|
1671
2040
|
// ../../src/bootstrap/web/session.ts
|
|
1672
2041
|
import { createHmac, randomBytes } from "crypto";
|
|
@@ -1945,8 +2314,10 @@ export {
|
|
|
1945
2314
|
DATABASE_URL_CONFIG_KEY,
|
|
1946
2315
|
DEFAULT_APP_PORT,
|
|
1947
2316
|
REDIS_URL_CONFIG_KEY,
|
|
2317
|
+
RedisSchedulerLeaseStore,
|
|
1948
2318
|
RouteRegistry,
|
|
1949
2319
|
Schedule,
|
|
2320
|
+
SchedulerLeaseLostError,
|
|
1950
2321
|
ServiceContainer,
|
|
1951
2322
|
appSchedule,
|
|
1952
2323
|
assertAppDependenciesComplete,
|
|
@@ -1958,6 +2329,7 @@ export {
|
|
|
1958
2329
|
checkRedis,
|
|
1959
2330
|
collectProviders,
|
|
1960
2331
|
configureModulesDirectory,
|
|
2332
|
+
configureModulesManifest,
|
|
1961
2333
|
coreProviders,
|
|
1962
2334
|
createAppContext,
|
|
1963
2335
|
createAppDependencies,
|
|
@@ -1970,6 +2342,7 @@ export {
|
|
|
1970
2342
|
createWebServer,
|
|
1971
2343
|
discoverJobs,
|
|
1972
2344
|
discoverModelTableNames,
|
|
2345
|
+
discoverModels,
|
|
1973
2346
|
discoverModules,
|
|
1974
2347
|
ensureModulesLoaded,
|
|
1975
2348
|
getRequiredDependency,
|
|
@@ -1998,6 +2371,7 @@ export {
|
|
|
1998
2371
|
setActiveApplicationContext,
|
|
1999
2372
|
slugify,
|
|
2000
2373
|
toRouteRequest,
|
|
2374
|
+
tracing_default as tracingProvider,
|
|
2001
2375
|
wrapSecuredRouteModelByKey,
|
|
2002
2376
|
wrapWebLogin,
|
|
2003
2377
|
wrapWebRegister
|