@c9up/ream 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -9
- package/dist/Application.d.ts +85 -5
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +171 -21
- package/dist/Application.js.map +1 -1
- package/dist/ErrorBoundary.d.ts +1 -7
- package/dist/ErrorBoundary.d.ts.map +1 -1
- package/dist/ErrorBoundary.js +31 -31
- package/dist/ErrorBoundary.js.map +1 -1
- package/dist/HttpKernel.js +2 -2
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +18 -49
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +211 -173
- package/dist/Ignitor.js.map +1 -1
- package/dist/env/Env.d.ts +6 -0
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +6 -0
- package/dist/env/Env.js.map +1 -1
- package/dist/events/Emitter.d.ts +0 -6
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +42 -42
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/native.d.ts +13 -11
- package/dist/events/native.d.ts.map +1 -1
- package/dist/events/native.js.map +1 -1
- package/dist/helpers/compose.d.ts +30 -0
- package/dist/helpers/compose.d.ts.map +1 -0
- package/dist/helpers/compose.js +27 -0
- package/dist/helpers/compose.js.map +1 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +1 -0
- package/dist/helpers/index.js.map +1 -1
- package/dist/helpers/is.d.ts +69 -16
- package/dist/helpers/is.d.ts.map +1 -1
- package/dist/helpers/is.js +79 -16
- package/dist/helpers/is.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.d.ts +9 -3
- package/dist/middleware/Pipeline.d.ts.map +1 -1
- package/dist/middleware/Pipeline.js +10 -4
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/native/generated.d.ts +243 -0
- package/dist/native/generated.d.ts.map +1 -0
- package/dist/native/generated.js +7 -0
- package/dist/native/generated.js.map +1 -0
- package/dist/rpc/RpcRouter.js +2 -2
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/services/app.d.ts +6 -0
- package/dist/services/app.d.ts.map +1 -1
- package/dist/services/app.js +8 -0
- package/dist/services/app.js.map +1 -1
- package/dist/services/config.d.ts +15 -0
- package/dist/services/config.d.ts.map +1 -0
- package/dist/services/config.js +18 -0
- package/dist/services/config.js.map +1 -0
- package/dist/services/encryption.d.ts +23 -0
- package/dist/services/encryption.d.ts.map +1 -0
- package/dist/services/encryption.js +33 -0
- package/dist/services/encryption.js.map +1 -0
- package/dist/services/urlBuilder.d.ts +21 -0
- package/dist/services/urlBuilder.d.ts.map +1 -0
- package/dist/services/urlBuilder.js +34 -0
- package/dist/services/urlBuilder.js.map +1 -0
- package/dist/session/drivers/CookieDriver.d.ts +1 -1
- package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
- package/dist/session/drivers/CookieDriver.js +4 -4
- package/dist/session/drivers/CookieDriver.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts +44 -44
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +48 -48
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/RequestBuilder.d.ts +39 -39
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +49 -49
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +5 -5
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +7 -7
- package/dist/testing/TestClient.js.map +1 -1
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +15 -2
- package/scheduler.win32-x64-msvc.node +0 -0
- package/scripts/build-napi-types.mjs +68 -0
- package/scripts/generate-napi-types.mjs +171 -0
- package/src/Application.ts +221 -21
- package/src/ErrorBoundary.ts +31 -31
- package/src/HttpKernel.ts +2 -2
- package/src/Ignitor.ts +228 -176
- package/src/env/Env.ts +6 -0
- package/src/events/Emitter.ts +42 -42
- package/src/events/native.ts +21 -14
- package/src/helpers/compose.ts +84 -0
- package/src/helpers/index.ts +1 -0
- package/src/helpers/is.ts +87 -16
- package/src/index.ts +3 -3
- package/src/middleware/Pipeline.ts +10 -4
- package/src/native/generated.ts +291 -0
- package/src/rpc/RpcRouter.ts +2 -2
- package/src/services/app.ts +9 -0
- package/src/services/config.ts +24 -0
- package/src/services/encryption.ts +42 -0
- package/src/services/urlBuilder.ts +41 -0
- package/src/session/drivers/CookieDriver.ts +4 -4
- package/src/testing/ApiResponse.ts +52 -52
- package/src/testing/RequestBuilder.ts +55 -55
- package/src/testing/TestClient.ts +8 -8
package/src/Application.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* @implements FR17, FR20
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { readFileSync } from 'node:fs'
|
|
16
17
|
import { join, relative } from 'node:path'
|
|
17
18
|
import { fileURLToPath } from 'node:url'
|
|
18
19
|
import { ConfigStore } from './ConfigLoader.js'
|
|
@@ -29,17 +30,45 @@ import { callProviderPhase } from './Provider.js'
|
|
|
29
30
|
*/
|
|
30
31
|
export type ApplicationMode = 'run' | 'warmup'
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* What the process is running as (AdonisJS `AppEnvironments`).
|
|
35
|
+
*
|
|
36
|
+
* Providers and preloads are filtered on it — a console-only provider must not
|
|
37
|
+
* boot inside an HTTP request. `repl` is absent because ream has no REPL; it
|
|
38
|
+
* belongs here the day one exists, not before.
|
|
39
|
+
*/
|
|
40
|
+
export type AppEnvironment = 'web' | 'console' | 'test' | 'unknown'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Where the application is in its lifecycle (AdonisJS `ApplicationStates`).
|
|
44
|
+
*
|
|
45
|
+
* Read it rather than inferring from a boolean: "not booted" covers both
|
|
46
|
+
* "starting up" and "already shut down", and code that guards on the wrong one
|
|
47
|
+
* runs at exactly the wrong moment.
|
|
48
|
+
*/
|
|
49
|
+
export type ApplicationState =
|
|
50
|
+
| 'created'
|
|
51
|
+
| 'initiated'
|
|
52
|
+
| 'booted'
|
|
53
|
+
| 'ready'
|
|
54
|
+
| 'terminating'
|
|
55
|
+
| 'terminated'
|
|
56
|
+
|
|
32
57
|
export class Application implements AppContext {
|
|
33
58
|
readonly container: Container
|
|
34
59
|
readonly config: ConfigStore
|
|
35
60
|
#appRoot?: URL
|
|
36
61
|
#directories: DirectoriesNode = { ...defaultDirectories }
|
|
37
|
-
|
|
38
|
-
|
|
62
|
+
#providers: ProviderContract[] = []
|
|
63
|
+
#booted = false
|
|
64
|
+
#environment: AppEnvironment = 'unknown'
|
|
65
|
+
#state: ApplicationState = 'created'
|
|
66
|
+
#packageJsonRead = false
|
|
67
|
+
#packageJsonCache: { name?: string; version?: string } | undefined
|
|
39
68
|
#mode: ApplicationMode = 'run'
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
69
|
+
#bootingHooks: Array<() => Promise<void> | void> = []
|
|
70
|
+
#bootedHooks: Array<() => Promise<void> | void> = []
|
|
71
|
+
#shutdownHooks: Array<() => Promise<void> | void> = []
|
|
43
72
|
|
|
44
73
|
constructor() {
|
|
45
74
|
this.container = new Container()
|
|
@@ -69,7 +98,7 @@ export class Application implements AppContext {
|
|
|
69
98
|
* otherwise would leave the app half-started.
|
|
70
99
|
*/
|
|
71
100
|
setMode(mode: ApplicationMode): this {
|
|
72
|
-
if (this
|
|
101
|
+
if (this.#booted) {
|
|
73
102
|
throw new Error(`Cannot switch to '${mode}' mode: the application is already booted.`)
|
|
74
103
|
}
|
|
75
104
|
this.#mode = mode
|
|
@@ -313,7 +342,7 @@ export class Application implements AppContext {
|
|
|
313
342
|
* Like AdonisJS app.booting().
|
|
314
343
|
*/
|
|
315
344
|
booting(callback: () => Promise<void> | void): void {
|
|
316
|
-
this.
|
|
345
|
+
this.#bootingHooks.push(callback)
|
|
317
346
|
}
|
|
318
347
|
|
|
319
348
|
/**
|
|
@@ -321,7 +350,7 @@ export class Application implements AppContext {
|
|
|
321
350
|
* Like AdonisJS app.booted().
|
|
322
351
|
*/
|
|
323
352
|
booted(callback: () => Promise<void> | void): void {
|
|
324
|
-
if (this
|
|
353
|
+
if (this.#booted) {
|
|
325
354
|
// Already booted — run immediately. Invoke callback() INSIDE the
|
|
326
355
|
// promise chain (not as `Promise.resolve(callback())`, which
|
|
327
356
|
// evaluates callback() eagerly and lets a synchronous throw
|
|
@@ -334,7 +363,7 @@ export class Application implements AppContext {
|
|
|
334
363
|
})
|
|
335
364
|
return
|
|
336
365
|
}
|
|
337
|
-
this.
|
|
366
|
+
this.#bootedHooks.push(callback)
|
|
338
367
|
}
|
|
339
368
|
|
|
340
369
|
// ─── Signal handling ──────────────────────────────────────
|
|
@@ -354,6 +383,166 @@ export class Application implements AppContext {
|
|
|
354
383
|
process.on(signal, callback)
|
|
355
384
|
}
|
|
356
385
|
|
|
386
|
+
/**
|
|
387
|
+
* What the process is running as (AdonisJS `getEnvironment`).
|
|
388
|
+
*
|
|
389
|
+
* Lives on the application, not on the Ignitor, because that is where a
|
|
390
|
+
* provider reads it: `if (app.getEnvironment() !== 'web') return`.
|
|
391
|
+
*/
|
|
392
|
+
getEnvironment(): AppEnvironment {
|
|
393
|
+
return this.#environment
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Declare what the process is running as (AdonisJS `setEnvironment`).
|
|
398
|
+
*
|
|
399
|
+
* Refused after boot: providers and preloads were already filtered on the
|
|
400
|
+
* old value, so changing it afterwards would describe an application that
|
|
401
|
+
* was never assembled.
|
|
402
|
+
*/
|
|
403
|
+
setEnvironment(environment: AppEnvironment): this {
|
|
404
|
+
if (this.#booted) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`Cannot switch to the '${environment}' environment: the application is already booted.`,
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
this.#environment = environment
|
|
410
|
+
return this
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** `process.env.NODE_ENV`, or `'unknown'` (AdonisJS `nodeEnvironment`). */
|
|
414
|
+
get nodeEnvironment(): string {
|
|
415
|
+
return process.env.NODE_ENV ?? 'unknown'
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Where the application is in its lifecycle (AdonisJS `getState`). */
|
|
419
|
+
getState(): ApplicationState {
|
|
420
|
+
return this.#state
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** True once boot and every ready hook have completed (AdonisJS `isReady`). */
|
|
424
|
+
get isReady(): boolean {
|
|
425
|
+
return this.#state === 'ready'
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** True while shutdown hooks are running (AdonisJS `isTerminating`). */
|
|
429
|
+
get isTerminating(): boolean {
|
|
430
|
+
return this.#state === 'terminating'
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** True once shutdown has completed (AdonisJS `isTerminated`). */
|
|
434
|
+
get isTerminated(): boolean {
|
|
435
|
+
return this.#state === 'terminated'
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** The name from the app's `package.json`, when it could be read. */
|
|
439
|
+
get appName(): string | undefined {
|
|
440
|
+
return this.#packageJson()?.name
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** The version from the app's `package.json`, when it could be read. */
|
|
444
|
+
get version(): string | undefined {
|
|
445
|
+
return this.#packageJson()?.version
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* A snapshot of the application, for a health endpoint or a bug report
|
|
450
|
+
* (AdonisJS `toJSON`).
|
|
451
|
+
*/
|
|
452
|
+
toJSON(): {
|
|
453
|
+
appName: string | undefined
|
|
454
|
+
version: string | undefined
|
|
455
|
+
environment: AppEnvironment
|
|
456
|
+
nodeEnvironment: string
|
|
457
|
+
state: ApplicationState
|
|
458
|
+
isReady: boolean
|
|
459
|
+
isTerminating: boolean
|
|
460
|
+
} {
|
|
461
|
+
return {
|
|
462
|
+
appName: this.appName,
|
|
463
|
+
version: this.version,
|
|
464
|
+
environment: this.#environment,
|
|
465
|
+
nodeEnvironment: this.nodeEnvironment,
|
|
466
|
+
state: this.#state,
|
|
467
|
+
isReady: this.isReady,
|
|
468
|
+
isTerminating: this.isTerminating,
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Listen for a signal, then stop listening (AdonisJS `listenOnce`).
|
|
474
|
+
*
|
|
475
|
+
* Registered through {@link listen} so the handler is still removed by
|
|
476
|
+
* `terminate()` when the signal never arrives — otherwise a stopped app
|
|
477
|
+
* would leave a listener behind, and the next real signal would run the
|
|
478
|
+
* handlers of every app instance that ever existed.
|
|
479
|
+
*/
|
|
480
|
+
listenOnce(signal: NodeJS.Signals, callback: () => void): void {
|
|
481
|
+
const once = (): void => {
|
|
482
|
+
process.removeListener(signal, once)
|
|
483
|
+
callback()
|
|
484
|
+
}
|
|
485
|
+
this.listen(signal, once)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** {@link listenOnce}, only when `condition` holds (AdonisJS `listenOnceIf`). */
|
|
489
|
+
listenOnceIf(condition: boolean, signal: NodeJS.Signals, callback: () => void): void {
|
|
490
|
+
if (condition) this.listenOnce(signal, callback)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Tell the process supervisor the app reached a state (AdonisJS `notify`).
|
|
495
|
+
*
|
|
496
|
+
* A no-op unless something is listening — systemd's `NOTIFY_SOCKET`, or a
|
|
497
|
+
* parent process. Silent by design: an app should not fail to start because
|
|
498
|
+
* nothing was there to hear it.
|
|
499
|
+
*/
|
|
500
|
+
notify(message: string): void {
|
|
501
|
+
process.send?.({ type: 'ream:notify', message })
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Import a module and return its default export (AdonisJS `importDefault`).
|
|
506
|
+
*
|
|
507
|
+
* Throws when there is none, naming the file: a preload or provider entry
|
|
508
|
+
* without a default export otherwise fails later, as `undefined is not a
|
|
509
|
+
* constructor`, far from the file at fault.
|
|
510
|
+
*/
|
|
511
|
+
async importDefault<T = unknown>(importFn: () => Promise<unknown>, label?: string): Promise<T> {
|
|
512
|
+
const module = await importFn()
|
|
513
|
+
if (module === null || typeof module !== 'object' || !('default' in module)) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
`Missing default export${label ? ` in "${label}"` : ''}. ` +
|
|
516
|
+
'Export the value as `export default`, or import the named export directly.',
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
// The caller declares what it expects to find, the same contract as
|
|
520
|
+
// `container.make<T>()` over a dynamic import.
|
|
521
|
+
return module.default as T
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** The app's `package.json`, read once and cached; `undefined` when absent. */
|
|
525
|
+
#packageJson(): { name?: string; version?: string } | undefined {
|
|
526
|
+
if (this.#packageJsonRead) return this.#packageJsonCache
|
|
527
|
+
this.#packageJsonRead = true
|
|
528
|
+
try {
|
|
529
|
+
const path = this.makePath('package.json')
|
|
530
|
+
const raw = readFileSync(path, 'utf8')
|
|
531
|
+
const parsed: unknown = JSON.parse(raw)
|
|
532
|
+
if (parsed !== null && typeof parsed === 'object') {
|
|
533
|
+
const name = 'name' in parsed && typeof parsed.name === 'string' ? parsed.name : undefined
|
|
534
|
+
const version =
|
|
535
|
+
'version' in parsed && typeof parsed.version === 'string' ? parsed.version : undefined
|
|
536
|
+
this.#packageJsonCache = { name, version }
|
|
537
|
+
}
|
|
538
|
+
} catch {
|
|
539
|
+
// No app root, no package.json, unreadable, or malformed — every one of
|
|
540
|
+
// those means "we do not know the name", not "fail to boot".
|
|
541
|
+
this.#packageJsonCache = undefined
|
|
542
|
+
}
|
|
543
|
+
return this.#packageJsonCache
|
|
544
|
+
}
|
|
545
|
+
|
|
357
546
|
/**
|
|
358
547
|
* Conditionally listen for a process signal.
|
|
359
548
|
* Like AdonisJS app.listenIf(app.managedByPm2, 'SIGINT', ...).
|
|
@@ -377,7 +566,7 @@ export class Application implements AppContext {
|
|
|
377
566
|
|
|
378
567
|
/** Register a provider instance. */
|
|
379
568
|
register(provider: ProviderContract): void {
|
|
380
|
-
this
|
|
569
|
+
this.#providers.push(provider)
|
|
381
570
|
if (typeof provider.register === 'function') {
|
|
382
571
|
provider.register()
|
|
383
572
|
}
|
|
@@ -385,26 +574,32 @@ export class Application implements AppContext {
|
|
|
385
574
|
|
|
386
575
|
/** Boot all registered providers. Runs booting/booted hooks. */
|
|
387
576
|
async boot(): Promise<void> {
|
|
388
|
-
if (this
|
|
577
|
+
if (this.#booted) return
|
|
389
578
|
|
|
390
579
|
// Run booting hooks
|
|
391
|
-
for (const hook of this
|
|
580
|
+
for (const hook of this.#bootingHooks) {
|
|
392
581
|
await hook()
|
|
393
582
|
}
|
|
394
583
|
|
|
395
584
|
// Boot providers. `callProviderPhase` skips providers that don't
|
|
396
585
|
// declare the phase — duck-typed packages (spectrum, warden, etc.)
|
|
397
586
|
// implement only what they need.
|
|
398
|
-
for (const provider of this
|
|
587
|
+
for (const provider of this.#providers) {
|
|
399
588
|
await callProviderPhase(provider, 'boot')
|
|
400
589
|
}
|
|
401
590
|
|
|
402
|
-
this
|
|
591
|
+
this.#booted = true
|
|
592
|
+
this.#state = 'booted'
|
|
403
593
|
|
|
404
594
|
// Run booted hooks
|
|
405
|
-
for (const hook of this
|
|
595
|
+
for (const hook of this.#bootedHooks) {
|
|
406
596
|
await hook()
|
|
407
597
|
}
|
|
598
|
+
|
|
599
|
+
// Ready only once the booted hooks have run: they are where an app wires
|
|
600
|
+
// the things a request will need, so reporting ready before them would
|
|
601
|
+
// green a health check on an application that cannot serve yet.
|
|
602
|
+
this.#state = 'ready'
|
|
408
603
|
}
|
|
409
604
|
|
|
410
605
|
/**
|
|
@@ -414,8 +609,9 @@ export class Application implements AppContext {
|
|
|
414
609
|
*/
|
|
415
610
|
async shutdown(): Promise<void> {
|
|
416
611
|
const errors: unknown[] = []
|
|
612
|
+
this.#state = 'terminating'
|
|
417
613
|
|
|
418
|
-
for (const hook of this
|
|
614
|
+
for (const hook of this.#shutdownHooks) {
|
|
419
615
|
try {
|
|
420
616
|
await hook()
|
|
421
617
|
} catch (err) {
|
|
@@ -423,7 +619,7 @@ export class Application implements AppContext {
|
|
|
423
619
|
}
|
|
424
620
|
}
|
|
425
621
|
|
|
426
|
-
for (const provider of [...this
|
|
622
|
+
for (const provider of [...this.#providers].reverse()) {
|
|
427
623
|
try {
|
|
428
624
|
await callProviderPhase(provider, 'shutdown')
|
|
429
625
|
} catch (err) {
|
|
@@ -437,7 +633,11 @@ export class Application implements AppContext {
|
|
|
437
633
|
}
|
|
438
634
|
this.#signalHandlers = []
|
|
439
635
|
|
|
440
|
-
this
|
|
636
|
+
this.#booted = false
|
|
637
|
+
// Terminated even when a hook threw: the hooks and providers have all been
|
|
638
|
+
// given their turn, so the application IS down. Reporting anything else
|
|
639
|
+
// would leave a supervisor waiting on a shutdown that already happened.
|
|
640
|
+
this.#state = 'terminated'
|
|
441
641
|
if (errors.length === 1) throw errors[0]
|
|
442
642
|
if (errors.length > 1) {
|
|
443
643
|
throw new AggregateError(errors, 'Application.shutdown() completed with errors')
|
|
@@ -446,14 +646,14 @@ export class Application implements AppContext {
|
|
|
446
646
|
|
|
447
647
|
/** Register a shutdown hook. */
|
|
448
648
|
onShutdown(callback: () => Promise<void> | void): void {
|
|
449
|
-
this.
|
|
649
|
+
this.#shutdownHooks.push(callback)
|
|
450
650
|
}
|
|
451
651
|
|
|
452
652
|
get isBooted(): boolean {
|
|
453
|
-
return this
|
|
653
|
+
return this.#booted
|
|
454
654
|
}
|
|
455
655
|
|
|
456
656
|
get providerCount(): number {
|
|
457
|
-
return this
|
|
657
|
+
return this.#providers.length
|
|
458
658
|
}
|
|
459
659
|
}
|
package/src/ErrorBoundary.ts
CHANGED
|
@@ -33,20 +33,20 @@ export type ErrorEmitter = (event: ErrorEvent) => void
|
|
|
33
33
|
* - In dev mode, also logs to console
|
|
34
34
|
*/
|
|
35
35
|
export class ErrorBoundary {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
#emitter: ErrorEmitter
|
|
37
|
+
#devMode: boolean
|
|
38
|
+
#installed = false
|
|
39
|
+
#rejectionHandler?: (reason: unknown) => void
|
|
40
|
+
#exceptionHandler?: (error: Error) => void
|
|
41
41
|
|
|
42
42
|
constructor(emitter: ErrorEmitter, devMode = false) {
|
|
43
|
-
this
|
|
44
|
-
this
|
|
43
|
+
this.#emitter = emitter
|
|
44
|
+
this.#devMode = devMode
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** Install global error handlers. */
|
|
48
48
|
install(): void {
|
|
49
|
-
if (this
|
|
49
|
+
if (this.#installed) return
|
|
50
50
|
|
|
51
51
|
// Skip global install under vitest — each test instantiates its own
|
|
52
52
|
// Ignitor and rarely calls `.stop()`, so per-test listeners pile up on
|
|
@@ -55,42 +55,42 @@ export class ErrorBoundary {
|
|
|
55
55
|
// `handleError`/`serviceError` calls; only the Node-level signal
|
|
56
56
|
// bridging is suppressed.
|
|
57
57
|
if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
|
|
58
|
-
this
|
|
58
|
+
this.#installed = true
|
|
59
59
|
return
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
this
|
|
63
|
-
this
|
|
62
|
+
this.#rejectionHandler = (reason) => {
|
|
63
|
+
this.#handleError('system.fatal', 'UnhandledRejection', reason)
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
this
|
|
67
|
-
this
|
|
66
|
+
this.#exceptionHandler = (error) => {
|
|
67
|
+
this.#handleError('system.fatal', 'UncaughtException', error)
|
|
68
68
|
// Node.js is in undefined state after uncaughtException — must exit
|
|
69
69
|
process.exit(1)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
process.on('unhandledRejection', this
|
|
73
|
-
process.on('uncaughtException', this
|
|
72
|
+
process.on('unhandledRejection', this.#rejectionHandler)
|
|
73
|
+
process.on('uncaughtException', this.#exceptionHandler)
|
|
74
74
|
|
|
75
|
-
this
|
|
75
|
+
this.#installed = true
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/** Uninstall global error handlers (for testing). Only removes OUR handlers. */
|
|
79
79
|
uninstall(): void {
|
|
80
|
-
if (this
|
|
81
|
-
process.removeListener('unhandledRejection', this
|
|
80
|
+
if (this.#rejectionHandler) {
|
|
81
|
+
process.removeListener('unhandledRejection', this.#rejectionHandler)
|
|
82
82
|
}
|
|
83
|
-
if (this
|
|
84
|
-
process.removeListener('uncaughtException', this
|
|
83
|
+
if (this.#exceptionHandler) {
|
|
84
|
+
process.removeListener('uncaughtException', this.#exceptionHandler)
|
|
85
85
|
}
|
|
86
|
-
this
|
|
87
|
-
this
|
|
88
|
-
this
|
|
86
|
+
this.#rejectionHandler = undefined
|
|
87
|
+
this.#exceptionHandler = undefined
|
|
88
|
+
this.#installed = false
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/** Emit a service error (handler failure). */
|
|
92
92
|
serviceError(source: string, error: unknown, correlationId?: string): void {
|
|
93
|
-
this
|
|
93
|
+
this.#handleError('service.error', source, error, correlationId)
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/** Emit a security rejection. */
|
|
@@ -103,15 +103,15 @@ export class ErrorBoundary {
|
|
|
103
103
|
correlationId,
|
|
104
104
|
timestamp: new Date().toISOString(),
|
|
105
105
|
}
|
|
106
|
-
this
|
|
106
|
+
this.#emit(event)
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/** Emit a system error (infrastructure failure). */
|
|
110
110
|
systemError(source: string, error: unknown, correlationId?: string): void {
|
|
111
|
-
this
|
|
111
|
+
this.#handleError('system.error', source, error, correlationId)
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
#handleError(
|
|
115
115
|
type: ErrorEvent['type'],
|
|
116
116
|
source: string,
|
|
117
117
|
error: unknown,
|
|
@@ -131,12 +131,12 @@ export class ErrorBoundary {
|
|
|
131
131
|
timestamp: new Date().toISOString(),
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
this
|
|
134
|
+
this.#emit(event)
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
#emit(event: ErrorEvent): void {
|
|
138
138
|
try {
|
|
139
|
-
this
|
|
139
|
+
this.#emitter(event)
|
|
140
140
|
} catch {
|
|
141
141
|
// If the emitter itself fails, log to stderr as last resort
|
|
142
142
|
process.stderr.write(`[ErrorBoundary] Failed to emit: ${JSON.stringify(event)}\n`)
|
|
@@ -150,7 +150,7 @@ export class ErrorBoundary {
|
|
|
150
150
|
// boundary would turn each forgotten `await` from a loud crash into
|
|
151
151
|
// nothing at all, which is the opposite of what it is for. A duplicated
|
|
152
152
|
// line when a listener also logs it is a much smaller price.
|
|
153
|
-
if (this
|
|
153
|
+
if (this.#devMode || event.type === 'system.fatal') {
|
|
154
154
|
const prefix =
|
|
155
155
|
event.type === 'system.fatal'
|
|
156
156
|
? '✗ FATAL'
|
package/src/HttpKernel.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
MiddlewareRegistry,
|
|
20
20
|
RuntimeValidator,
|
|
21
21
|
} from './middleware/Pipeline.js'
|
|
22
|
-
import {
|
|
22
|
+
import { composeMiddleware } from './middleware/Pipeline.js'
|
|
23
23
|
import type { AnyConstructor, RouteDefinition, Router } from './router/Router.js'
|
|
24
24
|
import type { CookieSigner } from './security/CookieSigner.js'
|
|
25
25
|
import type { SignedUrl } from './security/SignedUrl.js'
|
|
@@ -318,7 +318,7 @@ export function createHttpKernel(
|
|
|
318
318
|
// HttpContext ALS so HttpContext.get()/getOrFail() reach this request
|
|
319
319
|
// anywhere down the call stack (AdonisJS parity) — transparent to the
|
|
320
320
|
// pipeline itself.
|
|
321
|
-
const fullPipeline =
|
|
321
|
+
const fullPipeline = composeMiddleware([...serverMw, coreHandler])
|
|
322
322
|
await HttpContext.run(ctx, () => fullPipeline(ctx, async () => {}))
|
|
323
323
|
|
|
324
324
|
ctx.events?.emit('http:response', {
|