@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/dist/bootstrap/app.d.ts +7 -1
  4. package/dist/bootstrap/config.d.ts +1 -1
  5. package/dist/bootstrap/context.d.ts +8 -2
  6. package/dist/bootstrap/dependencies.d.ts +1 -1
  7. package/dist/bootstrap/discoverJobs.d.ts +11 -3
  8. package/dist/bootstrap/discoverListeners.d.ts +7 -3
  9. package/dist/bootstrap/discoverModels.d.ts +8 -0
  10. package/dist/bootstrap/discoverModules.d.ts +4 -1
  11. package/dist/bootstrap/httpKernel.d.ts +5 -0
  12. package/dist/bootstrap/inProcessCron.d.ts +2 -1
  13. package/dist/bootstrap/infrastructureDiscovery.d.ts +9 -0
  14. package/dist/bootstrap/modules.d.ts +1 -1
  15. package/dist/bootstrap/providers/tracing.d.ts +3 -0
  16. package/dist/bootstrap/public-api.d.ts +9 -4
  17. package/dist/bootstrap/web/index.d.ts +1 -1
  18. package/dist/bootstrap/web/server.d.ts +15 -7
  19. package/dist/entries/buildModuleRoutes.js +212 -67
  20. package/dist/entries/buildWebModuleRoutes.js +212 -67
  21. package/dist/entries/cache/modelCacheTags.js +157 -51
  22. package/dist/entries/config.js +6 -0
  23. package/dist/entries/context.js +426 -188
  24. package/dist/entries/createWebRoutes.js +214 -69
  25. package/dist/entries/dependencies.js +428 -190
  26. package/dist/entries/discoverJobs.js +132 -50
  27. package/dist/entries/discoverListeners.js +85 -37
  28. package/dist/entries/discoverModels.js +39 -0
  29. package/dist/entries/discoverModules.js +158 -51
  30. package/dist/entries/health.js +3 -0
  31. package/dist/entries/httpKernel.js +52 -13
  32. package/dist/entries/listeners/invalidateCacheOnModelWrite.js +157 -51
  33. package/dist/entries/providers.js +307 -164
  34. package/dist/entries/web/routing.js +52 -13
  35. package/dist/entries/web/server.js +69 -14
  36. package/dist/index.js +590 -216
  37. 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 { appSchedule, runDueScheduledTasks, Schedule } from "@getstrata/core/scheduler/schedule";
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 { readdirSync } from "fs";
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
- var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
343
- function readDiscoverModulesState() {
344
- const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
345
- if (existing) {
346
- return existing;
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
- const state = { appModules: [] };
349
- globalThis[DISCOVER_MODULES_STATE_KEY] = state;
350
- return state;
351
- }
352
- function configureModulesDirectory(modulesDir) {
353
- const state = readDiscoverModulesState();
354
- if (state.configuredModulesDir !== modulesDir) {
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
- state.configuredModulesDir = modulesDir;
426
+ return files;
359
427
  }
360
- function resolveModulesDirectory(options) {
361
- const state = readDiscoverModulesState();
362
- if (options?.modulesDir) {
363
- return options.modulesDir;
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
- if (state.configuredModulesDir) {
366
- return state.configuredModulesDir;
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 loadDiscoveredModules(options) {
371
- const modulesDirectory = resolveModulesDirectory(options);
372
- let moduleNames;
373
- try {
374
- moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
375
- } catch (error) {
376
- if (error.code === "ENOENT") {
377
- return [];
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 modules = await Promise.all(moduleNames.map(async (moduleName) => {
382
- const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
383
- const loaded = await import(moduleUrl);
384
- return loaded.default;
385
- }));
386
- return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
387
- }
388
- async function ensureModulesLoaded(options) {
389
- const state = readDiscoverModulesState();
390
- if (state.appModules.length > 0) {
391
- return state.appModules;
392
- }
393
- state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
394
- state.appModules.splice(0, state.appModules.length, ...modules);
395
- return state.appModules;
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
- return state.modulesReady;
545
+ current.modulesReady = pending;
546
+ return pending;
398
547
  }
399
548
  function discoverModules() {
400
- return readDiscoverModulesState().appModules;
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
- return new CacheRepository(store);
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 { existsSync, readdirSync as readdirSync2 } from "fs";
796
- import { createRequire } from "module";
797
- import { join as join2 } from "path";
798
- var requireListener = createRequire(import.meta.url);
799
- var DISCOVER_LISTENERS_STATE_KEY = Symbol.for("@getstrata/discoverListenersState");
800
- function readDiscoverListenersState() {
801
- const existing = globalThis[DISCOVER_LISTENERS_STATE_KEY];
802
- if (existing) {
803
- return existing;
804
- }
805
- const state = {};
806
- globalThis[DISCOVER_LISTENERS_STATE_KEY] = state;
807
- return state;
808
- }
809
- function resolveListenersDirectory() {
810
- const fromCwd = join2(process.cwd(), "src", "listeners");
811
- if (existsSync(fromCwd)) {
812
- return fromCwd;
813
- }
814
- return join2(import.meta.dir, "../listeners");
815
- }
816
- function loadDiscoveredListeners() {
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
- return listeners.filter((listener) => typeof listener === "function");
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
- registerListenerGroup("cache.invalidate-on-model-write", () => {
942
- registerInvalidateCacheOnModelWriteListeners();
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 { existsSync as existsSync2, readdirSync as readdirSync3 } from "fs";
970
- import { createRequire as createRequire2 } from "module";
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 requireJob = createRequire2(import.meta.url);
974
- var DISCOVER_JOBS_STATE_KEY = Symbol.for("@getstrata/discoverJobsState");
975
- function readDiscoverJobsState() {
976
- const existing = globalThis[DISCOVER_JOBS_STATE_KEY];
977
- if (existing) {
978
- return existing;
979
- }
980
- const state = {};
981
- globalThis[DISCOVER_JOBS_STATE_KEY] = state;
982
- return state;
983
- }
984
- function resolveJobsDirectory() {
985
- const fromCwd = join3(process.cwd(), "src", "jobs");
986
- if (existsSync2(fromCwd)) {
987
- return fromCwd;
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
- return join3(import.meta.dir, "../jobs");
990
- }
991
- function isDiscoveredJobClass(value) {
992
- if (typeof value !== "function") {
993
- return false;
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
- const jobName = value.jobName;
996
- return typeof jobName === "string" && jobName.length > 0;
1183
+ return [...jobs.keys()];
997
1184
  }
998
- function loadDiscoveredJobClasses() {
999
- const jobsDirectory = resolveJobsDirectory();
1000
- let entries;
1001
- try {
1002
- entries = readdirSync3(jobsDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.(ts|js)$/.test(entry.name)).map((entry) => entry.name);
1003
- } catch (error) {
1004
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1005
- return [];
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
- function discoverJobs() {
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
- const state = readDiscoverJobsState();
1030
- state.names = undefined;
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
- container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, () => {
1055
- registerDefaultJobs();
1056
- discoverJobs();
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
- function createAppContext() {
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
- container
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 context = {
1142
- container,
1143
- config,
1144
- dependencies
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 providers = collectProviders();
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
- setActiveApplicationContext(appContext);
1156
- return appContext;
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 join4 } from "path";
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(join4(process.cwd(), "public"), pathname);
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
- return Bun.serve({
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 options.onRequest?.(request);
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