@toa.io/extensions.introspection 1.0.0-alpha.262

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 (150) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/components/introspection.edges/manifest.toa.yaml +48 -0
  3. package/components/introspection.edges/operations/lib/types.d.ts +28 -0
  4. package/components/introspection.edges/operations/lib/types.js +3 -0
  5. package/components/introspection.edges/operations/lib/types.js.map +1 -0
  6. package/components/introspection.edges/operations/merge.d.ts +9 -0
  7. package/components/introspection.edges/operations/merge.js +25 -0
  8. package/components/introspection.edges/operations/merge.js.map +1 -0
  9. package/components/introspection.edges/source/lib/types.ts +33 -0
  10. package/components/introspection.edges/source/merge.ts +26 -0
  11. package/components/introspection.edges/tsconfig.json +9 -0
  12. package/components/introspection.nodes/manifest.toa.yaml +49 -0
  13. package/components/introspection.nodes/operations/lib/types.d.ts +16 -0
  14. package/components/introspection.nodes/operations/lib/types.js +3 -0
  15. package/components/introspection.nodes/operations/lib/types.js.map +1 -0
  16. package/components/introspection.nodes/operations/merge.d.ts +9 -0
  17. package/components/introspection.nodes/operations/merge.js +21 -0
  18. package/components/introspection.nodes/operations/merge.js.map +1 -0
  19. package/components/introspection.nodes/source/lib/types.ts +18 -0
  20. package/components/introspection.nodes/source/merge.ts +21 -0
  21. package/components/introspection.nodes/tsconfig.json +9 -0
  22. package/cucumber.js +8 -0
  23. package/features/calls.feature +123 -0
  24. package/features/nodes.feature +70 -0
  25. package/features/samples.feature +75 -0
  26. package/features/shutdown.feature +37 -0
  27. package/features/site/_app/immutable/asset.js +1 -0
  28. package/features/site/index.html +6 -0
  29. package/features/steps/Map.ts +151 -0
  30. package/features/steps/UI.ts +88 -0
  31. package/features/steps/components/probe.discreet/manifest.toa.yaml +12 -0
  32. package/features/steps/components/probe.discreet/operations/ping.js +7 -0
  33. package/features/steps/components/probe.quiet/manifest.toa.yaml +11 -0
  34. package/features/steps/components/probe.quiet/operations/ping.js +7 -0
  35. package/features/steps/components/probe.source/manifest.toa.yaml +27 -0
  36. package/features/steps/components/probe.source/operations/crash.js +7 -0
  37. package/features/steps/components/probe.source/operations/fail.js +12 -0
  38. package/features/steps/components/probe.source/operations/relay.js +7 -0
  39. package/features/steps/components/probe.target/manifest.toa.yaml +24 -0
  40. package/features/steps/components/probe.target/operations/compute.js +7 -0
  41. package/features/steps/components/probe.target/operations/count.js +9 -0
  42. package/features/steps/config.ts +20 -0
  43. package/features/steps/tsconfig.json +9 -0
  44. package/features/ui.feature +85 -0
  45. package/package.json +31 -0
  46. package/readme.md +141 -0
  47. package/schemas/annotation.cos.yaml +16 -0
  48. package/schemas/declaration.cos.yaml +2 -0
  49. package/source/Composition.ts +61 -0
  50. package/source/Explorer.ts +16 -0
  51. package/source/Factory.ts +119 -0
  52. package/source/Reporter.ts +210 -0
  53. package/source/Tenant.ts +47 -0
  54. package/source/UI.ts +197 -0
  55. package/source/annotation.ts +83 -0
  56. package/source/const.ts +32 -0
  57. package/source/describe.ts +68 -0
  58. package/source/extension.ts +45 -0
  59. package/source/index.ts +7 -0
  60. package/source/keys.ts +0 -0
  61. package/source/manifest.ts +19 -0
  62. package/source/model.ts +82 -0
  63. package/source/sample.ts +49 -0
  64. package/source/schemas.ts +10 -0
  65. package/transpiled/Composition.d.ts +21 -0
  66. package/transpiled/Composition.js +48 -0
  67. package/transpiled/Composition.js.map +1 -0
  68. package/transpiled/Explorer.d.ts +9 -0
  69. package/transpiled/Explorer.js +19 -0
  70. package/transpiled/Explorer.js.map +1 -0
  71. package/transpiled/Factory.d.ts +22 -0
  72. package/transpiled/Factory.js +93 -0
  73. package/transpiled/Factory.js.map +1 -0
  74. package/transpiled/Reporter.d.ts +44 -0
  75. package/transpiled/Reporter.js +187 -0
  76. package/transpiled/Reporter.js.map +1 -0
  77. package/transpiled/Tenant.d.ts +18 -0
  78. package/transpiled/Tenant.js +40 -0
  79. package/transpiled/Tenant.js.map +1 -0
  80. package/transpiled/UI.d.ts +25 -0
  81. package/transpiled/UI.js +186 -0
  82. package/transpiled/UI.js.map +1 -0
  83. package/transpiled/annotation.d.ts +40 -0
  84. package/transpiled/annotation.js +46 -0
  85. package/transpiled/annotation.js.map +1 -0
  86. package/transpiled/const.d.ts +23 -0
  87. package/transpiled/const.js +27 -0
  88. package/transpiled/const.js.map +1 -0
  89. package/transpiled/contracts.d.ts +51 -0
  90. package/transpiled/contracts.js +53 -0
  91. package/transpiled/contracts.js.map +1 -0
  92. package/transpiled/describe.d.ts +9 -0
  93. package/transpiled/describe.js +64 -0
  94. package/transpiled/describe.js.map +1 -0
  95. package/transpiled/extension.d.ts +11 -0
  96. package/transpiled/extension.js +64 -0
  97. package/transpiled/extension.js.map +1 -0
  98. package/transpiled/index.d.ts +6 -0
  99. package/transpiled/index.js +25 -0
  100. package/transpiled/index.js.map +1 -0
  101. package/transpiled/keys.d.ts +9 -0
  102. package/transpiled/keys.js +0 -0
  103. package/transpiled/keys.js.map +1 -0
  104. package/transpiled/manifest.d.ts +9 -0
  105. package/transpiled/manifest.js +43 -0
  106. package/transpiled/manifest.js.map +1 -0
  107. package/transpiled/model.d.ts +71 -0
  108. package/transpiled/model.js +3 -0
  109. package/transpiled/model.js.map +1 -0
  110. package/transpiled/sample.d.ts +7 -0
  111. package/transpiled/sample.js +44 -0
  112. package/transpiled/sample.js.map +1 -0
  113. package/transpiled/schemas.d.ts +4 -0
  114. package/transpiled/schemas.js +13 -0
  115. package/transpiled/schemas.js.map +1 -0
  116. package/tsconfig.json +12 -0
  117. package/ui/dist/_app/env.js +1 -0
  118. package/ui/dist/_app/immutable/assets/0.fg2cP6bO.css +2 -0
  119. package/ui/dist/_app/immutable/assets/2.BghF56l_.css +1 -0
  120. package/ui/dist/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
  121. package/ui/dist/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
  122. package/ui/dist/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
  123. package/ui/dist/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
  124. package/ui/dist/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
  125. package/ui/dist/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
  126. package/ui/dist/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
  127. package/ui/dist/_app/immutable/chunks/Bjy-W4x2.js +81 -0
  128. package/ui/dist/_app/immutable/chunks/CIvRtvVA.js +1 -0
  129. package/ui/dist/_app/immutable/chunks/D1jNriZ8.js +1 -0
  130. package/ui/dist/_app/immutable/chunks/DINmginf.js +1 -0
  131. package/ui/dist/_app/immutable/chunks/DKZCAkrF.js +1 -0
  132. package/ui/dist/_app/immutable/chunks/DLG2JvyF.js +6 -0
  133. package/ui/dist/_app/immutable/chunks/DSi-FjwI.js +2 -0
  134. package/ui/dist/_app/immutable/chunks/DdoZuyU1.js +1 -0
  135. package/ui/dist/_app/immutable/chunks/O9IjE_TH.js +1 -0
  136. package/ui/dist/_app/immutable/chunks/PUjOYRcs.js +3 -0
  137. package/ui/dist/_app/immutable/chunks/QKD4Mgck.js +5 -0
  138. package/ui/dist/_app/immutable/chunks/Ux-r8C9U.js +1 -0
  139. package/ui/dist/_app/immutable/chunks/xihTtKlq.js +1 -0
  140. package/ui/dist/_app/immutable/entry/app.CTY34L35.js +2 -0
  141. package/ui/dist/_app/immutable/entry/start.Bq2Gn1Ax.js +1 -0
  142. package/ui/dist/_app/immutable/nodes/0.lOXtBRjQ.js +1 -0
  143. package/ui/dist/_app/immutable/nodes/1.r8ESZdgM.js +1 -0
  144. package/ui/dist/_app/immutable/nodes/2.DuVj0loz.js +8 -0
  145. package/ui/dist/_app/immutable/nodes/3.DhdkQ2Q1.js +1 -0
  146. package/ui/dist/_app/immutable/nodes/4.DUAEZdRr.js +1 -0
  147. package/ui/dist/_app/immutable/nodes/5.CIVn_xgq.js +1 -0
  148. package/ui/dist/_app/version.json +1 -0
  149. package/ui/dist/index.html +48 -0
  150. package/ui/dist/robots.txt +3 -0
