@toa.io/extensions.exposition 1.0.0-alpha.151 → 1.0.0-alpha.153

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 (124) hide show
  1. package/components/identity.bans/operations/tsconfig.tsbuildinfo +1 -1
  2. package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
  3. package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
  4. package/components/identity.keys/operations/tsconfig.tsbuildinfo +1 -1
  5. package/components/identity.otp/operations/tsconfig.tsbuildinfo +1 -1
  6. package/components/identity.passkeys/operations/tsconfig.tsbuildinfo +1 -1
  7. package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
  8. package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
  9. package/documentation/io.md +42 -2
  10. package/documentation/map.md +10 -0
  11. package/documentation/notes/desync.jpg +0 -0
  12. package/documentation/notes/peers.md +59 -0
  13. package/documentation/notes/throttling.md +82 -0
  14. package/features/io.throttle.feature +40 -0
  15. package/features/map.feature +24 -0
  16. package/package.json +4 -3
  17. package/schemas/io/throttle.cos.yaml +36 -0
  18. package/source/Directive.test.ts +4 -2
  19. package/source/Directive.ts +16 -1
  20. package/source/Gateway.ts +2 -0
  21. package/source/HTTP/Server.ts +47 -21
  22. package/source/HTTP/exceptions.ts +21 -15
  23. package/source/RTD/Directives.ts +4 -0
  24. package/source/RTD/Route.ts +8 -2
  25. package/source/RTD/Tree.ts +4 -0
  26. package/source/RTD/segment.ts +2 -0
  27. package/source/directives/io/Directive.ts +5 -2
  28. package/source/directives/io/IO.ts +17 -5
  29. package/source/directives/io/Input.ts +1 -1
  30. package/source/directives/io/Output.ts +1 -1
  31. package/source/directives/io/Throttle.ts +32 -0
  32. package/source/directives/io/lib/throttle/Configuration.test.ts +40 -0
  33. package/source/directives/io/lib/throttle/Configuration.ts +58 -0
  34. package/source/directives/io/lib/throttle/Interval.ts +31 -0
  35. package/source/directives/io/lib/throttle/Keys.ts +40 -0
  36. package/source/directives/io/lib/throttle/Quota.ts +22 -0
  37. package/source/directives/io/lib/throttle/Quotas.test.ts +132 -0
  38. package/source/directives/io/lib/throttle/Quotas.ts +83 -0
  39. package/source/directives/io/lib/throttle/components/Component.ts +5 -0
  40. package/source/directives/io/lib/throttle/components/IP.ts +40 -0
  41. package/source/directives/io/lib/throttle/components/Path.ts +8 -0
  42. package/source/directives/io/lib/throttle/components/index.ts +13 -0
  43. package/source/directives/io/lib/throttle/conditions/Condition.ts +5 -0
  44. package/source/directives/io/lib/throttle/conditions/Status.ts +17 -0
  45. package/source/directives/io/lib/throttle/conditions/index.ts +11 -0
  46. package/source/directives/io/lib/throttle/index.ts +2 -0
  47. package/source/directives/io/schemas.test.ts +9 -0
  48. package/source/directives/io/schemas.ts +3 -0
  49. package/transpiled/Directive.d.ts +3 -0
  50. package/transpiled/Directive.js +12 -1
  51. package/transpiled/Directive.js.map +1 -1
  52. package/transpiled/Gateway.js +1 -0
  53. package/transpiled/Gateway.js.map +1 -1
  54. package/transpiled/HTTP/Server.js +41 -16
  55. package/transpiled/HTTP/Server.js.map +1 -1
  56. package/transpiled/HTTP/exceptions.d.ts +11 -8
  57. package/transpiled/HTTP/exceptions.js +23 -17
  58. package/transpiled/HTTP/exceptions.js.map +1 -1
  59. package/transpiled/RTD/Directives.d.ts +3 -0
  60. package/transpiled/RTD/Route.d.ts +1 -0
  61. package/transpiled/RTD/Route.js +7 -2
  62. package/transpiled/RTD/Route.js.map +1 -1
  63. package/transpiled/RTD/Tree.d.ts +1 -0
  64. package/transpiled/RTD/Tree.js +3 -0
  65. package/transpiled/RTD/Tree.js.map +1 -1
  66. package/transpiled/RTD/segment.d.ts +1 -0
  67. package/transpiled/RTD/segment.js +2 -0
  68. package/transpiled/RTD/segment.js.map +1 -1
  69. package/transpiled/directives/io/Directive.d.ts +5 -2
  70. package/transpiled/directives/io/IO.d.ts +4 -2
  71. package/transpiled/directives/io/IO.js +13 -3
  72. package/transpiled/directives/io/IO.js.map +1 -1
  73. package/transpiled/directives/io/Input.d.ts +1 -1
  74. package/transpiled/directives/io/Input.js +1 -1
  75. package/transpiled/directives/io/Input.js.map +1 -1
  76. package/transpiled/directives/io/Output.d.ts +1 -1
  77. package/transpiled/directives/io/Output.js +1 -1
  78. package/transpiled/directives/io/Output.js.map +1 -1
  79. package/transpiled/directives/io/Throttle.d.ts +11 -0
  80. package/transpiled/directives/io/Throttle.js +51 -0
  81. package/transpiled/directives/io/Throttle.js.map +1 -0
  82. package/transpiled/directives/io/lib/throttle/Configuration.d.ts +23 -0
  83. package/transpiled/directives/io/lib/throttle/Configuration.js +27 -0
  84. package/transpiled/directives/io/lib/throttle/Configuration.js.map +1 -0
  85. package/transpiled/directives/io/lib/throttle/Interval.d.ts +9 -0
  86. package/transpiled/directives/io/lib/throttle/Interval.js +31 -0
  87. package/transpiled/directives/io/lib/throttle/Interval.js.map +1 -0
  88. package/transpiled/directives/io/lib/throttle/Keys.d.ts +12 -0
  89. package/transpiled/directives/io/lib/throttle/Keys.js +34 -0
  90. package/transpiled/directives/io/lib/throttle/Keys.js.map +1 -0
  91. package/transpiled/directives/io/lib/throttle/Quota.d.ts +8 -0
  92. package/transpiled/directives/io/lib/throttle/Quota.js +22 -0
  93. package/transpiled/directives/io/lib/throttle/Quota.js.map +1 -0
  94. package/transpiled/directives/io/lib/throttle/Quotas.d.ts +26 -0
  95. package/transpiled/directives/io/lib/throttle/Quotas.js +61 -0
  96. package/transpiled/directives/io/lib/throttle/Quotas.js.map +1 -0
  97. package/transpiled/directives/io/lib/throttle/components/Component.d.ts +4 -0
  98. package/transpiled/directives/io/lib/throttle/components/Component.js +3 -0
  99. package/transpiled/directives/io/lib/throttle/components/Component.js.map +1 -0
  100. package/transpiled/directives/io/lib/throttle/components/IP.d.ts +6 -0
  101. package/transpiled/directives/io/lib/throttle/components/IP.js +33 -0
  102. package/transpiled/directives/io/lib/throttle/components/IP.js.map +1 -0
  103. package/transpiled/directives/io/lib/throttle/components/Path.d.ts +5 -0
  104. package/transpiled/directives/io/lib/throttle/components/Path.js +10 -0
  105. package/transpiled/directives/io/lib/throttle/components/Path.js.map +1 -0
  106. package/transpiled/directives/io/lib/throttle/components/index.d.ts +5 -0
  107. package/transpiled/directives/io/lib/throttle/components/index.js +10 -0
  108. package/transpiled/directives/io/lib/throttle/components/index.js.map +1 -0
  109. package/transpiled/directives/io/lib/throttle/conditions/Condition.d.ts +4 -0
  110. package/transpiled/directives/io/lib/throttle/conditions/Condition.js +3 -0
  111. package/transpiled/directives/io/lib/throttle/conditions/Condition.js.map +1 -0
  112. package/transpiled/directives/io/lib/throttle/conditions/Status.d.ts +7 -0
  113. package/transpiled/directives/io/lib/throttle/conditions/Status.js +19 -0
  114. package/transpiled/directives/io/lib/throttle/conditions/Status.js.map +1 -0
  115. package/transpiled/directives/io/lib/throttle/conditions/index.d.ts +5 -0
  116. package/transpiled/directives/io/lib/throttle/conditions/index.js +8 -0
  117. package/transpiled/directives/io/lib/throttle/conditions/index.js.map +1 -0
  118. package/transpiled/directives/io/lib/throttle/index.d.ts +2 -0
  119. package/transpiled/directives/io/lib/throttle/index.js +8 -0
  120. package/transpiled/directives/io/lib/throttle/index.js.map +1 -0
  121. package/transpiled/directives/io/schemas.d.ts +2 -0
  122. package/transpiled/directives/io/schemas.js +2 -1
  123. package/transpiled/directives/io/schemas.js.map +1 -1
  124. package/transpiled/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,36 @@
