@toa.io/extensions.exposition 1.0.0-alpha.3 → 1.0.0-alpha.5

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 (187) hide show
  1. package/components/identity.bans/manifest.toa.yaml +1 -0
  2. package/components/identity.basic/manifest.toa.yaml +1 -0
  3. package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
  4. package/components/identity.federation/manifest.toa.yaml +1 -0
  5. package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
  6. package/components/identity.roles/manifest.toa.yaml +1 -0
  7. package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
  8. package/components/identity.tokens/manifest.toa.yaml +1 -0
  9. package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
  10. package/components/octets.storage/manifest.toa.yaml +1 -0
  11. package/components/octets.storage/operations/store.js +1 -1
  12. package/documentation/components.md +5 -5
  13. package/documentation/query.md +45 -2
  14. package/features/body.feature +1 -1
  15. package/features/cors.feature +2 -2
  16. package/features/errors.feature +1 -1
  17. package/features/etag.feature +96 -0
  18. package/features/octets.entries.feature +1 -1
  19. package/features/steps/Gateway.ts +3 -0
  20. package/features/steps/components/echo/manifest.toa.yaml +1 -0
  21. package/features/steps/components/greeter/manifest.toa.yaml +1 -0
  22. package/features/steps/components/octets.tester/manifest.toa.yaml +1 -0
  23. package/features/steps/components/pots/manifest.toa.yaml +10 -3
  24. package/features/steps/components/sequences/manifest.toa.yaml +1 -0
  25. package/features/timing.feature +43 -0
  26. package/package.json +7 -10
  27. package/readme.md +7 -6
  28. package/schemas/annotation.cos.yaml +1 -0
  29. package/schemas/querystring.cos.yaml +1 -0
  30. package/source/Annotation.ts +1 -0
  31. package/source/Context.ts +6 -4
  32. package/source/Directive.test.ts +7 -7
  33. package/source/Directive.ts +16 -42
  34. package/source/Endpoint.ts +55 -6
  35. package/source/Factory.ts +17 -7
  36. package/source/Gateway.ts +38 -50
  37. package/source/HTTP/Context.ts +67 -0
  38. package/source/HTTP/Server.test.ts +1 -1
  39. package/source/HTTP/Server.ts +60 -95
  40. package/source/HTTP/Timing.ts +40 -0
  41. package/source/HTTP/index.ts +1 -0
  42. package/source/HTTP/messages.test.ts +27 -8
  43. package/source/HTTP/messages.ts +32 -48
  44. package/source/Mapping.ts +7 -8
  45. package/source/RTD/Context.ts +7 -10
  46. package/source/RTD/Directives.ts +28 -4
  47. package/source/RTD/Endpoint.ts +6 -4
  48. package/source/RTD/Match.ts +2 -7
  49. package/source/RTD/Method.ts +7 -13
  50. package/source/RTD/Node.ts +13 -14
  51. package/source/RTD/Tree.ts +17 -16
  52. package/source/RTD/factory.ts +2 -5
  53. package/source/deployment.ts +6 -0
  54. package/source/directives/auth/Anonymous.ts +3 -2
  55. package/source/directives/auth/Authorization.ts +7 -6
  56. package/source/directives/auth/Incept.ts +11 -6
  57. package/source/directives/auth/Role.ts +5 -3
  58. package/source/directives/auth/Scheme.ts +2 -2
  59. package/source/directives/cache/Cache.ts +4 -4
  60. package/source/directives/cache/Control.ts +5 -5
  61. package/source/directives/cache/types.ts +1 -1
  62. package/source/directives/cors/CORS.ts +18 -10
  63. package/source/directives/dev/Development.ts +3 -3
  64. package/source/directives/index.ts +2 -2
  65. package/source/directives/octets/Context.ts +1 -1
  66. package/source/directives/octets/Delete.ts +19 -9
  67. package/source/directives/octets/Fetch.ts +29 -14
  68. package/source/directives/octets/List.ts +14 -6
  69. package/source/directives/octets/Octets.ts +6 -5
  70. package/source/directives/octets/Permute.ts +12 -6
  71. package/source/directives/octets/Store.ts +25 -17
  72. package/source/directives/octets/Workflow.ts +3 -3
  73. package/source/directives/octets/workflows/Workflow.ts +2 -2
  74. package/source/directives/vary/Vary.ts +3 -3
  75. package/source/directives/vary/embeddings/Header.ts +2 -2
  76. package/source/directives/vary/embeddings/Language.ts +2 -2
  77. package/source/io.ts +2 -2
  78. package/transpiled/Annotation.d.ts +1 -0
  79. package/transpiled/Context.d.ts +6 -4
  80. package/transpiled/Directive.d.ts +8 -21
  81. package/transpiled/Directive.js +8 -11
  82. package/transpiled/Directive.js.map +1 -1
  83. package/transpiled/Endpoint.d.ts +7 -5
  84. package/transpiled/Endpoint.js +60 -2
  85. package/transpiled/Endpoint.js.map +1 -1
  86. package/transpiled/Factory.js +8 -2
  87. package/transpiled/Factory.js.map +1 -1
  88. package/transpiled/Gateway.d.ts +4 -8
  89. package/transpiled/Gateway.js +22 -32
  90. package/transpiled/Gateway.js.map +1 -1
  91. package/transpiled/HTTP/Context.d.ts +24 -0
  92. package/transpiled/HTTP/Context.js +47 -0
  93. package/transpiled/HTTP/Context.js.map +1 -0
  94. package/transpiled/HTTP/Server.d.ts +8 -7
  95. package/transpiled/HTTP/Server.js +68 -76
  96. package/transpiled/HTTP/Server.js.map +1 -1
  97. package/transpiled/HTTP/Timing.d.ts +10 -0
  98. package/transpiled/HTTP/Timing.js +29 -0
  99. package/transpiled/HTTP/Timing.js.map +1 -0
  100. package/transpiled/HTTP/index.d.ts +1 -0
  101. package/transpiled/HTTP/index.js +1 -0
  102. package/transpiled/HTTP/index.js.map +1 -1
  103. package/transpiled/HTTP/messages.d.ts +7 -21
  104. package/transpiled/HTTP/messages.js +24 -26
  105. package/transpiled/HTTP/messages.js.map +1 -1
  106. package/transpiled/Mapping.js +7 -7
  107. package/transpiled/Mapping.js.map +1 -1
  108. package/transpiled/RTD/Context.d.ts +7 -6
  109. package/transpiled/RTD/Directives.d.ts +19 -4
  110. package/transpiled/RTD/Endpoint.d.ts +6 -4
  111. package/transpiled/RTD/Match.d.ts +2 -4
  112. package/transpiled/RTD/Method.d.ts +7 -7
  113. package/transpiled/RTD/Method.js.map +1 -1
  114. package/transpiled/RTD/Node.d.ts +4 -6
  115. package/transpiled/RTD/Node.js +2 -1
  116. package/transpiled/RTD/Node.js.map +1 -1
  117. package/transpiled/RTD/Tree.d.ts +6 -6
  118. package/transpiled/RTD/Tree.js +4 -1
  119. package/transpiled/RTD/Tree.js.map +1 -1
  120. package/transpiled/RTD/factory.d.ts +2 -4
  121. package/transpiled/RTD/factory.js.map +1 -1
  122. package/transpiled/deployment.js +5 -0
  123. package/transpiled/deployment.js.map +1 -1
  124. package/transpiled/directives/auth/Anonymous.js +3 -4
  125. package/transpiled/directives/auth/Anonymous.js.map +1 -1
  126. package/transpiled/directives/auth/Authorization.d.ts +2 -3
  127. package/transpiled/directives/auth/Authorization.js +1 -1
  128. package/transpiled/directives/auth/Authorization.js.map +1 -1
  129. package/transpiled/directives/auth/Incept.d.ts +1 -1
  130. package/transpiled/directives/auth/Incept.js +11 -6
  131. package/transpiled/directives/auth/Incept.js.map +1 -1
  132. package/transpiled/directives/auth/Role.js +5 -3
  133. package/transpiled/directives/auth/Role.js.map +1 -1
  134. package/transpiled/directives/auth/Scheme.js +2 -2
  135. package/transpiled/directives/auth/Scheme.js.map +1 -1
  136. package/transpiled/directives/cache/Cache.d.ts +3 -3
  137. package/transpiled/directives/cache/Cache.js +2 -2
  138. package/transpiled/directives/cache/Cache.js.map +1 -1
  139. package/transpiled/directives/cache/Control.d.ts +3 -3
  140. package/transpiled/directives/cache/Control.js +3 -3
  141. package/transpiled/directives/cache/Control.js.map +1 -1
  142. package/transpiled/directives/cache/types.d.ts +1 -1
  143. package/transpiled/directives/cors/CORS.d.ts +2 -3
  144. package/transpiled/directives/cors/CORS.js +17 -10
  145. package/transpiled/directives/cors/CORS.js.map +1 -1
  146. package/transpiled/directives/dev/Development.d.ts +3 -3
  147. package/transpiled/directives/dev/Development.js.map +1 -1
  148. package/transpiled/directives/index.d.ts +2 -2
  149. package/transpiled/directives/index.js.map +1 -1
  150. package/transpiled/directives/octets/Context.d.ts +1 -1
  151. package/transpiled/directives/octets/Context.js.map +1 -1
  152. package/transpiled/directives/octets/Delete.d.ts +1 -1
  153. package/transpiled/directives/octets/Delete.js +19 -9
  154. package/transpiled/directives/octets/Delete.js.map +1 -1
  155. package/transpiled/directives/octets/Fetch.d.ts +1 -1
  156. package/transpiled/directives/octets/Fetch.js +28 -14
  157. package/transpiled/directives/octets/Fetch.js.map +1 -1
  158. package/transpiled/directives/octets/List.d.ts +1 -1
  159. package/transpiled/directives/octets/List.js +13 -6
  160. package/transpiled/directives/octets/List.js.map +1 -1
  161. package/transpiled/directives/octets/Octets.d.ts +2 -3
  162. package/transpiled/directives/octets/Octets.js +4 -2
  163. package/transpiled/directives/octets/Octets.js.map +1 -1
  164. package/transpiled/directives/octets/Permute.d.ts +1 -1
  165. package/transpiled/directives/octets/Permute.js +11 -6
  166. package/transpiled/directives/octets/Permute.js.map +1 -1
  167. package/transpiled/directives/octets/Store.d.ts +1 -1
  168. package/transpiled/directives/octets/Store.js +17 -12
  169. package/transpiled/directives/octets/Store.js.map +1 -1
  170. package/transpiled/directives/octets/Workflow.d.ts +1 -1
  171. package/transpiled/directives/octets/Workflow.js +3 -3
  172. package/transpiled/directives/octets/Workflow.js.map +1 -1
  173. package/transpiled/directives/octets/workflows/Workflow.d.ts +1 -1
  174. package/transpiled/directives/octets/workflows/Workflow.js +2 -2
  175. package/transpiled/directives/octets/workflows/Workflow.js.map +1 -1
  176. package/transpiled/directives/vary/Vary.d.ts +2 -2
  177. package/transpiled/directives/vary/Vary.js +1 -1
  178. package/transpiled/directives/vary/embeddings/Header.js +2 -2
  179. package/transpiled/directives/vary/embeddings/Header.js.map +1 -1
  180. package/transpiled/directives/vary/embeddings/Language.js +2 -2
  181. package/transpiled/directives/vary/embeddings/Language.js.map +1 -1
  182. package/transpiled/io.d.ts +2 -2
  183. package/transpiled/tsconfig.tsbuildinfo +1 -1
  184. package/source/HTTP/Server.fixtures.ts +0 -40
  185. package/transpiled/HTTP/Server.fixtures.d.ts +0 -10
  186. package/transpiled/HTTP/Server.fixtures.js +0 -31
  187. package/transpiled/HTTP/Server.fixtures.js.map +0 -1