@@ -0,0 +1,75 @@
1
+ Feature: Payload samples
2
+
3
+ Samples are real user data, so they are captured only when the context and the
4
+ component both allow it. Either level can veto.
5
+
6
+ Scenario: Nothing is captured by default
7
+ When the `probe.target.compute` is called with:
8
+ """yaml
9
+ input:
10
+ a: 4
11
+ b: 5
12
+ """
13
+ Then the map contains an edge:
14
+ """yaml
15
+ dst:
16
+ namespace: probe
17
+ component: target
18
+ operation: compute
19
+ """
20
+ And the map contains no edge:
21
+ """yaml
22
+ dst:
23
+ namespace: probe
24
+ component: target
25
+ operation: compute
26
+ sample:
27
+ input:
28
+ a: 4
29
+ """
30
+
31
+ @samples
32
+ Scenario: The context enables capturing
33
+ When the `probe.target.compute` is called with:
34
+ """yaml
35
+ input:
36
+ a: 4
37
+ b: 5
38
+ """
39
+ Then the map contains an edge:
40
+ """yaml
41
+ dst:
42
+ namespace: probe
43
+ component: target
44
+ operation: compute
45
+ sample:
46
+ outcome: ok
47
+ input:
48
+ a: 4
49
+ b: 5
50
+ """
51
+
52
+ @samples
53
+ Scenario: A component vetoes capturing for itself
54
+ When the `probe.discreet.ping` is called with:
55
+ """yaml
56
+ input:
57
+ a: 7
58
+ """
59
+ Then the map contains an edge:
60
+ """yaml
61
+ dst:
62
+ namespace: probe
63
+ component: discreet
64
+ operation: ping
65
+ """
66
+ And the map contains no edge:
67
+ """yaml
68
+ dst:
69
+ namespace: probe
70
+ component: discreet
71
+ operation: ping
72
+ sample:
73
+ input:
74
+ a: 7
75
+ """
@@ -0,0 +1,37 @@
1
+ @shutdown
2
+ Feature: Graceful shutdown
3
+
4
+ A component reports on a timer, so whatever it observed since the last report
5
+ would be lost on a deploy. Stopping flushes it instead: the collector drains
6
+ its buffer while its remotes are still connected.
7
+
8
+ This suite runs with a flush period long enough that the timer cannot fire,
9
+ so nothing here reaches the map unless the shutdown does it.
10
+
11
+ Scenario: Observations are flushed on shutdown
12
+ When the `probe.source.relay` is called with:
13
+ """yaml
14
+ input:
15
+ a: 2
16
+ b: 3
17
+ """
18
+ And the components are stopped
19
+ Then the map contains an edge:
20
+ """yaml
21
+ src:
22
+ namespace: probe
23
+ component: source
24
+ operation: relay
25
+ dst:
26
+ namespace: probe
27
+ component: target
28
+ operation: compute
29
+ """
30
+
31
+ Scenario: Component descriptions are flushed on shutdown
32
+ When the components are stopped
33
+ Then the map contains a node:
34
+ """yaml
35
+ namespace: probe
36
+ component: target
37
+ """
@@ -0,0 +1 @@
1
+ export const hashed = true
@@ -0,0 +1,6 @@
1
+ <!doctype html>
2
+ <!-- stands in for what `ui` builds; the scenarios are about the server, not the page -->
3
+ <html lang="en">
4
+ <head><title>fixture</title></head>
5
+ <body>the page</body>
6
+ </html>
@@ -0,0 +1,151 @@
1
+ import assert from 'node:assert'
2
+ import { resolve } from 'node:path'
3
+ import { readdirSync } from 'node:fs'
4
+ import { setTimeout } from 'node:timers/promises'
5
+ import { MongoClient } from 'mongodb'
6
+ import { after, before, binding, then, when } from 'cucumber-tsflow'
7
+ import { parse } from '@toa.io/yaml'
8
+ import { match } from '@toa.io/generic'
9
+ import * as boot from '@toa.io/boot'
10
+ import * as stage from '@toa.io/userland/stage'
11
+ import { Factory } from '../../source'
12
+ import type { Component, Connector, Request } from '@toa.io/core'
13
+
14
+ @binding()
15
+ export class Map {
16
+ private service: Connector | null = null
17
+ private composition: Connector | null = null
18
+ private remotes: Record<string, Component> = {}
19
+
20
+ @when('the `{word}` is called with:')
21
+ public async call (endpoint: string, yaml: string): Promise<void> {
22
+ const request = parse<Request>(yaml)
23
+ const [operation, component, namespace = 'default'] = endpoint.split('.').reverse()
24
+
25
+ // an operation may throw, and that is exactly what one of the scenarios is about
26
+ await this.invoke(`${namespace}.${component}`, operation, request).catch(() => undefined)
27
+ }
28
+
29
+ @then('the map contains a node:')
30
+ public async node (yaml: string): Promise<void> {
31
+ await this.eventually('introspection.nodes', parse(yaml))
32
+ }
33
+
34
+ @then('the map contains an edge:')
35
+ public async edge (yaml: string): Promise<void> {
36
+ await this.eventually('introspection.edges', parse(yaml))
37
+ }
38
+
39
+ @then('the map contains no node:')
40
+ public async noNode (yaml: string): Promise<void> {
41
+ await this.never('introspection.nodes', parse(yaml))
42
+ }
43
+
44
+ @then('the map contains no edge:')
45
+ public async noEdge (yaml: string): Promise<void> {
46
+ await this.never('introspection.edges', parse(yaml))
47
+ }
48
+
49
+ @when('the components are stopped')
50
+ public async stop (): Promise<void> {
51
+ await this.composition?.disconnect()
52
+
53
+ this.composition = null
54
+ }
55
+
56
+ @before('not @ui')
57
+ public async run (): Promise<void> {
58
+ await clean()
59
+
60
+ this.service = new Factory(boot).service()!
61
+
62
+ await this.service.connect()
63
+
64
+ // booted here rather than through the stage, so that a scenario can stop it on its own
65
+ this.composition = await boot.composition(components())
66
+
67
+ await this.composition.connect()
68
+ }
69
+
70
+ @after('not @ui')
71
+ public async shutdown (): Promise<void> {
72
+ this.remotes = {}
73
+
74
+ await this.composition?.disconnect()
75
+ await stage.shutdown()
76
+ await this.service?.disconnect()
77
+
78
+ this.composition = null
79
+ this.service = null
80
+ }
81
+
82
+ private async invoke (id: string, operation: string, request: Request): Promise<unknown> {
83
+ this.remotes[id] ??= await stage.remote(id)
84
+
85
+ return await this.remotes[id].invoke(operation, request)
86
+ }
87
+
88
+ /** The map is eventually consistent: the collector buffers, flushes, and the task is queued. */
89
+ private async eventually (id: string, expected: object): Promise<void> {
90
+ const deadline = Date.now() + DEADLINE
91
+
92
+ let records: any[] = []
93
+
94
+ while (Date.now() < deadline) {
95
+ records = await this.list(id)
96
+
97
+ if (records.some((record) => match(record, expected)))
98
+ return
99
+
100
+ await setTimeout(POLL)
101
+ }
102
+
103
+ assert.fail(`No record in '${id}' matches\n${JSON.stringify(expected, null, 2)}\n\n` +
104
+ `Present:\n${JSON.stringify(records, null, 2)}`)
105
+ }
106
+
107
+ /** Absence has to outlive a flush, otherwise it only proves the map is slow. */
108
+ private async never (id: string, expected: object): Promise<void> {
109
+ await setTimeout(SETTLE)
110
+
111
+ const records = await this.list(id)
112
+ const found = records.find((record) => match(record, expected))
113
+
114
+ assert.equal(found, undefined,
115
+ `Unexpected record in '${id}':\n${JSON.stringify(found, null, 2)}`)
116
+ }
117
+
118
+ private async list (id: string): Promise<any[]> {
119
+ return await this.invoke(id, 'enumerate', { query: { limit: 100 } }) as any[]
120
+ }
121
+ }
122
+
123
+ async function clean (): Promise<void> {
124
+ const client = new MongoClient(URL)
125
+
126
+ await client.connect()
127
+
128
+ const db = client.db(DB)
129
+
130
+ await Promise.all([
131
+ db.collection('introspection_nodes').deleteMany({}),
132
+ db.collection('introspection_edges').deleteMany({})
133
+ ])
134
+
135
+ await client.close()
136
+ }
137
+
138
+ function components (): string[] {
139
+ const entries = readdirSync(ROOT, { withFileTypes: true })
140
+
141
+ return entries
142
+ .filter((entry) => entry.isDirectory())
143
+ .map((entry) => resolve(ROOT, entry.name))
144
+ }
145
+
146
+ const ROOT = resolve(__dirname, 'components')
147
+ const URL = 'mongodb://developer:secret@localhost:27017'
148
+ const DB = 'toa-dev'
149
+ const DEADLINE = 20_000
150
+ const POLL = 250
151
+ const SETTLE = 4_000
@@ -0,0 +1,88 @@
1
+ import assert from 'node:assert'
2
+ import * as http from 'node:http'
3
+ import { resolve } from 'node:path'
4
+ import { after, binding, given, then, when } from 'cucumber-tsflow'
5
+ import { UI } from '../../source/UI'
6
+ import { UI_PORT } from '../../source/const'
7
+
8
+ /**
9
+ * The scenarios are about the server, not about the page: it is pointed at a fixture
10
+ * directory, so that running them never needs a UI build.
11
+ */
12
+ @binding()
13
+ export class Site {
14
+ private server: UI | null = null
15
+ private response: Response | null = null
16
+
17
+ @given('the UI is published')
18
+ public async publish (): Promise<void> {
19
+ this.server = new UI(UI_PORT, resolve(__dirname, '..', 'site'))
20
+
21
+ await this.server.connect()
22
+ }
23
+
24
+ @when('{string} is requested')
25
+ public async request (path: string): Promise<void> {
26
+ this.response = await get(path)
27
+ }
28
+
29
+ @then('the status is {int}')
30
+ public statusIs (status: number): void {
31
+ assert.equal(this.response?.status, status)
32
+ }
33
+
34
+ @then('the body contains {string}')
35
+ public bodyContains (text: string): void {
36
+ assert.ok(this.response?.body.includes(text),
37
+ `Expected the body to contain '${text}', got '${this.response?.body}'`)
38
+ }
39
+
40
+ @then('the body is empty')
41
+ public bodyIsEmpty (): void {
42
+ assert.equal(this.response?.body, '')
43
+ }
44
+
45
+ @then('the {string} header is {string}')
46
+ public headerIs (name: string, value: string): void {
47
+ assert.equal(this.response?.headers[name], value)
48
+ }
49
+
50
+ @after()
51
+ public async shutdown (): Promise<void> {
52
+ await this.server?.disconnect()
53
+
54
+ this.server = null
55
+ this.response = null
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Raw `http`, not `fetch`: the URL parser normalizes `%2e%2e` away, and one of the
61
+ * scenarios is about exactly that segment reaching the server.
62
+ */
63
+ async function get (path: string): Promise<Response> {
64
+ return await new Promise((resolve, reject) => {
65
+ // `agent: false` — a keep-alive socket outlives the scenario that opened it
66
+ const request = http.get({ port: UI_PORT, path, agent: false }, (response) => {
67
+ let body = ''
68
+
69
+ response.setEncoding('utf8')
70
+ response.on('data', (chunk: string) => (body += chunk))
71
+ response.on('end', () => {
72
+ resolve({
73
+ status: response.statusCode!,
74
+ headers: response.headers as Record<string, string>,
75
+ body
76
+ })
77
+ })
78
+ })
79
+
80
+ request.on('error', reject)
81
+ })
82
+ }
83
+
84
+ interface Response {
85
+ status: number
86
+ headers: Record<string, string>
87
+ body: string
88
+ }
@@ -0,0 +1,12 @@
1
+ namespace: probe
2
+ name: discreet
3
+
4
+ # on the map, but its payloads are never captured
5
+ introspection:
6
+ samples: false
7
+
8
+ operations:
9
+ ping:
10
+ input:
11
+ a: number
12
+ output: number
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ async function computation (input) {
4
+ return input.a
5
+ }
6
+
7
+ exports.computation = computation
@@ -0,0 +1,11 @@
1
+ namespace: probe
2
+ name: quiet
3
+
4
+ # opted out entirely: neither described nor instrumented
5
+ introspection: false
6
+
7
+ operations:
8
+ ping:
9
+ input:
10
+ a: number
11
+ output: number
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ async function computation (input) {
4
+ return input.a
5
+ }
6
+
7
+ exports.computation = computation
@@ -0,0 +1,27 @@
1
+ namespace: probe
2
+ name: source
3
+
4
+ entity:
5
+ associated: true
6
+ storage: null
7
+ schema:
8
+ value:
9
+ type: integer
10
+ default: 0
11
+ secret: string
12
+
13
+ operations:
14
+ transit:
15
+ concurrency: retry
16
+ input:
17
+ value: .
18
+ secret: .
19
+ relay:
20
+ input:
21
+ a: number
22
+ b: number
23
+ output: number
24
+ crash: {}
25
+ fail:
26
+ errors:
27
+ - NOPE
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function computation (input) {
4
+ throw new Error('crashed on purpose')
5
+ }
6
+
7
+ exports.computation = computation
@@ -0,0 +1,12 @@
1
+ 'use strict'
2
+
3
+ function computation (input) {
4
+ return NOPE
5
+ }
6
+
7
+ const NOPE = Object.create(Error.prototype, {
8
+ code: { value: 'NOPE' },
9
+ message: { value: 'declined on purpose', enumerable: true }
10
+ })
11
+
12
+ exports.computation = computation
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ async function computation (input, context) {
4
+ return context.remote.probe.target.compute({ input })
5
+ }
6
+
7
+ exports.computation = computation
@@ -0,0 +1,24 @@
1
+ namespace: probe
2
+ name: target
3
+
4
+ entity:
5
+ storage: null
6
+ schema:
7
+ counted:
8
+ type: integer
9
+ default: 0
10
+
11
+ operations:
12
+ compute:
13
+ input:
14
+ a: number
15
+ b: number
16
+ output: number
17
+ count:
18
+ concurrency: retry
19
+ input:
20
+ type: object
21
+ additionalProperties: true
22
+
23
+ receivers:
24
+ probe.source.created: count
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ async function computation (input) {
4
+ return input.a + input.b
5
+ }
6
+
7
+ exports.computation = computation
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ function transition (input, object) {
4
+ object.counted++
5
+
6
+ return object
7
+ }
8
+
9
+ exports.transition = transition
@@ -0,0 +1,20 @@
1
+ import { setDefaultTimeout } from '@cucumber/cucumber'
2
+ import { encode } from '@toa.io/generic'
3
+
4
+ process.env.TOA_DEV = '1'
5
+
6
+ /*
7
+ * The extension factory reads the environment once, and the bootloader caches
8
+ * factories per process — so the context level of the annotation is fixed for
9
+ * a whole run. `npm run features` therefore makes a pass per configuration.
10
+ */
11
+ process.env.TOA_INTROSPECTION = encode({
12
+ samples: process.env.TOA_INTROSPECTION_SAMPLES === '1',
13
+ interval: Number(process.env.TOA_INTROSPECTION_INTERVAL ?? 1),
14
+ threshold: 64,
15
+
16
+ // `ui.feature` starts its own server; the explorer must not take the port first
17
+ ui: false
18
+ })
19
+
20
+ setDefaultTimeout(60 * 1000)
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "noEmit": true,
5
+ "moduleResolution": "node",
6
+ "experimentalDecorators": true,
7
+ "emitDecoratorMetadata": true
8
+ }
9
+ }
@@ -0,0 +1,85 @@
1
+ @ui
2
+ Feature: Publishing the UI
3
+
4
+ The explorer serves the UI on its own port, out of the directory `ui` builds. The
5
+ mount path is baked into the bundle and forwarded by the ingress, so the server
6
+ routes relative to it.
7
+
8
+ Scenario: The root is the way in
9
+ Given the UI is published
10
+ When "/" is requested
11
+ Then the status is 302
12
+ And the "location" header is "/.introspection/"
13
+
14
+ Scenario: The UI answers at its mount path
15
+ Given the UI is published
16
+ When "/.introspection" is requested
17
+ Then the status is 200
18
+ And the body contains "the page"
19
+
20
+ Scenario: A trailing slash is the same resource
21
+ Given the UI is published
22
+ When "/.introspection/" is requested
23
+ Then the status is 200
24
+ And the body contains "the page"
25
+
26
+ Scenario: A query string does not change the resource
27
+ Given the UI is published
28
+ When "/.introspection?tab=edges" is requested
29
+ Then the status is 200
30
+ And the body contains "the page"
31
+
32
+ Scenario: An unknown route is the page
33
+ The client router knows what routes there are; this server does not.
34
+
35
+ Given the UI is published
36
+ When "/.introspection/whatever" is requested
37
+ Then the status is 200
38
+ And the body contains "the page"
39
+
40
+ Scenario: A route that looks like a file is still a route
41
+ A component is named `namespace.component`, so the address of one carries a dot and
42
+ an extension is no way to tell an asset from a route.
43
+
44
+ Given the UI is published
45
+ When "/.introspection/identity.passkeys/" is requested
46
+ Then the status is 200
47
+ And the body contains "the page"
48
+
49
+ Scenario: And with no trailing slash to give it away
50
+ Given the UI is published
51
+ When "/.introspection/identity.passkeys" is requested
52
+ Then the status is 200
53
+ And the body contains "the page"
54
+
55
+ Scenario: A hashed asset is served forever
56
+ Given the UI is published
57
+ When "/.introspection/_app/immutable/asset.js" is requested
58
+ Then the status is 200
59
+ And the "content-type" header is "text/javascript; charset=utf-8"
60
+ And the "cache-control" header is "public, max-age=31536000, immutable"
61
+
62
+ Scenario: The page itself is never cached
63
+ Given the UI is published
64
+ When "/.introspection/" is requested
65
+ Then the "cache-control" header is "no-cache"
66
+
67
+ Scenario: A missing asset is missing
68
+ Given the UI is published
69
+ When "/.introspection/_app/immutable/absent.js" is requested
70
+ Then the status is 404
71
+
72
+ Scenario: The path of the map API is not served by the UI
73
+ Given the UI is published
74
+ When "/introspection/nodes/" is requested
75
+ Then the status is 404
76
+
77
+ Scenario: A path that only looks like the mount path is absent
78
+ Given the UI is published
79
+ When "/.introspectionable" is requested
80
+ Then the status is 404
81
+
82
+ Scenario: Nothing outside the site is reachable
83
+ Given the UI is published
84
+ When "/.introspection/%2e%2e/%2e%2e/package.json" is requested
85
+ Then the status is 404
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@toa.io/extensions.introspection",
3
+ "version": "1.0.0-alpha.262",
4
+ "description": "Toa Introspection",
5
+ "author": "temich <tema.gurtovoy@gmail.com>",
6
+ "homepage": "https://github.com/toa-io/toa#readme",
7
+ "main": "transpiled/index.js",
8
+ "types": "transpiled/index.d.ts",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/toa-io/toa.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/toa-io/toa/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "dependencies": {
20
+ "@toa.io/core": "1.0.0-alpha.262",
21
+ "@toa.io/generic": "1.0.0-alpha.254",
22
+ "@toa.io/schemas": "1.0.0-alpha.254",
23
+ "openspan": "1.0.0-alpha.254"
24
+ },
25
+ "scripts": {
26
+ "transpile": "npx tsc && npx tsc -p ./components/introspection.nodes && npx tsc -p ./components/introspection.edges",
27
+ "build:ui": "npm --prefix ui ci && npm --prefix ui run build",
28
+ "prepack": "npm run build:ui",
29
+ "features": "npx cucumber-js --tags 'not @samples and not @shutdown' && TOA_INTROSPECTION_SAMPLES=1 npx cucumber-js --tags @samples && TOA_INTROSPECTION_INTERVAL=3600 npx cucumber-js --tags @shutdown"
30
+ }
31
+ }