1
+ definitions:
2
+ component: &component
3
+ type: string
4
+ enum: [ip, path]
5
+ condition: &condition
6
+ type: object
7
+ properties:
8
+ status:
9
+ type: number
10
+ type: object
11
+ properties:
12
+ key:
13
+ anyOf:
14
+ - *component
15
+ - type: array
16
+ items: *component
17
+ minItems: 1
18
+ condition:
19
+ anyOf:
20
+ - *condition
21
+ - type: array
22
+ items: *condition
23
+ minItems: 1
24
+ requests:
25
+ type: integer
26
+ minimum: 1
27
+ interval: &interval
28
+ description: Seconds
29
+ type: integer
30
+ minimum: 1
31
+ cooldown: *interval
32
+ required:
33
+ - key
34
+ - requests
35
+ - interval
36
+ - cooldown
@@ -11,14 +11,16 @@ const families: Array<jest.MockedObjectDeep<DirectiveFamily>> = [
11
11
  mandatory: true,
12
12
  create: jest.fn((_0: any, _1: any, _2: any) => generate() as any),
13
13
  preflight: jest.fn(),
14
- settle: jest.fn()
14
+ settle: jest.fn(),
15
+ dispose: jest.fn()
15
16
  },
16
17
  {
17
18
  name: 'bar',
18
19
  mandatory: false,
19
20
  create: jest.fn((_0: string, _1: any, _2: any) => generate() as any),
20
21
  preflight: jest.fn(),
21
- settle: jest.fn()
22
+ settle: jest.fn(),
23
+ dispose: jest.fn()
22
24
  }