@@ -0,0 +1,43 @@
1
+ Feature: Server timing
2
+
3
+ Background:
4
+ Given the `pots` is running with the following manifest:
5
+ """yaml
6
+ exposition:
7
+ /:
8
+ POST: create
9
+ """
10
+
11
+ Scenario: Server timing is not available by default
12
+ When the following request is received:
13
+ """
14
+ POST /pots/ HTTP/1.1
15
+ content-type: application/yaml
16
+
17
+ title: Hello
18
+ volume: 1.5
19
+ """
20
+ Then the reply does not contain:
21
+ """
22
+ server-timing:
23
+ """
24
+
25
+ Scenario: Server timing is sent when debug is enabled
26
+ Given the annotation:
27
+ """
28
+ trace: true
29
+ """
30
+ When the following request is received:
31
+ """
32
+ POST /pots/ HTTP/1.1
33
+ content-type: application/yaml
34
+
35
+ title: Hello
36
+ volume: 1.5
37
+ """
38
+ # to debug, break it and look at the console
39
+ Then the following reply is sent:
40
+ """
41
+ 201 Created
42
+ server-timing:
43
+ """
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.exposition",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.5",
4
4
  "description": "Toa Exposition",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -17,13 +17,11 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@toa.io/core": "1.0.0-alpha.3",
