@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.
- package/components/introspection.edges/manifest.toa.yaml +28 -30
- package/components/introspection.edges/migrations/0001-system-properties.yaml +14 -0
- package/components/introspection.edges/operations/merge.js +1 -1
- package/components/introspection.edges/source/merge.ts +3 -5
- package/components/introspection.edges/tsconfig.tsbuildinfo +1 -1
- package/components/introspection.edges/types/toa.d.ts +5 -5
- package/components/introspection.nodes/manifest.toa.yaml +16 -18
- package/components/introspection.nodes/migrations/0001-system-properties.yaml +14 -0
- package/components/introspection.nodes/operations/merge.js +1 -1
- package/components/introspection.nodes/source/merge.ts +2 -3
- package/components/introspection.nodes/tsconfig.tsbuildinfo +1 -1
- package/components/introspection.nodes/types/toa.d.ts +5 -5
- package/cucumber.mjs +3 -0
- package/features/site/index.html +6 -2
- package/features/steps/Map.ts +36 -25
- package/features/steps/UI.ts +20 -13
- package/features/steps/components/probe.discreet/operations/ping.js +1 -1
- package/features/steps/components/probe.quiet/operations/ping.js +1 -1
- package/features/steps/components/probe.source/manifest.toa.yaml +7 -8
- package/features/steps/components/probe.source/operations/crash.js +1 -1
- package/features/steps/components/probe.source/operations/fail.js +1 -1
- package/features/steps/components/probe.source/operations/relay.js +1 -1
- package/features/steps/components/probe.target/manifest.toa.yaml +5 -6
- package/features/steps/components/probe.target/operations/compute.js +1 -1
- package/features/steps/components/probe.target/operations/count.js +1 -1
- package/package.json +7 -7
- package/readme.md +7 -7
- package/schemas/annotation.cos.yaml +1 -1
- package/source/Composition.ts +6 -7
- package/source/Explorer.ts +2 -2
- package/source/Factory.ts +63 -29
- package/source/Reporter.ts +37 -38
- package/source/Tenant.ts +6 -7
- package/source/UI.ts +33 -32
- package/source/annotation.ts +30 -26
- package/source/const.ts +12 -0
- package/source/describe.ts +22 -16
- package/source/extension.ts +10 -6
- package/source/keys.ts +0 -0
- package/source/manifest.ts +2 -3
- package/source/model.ts +1 -1
- package/source/sample.ts +6 -8
- package/source/schemas.ts +4 -2
- package/transpiled/Composition.js.map +1 -1
- package/transpiled/Factory.d.ts +2 -1
- package/transpiled/Factory.js +27 -6
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Reporter.js +4 -1
- package/transpiled/Reporter.js.map +1 -1
- package/transpiled/Tenant.d.ts +1 -1
- package/transpiled/Tenant.js +1 -1
- package/transpiled/Tenant.js.map +1 -1
- package/transpiled/UI.js +2 -1
- package/transpiled/UI.js.map +1 -1
- package/transpiled/annotation.js.map +1 -1
- package/transpiled/const.d.ts +7 -0
- package/transpiled/const.js +10 -0
- package/transpiled/const.js.map +1 -1
- package/transpiled/describe.js +7 -7
- package/transpiled/describe.js.map +1 -1
- package/transpiled/extension.d.ts +3 -0
- package/transpiled/extension.js +6 -1
- package/transpiled/extension.js.map +1 -1
- package/transpiled/keys.js.map +1 -1
- package/transpiled/manifest.js.map +1 -1
- package/transpiled/model.d.ts +1 -1
- package/transpiled/sample.js.map +1 -1
- package/transpiled/schemas.js.map +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/ui/dist/_app/immutable/chunks/{DZPtpe9B.js → BLEzg-4t.js} +1 -1
- package/ui/dist/_app/immutable/chunks/BPZdECZt.js +1 -0
- package/ui/dist/_app/immutable/chunks/{DAr41bMZ.js → BeZwDLIn.js} +2 -2
- package/ui/dist/_app/immutable/chunks/DUO28Hbn.js +1 -0
- package/ui/dist/_app/immutable/chunks/{Cw2o6qJ6.js → DYKePAZ9.js} +3 -3
- package/ui/dist/_app/immutable/chunks/{BiLog9K-.js → Di1YrEXu.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{DrodQShZ.js → Dkg6on5h.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{Dlhv3b0H.js → UH-Xf7ZK.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{LxgdVMjV.js → Yvrddvsp.js} +1 -1
- package/ui/dist/_app/immutable/entry/{app.BZe_kjnw.js → app.T7brwbZX.js} +2 -2
- package/ui/dist/_app/immutable/entry/start.C8vo__MC.js +1 -0
- package/ui/dist/_app/immutable/nodes/{0.Bp_WDf60.js → 0.UFiNhGhx.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{1.BhTvtURh.js → 1.t--TxMm7.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{2.DXBctzq4.js → 2.CAV8jmGo.js} +1 -1
- package/ui/dist/_app/immutable/nodes/3.Br1yDs7H.js +1 -0
- package/ui/dist/_app/immutable/nodes/4.Ck0KmPgK.js +1 -0
- package/ui/dist/_app/immutable/nodes/{5.Cvt5-NZz.js → 5.qVRn66ML.js} +1 -1
- package/ui/dist/_app/version.json +1 -1
- package/ui/dist/index.html +30 -25
- package/CHANGELOG.md +0 -426
- package/ui/dist/_app/immutable/chunks/D8JS8JwD.js +0 -1
- package/ui/dist/_app/immutable/chunks/ay3E8ALF.js +0 -1
- package/ui/dist/_app/immutable/entry/start.CeI0OHwu.js +0 -1
- package/ui/dist/_app/immutable/nodes/3.DJPUlbK5.js +0 -1
- package/ui/dist/_app/immutable/nodes/4.DTcjak2q.js +0 -1
package/features/steps/Map.ts
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
126
|
-
`
|
|
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
|
|
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(
|
|
137
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
package/features/steps/UI.ts
CHANGED
|
@@ -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
|
|
22
|
-
this.server = new UI(
|
|
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
|
|
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
|
|
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
|
|
39
|
-
assert.ok(
|
|
40
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
76
|
+
const request = http.get({ port: PORT, path, agent: false }, (response) => {
|
|
70
77
|
let body = ''
|
|
71
78
|
|
|
72
79
|
response.setEncoding('utf8')
|
|
@@ -4,14 +4,13 @@ name: source
|
|
|
4
4
|
entity:
|
|
5
5
|
associated: true
|
|
6
6
|
storage: null
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.introspection",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
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.
|
|
22
|
-
"@toa.io/generic": "1.0.0-alpha.
|
|
23
|
-
"@toa.io/schemas": "1.0.0-alpha.
|
|
24
|
-
"openspan": "1.0.0-alpha.
|
|
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": "
|
|
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
|
|
33
|
-
|
|
|
34
|
-
| `samples`
|
|
35
|
-
| `interval`
|
|
36
|
-
| `threshold` | `1024`
|
|
37
|
-
| `ui`
|
|
38
|
-
| `resources` | —
|
|
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/
|
|
15
|
+
# or the other — see `migrations/262.md`.
|
|
16
16
|
type: object
|
|
17
17
|
nullable: true
|
|
18
18
|
properties:
|
package/source/Composition.ts
CHANGED
|
@@ -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
|
|
11
|
+
public constructor(host: Host) {
|
|
12
12
|
super()
|
|
13
13
|
this.host = host
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
protected override async open
|
|
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
|
|
26
|
+
export function find(): string[] {
|
|
27
27
|
return entries().map((entry) => resolve(ROOT, entry.name))
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function entries
|
|
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
|
|
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[] = []
|
package/source/Explorer.ts
CHANGED
|
@@ -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
|
|
9
|
+
protected override async open(): Promise<void> {
|
|
10
10
|
console.info('Introspection explorer started')
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
protected override dispose
|
|
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 {
|
|
3
|
-
|
|
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
|
|
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
|
|
28
|
+
public constructor(host: Host) {
|
|
23
29
|
this.host = host
|
|
24
30
|
this.options = environment()
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
public tenant
|
|
28
|
-
|
|
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
|
|
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
|
|
66
|
-
const dst: Target = {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
|
102
|
-
if (locator.namespace === NAMESPACE)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
settings(
|
|
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
|
|
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
|