23
25
  ]
24
26
 
@@ -36,12 +36,18 @@ export class Directives implements RTD.Directives {
36
36
  if (set.family.settle !== undefined)
37
37
  await set.family.settle(set.directives, context, response)
38
38
  }
39
+
40
+ public dispose (): void {
41
+ for (const set of this.sets)
42
+ set.family.dispose?.(set.directives)
43
+ }
39
44
  }
40
45
 
41
46
  export class DirectivesFactory implements RTD.DirectiveFactory {
42
47
  private readonly remotes: Remotes
43
48
  private readonly families: Record<string, RTD.DirectiveFamily> = {}
44
49
  private readonly mandatory: string[] = []
50
+ private readonly instances: Directives[] = []
45
51
 
46
52
  public constructor (families: RTD.DirectiveFamily[], remotes: Remotes) {
47
53
  for (const family of families) {
@@ -88,7 +94,16 @@ export class DirectivesFactory implements RTD.DirectiveFactory {
88
94
  directives
89
95
  })
90
96
 
91
- return new Directives(sets)
97
+ const directives = new Directives(sets)
98
+
99
+ this.instances.push(directives)
100
+
101
+ return directives
102
+ }
103
+
104
+ public dispose (): void {
105
+ for (const directives of this.instances)
106
+ directives.dispose()
92
107
  }
93
108
  }
