@toa.io/extensions.introspection 1.0.0-alpha.284 → 1.0.0-alpha.286

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 (94) hide show
  1. package/components/introspection.edges/manifest.toa.yaml +28 -30
  2. package/components/introspection.edges/migrations/0001-system-properties.yaml +14 -0
  3. package/components/introspection.edges/operations/merge.js +1 -1
  4. package/components/introspection.edges/source/merge.ts +3 -5
  5. package/components/introspection.edges/tsconfig.tsbuildinfo +1 -1
  6. package/components/introspection.edges/types/toa.d.ts +5 -5
  7. package/components/introspection.nodes/manifest.toa.yaml +16 -18
  8. package/components/introspection.nodes/migrations/0001-system-properties.yaml +14 -0
  9. package/components/introspection.nodes/operations/merge.js +1 -1
  10. package/components/introspection.nodes/source/merge.ts +2 -3
  11. package/components/introspection.nodes/tsconfig.tsbuildinfo +1 -1
  12. package/components/introspection.nodes/types/toa.d.ts +5 -5
  13. package/cucumber.mjs +3 -0
  14. package/features/site/index.html +6 -2
  15. package/features/steps/Map.ts +36 -25
  16. package/features/steps/UI.ts +20 -13
  17. package/features/steps/components/probe.discreet/operations/ping.js +1 -1
  18. package/features/steps/components/probe.quiet/operations/ping.js +1 -1
  19. package/features/steps/components/probe.source/manifest.toa.yaml +7 -8
  20. package/features/steps/components/probe.source/operations/crash.js +1 -1
  21. package/features/steps/components/probe.source/operations/fail.js +1 -1
  22. package/features/steps/components/probe.source/operations/relay.js +1 -1
  23. package/features/steps/components/probe.target/manifest.toa.yaml +5 -6
  24. package/features/steps/components/probe.target/operations/compute.js +1 -1
  25. package/features/steps/components/probe.target/operations/count.js +1 -1
  26. package/package.json +7 -7
  27. package/readme.md +7 -7
  28. package/schemas/annotation.cos.yaml +1 -1
  29. package/source/Composition.ts +6 -7
  30. package/source/Explorer.ts +2 -2
  31. package/source/Factory.ts +63 -29
  32. package/source/Reporter.ts +37 -38
  33. package/source/Tenant.ts +6 -7
  34. package/source/UI.ts +33 -32
  35. package/source/annotation.ts +30 -26
  36. package/source/const.ts +12 -0
  37. package/source/describe.ts +22 -16
  38. package/source/extension.ts +10 -6
  39. package/source/keys.ts +0 -0
  40. package/source/manifest.ts +2 -3
  41. package/source/model.ts +1 -1
  42. package/source/sample.ts +6 -8
  43. package/source/schemas.ts +4 -2
  44. package/transpiled/Composition.js.map +1 -1
  45. package/transpiled/Factory.d.ts +2 -1
  46. package/transpiled/Factory.js +27 -6
  47. package/transpiled/Factory.js.map +1 -1
  48. package/transpiled/Reporter.js +4 -1
  49. package/transpiled/Reporter.js.map +1 -1
  50. package/transpiled/Tenant.d.ts +1 -1
  51. package/transpiled/Tenant.js +1 -1
  52. package/transpiled/Tenant.js.map +1 -1
  53. package/transpiled/UI.js +2 -1
  54. package/transpiled/UI.js.map +1 -1
  55. package/transpiled/annotation.js.map +1 -1
  56. package/transpiled/const.d.ts +7 -0
  57. package/transpiled/const.js +10 -0
  58. package/transpiled/const.js.map +1 -1
  59. package/transpiled/describe.js +7 -7
  60. package/transpiled/describe.js.map +1 -1
  61. package/transpiled/extension.d.ts +3 -0
  62. package/transpiled/extension.js +6 -1
  63. package/transpiled/extension.js.map +1 -1
  64. package/transpiled/keys.js.map +1 -1
  65. package/transpiled/manifest.js.map +1 -1
  66. package/transpiled/model.d.ts +1 -1
  67. package/transpiled/sample.js.map +1 -1
  68. package/transpiled/schemas.js.map +1 -1
  69. package/tsconfig.tsbuildinfo +1 -1
  70. package/ui/dist/_app/immutable/chunks/{DZPtpe9B.js → BLEzg-4t.js} +1 -1
  71. package/ui/dist/_app/immutable/chunks/BPZdECZt.js +1 -0
  72. package/ui/dist/_app/immutable/chunks/{DAr41bMZ.js → BeZwDLIn.js} +2 -2
  73. package/ui/dist/_app/immutable/chunks/DUO28Hbn.js +1 -0
  74. package/ui/dist/_app/immutable/chunks/{Cw2o6qJ6.js → DYKePAZ9.js} +3 -3
  75. package/ui/dist/_app/immutable/chunks/{BiLog9K-.js → Di1YrEXu.js} +1 -1
  76. package/ui/dist/_app/immutable/chunks/{DrodQShZ.js → Dkg6on5h.js} +1 -1
  77. package/ui/dist/_app/immutable/chunks/{Dlhv3b0H.js → UH-Xf7ZK.js} +1 -1
  78. package/ui/dist/_app/immutable/chunks/{LxgdVMjV.js → Yvrddvsp.js} +1 -1
  79. package/ui/dist/_app/immutable/entry/{app.BZe_kjnw.js → app.T7brwbZX.js} +2 -2
  80. package/ui/dist/_app/immutable/entry/start.C8vo__MC.js +1 -0
  81. package/ui/dist/_app/immutable/nodes/{0.Bp_WDf60.js → 0.UFiNhGhx.js} +1 -1
  82. package/ui/dist/_app/immutable/nodes/{1.BhTvtURh.js → 1.t--TxMm7.js} +1 -1
  83. package/ui/dist/_app/immutable/nodes/{2.DXBctzq4.js → 2.CAV8jmGo.js} +1 -1
  84. package/ui/dist/_app/immutable/nodes/3.Br1yDs7H.js +1 -0
  85. package/ui/dist/_app/immutable/nodes/4.Ck0KmPgK.js +1 -0
  86. package/ui/dist/_app/immutable/nodes/{5.Cvt5-NZz.js → 5.qVRn66ML.js} +1 -1
  87. package/ui/dist/_app/version.json +1 -1
  88. package/ui/dist/index.html +30 -25
  89. package/CHANGELOG.md +0 -426
  90. package/ui/dist/_app/immutable/chunks/D8JS8JwD.js +0 -1
  91. package/ui/dist/_app/immutable/chunks/ay3E8ALF.js +0 -1
  92. package/ui/dist/_app/immutable/entry/start.CeI0OHwu.js +0 -1
  93. package/ui/dist/_app/immutable/nodes/3.DJPUlbK5.js +0 -1
  94. package/ui/dist/_app/immutable/nodes/4.DTcjak2q.js +0 -1
