@c9up/ream 0.2.14 → 0.2.15

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 (166) hide show
  1. package/README.md +1 -0
  2. package/dist/Application.d.ts.map +1 -1
  3. package/dist/Application.js +6 -1
  4. package/dist/Application.js.map +1 -1
  5. package/dist/Provider.d.ts +20 -2
  6. package/dist/Provider.d.ts.map +1 -1
  7. package/dist/Provider.js.map +1 -1
  8. package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
  9. package/dist/bodyparser/MultipartFile.js +7 -3
  10. package/dist/bodyparser/MultipartFile.js.map +1 -1
  11. package/dist/bodyparser/parseSize.d.ts.map +1 -1
  12. package/dist/bodyparser/parseSize.js +3 -2
  13. package/dist/bodyparser/parseSize.js.map +1 -1
  14. package/dist/commands/MigrateBase.d.ts.map +1 -1
  15. package/dist/commands/MigrateBase.js +4 -1
  16. package/dist/commands/MigrateBase.js.map +1 -1
  17. package/dist/console/Kernel.d.ts.map +1 -1
  18. package/dist/console/Kernel.js +7 -0
  19. package/dist/console/Kernel.js.map +1 -1
  20. package/dist/console/types.d.ts +14 -0
  21. package/dist/console/types.d.ts.map +1 -1
  22. package/dist/console/types.js.map +1 -1
  23. package/dist/container/Container.d.ts.map +1 -1
  24. package/dist/container/Container.js +6 -1
  25. package/dist/container/Container.js.map +1 -1
  26. package/dist/container/types.d.ts +11 -1
  27. package/dist/container/types.d.ts.map +1 -1
  28. package/dist/decorators/Service.d.ts +1 -1
  29. package/dist/decorators/Service.d.ts.map +1 -1
  30. package/dist/env/Env.d.ts.map +1 -1
  31. package/dist/env/Env.js +4 -2
  32. package/dist/env/Env.js.map +1 -1
  33. package/dist/env/interpolate.d.ts.map +1 -1
  34. package/dist/env/interpolate.js +5 -3
  35. package/dist/env/interpolate.js.map +1 -1
  36. package/dist/errors/FuzzyMatcher.d.ts.map +1 -1
  37. package/dist/errors/FuzzyMatcher.js +30 -13
  38. package/dist/errors/FuzzyMatcher.js.map +1 -1
  39. package/dist/errors/PipelineStageError.d.ts.map +1 -1
  40. package/dist/errors/PipelineStageError.js +4 -2
  41. package/dist/errors/PipelineStageError.js.map +1 -1
  42. package/dist/events/Emitter.d.ts +21 -4
  43. package/dist/events/Emitter.d.ts.map +1 -1
  44. package/dist/events/Emitter.js +6 -2
  45. package/dist/events/Emitter.js.map +1 -1
  46. package/dist/events/testing/FakeBus.d.ts +1 -1
  47. package/dist/events/testing/FakeBus.d.ts.map +1 -1
  48. package/dist/events/testing/FakeBus.js.map +1 -1
  49. package/dist/graphql/GraphQLEngine.d.ts +2 -2
  50. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  51. package/dist/graphql/GraphQLEngine.js +4 -2
  52. package/dist/graphql/GraphQLEngine.js.map +1 -1
  53. package/dist/helpers/bytes.d.ts.map +1 -1
  54. package/dist/helpers/bytes.js +13 -2
  55. package/dist/helpers/bytes.js.map +1 -1
  56. package/dist/helpers/string.js +3 -3
  57. package/dist/helpers/string.js.map +1 -1
  58. package/dist/http/Exception.d.ts +8 -4
  59. package/dist/http/Exception.d.ts.map +1 -1
  60. package/dist/http/Exception.js +14 -8
  61. package/dist/http/Exception.js.map +1 -1
  62. package/dist/http/Request.d.ts.map +1 -1
  63. package/dist/http/Request.js +10 -7
  64. package/dist/http/Request.js.map +1 -1
  65. package/dist/http/mime.d.ts.map +1 -1
  66. package/dist/http/mime.js +2 -1
  67. package/dist/http/mime.js.map +1 -1
  68. package/dist/index.d.ts +2 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +1 -1
  71. package/dist/native/generated.d.ts +12 -8
  72. package/dist/native/generated.d.ts.map +1 -1
  73. package/dist/openapi/OpenApiGenerator.d.ts.map +1 -1
  74. package/dist/openapi/OpenApiGenerator.js +6 -4
  75. package/dist/openapi/OpenApiGenerator.js.map +1 -1
  76. package/dist/router/Router.d.ts +10 -1
  77. package/dist/router/Router.d.ts.map +1 -1
  78. package/dist/router/Router.js +22 -10
  79. package/dist/router/Router.js.map +1 -1
  80. package/dist/rpc/RpcRouter.d.ts +16 -3
  81. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  82. package/dist/rpc/RpcRouter.js +34 -3
  83. package/dist/rpc/RpcRouter.js.map +1 -1
  84. package/dist/scheduler/Schedule.d.ts +6 -1
  85. package/dist/scheduler/Schedule.d.ts.map +1 -1
  86. package/dist/scheduler/Schedule.js +5 -0
  87. package/dist/scheduler/Schedule.js.map +1 -1
  88. package/dist/security/CookieSigner.d.ts.map +1 -1
  89. package/dist/security/CookieSigner.js +12 -5
  90. package/dist/security/CookieSigner.js.map +1 -1
  91. package/dist/security/SignedUrl.js +3 -2
  92. package/dist/security/SignedUrl.js.map +1 -1
  93. package/dist/session/drivers/DatabaseDriver.d.ts +13 -1
  94. package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
  95. package/dist/session/drivers/DatabaseDriver.js +23 -3
  96. package/dist/session/drivers/DatabaseDriver.js.map +1 -1
  97. package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
  98. package/dist/session/drivers/RedisDriver.js +10 -2
  99. package/dist/session/drivers/RedisDriver.js.map +1 -1
  100. package/dist/testing/ApiResponse.d.ts.map +1 -1
  101. package/dist/testing/ApiResponse.js +10 -11
  102. package/dist/testing/ApiResponse.js.map +1 -1
  103. package/dist/testing/TestClient.js +6 -6
  104. package/dist/testing/TestClient.js.map +1 -1
  105. package/dist/utils/Macroable.d.ts +6 -2
  106. package/dist/utils/Macroable.d.ts.map +1 -1
  107. package/dist/utils/Macroable.js +4 -0
  108. package/dist/utils/Macroable.js.map +1 -1
  109. package/dist/utils/objectPath.d.ts.map +1 -1
  110. package/dist/utils/objectPath.js +14 -6
  111. package/dist/utils/objectPath.js.map +1 -1
  112. package/events.darwin-arm64.node +0 -0
  113. package/events.darwin-x64.node +0 -0
  114. package/events.linux-arm64-gnu.node +0 -0
  115. package/events.linux-x64-gnu.node +0 -0
  116. package/events.win32-x64-msvc.node +0 -0
  117. package/index.darwin-arm64.node +0 -0
  118. package/index.darwin-x64.node +0 -0
  119. package/index.linux-arm64-gnu.node +0 -0
  120. package/index.linux-x64-gnu.node +0 -0
  121. package/index.win32-x64-msvc.node +0 -0
  122. package/package.json +3 -3
  123. package/scheduler.darwin-arm64.node +0 -0
  124. package/scheduler.darwin-x64.node +0 -0
  125. package/scheduler.linux-arm64-gnu.node +0 -0
  126. package/scheduler.linux-x64-gnu.node +0 -0
  127. package/scheduler.win32-x64-msvc.node +0 -0
  128. package/scripts/build-napi-types.mjs +4 -3
  129. package/scripts/generate-napi-types.mjs +28 -12
  130. package/src/Application.ts +6 -1
  131. package/src/Provider.ts +21 -2
  132. package/src/bodyparser/MultipartFile.ts +7 -3
  133. package/src/bodyparser/parseSize.ts +3 -2
  134. package/src/commands/MigrateBase.ts +4 -1
  135. package/src/console/BaseCommand.ts +1 -1
  136. package/src/console/Kernel.ts +8 -0
  137. package/src/console/types.ts +15 -0
  138. package/src/container/Container.ts +15 -6
  139. package/src/container/types.ts +11 -1
  140. package/src/decorators/Service.ts +1 -1
  141. package/src/env/Env.ts +4 -2
  142. package/src/env/interpolate.ts +5 -3
  143. package/src/errors/FuzzyMatcher.ts +34 -13
  144. package/src/errors/PipelineStageError.ts +4 -2
  145. package/src/events/Emitter.ts +34 -8
  146. package/src/events/testing/FakeBus.ts +12 -5
  147. package/src/graphql/GraphQLEngine.ts +11 -7
  148. package/src/helpers/bytes.ts +13 -2
  149. package/src/helpers/string.ts +3 -3
  150. package/src/http/Exception.ts +14 -8
  151. package/src/http/Request.ts +8 -5
  152. package/src/http/mime.ts +2 -1
  153. package/src/index.ts +2 -2
  154. package/src/native/generated.ts +12 -8
  155. package/src/openapi/OpenApiGenerator.ts +6 -4
  156. package/src/router/Router.ts +31 -11
  157. package/src/rpc/RpcRouter.ts +38 -8
  158. package/src/scheduler/Schedule.ts +8 -1
  159. package/src/security/CookieSigner.ts +15 -5
  160. package/src/security/SignedUrl.ts +3 -2
  161. package/src/session/drivers/DatabaseDriver.ts +36 -8
  162. package/src/session/drivers/RedisDriver.ts +13 -5
  163. package/src/testing/ApiResponse.ts +9 -10
  164. package/src/testing/TestClient.ts +6 -6
  165. package/src/utils/Macroable.ts +6 -2
  166. package/src/utils/objectPath.ts +12 -6
