@c9up/ream 0.1.32 → 0.1.34
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/dist/Application.d.ts +27 -0
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +29 -0
- package/dist/Application.js.map +1 -1
- package/dist/ConfigLoader.d.ts +4 -0
- package/dist/ConfigLoader.d.ts.map +1 -1
- package/dist/ConfigLoader.js +0 -8
- package/dist/ConfigLoader.js.map +1 -1
- package/dist/ErrorBoundary.d.ts.map +1 -1
- package/dist/ErrorBoundary.js +13 -1
- package/dist/ErrorBoundary.js.map +1 -1
- package/dist/HttpKernel.d.ts +8 -0
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +35 -12
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +8 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +39 -0
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +11 -4
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +55 -43
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/qsParse.d.ts +52 -0
- package/dist/bodyparser/qsParse.d.ts.map +1 -0
- package/dist/bodyparser/qsParse.js +161 -0
- package/dist/bodyparser/qsParse.js.map +1 -0
- package/dist/bootstrap/hyper-server.d.ts.map +1 -1
- package/dist/bootstrap/hyper-server.js +2 -8
- package/dist/bootstrap/hyper-server.js.map +1 -1
- package/dist/container/Container.d.ts +41 -0
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +193 -26
- package/dist/container/Container.js.map +1 -1
- package/dist/container/ContainerResolver.d.ts +46 -0
- package/dist/container/ContainerResolver.d.ts.map +1 -0
- package/dist/container/ContainerResolver.js +60 -0
- package/dist/container/ContainerResolver.js.map +1 -0
- package/dist/container/ContextualBindingsBuilder.d.ts +20 -0
- package/dist/container/ContextualBindingsBuilder.d.ts.map +1 -0
- package/dist/container/ContextualBindingsBuilder.js +31 -0
- package/dist/container/ContextualBindingsBuilder.js.map +1 -0
- package/dist/container/types.d.ts +9 -6
- package/dist/container/types.d.ts.map +1 -1
- package/dist/decorators/Service.d.ts +1 -5
- package/dist/decorators/Service.d.ts.map +1 -1
- package/dist/decorators/Service.js +10 -1
- package/dist/decorators/Service.js.map +1 -1
- package/dist/events/Emitter.d.ts +66 -6
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +165 -28
- package/dist/events/Emitter.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +5 -5
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/helpers/duration.d.ts +32 -0
- package/dist/helpers/duration.d.ts.map +1 -0
- package/dist/helpers/duration.js +74 -0
- package/dist/helpers/duration.js.map +1 -0
- package/dist/helpers/napi-loader.d.ts +10 -1
- package/dist/helpers/napi-loader.d.ts.map +1 -1
- package/dist/helpers/napi-loader.js +18 -3
- package/dist/helpers/napi-loader.js.map +1 -1
- package/dist/helpers/types.d.ts +1 -1
- package/dist/helpers/types.d.ts.map +1 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +17 -5
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/HttpContext.d.ts +24 -5
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +12 -2
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts +48 -2
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +95 -14
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts +143 -8
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +241 -21
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +74 -3
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +177 -4
- package/dist/http/Response.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/router/Router.d.ts +36 -7
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +66 -22
- package/dist/router/Router.js.map +1 -1
- package/dist/security/CookieSigner.d.ts +35 -4
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +99 -10
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/server/Server.d.ts +2 -3
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +30 -13
- package/dist/server/Server.js.map +1 -1
- package/dist/session/ReadOnlyValuesStore.d.ts +24 -0
- package/dist/session/ReadOnlyValuesStore.d.ts.map +1 -0
- package/dist/session/ReadOnlyValuesStore.js +45 -0
- package/dist/session/ReadOnlyValuesStore.js.map +1 -0
- package/dist/session/Session.d.ts +181 -9
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +284 -33
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +87 -33
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts +66 -0
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -0
- package/dist/session/drivers/DatabaseDriver.js +132 -0
- package/dist/session/drivers/DatabaseDriver.js.map +1 -0
- package/dist/session/drivers/FileDriver.d.ts +22 -0
- package/dist/session/drivers/FileDriver.d.ts.map +1 -0
- package/dist/session/drivers/FileDriver.js +66 -0
- package/dist/session/drivers/FileDriver.js.map +1 -0
- package/dist/session/drivers/MemoryDriver.d.ts +10 -2
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +70 -0
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts +10 -2
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +58 -0
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/dist/session/templateTags.d.ts +42 -0
- package/dist/session/templateTags.d.ts.map +1 -0
- package/dist/session/templateTags.js +101 -0
- package/dist/session/templateTags.js.map +1 -0
- package/dist/testing/RequestBuilder.d.ts +38 -1
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +40 -1
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +3 -1
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +3 -1
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/types/helpers.d.ts +1 -1
- package/dist/types/helpers.d.ts.map +1 -1
- package/dist/utils/Macroable.d.ts +14 -5
- package/dist/utils/Macroable.d.ts.map +1 -1
- package/dist/utils/Macroable.js +20 -18
- package/dist/utils/Macroable.js.map +1 -1
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Application.ts +39 -0
- package/src/ConfigLoader.ts +6 -0
- package/src/ErrorBoundary.ts +13 -1
- package/src/HttpKernel.ts +45 -19
- package/src/Ignitor.ts +39 -0
- package/src/bodyparser/BodyParserMiddleware.ts +66 -44
- package/src/bodyparser/qsParse.ts +179 -0
- package/src/bootstrap/hyper-server.ts +2 -9
- package/src/container/Container.ts +265 -25
- package/src/container/ContainerResolver.ts +84 -0
- package/src/container/ContextualBindingsBuilder.ts +41 -0
- package/src/container/types.ts +11 -7
- package/src/decorators/Service.ts +16 -3
- package/src/events/Emitter.ts +200 -25
- package/src/graphql/GraphQLEngine.ts +7 -8
- package/src/helpers/duration.ts +71 -0
- package/src/helpers/napi-loader.ts +19 -3
- package/src/helpers/types.ts +4 -1
- package/src/http/Exception.ts +17 -5
- package/src/http/HttpContext.ts +28 -5
- package/src/http/RedirectBuilder.ts +126 -11
- package/src/http/Request.ts +303 -13
- package/src/http/Response.ts +213 -6
- package/src/index.ts +17 -3
- package/src/router/Router.ts +98 -33
- package/src/security/CookieSigner.ts +102 -10
- package/src/server/Server.ts +36 -16
- package/src/session/ReadOnlyValuesStore.ts +52 -0
- package/src/session/Session.ts +364 -37
- package/src/session/SessionMiddleware.ts +93 -30
- package/src/session/drivers/DatabaseDriver.ts +184 -0
- package/src/session/drivers/FileDriver.ts +82 -0
- package/src/session/drivers/MemoryDriver.ts +75 -2
- package/src/session/drivers/RedisDriver.ts +77 -2
- package/src/session/templateTags.ts +124 -0
- package/src/testing/RequestBuilder.ts +76 -0
- package/src/testing/TestClient.ts +10 -2
- package/src/types/helpers.ts +4 -1
- package/src/utils/Macroable.ts +23 -8
- package/dist/utils/inflect.d.ts +0 -17
- package/dist/utils/inflect.d.ts.map +0 -1
- package/dist/utils/inflect.js +0 -32
- package/dist/utils/inflect.js.map +0 -1
- package/src/utils/inflect.ts +0 -29
package/src/events/Emitter.ts
CHANGED
|
@@ -33,18 +33,35 @@ type ListenerConstructor<T = unknown> = new (...args: never[]) => ListenerClass<
|
|
|
33
33
|
|
|
34
34
|
type Listener<T = unknown> = ListenerFn<T> | ListenerConstructor<T>
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Resolver for instantiating listener classes with DI — the narrow shape the
|
|
38
|
+
* emitter needs, so a test can hand it a fake without building a container.
|
|
39
|
+
* The real {@link import('../container/ContainerResolver.js').ContainerResolver}
|
|
40
|
+
* satisfies it.
|
|
41
|
+
*/
|
|
42
|
+
export interface EmitterResolver {
|
|
38
43
|
make<T>(target: new (...args: never[]) => T): Promise<T>
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
/**
|
|
47
|
+
* @deprecated Use {@link EmitterResolver}. The name now belongs to the real
|
|
48
|
+
* per-request resolver exported from `@c9up/ream`, as in AdonisJS.
|
|
49
|
+
*/
|
|
50
|
+
export type ContainerResolver = EmitterResolver
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What `on` / `once` / `onAny` hand back: call it to stop listening. Named
|
|
54
|
+
* after the AdonisJS type of the same shape.
|
|
55
|
+
*/
|
|
56
|
+
export type UnsubscribeFunction = () => void
|
|
57
|
+
|
|
41
58
|
export class Emitter {
|
|
42
59
|
private bus: EventBus
|
|
43
|
-
private resolver?:
|
|
60
|
+
private resolver?: EmitterResolver
|
|
44
61
|
private classListeners: Map<EventConstructor, Listener[]> = new Map()
|
|
45
62
|
private stringListeners: Map<string, ListenerFn[]> = new Map()
|
|
46
63
|
|
|
47
|
-
constructor(bus: EventBus, resolver?:
|
|
64
|
+
constructor(bus: EventBus, resolver?: EmitterResolver) {
|
|
48
65
|
this.bus = bus
|
|
49
66
|
this.resolver = resolver
|
|
50
67
|
}
|
|
@@ -56,46 +73,184 @@ export class Emitter {
|
|
|
56
73
|
* emitter.on(TaskDeclared, SendNotification) // listener class
|
|
57
74
|
* emitter.on(TaskDeclared, (event) => { ... }) // inline function
|
|
58
75
|
*/
|
|
59
|
-
on<T>(event: EventConstructor<T>, listener: Listener<T>):
|
|
76
|
+
on<T>(event: EventConstructor<T>, listener: Listener<T>): UnsubscribeFunction
|
|
60
77
|
/**
|
|
61
78
|
* Listen for a string-based event.
|
|
62
79
|
* emitter.on('user:registered', (user) => { ... })
|
|
63
80
|
* emitter.on<User>('user:registered', (user) => { ... }) // typed payload
|
|
81
|
+
*
|
|
82
|
+
* Returns an unsubscribe function, as AdonisJS does — keeping the listener
|
|
83
|
+
* reference around just to pass it back to `off` is the boilerplate that
|
|
84
|
+
* function removes.
|
|
64
85
|
*/
|
|
65
|
-
on<T>(event: string, listener: ListenerFn<T>):
|
|
66
|
-
on(event: EventConstructor | string, listener: Listener):
|
|
86
|
+
on<T>(event: string, listener: ListenerFn<T>): UnsubscribeFunction
|
|
87
|
+
on(event: EventConstructor | string, listener: Listener): UnsubscribeFunction {
|
|
67
88
|
if (typeof event === 'string') {
|
|
68
89
|
const list = this.stringListeners.get(event) ?? []
|
|
69
90
|
list.push(listener as ListenerFn)
|
|
70
91
|
this.stringListeners.set(event, list)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
return () => {
|
|
93
|
+
this.off(event, listener as ListenerFn)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const list = this.classListeners.get(event) ?? []
|
|
97
|
+
list.push(listener)
|
|
98
|
+
this.classListeners.set(event, list)
|
|
99
|
+
return () => {
|
|
100
|
+
const current = this.classListeners.get(event)
|
|
101
|
+
if (!current) return
|
|
102
|
+
const index = current.indexOf(listener)
|
|
103
|
+
if (index !== -1) current.splice(index, 1)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Listen for the NEXT occurrence only (AdonisJS `once`).
|
|
109
|
+
*
|
|
110
|
+
* The listener removes itself before running, so a handler that emits the
|
|
111
|
+
* same event does not re-enter it.
|
|
112
|
+
*/
|
|
113
|
+
once<T>(event: string, listener: ListenerFn<T>): void {
|
|
114
|
+
const wrapper: ListenerFn = (data) => {
|
|
115
|
+
this.off(event, wrapper)
|
|
116
|
+
return (listener as ListenerFn)(data)
|
|
75
117
|
}
|
|
118
|
+
this.on(event, wrapper as ListenerFn<T>)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Register the listener only when `condition` holds (AdonisJS `listenIf`) —
|
|
123
|
+
* the shape a feature flag takes at boot, without an `if` around every call.
|
|
124
|
+
*/
|
|
125
|
+
listenIf<T>(condition: boolean, event: string, listener: ListenerFn<T>): void {
|
|
126
|
+
if (condition) this.on(event, listener)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Alias of {@link on} (AdonisJS names it `listen`). */
|
|
130
|
+
listen<T>(event: string, listener: ListenerFn<T>): void {
|
|
131
|
+
this.on(event, listener)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Report listener failures (AdonisJS `onError`). */
|
|
135
|
+
onError(listener: (event: string, error: unknown) => void): void {
|
|
136
|
+
this.on('emitter:error', (payload) => {
|
|
137
|
+
const detail = payload as { event?: string; error?: unknown }
|
|
138
|
+
listener(detail?.event ?? 'unknown', detail?.error)
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Remove one listener from an event (AdonisJS `off`). */
|
|
143
|
+
off(event: string, listener: ListenerFn): void {
|
|
144
|
+
const list = this.stringListeners.get(event)
|
|
145
|
+
if (!list) return
|
|
146
|
+
const index = list.indexOf(listener)
|
|
147
|
+
if (index !== -1) list.splice(index, 1)
|
|
148
|
+
if (list.length === 0) this.stringListeners.delete(event)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Alias of {@link off} (AdonisJS `clearListener`). */
|
|
152
|
+
clearListener(event: string, listener: ListenerFn): void {
|
|
153
|
+
this.off(event, listener)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Drop every listener of one event (AdonisJS `clearListeners`). */
|
|
157
|
+
clearListeners(event: string): void {
|
|
158
|
+
this.stringListeners.delete(event)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Drop every listener of every event (AdonisJS `clearAllListeners`). */
|
|
162
|
+
clearAllListeners(): void {
|
|
163
|
+
this.stringListeners.clear()
|
|
164
|
+
this.classListeners.clear()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** How many listeners an event has, or all of them (AdonisJS `listenerCount`). */
|
|
168
|
+
listenerCount(event?: string): number {
|
|
169
|
+
if (event !== undefined) return this.stringListeners.get(event)?.length ?? 0
|
|
170
|
+
let total = 0
|
|
171
|
+
for (const list of this.stringListeners.values()) total += list.length
|
|
172
|
+
for (const list of this.classListeners.values()) total += list.length
|
|
173
|
+
return total
|
|
76
174
|
}
|
|
77
175
|
|
|
78
176
|
// ─── Emit ─────────────────────────────────────────────────
|
|
79
177
|
|
|
80
178
|
/**
|
|
81
|
-
*
|
|
179
|
+
* Run an event's listeners, in parallel or one after another.
|
|
180
|
+
*
|
|
181
|
+
* A listener that throws is reported and does NOT stop the others: one
|
|
182
|
+
* failing subscriber must not silently cancel the rest of the reaction to an
|
|
183
|
+
* event it does not own.
|
|
82
184
|
*/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
185
|
+
async #dispatchListeners(event: string, data: unknown, serial: boolean): Promise<void> {
|
|
186
|
+
const listeners = [
|
|
187
|
+
...(this.stringListeners.get(event) ?? []),
|
|
188
|
+
...this.#wildcardListenersFor(event),
|
|
189
|
+
]
|
|
190
|
+
if (listeners.length === 0) return
|
|
191
|
+
const run = async (fn: ListenerFn): Promise<void> => {
|
|
192
|
+
try {
|
|
193
|
+
await fn(data)
|
|
194
|
+
} catch (err) {
|
|
195
|
+
this.emitError(event, err)
|
|
92
196
|
}
|
|
93
197
|
}
|
|
198
|
+
if (serial) {
|
|
199
|
+
for (const fn of listeners) await run(fn)
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
await Promise.all(listeners.map(run))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Listeners registered through `onAny` whose pattern matches `event`. */
|
|
206
|
+
#wildcardListenersFor(_event: string): ListenerFn[] {
|
|
207
|
+
return []
|
|
208
|
+
}
|
|
94
209
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Emit and run the listeners ONE AT A TIME (AdonisJS `emitSerial`).
|
|
212
|
+
*
|
|
213
|
+
* Use it when order matters — a listener that seeds what the next one reads.
|
|
214
|
+
*/
|
|
215
|
+
async emitSerial(event: string, data: unknown): Promise<void> {
|
|
216
|
+
await this.#dispatchListeners(event, data, true)
|
|
217
|
+
this.#publishToBus(event, data)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Emit a string-based event.
|
|
222
|
+
*
|
|
223
|
+
* Returns a promise resolving once every listener has finished, as AdonisJS
|
|
224
|
+
* does — awaiting it is how a handler makes sure the work it triggered
|
|
225
|
+
* actually happened before it answers. Listeners run in PARALLEL; use
|
|
226
|
+
* {@link emitSerial} when one has to finish before the next starts.
|
|
227
|
+
*
|
|
228
|
+
* Not awaiting it keeps the old fire-and-forget behaviour: a listener that
|
|
229
|
+
* rejects is reported through `emitError`, never left unhandled.
|
|
230
|
+
*/
|
|
231
|
+
async emit(event: string, data: unknown): Promise<void> {
|
|
232
|
+
await this.#dispatchListeners(event, data, false)
|
|
233
|
+
this.#publishToBus(event, data)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Push the event onto the bus for cross-service / Rust subscribers.
|
|
238
|
+
*
|
|
239
|
+
* `#wrapForBus` prepends the correlation envelope when set so the ID reaches
|
|
240
|
+
* the other side; without it `setCorrelationId()` was a no-op.
|
|
241
|
+
*/
|
|
242
|
+
#publishToBus(event: string, data: unknown): void {
|
|
243
|
+
// Serialize INSIDE the guard. `JSON.stringify` throws synchronously on a
|
|
244
|
+
// cycle or a BigInt, and it was evaluated as an argument — so the throw
|
|
245
|
+
// escaped past the `.catch` and crashed the caller.
|
|
246
|
+
let payload: string
|
|
247
|
+
try {
|
|
248
|
+
payload = JSON.stringify(this.#wrapForBus(data))
|
|
249
|
+
} catch (err) {
|
|
250
|
+
this.emitError(event, err)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
void this.bus.emit(event, payload).catch((err: unknown) => {
|
|
99
254
|
this.emitError(event, err)
|
|
100
255
|
})
|
|
101
256
|
}
|
|
@@ -161,7 +316,27 @@ export class Emitter {
|
|
|
161
316
|
* emitter.onAny('order.*', (name, data) => { ... }) // single segment
|
|
162
317
|
* emitter.onAny('order.**', (name, data) => { ... }) // deep match
|
|
163
318
|
*/
|
|
319
|
+
async onAny(listener: (eventName: string, data: unknown) => void): Promise<number>
|
|
320
|
+
async onAny(
|
|
321
|
+
pattern: string,
|
|
322
|
+
listener: (eventName: string, data: unknown) => void,
|
|
323
|
+
): Promise<number>
|
|
164
324
|
async onAny(
|
|
325
|
+
patternOrListener: string | ((eventName: string, data: unknown) => void),
|
|
326
|
+
maybeListener?: (eventName: string, data: unknown) => void,
|
|
327
|
+
): Promise<number> {
|
|
328
|
+
// `onAny(listener)` — every event, the AdonisJS signature. `**` is the
|
|
329
|
+
// deep-match pattern the Rust matcher already understands, so the two
|
|
330
|
+
// forms share one implementation.
|
|
331
|
+
const pattern = typeof patternOrListener === 'string' ? patternOrListener : '**'
|
|
332
|
+
const listener = typeof patternOrListener === 'string' ? maybeListener : patternOrListener
|
|
333
|
+
if (listener === undefined) {
|
|
334
|
+
throw new TypeError('onAny() requires a listener')
|
|
335
|
+
}
|
|
336
|
+
return this.#subscribeAny(pattern, listener)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async #subscribeAny(
|
|
165
340
|
pattern: string,
|
|
166
341
|
listener: (eventName: string, data: unknown) => void,
|
|
167
342
|
): Promise<number> {
|
|
@@ -509,6 +509,11 @@ export class GraphQLEngine {
|
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
/** Parse and validate an unknown request body as a GraphQL request object. */
|
|
512
|
+
/** GraphQL `variables` must be a keyed object — never an array or null. */
|
|
513
|
+
function isVariablesRecord(value: unknown): value is Record<string, unknown> {
|
|
514
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
515
|
+
}
|
|
516
|
+
|
|
512
517
|
function parseGraphQLBody(body: unknown): GraphQLRequest | null {
|
|
513
518
|
if (!body || typeof body !== 'object' || Array.isArray(body)) return null
|
|
514
519
|
// After typeof check, body is a non-null non-array object; index via 'in' narrowing
|
|
@@ -516,14 +521,8 @@ function parseGraphQLBody(body: unknown): GraphQLRequest | null {
|
|
|
516
521
|
if (typeof q !== 'string') return null
|
|
517
522
|
|
|
518
523
|
let variables: Record<string, unknown> | undefined
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
body.variables !== null &&
|
|
522
|
-
typeof body.variables === 'object' &&
|
|
523
|
-
!Array.isArray(body.variables)
|
|
524
|
-
) {
|
|
525
|
-
// biome-ignore lint/suspicious/noExplicitAny: narrowed to non-null non-array object above; branded as Record for safe spreading
|
|
526
|
-
variables = body.variables as any as Record<string, unknown>
|
|
524
|
+
if ('variables' in body && isVariablesRecord(body.variables)) {
|
|
525
|
+
variables = body.variables
|
|
527
526
|
}
|
|
528
527
|
|
|
529
528
|
const rawOpName = 'operationName' in body ? body.operationName : undefined
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duration parsing, matching what AdonisJS accepts.
|
|
3
|
+
*
|
|
4
|
+
* Adonis runs every duration through `string.seconds.parse()`
|
|
5
|
+
* (`@poppinss/string`), which delegates string forms to `@lukeed/ms`. A config
|
|
6
|
+
* carrying `age: '2 hours'` or `maxAge: '30 mins'` therefore has to work here
|
|
7
|
+
* too — this is a value an app moves over verbatim.
|
|
8
|
+
*
|
|
9
|
+
* Two rules taken straight from that implementation, both easy to get wrong:
|
|
10
|
+
*
|
|
11
|
+
* - A **number is already in seconds** and is returned untouched. It is NOT
|
|
12
|
+
* milliseconds, whatever a reading of the guides suggests: `seconds.parse`
|
|
13
|
+
* starts with `if (typeof duration === 'number') return duration`.
|
|
14
|
+
* - A **string with no unit is in milliseconds**. `@lukeed/ms` falls through
|
|
15
|
+
* its unit branches and returns the bare number as ms, so Adonis' `'7200'`
|
|
16
|
+
* is seven seconds, not two hours. Treating it as seconds — which is the
|
|
17
|
+
* intuitive reading — would silently stretch every such value by 1000.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The unit vocabulary of `@lukeed/ms`, in the order its regex tests them.
|
|
22
|
+
* Written out rather than reusing its regex so the accepted spellings are
|
|
23
|
+
* readable: `2h`, `2 hrs`, `2 hours` all mean the same thing.
|
|
24
|
+
*/
|
|
25
|
+
const DURATION_PATTERN =
|
|
26
|
+
/^(-?(?:\d+)?\.?\d+) *(m(?:illiseconds?|s(?:ecs?)?))?(s(?:ec(?:onds?|s)?)?)?(m(?:in(?:utes?|s)?)?)?(h(?:ours?|rs?)?)?(d(?:ays?)?)?(w(?:eeks?|ks?)?)?(y(?:ears?|rs?)?)?$/
|
|
27
|
+
|
|
28
|
+
const SECOND = 1000
|
|
29
|
+
const MINUTE = SECOND * 60
|
|
30
|
+
const HOUR = MINUTE * 60
|
|
31
|
+
const DAY = HOUR * 24
|
|
32
|
+
const YEAR = DAY * 365.25
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a duration string to MILLISECONDS, or `undefined` when it is not a
|
|
36
|
+
* duration. Mirrors `@lukeed/ms`'s `parse`, including its quirk that a value
|
|
37
|
+
* with no unit is milliseconds.
|
|
38
|
+
*/
|
|
39
|
+
export function parseDurationMs(value: string): number | undefined {
|
|
40
|
+
const matched = DURATION_PATTERN.exec(value.toLowerCase())
|
|
41
|
+
if (matched === null) return undefined
|
|
42
|
+
const amount = Number.parseFloat(matched[1] ?? '')
|
|
43
|
+
// `@lukeed/ms` rejects a zero amount here (`(num = parseFloat(...))` is
|
|
44
|
+
// falsy), and so must we, or `'0 hours'` would part company with Adonis.
|
|
45
|
+
if (!amount) return undefined
|
|
46
|
+
if (matched[3] !== undefined) return amount * SECOND
|
|
47
|
+
if (matched[4] !== undefined) return amount * MINUTE
|
|
48
|
+
if (matched[5] !== undefined) return amount * HOUR
|
|
49
|
+
if (matched[6] !== undefined) return amount * DAY
|
|
50
|
+
if (matched[7] !== undefined) return amount * DAY * 7
|
|
51
|
+
if (matched[8] !== undefined) return amount * YEAR
|
|
52
|
+
// No unit — and `matched[2]` (an explicit `ms`) lands here too.
|
|
53
|
+
return amount
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A duration as SECONDS, the way `string.seconds.parse` returns it: a number
|
|
58
|
+
* passes through untouched, a string is parsed and floored.
|
|
59
|
+
*
|
|
60
|
+
* @param label - what is being parsed, for the error message.
|
|
61
|
+
*/
|
|
62
|
+
export function durationToSeconds(value: number | string, label: string): number {
|
|
63
|
+
if (typeof value === 'number') return Math.trunc(value)
|
|
64
|
+
const milliseconds = parseDurationMs(value)
|
|
65
|
+
if (milliseconds === undefined) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Cannot read "${value}" as ${label}. Use a number of seconds, or a duration such as '30m', '2 hours' or '7 days'.`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
return Math.floor(milliseconds / SECOND)
|
|
71
|
+
}
|
|
@@ -13,7 +13,16 @@ import { arch, platform } from 'node:process'
|
|
|
13
13
|
import { fileURLToPath } from 'node:url'
|
|
14
14
|
import { ReamError } from '../errors/ReamError.js'
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Platform triple suffix (matches napi-rs naming).
|
|
18
|
+
*
|
|
19
|
+
* The ONE map for `@c9up/ream`. Every native package carries its own — they are
|
|
20
|
+
* independently publishable and must not import each other — so a new target
|
|
21
|
+
* still has to be added per package; within ream it is added here once.
|
|
22
|
+
*
|
|
23
|
+
* Linux binaries target GLIBC. musl (Alpine) is not a supported target, and the
|
|
24
|
+
* loader says so rather than reporting a bare "not found".
|
|
25
|
+
*/
|
|
17
26
|
export const NAPI_PLATFORM_MAP: Record<string, string> = {
|
|
18
27
|
'linux-x64': 'linux-x64-gnu',
|
|
19
28
|
'linux-arm64': 'linux-arm64-gnu',
|
|
@@ -94,13 +103,20 @@ export function loadNapi<T>(options: LoadNapiOptions): T {
|
|
|
94
103
|
}
|
|
95
104
|
}
|
|
96
105
|
|
|
106
|
+
// On a musl host the glibc binary is present but never loads, so "not found"
|
|
107
|
+
// sends the reader looking for a missing file that is right there. Name the
|
|
108
|
+
// real reason instead.
|
|
109
|
+
const muslHint = suffix.endsWith('-gnu')
|
|
110
|
+
? ' If you are on Alpine/musl, note the prebuilt Linux binaries target glibc — musl is not a supported target.'
|
|
111
|
+
: ''
|
|
97
112
|
throw new ReamError(
|
|
98
113
|
`${errorCodePrefix}_NAPI_NOT_FOUND`,
|
|
99
114
|
`${binaryName} NAPI binary not found. Expected ${binaryName}.${suffix}.node`,
|
|
100
115
|
{
|
|
101
|
-
hint:
|
|
116
|
+
hint: `${
|
|
102
117
|
options.notFoundHint ??
|
|
103
|
-
"Run 'pnpm --filter @c9up/ream build:rust' to build the native module."
|
|
118
|
+
"Run 'pnpm --filter @c9up/ream build:rust' to build the native module."
|
|
119
|
+
}${muslHint}`,
|
|
104
120
|
},
|
|
105
121
|
)
|
|
106
122
|
}
|
package/src/helpers/types.ts
CHANGED
|
@@ -25,7 +25,10 @@ export type UnwrapOpaque<T> = T extends Opaque<infer Base, string> ? Base : T
|
|
|
25
25
|
export type AsyncOrSync<T> = T | Promise<T>
|
|
26
26
|
|
|
27
27
|
/** Constructor type. */
|
|
28
|
-
|
|
28
|
+
// The default `Args` is `never[]`, not `unknown[]`: parameters are
|
|
29
|
+
// contravariant, so only a rest of `never` accepts EVERY concrete parameter
|
|
30
|
+
// list. Pass `Args` explicitly when you need to call the constructor.
|
|
31
|
+
export type Constructor<T = unknown, Args extends unknown[] = never[]> = new (...args: Args) => T
|
|
29
32
|
|
|
30
33
|
/** Abstract constructor type. */
|
|
31
34
|
export type AbstractConstructor<T = unknown> = abstract new (...args: unknown[]) => T
|
package/src/http/Exception.ts
CHANGED
|
@@ -42,11 +42,23 @@ export class Exception extends Error {
|
|
|
42
42
|
constructor(message: string, options?: { status?: number; code?: string }) {
|
|
43
43
|
super(message)
|
|
44
44
|
this.name = this.constructor.name
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
// `this.constructor` is typed `Function`, which carries none of the statics
|
|
46
|
+
// a subclass sets (`status`, `code`, `help`). Reading them through
|
|
47
|
+
// `Reflect.get` states that we are looking at OPTIONAL statics, which is
|
|
48
|
+
// exactly the contract — a subclass need not define any of them.
|
|
49
|
+
const ctor = this.constructor
|
|
50
|
+
const staticNumber = (key: string): number | undefined => {
|
|
51
|
+
const v = Reflect.get(ctor, key)
|
|
52
|
+
return typeof v === 'number' ? v : undefined
|
|
53
|
+
}
|
|
54
|
+
const staticString = (key: string): string | undefined => {
|
|
55
|
+
const v = Reflect.get(ctor, key)
|
|
56
|
+
return typeof v === 'string' ? v : undefined
|
|
57
|
+
}
|
|
58
|
+
this.status = options?.status ?? staticNumber('status') ?? Exception.status
|
|
59
|
+
this.code = options?.code ?? staticString('code') ?? Exception.code
|
|
60
|
+
const help = staticString('help')
|
|
61
|
+
if (help !== undefined) this.help = help
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
/** Override to self-handle the exception (convert to HTTP response). */
|
package/src/http/HttpContext.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
11
11
|
import type { ServiceToken } from '../container/types.js'
|
|
12
12
|
import type { Emitter } from '../events/Emitter.js'
|
|
13
|
+
import type { MatchedParams } from '../router/Router.js'
|
|
13
14
|
import type { CookieSigner } from '../security/CookieSigner.js'
|
|
14
15
|
import type { SignedUrl } from '../security/SignedUrl.js'
|
|
15
16
|
import type { Session } from '../session/Session.js'
|
|
@@ -87,7 +88,19 @@ export interface Authorizer {
|
|
|
87
88
|
// pattern as warden/blackhole middleware), so nothing cross-imports it.
|
|
88
89
|
interface ContainerResolver {
|
|
89
90
|
/** Resolve/construct a service by token (class, string, or symbol). Async (AdonisJS parity). */
|
|
90
|
-
make<T>(token: ServiceToken): Promise<T>
|
|
91
|
+
make<T>(token: ServiceToken, runtimeValues?: unknown[]): Promise<T>
|
|
92
|
+
/** Call a method with its dependencies injected (AdonisJS `resolver.call`). */
|
|
93
|
+
call<T, K extends string & keyof T>(
|
|
94
|
+
instance: T,
|
|
95
|
+
method: K,
|
|
96
|
+
runtimeValues?: unknown[],
|
|
97
|
+
): Promise<unknown>
|
|
98
|
+
/** Bind a value for THIS request only (AdonisJS `resolver.bindValue`). */
|
|
99
|
+
bindValue<T>(token: ServiceToken, value: T): void
|
|
100
|
+
/** Whether the token resolves (AdonisJS `resolver.hasBinding`). */
|
|
101
|
+
hasBinding(token: ServiceToken): boolean
|
|
102
|
+
/** Whether every token resolves (AdonisJS `resolver.hasAllBindings`). */
|
|
103
|
+
hasAllBindings(tokens: ServiceToken[]): boolean
|
|
91
104
|
}
|
|
92
105
|
|
|
93
106
|
/**
|
|
@@ -204,7 +217,16 @@ export class HttpContext extends Macroable {
|
|
|
204
217
|
readonly response: Response
|
|
205
218
|
|
|
206
219
|
/** Route parameters extracted from the URL pattern. */
|
|
207
|
-
|
|
220
|
+
/** Matched route parameters — `*` holds the segments it swallowed, as an array. */
|
|
221
|
+
/**
|
|
222
|
+
* Route parameters.
|
|
223
|
+
*
|
|
224
|
+
* `string | string[]`, not AdonisJS's `any`: a catch-all `*` really is the
|
|
225
|
+
* ARRAY of segments on both sides, so the `any` upstream only hides it. Use
|
|
226
|
+
* {@link Request.param} when you want the single-value form — it joins a
|
|
227
|
+
* catch-all with `/` and returns `string | undefined`.
|
|
228
|
+
*/
|
|
229
|
+
readonly params: MatchedParams
|
|
208
230
|
|
|
209
231
|
/** Information about the matched route. */
|
|
210
232
|
readonly route: RouteInfo
|
|
@@ -246,8 +268,9 @@ export class HttpContext extends Macroable {
|
|
|
246
268
|
|
|
247
269
|
/**
|
|
248
270
|
* Per-request IoC resolver (Adonis idiom: `ctx.containerResolver.make(...)`).
|
|
249
|
-
*
|
|
250
|
-
*
|
|
271
|
+
* Built by `HttpKernel` with `container.createResolver()`, so a value bound
|
|
272
|
+
* on it belongs to THIS request and no other. Undefined only when the context
|
|
273
|
+
* was built without one (e.g. a mock in a unit test).
|
|
251
274
|
* Agnostic middleware resolves host services through this, never by importing
|
|
252
275
|
* `@c9up/ream`. See {@link ContainerResolver}.
|
|
253
276
|
*/
|
|
@@ -303,7 +326,7 @@ export class HttpContext extends Macroable {
|
|
|
303
326
|
constructor(
|
|
304
327
|
id: string,
|
|
305
328
|
rawRequest: RawRequest,
|
|
306
|
-
params:
|
|
329
|
+
params: MatchedParams,
|
|
307
330
|
route: RouteInfo,
|
|
308
331
|
containerResolver?: ContainerResolver,
|
|
309
332
|
) {
|