@c9up/ream 0.1.33 → 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.
Files changed (215) hide show
  1. package/dist/Application.d.ts +68 -0
  2. package/dist/Application.d.ts.map +1 -1
  3. package/dist/Application.js +142 -6
  4. package/dist/Application.js.map +1 -1
  5. package/dist/ConfigLoader.d.ts +4 -0
  6. package/dist/ConfigLoader.d.ts.map +1 -1
  7. package/dist/ConfigLoader.js +0 -8
  8. package/dist/ConfigLoader.js.map +1 -1
  9. package/dist/HealthCheck.d.ts +14 -7
  10. package/dist/HealthCheck.d.ts.map +1 -1
  11. package/dist/HealthCheck.js +21 -12
  12. package/dist/HealthCheck.js.map +1 -1
  13. package/dist/HttpKernel.d.ts +12 -0
  14. package/dist/HttpKernel.d.ts.map +1 -1
  15. package/dist/HttpKernel.js +15 -4
  16. package/dist/HttpKernel.js.map +1 -1
  17. package/dist/Ignitor.d.ts +8 -0
  18. package/dist/Ignitor.d.ts.map +1 -1
  19. package/dist/Ignitor.js +11 -0
  20. package/dist/Ignitor.js.map +1 -1
  21. package/dist/bodyparser/BodyParserMiddleware.d.ts +13 -2
  22. package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
  23. package/dist/bodyparser/BodyParserMiddleware.js +50 -30
  24. package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
  25. package/dist/bodyparser/MultipartFile.d.ts +65 -6
  26. package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
  27. package/dist/bodyparser/MultipartFile.js +100 -7
  28. package/dist/bodyparser/MultipartFile.js.map +1 -1
  29. package/dist/console/BaseCommand.d.ts +8 -1
  30. package/dist/console/BaseCommand.d.ts.map +1 -1
  31. package/dist/console/BaseCommand.js +1 -1
  32. package/dist/console/BaseCommand.js.map +1 -1
  33. package/dist/console/Kernel.d.ts +15 -0
  34. package/dist/console/Kernel.d.ts.map +1 -1
  35. package/dist/console/Kernel.js +17 -0
  36. package/dist/console/Kernel.js.map +1 -1
  37. package/dist/directories.d.ts +52 -0
  38. package/dist/directories.d.ts.map +1 -0
  39. package/dist/directories.js +38 -0
  40. package/dist/directories.js.map +1 -0
  41. package/dist/events/Emitter.d.ts +49 -6
  42. package/dist/events/Emitter.d.ts.map +1 -1
  43. package/dist/events/Emitter.js +117 -7
  44. package/dist/events/Emitter.js.map +1 -1
  45. package/dist/events/EventsBuffer.d.ts +47 -0
  46. package/dist/events/EventsBuffer.d.ts.map +1 -0
  47. package/dist/events/EventsBuffer.js +107 -0
  48. package/dist/events/EventsBuffer.js.map +1 -0
  49. package/dist/events/index.d.ts +2 -0
  50. package/dist/events/index.d.ts.map +1 -1
  51. package/dist/events/index.js +1 -0
  52. package/dist/events/index.js.map +1 -1
  53. package/dist/health/BaseCheck.d.ts +19 -0
  54. package/dist/health/BaseCheck.d.ts.map +1 -0
  55. package/dist/health/BaseCheck.js +24 -0
  56. package/dist/health/BaseCheck.js.map +1 -0
  57. package/dist/health/HealthChecks.d.ts +15 -0
  58. package/dist/health/HealthChecks.d.ts.map +1 -0
  59. package/dist/health/HealthChecks.js +69 -0
  60. package/dist/health/HealthChecks.js.map +1 -0
  61. package/dist/health/Result.d.ts +27 -0
  62. package/dist/health/Result.d.ts.map +1 -0
  63. package/dist/health/Result.js +55 -0
  64. package/dist/health/Result.js.map +1 -0
  65. package/dist/health/checks/DiskSpaceCheck.d.ts +29 -0
  66. package/dist/health/checks/DiskSpaceCheck.d.ts.map +1 -0
  67. package/dist/health/checks/DiskSpaceCheck.js +58 -0
  68. package/dist/health/checks/DiskSpaceCheck.js.map +1 -0
  69. package/dist/health/checks/MemoryHeapCheck.d.ts +21 -0
  70. package/dist/health/checks/MemoryHeapCheck.d.ts.map +1 -0
  71. package/dist/health/checks/MemoryHeapCheck.js +55 -0
  72. package/dist/health/checks/MemoryHeapCheck.js.map +1 -0
  73. package/dist/health/checks/MemoryRSSCheck.d.ts +21 -0
  74. package/dist/health/checks/MemoryRSSCheck.d.ts.map +1 -0
  75. package/dist/health/checks/MemoryRSSCheck.js +55 -0
  76. package/dist/health/checks/MemoryRSSCheck.js.map +1 -0
  77. package/dist/health/checks/memory.d.ts +24 -0
  78. package/dist/health/checks/memory.d.ts.map +1 -0
  79. package/dist/health/checks/memory.js +88 -0
  80. package/dist/health/checks/memory.js.map +1 -0
  81. package/dist/health/index.d.ts +9 -0
  82. package/dist/health/index.d.ts.map +1 -0
  83. package/dist/health/index.js +8 -0
  84. package/dist/health/index.js.map +1 -0
  85. package/dist/health/tracing.d.ts +12 -0
  86. package/dist/health/tracing.d.ts.map +1 -0
  87. package/dist/health/tracing.js +11 -0
  88. package/dist/health/tracing.js.map +1 -0
  89. package/dist/health/types.d.ts +51 -0
  90. package/dist/health/types.d.ts.map +1 -0
  91. package/dist/health/types.js +5 -0
  92. package/dist/health/types.js.map +1 -0
  93. package/dist/helpers/bytes.d.ts +28 -0
  94. package/dist/helpers/bytes.d.ts.map +1 -0
  95. package/dist/helpers/bytes.js +63 -0
  96. package/dist/helpers/bytes.js.map +1 -0
  97. package/dist/helpers/string.d.ts +4 -0
  98. package/dist/helpers/string.d.ts.map +1 -1
  99. package/dist/helpers/string.js +2 -0
  100. package/dist/helpers/string.js.map +1 -1
  101. package/dist/http/HttpContext.d.ts +38 -2
  102. package/dist/http/HttpContext.d.ts.map +1 -1
  103. package/dist/http/HttpContext.js +81 -3
  104. package/dist/http/HttpContext.js.map +1 -1
  105. package/dist/http/Request.d.ts +57 -3
  106. package/dist/http/Request.d.ts.map +1 -1
  107. package/dist/http/Request.js +51 -38
  108. package/dist/http/Request.js.map +1 -1
  109. package/dist/http/Response.d.ts +82 -2
  110. package/dist/http/Response.d.ts.map +1 -1
  111. package/dist/http/Response.js +240 -13
  112. package/dist/http/Response.js.map +1 -1
  113. package/dist/http/SseStream.d.ts +10 -0
  114. package/dist/http/SseStream.d.ts.map +1 -1
  115. package/dist/http/SseStream.js.map +1 -1
  116. package/dist/index.d.ts +7 -2
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +4 -1
  119. package/dist/index.js.map +1 -1
  120. package/dist/migrations/MigrationRegistry.d.ts +28 -0
  121. package/dist/migrations/MigrationRegistry.d.ts.map +1 -0
  122. package/dist/migrations/MigrationRegistry.js +41 -0
  123. package/dist/migrations/MigrationRegistry.js.map +1 -0
  124. package/dist/migrations/types.d.ts +64 -0
  125. package/dist/migrations/types.d.ts.map +1 -0
  126. package/dist/migrations/types.js +15 -0
  127. package/dist/migrations/types.js.map +1 -0
  128. package/dist/router/Router.d.ts +112 -9
  129. package/dist/router/Router.d.ts.map +1 -1
  130. package/dist/router/Router.js +206 -28
  131. package/dist/router/Router.js.map +1 -1
  132. package/dist/security/CookieSigner.d.ts +43 -4
  133. package/dist/security/CookieSigner.d.ts.map +1 -1
  134. package/dist/security/CookieSigner.js +68 -10
  135. package/dist/security/CookieSigner.js.map +1 -1
  136. package/dist/session/Session.d.ts +46 -1
  137. package/dist/session/Session.d.ts.map +1 -1
  138. package/dist/session/Session.js +57 -4
  139. package/dist/session/Session.js.map +1 -1
  140. package/dist/session/SessionMiddleware.d.ts.map +1 -1
  141. package/dist/session/SessionMiddleware.js +19 -9
  142. package/dist/session/SessionMiddleware.js.map +1 -1
  143. package/dist/session/drivers/CookieDriver.d.ts +1 -1
  144. package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
  145. package/dist/session/drivers/CookieDriver.js +3 -3
  146. package/dist/session/drivers/CookieDriver.js.map +1 -1
  147. package/dist/session/drivers/DatabaseDriver.d.ts +1 -1
  148. package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
  149. package/dist/session/drivers/DatabaseDriver.js +2 -2
  150. package/dist/session/drivers/DatabaseDriver.js.map +1 -1
  151. package/dist/session/drivers/FileDriver.d.ts +1 -1
  152. package/dist/session/drivers/FileDriver.d.ts.map +1 -1
  153. package/dist/session/drivers/FileDriver.js +7 -5
  154. package/dist/session/drivers/FileDriver.js.map +1 -1
  155. package/dist/session/drivers/MemoryDriver.d.ts +1 -1
  156. package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
  157. package/dist/session/drivers/MemoryDriver.js +1 -1
  158. package/dist/session/drivers/MemoryDriver.js.map +1 -1
  159. package/dist/session/drivers/RedisDriver.d.ts +1 -1
  160. package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
  161. package/dist/session/drivers/RedisDriver.js +11 -4
  162. package/dist/session/drivers/RedisDriver.js.map +1 -1
  163. package/events.win32-x64-msvc.node +0 -0
  164. package/index.darwin-arm64.node +0 -0
  165. package/index.darwin-x64.node +0 -0
  166. package/index.linux-arm64-gnu.node +0 -0
  167. package/index.linux-x64-gnu.node +0 -0
  168. package/index.win32-x64-msvc.node +0 -0
  169. package/package.json +5 -2
  170. package/scheduler.win32-x64-msvc.node +0 -0
  171. package/src/Application.ts +169 -6
  172. package/src/ConfigLoader.ts +6 -0
  173. package/src/HealthCheck.ts +27 -14
  174. package/src/HttpKernel.ts +27 -4
  175. package/src/Ignitor.ts +17 -0
  176. package/src/bodyparser/BodyParserMiddleware.ts +70 -36
  177. package/src/bodyparser/MultipartFile.ts +125 -8
  178. package/src/console/BaseCommand.ts +8 -1
  179. package/src/console/Kernel.ts +18 -0
  180. package/src/directories.ts +81 -0
  181. package/src/events/Emitter.ts +129 -13
  182. package/src/events/EventsBuffer.ts +131 -0
  183. package/src/events/index.ts +7 -0
  184. package/src/health/BaseCheck.ts +30 -0
  185. package/src/health/HealthChecks.ts +83 -0
  186. package/src/health/Result.ts +69 -0
  187. package/src/health/checks/DiskSpaceCheck.ts +69 -0
  188. package/src/health/checks/MemoryHeapCheck.ts +68 -0
  189. package/src/health/checks/MemoryRSSCheck.ts +68 -0
  190. package/src/health/checks/memory.ts +115 -0
  191. package/src/health/index.ts +13 -0
  192. package/src/health/tracing.ts +15 -0
  193. package/src/health/types.ts +51 -0
  194. package/src/helpers/bytes.ts +71 -0
  195. package/src/helpers/string.ts +2 -0
  196. package/src/http/HttpContext.ts +86 -4
  197. package/src/http/Request.ts +82 -10
  198. package/src/http/Response.ts +283 -14
  199. package/src/http/SseStream.ts +10 -0
  200. package/src/index.ts +27 -1
  201. package/src/migrations/MigrationRegistry.ts +49 -0
  202. package/src/migrations/types.ts +68 -0
  203. package/src/router/Router.ts +241 -32
  204. package/src/security/CookieSigner.ts +92 -18
  205. package/src/session/Session.ts +71 -5
  206. package/src/session/SessionMiddleware.ts +19 -9
  207. package/src/session/drivers/CookieDriver.ts +4 -4
  208. package/src/session/drivers/DatabaseDriver.ts +3 -3
  209. package/src/session/drivers/FileDriver.ts +8 -6
  210. package/src/session/drivers/MemoryDriver.ts +2 -2
  211. package/src/session/drivers/RedisDriver.ts +12 -5
  212. package/tsconfig.app.json +13 -13
  213. package/tsconfig.base.json +19 -0
  214. package/tsconfig.client.json +16 -0
  215. package/tsconfig.package.json +15 -0
@@ -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
- if (condition) this.on(event, listener)
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<number>
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<number>
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<number> {
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
- return this.#subscribeAny(pattern, listener)
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
- /** Unsubscribe a wildcard subscription by ID. */
367
- async offAny(subscriptionId: number): Promise<void> {
368
- await this.bus.unsubscribe(subscriptionId)
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: EventConstructor | string): boolean {
450
- if (typeof event === 'string') {
451
- return (this.stringListeners.get(event)?.length ?? 0) > 0
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
+ }
@@ -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
+ }