21
- "@toa.io/generic": "1.0.0-alpha.3",
22
- "@toa.io/schemas": "1.0.0-alpha.3",
23
- "@toa.io/streams": "1.0.0-alpha.3",
20
+ "@toa.io/core": "1.0.0-alpha.5",
21
+ "@toa.io/generic": "1.0.0-alpha.5",
22
+ "@toa.io/schemas": "1.0.0-alpha.5",
24
23
  "bcryptjs": "2.4.3",
25
24
  "error-value": "0.3.0",
26
- "express": "4.18.2",
27
25
  "js-yaml": "4.1.0",
28
26
  "matchacho": "0.3.5",
29
27
  "msgpackr": "1.10.1",
@@ -45,12 +43,11 @@
45
43
  "features": "cucumber-js"
46
44
  },
47
45
  "devDependencies": {
48
- "@toa.io/agent": "1.0.0-alpha.3",
49
- "@toa.io/extensions.storages": "1.0.0-alpha.3",
46
+ "@toa.io/agent": "1.0.0-alpha.5",
47
+ "@toa.io/extensions.storages": "1.0.0-alpha.5",
50
48
  "@types/bcryptjs": "2.4.3",
51
49
  "@types/cors": "2.8.13",
52
- "@types/express": "4.17.17",
53
50
  "@types/negotiator": "0.6.1"
54
51
  },
