@getstrata/bootstrap 2.0.9 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/dist/bootstrap/app.d.ts +7 -1
  4. package/dist/bootstrap/config.d.ts +1 -1
  5. package/dist/bootstrap/context.d.ts +8 -2
  6. package/dist/bootstrap/contracts.d.ts +1 -1
  7. package/dist/bootstrap/dependencies.d.ts +1 -1
  8. package/dist/bootstrap/discoverJobs.d.ts +11 -3
  9. package/dist/bootstrap/discoverListeners.d.ts +7 -3
  10. package/dist/bootstrap/discoverModels.d.ts +8 -0
  11. package/dist/bootstrap/discoverModules.d.ts +4 -1
  12. package/dist/bootstrap/httpKernel.d.ts +25 -20
  13. package/dist/bootstrap/inProcessCron.d.ts +2 -1
  14. package/dist/bootstrap/infrastructureDiscovery.d.ts +9 -0
  15. package/dist/bootstrap/modules.d.ts +1 -1
  16. package/dist/bootstrap/providers/tracing.d.ts +3 -0
  17. package/dist/bootstrap/public-api.d.ts +9 -4
  18. package/dist/bootstrap/web/index.d.ts +1 -1
  19. package/dist/bootstrap/web/routing.d.ts +2 -2
  20. package/dist/bootstrap/web/server.d.ts +15 -7
  21. package/dist/entries/buildModuleRoutes.js +212 -67
  22. package/dist/entries/buildWebModuleRoutes.js +212 -67
  23. package/dist/entries/cache/modelCacheTags.js +157 -51
  24. package/dist/entries/config.js +6 -0
  25. package/dist/entries/context.js +427 -188
  26. package/dist/entries/contracts.js +2 -0
  27. package/dist/entries/createWebRoutes.js +214 -69
  28. package/dist/entries/dependencies.js +429 -190
  29. package/dist/entries/discoverJobs.js +132 -50
  30. package/dist/entries/discoverListeners.js +85 -37
  31. package/dist/entries/discoverModels.js +39 -0
  32. package/dist/entries/discoverModules.js +158 -51
  33. package/dist/entries/health.js +3 -0
  34. package/dist/entries/httpKernel.js +52 -13
  35. package/dist/entries/listeners/invalidateCacheOnModelWrite.js +157 -51
  36. package/dist/entries/providers.js +307 -164
  37. package/dist/entries/web/routing.js +52 -13
  38. package/dist/entries/web/server.js +69 -14
  39. package/dist/index.js +591 -216
  40. package/package.json +8 -3
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,9 +676,14 @@ function cacheTagsForModelWrite(tableName, action) {
527
676
  function discoverModelTableNames() {
528
677
  return discoverModules().map((module) => module.tableName).filter((tableName) => tableName !== undefined);
529
678
  }
679
+ // ../../src/bootstrap/context.ts
680
+ import { CORE_HTTP_CLEANUP_TOKEN as CORE_HTTP_CLEANUP_TOKEN2 } from "@getstrata/core/contracts/serviceTokens";
681
+ import { clearActiveApplicationContext } from "@getstrata/core/runtime/applicationRegistry";
682
+
530
683
  // ../../src/bootstrap/contracts.ts
531
684
  import {
532
685
  ConfigStore,
686
+ createServiceToken,
533
687
  ServiceContainer
534
688
  } from "@getstrata/core/contracts/container";
535
689
  import {
@@ -586,7 +740,7 @@ import { createCacheStore } from "@getstrata/core/cache/createCacheStore";
586
740
  import { CacheRepository } from "@getstrata/core/cache/repository";
587
741
  var cacheProvider = {
588
742
  name: "core.cache",
589
- register({ container, config, dependencies }) {
743
+ register({ container, config, dependencies, onCleanup }) {
590
744
  container.singleton(CORE_CACHE_TOKEN, () => {
591
745
  const store = createCacheStore({
592
746
  driver: config.require(CACHE_DRIVER_CONFIG_KEY),
@@ -594,7 +748,9 @@ var cacheProvider = {
594
748
  maxEntries: config.require(CACHE_MAX_ENTRIES_CONFIG_KEY),
595
749
  redisUrl: config.get(REDIS_URL_CONFIG_KEY) || undefined
596
750
  });
597
- return new CacheRepository(store);
751
+ const cache = new CacheRepository(store);
752
+ onCleanup(() => cache.close());
753
+ return cache;
598
754
  });
599
755
  dependencies.cache = container.resolve(CORE_CACHE_TOKEN);
600
756
  }
@@ -791,49 +947,47 @@ var eventsProvider = {
791
947
  var events_default = eventsProvider;
792
948
 
793
949
  // ../../src/bootstrap/discoverListeners.ts
794
- import { existsSync, readdirSync as readdirSync2 } from "fs";
795
- import { createRequire } from "module";
796
- import { join as join2 } from "path";
797
- var requireListener = createRequire(import.meta.url);
798
- var DISCOVER_LISTENERS_STATE_KEY = Symbol.for("@getstrata/discoverListenersState");
799
- function readDiscoverListenersState() {
800
- const existing = globalThis[DISCOVER_LISTENERS_STATE_KEY];
801
- if (existing) {
802
- return existing;
803
- }
804
- const state = {};
805
- globalThis[DISCOVER_LISTENERS_STATE_KEY] = state;
806
- return state;
807
- }
808
- function resolveListenersDirectory() {
809
- const fromCwd = join2(process.cwd(), "src", "listeners");
810
- if (existsSync(fromCwd)) {
811
- return fromCwd;
812
- }
813
- return join2(import.meta.dir, "../listeners");
814
- }
815
- function loadDiscoveredListeners() {
816
- const listenersDirectory = resolveListenersDirectory();
817
- let entries;
818
- try {
819
- entries = readdirSync2(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.(ts|js)$/.test(entry.name)).map((entry) => entry.name);
820
- } catch (error) {
821
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
822
- return [];
950
+ import { join as join3, resolve as resolve2 } from "path";
951
+ var KEY2 = Symbol.for("@getstrata/discoverListenersState");
952
+ function state2() {
953
+ const globals = globalThis;
954
+ globals[KEY2] ??= { loads: new Map };
955
+ return globals[KEY2];
956
+ }
957
+ async function load2(options) {
958
+ const listeners = new Set;
959
+ const add = (value, location) => {
960
+ if (typeof value !== "function")
961
+ throw new TypeError(`Invalid listener registrar at ${location}: expected a default function.`);
962
+ listeners.add(value);
963
+ };
964
+ if (options.manifest)
965
+ for (const [index, value] of options.manifest.entries())
966
+ add(value, `listener manifest[${index}]`);
967
+ else
968
+ for (const file of await infrastructureFiles(options.directory ?? join3(process.cwd(), "src", "listeners"), options.exclude)) {
969
+ const exports = await importInfrastructure(file);
970
+ const value = exports.default;
971
+ add(value && typeof value === "object" && "default" in value ? value.default : value, file);
823
972
  }
973
+ return [...listeners];
974
+ }
975
+ function discoverListeners(options = {}) {
976
+ const cache = state2().loads;
977
+ const key = options.manifest ?? JSON.stringify([
978
+ resolve2(options.directory ?? join3(process.cwd(), "src", "listeners")),
979
+ [...options.exclude ?? []].sort()
980
+ ]);
981
+ const existing = cache.get(key);
982
+ if (existing)
983
+ return existing;
984
+ const pending = load2(options).catch((error) => {
985
+ if (cache.get(key) === pending)
986
+ cache.delete(key);
824
987
  throw error;
825
- }
826
- const listeners = entries.map((fileName) => {
827
- const filePath = join2(listenersDirectory, fileName);
828
- const loaded = requireListener(filePath);
829
- return loaded.default;
830
988
  });
831
- return listeners.filter((listener) => typeof listener === "function");
832
- }
833
- function discoverListeners() {
834
- const state = readDiscoverListenersState();
835
- state.appListeners ??= loadDiscoveredListeners();
836
- return state.appListeners;
989
+ cache.set(key, pending);
990
+ return pending;
837
991
  }
838
992
 
839
993
  // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
@@ -926,22 +1080,12 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options =
926
1080
  }
927
1081
 
928
1082
  // ../../src/bootstrap/providers/listeners.ts
929
- var registeredListenerGroups = new Set;
930
- function registerListenerGroup(name, register) {
931
- if (registeredListenerGroups.has(name)) {
932
- return;
933
- }
934
- registeredListenerGroups.add(name);
935
- register();
936
- }
937
1083
  var listenersProvider = {
938
1084
  name: "core.listeners",
939
- boot() {
940
- registerListenerGroup("cache.invalidate-on-model-write", () => {
941
- registerInvalidateCacheOnModelWriteListeners();
942
- });
943
- for (const registerListener of discoverListeners()) {
944
- registerListener();
1085
+ async boot({ onCleanup }) {
1086
+ onCleanup(registerInvalidateCacheOnModelWriteListeners());
1087
+ for (const registerListener of await discoverListeners()) {
1088
+ await registerListener();
945
1089
  }
946
1090
  }
947
1091
  };
@@ -965,68 +1109,99 @@ import {
965
1109
  } from "@getstrata/core/queue/createAppQueue";
966
1110
 
967
1111
  // ../../src/bootstrap/discoverJobs.ts
968
- import { existsSync as existsSync2, readdirSync as readdirSync3 } from "fs";
969
- import { createRequire as createRequire2 } from "module";
970
- import { join as join3 } from "path";
1112
+ import { join as join4, resolve as resolve3 } from "path";
1113
+ import { Job } from "@getstrata/core/queue";
971
1114
  import { jobRegistry } from "@getstrata/core/queue/jobRegistry";
972
- var requireJob = createRequire2(import.meta.url);
973
- var DISCOVER_JOBS_STATE_KEY = Symbol.for("@getstrata/discoverJobsState");
974
- function readDiscoverJobsState() {
975
- const existing = globalThis[DISCOVER_JOBS_STATE_KEY];
976
- if (existing) {
977
- return existing;
978
- }
979
- const state = {};
980
- globalThis[DISCOVER_JOBS_STATE_KEY] = state;
981
- return state;
982
- }
983
- function resolveJobsDirectory() {
984
- const fromCwd = join3(process.cwd(), "src", "jobs");
985
- if (existsSync2(fromCwd)) {
986
- 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
+ }
987
1167
  }
988
- return join3(import.meta.dir, "../jobs");
989
- }
990
- function isDiscoveredJobClass(value) {
991
- if (typeof value !== "function") {
992
- 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
+ }
993
1182
  }
994
- const jobName = value.jobName;
995
- return typeof jobName === "string" && jobName.length > 0;
1183
+ return [...jobs.keys()];
996
1184
  }
997
- function loadDiscoveredJobClasses() {
998
- const jobsDirectory = resolveJobsDirectory();
999
- let entries;
1000
- try {
1001
- entries = readdirSync3(jobsDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.(ts|js)$/.test(entry.name)).map((entry) => entry.name);
1002
- } catch (error) {
1003
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1004
- return [];
1005
- }
1185
+ function discoverJobs(options = {}) {
1186
+ const cache = state3().loads;
1187
+ const key = options.manifest ?? JSON.stringify([
1188
+ resolve3(options.directory ?? join4(process.cwd(), "src", "jobs")),
1189
+ [...options.exclude ?? []].sort()
1190
+ ]);
1191
+ const existing = cache.get(key);
1192
+ if (existing)
1193
+ return existing;
1194
+ const pending = load3(options, () => state3().loads === cache).catch((error) => {
1195
+ if (cache.get(key) === pending)
1196
+ cache.delete(key);
1006
1197
  throw error;
1007
- }
1008
- return entries.flatMap((fileName) => {
1009
- const filePath = join3(jobsDirectory, fileName);
1010
- const loaded = requireJob(filePath);
1011
- return isDiscoveredJobClass(loaded.default) ? [loaded.default] : [];
1012
1198
  });
1013
- }
1014
- function discoverJobs() {
1015
- const state = readDiscoverJobsState();
1016
- if (state.names) {
1017
- return state.names;
1018
- }
1019
- const names = [];
1020
- for (const JobClass of loadDiscoveredJobClasses()) {
1021
- jobRegistry.register(JobClass.jobName, () => new JobClass);
1022
- names.push(JobClass.jobName);
1023
- }
1024
- state.names = names;
1025
- return names;
1199
+ cache.set(key, pending);
1200
+ return pending;
1026
1201
  }
1027
1202
  function resetDiscoverJobsForTests() {
1028
- const state = readDiscoverJobsState();
1029
- state.names = undefined;
1203
+ state3().loads = new Map;
1204
+ state3().registered = new Map;
1030
1205
  }
1031
1206
 
1032
1207
  // ../../src/bootstrap/queue/defaultJobs.ts
@@ -1044,16 +1219,17 @@ function registerDefaultJobs() {
1044
1219
  // ../../src/bootstrap/providers/queue.ts
1045
1220
  var queueProvider = {
1046
1221
  name: "core.queue",
1047
- register({ container, config }) {
1222
+ async register({ container, config, onCleanup }) {
1048
1223
  const configuredDriver = process.env.QUEUE_DRIVER ?? DEFAULT_QUEUE_DRIVER;
1049
1224
  const driver = configuredDriver === "async" || configuredDriver === "redis" || configuredDriver === "sync" ? configuredDriver : DEFAULT_QUEUE_DRIVER;
1050
1225
  config.set("queue.driver", driver);
1051
1226
  const failedJobs = createFailedJobService();
1052
1227
  container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
1053
- container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, () => {
1054
- registerDefaultJobs();
1055
- discoverJobs();
1056
- }));
1228
+ registerDefaultJobs();
1229
+ await discoverJobs();
1230
+ const queue = createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs);
1231
+ onCleanup(() => queue.close?.(), driver === "async" ? "drain" : "close");
1232
+ container.set(CORE_QUEUE_TOKEN, queue);
1057
1233
  }
1058
1234
  };
1059
1235
  var queue_default = queueProvider;
@@ -1068,6 +1244,18 @@ var storageProvider = {
1068
1244
  };
1069
1245
  var storage_default = storageProvider;
1070
1246
 
1247
+ // ../../src/bootstrap/providers/tracing.ts
1248
+ import { acquireTracingRuntime } from "@getstrata/core/tracing/tracingMiddleware";
1249
+ var tracingProvider = {
1250
+ name: "core.tracing",
1251
+ register({ onCleanup }) {
1252
+ const ownership = acquireTracingRuntime();
1253
+ onCleanup(() => ownership.flush(), "flush");
1254
+ onCleanup(() => ownership.release(), "close");
1255
+ }
1256
+ };
1257
+ var tracing_default = tracingProvider;
1258
+
1071
1259
  // ../../src/bootstrap/providers/view.ts
1072
1260
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
1073
1261
  import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
@@ -1112,6 +1300,7 @@ var viewProvider = {
1112
1300
  // ../../src/bootstrap/providers/index.ts
1113
1301
  var coreProviders = [
1114
1302
  config_default,
1303
+ tracing_default,
1115
1304
  cache_default,
1116
1305
  storage_default,
1117
1306
  auth_default,
@@ -1126,36 +1315,127 @@ var coreProviders = [
1126
1315
  function collectProviders(modules = discoverModules()) {
1127
1316
  return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
1128
1317
  }
1129
- function runProviderPhase(providers, phase, context) {
1130
- for (const provider of providers) {
1131
- provider[phase]?.(context);
1132
- }
1318
+ async function runProviderPhase(providers, phase, context) {
1319
+ for (const provider of providers)
1320
+ await provider[phase]?.(context);
1133
1321
  }
1134
- function createAppContext() {
1322
+ var readyContext;
1323
+ async function createAppContext(providers) {
1324
+ if (!providers)
1325
+ await ensureModulesLoaded();
1326
+ const configured = [...providers ?? collectProviders()];
1135
1327
  const container = new ServiceContainer;
1136
1328
  const config = new ConfigStore;
1137
- const dependencies = {
1138
- 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;
1139
1347
  };
1140
- const context = {
1141
- container,
1142
- config,
1143
- 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;
1144
1394
  };
1145
- const providers = collectProviders();
1146
- runProviderPhase(providers, "register", context);
1147
- runProviderPhase(providers, "boot", context);
1148
- assertAppDependenciesComplete(dependencies);
1149
- const appContext = {
1395
+ const context = {
1150
1396
  container,
1151
1397
  config,
1152
- dependencies
1398
+ dependencies,
1399
+ onCleanup(handler, phase = "close") {
1400
+ if (disposed)
1401
+ throw new Error("Cannot register cleanup on a disposed application context.");
1402
+ if (phase !== "drain" && phase !== "flush" && phase !== "close")
1403
+ throw new TypeError("Unknown provider cleanup phase.");
1404
+ if (phase === "drain" && draining || phase === "flush" && flushing)
1405
+ throw new Error("Cannot register cleanup after its phase started.");
1406
+ (phase === "drain" ? drains : phase === "flush" ? flushes : cleanups).push(handler);
1407
+ }
1153
1408
  };
1154
- setActiveApplicationContext(appContext);
1155
- 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
+ }
1156
1436
  }
1157
1437
  // ../../src/bootstrap/createWebRoutes.ts
1158
- import { join as join4 } from "path";
1438
+ import { join as join5 } from "path";
1159
1439
  import { isViewsEnabled as isViewsEnabled3 } from "@getstrata/core/runtime/frontendMode";
1160
1440
  import { assertUrlPathUnderRoot } from "@getstrata/core/security/safePath";
1161
1441
  import { notFoundHtmlResponse } from "@getstrata/core/view";
@@ -1172,7 +1452,7 @@ function createWebRoutes(dependencies, options = {}) {
1172
1452
  registerRoute("GET", "/assets/*", ["global", "web"]);
1173
1453
  const pathname = new URL(request.url).pathname;
1174
1454
  try {
1175
- const filePath = assertUrlPathUnderRoot(join4(process.cwd(), "public"), pathname);
1455
+ const filePath = assertUrlPathUnderRoot(join5(process.cwd(), "public"), pathname);
1176
1456
  const file = Bun.file(filePath);
1177
1457
  if (!await file.exists()) {
1178
1458
  return notFoundHtmlResponse();
@@ -1195,8 +1475,43 @@ function mergeWebRoutes(dependencies, routes, options = {}) {
1195
1475
  };
1196
1476
  }
1197
1477
  // ../../src/bootstrap/dependencies.ts
1198
- function createAppDependencies() {
1199
- return createAppContext().dependencies;
1478
+ async function createAppDependencies() {
1479
+ return (await createAppContext()).dependencies;
1480
+ }
1481
+ // ../../src/bootstrap/discoverModels.ts
1482
+ import { readdir as readdir3 } from "fs/promises";
1483
+ import { join as join6 } from "path";
1484
+ import { pathToFileURL as pathToFileURL2 } from "url";
1485
+ import { bootModels, Model } from "@getstrata/core/database/model";
1486
+ async function discoverModels(directory = join6(process.cwd(), "src", "models"), options = {}) {
1487
+ const excluded = new Set([
1488
+ "register.ts",
1489
+ "register.js",
1490
+ "index.ts",
1491
+ "index.js",
1492
+ ...options.exclude ?? []
1493
+ ]);
1494
+ const models = new Set;
1495
+ async function visit(path) {
1496
+ const entries = await readdir3(path, { withFileTypes: true });
1497
+ entries.sort((a, b) => a.name.localeCompare(b.name));
1498
+ for (const entry of entries) {
1499
+ const file = join6(path, entry.name);
1500
+ if (entry.isDirectory())
1501
+ await visit(file);
1502
+ else if (!excluded.has(entry.name) && entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.d\.[cm]?ts$/.test(entry.name)) {
1503
+ const exports = await import(pathToFileURL2(file).href);
1504
+ for (const value of Object.values(exports)) {
1505
+ if (typeof value === "function" && value.prototype instanceof Model)
1506
+ models.add(value);
1507
+ }
1508
+ }
1509
+ }
1510
+ }
1511
+ await visit(directory);
1512
+ const result = [...models];
1513
+ bootModels(result);
1514
+ return result;
1200
1515
  }
1201
1516
  // ../../src/bootstrap/health.ts
1202
1517
  import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
@@ -1579,6 +1894,7 @@ function createRouteKernel(dependencies) {
1579
1894
  return createHttpKernel(dependencies);
1580
1895
  }
1581
1896
  // ../../src/bootstrap/web/server.ts
1897
+ import { resolveMaxBodyBytes } from "@getstrata/core/http/bodySizeLimitMiddleware";
1582
1898
  import { createCorsMiddleware as createCorsMiddleware2 } from "@getstrata/core/http/corsMiddleware";
1583
1899
  import { currentRequestMeta as currentRequestMeta2, runWithRequestMeta } from "@getstrata/core/http/requestMetaContext";
1584
1900
  import { assertUrlPathUnderRoot as assertUrlPathUnderRoot2 } from "@getstrata/core/security/safePath";
@@ -1590,29 +1906,32 @@ function socketAddress(server, request) {
1590
1906
  async function missingHtmlResponse() {
1591
1907
  return notFoundHtmlResponse2();
1592
1908
  }
1593
- function wrapRouteHandler(handler, path) {
1909
+ function wrapRouteHandler(handler, path, onRequest, track = (work) => work()) {
1594
1910
  return async (request, server) => {
1595
- return await runWithRequestMeta({
1911
+ return await track(async () => await runWithRequestMeta({
1596
1912
  ...currentRequestMeta2(),
1597
1913
  request,
1598
1914
  routeTemplate: path,
1599
1915
  ipAddress: socketAddress(server, request),
1600
1916
  userAgent: request.headers.get("user-agent")
1601
1917
  }, async () => {
1918
+ const intercepted = await onRequest?.(request, server);
1919
+ if (intercepted instanceof Response)
1920
+ return intercepted;
1602
1921
  const response = await handler(request);
1603
1922
  return response ?? await missingHtmlResponse();
1604
- });
1923
+ }));
1605
1924
  };
1606
1925
  }
1607
- function convertAppRoutesToBunRoutes(routes, cors = {}) {
1926
+ function convertAppRoutesToBunRoutes(routes, cors = {}, onRequest, track) {
1608
1927
  const bunRoutes = {};
1609
1928
  const middleware = createCorsMiddleware2(cors);
1610
1929
  const preflight = (request) => middleware(request, async () => new Response(null, { status: 405 }));
1611
1930
  for (const [path, handler] of Object.entries(routes)) {
1612
1931
  if (typeof handler === "function") {
1613
1932
  bunRoutes[path] = {
1614
- GET: wrapRouteHandler(handler, path),
1615
- OPTIONS: wrapRouteHandler(preflight, path)
1933
+ GET: wrapRouteHandler(handler, path, onRequest, track),
1934
+ OPTIONS: wrapRouteHandler(preflight, path, onRequest, track)
1616
1935
  };
1617
1936
  continue;
1618
1937
  }
@@ -1622,10 +1941,10 @@ function convertAppRoutesToBunRoutes(routes, cors = {}) {
1622
1941
  if (typeof methodHandler !== "function") {
1623
1942
  continue;
1624
1943
  }
1625
- methods[method.toUpperCase()] = wrapRouteHandler(methodHandler, path);
1944
+ methods[method.toUpperCase()] = wrapRouteHandler(methodHandler, path, onRequest, track);
1626
1945
  }
1627
1946
  if (Object.keys(methods).length > 0) {
1628
- methods.OPTIONS ??= wrapRouteHandler(preflight, path);
1947
+ methods.OPTIONS ??= wrapRouteHandler(preflight, path, onRequest, track);
1629
1948
  bunRoutes[path] = methods;
1630
1949
  }
1631
1950
  }
@@ -1633,19 +1952,47 @@ function convertAppRoutesToBunRoutes(routes, cors = {}) {
1633
1952
  return bunRoutes;
1634
1953
  }
1635
1954
  function createWebServer(options) {
1955
+ if (options.lifecycle?.isShuttingDown)
1956
+ throw new Error("Cannot start HTTP during shutdown.");
1957
+ const onRequest = (request, server) => {
1958
+ if (options.lifecycle?.isShuttingDown)
1959
+ return new Response("Service Unavailable", { status: 503 });
1960
+ return options.onRequest?.(request, server);
1961
+ };
1962
+ for (const field of ["fetch", "routes", "port", "unix"])
1963
+ if (options.native && field in options.native)
1964
+ throw new TypeError(`native.${field} is owned by createWebServer.`);
1965
+ const maxRequestBodySize = options.native?.maxRequestBodySize ?? resolveMaxBodyBytes();
1966
+ if (!Number.isSafeInteger(maxRequestBodySize) || maxRequestBodySize <= 0)
1967
+ throw new RangeError("maxRequestBodySize must be a positive safe integer.");
1968
+ const active = new Set;
1969
+ const track = (work) => {
1970
+ if (!options.lifecycle)
1971
+ return work();
1972
+ const running = work();
1973
+ active.add(running);
1974
+ return running.finally(() => {
1975
+ active.delete(running);
1976
+ });
1977
+ };
1636
1978
  const publicDir = options.publicDir ?? "./public";
1637
- const bunRoutes = options.routes ? convertAppRoutesToBunRoutes(options.routes, options.cors) : undefined;
1638
- 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,
1639
1984
  port: options.port,
1640
1985
  ...bunRoutes ? { routes: bunRoutes } : {},
1641
1986
  async fetch(request, server) {
1642
- return await runWithRequestMeta({
1987
+ return await track(async () => await runWithRequestMeta({
1643
1988
  ...currentRequestMeta2(),
1644
1989
  request,
1645
1990
  ipAddress: socketAddress(server, request),
1646
1991
  userAgent: request.headers.get("user-agent")
1647
1992
  }, async () => {
1648
- await options.onRequest?.(request);
1993
+ const intercepted = await onRequest(request, server);
1994
+ if (intercepted instanceof Response)
1995
+ return intercepted;
1649
1996
  const url = new URL(request.url);
1650
1997
  if (url.pathname.startsWith("/assets/")) {
1651
1998
  try {
@@ -1659,13 +2006,36 @@ function createWebServer(options) {
1659
2006
  }
1660
2007
  }
1661
2008
  if (options.handle) {
1662
- const response = await options.handle(request);
2009
+ const response = await options.handle(request, server);
2010
+ if (response === undefined && websocket)
2011
+ return;
1663
2012
  return response ?? await missingHtmlResponse();
1664
2013
  }
1665
2014
  return await missingHtmlResponse();
1666
- });
2015
+ }));
2016
+ }
2017
+ };
2018
+ const server = websocket ? Bun.serve({ ...configuration, websocket }) : Bun.serve({
2019
+ ...configuration,
2020
+ async fetch(request, server) {
2021
+ return await configuration.fetch(request, server) ?? await missingHtmlResponse();
1667
2022
  }
1668
2023
  });
2024
+ if (options.lifecycle) {
2025
+ const lifecycle = options.lifecycle;
2026
+ const name = `http:${crypto.randomUUID()}`;
2027
+ let draining;
2028
+ lifecycle.register(`${name}:stop`, () => {
2029
+ draining ??= server.stop(false);
2030
+ draining.catch(() => {});
2031
+ }, "stop");
2032
+ lifecycle.register(`${name}:drain`, async () => {
2033
+ await draining;
2034
+ await Promise.allSettled([...active]);
2035
+ }, "drain");
2036
+ lifecycle.register(`${name}:force`, () => server.stop(true), "force");
2037
+ }
2038
+ return server;
1669
2039
  }
1670
2040
  // ../../src/bootstrap/web/session.ts
1671
2041
  import { createHmac, randomBytes } from "crypto";
@@ -1944,8 +2314,10 @@ export {
1944
2314
  DATABASE_URL_CONFIG_KEY,
1945
2315
  DEFAULT_APP_PORT,
1946
2316
  REDIS_URL_CONFIG_KEY,
2317
+ RedisSchedulerLeaseStore,
1947
2318
  RouteRegistry,
1948
2319
  Schedule,
2320
+ SchedulerLeaseLostError,
1949
2321
  ServiceContainer,
1950
2322
  appSchedule,
1951
2323
  assertAppDependenciesComplete,
@@ -1957,6 +2329,7 @@ export {
1957
2329
  checkRedis,
1958
2330
  collectProviders,
1959
2331
  configureModulesDirectory,
2332
+ configureModulesManifest,
1960
2333
  coreProviders,
1961
2334
  createAppContext,
1962
2335
  createAppDependencies,
@@ -1969,6 +2342,7 @@ export {
1969
2342
  createWebServer,
1970
2343
  discoverJobs,
1971
2344
  discoverModelTableNames,
2345
+ discoverModels,
1972
2346
  discoverModules,
1973
2347
  ensureModulesLoaded,
1974
2348
  getRequiredDependency,
@@ -1997,6 +2371,7 @@ export {
1997
2371
  setActiveApplicationContext,
1998
2372
  slugify,
1999
2373
  toRouteRequest,
2374
+ tracing_default as tracingProvider,
2000
2375
  wrapSecuredRouteModelByKey,
2001
2376
  wrapWebLogin,
2002
2377
  wrapWebRegister