@c9up/ream 0.2.15 → 0.2.17
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 +1 -1
- package/dist/Application.d.ts +54 -8
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +77 -7
- package/dist/Application.js.map +1 -1
- package/dist/Ignitor.d.ts +9 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +40 -6
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +12 -1
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/config.d.ts +15 -0
- package/dist/bodyparser/config.d.ts.map +1 -0
- package/dist/bodyparser/config.js +16 -0
- package/dist/bodyparser/config.js.map +1 -0
- package/dist/commands/ScheduleList.d.ts.map +1 -1
- package/dist/commands/ScheduleList.js +29 -0
- package/dist/commands/ScheduleList.js.map +1 -1
- package/dist/config/moduleConfig.d.ts +15 -0
- package/dist/config/moduleConfig.d.ts.map +1 -0
- package/dist/config/moduleConfig.js +24 -0
- package/dist/config/moduleConfig.js.map +1 -0
- package/dist/console/cliui.d.ts +2 -0
- package/dist/console/cliui.d.ts.map +1 -1
- package/dist/console/cliui.js +2 -1
- package/dist/console/cliui.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/scheduler/ScheduleProvider.d.ts +15 -11
- package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
- package/dist/scheduler/ScheduleProvider.js +21 -12
- package/dist/scheduler/ScheduleProvider.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts +2 -4
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +13 -1
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/config.d.ts +25 -0
- package/dist/session/config.d.ts.map +1 -0
- package/dist/session/config.js +18 -0
- package/dist/session/config.js.map +1 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +11 -2
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Application.ts +89 -8
- package/src/Ignitor.ts +54 -6
- package/src/bodyparser/BodyParserMiddleware.ts +12 -1
- package/src/bodyparser/config.ts +18 -0
- package/src/commands/ScheduleList.ts +30 -0
- package/src/config/moduleConfig.ts +24 -0
- package/src/console/cliui.ts +2 -1
- package/src/index.ts +1 -1
- package/src/scheduler/ScheduleProvider.ts +22 -12
- package/src/session/SessionMiddleware.ts +15 -2
- package/src/session/config.ts +29 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/session/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAaH,MAAM,UAAU,YAAY,CAAC,MAA0B;IACrD,OAAO,MAAM,CAAA;AACf,CAAC"}
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/ream",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
4
4
|
"description": "Ream — Rust-powered Node.js application framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -187,6 +187,14 @@
|
|
|
187
187
|
"./swcrc.app.json": "./swcrc.app.json",
|
|
188
188
|
"./tsconfig.package.json": "./tsconfig.package.json",
|
|
189
189
|
"./tsconfig.client.json": "./tsconfig.client.json",
|
|
190
|
+
"./session/config": {
|
|
191
|
+
"types": "./dist/session/config.d.ts",
|
|
192
|
+
"import": "./dist/session/config.js"
|
|
193
|
+
},
|
|
194
|
+
"./bodyparser/config": {
|
|
195
|
+
"types": "./dist/bodyparser/config.d.ts",
|
|
196
|
+
"import": "./dist/bodyparser/config.js"
|
|
197
|
+
},
|
|
190
198
|
"./session_middleware": {
|
|
191
199
|
"types": "./dist/session/SessionMiddleware.d.ts",
|
|
192
200
|
"import": "./dist/session/SessionMiddleware.js"
|
|
@@ -231,6 +239,7 @@
|
|
|
231
239
|
"@biomejs/biome": "2.5.12",
|
|
232
240
|
"@c9up/comet": "^0.1.0",
|
|
233
241
|
"@types/node": "^22.19.15",
|
|
242
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
234
243
|
"mime-db": "^1.54.0",
|
|
235
244
|
"mime-types": "^3.0.2",
|
|
236
245
|
"typescript": "^6.0.2",
|
|
@@ -254,7 +263,7 @@
|
|
|
254
263
|
"build:napi": "pnpm build:rust && pnpm build:napi-types && node scripts/copy-napi.mjs",
|
|
255
264
|
"test": "vitest run",
|
|
256
265
|
"test:rust": "cargo test --all",
|
|
257
|
-
"lint": "biome check src/",
|
|
266
|
+
"lint": "biome check src/ tests/",
|
|
258
267
|
"test:coverage": "vitest run --coverage",
|
|
259
268
|
"typecheck": "tsc --noEmit",
|
|
260
269
|
"typecheck:tests": "tsc --noEmit -p tsconfig.tests.json"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/Application.ts
CHANGED
|
@@ -56,11 +56,26 @@ export type ApplicationState =
|
|
|
56
56
|
| 'terminating'
|
|
57
57
|
| 'terminated'
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* What the application keeps of the rc file.
|
|
61
|
+
*
|
|
62
|
+
* Structurally a subset of `ReamrcConfig`, declared here rather than imported:
|
|
63
|
+
* that type lives in `Ignitor.ts`, which imports this module, and naming it
|
|
64
|
+
* from here would close the cycle. Only the keys something reads back through
|
|
65
|
+
* `app.rcFile` are spelt out; the rest rides along untouched.
|
|
66
|
+
*/
|
|
67
|
+
export interface ReamrcContents {
|
|
68
|
+
directories?: Partial<DirectoriesNode>
|
|
69
|
+
/** `app/modules/**` auto-loading — opt-in, and scoped to named files. */
|
|
70
|
+
modules?: { path?: string; autoload?: string[] }
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
export class Application implements AppContext {
|
|
60
74
|
readonly container: Container
|
|
61
75
|
readonly config: ConfigStore
|
|
62
76
|
#appRoot?: URL
|
|
63
77
|
#directories: DirectoriesNode = { ...defaultDirectories }
|
|
78
|
+
#rcFile: ReamrcContents = {}
|
|
64
79
|
#providers: ProviderContract[] = []
|
|
65
80
|
#booted = false
|
|
66
81
|
#environment: AppEnvironment = 'unknown'
|
|
@@ -68,6 +83,10 @@ export class Application implements AppContext {
|
|
|
68
83
|
#packageJsonRead = false
|
|
69
84
|
#packageJsonCache: { name?: string; version?: string } | undefined
|
|
70
85
|
#mode: ApplicationMode = 'run'
|
|
86
|
+
/** Hooks that run — in reverse — when `terminate()` begins. */
|
|
87
|
+
#terminatingHooks: Array<() => Promise<void> | void> = []
|
|
88
|
+
/** Re-entrancy guard: two signals must terminate the application once. */
|
|
89
|
+
#terminating = false
|
|
71
90
|
#bootingHooks: Array<() => Promise<void> | void> = []
|
|
72
91
|
#bootedHooks: Array<() => Promise<void> | void> = []
|
|
73
92
|
#shutdownHooks: Array<() => Promise<void> | void> = []
|
|
@@ -161,14 +180,27 @@ export class Application implements AppContext {
|
|
|
161
180
|
return this.#directories
|
|
162
181
|
}
|
|
163
182
|
|
|
183
|
+
/**
|
|
184
|
+
* The parsed rc file (AdonisJS `app.rcFile`).
|
|
185
|
+
*
|
|
186
|
+
* Kept whole, not just the keys this class acts on. A console command has the
|
|
187
|
+
* application and nothing else, so anything it needs to explain itself — why
|
|
188
|
+
* `app/modules/**` loaded nothing, say — has to be reachable from here. Empty
|
|
189
|
+
* until `rcContents()` has run.
|
|
190
|
+
*/
|
|
191
|
+
get rcFile(): ReamrcContents {
|
|
192
|
+
return this.#rcFile
|
|
193
|
+
}
|
|
194
|
+
|
|
164
195
|
/**
|
|
165
196
|
* Feed the loaded rc file to the application (AdonisJS `rcContents`).
|
|
166
197
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
198
|
+
* `directories` is merged over the defaults; the rest is stored as-is and
|
|
199
|
+
* read back through {@link rcFile}. Providers, preloads, commands and test
|
|
200
|
+
* suites are still consumed by the Ignitor and the CLI, which own them.
|
|
170
201
|
*/
|
|
171
|
-
rcContents(value:
|
|
202
|
+
rcContents(value: ReamrcContents): this {
|
|
203
|
+
this.#rcFile = value
|
|
172
204
|
if (value.directories) {
|
|
173
205
|
// Skip the undefined entries a `Partial` may carry: spreading them would
|
|
174
206
|
// blank a default rather than leave it alone.
|
|
@@ -576,12 +608,56 @@ export class Application implements AppContext {
|
|
|
576
608
|
}
|
|
577
609
|
|
|
578
610
|
/**
|
|
579
|
-
*
|
|
580
|
-
*
|
|
611
|
+
* Register a hook that runs when the application begins to terminate
|
|
612
|
+
* (AdonisJS `app.terminating`).
|
|
613
|
+
*
|
|
614
|
+
* This is how the HTTP server closes on a signal: whoever opened the socket
|
|
615
|
+
* registers a hook that closes it, and `terminate()` runs them in reverse —
|
|
616
|
+
* last opened, first closed. Upstream's HTTP process does exactly this.
|
|
617
|
+
*/
|
|
618
|
+
terminating(callback: () => Promise<void> | void): void {
|
|
619
|
+
this.#terminatingHooks.push(callback)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Graceful shutdown (AdonisJS `app.terminate()`).
|
|
624
|
+
*
|
|
625
|
+
* Runs the `terminating` hooks in reverse, then shuts the providers down.
|
|
626
|
+
* Two things it deliberately does NOT do:
|
|
627
|
+
*
|
|
628
|
+
* - It does not close the HTTP server itself. Whoever opened the socket
|
|
629
|
+
* registers a `terminating` hook for it; the application does not know
|
|
630
|
+
* what a server is. Terminating without one used to skip the close, the
|
|
631
|
+
* error boundary and the service locators entirely.
|
|
632
|
+
* - It does not call `process.exit()`. Exiting from here cut the shutdown
|
|
633
|
+
* short whenever a second path was already draining the server — and the
|
|
634
|
+
* documented signal wiring set up exactly that race. The process ends when
|
|
635
|
+
* nothing is left holding the loop, which is what "graceful" means.
|
|
636
|
+
*
|
|
637
|
+
* Re-entrant calls are ignored, so a SIGTERM handler and a crash handler
|
|
638
|
+
* firing together terminate once.
|
|
581
639
|
*/
|
|
582
640
|
async terminate(): Promise<void> {
|
|
583
|
-
|
|
584
|
-
|
|
641
|
+
if (this.#terminating) return
|
|
642
|
+
this.#terminating = true
|
|
643
|
+
const errors: unknown[] = []
|
|
644
|
+
for (const hook of [...this.#terminatingHooks].reverse()) {
|
|
645
|
+
try {
|
|
646
|
+
await hook()
|
|
647
|
+
} catch (err) {
|
|
648
|
+
errors.push(err)
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
this.#terminatingHooks = []
|
|
652
|
+
try {
|
|
653
|
+
await this.shutdown()
|
|
654
|
+
} catch (err) {
|
|
655
|
+
errors.push(err)
|
|
656
|
+
}
|
|
657
|
+
if (errors.length === 1) throw errors[0]
|
|
658
|
+
if (errors.length > 1) {
|
|
659
|
+
throw new AggregateError(errors, 'Application.terminate() completed with errors')
|
|
660
|
+
}
|
|
585
661
|
}
|
|
586
662
|
|
|
587
663
|
// ─── Provider lifecycle ───────────────────────────────────
|
|
@@ -640,6 +716,11 @@ export class Application implements AppContext {
|
|
|
640
716
|
* from closing their resources (DB pools, queues). Errors are aggregated.
|
|
641
717
|
*/
|
|
642
718
|
async shutdown(): Promise<void> {
|
|
719
|
+
// Idempotent: `terminate()` runs the `terminating` hooks and one of them is
|
|
720
|
+
// the Ignitor's `stop()`, which shuts the application down on its way. The
|
|
721
|
+
// second pass must not call every provider's `shutdown()` again — a pool
|
|
722
|
+
// closed twice, a socket destroyed twice.
|
|
723
|
+
if (this.#state === 'terminated') return
|
|
643
724
|
const errors: unknown[] = []
|
|
644
725
|
this.#state = 'terminating'
|
|
645
726
|
|
package/src/Ignitor.ts
CHANGED
|
@@ -210,6 +210,15 @@ export interface HyperServerLike {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
export interface IgnitorConfig {
|
|
213
|
+
/**
|
|
214
|
+
* Bind port. Falls back to `process.env.PORT`, then to `3000`. Prefer the
|
|
215
|
+
* environment: the fallback is read when the server binds, AFTER `booting()`
|
|
216
|
+
* has loaded `#start/env`, whereas a value passed here is fixed at
|
|
217
|
+
* construction — before `.env` has been read at all.
|
|
218
|
+
*
|
|
219
|
+
* It stays available because a test harness booting several applications in
|
|
220
|
+
* one process needs to hand each one an ephemeral port it has already picked.
|
|
221
|
+
*/
|
|
213
222
|
port?: number
|
|
214
223
|
/**
|
|
215
224
|
* Bind address of the HTTP server. Falls back to `process.env.HOST`, then to
|
|
@@ -690,12 +699,31 @@ export class Ignitor {
|
|
|
690
699
|
this.#phase = 'started'
|
|
691
700
|
}
|
|
692
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Whether this process should bind an HTTP server.
|
|
704
|
+
*
|
|
705
|
+
* Not the same question as "is the environment 'web'". A test harness needs
|
|
706
|
+
* the HTTP surface — it passes a `serverFactory` precisely to get one — but
|
|
707
|
+
* it also needs to declare itself as `test`, because that is what an
|
|
708
|
+
* environment-scoped provider entry reads. Tying the two together forced a
|
|
709
|
+
* test bootstrap to call `httpServer()`, which set the environment to
|
|
710
|
+
* `'web'`, so `{ environment: ['web'] }` excluded nothing from a test run and
|
|
711
|
+
* a scheduled task fired in the middle of the suite.
|
|
712
|
+
*
|
|
713
|
+
* A test process that passes no factory still gets no server, and no error.
|
|
714
|
+
*/
|
|
715
|
+
#servesHttp(): boolean {
|
|
716
|
+
const environment = this.#app.getEnvironment()
|
|
717
|
+
if (environment === 'web') return true
|
|
718
|
+
return environment === 'test' && this.#config.serverFactory !== undefined
|
|
719
|
+
}
|
|
720
|
+
|
|
693
721
|
async #phaseReady(): Promise<void> {
|
|
694
722
|
// Boot the Server (resolves lazy error handler etc.)
|
|
695
723
|
await this.#server.boot()
|
|
696
724
|
|
|
697
|
-
// Start HTTP server if
|
|
698
|
-
if (this.#
|
|
725
|
+
// Start HTTP server if this process serves one
|
|
726
|
+
if (this.#servesHttp() && this.#config.serverFactory) {
|
|
699
727
|
// Build the HttpKernel with server middleware + router middleware.
|
|
700
728
|
// The streaming backend is resolved lazily — the HyperServer is
|
|
701
729
|
// created a few lines down, after the kernel — so the kernel
|
|
@@ -737,7 +765,18 @@ export class Ignitor {
|
|
|
737
765
|
},
|
|
738
766
|
})
|
|
739
767
|
|
|
740
|
-
|
|
768
|
+
// Read HERE, not at construction: `#start/env` runs in `booting()`, so a
|
|
769
|
+
// PORT living in `.env` does not exist in `process.env` until this point.
|
|
770
|
+
// Reading it early bound 3000 while every later reader — the boot banner
|
|
771
|
+
// included — saw 3007 and said so. Same lazy rule as `host` below.
|
|
772
|
+
const desiredPort = this.#config.port ?? Number(process.env.PORT || 3000)
|
|
773
|
+
if (!Number.isInteger(desiredPort) || desiredPort < 0 || desiredPort > 65535) {
|
|
774
|
+
throw new ReamError(
|
|
775
|
+
'E_IGNITOR_INVALID_PORT',
|
|
776
|
+
`PORT must be an integer between 0 and 65535, received '${process.env.PORT}'.`,
|
|
777
|
+
{ hint: 'Set PORT in .env, or pass { port } to the Ignitor.' },
|
|
778
|
+
)
|
|
779
|
+
}
|
|
741
780
|
// Port-scan fallback (+1..+19) is a DEV convenience only. In production
|
|
742
781
|
// a silent drift to :3001 while the LB targets :3000 is an outage —
|
|
743
782
|
// bind the configured port and let EADDRINUSE fail loudly. The probe
|
|
@@ -760,15 +799,24 @@ export class Ignitor {
|
|
|
760
799
|
}
|
|
761
800
|
await this.#_httpServer.listen()
|
|
762
801
|
|
|
802
|
+
// Whoever opened the socket closes it. `app.terminate()` runs the
|
|
803
|
+
// `terminating` hooks in reverse before shutting the providers down, so
|
|
804
|
+
// an application terminated by ANY path — this Ignitor's signal handler,
|
|
805
|
+
// an `app.listen('SIGTERM', () => app.terminate())` the entry point wired
|
|
806
|
+
// itself, a crash handler — drains the HTTP server. It used to skip it:
|
|
807
|
+
// `terminate()` shut the providers down and called `process.exit(0)`,
|
|
808
|
+
// which could win the race against a drain already in progress.
|
|
809
|
+
this.#app.terminating(() => this.stop())
|
|
810
|
+
|
|
763
811
|
// Wire OS-signal graceful shutdown. Without it the process never closes
|
|
764
812
|
// the HTTP server on SIGTERM/SIGINT, so live keep-alive / SSE sockets keep
|
|
765
813
|
// the event loop alive — a `ream dev` restart or Ctrl+C ends in a watcher
|
|
766
814
|
// force-kill, and an orchestrator's rolling deploy drops in-flight work.
|
|
767
|
-
//
|
|
768
|
-
//
|
|
815
|
+
// It terminates the APPLICATION rather than stopping this Ignitor, so
|
|
816
|
+
// there is one authority however the shutdown was triggered.
|
|
769
817
|
if (this.#config.gracefulShutdown !== false) {
|
|
770
818
|
this.#shutdownHandle = installGracefulShutdown({
|
|
771
|
-
onShutdown: () => this.
|
|
819
|
+
onShutdown: () => this.#app.terminate(),
|
|
772
820
|
logger: {
|
|
773
821
|
info: (message) => this.#emitSystemInfo('GracefulShutdown', message, 'info'),
|
|
774
822
|
error: (message) => this.#emitSystemInfo('GracefulShutdown', message, 'error'),
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Configured via config/bodyparser.ts.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { readModuleConfig } from '../config/moduleConfig.js'
|
|
9
10
|
import { Exception } from '../http/Exception.js'
|
|
10
11
|
import type { HttpContext } from '../http/HttpContext.js'
|
|
11
12
|
import { hydrateMultipartPayload } from './MultipartFile.js'
|
|
@@ -98,7 +99,17 @@ const DEFAULT_CONFIG: ResolvedBodyParserConfig = {
|
|
|
98
99
|
export default class BodyParserMiddleware {
|
|
99
100
|
#config: ResolvedBodyParserConfig
|
|
100
101
|
|
|
101
|
-
constructor(
|
|
102
|
+
constructor(explicitConfig: BodyParserConfig | undefined = undefined) {
|
|
103
|
+
// The default value is load-bearing: an OPTIONAL parameter still counts
|
|
104
|
+
// toward `Function.length`, and the container refuses to auto-construct a
|
|
105
|
+
// class that declares parameters it has no metadata for. A parameter with a
|
|
106
|
+
// default counts for nothing, which is what lets
|
|
107
|
+
// `router.use([() => import('…')])` build this middleware.
|
|
108
|
+
// Nothing is passed when the container builds this middleware from
|
|
109
|
+
// `router.use([() => import('@c9up/ream/bodyparser_middleware')])`, so the
|
|
110
|
+
// settings come from `config/bodyparser.ts` — the file this class's own
|
|
111
|
+
// documentation has always named.
|
|
112
|
+
const config = explicitConfig ?? readModuleConfig<BodyParserConfig>('bodyparser')
|
|
102
113
|
assertNoEnabledFlag(config)
|
|
103
114
|
this.#config = {
|
|
104
115
|
allowedMethods: config?.allowedMethods ?? DEFAULT_CONFIG.allowedMethods,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `config/bodyparser.ts` — body-parsing limits and types, in the file the
|
|
3
|
+
* config loader reads (`config/<name>.ts` → `config.get('<name>')`).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* // config/bodyparser.ts
|
|
7
|
+
* import { defineConfig } from '@c9up/ream/bodyparser/config'
|
|
8
|
+
*
|
|
9
|
+
* export default defineConfig({
|
|
10
|
+
* multipart: { limit: '20mb' },
|
|
11
|
+
* })
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { BodyParserConfig } from './BodyParserMiddleware.js'
|
|
15
|
+
|
|
16
|
+
export function defineConfig(config: BodyParserConfig): BodyParserConfig {
|
|
17
|
+
return config
|
|
18
|
+
}
|
|
@@ -33,6 +33,9 @@ export default class ScheduleList extends BaseCommand {
|
|
|
33
33
|
const tasks = scheduler.listTasks()
|
|
34
34
|
if (tasks.length === 0) {
|
|
35
35
|
this.logger.info('No scheduled tasks registered.')
|
|
36
|
+
for (const line of whyNothingWasFound(this.app.rcFile.modules)) {
|
|
37
|
+
this.logger.info(` ${line}`)
|
|
38
|
+
}
|
|
36
39
|
return
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -64,3 +67,30 @@ export default class ScheduleList extends BaseCommand {
|
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Why an empty list is empty.
|
|
73
|
+
*
|
|
74
|
+
* Discovery walks the IoC service registry, and a class the process never
|
|
75
|
+
* imported is not in it. `@Schedule` in `app/modules/**` therefore depends on
|
|
76
|
+
* the module auto-loader having imported the file, and that has two conditions
|
|
77
|
+
* a reader of the folder-structure guide would not expect. Saying "no tasks"
|
|
78
|
+
* and stopping leaves them to find both by reading the Ignitor.
|
|
79
|
+
*/
|
|
80
|
+
function whyNothingWasFound(modules: { path?: string; autoload?: string[] } | undefined): string[] {
|
|
81
|
+
if (modules?.path === undefined) {
|
|
82
|
+
return [
|
|
83
|
+
'A @Schedule is only found once something has imported the class that declares it.',
|
|
84
|
+
'reamrc.ts has no `modules.path`, so nothing under app/modules/ is auto-loaded at all.',
|
|
85
|
+
"Either add `modules: { path: './app/modules', autoload: ['routes', 'events'] }`,",
|
|
86
|
+
'or import the file from a preload — which is explicit, and works anywhere.',
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
const autoload = modules.autoload ?? ['routes', 'events']
|
|
90
|
+
return [
|
|
91
|
+
'A @Schedule is only found once something has imported the class that declares it.',
|
|
92
|
+
`modules.autoload is [${autoload.map((f) => `'${f}'`).join(', ')}], so app/modules/<name>/<file>.ts`,
|
|
93
|
+
'is imported only under one of those names. Add the file name to that list,',
|
|
94
|
+
'or import it from a preload — which is explicit, and works anywhere.',
|
|
95
|
+
]
|
|
96
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a module's own `config/<name>.ts` entry without demanding one.
|
|
3
|
+
*
|
|
4
|
+
* A middleware constructed by the container gets nothing passed to it — the
|
|
5
|
+
* resolver calls `make(Class)`, which is what makes `router.use([() =>
|
|
6
|
+
* import('…/session_middleware')])` possible. The configuration therefore has
|
|
7
|
+
* to come from the config store, the way it does everywhere else.
|
|
8
|
+
*
|
|
9
|
+
* The read is deliberately forgiving. The same classes are also constructed
|
|
10
|
+
* directly (`new BodyParserMiddleware()`) in tests and in embedding hosts that
|
|
11
|
+
* never boot an Application, and there the store does not exist. Throwing there
|
|
12
|
+
* would trade a working default for a crash.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getApp } from '../services/app.js'
|
|
16
|
+
|
|
17
|
+
export function readModuleConfig<T extends object>(key: string): T | undefined {
|
|
18
|
+
const store = getApp()?.config
|
|
19
|
+
if (store === undefined || !store.has(key)) return undefined
|
|
20
|
+
const value = store.get<T>(key)
|
|
21
|
+
// A config file exporting something that is not an object (a stray
|
|
22
|
+
// `export default 42`) is ignored rather than spread over the defaults.
|
|
23
|
+
return typeof value === 'object' && value !== null ? value : undefined
|
|
24
|
+
}
|
package/src/console/cliui.ts
CHANGED
|
@@ -750,7 +750,8 @@ function pad(text: string, width: number): string {
|
|
|
750
750
|
return text + ' '.repeat(Math.max(0, width - visibleWidth(text)))
|
|
751
751
|
}
|
|
752
752
|
|
|
753
|
-
|
|
753
|
+
/** Removes the SGR escape sequences `colors` writes, leaving the visible text. */
|
|
754
|
+
export function stripAnsi(text: string): string {
|
|
754
755
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: matching ANSI escapes is the point
|
|
755
756
|
return text.replace(/\u001B\[[0-9;]*m/g, '')
|
|
756
757
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* throws, tasks 1..N-1 registered in the same boot are unregistered
|
|
11
11
|
* before the error propagates, so the scheduler is never left in a
|
|
12
12
|
* half-registered state.
|
|
13
|
-
* - `
|
|
13
|
+
* - `ready()` — discovers the tasks `app/modules/**` declared, then
|
|
14
|
+
* launches the Rust tick loop.
|
|
14
15
|
* - `shutdown()` — cancels the Rust tick loop.
|
|
15
16
|
*
|
|
16
17
|
* The registered callback resolves the service **at invocation time**
|
|
@@ -86,26 +87,35 @@ export class ScheduleProvider extends Provider {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
/**
|
|
89
|
-
*
|
|
90
|
+
* The pass that matters runs at READY, and the ordering is the reason.
|
|
90
91
|
*
|
|
91
|
-
* `
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* registry
|
|
95
|
-
* and nothing said so
|
|
96
|
-
* normally and the task simply never fired.
|
|
92
|
+
* A `@Service()` carrying `@Schedule` naturally lives in `app/modules/**`,
|
|
93
|
+
* which the application auto-loads at the END of the start phase — after
|
|
94
|
+
* every provider's `start()`. Discovering in `start()` therefore read the
|
|
95
|
+
* service registry before the modules had put anything in it: the task was
|
|
96
|
+
* never registered, and nothing said so. No error, no warning, the
|
|
97
|
+
* application started normally and the task simply never fired. `ready()`
|
|
98
|
+
* runs after the autoload, which is what makes the second pass real.
|
|
97
99
|
*
|
|
98
|
-
*
|
|
99
|
-
* service declared
|
|
100
|
-
*
|
|
100
|
+
* The earlier passes are kept because they cost nothing and shorten the gap:
|
|
101
|
+
* a service declared by a provider or a preload is registered as soon as it
|
|
102
|
+
* is seen. `#discover()` is idempotent, so a task found twice is registered
|
|
103
|
+
* once.
|
|
101
104
|
*/
|
|
102
105
|
override async boot(): Promise<void> {
|
|
103
106
|
// Anything already in the registry — declared by a provider, or imported
|
|
104
|
-
// by one. The rest is caught
|
|
107
|
+
// by one. The rest is caught later.
|
|
105
108
|
this.#discover()
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
override async start(): Promise<void> {
|
|
112
|
+
// Preloads have not run yet either; this catches whatever the boot phase
|
|
113
|
+
// added. The ticker deliberately does NOT start here.
|
|
114
|
+
this.#discover()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override async ready(): Promise<void> {
|
|
118
|
+
// The pass after `app/modules/**`.
|
|
109
119
|
this.#discover()
|
|
110
120
|
if (activeProvider !== undefined && activeProvider !== this) {
|
|
111
121
|
// A previous app's scheduler is still running — stop it before this one
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { randomBytes } from 'node:crypto'
|
|
9
|
+
import { readModuleConfig } from '../config/moduleConfig.js'
|
|
9
10
|
import { currentNodeEnv } from '../env/nodeEnv.js'
|
|
10
11
|
import { durationToSeconds } from '../helpers/duration.js'
|
|
11
12
|
import type { HttpContext } from '../http/HttpContext.js'
|
|
13
|
+
import type { SessionConfigInput } from './config.js'
|
|
12
14
|
import { CookieDriver } from './drivers/CookieDriver.js'
|
|
13
15
|
import { DatabaseDriver, type SessionDbConnection } from './drivers/DatabaseDriver.js'
|
|
14
16
|
import { FileDriver } from './drivers/FileDriver.js'
|
|
@@ -16,7 +18,7 @@ import { MemoryDriver } from './drivers/MemoryDriver.js'
|
|
|
16
18
|
import { RedisDriver } from './drivers/RedisDriver.js'
|
|
17
19
|
import { quasarConnection } from './quasar.js'
|
|
18
20
|
import { ReadOnlyValuesStore, type ValuePath } from './ReadOnlyValuesStore.js'
|
|
19
|
-
import type {
|
|
21
|
+
import type { SessionDriver } from './Session.js'
|
|
20
22
|
import { Session } from './Session.js'
|
|
21
23
|
|
|
22
24
|
interface ResolvedSessionConfig {
|
|
@@ -42,7 +44,18 @@ export default class SessionMiddleware {
|
|
|
42
44
|
#cookieDriver: CookieDriver | null = null
|
|
43
45
|
#config: ResolvedSessionConfig
|
|
44
46
|
|
|
45
|
-
constructor(
|
|
47
|
+
constructor(explicitConfig: SessionConfigInput | undefined = undefined) {
|
|
48
|
+
// The default value is load-bearing: an OPTIONAL parameter still counts
|
|
49
|
+
// toward `Function.length`, and the container refuses to auto-construct a
|
|
50
|
+
// class that declares parameters it has no metadata for. A parameter with a
|
|
51
|
+
// default counts for nothing, which is what lets
|
|
52
|
+
// `router.use([() => import('…')])` build this middleware.
|
|
53
|
+
// Nothing is passed when the container builds this middleware from
|
|
54
|
+
// `router.use([() => import('@c9up/ream/session_middleware')])`, which is
|
|
55
|
+
// the registration shape the framework documents. `config/session.ts` is
|
|
56
|
+
// where the settings live in that case — the same file the config loader
|
|
57
|
+
// already reads into `config.get('session')`.
|
|
58
|
+
const config = explicitConfig ?? readModuleConfig<SessionConfigInput>('session')
|
|
46
59
|
// `store` + `stores` (AdonisJS) and `driver` (ream) name the same thing.
|
|
47
60
|
// A named store supplies the driver and its own options, so
|
|
48
61
|
// `{ store: 'redis', stores: { redis: { driver: 'redis' } } }` works.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `config/session.ts` — the session store's configuration, in the file the
|
|
3
|
+
* config loader reads (`config/<name>.ts` → `config.get('<name>')`).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* // config/session.ts
|
|
7
|
+
* import env from '#start/env.js'
|
|
8
|
+
* import { defineConfig } from '@c9up/ream/session/config'
|
|
9
|
+
*
|
|
10
|
+
* export default defineConfig({
|
|
11
|
+
* store: 'cookie',
|
|
12
|
+
* secret: env.get('APP_KEY'),
|
|
13
|
+
* })
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { SessionConfig } from './Session.js'
|
|
17
|
+
|
|
18
|
+
/** What a session config file may declare. */
|
|
19
|
+
export type SessionConfigInput = SessionConfig & {
|
|
20
|
+
/**
|
|
21
|
+
* Signs the session cookie. Required by the cookie store, which carries the
|
|
22
|
+
* data itself and would otherwise hand the client something it could edit.
|
|
23
|
+
*/
|
|
24
|
+
secret?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function defineConfig(config: SessionConfigInput): SessionConfigInput {
|
|
28
|
+
return config
|
|
29
|
+
}
|