94
109
 
package/source/Gateway.ts CHANGED
@@ -65,6 +65,8 @@ export class Gateway extends Connector {
65
65
  }
66
66
 
67
67
  protected override dispose (): void {
68
+ this.tree.dispose()
69
+
68
70
  console.info('Gateway is closed')
69
71
  }
70
72
 
@@ -29,7 +29,9 @@ export class Server extends Connector {
29
29
 
30
30
  this.server.on('clientError', (error, socket) => {
31
31
  console.warn('Client connection error', error)
32
- socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
32
+
33
+ if (socket.writable) socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
34
+ else socket.destroy()
33
35
  })
34
36
  }
35
37
 
@@ -70,6 +72,20 @@ export class Server extends Connector {
70
72
  }
71
73
 
72
74
  private listener (request: http.IncomingMessage, response: http.ServerResponse): void {
75
+ request.once('error', (error) => {
76
+ console.warn('Request error', errorAttributes(request, error))
77
+
78
+ if (!response.writableEnded)
79
+ response.destroy()
80
+ })
81
+
82
+ request.socket.once('error', (error) => {
83
+ console.warn('Socket error', errorAttributes(request, error))
84
+
85
+ if (!response.writableEnded)
86
+ response.destroy()
87
+ })
88
+
73
89
  const invalid = validate(request)
74
90
 
75
91
  if (invalid !== null) {
@@ -80,9 +96,6 @@ export class Server extends Connector {
80
96
  return
81
97
  }
82
98
 
83
- request.once('error', (error) => console.warn('Request error', errorAttributes(request, error)))
84
- request.socket.once('error', (error) => console.warn('Socket error', errorAttributes(request, error)))
85
-
86
99
  if (request.method === undefined || !this.properties.methods.has(request.method)) {
87
100
  response.writeHead(501).end()
88
101
 
@@ -113,7 +126,6 @@ export class Server extends Connector {
113
126
  .finally(() => {
114
127
  request.removeAllListeners('error')
115
128
  request.socket.removeAllListeners('error')
116
- response.removeAllListeners('error')
117
129
  })
118
130
  }
119
131
 
@@ -139,22 +151,36 @@ export class Server extends Connector {
139
151
 
140
152
  private fail (context: Context, response: http.ServerResponse) {
141
153
  return async (exception: Error) => {
142
- if (!context.request.complete)
143
- await adam(context.request)
144
-
145
- response.statusCode = exception instanceof Exception ? exception.status : 500
146
-
147
- const message: OutgoingMessage = { status: response.statusCode }
148
-
149
- if (context.encoder === null)
150
- message.body = undefined
151
- else if (exception instanceof ClientError || this.properties.debug)
152
- message.body =
153
- exception instanceof Exception
154
- ? exception.body
155
- : exception.stack ?? exception.message
156
-
157
- await write(context, response, message)
154
+ try {
155
+ if (!context.request.complete)
156
+ await adam(context.request)
157
+
158
+ if (!response.writableEnded) {
159
+ response.statusCode = exception instanceof Exception ? exception.status : 500
160
+
161
+ const message: OutgoingMessage = { status: response.statusCode }
162
+
163
+ // eslint-disable-next-line max-depth
164
+ if (context.encoder === null)
165
+ message.body = undefined
166
+ else if (exception instanceof ClientError || this.properties.debug)
167
+ message.body =
168
+ exception instanceof Exception
169
+ ? exception.body
170
+ : exception.stack ?? exception.message
171
+
172
+ await write(context, response, message)
173
+ }
174
+ } catch (final) {
175
+ console.error('Error in error handler', final)
176
+
177
+ if (!response.writableEnded)
178
+ try {
179
+ response.writeHead(500).end()
180
+ } catch (e) {
181
+ // Nothing more we can do
182
+ }
183
+ }
158
184
  }
159
185
  }
160
186
  }
@@ -36,18 +36,6 @@ export class NotFound extends ClientError {
36
36
  }
37
37
  }
38
38
 
39
- export class Conflict extends ClientError {
40
- public constructor (body?: any) {
41
- super(409, body)
42
- }
43
- }
44
-
45
- export class UnprocessableEntity extends ClientError {
46
- public constructor (body?: any) {
47
- super(422, body)
48
- }
49
- }
50
-
51
39
  export class MethodNotAllowed extends ClientError {
52
40
  public constructor () {
53
41
  super(405)
@@ -60,9 +48,9 @@ export class NotAcceptable extends ClientError {
60
48
  }
61
49
  }
62
50
 
63
- export class UnsupportedMediaType extends ClientError {
64
- public constructor () {
65
- super(415)
51
+ export class Conflict extends ClientError {
52
+ public constructor (body?: any) {
53
+ super(409, body)
66
54
  }
67
55
  }
68
56
 
@@ -77,3 +65,21 @@ export class RequestEntityTooLarge extends ClientError {
77
65
  super(413, body)
78
66
  }
79
67
  }
68
+
69
+ export class UnsupportedMediaType extends ClientError {
70
+ public constructor () {
71
+ super(415)
72
+ }
73
+ }
74
+
75
+ export class UnprocessableEntity extends ClientError {
76
+ public constructor (body?: any) {
77
+ super(422, body)
78
+ }
79
+ }
80
+
81
+ export class TooManyRequests extends ClientError {
82
+ public constructor () {
83
+ super(429)
84
+ }
85
+ }
@@ -6,10 +6,12 @@ import type { Output } from '../io'
6
6
  export interface Directives {
7
7
  preflight: (context: Context, parameters: Parameter[]) => Promise<Output>
8
8
  settle: (context: Context, response: OutgoingMessage) => Promise<void>
9
+ dispose: () => void
9
10
  }
10
11
 
11
12
  export interface DirectiveFactory {
12
13
  create: (directives: syntax.Directive[]) => Directives
14
+ dispose: () => void
13
15
  }
14
16
 
15
17
  export interface DirectiveSet {
@@ -30,4 +32,6 @@ export interface DirectiveFamily<TDirective = any, TExtension = any> {
30
32
  settle?: (directives: TDirective[],
31
33
  request: Context & TExtension,
32
34
  response: OutgoingMessage) => void | Promise<void>
35
+
36
+ dispose?: (directives: TDirective[]) => void
33
37
  }
@@ -7,6 +7,7 @@ export class Route {
7
7
  public readonly variables: number = 0
8
8
  public readonly segments: Segment[]
9
9
  private readonly node: Node
10
+ private readonly wildcard: boolean = false
10
11
 
11
12
  public constructor (segments: Segment[], node: Node) {
12
13
  this.root = segments.length === 0
@@ -14,8 +15,10 @@ export class Route {
14
15
  this.node = node
15
16
 
16
17
  for (const segment of segments)
17
- if (segment.fragment === null)
18
+ if (segment.fragment === null) {
18
19
  this.variables++
20
+ this.wildcard ||= segment.wildcard === true
21
+ }
19
22
  }
20
23
 
21
24
  public match (fragments: string[], parameters: Parameter[]): Match | null {
@@ -27,11 +30,14 @@ export class Route {
27
30
 
28
31
  if (segment.fragment === null && segment.placeholder !== null)
29
32
  parameters.push({ name: segment.placeholder, value: fragments[i] })
33
+
34
+ if (segment.fragment === null && segment.wildcard === true)
35
+ parameters.push({ name: '**', value: fragments.slice(this.segments.length - 1).join('/') })
30
36
  }
31
37
 
32
38
  const exact = this.segments.length === fragments.length
33
39
 
34
- if (exact && !this.node.intermediate)
40
+ if ((exact && !this.node.intermediate) || this.wildcard)
35
41
  return { node: this.node, parameters }
36
42
  else
37
43
  return this.matchNested(fragments, parameters)
@@ -39,6 +39,10 @@ export class Tree {
39
39
  this.trunk.merge(branch)
40
40
  }
41
41
 
42
+ public dispose (): void {
43
+ this.directives.dispose()
44
+ }
45
+
42
46
  private createNode
43
47
  (node: syntax.Node, protect: boolean, extension?: unknown): Node {
44
48
  const context: Context = {
@@ -15,6 +15,7 @@ export function fragment (path: string): string[] {
15
15
  function parse (segment: string): Segment {
16
16
  if (segment[0] === ':') return { fragment: null, placeholder: segment.substring(1) }
17
17
  else if (segment === '*') return { fragment: null, placeholder: null }
18
+ else if (segment === '**') return { fragment: null, placeholder: null, wildcard: true }
18
19
  else return { fragment: segment }
19
20
  }
20
21
 
@@ -23,4 +24,5 @@ export type Segment = {
23
24
  } | {
24
25
  fragment: null
25
26
  placeholder: string | null
27
+ wildcard?: boolean
26
28
  }
@@ -1,7 +1,10 @@
1
- import type { Input } from '../../io'
1
+ import type { Input as Context } from '../../io'
2
+ import type * as http from '../../HTTP'
2
3
 
3
4
  export interface Directive {
4
- attach: (context: Input) => void
5
+ preflight: (context: Context) => void
6
+ settle?: (context: Context, response: http.OutgoingMessage) => Promise<void> | void
7
+ dispose?: () => void
5
8
  }
6
9
 
7
10
  export interface Constructor {
@@ -1,7 +1,8 @@
1
1
  import { Output } from './Output'
2
2
  import { Input } from './Input'
3
+ import { Throttle } from './Throttle'
4
+ import type * as http from '../../HTTP'
3
5
  import type { Constructor, Directive } from './Directive'
4
- import type { Input as Context } from '../../io'
5
6
  import type { DirectiveFamily } from '../../RTD'
6
7
 
7
8
  export class IO implements DirectiveFamily<Directive> {
@@ -19,25 +20,36 @@ export class IO implements DirectiveFamily<Directive> {
19
20
  return new Directive(value)
20
21
  }
21
22
 
22
- public preflight (directives: Directive[], context: Context): null {
23
+ public preflight (directives: Directive[], context: http.Context): null {
23
24
  let restricted = false
24
25
 
25
26
  for (const directive of directives) {
26
27
  restricted ||= directive instanceof Output
27
28
 
28
- directive.attach(context)
29
+ directive.preflight(context)
29
30
  }
30
31
 
31
32
  if (!restricted)
32
- DENIAL.attach(context)
33
+ DENIAL.preflight(context)
33
34
 
34
35
  return null
35
36
  }
37
+
38
+ public async settle (directives: Directive[], context: http.Context, output: http.OutgoingMessage): Promise<void> {
39
+ for (const directive of directives)
40
+ await directive.settle?.(context, output)
41
+ }
42
+
43
+ public dispose (directives: Directive[]): void {
44
+ for (const directive of directives)
45
+ directive.dispose?.()
46
+ }
36
47
  }
37
48
 
38
49
  const constructors: Record<string, Constructor> = {
50
+ input: Input,
39
51
  output: Output,
40
- input: Input
52
+ throttle: Throttle
41
53
  }
42
54
 
43
55
  const DENIAL: Output = new Output([])
@@ -15,7 +15,7 @@ export class Input implements Directive {
15
15
  schemas.input.validate<Permissions>(permissions, 'Incorrect \'io:input\' format')
16
16
  }
17
17
 
18
- public attach (context: Context): void {
18
+ public preflight (context: Context): void {
19
19
  context.pipelines.body.push((body) => this.check(body))
20
20
  }
21
21
 
@@ -26,7 +26,7 @@ export class Output implements Directive {
26
26
  schemas.output.validate(permissions, 'Incorrect \'io:output\' format')
27
27
  }
28
28
 
29
- public attach (context: Context): void {
29
+ public preflight (context: Context): void {
30
30
  context.pipelines.response.push(this.restriction(context))
31
31
  }
32
32
 
@@ -0,0 +1,32 @@
1
+ import { TooManyRequests } from '../../HTTP'
2
+ import * as schemas from './schemas'
3
+ import { parse, Quotas, type Declaration } from './lib/throttle'
4
+ import type * as http from '../../HTTP'
5
+ import type { Directive } from './Directive'
6
+
7
+ export class Throttle implements Directive {
8
+ private readonly quotas: Quotas
9
+
10
+ public constructor (declaration: Declaration) {
11
+ const configuration = parse(declaration)
12
+
13
+ this.quotas = Quotas.create(configuration)
14
+ }
15
+
16
+ public static validate (declaration: unknown): asserts declaration is Declaration {
17
+ schemas.throttle.validate(declaration, 'Incorrect \'io:throttle\' format')
18
+ }
19
+
20
+ public preflight (context: http.Context): void {
21
+ if (!this.quotas.ok(context))
22
+ throw new TooManyRequests()
23
+ }
24
+
25
+ public settle (context: http.Context, output: http.OutgoingMessage): void {
26
+ this.quotas.use(context, output)
27
+ }
28
+
29
+ public dispose (): void {
30
+ this.quotas.dispose()
31
+ }
32
+ }
@@ -0,0 +1,40 @@
1
+ import { parse, type Configuration } from './Configuration'
2
+
3
+ const rest: Omit<Configuration, 'key' | 'condition'> = {
4
+ interval: 1,
5
+ requests: 1,
6
+ cooldown: 1
7
+ }
8
+
9
+ it('should convert key', () => {
10
+ const result: Partial<Configuration> = {
11
+ key: [
12
+ {
13
+ method: 'ip'
14
+ }
15
+ ]
16
+ }
17
+
18
+ expect(parse({ key: 'ip', ...rest })).toMatchObject(result)
19
+ expect(parse({ key: ['ip'], ...rest })).toMatchObject(result)
20
+ })
21
+
22
+ it('should convert condition', () => {
23
+ expect(parse({ key: ['ip', 'path'], condition: { status: '404' }, ...rest })).toMatchObject({
24
+ condition: [
25
+ {
26
+ method: 'status',
27
+ options: '404'
28
+ }
29
+ ]
30
+ })
31
+
32
+ expect(parse({ key: 'ip', condition: { status: '404' }, ...rest })).toMatchObject({
33
+ condition: [
34
+ {
35
+ method: 'status',
36
+ options: '404'
37
+ }
38
+ ]
39
+ })
40
+ })
@@ -0,0 +1,58 @@
1
+ export interface Configuration {
2
+ key: KeyComponent[]
3
+ condition?: KeyCondition[]
4
+ requests: number
5
+ interval: number
6
+ cooldown: number
7
+ }
8
+
9
+ interface Rule<T, K = unknown> {
10
+ method: T
11
+ options?: K
12
+ }
13
+
14
+ export type KeyComponentMethod = 'ip' | 'path'
15
+ export type KeyConditionMethod = 'status'
16
+
17
+ export type KeyComponent = Rule<KeyComponentMethod>
18
+ export type KeyCondition = Rule<KeyConditionMethod>
19
+
20
+ type KeyDeclaration = KeyComponentMethod | KeyComponentMethod[]
21
+
22
+ type ConditionDeclaration =
23
+ Record<KeyConditionMethod, unknown>
24
+ | Array<Record<KeyConditionMethod, unknown>>
25
+
26
+ export interface Declaration extends Omit<Configuration, 'key' | 'condition'> {
27
+ key: KeyDeclaration
28
+ condition?: ConditionDeclaration
29
+ }
30
+
31
+ export function parse (declaration: Declaration): Configuration {
32
+ const { key, condition, requests, interval, cooldown } = declaration
33
+
34
+ return {
35
+ key: mapKey(key),
36
+ condition: mapCondition(condition),
37
+ requests,
38
+ interval: interval * 1000,
39
+ cooldown: cooldown * 1000
40
+ }
41
+ }
42
+
43
+ function mapKey (declaration: KeyDeclaration): KeyComponent[] {
44
+ const methods = Array.isArray(declaration) ? declaration : [declaration]
45
+
46
+ return methods.map((method) => ({ method }))
47
+ }
48
+
49
+ function mapCondition (declaration?: ConditionDeclaration): KeyCondition[] | undefined {
50
+ if (declaration === undefined)
51
+ return
52
+
53
+ // reduce to a single object, then map entries to rules
54
+ const conditions = Array.isArray(declaration) ? declaration : [declaration]
55
+ const single = conditions.reduce((acc, condition) => ({ ...acc, ...condition }), {})
56
+
57
+ return Object.entries(single).map(([method, options]) => ({ method, options })) as KeyCondition[]
58
+ }
@@ -0,0 +1,31 @@
1
+ import assert from 'node:assert'
2
+ import { EventEmitter } from 'node:events'
3
+
4
+ export class Interval extends EventEmitter {
5
+ public number: number = 0
6
+ private interval: ReturnType<typeof setInterval> | null = null
7
+
8
+ public constructor (interval: number) {
9
+ super()
10
+
11
+ const number = Math.ceil(Date.now() / interval)
12
+ const shift = number * interval - Date.now()
13
+
14
+ assert.ok(shift >= 0, 'shift must be positive')
15
+
16
+ setTimeout(() => this.start(interval), shift)
17
+ }
18
+
19
+ public start (interval: number): void {
20
+ this.interval = setInterval(() => this.emit('tick'), interval)
21
+ }
22
+
23
+ public dispose (): void {
24
+ if (this.interval !== null) {
25
+ clearInterval(this.interval)
26
+ this.interval = null
27
+ }
28
+
29
+ this.removeAllListeners()
30
+ }
31
+ }
@@ -0,0 +1,40 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { Components, type Component } from './components'
3
+ import { Conditions, type Condition } from './conditions'
4
+ import type { KeyComponent, KeyCondition } from './Configuration'
5
+ import type { Input as Context, Output } from '../../../../io'
6
+
7
+ export class Keys {
8
+ private readonly components: Component[]
9
+ private readonly conditions?: Condition[]
10
+
11
+ public constructor (components: Component[], conditions?: Condition[]) {
12
+ this.components = components
13
+ this.conditions = conditions
14
+ }
15
+
16
+ public static create (componentRules: KeyComponent[], conditionRules?: KeyCondition[]): Keys {
17
+ const components = componentRules.map((rule) => new Components[rule.method](rule.options))
18
+ const conditions = conditionRules?.map((rule) => new Conditions[rule.method](rule.options))
19
+
20
+ return new this(components, conditions)
21
+ }
22
+
23
+ public get (context: Context): string {
24
+ const hash = createHash('sha256')
25
+
26
+ for (const component of this.components)
27
+ hash.update(component.get(context))
28
+
29
+ return hash.digest('hex')
30
+ }
31
+
32
+ public match (input: Context, output: Output): string | null {
33
+ const miss = this.conditions?.some((condition) => !condition.match(input, output))
34
+
35
+ if (miss === true)
36
+ return null
37
+ else
38
+ return this.get(input)
39
+ }
40
+ }
@@ -0,0 +1,22 @@
1
+ export class Quota {
2
+ private readonly limit: number = 0
3
+ private used: number = 0
4
+
5
+ public constructor (limit: number) {
6
+ this.limit = limit
7
+ }
8
+
9
+ public get idle (): boolean {
10
+ return this.used === 0
11
+ }
12
+
13
+ public use (count = 1): boolean {
14
+ this.used += count
15
+
16
+ return this.used < this.limit
17
+ }
18
+
19
+ public reset (): void {
20
+ this.used = 0
21
+ }
22
+ }