@@ -11,7 +11,8 @@ import * as boot from '@toa.io/boot'
11
11
  import { Locator } from '@toa.io/core'
12
12
  import * as stage from '@toa.io/userland/stage'
13
13
  import { Factory } from '../../source/index.js'
14
- import type { Component, Connector, Request } from '@toa.io/core'
14
+ import type { Component, Connector } from '@toa.io/core'
15
+ import type { Request } from '@toa.io/core/types'
15
16
 
16
17
  const { after, before, binding, then, when } = tsflow
17
18
 
@@ -23,12 +24,14 @@ export class Map {
23
24
  private unidentified: Component | null = null
24
25
 
25
26
  @when('the `{word}` is called with:')
26
- public async call (endpoint: string, yaml: string): Promise<void> {
27
+ public async call(endpoint: string, yaml: string): Promise<void> {
27
28
  const request = parse(yaml) as Request
28
29
  const [operation, component, namespace = 'default'] = endpoint.split('.').reverse()
29
30
 
30
31
  // an operation may throw, and that is exactly what one of the scenarios is about
31
- await this.invoke(`${namespace}.${component}`, operation, request).catch(() => undefined)
32
+ await this.invoke(`${namespace}.${component}`, operation, request).catch(
33
+ () => undefined
34
+ )
32
35
  }
33
36
 
34
37
  /**
@@ -36,7 +39,7 @@ export class Map {
36
39
  * nobody is made past it, straight through a remote of its own.
37
40
  */
38
41
  @when('the `{word}` is called by an unidentified caller with:')
39
- public async callUnidentified (endpoint: string, yaml: string): Promise<void> {
42
+ public async callUnidentified(endpoint: string, yaml: string): Promise<void> {
40
43
  const request = parse(yaml) as Request
41
44
  const [operation, component, namespace = 'default'] = endpoint.split('.').reverse()
42
45
 
@@ -47,34 +50,34 @@ export class Map {
47
50
  }
48
51
 
49
52
  @then('the map contains a node:')
50
- public async node (yaml: string): Promise<void> {
53
+ public async node(yaml: string): Promise<void> {
51
54
  await this.eventually('introspection.nodes', parse(yaml) as object)
52
55
  }
53
56
 
54
57
  @then('the map contains an edge:')
55
- public async edge (yaml: string): Promise<void> {
58
+ public async edge(yaml: string): Promise<void> {
56
59
  await this.eventually('introspection.edges', parse(yaml) as object)
57
60
  }
58
61
 
59
62
  @then('the map contains no node:')
60
- public async noNode (yaml: string): Promise<void> {
63
+ public async noNode(yaml: string): Promise<void> {
61
64
  await this.never('introspection.nodes', parse(yaml) as object)
62
65
  }
63
66
 
64
67
  @then('the map contains no edge:')
65
- public async noEdge (yaml: string): Promise<void> {
68
+ public async noEdge(yaml: string): Promise<void> {
66
69
  await this.never('introspection.edges', parse(yaml) as object)
67
70
  }
68
71
 
69
72
  @when('the components are stopped')
70
- public async stop (): Promise<void> {
73
+ public async stop(): Promise<void> {
71
74
  await this.composition?.disconnect()
72
75
 
73
76
  this.composition = null
74
77
  }
75
78
 
76
79
  @before('not @ui')
77
- public async run (): Promise<void> {
80
+ public async run(): Promise<void> {
78
81
  await clean()
79
82
 
80
83
  this.service = (await new Factory(boot.host()).service())!
@@ -88,7 +91,7 @@ export class Map {
88
91
  }
89
92
 
90
93
  @after('not @ui')
91
- public async shutdown (): Promise<void> {
94
+ public async shutdown(): Promise<void> {
92
95
  this.remotes = {}
93
96
 
94
97
  await this.unidentified?.disconnect()
@@ -101,14 +104,18 @@ export class Map {
101
104
  this.unidentified = null
102
105
  }
103
106
 
104
- private async invoke (id: string, operation: string, request: Request): Promise<unknown> {
107
+ private async invoke(
108
+ id: string,
109
+ operation: string,
110
+ request: Request
111
+ ): Promise<unknown> {
105
112
  this.remotes[id] ??= await stage.remote(id)
106
113
 
107
114
  return await this.remotes[id].invoke(operation, request)
108
115
  }
109
116
 
110
117
  /** The map is eventually consistent: the collector buffers, flushes, and the task is queued. */
111
- private async eventually (id: string, expected: object): Promise<void> {
118
+ private async eventually(id: string, expected: object): Promise<void> {
112
119
  const deadline = Date.now() + DEADLINE
113
120
 
114
121
  let records: any[] = []
@@ -116,33 +123,37 @@ export class Map {
116
123
  while (Date.now() < deadline) {
117
124
  records = await this.list(id)
118
125
 
119
- if (records.some((record) => match(record, expected)))
120
- return
126
+ if (records.some((record) => match(record, expected))) return
121
127
 
122
128
  await setTimeout(POLL)
123
129
  }
124
130
 
125
- assert.fail(`No record in '${id}' matches\n${JSON.stringify(expected, null, 2)}\n\n` +
126
- `Present:\n${JSON.stringify(records, null, 2)}`)
131
+ assert.fail(
132
+ `No record in '${id}' matches\n${JSON.stringify(expected, null, 2)}\n\n` +
133
+ `Present:\n${JSON.stringify(records, null, 2)}`
134
+ )
127
135
  }
128
136
 
129
137
  /** Absence has to outlive a flush, otherwise it only proves the map is slow. */
130
- private async never (id: string, expected: object): Promise<void> {
138
+ private async never(id: string, expected: object): Promise<void> {
131
139
  await setTimeout(SETTLE)
132
140
 
133
141
  const records = await this.list(id)
134
142
  const found = records.find((record) => match(record, expected))
135
143
 
136
- assert.equal(found, undefined,
137
- `Unexpected record in '${id}':\n${JSON.stringify(found, null, 2)}`)
144
+ assert.equal(
145
+ found,
146
+ undefined,
147
+ `Unexpected record in '${id}':\n${JSON.stringify(found, null, 2)}`
148
+ )
138
149
  }
139
150
 
140
- private async list (id: string): Promise<any[]> {
141
- return await this.invoke(id, 'enumerate', { query: { limit: 100 } }) as any[]
151
+ private async list(id: string): Promise<any[]> {
152
+ return (await this.invoke(id, 'enumerate', { query: { limit: 100 } })) as any[]
142
153
  }
143
154
  }
144
155
 
145
- async function clean (): Promise<void> {
156
+ async function clean(): Promise<void> {
146
157
  const client = new MongoClient(URL)
147
158
 
148
159
  await client.connect()
@@ -157,7 +168,7 @@ async function clean (): Promise<void> {
157
168
  await client.close()
158
169
  }
159
170
 
160
- function components (): string[] {
171
+ function components(): string[] {
161
172
  const entries = readdirSync(ROOT, { withFileTypes: true })
162
173
 
163
174
  return entries
@@ -166,7 +177,7 @@ function components (): string[] {
166
177
  }
167
178
 
168
179
  const ROOT = resolve(import.meta.dirname, 'components')
169
- const URL = 'mongodb://developer:secret@localhost:27017'
180
+ const URL = 'mongodb://developer:secret@localhost:31020'
170
181
  const DB = 'toa-dev'
171
182
  const DEADLINE = 20_000
172
183
  const POLL = 250
@@ -4,10 +4,15 @@ import { resolve } from 'node:path'
4
4
  import tsflow from 'cucumber-tsflow'
5
5
 
6
6
  import { UI } from '../../source/UI.js'
7
- import { UI_PORT } from '../../source/const.js'
8
7
 
9
8
  const { after, binding, given, then, when } = tsflow
10
9
 
10
+ /**
11
+ * Not `UI_PORT`, which a deployment serves the page on: an application built on Toa is served
12
+ * on this machine too, and its own explorer holds that one. See CONTRIBUTING.md.
13
+ */
14
+ const PORT = 31002
15
+
11
16
  /**
12
17
  * The scenarios are about the server, not about the page: it is pointed at a fixture
13
18
  * directory, so that running them never needs a UI build.
@@ -18,40 +23,42 @@ export class Site {
18
23
  private response: Response | null = null
19
24
 
20
25
  @given('the UI is published')
21
- public async publish (): Promise<void> {
22
- this.server = new UI(UI_PORT, resolve(import.meta.dirname, '..', 'site'))
26
+ public async publish(): Promise<void> {
27
+ this.server = new UI(PORT, resolve(import.meta.dirname, '..', 'site'))
23
28
 
24
29
  await this.server.connect()
25
30
  }
26
31
 
27
32
  @when('{string} is requested')
28
- public async request (path: string): Promise<void> {
33
+ public async request(path: string): Promise<void> {
29
34
  this.response = await get(path)
30
35
  }
31
36
 
32
37
  @then('the status is {int}')
33
- public statusIs (status: number): void {
38
+ public statusIs(status: number): void {
34
39
  assert.equal(this.response?.status, status)
35
40
  }
36
41
 
37
42
  @then('the body contains {string}')
38
- public bodyContains (text: string): void {
39
- assert.ok(this.response?.body.includes(text),
40
- `Expected the body to contain '${text}', got '${this.response?.body}'`)
43
+ public bodyContains(text: string): void {
44
+ assert.ok(
45
+ this.response?.body.includes(text),
46
+ `Expected the body to contain '${text}', got '${this.response?.body}'`
47
+ )
41
48
  }
42
49
 
43
50
  @then('the body is empty')
44
- public bodyIsEmpty (): void {
51
+ public bodyIsEmpty(): void {
45
52
  assert.equal(this.response?.body, '')
46
53
  }
47
54
 
48
55
  @then('the {string} header is {string}')
49
- public headerIs (name: string, value: string): void {
56
+ public headerIs(name: string, value: string): void {
50
57
  assert.equal(this.response?.headers[name], value)
51
58
  }
52
59
 
53
60
  @after()
54
- public async shutdown (): Promise<void> {
61
+ public async shutdown(): Promise<void> {
55
62
  await this.server?.disconnect()
56
63
 
57
64
  this.server = null
@@ -63,10 +70,10 @@ export class Site {
63
70
  * Raw `http`, not `fetch`: the URL parser normalizes `%2e%2e` away, and one of the
64
71
  * scenarios is about exactly that segment reaching the server.
65
72
  */
66
- async function get (path: string): Promise<Response> {
73
+ async function get(path: string): Promise<Response> {
67
74
  return await new Promise((resolve, reject) => {
68
75
  // `agent: false` — a keep-alive socket outlives the scenario that opened it
69
- const request = http.get({ port: UI_PORT, path, agent: false }, (response) => {
76
+ const request = http.get({ port: PORT, path, agent: false }, (response) => {
70
77
  let body = ''
71
78
 
72
79
  response.setEncoding('utf8')
@@ -1,3 +1,3 @@
1
- export async function computation (input) {
1
+ export async function computation(input) {
2
2
  return input.a
3
3
  }
@@ -1,3 +1,3 @@
1
- export async function computation (input) {
1
+ export async function computation(input) {
2
2
  return input.a
3
3
  }
@@ -4,14 +4,13 @@ name: source
4
4
  entity:
5
5
  associated: true
6
6
  storage: null
7
- schema:
8
- properties:
9
- value:
10
- type: integer
11
- default: 0
12
- secret:
13
- type: string
14
- type: object
7
+ properties:
8
+ value:
9
+ type: integer
10
+ secret:
11
+ type: string
12
+ blank:
13
+ value: 0
15
14
 
16
15
  operations:
17
16
  transit:
@@ -1,3 +1,3 @@
1
- export function computation (input) {
1
+ export function computation(input) {
2
2
  throw new Error('crashed on purpose')
3
3
  }
@@ -1,4 +1,4 @@
1
- export function computation (input) {
1
+ export function computation(input) {
2
2
  return NOPE
3
3
  }
4
4
 
@@ -1,3 +1,3 @@
1
- export async function computation (input, context) {
1
+ export async function computation(input, context) {
2
2
  return context.remote.probe.target.compute({ input })
3
3
  }
@@ -3,12 +3,11 @@ name: target
3
3
 
4
4
  entity:
5
5
  storage: null
6
- schema:
7
- properties:
8
- counted:
9
- type: integer
10
- default: 0
11
- type: object
6
+ properties:
7
+ counted:
8
+ type: integer
9
+ blank:
10
+ counted: 0
12
11
 
13
12
  operations:
14
13
  compute:
@@ -1,3 +1,3 @@
1
- export async function computation (input) {
1
+ export async function computation(input) {
2
2
  return input.a + input.b
3
3
  }
@@ -1,4 +1,4 @@
1
- export function transition (input, object) {
1
+ export function transition(input, object) {
2
2
  object.counted++
3
3
 
4
4
  return object
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.introspection",
3
- "version": "1.0.0-alpha.284",
3
+ "version": "1.0.0-alpha.286",
4
4
  "type": "module",
5
5
  "description": "Toa Introspection",
6
6
  "author": "temich <tema.gurtovoy@gmail.com>",
@@ -18,16 +18,16 @@
18
18
  "access": "public"
19
19
  },
20
20
  "dependencies": {
21
- "@toa.io/core": "1.0.0-alpha.284",
22
- "@toa.io/generic": "1.0.0-alpha.283",
23
- "@toa.io/schemas": "1.0.0-alpha.283",
24
- "openspan": "1.0.0-alpha.277"
21
+ "@toa.io/core": "1.0.0-alpha.286",
22
+ "@toa.io/generic": "1.0.0-alpha.286",
23
+ "@toa.io/schemas": "1.0.0-alpha.286",
24
+ "openspan": "1.0.0-alpha.286"
25
25
  },
26
26
  "scripts": {
27
27
  "transpile": "npx tsc && npx tsc -p ./components/introspection.nodes && npx tsc -p ./components/introspection.edges",
28
28
  "build:ui": "npm --prefix ui ci && npm --prefix ui run build",
29
29
  "prepack": "npm run build:ui",
30
- "features": "TSX_TSCONFIG_PATH=features/steps/tsconfig.json NODE_OPTIONS=--import=tsx cucumber-js --tags 'not @samples and not @shutdown' && TOA_INTROSPECTION_SAMPLES=1 TSX_TSCONFIG_PATH=features/steps/tsconfig.json NODE_OPTIONS=--import=tsx cucumber-js --tags @samples && TOA_INTROSPECTION_INTERVAL=3600 TSX_TSCONFIG_PATH=features/steps/tsconfig.json NODE_OPTIONS=--import=tsx cucumber-js --tags @shutdown"
30
+ "features": "TSX_TSCONFIG_PATH=features/steps/tsconfig.json NODE_OPTIONS=--import=tsx cucumber-js --tags 'not @skip and not @samples and not @shutdown' && TOA_INTROSPECTION_SAMPLES=1 TSX_TSCONFIG_PATH=features/steps/tsconfig.json NODE_OPTIONS=--import=tsx cucumber-js --tags 'not @skip and @samples' && TOA_INTROSPECTION_INTERVAL=3600 TSX_TSCONFIG_PATH=features/steps/tsconfig.json NODE_OPTIONS=--import=tsx cucumber-js --tags 'not @skip and @shutdown'"
31
31
  },
32
- "gitHead": "006755556879e91e8d68fa86753aa3b475bdf4bf"
32
+ "gitHead": "81f3d0688f17668b902be336d1749cc715edcdb6"
33
33
  }
package/readme.md CHANGED
@@ -29,13 +29,13 @@ introspection:
29
29
 
30
30
  All properties are optional.
31
31
 
32
- | Property | Default | Description |
33
- | --- | --- | --- |
34
- | `samples` | `false` | Enables collection of call input and outcome samples. |
35
- | `interval` | `300` | Interval between topology updates, in seconds. |
36
- | `threshold` | `1024` | Number of distinct observed interactions that triggers an update before the interval expires. |
37
- | `ui` | `true` | Publishes the web UI. |
38
- | `resources` | — | Resource requirements for the Introspection deployment. |
32
+ | Property | Default | Description |
33
+ | ----------- | ------- | --------------------------------------------------------------------------------------------- |
34
+ | `samples` | `false` | Enables collection of call input and outcome samples. |
35
+ | `interval` | `300` | Interval between topology updates, in seconds. |
36
+ | `threshold` | `1024` | Number of distinct observed interactions that triggers an update before the interval expires. |
37
+ | `ui` | `true` | Publishes the web UI. |
38
+ | `resources` | — | Resource requirements for the Introspection deployment. |
39
39
 
40
40
  Resource requirements can be declared specifically for Introspection:
41
41
 
@@ -12,7 +12,7 @@ properties:
12
12
  type: boolean
13
13
  resources:
14
14
  # `null` deploys the service without any, which every deployment has to state one way
15
- # or the other — see `migrations/263.md`.
15
+ # or the other — see `migrations/262.md`.
16
16
  type: object
17
17
  nullable: true
18
18
  properties:
@@ -8,12 +8,12 @@ import type { Annotation } from './annotation.js'
8
8
  export class Composition extends Connector {
9
9
  private readonly host: Host
10
10
 
11
- public constructor (host: Host) {
11
+ public constructor(host: Host) {
12
12
  super()
13
13
  this.host = host
14
14
  }
15
15
 
16
- protected override async open (): Promise<void> {
16
+ protected override async open(): Promise<void> {
17
17
  const paths = find()
18
18
  const composition = await this.host.composition(paths)
19
19
 
@@ -23,11 +23,11 @@ export class Composition extends Connector {
23
23
  }
24
24
  }
25
25
 
26
- export function find (): string[] {
26
+ export function find(): string[] {
27
27
  return entries().map((entry) => resolve(ROOT, entry.name))
28
28
  }
29
29
 
30
- function entries (): Dirent[] {
30
+ function entries(): Dirent[] {
31
31
  const entries = readdirSync(ROOT, { withFileTypes: true })
32
32
 
33
33
  return entries.filter((entry) => entry.isDirectory())
@@ -38,9 +38,8 @@ function entries (): Dirent[] {
38
38
  * must not end up with the explorer components — nor with the exposition
39
39
  * dependency they bring in.
40
40
  */
41
- export function components (annotation?: Annotation): Components {
42
- if (annotation === false)
43
- return { labels: [], paths: [] }
41
+ export function components(annotation?: Annotation): Components {
42
+ if (annotation === false) return { labels: [], paths: [] }
44
43
 
45
44
  const labels: string[] = []
46
45
  const paths: string[] = []
@@ -6,11 +6,11 @@ import { console } from 'openspan'
6
6
  * through their own operations — and serves the UI.
7
7
  */
8
8
  export class Explorer extends Connector {
9
- protected override async open (): Promise<void> {
9
+ protected override async open(): Promise<void> {
10
10
  console.info('Introspection explorer started')
11
11
  }
12
12
 
13
- protected override dispose (): void {
13
+ protected override dispose(): void {
14
14
  console.info('Introspection explorer is closed')
15
15
  }
16
16
  }
package/source/Factory.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import { Connector } from '@toa.io/core'
2
- import { DISABLED, environment, component as declaration, settings } from './annotation.js'
3
- import { NAMESPACE, UI_PORT } from './const.js'
2
+ import {
3
+ DISABLED,
4
+ environment,
5
+ component as declaration,
6
+ settings
7
+ } from './annotation.js'
8
+ import { NAMESPACE, uiPort } from './const.js'
4
9
  import { describe } from './describe.js'
5
10
  import { Reporter } from './Reporter.js'
6
11
  import { Tenant } from './Tenant.js'
@@ -11,7 +16,8 @@ import { capture, samplable } from './sample.js'
11
16
  import type { Declaration, Options, Settings } from './annotation.js'
12
17
  import type { Origin, Outcome, Target } from './model.js'
13
18
  import type { Manifest } from '@toa.io/norm'
14
- import type { Component, Locator, Reply, Request, extensions } from '@toa.io/core'
19
+ import type { Component, Locator } from '@toa.io/core'
20
+ import type { Reply, Request, extensions } from '@toa.io/core/types'
15
21
 
16
22
  export class Factory implements extensions.Factory {
17
23
  private readonly host: Host
@@ -19,28 +25,30 @@ export class Factory implements extensions.Factory {
19
25
  private readonly settings: Record<string, Settings> = {}
20
26
  private reporter: Reporter | null = null
21
27
 
22
- public constructor (host: Host) {
28
+ public constructor(host: Host) {
23
29
  this.host = host
24
30
  this.options = environment()
25
31
  }
26
32
 
27
- public tenant (locator: Locator, decl: Declaration | null, manifest: Manifest): Connector {
28
- const resolved = settings(locator.namespace, declaration(decl), this.options)
33
+ public tenant(
34
+ locator: Locator,
35
+ decl: Declaration | null,
36
+ manifest: Manifest
37
+ ): Connector {
38
+ const resolved = settings(locator.namespace!, declaration(decl), this.options)
29
39
 
30
40
  this.settings[locator.id] = resolved
31
41
 
32
- if (!resolved.enabled || locator.namespace === NAMESPACE)
33
- return new Connector()
42
+ if (!resolved.enabled || locator.namespace === NAMESPACE) return new Connector()
34
43
 
35
44
  return new Tenant(this.collector(), describe(manifest))
36
45
  }
37
46
 
38
- public component (component: Component): Component {
47
+ public component(component: Component): Component {
39
48
  const locator = component.locator
40
49
  const resolved = this.resolve(locator)
41
50
 
42
- if (!resolved.enabled)
43
- return component
51
+ if (!resolved.enabled) return component
44
52
 
45
53
  const reporter = this.collector()
46
54
  const invoke = component.invoke.bind(component)
@@ -62,12 +70,17 @@ export class Factory implements extensions.Factory {
62
70
  throw error
63
71
  } finally {
64
72
  // a call that failed is still a connection between two components
65
- const src: Origin = request?.source ?? UNKNOWN
66
- const dst: Target = { namespace: locator.namespace, component: locator.name, operation: endpoint }
67
-
68
- const sample = resolved.samples && samplable(request?.input)
69
- ? capture(request?.input, outcome)
70
- : undefined
73
+ const src: Origin = origin(request?.source)
74
+ const dst: Target = {
75
+ namespace: locator.namespace!,
76
+ component: locator.name,
77
+ operation: endpoint
78
+ }
79
+
80
+ const sample =
81
+ resolved.samples && samplable(request?.input)
82
+ ? capture(request?.input, outcome)
83
+ : undefined
71
84
 
72
85
  reporter.observe({ src, dst, sample })
73
86
  }
@@ -78,17 +91,15 @@ export class Factory implements extensions.Factory {
78
91
  return component
79
92
  }
80
93
 
81
- public service (): Connector | null {
82
- if (this.options === null)
83
- return null
94
+ public service(): Connector | null {
95
+ if (this.options === null) return null
84
96
 
85
97
  const composition = new Composition(this.host)
86
98
  const explorer = new Explorer()
87
99
 
88
100
  explorer.depends(composition)
89
101
 
90
- if (this.options.ui)
91
- explorer.depends(new UI(UI_PORT))
102
+ if (this.options.ui) explorer.depends(new UI(uiPort()))
92
103
 
93
104
  return explorer
94
105
  }
@@ -98,15 +109,20 @@ export class Factory implements extensions.Factory {
98
109
  * A component booted on its own (without a composition) falls back to
99
110
  * the environment, with sampling off.
100
111
  */
101
- private resolve (locator: Locator): Settings {
102
- if (locator.namespace === NAMESPACE)
103
- return DISABLED
104
-
105
- return this.settings[locator.id] ??
106
- settings(locator.namespace, {}, this.options === null ? null : { ...this.options, samples: false })
112
+ private resolve(locator: Locator): Settings {
113
+ if (locator.namespace === NAMESPACE) return DISABLED
114
+
115
+ return (
116
+ this.settings[locator.id] ??
117
+ settings(
118
+ locator.namespace!,
119
+ {},
120
+ this.options === null ? null : { ...this.options, samples: false }
121
+ )
122
+ )
107
123
  }
108
124
 
109
- private collector (): Reporter {
125
+ private collector(): Reporter {
110
126
  this.reporter ??= new Reporter(this.host, this.options!)
111
127
 
112
128
  return this.reporter
@@ -115,4 +131,22 @@ export class Factory implements extensions.Factory {
115
131
 
116
132
  const UNKNOWN = { service: 'unknown' } as const
117
133
 
134
+ /**
135
+ * `source` crosses the wire, and what it names is stored as an edge of the map — so what is
136
+ * read off it is the keys this release knows, and never whatever a peer put beside them.
137
+ */
138
+ function origin(source: Origin | undefined): Origin {
139
+ if (source === undefined) return UNKNOWN
140
+
141
+ if ('service' in source) return { service: source.service }
142
+
143
+ return 'event' in source
144
+ ? { namespace: source.namespace, component: source.component, event: source.event }
145
+ : {
146
+ namespace: source.namespace,
147
+ component: source.component,
148
+ operation: source.operation
149
+ }
150
+ }
151
+
118
152
  export type Host = extensions.Host