55
- "gitHead": "e36ac7871fc14d15863aaf8f9bbdeace8bdfa9f0"
52
+ "gitHead": "1e4bb4ac28a6dddff1f4b8c5be7224fcdc47b847"
56
53
  }
package/readme.md CHANGED
@@ -127,12 +127,13 @@ exposition:
127
127
  host: the.example.com
128
128
  ```
129
129
 
130
- | Option | Type | Description |
131
- |---------------|-----------|------------------------------------------------------------------|
132
- | `host` | `string` | Domain name to be used for the corresponding Kubernetes Ingress. |
133
- | `class` | `string` | Ingress class |
134
- | `annotations` | `object` | Ingress annotations |
135
- | `debug` | `boolean` | Output server errors. Default `false`. |
130
+ | Option | Type | Description |
131
+ |---------------|-----------|-------------------------------------------------------------------------------------------------------------------|
132
+ | `host` | `string` | Domain name to be used for the corresponding Kubernetes Ingress. |
133
+ | `class` | `string` | Ingress class |
134
+ | `annotations` | `object` | Ingress annotations |
135
+ | `debug` | `boolean` | Output server errors. Default `false`. |
136
+ | `trace` | `boolean` | Output [server timing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing). Default `false`. |
136
137
 
137
138
  ### Context resources
138
139
 
@@ -2,4 +2,5 @@ host: string
2
2
  class: string
3
3
  annotations: <string>
4
4
  debug: boolean
5
+ trace: boolean
5
6
  /: ~
@@ -3,3 +3,4 @@ criteria: string
3
3
  sort: string
4
4
  omit: number
5
5
  limit: number
6
+ version: number
@@ -3,5 +3,6 @@ export interface Annotation {
3
3
  class?: string
4
4
  annotations?: Record<string, string>
5
5
  debug: boolean
6
+ trace: boolean
6
7
  '/'?: object // parsed and validated by RTD.syntax.parse
7
8
  }
package/source/Context.ts CHANGED
@@ -1,6 +1,8 @@
1
- import { type Endpoint } from './Endpoint'
2
- import { type Directives } from './Directive'
3
- import { type Branch } from './Branch'
4
1
  import type * as RTD from './RTD'
5
2
 
6
- export type Context = RTD.Context<Endpoint, Directives, Branch>
3
+ export type Context = RTD.Context<Extension>
4
+
5
+ interface Extension {
6
+ namespace: string
7
+ component: string
8
+ }
@@ -1,11 +1,11 @@
1
1
  import assert from 'node:assert'
2
2
  import { generate } from 'randomstring'
3
- import { DirectivesFactory, type Family } from './Directive'
4
- import { type syntax } from './RTD'
5
- import { type IncomingMessage } from './HTTP'
6
- import { type Remotes } from './Remotes'
3
+ import { DirectivesFactory } from './Directive'
4
+ import type { syntax, DirectiveFamily } from './RTD'
5
+ import type { Remotes } from './Remotes'
6
+ import type { Context } from './HTTP'
7
7
 
8
- const families: Array<jest.MockedObjectDeep<Family>> = [
8
+ const families: Array<jest.MockedObjectDeep<DirectiveFamily>> = [
9
9
  {
10
10
  name: 'foo',
11
11
  mandatory: true,
@@ -76,7 +76,7 @@ it('should apply directive', async () => {
76
76
  }
77
77
 
78
78
  const directives = factory.create([declaration])
79
- const request = generate() as unknown as IncomingMessage
79
+ const request = generate() as unknown as Context
80
80
  const directive = families[0].create.mock.results[0].value
81
81
 
82
82
  await directives.preflight(request, [])
@@ -89,7 +89,7 @@ it('should apply directive', async () => {
89
89
 
90
90
  it('should apply mandatory families', async () => {
91
91
  const directives = factory.create([])
92
- const request = generate() as unknown as IncomingMessage
92
+ const request = generate() as unknown as Context
93
93
 
94
94
  await directives.preflight(request, [])
95
95
 
@@ -1,24 +1,24 @@
1
- import type { IncomingMessage, OutgoingMessage } from './HTTP'
1
+ import type { Context, OutgoingMessage } from './HTTP'
2
2
  import type { Remotes } from './Remotes'
3
3
  import type { Output } from './io'
4
4
  import type * as RTD from './RTD'
5
5
 
6
- export class Directives implements RTD.Directives<Directives> {
7
- private readonly sets: DirectiveSet[]
6
+ export class Directives implements RTD.Directives {
7
+ private readonly sets: RTD.DirectiveSet[]
8
8
 
9
- public constructor (sets: DirectiveSet[]) {
9
+ public constructor (sets: RTD.DirectiveSet[]) {
10
10
  this.sets = sets
11
11
  }
12
12
 
13
- public async preflight (request: IncomingMessage, parameters: RTD.Parameter[]): Promise<Output> {
13
+ public async preflight (context: Context, parameters: RTD.Parameter[]): Promise<Output> {
14
14
  for (const set of this.sets) {
15
15
  if (set.family.preflight === undefined)
16
16
  continue
17
17
 
18
- const output = await set.family.preflight(set.directives, request, parameters)
18
+ const output = await set.family.preflight(set.directives, context, parameters)
19
19
 
20
20
  if (output !== null) {
21
- await this.settle(request, output)
21
+ await this.settle(context, output)
22
22
 
23
23
  return output
24
24
  }
@@ -27,23 +27,19 @@ export class Directives implements RTD.Directives<Directives> {
27
27
  return null
28
28
  }
29
29
 
30
- public async settle (request: IncomingMessage, response: OutgoingMessage): Promise<void> {
30
+ public async settle (context: Context, response: OutgoingMessage): Promise<void> {
31
31
  for (const set of this.sets)
32
32
  if (set.family.settle !== undefined)
33
- await set.family.settle(set.directives, request, response)
34
- }
35
-
36
- public merge (directives: Directives): void {
37
- this.sets.push(...directives.sets)
33
+ await set.family.settle(set.directives, context, response)
38
34
  }
39
35
  }
40
36
 
41
- export class DirectivesFactory implements RTD.DirectivesFactory<Directives> {
42
- private readonly remtoes: Remotes
43
- private readonly families: Record<string, Family> = {}
37
+ export class DirectivesFactory implements RTD.DirectiveFactory {
38
+ private readonly remotes: Remotes
39
+ private readonly families: Record<string, RTD.DirectiveFamily> = {}
44
40
  private readonly mandatory: string[] = []
45
41
 
46
- public constructor (families: Family[], remotes: Remotes) {
42
+ public constructor (families: RTD.DirectiveFamily[], remotes: Remotes) {
47
43
  for (const family of families) {
48
44
  this.families[family.name] = family
49
45
 
@@ -51,7 +47,7 @@ export class DirectivesFactory implements RTD.DirectivesFactory<Directives> {
51
47
  this.mandatory.push(family.name)
52
48
  }
53
49
 
54
- this.remtoes = remotes
50
+ this.remotes = remotes
55
51
  }
56
52
 
57
53
  public create (declarations: RTD.syntax.Directive[]): Directives {
@@ -67,14 +63,14 @@ export class DirectivesFactory implements RTD.DirectivesFactory<Directives> {
67
63
  if (family === undefined)
68
64
  throw new Error(`Directive family '${declaration.family}' is not found.`)
69
65
 
70
- const directive = family.create(declaration.name, declaration.value, this.remtoes)
66
+ const directive = family.create(declaration.name, declaration.value, this.remotes)
71
67
 
72
68
  groups[family.name] ??= []
73
69
  groups[family.name].push(directive)
74
70
  mandatory.delete(family.name)
75
71
  }
76
72
 
77
- const sets: DirectiveSet[] = []
73
+ const sets: RTD.DirectiveSet[] = []
78
74
 
79
75
  for (const family of mandatory)
80
76
  sets.push({
@@ -99,25 +95,3 @@ export const shortcuts: RTD.syntax.Shortcuts = new Map([
99
95
  ['rule', 'auth:rule'],
100
96
  ['incept', 'auth:incept']
101
97
  ])
102
-
103
- export interface Family<TDirective = any, TExtension = any> {
104
- readonly name: string
105
- readonly mandatory: boolean
106
-
107
- // produce: (declarations: RTD.syntax.Directive[], remotes: Remotes) => TDirective[]
108
-
109
- create: (name: string, value: any, remotes: Remotes) => TDirective
110
-
111
- preflight?: (directives: TDirective[],
112
- request: IncomingMessage & TExtension,
113
- parameters: RTD.Parameter[]) => Output | Promise<Output>
114
-
115
- settle?: (directives: TDirective[],
116
- request: IncomingMessage & TExtension,
117
- response: OutgoingMessage) => void | Promise<void>
118
- }
119
-
120
- interface DirectiveSet {
121
- family: Family
122
- directives: any[]
123
- }
@@ -1,11 +1,11 @@
1
- import { type Component, type Reply } from '@toa.io/core'
1
+ import { type Component } from '@toa.io/core'
2
2
  import { type Remotes } from './Remotes'
3
3
  import { Mapping } from './Mapping'
4
4
  import { type Context } from './Context'
5
+ import * as http from './HTTP'
5
6
  import type * as RTD from './RTD'
6
- import type * as http from './HTTP'
7
7
 
8
- export class Endpoint implements RTD.Endpoint<Endpoint> {
8
+ export class Endpoint implements RTD.Endpoint {
9
9
  private readonly endpoint: string
10
10
  private readonly mapping: Mapping
11
11
  private readonly discovery: Promise<Component>
@@ -17,12 +17,40 @@ export class Endpoint implements RTD.Endpoint<Endpoint> {
17
17
  this.discovery = discovery
18
18
  }
19
19
 
20
- public async call (body: any, query: http.Query, parameters: RTD.Parameter[]): Promise<Reply> {
20
+ public async call
21
+ (context: http.Context, parameters: RTD.Parameter[]): Promise<http.OutgoingMessage> {
22
+ const body = await context.body()
23
+ const query = this.query(context)
21
24
  const request = this.mapping.fit(body, query, parameters)
22
25
 
23
26
  this.remote ??= await this.discovery
24
27
 
25
- return await this.remote.invoke(this.endpoint, request)
28
+ const reply = await this.remote.invoke(this.endpoint, request)
29
+
30
+ if (reply instanceof Error)
31
+ throw new http.Conflict(reply)
32
+
33
+ const message: http.OutgoingMessage = {}
34
+
35
+ if (typeof reply === 'object' && reply !== null && '_version' in reply) {
36
+ const etag = context.request.headers['if-none-match']
37
+
38
+ message.headers ??= new Headers()
39
+
40
+ if (etag !== undefined && reply._version === this.version(etag)) {
41
+ message.status = 304
42
+ message.headers.set('etag', etag)
43
+
44
+ return message
45
+ } else {
46
+ message.headers.set('etag', `"${reply._version.toString()}"`)
47
+ delete reply._version
48
+ }
49
+ }
50
+
51
+ message.body = reply
52
+
53
+ return message
26
54
  }
27
55
 
28
56
  public async close (): Promise<void> {
@@ -30,9 +58,28 @@ export class Endpoint implements RTD.Endpoint<Endpoint> {
30
58
 
31
59
  await this.remote.disconnect(INTERRUPT)
32
60
  }
61
+
62
+ private query (context: http.Context): http.Query {
63
+ const query: http.Query = Object.fromEntries(context.url.searchParams)
64
+ const etag = context.request.headers['if-match']
65
+
66
+ if (etag !== undefined)
67
+ query.version = this.version(etag)
68
+
69
+ return query
70
+ }
71
+
72
+ private version (etag: string): number {
73
+ const match = etag.match(ETAG)
74
+
75
+ if (match === null)
76
+ throw new http.BadRequest('Invalid ETag.')
77
+
78
+ return Number.parseInt(match.groups!.version)
79
+ }
33
80
  }
34
81
 
35
- export class EndpointsFactory implements RTD.EndpointsFactory<Endpoint> {
82
+ export class EndpointsFactory implements RTD.EndpointsFactory {
36
83
  private readonly remotes: Remotes
37
84
 
38
85
  public constructor (remotes: Remotes) {
@@ -56,4 +103,6 @@ export class EndpointsFactory implements RTD.EndpointsFactory<Endpoint> {
56
103
  }
57
104
  }
58
105
 
106
+ const ETAG = /^"(?<version>\d{1,32})"$/
107
+
59
108
  const INTERRUPT = true
package/source/Factory.ts CHANGED
@@ -3,12 +3,13 @@ import { Gateway } from './Gateway'
3
3
  import { Remotes } from './Remotes'
4
4
  import { Tree, syntax } from './RTD'
5
5
  import { Server } from './HTTP'
6
- import { type Endpoint, EndpointsFactory } from './Endpoint'
6
+ import { EndpointsFactory } from './Endpoint'
7
7
  import { families, interceptors } from './directives'
8
- import { type Directives, DirectivesFactory } from './Directive'
8
+ import { DirectivesFactory } from './Directive'
9
9
  import { Composition } from './Composition'
10
10
  import * as root from './root'
11
11
  import { Interception } from './Interception'
12
+ import type { Broadcast } from './Gateway'
12
13
  import type { Connector, Locator, extensions } from '@toa.io/core'
13
14
 
14
15
  export class Factory implements extensions.Factory {
@@ -19,27 +20,36 @@ export class Factory implements extensions.Factory {
19
20
  }
20
21
 
21
22
  public tenant (locator: Locator, node: syntax.Node): Connector {
22
- const broadcast = this.boot.bindings.broadcast(CHANNEL, locator.id)
23
+ const broadcast: Broadcast = this.boot.bindings.broadcast(CHANNEL, locator.id)
23
24
 
24
25
  return new Tenant(broadcast, locator, node)
25
26
  }
26
27
 
27
28
  public service (): Connector | null {
28
29
  const debug = process.env.TOA_EXPOSITION_DEBUG === '1'
29
- const broadcast = this.boot.bindings.broadcast(CHANNEL)
30
- const server = Server.create({ methods: syntax.verbs, debug })
30
+ const trace = process.env.TOA_EXPOSITION_TRACE === '1'
31
+ const broadcast: Broadcast = this.boot.bindings.broadcast(CHANNEL)
32
+
33
+ const server = Server.create({
34
+ methods: syntax.verbs,
35
+ debug,
36
+ trace
37
+ })
38
+
31
39
  const remotes = new Remotes(this.boot)
32
40
  const node = root.resolve()
33
41
  const methods = new EndpointsFactory(remotes)
34
42
  const directives = new DirectivesFactory(families, remotes)
35
43
  const interception = new Interception(interceptors)
36
- const tree = new Tree<Endpoint, Directives>(node, methods, directives)
44
+ const tree = new Tree(node, methods, directives)
37
45
 
38
46
  const composition = new Composition(this.boot)
39
- const gateway = new Gateway(broadcast, server, tree, interception)
47
+ const gateway = new Gateway(broadcast, tree, interception)
40
48
 
41
49
  gateway.depends(remotes)
42
50
  gateway.depends(composition)
51
+
52
+ server.attach(gateway.process.bind(gateway))
43
53
  server.depends(gateway)
44
54
 
45
55
  return server
package/source/Gateway.ts CHANGED
@@ -1,95 +1,83 @@
1
1
  import { type bindings, Connector } from '@toa.io/core'
2
2
  import * as http from './HTTP'
3
3
  import { rethrow } from './exceptions'
4
- import type { Interceptor } from './Interception'
5
- import type { Maybe } from '@toa.io/types'
4
+ import type { Interception } from './Interception'
6
5
  import type { Method, Parameter, Tree } from './RTD'
7
6
  import type { Label } from './discovery'
8
7
  import type { Branch } from './Branch'
9
- import type { Endpoint } from './Endpoint'
10
- import type { Directives } from './Directive'
11
8
 
12
9
  export class Gateway extends Connector {
13
10
  private readonly broadcast: Broadcast
14
- private readonly tree: Tree<Endpoint, Directives>
15
- private readonly interceptor: Interceptor
16
- private readonly server: Connector
11
+ private readonly tree: Tree
12
+ private readonly interceptor: Interception
17
13
 
18
- // eslint-disable-next-line max-params, max-len
19
- public constructor (broadcast: Broadcast, server: http.Server, tree: Tree<Endpoint, Directives>, interception: Interceptor) {
14
+ public constructor (broadcast: Broadcast, tree: Tree, interception: Interception) {
20
15
  super()
21
16
 
22
17
  this.broadcast = broadcast
23
18
  this.tree = tree
24
19
  this.interceptor = interception
25
- this.server = server
26
20
 
27
21
  this.depends(broadcast)
28
- // this.depends(server)
29
-
30
- server.attach(this.process.bind(this))
31
- }
32
-
33
- protected override async open (): Promise<void> {
34
- await this.discover()
35
-
36
- console.info('Gateway has started and is awaiting resource branches.')
37
22
  }
38
23
 
39
- protected override dispose (): void {
40
- console.info('Gateway is closed.')
41
- }
42
-
43
- private async process (request: http.IncomingMessage): Promise<http.OutgoingMessage> {
44
- const interception = await this.interceptor.intercept(request)
24
+ public async process (context: http.Context): Promise<http.OutgoingMessage> {
25
+ const interception = await context.timing.capture('gate:intercept',
26
+ this.interceptor.intercept(context))
45
27
 
46
28
  if (interception !== null)
47
29
  return interception
48
30
 
49
- const match = this.tree.match(request.path)
31
+ const match = this.tree.match(context.url.pathname)
50
32
 
51
33
  if (match === null)
52
34
  throw new http.NotFound()
53
35
 
54
- const { node, parameters } = match
36
+ const {
37
+ node,
38
+ parameters
39
+ } = match
55
40
 
56
- if (!(request.method in node.methods))
41
+ if (!(context.request.method in node.methods))
57
42
  throw new http.MethodNotAllowed()
58
43
 
59
- const method = node.methods[request.method]
60
- const interruption = await method.directives.preflight(request, parameters)
61
- const response = interruption ?? await this.call(method, request, parameters)
44
+ const method = node.methods[context.request.method]
62
45
 
63
- await method.directives.settle(request, response)
46
+ const interruption = await context.timing.capture('gate:preflight',
47
+ method.directives.preflight(context, parameters))
48
+
49
+ const response = interruption ??
50
+ await context.timing.capture('gate:call', this.call(method, context, parameters))
51
+
52
+ await context.timing.capture('gate:settle', method.directives.settle(context, response))
64
53
 
65
54
  return response
66
55
  }
67
56
 
68
- private async call
69
- (method: Method<Endpoint, Directives>, request: http.IncomingMessage, parameters: Parameter[]):
57
+ protected override async open (): Promise<void> {
58
+ await this.discover()
59
+
60
+ console.info('Gateway has started and is awaiting resource branches.')
61
+ }
62
+
63
+ protected override dispose (): void {
64
+ console.info('Gateway is closed.')
65
+ }
66
+
67
+ private async call (method: Method, context: http.Context, parameters: Parameter[]):
70
68
  Promise<http.OutgoingMessage> {
71
- if (request.path[request.path.length - 1] !== '/')
69
+ if (context.url.pathname[context.url.pathname.length - 1] !== '/')
72
70
  throw new http.NotFound('Trailing slash is required.')
73
71
 
74
- if (request.encoder === null)
72
+ if (context.encoder === null)
75
73
  throw new http.NotAcceptable()
76
74
 
77
75
  if (method.endpoint === null)
78
76
  throw new http.MethodNotAllowed()
79
77
 
80
- const body = await request.parse()
81
-
82
- if ('embed' in request && typeof body === 'object' && body !== null)
83
- Object.assign(body, request.embed)
84
-
85
- const reply = await method.endpoint
86
- .call(body, request.query, parameters)
87
- .catch(rethrow) as Maybe<unknown>
88
-
89
- if (reply instanceof Error)
90
- throw new http.Conflict(reply)
91
-
92
- return { body: reply }
78
+ return await method.endpoint
79
+ .call(context, parameters)
80
+ .catch(rethrow) as http.OutgoingMessage
93
81
  }
94
82
 
95
83
  private async discover (): Promise<void> {
@@ -109,4 +97,4 @@ export class Gateway extends Connector {
109
97
  }
110
98
  }
111
99
 
112
- type Broadcast = bindings.Broadcast<Label>
100
+ export type Broadcast = bindings.Broadcast<Label>
@@ -0,0 +1,67 @@
1
+ import Negotiator from 'negotiator'
2
+ import { Timing } from './Timing'
3
+ import { type Format, formats, types } from './formats'
4
+ import { read } from './messages'
5
+ import type { OutgoingMessage } from './messages'
6
+ import type * as http from 'node:http'
7
+
8
+ export class Context {
9
+ public readonly request: IncomingMessage
10
+ public readonly url: URL
11
+ public readonly subtype: string | null = null
12
+ public readonly encoder: Format | null = null
13
+ public readonly timing: Timing
14
+
15
+ public readonly pipelines: Pipelines = {
16
+ body: [],
17
+ response: []
18
+ }
19
+
20
+ public constructor (request: IncomingMessage, trace = false) {
21
+ this.request = request
22
+
23
+ this.url = new URL(request.url, `https://${request.headers.host}`)
24
+ this.timing = new Timing(trace)
25
+
26
+ if (this.request.headers.accept !== undefined) {
27
+ const match = SUBTYPE.exec(this.request.headers.accept)
28
+
29
+ if (match !== null) {
30
+ const {
31
+ type,
32
+ subtype,
33
+ suffix
34
+ } = match.groups!
35
+
36
+ this.request.headers.accept = `${type}/${suffix}`
37
+ this.subtype = subtype
38
+ }
39
+ }
40
+
41
+ const negotiator = new Negotiator(this.request)
42
+ const mediaType = negotiator.mediaType(types)
43
+
44
+ if (mediaType !== undefined)
45
+ this.encoder = formats[mediaType]
46
+ }
47
+
48
+ public async body<T> (): Promise<T> {
49
+ const value = await read(this)
50
+
51
+ return this.pipelines.body.length === 0
52
+ ? value
53
+ : this.pipelines.body.reduce((value, transform) => transform(value), value)
54
+ }
55
+ }
56
+
57
+ export interface IncomingMessage extends http.IncomingMessage {
58
+ url: string
59
+ method: string
60
+ }
61
+
62
+ interface Pipelines {
63
+ body: Array<(input: unknown) => unknown>
64
+ response: Array<(output: OutgoingMessage) => void>
65
+ }
66
+
67
+ const SUBTYPE = /^(?<type>\w{1,32})\/(vnd\.toa\.(?<subtype>\S{1,32})\+)(?<suffix>\S{1,32})$/
@@ -41,8 +41,8 @@ it('should send 501 on unknown method', async () => {
41
41
  })
42
42
 
43
43
  it('should stop HTTP server', async () => {
44
+ await server.connect()
44
45
  await server.disconnect()
45
- expect(server.port).toBe(0)
46
46
  expect(server.connected).toBeFalsy()
47
47
  })
48
48