@c9up/ream 0.1.34 → 0.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.
- package/dist/Application.d.ts +68 -0
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +142 -6
- package/dist/Application.js.map +1 -1
- package/dist/HealthCheck.d.ts +14 -7
- package/dist/HealthCheck.d.ts.map +1 -1
- package/dist/HealthCheck.js +21 -12
- package/dist/HealthCheck.js.map +1 -1
- package/dist/HttpKernel.d.ts +12 -0
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +15 -4
- 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 +11 -0
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +13 -2
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +50 -30
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts +65 -6
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +100 -7
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/console/BaseCommand.d.ts +8 -1
- package/dist/console/BaseCommand.d.ts.map +1 -1
- package/dist/console/BaseCommand.js +1 -1
- package/dist/console/BaseCommand.js.map +1 -1
- package/dist/console/Kernel.d.ts +15 -0
- package/dist/console/Kernel.d.ts.map +1 -1
- package/dist/console/Kernel.js +17 -0
- package/dist/console/Kernel.js.map +1 -1
- package/dist/directories.d.ts +52 -0
- package/dist/directories.d.ts.map +1 -0
- package/dist/directories.js +38 -0
- package/dist/directories.js.map +1 -0
- package/dist/events/Emitter.d.ts +49 -6
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +117 -7
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/EventsBuffer.d.ts +47 -0
- package/dist/events/EventsBuffer.d.ts.map +1 -0
- package/dist/events/EventsBuffer.js +107 -0
- package/dist/events/EventsBuffer.js.map +1 -0
- package/dist/events/index.d.ts +2 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +1 -0
- package/dist/events/index.js.map +1 -1
- package/dist/health/BaseCheck.d.ts +19 -0
- package/dist/health/BaseCheck.d.ts.map +1 -0
- package/dist/health/BaseCheck.js +24 -0
- package/dist/health/BaseCheck.js.map +1 -0
- package/dist/health/HealthChecks.d.ts +15 -0
- package/dist/health/HealthChecks.d.ts.map +1 -0
- package/dist/health/HealthChecks.js +69 -0
- package/dist/health/HealthChecks.js.map +1 -0
- package/dist/health/Result.d.ts +27 -0
- package/dist/health/Result.d.ts.map +1 -0
- package/dist/health/Result.js +55 -0
- package/dist/health/Result.js.map +1 -0
- package/dist/health/checks/DiskSpaceCheck.d.ts +29 -0
- package/dist/health/checks/DiskSpaceCheck.d.ts.map +1 -0
- package/dist/health/checks/DiskSpaceCheck.js +58 -0
- package/dist/health/checks/DiskSpaceCheck.js.map +1 -0
- package/dist/health/checks/MemoryHeapCheck.d.ts +21 -0
- package/dist/health/checks/MemoryHeapCheck.d.ts.map +1 -0
- package/dist/health/checks/MemoryHeapCheck.js +55 -0
- package/dist/health/checks/MemoryHeapCheck.js.map +1 -0
- package/dist/health/checks/MemoryRSSCheck.d.ts +21 -0
- package/dist/health/checks/MemoryRSSCheck.d.ts.map +1 -0
- package/dist/health/checks/MemoryRSSCheck.js +55 -0
- package/dist/health/checks/MemoryRSSCheck.js.map +1 -0
- package/dist/health/checks/memory.d.ts +24 -0
- package/dist/health/checks/memory.d.ts.map +1 -0
- package/dist/health/checks/memory.js +88 -0
- package/dist/health/checks/memory.js.map +1 -0
- package/dist/health/index.d.ts +9 -0
- package/dist/health/index.d.ts.map +1 -0
- package/dist/health/index.js +8 -0
- package/dist/health/index.js.map +1 -0
- package/dist/health/tracing.d.ts +12 -0
- package/dist/health/tracing.d.ts.map +1 -0
- package/dist/health/tracing.js +11 -0
- package/dist/health/tracing.js.map +1 -0
- package/dist/health/types.d.ts +51 -0
- package/dist/health/types.d.ts.map +1 -0
- package/dist/health/types.js +5 -0
- package/dist/health/types.js.map +1 -0
- package/dist/helpers/bytes.d.ts +28 -0
- package/dist/helpers/bytes.d.ts.map +1 -0
- package/dist/helpers/bytes.js +63 -0
- package/dist/helpers/bytes.js.map +1 -0
- package/dist/helpers/string.d.ts +4 -0
- package/dist/helpers/string.d.ts.map +1 -1
- package/dist/helpers/string.js +2 -0
- package/dist/helpers/string.js.map +1 -1
- package/dist/http/HttpContext.d.ts +30 -2
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +73 -3
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/Request.d.ts +42 -3
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +51 -7
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +82 -2
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +240 -13
- package/dist/http/Response.js.map +1 -1
- package/dist/http/SseStream.d.ts +10 -0
- package/dist/http/SseStream.d.ts.map +1 -1
- package/dist/http/SseStream.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/migrations/MigrationRegistry.d.ts +28 -0
- package/dist/migrations/MigrationRegistry.d.ts.map +1 -0
- package/dist/migrations/MigrationRegistry.js +41 -0
- package/dist/migrations/MigrationRegistry.js.map +1 -0
- package/dist/migrations/types.d.ts +64 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +15 -0
- package/dist/migrations/types.js.map +1 -0
- package/dist/router/Router.d.ts +112 -9
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +206 -28
- package/dist/router/Router.js.map +1 -1
- package/dist/security/CookieSigner.d.ts +43 -4
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +68 -10
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/session/Session.d.ts +46 -1
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +57 -4
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +19 -9
- package/dist/session/SessionMiddleware.js.map +1 -1
- 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 +3 -3
- package/dist/session/drivers/CookieDriver.js.map +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
- package/dist/session/drivers/DatabaseDriver.js +2 -2
- package/dist/session/drivers/DatabaseDriver.js.map +1 -1
- package/dist/session/drivers/FileDriver.d.ts +1 -1
- package/dist/session/drivers/FileDriver.d.ts.map +1 -1
- package/dist/session/drivers/FileDriver.js +7 -5
- package/dist/session/drivers/FileDriver.js.map +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +1 -1
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts +1 -1
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +11 -4
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +5 -2
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Application.ts +169 -6
- package/src/HealthCheck.ts +27 -14
- package/src/HttpKernel.ts +27 -4
- package/src/Ignitor.ts +17 -0
- package/src/bodyparser/BodyParserMiddleware.ts +70 -36
- package/src/bodyparser/MultipartFile.ts +125 -8
- package/src/console/BaseCommand.ts +8 -1
- package/src/console/Kernel.ts +18 -0
- package/src/directories.ts +81 -0
- package/src/events/Emitter.ts +129 -13
- package/src/events/EventsBuffer.ts +131 -0
- package/src/events/index.ts +7 -0
- package/src/health/BaseCheck.ts +30 -0
- package/src/health/HealthChecks.ts +83 -0
- package/src/health/Result.ts +69 -0
- package/src/health/checks/DiskSpaceCheck.ts +69 -0
- package/src/health/checks/MemoryHeapCheck.ts +68 -0
- package/src/health/checks/MemoryRSSCheck.ts +68 -0
- package/src/health/checks/memory.ts +115 -0
- package/src/health/index.ts +13 -0
- package/src/health/tracing.ts +15 -0
- package/src/health/types.ts +51 -0
- package/src/helpers/bytes.ts +71 -0
- package/src/helpers/string.ts +2 -0
- package/src/http/HttpContext.ts +78 -4
- package/src/http/Request.ts +62 -10
- package/src/http/Response.ts +283 -14
- package/src/http/SseStream.ts +10 -0
- package/src/index.ts +27 -1
- package/src/migrations/MigrationRegistry.ts +49 -0
- package/src/migrations/types.ts +68 -0
- package/src/router/Router.ts +241 -32
- package/src/security/CookieSigner.ts +92 -18
- package/src/session/Session.ts +71 -5
- package/src/session/SessionMiddleware.ts +19 -9
- package/src/session/drivers/CookieDriver.ts +4 -4
- package/src/session/drivers/DatabaseDriver.ts +3 -3
- package/src/session/drivers/FileDriver.ts +8 -6
- package/src/session/drivers/MemoryDriver.ts +2 -2
- package/src/session/drivers/RedisDriver.ts +12 -5
- package/tsconfig.app.json +13 -13
- package/tsconfig.base.json +19 -0
- package/tsconfig.client.json +16 -0
- package/tsconfig.package.json +15 -0
package/src/events/Emitter.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
16
|
+
import { type BufferedEventName, EventsBuffer } from './EventsBuffer.js'
|
|
16
17
|
import type { EventBus } from './native.js'
|
|
17
18
|
|
|
18
19
|
/** Per-request correlation ID stored in async context so concurrent requests don't corrupt each other. */
|
|
@@ -55,11 +56,26 @@ export type ContainerResolver = EmitterResolver
|
|
|
55
56
|
*/
|
|
56
57
|
export type UnsubscribeFunction = () => void
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* What `onAny` hands back. Same shape as {@link UnsubscribeFunction} — and
|
|
61
|
+
* assignable wherever one is expected — but awaitable, because dropping a
|
|
62
|
+
* wildcard subscription is a NAPI call into the Rust bus. Ignore the promise
|
|
63
|
+
* for the AdonisJS behaviour; await it when the next assertion depends on the
|
|
64
|
+
* subscription being gone.
|
|
65
|
+
*/
|
|
66
|
+
export type AsyncUnsubscribeFunction = () => Promise<void>
|
|
67
|
+
|
|
58
68
|
export class Emitter {
|
|
59
69
|
private bus: EventBus
|
|
60
70
|
private resolver?: EmitterResolver
|
|
61
71
|
private classListeners: Map<EventConstructor, Listener[]> = new Map()
|
|
62
72
|
private stringListeners: Map<string, ListenerFn[]> = new Map()
|
|
73
|
+
/** Set while faking; every emission lands here instead of running. */
|
|
74
|
+
#buffer?: EventsBuffer
|
|
75
|
+
/** Which events are faked. `undefined` means all of them. */
|
|
76
|
+
#fakedEvents?: Set<BufferedEventName>
|
|
77
|
+
/** Wildcard subscriptions, so `offAny(listener)` can find its Rust handle. */
|
|
78
|
+
readonly #anySubscriptions = new Map<(eventName: string, data: unknown) => void, Set<number>>()
|
|
63
79
|
|
|
64
80
|
constructor(bus: EventBus, resolver?: EmitterResolver) {
|
|
65
81
|
this.bus = bus
|
|
@@ -122,8 +138,10 @@ export class Emitter {
|
|
|
122
138
|
* Register the listener only when `condition` holds (AdonisJS `listenIf`) —
|
|
123
139
|
* the shape a feature flag takes at boot, without an `if` around every call.
|
|
124
140
|
*/
|
|
125
|
-
listenIf<T>(condition: boolean, event: string, listener: ListenerFn<T>): void {
|
|
126
|
-
|
|
141
|
+
listenIf<T>(condition: boolean | (() => boolean), event: string, listener: ListenerFn<T>): void {
|
|
142
|
+
// AdonisJS takes either — a thunk lets the flag be read at registration
|
|
143
|
+
// time rather than at the call site.
|
|
144
|
+
if (typeof condition === 'function' ? condition() : condition) this.on(event, listener)
|
|
127
145
|
}
|
|
128
146
|
|
|
129
147
|
/** Alias of {@link on} (AdonisJS names it `listen`). */
|
|
@@ -164,6 +182,20 @@ export class Emitter {
|
|
|
164
182
|
this.classListeners.clear()
|
|
165
183
|
}
|
|
166
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Every event with the listeners registered for it (AdonisJS
|
|
187
|
+
* `eventsListeners`).
|
|
188
|
+
*
|
|
189
|
+
* Read-only copies: a caller inspecting the map — a debug screen, a test
|
|
190
|
+
* asserting nothing leaked — must not be able to unsubscribe by mutating it.
|
|
191
|
+
*/
|
|
192
|
+
get eventsListeners(): Map<EventConstructor | string, Listener[]> {
|
|
193
|
+
const all = new Map<EventConstructor | string, Listener[]>()
|
|
194
|
+
for (const [event, listeners] of this.classListeners) all.set(event, [...listeners])
|
|
195
|
+
for (const [event, listeners] of this.stringListeners) all.set(event, [...listeners])
|
|
196
|
+
return all
|
|
197
|
+
}
|
|
198
|
+
|
|
167
199
|
/** How many listeners an event has, or all of them (AdonisJS `listenerCount`). */
|
|
168
200
|
listenerCount(event?: string): number {
|
|
169
201
|
if (event !== undefined) return this.stringListeners.get(event)?.length ?? 0
|
|
@@ -213,6 +245,7 @@ export class Emitter {
|
|
|
213
245
|
* Use it when order matters — a listener that seeds what the next one reads.
|
|
214
246
|
*/
|
|
215
247
|
async emitSerial(event: string, data: unknown): Promise<void> {
|
|
248
|
+
if (this.#capture(event, data)) return
|
|
216
249
|
await this.#dispatchListeners(event, data, true)
|
|
217
250
|
this.#publishToBus(event, data)
|
|
218
251
|
}
|
|
@@ -229,6 +262,7 @@ export class Emitter {
|
|
|
229
262
|
* rejects is reported through `emitError`, never left unhandled.
|
|
230
263
|
*/
|
|
231
264
|
async emit(event: string, data: unknown): Promise<void> {
|
|
265
|
+
if (this.#capture(event, data)) return
|
|
232
266
|
await this.#dispatchListeners(event, data, false)
|
|
233
267
|
this.#publishToBus(event, data)
|
|
234
268
|
}
|
|
@@ -277,6 +311,9 @@ export class Emitter {
|
|
|
277
311
|
*/
|
|
278
312
|
async dispatchEvent<T extends object>(event: T): Promise<void> {
|
|
279
313
|
const EventClass = event.constructor as EventConstructor<T>
|
|
314
|
+
// Faked by the class, as AdonisJS does — `fake([TaskDeclared])` catches
|
|
315
|
+
// `new TaskDeclared(...).emit()`.
|
|
316
|
+
if (this.#capture(EventClass, event)) return
|
|
280
317
|
const listeners = this.classListeners.get(EventClass) ?? []
|
|
281
318
|
const name = (EventClass as { eventName?: string }).eventName ?? classToEventName(EventClass)
|
|
282
319
|
|
|
@@ -307,6 +344,51 @@ export class Emitter {
|
|
|
307
344
|
await this.bus.emit(name, JSON.stringify(this.#wrapForBus(event)))
|
|
308
345
|
}
|
|
309
346
|
|
|
347
|
+
// ─── Faking (AdonisJS `fake` / `restore`) ─────────────────
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Stop the named events from reaching their listeners and the bus, and
|
|
351
|
+
* collect them instead.
|
|
352
|
+
*
|
|
353
|
+
* With no argument every event is faked. Calling it again drops the previous
|
|
354
|
+
* fake and starts a fresh buffer, as AdonisJS does.
|
|
355
|
+
*
|
|
356
|
+
* ```ts
|
|
357
|
+
* const events = emitter.fake(['user:registered'])
|
|
358
|
+
* await register(payload)
|
|
359
|
+
* events.assertEmitted('user:registered')
|
|
360
|
+
* emitter.restore()
|
|
361
|
+
* ```
|
|
362
|
+
*/
|
|
363
|
+
fake(events?: BufferedEventName[]): EventsBuffer {
|
|
364
|
+
this.restore()
|
|
365
|
+
this.#fakedEvents = events === undefined ? undefined : new Set(events)
|
|
366
|
+
this.#buffer = new EventsBuffer(() => {
|
|
367
|
+
this.restore()
|
|
368
|
+
})
|
|
369
|
+
return this.#buffer
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Let events through again (AdonisJS `restore`). */
|
|
373
|
+
restore(): void {
|
|
374
|
+
this.#buffer = undefined
|
|
375
|
+
this.#fakedEvents = undefined
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Record the emission and report whether it was swallowed.
|
|
380
|
+
*
|
|
381
|
+
* Returns `true` when the event is faked — the caller must then return
|
|
382
|
+
* without dispatching or publishing, which is what "the listeners are not
|
|
383
|
+
* invoked" means.
|
|
384
|
+
*/
|
|
385
|
+
#capture(event: BufferedEventName, data: unknown): boolean {
|
|
386
|
+
if (this.#buffer === undefined) return false
|
|
387
|
+
if (this.#fakedEvents !== undefined && !this.#fakedEvents.has(event)) return false
|
|
388
|
+
this.#buffer.add(event, data)
|
|
389
|
+
return true
|
|
390
|
+
}
|
|
391
|
+
|
|
310
392
|
// ─── Wildcard subscriptions (via Rust NAPI) ───────────────
|
|
311
393
|
|
|
312
394
|
/**
|
|
@@ -316,15 +398,15 @@ export class Emitter {
|
|
|
316
398
|
* emitter.onAny('order.*', (name, data) => { ... }) // single segment
|
|
317
399
|
* emitter.onAny('order.**', (name, data) => { ... }) // deep match
|
|
318
400
|
*/
|
|
319
|
-
async onAny(listener: (eventName: string, data: unknown) => void): Promise<
|
|
401
|
+
async onAny(listener: (eventName: string, data: unknown) => void): Promise<UnsubscribeFunction>
|
|
320
402
|
async onAny(
|
|
321
403
|
pattern: string,
|
|
322
404
|
listener: (eventName: string, data: unknown) => void,
|
|
323
|
-
): Promise<
|
|
405
|
+
): Promise<UnsubscribeFunction>
|
|
324
406
|
async onAny(
|
|
325
407
|
patternOrListener: string | ((eventName: string, data: unknown) => void),
|
|
326
408
|
maybeListener?: (eventName: string, data: unknown) => void,
|
|
327
|
-
): Promise<
|
|
409
|
+
): Promise<UnsubscribeFunction> {
|
|
328
410
|
// `onAny(listener)` — every event, the AdonisJS signature. `**` is the
|
|
329
411
|
// deep-match pattern the Rust matcher already understands, so the two
|
|
330
412
|
// forms share one implementation.
|
|
@@ -333,7 +415,21 @@ export class Emitter {
|
|
|
333
415
|
if (listener === undefined) {
|
|
334
416
|
throw new TypeError('onAny() requires a listener')
|
|
335
417
|
}
|
|
336
|
-
|
|
418
|
+
const id = await this.#subscribeAny(pattern, listener)
|
|
419
|
+
|
|
420
|
+
// AdonisJS hands back the unsubscribe function, not the handle. The Rust
|
|
421
|
+
// subscription id stays reachable through `offAny(id)` because that is what
|
|
422
|
+
// the bus needs, but a caller following the AdonisJS shape never sees it.
|
|
423
|
+
let ids = this.#anySubscriptions.get(listener)
|
|
424
|
+
if (!ids) {
|
|
425
|
+
ids = new Set()
|
|
426
|
+
this.#anySubscriptions.set(listener, ids)
|
|
427
|
+
}
|
|
428
|
+
ids.add(id)
|
|
429
|
+
|
|
430
|
+
return async () => {
|
|
431
|
+
await this.offAny(id)
|
|
432
|
+
}
|
|
337
433
|
}
|
|
338
434
|
|
|
339
435
|
async #subscribeAny(
|
|
@@ -363,9 +459,28 @@ export class Emitter {
|
|
|
363
459
|
})
|
|
364
460
|
}
|
|
365
461
|
|
|
366
|
-
/**
|
|
367
|
-
|
|
368
|
-
|
|
462
|
+
/**
|
|
463
|
+
* Stop a wildcard subscription (AdonisJS `offAny`).
|
|
464
|
+
*
|
|
465
|
+
* Takes the listener, as AdonisJS does. The numeric handle the Rust bus
|
|
466
|
+
* issued is also accepted, because that is what the unsubscribe crosses NAPI
|
|
467
|
+
* with and `onAny`'s unsubscribe function closes over it.
|
|
468
|
+
*
|
|
469
|
+
* Async where AdonisJS is sync: dropping the subscription is a NAPI call, and
|
|
470
|
+
* returning before it lands would let an event through after `offAny`.
|
|
471
|
+
*/
|
|
472
|
+
async offAny(listenerOrId: number | ((eventName: string, data: unknown) => void)): Promise<this> {
|
|
473
|
+
if (typeof listenerOrId === 'number') {
|
|
474
|
+
await this.bus.unsubscribe(listenerOrId)
|
|
475
|
+
for (const ids of this.#anySubscriptions.values()) ids.delete(listenerOrId)
|
|
476
|
+
return this
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const ids = this.#anySubscriptions.get(listenerOrId)
|
|
480
|
+
if (!ids) return this
|
|
481
|
+
this.#anySubscriptions.delete(listenerOrId)
|
|
482
|
+
await Promise.all([...ids].map((id) => this.bus.unsubscribe(id)))
|
|
483
|
+
return this
|
|
369
484
|
}
|
|
370
485
|
|
|
371
486
|
/** Check if a pattern matches an event name (wildcard matching via Rust). */
|
|
@@ -446,10 +561,11 @@ export class Emitter {
|
|
|
446
561
|
/**
|
|
447
562
|
* Check if any listeners are registered for an event.
|
|
448
563
|
*/
|
|
449
|
-
hasListeners(event
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
564
|
+
hasListeners(event?: EventConstructor | string): boolean {
|
|
565
|
+
// AdonisJS makes the argument optional: no argument asks whether ANY event
|
|
566
|
+
// has a listener at all.
|
|
567
|
+
if (event === undefined) return this.listenerCount() > 0
|
|
568
|
+
if (typeof event === 'string') return (this.stringListeners.get(event)?.length ?? 0) > 0
|
|
453
569
|
return (this.classListeners.get(event)?.length ?? 0) > 0
|
|
454
570
|
}
|
|
455
571
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What {@link import('./Emitter.js').Emitter.fake} hands back: the events that
|
|
3
|
+
* were emitted while faking, and the assertions to check them.
|
|
4
|
+
*
|
|
5
|
+
* AdonisJS parity (`@adonisjs/events` `EventsBuffer`). A faked event never
|
|
6
|
+
* reaches its listeners nor the bus — it lands here instead, so a test asserts
|
|
7
|
+
* on what the code decided to announce without running the reaction to it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AssertionError } from 'node:assert'
|
|
11
|
+
|
|
12
|
+
/** An event name: a string, or the class of a class-based event. */
|
|
13
|
+
export type BufferedEventName = string | (new (...args: never[]) => unknown)
|
|
14
|
+
|
|
15
|
+
/** One captured emission. */
|
|
16
|
+
export interface BufferedEvent<Data = unknown> {
|
|
17
|
+
event: BufferedEventName
|
|
18
|
+
data: Data
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Narrows an assertion to one emission among several of the same event. */
|
|
22
|
+
export type EventFinder<Data = unknown> = (event: BufferedEvent<Data>) => boolean
|
|
23
|
+
|
|
24
|
+
export class EventsBuffer {
|
|
25
|
+
readonly #events: BufferedEvent[] = []
|
|
26
|
+
readonly #restore: () => void
|
|
27
|
+
|
|
28
|
+
constructor(restore: () => void) {
|
|
29
|
+
this.#restore = restore
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Record an emission. Called by the emitter while faking. */
|
|
33
|
+
add(event: BufferedEventName, data: unknown): void {
|
|
34
|
+
this.#events.push({ event, data })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Every captured emission, in the order they happened. */
|
|
38
|
+
all(): BufferedEvent[] {
|
|
39
|
+
return [...this.#events]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** How many emissions were captured. */
|
|
43
|
+
size(): number {
|
|
44
|
+
return this.#events.length
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Was this event emitted (optionally, one matching `finder`)? */
|
|
48
|
+
exists<Data = unknown>(event: BufferedEventName, finder?: EventFinder<Data>): boolean {
|
|
49
|
+
return this.find(event, finder) !== null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The first matching emission, or `null`. */
|
|
53
|
+
find<Data = unknown>(
|
|
54
|
+
event: BufferedEventName,
|
|
55
|
+
finder?: EventFinder<Data>,
|
|
56
|
+
): BufferedEvent<Data> | null {
|
|
57
|
+
for (const buffered of this.#events) {
|
|
58
|
+
if (buffered.event !== event) continue
|
|
59
|
+
const typed = buffered as BufferedEvent<Data>
|
|
60
|
+
if (finder === undefined || finder(typed)) return typed
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Fail unless the event was emitted. */
|
|
66
|
+
assertEmitted<Data = unknown>(event: BufferedEventName, finder?: EventFinder<Data>): void {
|
|
67
|
+
if (this.exists(event, finder)) return
|
|
68
|
+
throw new AssertionError({
|
|
69
|
+
message: `Expected "${nameOf(event)}" event to be emitted`,
|
|
70
|
+
operator: 'assertEmitted',
|
|
71
|
+
expected: nameOf(event),
|
|
72
|
+
actual: this.#emittedNames(),
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Fail unless the event was emitted exactly `count` times. */
|
|
77
|
+
assertEmittedCount(event: BufferedEventName, count: number): void {
|
|
78
|
+
const actual = this.#events.filter((buffered) => buffered.event === event).length
|
|
79
|
+
if (actual === count) return
|
|
80
|
+
throw new AssertionError({
|
|
81
|
+
message: `Expected "${nameOf(event)}" event to be emitted "${count}" times, instead it was emitted "${actual}" times`,
|
|
82
|
+
operator: 'assertEmittedCount',
|
|
83
|
+
expected: count,
|
|
84
|
+
actual,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Fail if the event was emitted. */
|
|
89
|
+
assertNotEmitted<Data = unknown>(event: BufferedEventName, finder?: EventFinder<Data>): void {
|
|
90
|
+
if (!this.exists(event, finder)) return
|
|
91
|
+
throw new AssertionError({
|
|
92
|
+
message: `Unexpected "${nameOf(event)}" event was emitted`,
|
|
93
|
+
operator: 'assertNotEmitted',
|
|
94
|
+
expected: [],
|
|
95
|
+
actual: this.#emittedNames(),
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Fail if anything at all was emitted. */
|
|
100
|
+
assertNoneEmitted(): void {
|
|
101
|
+
if (this.#events.length === 0) return
|
|
102
|
+
throw new AssertionError({
|
|
103
|
+
message: `Expected zero events to be emitted, instead received "${this.#events.length}" events`,
|
|
104
|
+
operator: 'assertNoneEmitted',
|
|
105
|
+
expected: 0,
|
|
106
|
+
actual: this.#events.length,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Drop the captured events, keeping the fake in place. */
|
|
111
|
+
flush(): void {
|
|
112
|
+
this.#events.length = 0
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Restore the emitter when the buffer leaves a `using` block, so a test that
|
|
117
|
+
* throws mid-way cannot leave events faked for the next one.
|
|
118
|
+
*/
|
|
119
|
+
[Symbol.dispose](): void {
|
|
120
|
+
this.#restore()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#emittedNames(): string[] {
|
|
124
|
+
return this.#events.map((buffered) => nameOf(buffered.event))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A readable name for an event, whether it is a string or a class. */
|
|
129
|
+
function nameOf(event: BufferedEventName): string {
|
|
130
|
+
return typeof event === 'string' ? event : event.name
|
|
131
|
+
}
|
package/src/events/index.ts
CHANGED
|
@@ -11,12 +11,19 @@
|
|
|
11
11
|
|
|
12
12
|
export type { EventsConfig } from './config.js'
|
|
13
13
|
export { defineConfig } from './config.js'
|
|
14
|
+
export type { AsyncUnsubscribeFunction, UnsubscribeFunction } from './Emitter.js'
|
|
14
15
|
export {
|
|
15
16
|
BaseEvent,
|
|
16
17
|
type ContainerResolver,
|
|
17
18
|
Emitter,
|
|
18
19
|
type ListenerClass,
|
|
19
20
|
} from './Emitter.js'
|
|
21
|
+
export {
|
|
22
|
+
type BufferedEvent,
|
|
23
|
+
type BufferedEventName,
|
|
24
|
+
type EventFinder,
|
|
25
|
+
EventsBuffer,
|
|
26
|
+
} from './EventsBuffer.js'
|
|
20
27
|
export type { EventsAppContext } from './EventsProvider.js'
|
|
21
28
|
export { default as EventsProvider } from './EventsProvider.js'
|
|
22
29
|
export { EventBus } from './native.js'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { durationToSeconds } from '../helpers/duration.js'
|
|
2
|
+
import type { HealthCheckContract, HealthCheckResult } from './types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The base every check extends (AdonisJS `BaseCheck`).
|
|
6
|
+
*/
|
|
7
|
+
export abstract class BaseCheck implements HealthCheckContract {
|
|
8
|
+
abstract name: string
|
|
9
|
+
/** Seconds to reuse a previous result for. */
|
|
10
|
+
cacheDuration?: number
|
|
11
|
+
|
|
12
|
+
/** Rename the check, so two instances of one check stay distinguishable. */
|
|
13
|
+
as(name: string): this {
|
|
14
|
+
this.name = name
|
|
15
|
+
return this
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reuse this check's result for the given duration — `60`, `'1 minute'`,
|
|
20
|
+
* `'30s'`. An expensive check (a remote ping, a disk stat) should not run on
|
|
21
|
+
* every probe.
|
|
22
|
+
*/
|
|
23
|
+
cacheFor(duration: string | number): this {
|
|
24
|
+
this.cacheDuration =
|
|
25
|
+
typeof duration === 'number' ? duration : durationToSeconds(duration, 'a health check cache')
|
|
26
|
+
return this
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
abstract run(): Promise<HealthCheckResult>
|
|
30
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { debuglog } from 'node:util'
|
|
2
|
+
import { healthCheck } from './tracing.js'
|
|
3
|
+
import type { HealthCheckContract, HealthCheckReport } from './types.js'
|
|
4
|
+
|
|
5
|
+
const debug = debuglog('ream:health')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Runs the registered checks and aggregates them into one report
|
|
9
|
+
* (AdonisJS `HealthChecks`).
|
|
10
|
+
*/
|
|
11
|
+
export class HealthChecks {
|
|
12
|
+
#checks: HealthCheckContract[] = []
|
|
13
|
+
#cachedResults = new Map<string, Awaited<ReturnType<HealthCheckContract['run']>>>()
|
|
14
|
+
|
|
15
|
+
/** Replace the registered checks. */
|
|
16
|
+
register(checks: HealthCheckContract[]): this {
|
|
17
|
+
this.#checks = checks
|
|
18
|
+
return this
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Add to the registered checks. */
|
|
22
|
+
append(checks: HealthCheckContract[]): this {
|
|
23
|
+
this.#checks = this.#checks.concat(checks)
|
|
24
|
+
return this
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#getDebugInfo(): HealthCheckReport['debugInfo'] {
|
|
28
|
+
return {
|
|
29
|
+
pid: process.pid,
|
|
30
|
+
ppid: process.ppid,
|
|
31
|
+
platform: process.platform,
|
|
32
|
+
uptime: process.uptime(),
|
|
33
|
+
version: process.version,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async #runCheck(check: HealthCheckContract): Promise<HealthCheckReport['checks'][number]> {
|
|
38
|
+
if (check.cacheDuration) {
|
|
39
|
+
const cached = this.#cachedResults.get(check.name)
|
|
40
|
+
const cacheMilliseconds = Math.floor(check.cacheDuration * 1000)
|
|
41
|
+
if (cached && Date.now() < cached.finishedAt.getTime() + cacheMilliseconds) {
|
|
42
|
+
debug('returning cached results for "%s" check', check.name)
|
|
43
|
+
return { name: check.name, isCached: true, ...cached }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = await healthCheck.tracePromise(
|
|
48
|
+
check.run,
|
|
49
|
+
healthCheck.hasSubscribers ? { check } : undefined,
|
|
50
|
+
check,
|
|
51
|
+
)
|
|
52
|
+
debug('executed "%s" check', check.name)
|
|
53
|
+
if (check.cacheDuration) this.#cachedResults.set(check.name, result)
|
|
54
|
+
return { name: check.name, isCached: false, ...result }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Run every check concurrently and aggregate the verdicts. */
|
|
58
|
+
async run(): Promise<HealthCheckReport> {
|
|
59
|
+
let isHealthy = true
|
|
60
|
+
let status: HealthCheckReport['status'] = 'ok'
|
|
61
|
+
|
|
62
|
+
const checks = await Promise.all(
|
|
63
|
+
this.#checks.map(async (check) => {
|
|
64
|
+
const result = await this.#runCheck(check)
|
|
65
|
+
if (result.status === 'error') {
|
|
66
|
+
status = 'error'
|
|
67
|
+
isHealthy = false
|
|
68
|
+
} else if (status === 'ok' && result.status === 'warning') {
|
|
69
|
+
status = 'warning'
|
|
70
|
+
}
|
|
71
|
+
return result
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
isHealthy,
|
|
77
|
+
status,
|
|
78
|
+
finishedAt: new Date(),
|
|
79
|
+
debugInfo: this.#getDebugInfo(),
|
|
80
|
+
checks,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { HealthCheckResult } from './types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A chainable builder for a check result (AdonisJS `Result`).
|
|
5
|
+
*/
|
|
6
|
+
export class Result implements HealthCheckResult {
|
|
7
|
+
message: string
|
|
8
|
+
status: HealthCheckResult['status']
|
|
9
|
+
finishedAt: Date
|
|
10
|
+
/** Free-form data attached to the result. */
|
|
11
|
+
meta?: HealthCheckResult['meta']
|
|
12
|
+
|
|
13
|
+
constructor(message: string, status: HealthCheckResult['status'], finishedAt: Date) {
|
|
14
|
+
this.message = message
|
|
15
|
+
this.status = status
|
|
16
|
+
this.finishedAt = finishedAt
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A passing result. */
|
|
20
|
+
static ok(message: string): Result {
|
|
21
|
+
return new Result(message, 'ok', new Date())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A failing result. Accepts a message, a message and an error, or an error. */
|
|
25
|
+
static failed(message: string, error?: Error): Result
|
|
26
|
+
static failed(error: Error): Result
|
|
27
|
+
static failed(message: string | Error, error?: Error): Result {
|
|
28
|
+
const result = new Result(
|
|
29
|
+
typeof message === 'string' ? message : message.message,
|
|
30
|
+
'error',
|
|
31
|
+
new Date(),
|
|
32
|
+
)
|
|
33
|
+
if (error) result.setMetaData({ error })
|
|
34
|
+
if (typeof message !== 'string') result.setMetaData({ error: message })
|
|
35
|
+
return result
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A result that is not yet failing but wants attention. */
|
|
39
|
+
static warning(message: string): Result {
|
|
40
|
+
return new Result(message, 'warning', new Date())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Override when the check finished. */
|
|
44
|
+
setFinishedAt(finishedAt: Date): this {
|
|
45
|
+
this.finishedAt = finishedAt
|
|
46
|
+
return this
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Replace the meta-data wholesale. */
|
|
50
|
+
setMetaData(metaData: Record<string, unknown>): this {
|
|
51
|
+
this.meta = metaData
|
|
52
|
+
return this
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Shallow-merge into the existing meta-data. */
|
|
56
|
+
mergeMetaData(metaData: Record<string, unknown>): this {
|
|
57
|
+
this.meta = { ...this.meta, ...metaData }
|
|
58
|
+
return this
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
toJSON(): HealthCheckResult {
|
|
62
|
+
return {
|
|
63
|
+
finishedAt: this.finishedAt,
|
|
64
|
+
message: this.message,
|
|
65
|
+
status: this.status,
|
|
66
|
+
...(this.meta ? { meta: this.meta } : {}),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { statfs } from 'node:fs/promises'
|
|
2
|
+
import { BaseCheck } from '../BaseCheck.js'
|
|
3
|
+
import { Result } from '../Result.js'
|
|
4
|
+
import type { HealthCheckResult } from '../types.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Watches how full the disk is (AdonisJS `DiskSpaceCheck`).
|
|
8
|
+
*
|
|
9
|
+
* NAMED DEVIATION — upstream depends on the `check-disk-space` package, which
|
|
10
|
+
* shells out to `df` on POSIX and `wmic` on Windows. Ream reads Node's own
|
|
11
|
+
* `fs.statfs` instead: no dependency, no subprocess, and the same two numbers.
|
|
12
|
+
* `bavail` rather than `bfree` is deliberate — it is the space actually
|
|
13
|
+
* available to this process, which is what `df` reports and what an operator
|
|
14
|
+
* means by "free".
|
|
15
|
+
*/
|
|
16
|
+
export class DiskSpaceCheck extends BaseCheck {
|
|
17
|
+
#warnThreshold = 75
|
|
18
|
+
#failThreshold = 80
|
|
19
|
+
#computeFn: () => Promise<{ free: number; size: number }> = async () => {
|
|
20
|
+
const stats = await statfs(this.diskPath)
|
|
21
|
+
return { free: Number(stats.bavail) * stats.bsize, size: Number(stats.blocks) * stats.bsize }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
name = 'Disk space check'
|
|
25
|
+
/** The mount point to measure. */
|
|
26
|
+
diskPath = process.platform === 'win32' ? 'C:\\' : '/'
|
|
27
|
+
|
|
28
|
+
/** Warn once this percentage of the disk is used. */
|
|
29
|
+
warnWhenExceeds(valueInPercentage: number): this {
|
|
30
|
+
this.#warnThreshold = valueInPercentage
|
|
31
|
+
return this
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Fail once this percentage of the disk is used. */
|
|
35
|
+
failWhenExceeds(valueInPercentage: number): this {
|
|
36
|
+
this.#failThreshold = valueInPercentage
|
|
37
|
+
return this
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Replace how the reading is taken, so a test can drive the thresholds. */
|
|
41
|
+
compute(callback: () => Promise<{ free: number; size: number }>): this {
|
|
42
|
+
this.#computeFn = callback
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async run(): Promise<HealthCheckResult> {
|
|
47
|
+
const { free, size } = await this.#computeFn()
|
|
48
|
+
const usedPercentage = Math.floor(((size - free) / size) * 100)
|
|
49
|
+
const metaData = {
|
|
50
|
+
sizeInPercentage: {
|
|
51
|
+
used: usedPercentage,
|
|
52
|
+
failureThreshold: this.#failThreshold,
|
|
53
|
+
warningThreshold: this.#warnThreshold,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (usedPercentage >= this.#failThreshold) {
|
|
58
|
+
return Result.failed(
|
|
59
|
+
`Disk usage is ${usedPercentage}%, which is above the threshold of ${this.#failThreshold}%`,
|
|
60
|
+
).mergeMetaData(metaData)
|
|
61
|
+
}
|
|
62
|
+
if (usedPercentage >= this.#warnThreshold) {
|
|
63
|
+
return Result.warning(
|
|
64
|
+
`Disk usage is ${usedPercentage}%, which is above the threshold of ${this.#warnThreshold}%`,
|
|
65
|
+
).mergeMetaData(metaData)
|
|
66
|
+
}
|
|
67
|
+
return Result.ok('Disk usage is under defined thresholds').mergeMetaData(metaData)
|
|
68
|
+
}
|
|
69
|
+
}
|