@@ -6,8 +6,9 @@
6
6
  export function parseSize(size: string): number {
7
7
  const match = size.match(/^(\d+)(kb|mb|gb)?$/i)
8
8
  if (!match) return 1024 * 1024 // default 1mb
9
- const num = parseInt(match[1], 10)
10
- switch (match[2]?.toLowerCase()) {
9
+ const [, digits, unit] = match
10
+ const num = parseInt(digits ?? '0', 10)
11
+ switch (unit?.toLowerCase()) {
11
12
  case 'kb':
12
13
  return num * 1024
13
14
  case 'mb':
@@ -19,7 +19,10 @@ import type { MigrationRegistry } from '../migrations/MigrationRegistry.js'
19
19
  import type { RegisteredMigrationSource } from '../migrations/types.js'
20
20
 
21
21
  export abstract class MigrateBase extends BaseCommand {
22
- static override options: CommandOptions = { startApp: true }
22
+ // `drivesMigrations` because these three ARE the migration run: without it the
23
+ // boot applies everything first, so `migrate` says "nothing to migrate" after
24
+ // the work is done, and `migrate:status` mutates the schema it reports on.
25
+ static override options: CommandOptions = { startApp: true, drivesMigrations: true }
23
26
 
24
27
  @flags.string({
25
28
  description: 'Only this migration source, by the name its package registered',
@@ -117,7 +117,7 @@ export abstract class BaseCommand {
117
117
  error?: unknown
118
118
 
119
119
  /** Assigned by the kernel — see {@link terminate}. */
120
- protected declare onTerminate?: () => void | Promise<void>
120
+ declare protected onTerminate?: () => void | Promise<void>
121
121
 
122
122
  /**
123
123
  * Lifecycle hooks, run in this order around `run()`: `prepare`, `interact`,
@@ -999,6 +999,14 @@ export class Kernel {
999
999
  )
1000
1000
  }
1001
1001
 
1002
+ // Before the boot, not after: a data package that migrates on boot for the
1003
+ // convenience of `dev` would otherwise apply everything a moment before the
1004
+ // command that exists to apply it runs — leaving `migrate` with nothing to
1005
+ // report and `migrate:status` having changed what it was asked to look at.
1006
+ if (Command.options?.drivesMigrations === true) {
1007
+ process.env.REAM_SKIP_BOOT_MIGRATE = '1'
1008
+ }
1009
+
1002
1010
  return this.#startApp()
1003
1011
  }
1004
1012
 
@@ -37,6 +37,21 @@ export interface CommandOptions {
37
37
  * default so a typo (`--warm` for `--warn`) is reported, not swallowed.
38
38
  */
39
39
  allowUnknownFlags?: boolean
40
+
41
+ /**
42
+ * This command drives migrations itself, so booting must not run them.
43
+ *
44
+ * A data package may migrate on boot for the convenience of `dev` — atlas
45
+ * does, outside production. For the commands that exist to migrate, that
46
+ * convenience is the bug: the boot applies everything, and then `migrate`
47
+ * reports "nothing to migrate" while `migrate:status`, asked only to look,
48
+ * has changed the schema it is reporting on.
49
+ *
50
+ * Declaring this sets `REAM_SKIP_BOOT_MIGRATE=1` before the application
51
+ * starts, which is the flag a data package watches for. Any package's own
52
+ * migration command opts in the same way; the framework names none of them.
53
+ */
54
+ drivesMigrations?: boolean
40
55
  }
41
56
 
42
57
  /** Metadata recorded by an `@args.*` decorator. */
@@ -63,7 +63,7 @@ interface ResolutionChain {
63
63
  * down. Saved and restored around each construction, so a dependency's own
64
64
  * dependencies see IT as their parent, not its parent.
65
65
  */
66
- parent?: new (
66
+ parent?: abstract new (
67
67
  ...args: never[]
68
68
  ) => unknown
69
69
  }
@@ -91,8 +91,12 @@ export class Container {
91
91
  /** Alias key → the token it forwards to (AdonisJS `container.alias`). */
92
92
  #aliases: Map<ServiceToken, ServiceToken> = new Map()
93
93
  /** parent class → (binding key → the factory that class gets instead). */
94
- #contextualBindings: Map<new (...args: never[]) => unknown, Map<ServiceToken, ServiceFactory>> =
95
- new Map()
94
+ #contextualBindings: Map<
95
+ abstract new (
96
+ ...args: never[]
97
+ ) => unknown,
98
+ Map<ServiceToken, ServiceFactory>
99
+ > = new Map()
96
100
  /** token key → post-resolution callbacks (AdonisJS `container.resolving`). */
97
101
  #resolvingHooks: Map<ServiceToken, Array<(value: unknown) => void | Promise<void>>> = new Map()
98
102
  /**
@@ -625,7 +629,7 @@ export class Container {
625
629
  * 3. No params → plain `new Class()`
626
630
  */
627
631
  async #autoConstruct(
628
- target: new (...args: never[]) => unknown,
632
+ target: abstract new (...args: never[]) => unknown,
629
633
  runtimeValues?: unknown[],
630
634
  ): Promise<unknown> {
631
635
  // Everything resolved inside is a dependency OF `target`, which is what a
@@ -642,7 +646,7 @@ export class Container {
642
646
  }
643
647
 
644
648
  async #constructWithDeps(
645
- target: new (...args: never[]) => unknown,
649
+ target: abstract new (...args: never[]) => unknown,
646
650
  runtimeValues?: unknown[],
647
651
  ): Promise<unknown> {
648
652
  const metadata = getServiceMetadata(target)
@@ -719,7 +723,12 @@ export class Container {
719
723
  )
720
724
  }
721
725
  // (d) Genuine zero-argument class.
722
- const instance = new target()
726
+ //
727
+ // `Reflect.construct`, not `new target()`: a token may be an abstract
728
+ // class (that is what a contextual binding binds against), and `new` on
729
+ // one is a compile error even though the runtime constructs it happily —
730
+ // `abstract` exists only at compile time.
731
+ const instance = Reflect.construct(target, [])
723
732
  this.#cacheIfAppWide(scope, key, instance, store, readsBefore)
724
733
  return instance
725
734
  }
@@ -16,7 +16,17 @@ export interface ServiceMetadata {
16
16
  /** Token used to resolve a service — class constructor, string name, or registered symbol. */
17
17
  // `never[]` accepts every constructor shape — parameters are contravariant, so
18
18
  // a rest of `never` is assignable from any concrete list.
19
- export type ServiceToken = (new (...args: never[]) => unknown) | string | symbol
19
+ /**
20
+ * What the container accepts as a token.
21
+ *
22
+ * `abstract new`, not `new`: binding an implementation against an abstract base
23
+ * is the whole point of a contextual binding — `@Inject(Hash)` where `Hash` is
24
+ * the abstract class and `Bcrypt` is what the container provides. An abstract
25
+ * class has no construct signature, so spelt `new (...)` the type rejected
26
+ * exactly the pattern the feature exists for. `abstract new` admits concrete
27
+ * classes too.
28
+ */
29
+ export type ServiceToken = (abstract new (...args: never[]) => unknown) | string | symbol
20
30
 
21
31
  /**
22
32
  * What a binding factory is handed so it can resolve its own dependencies
@@ -14,7 +14,7 @@ const INJECT_METADATA_KEY = Symbol('ream:inject')
14
14
  // `never[]` accepts every constructor shape: parameters are contravariant, so a
15
15
  // rest of `never` is assignable from any concrete list. Same spelling as the
16
16
  // router's, so a class flows between the two without an assertion.
17
- type AnyConstructor = new (...args: never[]) => unknown
17
+ type AnyConstructor = abstract new (...args: never[]) => unknown
18
18
 
19
19
  /** Registry of all decorated services. */
20
20
  const serviceRegistry: Map<AnyConstructor, ServiceMetadata> = new Map()
package/src/env/Env.ts CHANGED
@@ -108,9 +108,11 @@ export class Env<Values extends Record<string, unknown>> {
108
108
  validate(values: Record<string, string | undefined>): EnvRecord<S> {
109
109
  const failures: string[] = []
110
110
  const collected: Record<string, unknown> = {}
111
- for (const key of Object.keys(schema)) {
111
+ // `entries` rather than `keys` plus a lookup: the validator comes back
112
+ // already known to be there instead of read again as maybe-missing.
113
+ for (const [key, node] of Object.entries(schema)) {
112
114
  try {
113
- collected[key] = schema[key].validate(key, values[key])
115
+ collected[key] = node.validate(key, values[key])
114
116
  } catch (err) {
115
117
  failures.push(err instanceof Error ? err.message : String(err))
116
118
  }
@@ -46,9 +46,11 @@ export function interpolate(value: string, lookup: (name: string) => string | un
46
46
  })
47
47
 
48
48
  const prefixed = /^([A-Za-z_][A-Za-z0-9_]*):([\s\S]*)$/.exec(substituted)
49
- if (prefixed) {
50
- const resolver = identifiers.get(prefixed[1])
51
- if (resolver) return resolver(prefixed[2])
49
+ // Both groups are required by the pattern, so a match carries both.
50
+ const [, identifier, rest] = prefixed ?? []
51
+ if (identifier !== undefined && rest !== undefined) {
52
+ const resolver = identifiers.get(identifier)
53
+ if (resolver) return resolver(rest)
52
54
  }
53
55
  return substituted
54
56
  }
@@ -14,23 +14,43 @@ export function levenshtein(a: string, b: string): number {
14
14
  if (m === 0) return n
15
15
  if (n === 0) return m
16
16
 
17
- const dp: number[][] = Array.from({ length: m + 1 }, () => Array<number>(n + 1).fill(0))
18
-
19
- for (let i = 0; i <= m; i++) dp[i][0] = i
20
- for (let j = 0; j <= n; j++) dp[0][j] = j
17
+ // Two rows rather than the whole matrix: the recurrence only ever reads the
18
+ // row above and the cell to the left, so the rest was allocated and never
19
+ // looked at again.
20
+ let previous: number[] = Array.from({ length: n + 1 }, (_, j) => j)
21
21
 
22
22
  for (let i = 1; i <= m; i++) {
23
+ const current: number[] = [i]
23
24
  for (let j = 1; j <= n; j++) {
24
25
  const cost = a[i - 1] === b[j - 1] ? 0 : 1
25
- dp[i][j] = Math.min(
26
- dp[i - 1][j] + 1, // deletion
27
- dp[i][j - 1] + 1, // insertion
28
- dp[i - 1][j - 1] + cost, // substitution
26
+ current.push(
27
+ Math.min(
28
+ cell(previous, j) + 1, // deletion
29
+ cell(current, j - 1) + 1, // insertion
30
+ cell(previous, j - 1) + cost, // substitution
31
+ ),
29
32
  )
30
33
  }
34
+ previous = current
31
35
  }
32
36
 
33
- return dp[m][n]
37
+ return cell(previous, n)
38
+ }
39
+
40
+ /**
41
+ * One cell of a row that has already been filled.
42
+ *
43
+ * Each row is built left to right and every column is written before it is
44
+ * read, so a miss cannot happen — and this is where that invariant is stated
45
+ * rather than asserted past it. Reading the row directly would have every
46
+ * access typed "a number, or nothing", which is not what the algorithm knows.
47
+ */
48
+ function cell(row: number[], index: number): number {
49
+ const value = row[index]
50
+ if (value === undefined) {
51
+ throw new RangeError(`levenshtein: column ${index} was read before it was written`)
52
+ }
53
+ return value
34
54
  }
35
55
 
36
56
  /**
@@ -81,12 +101,13 @@ export function findClosestMatches(
81
101
  * @returns Formatted suggestion string, or empty string if no matches
82
102
  */
83
103
  export function didYouMean(input: string, candidates: string[]): string {
84
- const matches = findClosestMatches(input, candidates)
85
- if (matches.length === 0) return ''
104
+ const [best, ...rest] = findClosestMatches(input, candidates)
105
+ if (best === undefined) return ''
86
106
 
87
- if (matches.length === 1) {
88
- return `Did you mean '${matches[0].candidate}'?`
107
+ if (rest.length === 0) {
108
+ return `Did you mean '${best.candidate}'?`
89
109
  }
110
+ const matches = [best, ...rest]
90
111
 
91
112
  const suggestions = matches.map((m) => `'${m.candidate}'`).join(', ')
92
113
  return `Did you mean one of: ${suggestions}?`
@@ -42,9 +42,11 @@ export function createPipelineError(
42
42
  ): ReamError {
43
43
  const total = PIPELINE_STAGES.length
44
44
  const clampedIndex = Math.max(0, Math.min(stageIndex, total - 1))
45
- const stageName = PIPELINE_STAGES[clampedIndex]
45
+ // The index is clamped into the list just above, so the lookup lands — the
46
+ // fallback is what says so without asserting past the read.
47
+ const stageName = PIPELINE_STAGES[clampedIndex] ?? '(unknown)'
46
48
  const position = `${clampedIndex + 1}/${total}`
47
- const nextStage = clampedIndex + 1 < total ? PIPELINE_STAGES[clampedIndex + 1] : '(end)'
49
+ const nextStage = PIPELINE_STAGES[clampedIndex + 1] ?? '(end)'
48
50
 
49
51
  const errorCode = originalError instanceof ReamError ? originalError.code : 'E_PIPELINE_ERROR'
50
52
 
@@ -23,14 +23,25 @@ const correlationStorage = new AsyncLocalStorage<string | undefined>()
23
23
  * Listener class interface — must have a handle() method.
24
24
  */
25
25
  export interface ListenerClass<T = unknown> {
26
- handle(event: T): Promise<void> | void
26
+ handle(event: T): unknown
27
27
  }
28
28
 
29
29
  type EventConstructor<T = unknown> = new (...args: never[]) => T
30
30
 
31
- type ListenerFn<T = unknown> = (event: T) => Promise<void> | void
31
+ /**
32
+ * A listener. Whatever it returns is awaited and dropped.
33
+ *
34
+ * `unknown`, not `Promise<void> | void`: a bare `void` return type accepts a
35
+ * function returning anything, but a UNION containing it does not — so
36
+ * `emitter.on('x', (d) => received.push(d))` was a type error for returning
37
+ * what `push` returns, which nothing reads.
38
+ */
39
+ type ListenerFn<T = unknown> = (event: T) => unknown
32
40
 
33
- type ListenerConstructor<T = unknown> = new (...args: never[]) => ListenerClass<T>
41
+ // `abstract new`: a listener bound against an abstract base is as valid a
42
+ // registration as one bound against a concrete class, and the container builds
43
+ // either.
44
+ type ListenerConstructor<T = unknown> = abstract new (...args: never[]) => ListenerClass<T>
34
45
 
35
46
  type Listener<T = unknown> = ListenerFn<T> | ListenerConstructor<T>
36
47
 
@@ -41,7 +52,16 @@ type Listener<T = unknown> = ListenerFn<T> | ListenerConstructor<T>
41
52
  * satisfies it.
42
53
  */
43
54
  export interface EmitterResolver {
44
- make<T>(target: new (...args: never[]) => T): Promise<T>
55
+ /**
56
+ * Build a listener.
57
+ *
58
+ * Not generic: a generic method must work for every `T`, so nothing but a
59
+ * real container could implement it — the fake in the tests could not, and
60
+ * this file's own call site reached for `as ListenerConstructor<T>` to get
61
+ * past it. What comes back is checked below, which is what a container's
62
+ * answer deserves anyway.
63
+ */
64
+ make(target: abstract new (...args: never[]) => object): Promise<object>
45
65
  }
46
66
 
47
67
  /**
@@ -327,9 +347,15 @@ export class Emitter {
327
347
  if (isListenerClass(listener)) {
328
348
  // Listener class — resolve via container for @inject() support
329
349
  const instance = this.#resolver
330
- ? await this.#resolver.make(listener as ListenerConstructor<T>)
331
- : new (listener as ListenerConstructor<T>)()
332
- await instance.handle(event)
350
+ ? await this.#resolver.make(listener)
351
+ : Reflect.construct(listener, [])
352
+ const handle = Reflect.get(instance, 'handle')
353
+ if (typeof handle !== 'function') {
354
+ throw new Error(
355
+ `Listener ${String(Reflect.get(listener, 'name'))} resolved to something with no handle()`,
356
+ )
357
+ }
358
+ await handle.call(instance, event)
333
359
  } else {
334
360
  await (listener as ListenerFn<T>)(event)
335
361
  }
@@ -651,7 +677,7 @@ function classToEventName(cls: EventConstructor): string {
651
677
  }
652
678
 
653
679
  /** Check if a listener entry is a class (has prototype.handle) vs a function. */
654
- function isListenerClass(listener: Listener): boolean {
680
+ function isListenerClass(listener: Listener): listener is ListenerConstructor {
655
681
  return typeof listener === 'function' && typeof listener.prototype?.handle === 'function'
656
682
  }
657
683
 
@@ -41,10 +41,17 @@ export type FakeBusPredicateArg = FakeBusPredicate | ((event: CapturedEmit) => b
41
41
 
42
42
  interface Subscription {
43
43
  pattern: string
44
- /** Subscribers may return `void` (sync) or `PromiseLike<void>` (async).
45
- * Async rejections are logged via `console.error` to keep them from
46
- * escaping to `unhandledRejection`. */
47
- callback: (eventJson: string) => void | PromiseLike<void>
44
+ /**
45
+ * Subscribers may be sync or async; whatever they return is awaited and
46
+ * dropped, and an async rejection is logged via `console.error` rather than
47
+ * escaping to `unhandledRejection`.
48
+ *
49
+ * `unknown`, not `void | PromiseLike<void>`: a bare `void` return type
50
+ * accepts a function returning anything, but a UNION containing it does not
51
+ * — so widening the native bus's `=> void` to that union made it NARROWER in
52
+ * practice, and `(json) => received.push(json)` stopped compiling.
53
+ */
54
+ callback: (eventJson: string) => unknown
48
55
  }
49
56
 
50
57
  /** Request handler may return `void` (sync, fire-and-forget after `reply`)
@@ -106,7 +113,7 @@ export class FakeBus {
106
113
  return Promise.resolve('ok')
107
114
  }
108
115
 
109
- subscribe(pattern: string, callback: (eventJson: string) => void | PromiseLike<void>): number {
116
+ subscribe(pattern: string, callback: (eventJson: string) => unknown): number {
110
117
  const id = this.#nextSubId++
111
118
  this.#subscribers.set(id, { pattern, callback })
112
119
  return id
@@ -182,7 +182,7 @@ export interface ResolverOptions {
182
182
  interface ResolverEntry {
183
183
  typeName: string
184
184
  fieldName: string
185
- handlerClass: new (...args: unknown[]) => Record<string, (...args: unknown[]) => unknown>
185
+ handlerClass: abstract new (...args: never[]) => object
186
186
  methodName: string
187
187
  options: ResolverOptions
188
188
  }
@@ -204,7 +204,7 @@ export interface SelectionField {
204
204
  export class GraphQLEngine {
205
205
  #schemaSource: string
206
206
  #resolvers: Map<string, ResolverEntry> = new Map()
207
- #container?: { make<T>(target: new (...args: unknown[]) => T): Promise<T> }
207
+ #container?: { make<T>(target: abstract new (...args: never[]) => T): Promise<T> }
208
208
  readonly path: string
209
209
  #playground: boolean
210
210
  #maxQueryBytes: number
@@ -239,7 +239,9 @@ export class GraphQLEngine {
239
239
  }
240
240
 
241
241
  /** Set IoC container for resolver instantiation. */
242
- useContainer(container: { make<T>(target: new (...args: unknown[]) => T): Promise<T> }): void {
242
+ useContainer(container: {
243
+ make<T>(target: abstract new (...args: never[]) => T): Promise<T>
244
+ }): void {
243
245
  this.#container = container
244
246
  }
245
247
 
@@ -247,7 +249,7 @@ export class GraphQLEngine {
247
249
  resolver(
248
250
  typeName: string,
249
251
  fieldName: string,
250
- handlerClass: new (...args: unknown[]) => Record<string, (...args: unknown[]) => unknown>,
252
+ handlerClass: abstract new (...args: never[]) => object,
251
253
  methodName: string,
252
254
  options?: ResolverOptions,
253
255
  ): void {
@@ -369,11 +371,13 @@ export class GraphQLEngine {
369
371
 
370
372
  try {
371
373
  // Resolve handler via IoC or direct instantiation.
372
- const instance = this.#container
374
+ // `Reflect.construct`, not `new`: the resolver may be an abstract class,
375
+ // which the runtime builds happily and `new` refuses at compile time.
376
+ const instance: object = this.#container
373
377
  ? await this.#container.make(entry.handlerClass)
374
- : new entry.handlerClass()
378
+ : Reflect.construct(entry.handlerClass, [])
375
379
 
376
- const handler = instance[entry.methodName]
380
+ const handler = Reflect.get(instance, entry.methodName)
377
381
  if (typeof handler !== 'function') {
378
382
  return {
379
383
  ok: false,
@@ -18,6 +18,11 @@ const UNITS = {
18
18
 
19
19
  type Unit = keyof typeof UNITS
20
20
 
21
+ /** A suffix the table knows. Checked, so nothing has to be asserted. */
22
+ function isUnit(value: string): value is Unit {
23
+ return value in UNITS
24
+ }
25
+
21
26
  const PARSE = /^((?:-|\+)?(?:\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i
22
27
  /** Trailing `.00`, or the zeros trailing a real decimal. */
23
28
  const TRAILING_ZEROS = /(?:\.0*|(\.[^0]+)0+)$/
@@ -33,10 +38,16 @@ export function parse(value: string | number): number | null {
33
38
  if (typeof value !== 'string') return null
34
39
 
35
40
  const matched = PARSE.exec(value)
36
- const amount = matched ? Number.parseFloat(matched[1]) : Number.parseInt(value, 10)
41
+ // Both groups are required by the pattern, so a match carries both — but
42
+ // read as `matched[2]` the second was typed "a string, or nothing", and the
43
+ // `as Unit` that followed asserted past exactly that. Named, with the
44
+ // suffix checked against the table it indexes, nothing is asserted.
45
+ const [, digits, suffix] = matched ?? []
46
+ const amount = digits !== undefined ? Number.parseFloat(digits) : Number.parseInt(value, 10)
37
47
  if (Number.isNaN(amount)) return null
38
48
 
39
- const unit = (matched ? matched[2].toLowerCase() : 'b') as Unit
49
+ const unit = suffix === undefined ? 'b' : suffix.toLowerCase()
50
+ if (!isUnit(unit)) return null
40
51
  return Math.floor(UNITS[unit] * amount)
41
52
  }
42
53
 
@@ -214,9 +214,9 @@ export function pluralize(word: string, count?: number): string {
214
214
  * @example humanize('user_name') // 'User name'
215
215
  */
216
216
  export function humanize(s: string): string {
217
- const words = splitWords(s)
218
- if (words.length === 0) return ''
219
- return [words[0]?.charAt(0).toUpperCase() + words[0]?.slice(1), ...words.slice(1)].join(' ')
217
+ const [first, ...rest] = splitWords(s)
218
+ if (first === undefined) return ''
219
+ return [first.charAt(0).toUpperCase() + first.slice(1), ...rest].join(' ')
220
220
  }
221
221
 
222
222
  // ---------------------------------------------------------------------------
@@ -315,8 +315,10 @@ export class ExceptionHandler extends Macroable {
315
315
  #expandStatusPages(): Record<number, StatusPageRenderer> {
316
316
  if (!this.#expandedStatusPages) {
317
317
  const expanded: Record<number, StatusPageRenderer> = {}
318
- for (const range of Object.keys(this.statusPages)) {
319
- Object.assign(expanded, parseStatusRange(range, this.statusPages[range]))
318
+ // `entries` rather than `keys` plus a lookup: the value comes back
319
+ // already known to be there, instead of read again as maybe-missing.
320
+ for (const [range, renderer] of Object.entries(this.statusPages)) {
321
+ Object.assign(expanded, parseStatusRange(range, renderer))
320
322
  }
321
323
  this.#expandedStatusPages = expanded
322
324
  }
@@ -440,13 +442,17 @@ export class ExceptionHandler extends Macroable {
440
442
  /**
441
443
  * Pick a renderer by content negotiation.
442
444
  *
443
- * DEVIATION (named): ream lists `json` FIRST, so a request with no or `*` /`*`
444
- * Accept defaults to JSON (API-first). AdonisJS lists `html` first
445
- * (full-stack default). Explicit `text/html` / `vnd.api+json` are honoured
446
- * identically.
445
+ * `html` is listed first, as AdonisJS does, so a request that states no
446
+ * preference — no `Accept` at all, or `*` /`*` — gets the error page rather
447
+ * than a JSON body. A browser always sends `text/html` and an API client
448
+ * always asks for JSON, so the order only decides for the clients in
449
+ * between: curl, a health probe, a proxy that stripped the header. For a
450
+ * framework that ships a view layer, a page is the better answer there.
451
+ *
452
+ * The fallback stays JSON, for a client that accepts none of the three.
447
453
  */
448
454
  async renderError(error: HttpError, ctx: HttpContext): Promise<void> {
449
- switch (ctx.request.accepts(['json', 'html', 'application/vnd.api+json'])) {
455
+ switch (ctx.request.accepts(['html', 'json', 'application/vnd.api+json'])) {
450
456
  case 'application/vnd.api+json':
451
457
  return this.renderErrorAsJSONAPI(error, ctx)
452
458
  case 'html':
@@ -458,7 +464,7 @@ export class ExceptionHandler extends Macroable {
458
464
 
459
465
  /** Pick a validation renderer by content negotiation. */
460
466
  async renderValidationError(error: HttpError, ctx: HttpContext): Promise<void> {
461
- switch (ctx.request.accepts(['json', 'html', 'application/vnd.api+json'])) {
467
+ switch (ctx.request.accepts(['html', 'json', 'application/vnd.api+json'])) {
462
468
  case 'application/vnd.api+json':
463
469
  return this.renderValidationErrorAsJSONAPI(error, ctx)
464
470
  case 'html':
@@ -272,7 +272,10 @@ export class Request extends Macroable {
272
272
  host(): string | null {
273
273
  if (this.#trustProxy) {
274
274
  const forwarded = this.#raw.headers['x-forwarded-host']
275
- if (forwarded) return forwarded.split(',')[0].trim()
275
+ // The first hop in the list, named rather than indexed: `split` always
276
+ // yields one element, and saying so beats being told it might not.
277
+ const [first] = forwarded?.split(',') ?? []
278
+ if (first !== undefined) return first.trim()
276
279
  }
277
280
  return this.#raw.headers.host ?? null
278
281
  }
@@ -285,7 +288,8 @@ export class Request extends Macroable {
285
288
  protocol(): string {
286
289
  if (this.#trustProxy) {
287
290
  const forwarded = this.#raw.headers['x-forwarded-proto']
288
- if (forwarded) return forwarded.split(',')[0].trim().toLowerCase()
291
+ const [first] = forwarded?.split(',') ?? []
292
+ if (first !== undefined) return first.trim().toLowerCase()
289
293
  }
290
294
  return this.#raw.scheme ?? 'http'
291
295
  }
@@ -900,9 +904,8 @@ export class Request extends Macroable {
900
904
  fieldName: string,
901
905
  options?: import('../bodyparser/MultipartFile.js').FileValidationOptions,
902
906
  ): import('../bodyparser/MultipartFile.js').MultipartFile | null {
903
- const files = this.#files.get(fieldName)
904
- if (!files || files.length === 0) return null
905
- const file = files[0]
907
+ const [file] = this.#files.get(fieldName) ?? []
908
+ if (file === undefined) return null
906
909
  if (options) file.validate(options)
907
910
  return file
908
911
  }
package/src/http/mime.ts CHANGED
@@ -1317,7 +1317,8 @@ const CHARSETS: Record<string, string> = {
1317
1317
 
1318
1318
  /** The charset for a type, or `undefined` when it carries none. */
1319
1319
  function charsetFor(type: string): string | undefined {
1320
- const base = type.split(';')[0].trim().toLowerCase()
1320
+ const [head = ''] = type.split(';')
1321
+ const base = head.trim().toLowerCase()
1321
1322
  const declared = CHARSETS[base]
1322
1323
  if (declared !== undefined) return declared
1323
1324
  return base.startsWith('text/') ? 'utf-8' : undefined
package/src/index.ts CHANGED
@@ -254,7 +254,7 @@ export { ConsoleKernel, defineConfig, Ignitor, prettyPrintError } from './Ignito
254
254
  // ─── Providers ──────────────────────────────────────────────
255
255
 
256
256
  export { errors } from './errors/aggregate.js'
257
- export type { AppContext, ProviderContract } from './Provider.js'
257
+ export type { AppContext, ConfigReader, ProviderContract } from './Provider.js'
258
258
  export { Provider } from './Provider.js'
259
259
  export type {
260
260
  AuthenticatorName,
@@ -378,4 +378,4 @@ export {
378
378
  StatsTracker,
379
379
  } from './scheduler/index.js'
380
380
 
381
- export const VERSION = '0.2.14'
381
+ export const VERSION = '0.2.15'
@@ -87,8 +87,14 @@ export declare class HyperServer {
87
87
  * the safe default for a dev machine.
88
88
  */
89
89
  constructor(port?: number | undefined | null, host?: string | undefined | null)
90
- /** Register the request handler. Callback receives JSON request string, must return JSON response string. */
91
- onRequest(callback: (request: string) => Promise<string> | string): void
90
+ /**
91
+ * Register the request handler.
92
+ *
93
+ * The request arrives as an OBJECT (`serde_json::Value` → `JsObject`) and
94
+ * the handler answers with a `NapiResponse` object — not the JSON strings
95
+ * this comment described until the boundary stopped carrying them.
96
+ */
97
+ onRequest(callback: (request: Record<string, unknown>) => Promise<NapiResponse>): void
92
98
  /**
93
99
  * Configure the trusted-proxy CIDR list used by `request.ip` resolution.
94
100
  * Must be called BEFORE `listen()`. Empty list = legacy permissive mode
@@ -155,12 +161,10 @@ export declare class HyperServer {
155
161
  */
156
162
  writeStreamBytes(streamId: string, chunk: Uint8Array): Promise<boolean>
157
163
  /**
158
- * Close a stream from the server side. The matching response body
159
- * finishes cleanly (hyper writes the final chunk + `0
160
-
161
- ` for
162
- * HTTP/1.1 chunked encoding). Idempotent.
163
- */
164
+ * Close a stream from the server side. The matching response body
165
+ * finishes cleanly (hyper writes the final chunk + `0\r\n\r\n` for
166
+ * HTTP/1.1 chunked encoding). Idempotent.
167
+ */
164
168
  closeStream(streamId: string): Promise<boolean>
165
169
  /**
166
170
  * Install a one-shot callback fired the moment the matching stream