@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
@@ -53,8 +53,11 @@ import {
53
53
  CORE_CACHE_TOKEN,
54
54
  CORE_CONFIG_TOKEN,
55
55
  CORE_EVENT_BUS_TOKEN,
56
+ CORE_HTTP_CLEANUP_TOKEN,
56
57
  CORE_POLICY_GATE_TOKEN,
57
58
  CORE_QUEUE_TOKEN,
59
+ CORE_TENANT_RESOLVER_TOKEN,
60
+ CORE_THROTTLE_QUOTA_POLICY_TOKEN,
58
61
  CORE_TOKEN_SERVICE_TOKEN
59
62
  } from "@getstrata/core/contracts/serviceTokens";
60
63
  var APP_PORT_CONFIG_KEY = "app.port";
@@ -120,8 +123,39 @@ function resolveRegisterRateLimit() {
120
123
  // ../../src/bootstrap/httpKernel.ts
121
124
  class HttpKernel {
122
125
  dependencies;
126
+ ownedThrottles = new Set;
127
+ disposed = false;
123
128
  constructor(dependencies) {
124
129
  this.dependencies = dependencies;
130
+ if (dependencies.container.has(CORE_HTTP_CLEANUP_TOKEN)) {
131
+ dependencies.container.resolve(CORE_HTTP_CLEANUP_TOKEN)(() => this.dispose());
132
+ }
133
+ }
134
+ dispose() {
135
+ this.disposed = true;
136
+ const owned = [...this.ownedThrottles];
137
+ this.ownedThrottles.clear();
138
+ const errors = [];
139
+ for (const throttle of owned.reverse()) {
140
+ try {
141
+ throttle.dispose();
142
+ } catch (error) {
143
+ errors.push(error);
144
+ }
145
+ }
146
+ if (errors.length)
147
+ throw new AggregateError(errors, "HTTP throttle cleanup failed.");
148
+ }
149
+ ownThrottle(throttle) {
150
+ if (this.disposed) {
151
+ if ("dispose" in throttle && typeof throttle.dispose === "function")
152
+ throttle.dispose();
153
+ throw new Error("HTTP kernel is disposed.");
154
+ }
155
+ if ("dispose" in throttle && typeof throttle.dispose === "function") {
156
+ this.ownedThrottles.add(throttle);
157
+ }
158
+ return throttle;
125
159
  }
126
160
  globalMiddleware(errorFormat = "json") {
127
161
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
@@ -136,7 +170,9 @@ class HttpKernel {
136
170
  errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
137
171
  createAuthMiddleware(auth),
138
172
  createMembershipMiddleware(),
139
- createTenantMiddleware()
173
+ createTenantMiddleware({
174
+ resolveTenant: this.dependencies.container.has(CORE_TENANT_RESOLVER_TOKEN) ? this.dependencies.container.resolve(CORE_TENANT_RESOLVER_TOKEN) : undefined
175
+ })
140
176
  ];
141
177
  }
142
178
  group(name) {
@@ -151,25 +187,28 @@ class HttpKernel {
151
187
  if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
152
188
  return [csrf];
153
189
  }
190
+ const quotaPolicy = this.dependencies.container.has(CORE_THROTTLE_QUOTA_POLICY_TOKEN) ? this.dependencies.container.resolve(CORE_THROTTLE_QUOTA_POLICY_TOKEN) : undefined;
154
191
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
155
192
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
156
193
  if (!redisUrl) {
157
194
  const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
158
195
  return [
159
- createMemoryThrottleMiddleware({
196
+ this.ownThrottle(createMemoryThrottleMiddleware({
160
197
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
161
- decaySeconds: 60
162
- }),
198
+ decaySeconds: 60,
199
+ quotaPolicy
200
+ })),
163
201
  csrf
164
202
  ];
165
203
  }
166
204
  const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
167
205
  return [
168
- createThrottleMiddleware({
206
+ this.ownThrottle(createThrottleMiddleware({
169
207
  redisUrl,
170
208
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
171
- decaySeconds: 60
172
- }),
209
+ decaySeconds: 60,
210
+ quotaPolicy
211
+ })),
173
212
  csrf
174
213
  ];
175
214
  }
@@ -299,25 +338,25 @@ class HttpKernel {
299
338
  const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
300
339
  const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
301
340
  if (scope === "login") {
302
- return withMiddleware(createLoginThrottleMiddleware({
341
+ return withMiddleware(this.ownThrottle(createLoginThrottleMiddleware({
303
342
  ...redisUrl ? { redisUrl } : {},
304
343
  maxAttempts: rateLimit.maxAttempts,
305
344
  decaySeconds: rateLimit.decaySeconds
306
- }))(handler);
345
+ })))(handler);
307
346
  }
308
347
  if (redisUrl) {
309
- return withMiddleware(createThrottleMiddleware({
348
+ return withMiddleware(this.ownThrottle(createThrottleMiddleware({
310
349
  redisUrl,
311
350
  maxAttempts: rateLimit.maxAttempts,
312
351
  decaySeconds: rateLimit.decaySeconds,
313
352
  keyPrefix: memoryKeyPrefix
314
- }))(handler);
353
+ })))(handler);
315
354
  }
316
- return withMiddleware(createMemoryThrottleMiddleware({
355
+ return withMiddleware(this.ownThrottle(createMemoryThrottleMiddleware({
317
356
  maxAttempts: rateLimit.maxAttempts,
318
357
  decaySeconds: rateLimit.decaySeconds,
319
358
  keyPrefix: memoryKeyPrefix
320
- }))(handler);
359
+ })))(handler);
321
360
  }
322
361
  }
323
362
  function createHttpKernel(dependencies) {
@@ -325,74 +364,180 @@ function createHttpKernel(dependencies) {
325
364
  }
326
365
 
327
366
  // ../../src/bootstrap/discoverModules.ts
328
- import { readdirSync } from "fs";
329
- import { join } from "path";
367
+ import { readdir as readdir2 } from "fs/promises";
368
+ import { join as join2, resolve } from "path";
369
+
370
+ // ../../src/bootstrap/infrastructureDiscovery.ts
371
+ import { readdir } from "fs/promises";
372
+ import { join, relative } from "path";
330
373
  import { pathToFileURL } from "url";
331
- var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
332
- function readDiscoverModulesState() {
333
- const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
334
- if (existing) {
335
- return existing;
336
- }
337
- const state = { appModules: [] };
338
- globalThis[DISCOVER_MODULES_STATE_KEY] = state;
339
- return state;
340
- }
341
- function configureModulesDirectory(modulesDir) {
342
- const state = readDiscoverModulesState();
343
- if (state.configuredModulesDir !== modulesDir) {
344
- state.appModules.length = 0;
345
- state.modulesReady = undefined;
346
- }
347
- state.configuredModulesDir = modulesDir;
348
- }
349
- function resolveModulesDirectory(options) {
350
- const state = readDiscoverModulesState();
351
- if (options?.modulesDir) {
352
- return options.modulesDir;
374
+ async function infrastructureFiles(directory, exclude = []) {
375
+ const excluded = new Set([
376
+ "index.ts",
377
+ "index.js",
378
+ "index.mts",
379
+ "index.mjs",
380
+ "register.ts",
381
+ "register.js",
382
+ "register.mts",
383
+ "register.mjs",
384
+ ...exclude
385
+ ]);
386
+ const files = [];
387
+ async function visit(path) {
388
+ const entries = await readdir(path, { withFileTypes: true });
389
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
390
+ for (const entry of entries) {
391
+ const file = join(path, entry.name);
392
+ const key = relative(directory, file).replaceAll("\\", "/");
393
+ if (excluded.has(entry.name) || excluded.has(key))
394
+ continue;
395
+ if (entry.isDirectory())
396
+ await visit(file);
397
+ else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
398
+ files.push(file);
399
+ }
353
400
  }
354
- if (state.configuredModulesDir) {
355
- return state.configuredModulesDir;
401
+ try {
402
+ await visit(directory);
403
+ } catch (error) {
404
+ if (error.code !== "ENOENT")
405
+ throw error;
406
+ if (error.path !== directory)
407
+ throw error;
356
408
  }
357
- throw new Error("configureModulesDirectory() must be called before discovering modules. The app preload (or starter) should call it.");
409
+ return files;
358
410
  }
359
- async function loadDiscoveredModules(options) {
360
- const modulesDirectory = resolveModulesDirectory(options);
361
- let moduleNames;
411
+ async function importInfrastructure(file) {
362
412
  try {
363
- moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
413
+ return await import(pathToFileURL(file).href);
364
414
  } catch (error) {
365
- if (error.code === "ENOENT") {
366
- return [];
415
+ throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
416
+ }
417
+ }
418
+
419
+ // ../../src/bootstrap/discoverModules.ts
420
+ var KEY = Symbol.for("@getstrata/discoverModulesState");
421
+ function state() {
422
+ const globals = globalThis;
423
+ globals[KEY] ??= { appModules: [] };
424
+ return globals[KEY];
425
+ }
426
+ function configureModulesDirectory(modulesDir) {
427
+ const current = state();
428
+ const directory = resolve(modulesDir);
429
+ if (current.configuredModulesDir !== directory || current.configuredManifest) {
430
+ current.appModules.length = 0;
431
+ current.modulesReady = undefined;
432
+ current.source = undefined;
433
+ }
434
+ current.configuredManifest = undefined;
435
+ current.configuredModulesDir = directory;
436
+ }
437
+ function configureModulesManifest(manifest) {
438
+ const current = state();
439
+ if (current.configuredManifest !== manifest) {
440
+ current.appModules.length = 0;
441
+ current.modulesReady = undefined;
442
+ current.source = undefined;
443
+ }
444
+ current.configuredManifest = manifest;
445
+ }
446
+ function validateModule(value, location) {
447
+ if (!value || typeof value !== "object" || Array.isArray(value))
448
+ throw new TypeError(`Invalid application module at ${location}.`);
449
+ const module = value;
450
+ if (typeof module.name !== "string" || !module.name.trim())
451
+ throw new TypeError(`Invalid module name at ${location}.`);
452
+ if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
453
+ throw new TypeError(`Invalid module order at ${location}.`);
454
+ for (const hook of ["routes", "webRoutes"])
455
+ if (module[hook] !== undefined && typeof module[hook] !== "function")
456
+ throw new TypeError(`Invalid ${hook} at ${location}.`);
457
+ if (module.providers !== undefined) {
458
+ if (!Array.isArray(module.providers))
459
+ throw new TypeError(`Invalid providers at ${location}.`);
460
+ for (const provider of module.providers) {
461
+ if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
462
+ throw new TypeError(`Invalid provider at ${location}.`);
367
463
  }
368
- throw error;
369
464
  }
370
- const modules = await Promise.all(moduleNames.map(async (moduleName) => {
371
- const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
372
- const loaded = await import(moduleUrl);
373
- return loaded.default;
374
- }));
375
- return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
376
465
  }
377
- async function ensureModulesLoaded(options) {
378
- const state = readDiscoverModulesState();
379
- if (state.appModules.length > 0) {
380
- return state.appModules;
381
- }
382
- state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
383
- state.appModules.splice(0, state.appModules.length, ...modules);
384
- return state.appModules;
466
+ async function load(options) {
467
+ const manifest = options.manifest ?? (options.modulesDir === undefined ? state().configuredManifest : undefined);
468
+ const entries = [];
469
+ if (manifest)
470
+ for (const [index, value] of manifest.entries())
471
+ entries.push({ value, location: `module manifest[${index}]` });
472
+ else {
473
+ const directory = options.modulesDir ?? state().configuredModulesDir;
474
+ if (!directory)
475
+ throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
476
+ let folders;
477
+ try {
478
+ folders = await readdir2(directory, { withFileTypes: true });
479
+ } catch (error) {
480
+ if (error.code === "ENOENT")
481
+ return [];
482
+ throw error;
483
+ }
484
+ const excluded = new Set(options.exclude ?? []);
485
+ 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)) {
486
+ const path = join2(directory, folder.name);
487
+ const files = await readdir2(path);
488
+ const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
489
+ if (candidates.length !== 1)
490
+ throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
491
+ const file = join2(path, candidates[0]);
492
+ const exports = await importInfrastructure(file);
493
+ entries.push({ value: exports.default, location: file });
494
+ }
495
+ }
496
+ const names = new Map;
497
+ const modules = [];
498
+ for (const { value, location } of entries) {
499
+ validateModule(value, location);
500
+ const previous = names.get(value.name);
501
+ if (previous)
502
+ throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
503
+ names.set(value.name, location);
504
+ modules.push(value);
505
+ }
506
+ return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
507
+ }
508
+ function ensureModulesLoaded(options = {}) {
509
+ const current = state();
510
+ const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
511
+ options.modulesDir ? resolve(options.modulesDir) : current.configuredModulesDir,
512
+ [...options.exclude ?? []].sort()
513
+ ]);
514
+ if (current.modulesReady && current.source === key)
515
+ return current.modulesReady;
516
+ current.appModules.length = 0;
517
+ current.source = key;
518
+ const pending = load(options).then((modules) => {
519
+ if (current.modulesReady !== pending)
520
+ return modules;
521
+ current.appModules.splice(0, current.appModules.length, ...modules);
522
+ return current.appModules;
523
+ }).catch((error) => {
524
+ if (current.modulesReady === pending)
525
+ current.modulesReady = undefined;
526
+ throw error;
385
527
  });
386
- return state.modulesReady;
528
+ current.modulesReady = pending;
529
+ return pending;
387
530
  }
388
531
  function discoverModules() {
389
- return readDiscoverModulesState().appModules;
532
+ return state().appModules;
390
533
  }
391
534
  function resetDiscoverModulesForTests() {
392
- const state = readDiscoverModulesState();
393
- state.configuredModulesDir = undefined;
394
- state.appModules.length = 0;
395
- state.modulesReady = undefined;
535
+ const current = state();
536
+ current.configuredModulesDir = undefined;
537
+ current.configuredManifest = undefined;
538
+ current.appModules.length = 0;
539
+ current.modulesReady = undefined;
540
+ current.source = undefined;
396
541
  }
397
542
  // ../../src/bootstrap/prefixRouteMap.ts
398
543
  function prefixRouteMap(prefix, routes) {
@@ -1,73 +1,179 @@
1
1
  // @bun
2
2
  // ../../src/bootstrap/discoverModules.ts
3
- import { readdirSync } from "fs";
4
- import { join } from "path";
3
+ import { readdir as readdir2 } from "fs/promises";
4
+ import { join as join2, resolve } from "path";
5
+
6
+ // ../../src/bootstrap/infrastructureDiscovery.ts
7
+ import { readdir } from "fs/promises";
8
+ import { join, relative } from "path";
5
9
  import { pathToFileURL } from "url";
6
- var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
7
- function readDiscoverModulesState() {
8
- const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
9
- if (existing) {
10
- return existing;
10
+ async function infrastructureFiles(directory, exclude = []) {
11
+ const excluded = new Set([
12
+ "index.ts",
13
+ "index.js",
14
+ "index.mts",
15
+ "index.mjs",
16
+ "register.ts",
17
+ "register.js",
18
+ "register.mts",
19
+ "register.mjs",
20
+ ...exclude
21
+ ]);
22
+ const files = [];
23
+ async function visit(path) {
24
+ const entries = await readdir(path, { withFileTypes: true });
25
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
26
+ for (const entry of entries) {
27
+ const file = join(path, entry.name);
28
+ const key = relative(directory, file).replaceAll("\\", "/");
29
+ if (excluded.has(entry.name) || excluded.has(key))
30
+ continue;
31
+ if (entry.isDirectory())
32
+ await visit(file);
33
+ else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
34
+ files.push(file);
35
+ }
36
+ }
37
+ try {
38
+ await visit(directory);
39
+ } catch (error) {
40
+ if (error.code !== "ENOENT")
41
+ throw error;
42
+ if (error.path !== directory)
43
+ throw error;
11
44
  }
12
- const state = { appModules: [] };
13
- globalThis[DISCOVER_MODULES_STATE_KEY] = state;
14
- return state;
45
+ return files;
15
46
  }
16
- function configureModulesDirectory(modulesDir) {
17
- const state = readDiscoverModulesState();
18
- if (state.configuredModulesDir !== modulesDir) {
19
- state.appModules.length = 0;
20
- state.modulesReady = undefined;
47
+ async function importInfrastructure(file) {
48
+ try {
49
+ return await import(pathToFileURL(file).href);
50
+ } catch (error) {
51
+ throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
21
52
  }
22
- state.configuredModulesDir = modulesDir;
23
53
  }
24
- function resolveModulesDirectory(options) {
25
- const state = readDiscoverModulesState();
26
- if (options?.modulesDir) {
27
- return options.modulesDir;
54
+
55
+ // ../../src/bootstrap/discoverModules.ts
56
+ var KEY = Symbol.for("@getstrata/discoverModulesState");
57
+ function state() {
58
+ const globals = globalThis;
59
+ globals[KEY] ??= { appModules: [] };
60
+ return globals[KEY];
61
+ }
62
+ function configureModulesDirectory(modulesDir) {
63
+ const current = state();
64
+ const directory = resolve(modulesDir);
65
+ if (current.configuredModulesDir !== directory || current.configuredManifest) {
66
+ current.appModules.length = 0;
67
+ current.modulesReady = undefined;
68
+ current.source = undefined;
28
69
  }
29
- if (state.configuredModulesDir) {
30
- return state.configuredModulesDir;
70
+ current.configuredManifest = undefined;
71
+ current.configuredModulesDir = directory;
72
+ }
73
+ function configureModulesManifest(manifest) {
74
+ const current = state();
75
+ if (current.configuredManifest !== manifest) {
76
+ current.appModules.length = 0;
77
+ current.modulesReady = undefined;
78
+ current.source = undefined;
31
79
  }
32
- throw new Error("configureModulesDirectory() must be called before discovering modules. The app preload (or starter) should call it.");
80
+ current.configuredManifest = manifest;
33
81
  }
34
- async function loadDiscoveredModules(options) {
35
- const modulesDirectory = resolveModulesDirectory(options);
36
- let moduleNames;
37
- try {
38
- moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
39
- } catch (error) {
40
- if (error.code === "ENOENT") {
41
- return [];
82
+ function validateModule(value, location) {
83
+ if (!value || typeof value !== "object" || Array.isArray(value))
84
+ throw new TypeError(`Invalid application module at ${location}.`);
85
+ const module = value;
86
+ if (typeof module.name !== "string" || !module.name.trim())
87
+ throw new TypeError(`Invalid module name at ${location}.`);
88
+ if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
89
+ throw new TypeError(`Invalid module order at ${location}.`);
90
+ for (const hook of ["routes", "webRoutes"])
91
+ if (module[hook] !== undefined && typeof module[hook] !== "function")
92
+ throw new TypeError(`Invalid ${hook} at ${location}.`);
93
+ if (module.providers !== undefined) {
94
+ if (!Array.isArray(module.providers))
95
+ throw new TypeError(`Invalid providers at ${location}.`);
96
+ for (const provider of module.providers) {
97
+ if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
98
+ throw new TypeError(`Invalid provider at ${location}.`);
42
99
  }
43
- throw error;
44
100
  }
45
- const modules = await Promise.all(moduleNames.map(async (moduleName) => {
46
- const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
47
- const loaded = await import(moduleUrl);
48
- return loaded.default;
49
- }));
50
- return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
51
101
  }
52
- async function ensureModulesLoaded(options) {
53
- const state = readDiscoverModulesState();
54
- if (state.appModules.length > 0) {
55
- return state.appModules;
102
+ async function load(options) {
103
+ const manifest = options.manifest ?? (options.modulesDir === undefined ? state().configuredManifest : undefined);
104
+ const entries = [];
105
+ if (manifest)
106
+ for (const [index, value] of manifest.entries())
107
+ entries.push({ value, location: `module manifest[${index}]` });
108
+ else {
109
+ const directory = options.modulesDir ?? state().configuredModulesDir;
110
+ if (!directory)
111
+ throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
112
+ let folders;
113
+ try {
114
+ folders = await readdir2(directory, { withFileTypes: true });
115
+ } catch (error) {
116
+ if (error.code === "ENOENT")
117
+ return [];
118
+ throw error;
119
+ }
120
+ const excluded = new Set(options.exclude ?? []);
121
+ 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)) {
122
+ const path = join2(directory, folder.name);
123
+ const files = await readdir2(path);
124
+ const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
125
+ if (candidates.length !== 1)
126
+ throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
127
+ const file = join2(path, candidates[0]);
128
+ const exports = await importInfrastructure(file);
129
+ entries.push({ value: exports.default, location: file });
130
+ }
131
+ }
132
+ const names = new Map;
133
+ const modules = [];
134
+ for (const { value, location } of entries) {
135
+ validateModule(value, location);
136
+ const previous = names.get(value.name);
137
+ if (previous)
138
+ throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
139
+ names.set(value.name, location);
140
+ modules.push(value);
56
141
  }
57
- state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
58
- state.appModules.splice(0, state.appModules.length, ...modules);
59
- return state.appModules;
142
+ return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
143
+ }
144
+ function ensureModulesLoaded(options = {}) {
145
+ const current = state();
146
+ const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
147
+ options.modulesDir ? resolve(options.modulesDir) : current.configuredModulesDir,
148
+ [...options.exclude ?? []].sort()
149
+ ]);
150
+ if (current.modulesReady && current.source === key)
151
+ return current.modulesReady;
152
+ current.appModules.length = 0;
153
+ current.source = key;
154
+ const pending = load(options).then((modules) => {
155
+ if (current.modulesReady !== pending)
156
+ return modules;
157
+ current.appModules.splice(0, current.appModules.length, ...modules);
158
+ return current.appModules;
159
+ }).catch((error) => {
160
+ if (current.modulesReady === pending)
161
+ current.modulesReady = undefined;
162
+ throw error;
60
163
  });
61
- return state.modulesReady;
164
+ current.modulesReady = pending;
165
+ return pending;
62
166
  }
63
167
  function discoverModules() {
64
- return readDiscoverModulesState().appModules;
168
+ return state().appModules;
65
169
  }
66
170
  function resetDiscoverModulesForTests() {
67
- const state = readDiscoverModulesState();
68
- state.configuredModulesDir = undefined;
69
- state.appModules.length = 0;
70
- state.modulesReady = undefined;
171
+ const current = state();
172
+ current.configuredModulesDir = undefined;
173
+ current.configuredManifest = undefined;
174
+ current.appModules.length = 0;
175
+ current.modulesReady = undefined;
176
+ current.source = undefined;
71
177
  }
72
178
 
73
179
  // ../../src/bootstrap/cache/modelCacheTags.ts
@@ -7,8 +7,11 @@ import {
7
7
  CORE_CACHE_TOKEN,
8
8
  CORE_CONFIG_TOKEN,
9
9
  CORE_EVENT_BUS_TOKEN,
10
+ CORE_HTTP_CLEANUP_TOKEN,
10
11
  CORE_POLICY_GATE_TOKEN,
11
12
  CORE_QUEUE_TOKEN,
13
+ CORE_TENANT_RESOLVER_TOKEN,
14
+ CORE_THROTTLE_QUOTA_POLICY_TOKEN,
12
15
  CORE_TOKEN_SERVICE_TOKEN
13
16
  } from "@getstrata/core/contracts/serviceTokens";
14
17
  var APP_PORT_CONFIG_KEY = "app.port";
@@ -36,8 +39,11 @@ export {
36
39
  CORE_CACHE_TOKEN,
37
40
  CORE_CONFIG_TOKEN,
38
41
  CORE_EVENT_BUS_TOKEN,
42
+ CORE_HTTP_CLEANUP_TOKEN,
39
43
  CORE_POLICY_GATE_TOKEN,
40
44
  CORE_QUEUE_TOKEN,
45
+ CORE_TENANT_RESOLVER_TOKEN,
46
+ CORE_THROTTLE_QUOTA_POLICY_TOKEN,
41
47
  CORE_TOKEN_SERVICE_TOKEN,
42
48
  DATABASE_URL_CONFIG_KEY,
43
49
  DEFAULT_API_TOKEN,