@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/source/Reporter.ts
CHANGED
|
@@ -30,7 +30,7 @@ export class Reporter extends Connector {
|
|
|
30
30
|
private acquiring = false
|
|
31
31
|
private dropped = 0
|
|
32
32
|
|
|
33
|
-
public constructor
|
|
33
|
+
public constructor(host: Host, options: Options) {
|
|
34
34
|
super()
|
|
35
35
|
|
|
36
36
|
this.host = host
|
|
@@ -38,14 +38,14 @@ export class Reporter extends Connector {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** The static description of a component. */
|
|
41
|
-
public expose
|
|
41
|
+
public expose(node: Node): void {
|
|
42
42
|
this.nodes.set(keys.node(node.namespace, node.component), node)
|
|
43
43
|
|
|
44
44
|
void this.flush()
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** A call between two components. */
|
|
48
|
-
public observe
|
|
48
|
+
public observe(observed: Edge): void {
|
|
49
49
|
const id = keys.edge(observed.src, observed.dst)
|
|
50
50
|
const edge = this.edges.get(id)
|
|
51
51
|
|
|
@@ -62,14 +62,12 @@ export class Reporter extends Connector {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
this.edges.set(id, observed)
|
|
65
|
-
} else if (observed.sample !== undefined)
|
|
66
|
-
edge.sample = observed.sample
|
|
65
|
+
} else if (observed.sample !== undefined) edge.sample = observed.sample
|
|
67
66
|
|
|
68
|
-
if (this.edges.size >= this.options.threshold)
|
|
69
|
-
void this.flush()
|
|
67
|
+
if (this.edges.size >= this.options.threshold) void this.flush()
|
|
70
68
|
}
|
|
71
69
|
|
|
72
|
-
protected override async open
|
|
70
|
+
protected override async open(): Promise<void> {
|
|
73
71
|
// deliberately not awaited: the explorer may not be there yet, or at all
|
|
74
72
|
this.acquire()
|
|
75
73
|
|
|
@@ -77,7 +75,7 @@ export class Reporter extends Connector {
|
|
|
77
75
|
this.timer.unref()
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
protected override async close
|
|
78
|
+
protected override async close(): Promise<void> {
|
|
81
79
|
if (this.timer !== null) {
|
|
82
80
|
clearInterval(this.timer)
|
|
83
81
|
this.timer = null
|
|
@@ -97,23 +95,20 @@ export class Reporter extends Connector {
|
|
|
97
95
|
})
|
|
98
96
|
}
|
|
99
97
|
|
|
100
|
-
private ready
|
|
98
|
+
private ready(): boolean {
|
|
101
99
|
return NODES in this.remotes && EDGES in this.remotes
|
|
102
100
|
}
|
|
103
101
|
|
|
104
|
-
private async flush
|
|
102
|
+
private async flush(): Promise<void> {
|
|
105
103
|
// a dispatch in flight keeps observations buffered, they join the next batch
|
|
106
|
-
if (this.flushing !== null)
|
|
107
|
-
return
|
|
104
|
+
if (this.flushing !== null) return
|
|
108
105
|
|
|
109
|
-
if (this.nodes.size === 0 && this.edges.size === 0)
|
|
110
|
-
return
|
|
106
|
+
if (this.nodes.size === 0 && this.edges.size === 0) return
|
|
111
107
|
|
|
112
108
|
if (!this.ready()) {
|
|
113
109
|
this.acquire()
|
|
114
110
|
|
|
115
|
-
if (this.edges.size >= MAX_EDGES)
|
|
116
|
-
this.discard('the explorer is not reachable')
|
|
111
|
+
if (this.edges.size >= MAX_EDGES) this.discard('the explorer is not reachable')
|
|
117
112
|
|
|
118
113
|
return
|
|
119
114
|
}
|
|
@@ -129,7 +124,7 @@ export class Reporter extends Connector {
|
|
|
129
124
|
await this.flushing
|
|
130
125
|
}
|
|
131
126
|
|
|
132
|
-
private discard
|
|
127
|
+
private discard(reason: string): void {
|
|
133
128
|
const nodes = this.nodes.size
|
|
134
129
|
const edges = this.edges.size + this.dropped
|
|
135
130
|
|
|
@@ -137,13 +132,12 @@ export class Reporter extends Connector {
|
|
|
137
132
|
this.edges.clear()
|
|
138
133
|
this.dropped = 0
|
|
139
134
|
|
|
140
|
-
if (nodes === 0 && edges === 0)
|
|
141
|
-
return
|
|
135
|
+
if (nodes === 0 && edges === 0) return
|
|
142
136
|
|
|
143
137
|
console.warn(`Introspection data discarded, ${reason}`, { nodes, edges })
|
|
144
138
|
}
|
|
145
139
|
|
|
146
|
-
private async dispatch
|
|
140
|
+
private async dispatch(): Promise<void> {
|
|
147
141
|
const nodes = [...this.nodes.entries()]
|
|
148
142
|
const edges = [...this.edges.entries()]
|
|
149
143
|
|
|
@@ -151,7 +145,10 @@ export class Reporter extends Connector {
|
|
|
151
145
|
this.edges.clear()
|
|
152
146
|
|
|
153
147
|
if (this.dropped > 0) {
|
|
154
|
-
console.warn('Introspection edges dropped', {
|
|
148
|
+
console.warn('Introspection edges dropped', {
|
|
149
|
+
dropped: this.dropped,
|
|
150
|
+
limit: MAX_EDGES
|
|
151
|
+
})
|
|
155
152
|
this.dropped = 0
|
|
156
153
|
}
|
|
157
154
|
|
|
@@ -165,15 +162,16 @@ export class Reporter extends Connector {
|
|
|
165
162
|
* A mass transition: every affected object is acquired and committed at once,
|
|
166
163
|
* so a flush is one call per component whatever it carries.
|
|
167
164
|
*/
|
|
168
|
-
private async merge
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
165
|
+
private async merge(
|
|
166
|
+
name: string,
|
|
167
|
+
property: string,
|
|
168
|
+
observed: Array<[string, Node | Edge]>
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
if (observed.length === 0) return
|
|
172
171
|
|
|
173
172
|
const objects: Record<string, Node | Edge> = {}
|
|
174
173
|
|
|
175
|
-
for (const [id, object] of observed)
|
|
176
|
-
objects[id] = object
|
|
174
|
+
for (const [id, object] of observed) objects[id] = object
|
|
177
175
|
|
|
178
176
|
await this.remotes[name].invoke('merge', {
|
|
179
177
|
query: { ids: observed.map(([id]) => id) },
|
|
@@ -183,9 +181,8 @@ export class Reporter extends Connector {
|
|
|
183
181
|
}
|
|
184
182
|
|
|
185
183
|
/** Runs in the background: discovery waits for the explorer as long as it takes. */
|
|
186
|
-
private acquire
|
|
187
|
-
if (this.acquiring)
|
|
188
|
-
return
|
|
184
|
+
private acquire(): void {
|
|
185
|
+
if (this.acquiring) return
|
|
189
186
|
|
|
190
187
|
this.acquiring = true
|
|
191
188
|
|
|
@@ -196,15 +193,17 @@ export class Reporter extends Connector {
|
|
|
196
193
|
})
|
|
197
194
|
}
|
|
198
195
|
|
|
199
|
-
private async reach
|
|
200
|
-
await Promise.all(
|
|
201
|
-
|
|
196
|
+
private async reach(): Promise<void> {
|
|
197
|
+
await Promise.all(
|
|
198
|
+
[NODES, EDGES].map(async (name) => {
|
|
199
|
+
const remote = await this.host.remote(new Locator(name, NAMESPACE))
|
|
202
200
|
|
|
203
|
-
|
|
201
|
+
this.depends(remote)
|
|
204
202
|
|
|
205
|
-
|
|
203
|
+
await remote.connect()
|
|
206
204
|
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
this.remotes[name] = remote
|
|
206
|
+
})
|
|
207
|
+
)
|
|
209
208
|
}
|
|
210
209
|
}
|
package/source/Tenant.ts
CHANGED
|
@@ -8,14 +8,14 @@ import type { Node } from './model.js'
|
|
|
8
8
|
* Announces the static description of a component.
|
|
9
9
|
*
|
|
10
10
|
* Delivery is guaranteed, so the repeat is not about reliability — it keeps
|
|
11
|
-
* `
|
|
11
|
+
* `UPDATED` fresh, which is how a removed component fades off the map.
|
|
12
12
|
*/
|
|
13
13
|
export class Tenant extends Connector {
|
|
14
14
|
private readonly reporter: Reporter
|
|
15
15
|
private readonly node: Node
|
|
16
16
|
private stopped = false
|
|
17
17
|
|
|
18
|
-
public constructor
|
|
18
|
+
public constructor(reporter: Reporter, node: Node) {
|
|
19
19
|
super()
|
|
20
20
|
|
|
21
21
|
this.reporter = reporter
|
|
@@ -24,22 +24,21 @@ export class Tenant extends Connector {
|
|
|
24
24
|
this.depends(reporter)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
protected override async open
|
|
27
|
+
protected override async open(): Promise<void> {
|
|
28
28
|
this.reporter.expose(this.node)
|
|
29
29
|
|
|
30
30
|
void this.announce()
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
protected override dispose
|
|
33
|
+
protected override dispose(): void {
|
|
34
34
|
this.stopped = true
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
private async announce
|
|
37
|
+
private async announce(): Promise<void> {
|
|
38
38
|
while (!this.stopped) {
|
|
39
39
|
await setTimeout(ANNOUNCE_INTERVAL, undefined, { ref: false })
|
|
40
40
|
|
|
41
|
-
if (this.stopped)
|
|
42
|
-
break
|
|
41
|
+
if (this.stopped) break
|
|
43
42
|
|
|
44
43
|
this.reporter.expose(this.node)
|
|
45
44
|
}
|
package/source/UI.ts
CHANGED
|
@@ -17,7 +17,7 @@ export class UI extends Connector {
|
|
|
17
17
|
private readonly port: number
|
|
18
18
|
private readonly root: string
|
|
19
19
|
|
|
20
|
-
public constructor
|
|
20
|
+
public constructor(port: number, root: string = SITE) {
|
|
21
21
|
super()
|
|
22
22
|
|
|
23
23
|
this.port = port
|
|
@@ -27,7 +27,7 @@ export class UI extends Connector {
|
|
|
27
27
|
})
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
protected override async open
|
|
30
|
+
protected override async open(): Promise<void> {
|
|
31
31
|
/*
|
|
32
32
|
* A taken port is a real error: uniqueness across services is settled at export
|
|
33
33
|
* time, so nothing here has to negotiate for one.
|
|
@@ -54,10 +54,12 @@ export class UI extends Connector {
|
|
|
54
54
|
console.info('Introspection UI started', { port: this.port, path: UI_PATH + '/' })
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
protected override async close
|
|
58
|
-
const closed = new Promise<void>((resolve) =>
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
protected override async close(): Promise<void> {
|
|
58
|
+
const closed = new Promise<void>((resolve) =>
|
|
59
|
+
this.server.once('close', () => {
|
|
60
|
+
resolve()
|
|
61
|
+
})
|
|
62
|
+
)
|
|
61
63
|
|
|
62
64
|
this.server.close()
|
|
63
65
|
this.server.closeAllConnections()
|
|
@@ -65,16 +67,18 @@ export class UI extends Connector {
|
|
|
65
67
|
await closed
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
private listen
|
|
70
|
+
private listen(request: http.IncomingMessage, response: http.ServerResponse): void {
|
|
69
71
|
void this.respond(request, response).catch((error: Error) => {
|
|
70
72
|
console.error('Introspection UI failure', { message: error.message })
|
|
71
73
|
|
|
72
|
-
if (!response.writableEnded)
|
|
73
|
-
response.writeHead(500).end()
|
|
74
|
+
if (!response.writableEnded) response.writeHead(500).end()
|
|
74
75
|
})
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
private async respond
|
|
78
|
+
private async respond(
|
|
79
|
+
request: http.IncomingMessage,
|
|
80
|
+
response: http.ServerResponse
|
|
81
|
+
): Promise<void> {
|
|
78
82
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
79
83
|
response.writeHead(405, { allow: 'GET, HEAD' }).end()
|
|
80
84
|
|
|
@@ -98,10 +102,8 @@ export class UI extends Connector {
|
|
|
98
102
|
|
|
99
103
|
const file = this.resolve(pathname)
|
|
100
104
|
|
|
101
|
-
if (file === null)
|
|
102
|
-
|
|
103
|
-
else
|
|
104
|
-
await this.send(file, request, response)
|
|
105
|
+
if (file === null) response.writeHead(404).end()
|
|
106
|
+
else await this.send(file, request, response)
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
/**
|
|
@@ -109,22 +111,18 @@ export class UI extends Connector {
|
|
|
109
111
|
* served as it is; anything else that could be a route falls back to the page,
|
|
110
112
|
* because the client router — not this server — knows what routes there are.
|
|
111
113
|
*/
|
|
112
|
-
private resolve
|
|
113
|
-
if (!pathname.startsWith(UI_PATH))
|
|
114
|
-
return null
|
|
114
|
+
private resolve(pathname: string): string | null {
|
|
115
|
+
if (!pathname.startsWith(UI_PATH)) return null
|
|
115
116
|
|
|
116
117
|
const relative = pathname.slice(UI_PATH.length)
|
|
117
118
|
|
|
118
|
-
if (relative !== '' && !relative.startsWith('/'))
|
|
119
|
-
return null
|
|
119
|
+
if (relative !== '' && !relative.startsWith('/')) return null
|
|
120
120
|
|
|
121
121
|
const file = path.join(this.root, relative)
|
|
122
122
|
|
|
123
|
-
if (file !== this.root && !file.startsWith(this.root + path.sep))
|
|
124
|
-
return null
|
|
123
|
+
if (file !== this.root && !file.startsWith(this.root + path.sep)) return null
|
|
125
124
|
|
|
126
|
-
if (isFile(file))
|
|
127
|
-
return file
|
|
125
|
+
if (isFile(file)) return file
|
|
128
126
|
|
|
129
127
|
/*
|
|
130
128
|
* A missing asset is missing, but a route can look like one: `identity.passkeys` is a
|
|
@@ -137,11 +135,16 @@ export class UI extends Connector {
|
|
|
137
135
|
return asset ? null : path.join(this.root, 'index.html')
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
private async send
|
|
138
|
+
private async send(
|
|
139
|
+
file: string,
|
|
140
|
+
request: http.IncomingMessage,
|
|
141
|
+
response: http.ServerResponse
|
|
142
|
+
): Promise<void> {
|
|
141
143
|
const stats = await fs.promises.stat(file).catch(() => null)
|
|
142
144
|
|
|
143
145
|
if (stats === null) {
|
|
144
|
-
response
|
|
146
|
+
response
|
|
147
|
+
.writeHead(503, { 'content-type': 'text/plain' })
|
|
145
148
|
.end('The introspection UI is not built. Run `npm run build:ui`.\n')
|
|
146
149
|
|
|
147
150
|
return
|
|
@@ -153,14 +156,12 @@ export class UI extends Connector {
|
|
|
153
156
|
'cache-control': caching(path.relative(this.root, file))
|
|
154
157
|
})
|
|
155
158
|
|
|
156
|
-
if (request.method === 'HEAD')
|
|
157
|
-
|
|
158
|
-
else
|
|
159
|
-
fs.createReadStream(file).pipe(response)
|
|
159
|
+
if (request.method === 'HEAD') response.end()
|
|
160
|
+
else fs.createReadStream(file).pipe(response)
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
function decode
|
|
164
|
+
function decode(url: string): string | null {
|
|
164
165
|
try {
|
|
165
166
|
return decodeURIComponent(url.split('?')[0])
|
|
166
167
|
} catch {
|
|
@@ -168,7 +169,7 @@ function decode (url: string): string | null {
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
function isFile
|
|
172
|
+
function isFile(file: string): boolean {
|
|
172
173
|
return fs.existsSync(file) && fs.statSync(file).isFile()
|
|
173
174
|
}
|
|
174
175
|
|
|
@@ -188,7 +189,7 @@ const DAY = 'public, max-age=86400'
|
|
|
188
189
|
* and their names carry the build. An icon is named for what it is rather than for its
|
|
189
190
|
* content, so it is asked about again, but not on every page load.
|
|
190
191
|
*/
|
|
191
|
-
function caching
|
|
192
|
+
function caching(relative: string): string {
|
|
192
193
|
if (relative.startsWith(IMMUTABLE)) return FOREVER
|
|
193
194
|
|
|
194
195
|
return ICONS.has(relative) ? DAY : 'no-cache'
|
package/source/annotation.ts
CHANGED
|
@@ -2,22 +2,26 @@ import { DEFAULT_INTERVAL, DEFAULT_THRESHOLD, DENIED, ENV } from './const.js'
|
|
|
2
2
|
import type { Resources } from '@toa.io/operations'
|
|
3
3
|
|
|
4
4
|
/** `context.toa.yaml` */
|
|
5
|
-
export type Annotation =
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
export type Annotation =
|
|
6
|
+
| false
|
|
7
|
+
| {
|
|
8
|
+
/** Capture real payloads. Off by default: this is production data. */
|
|
9
|
+
samples?: boolean
|
|
10
|
+
/** Flush period, seconds. */
|
|
11
|
+
interval?: number
|
|
12
|
+
/** Flush as soon as this many distinct edges are buffered. */
|
|
13
|
+
threshold?: number
|
|
14
|
+
/** Publish the UI. On by default. */
|
|
15
|
+
ui?: boolean
|
|
16
|
+
resources?: Resources
|
|
17
|
+
}
|
|
16
18
|
|
|
17
19
|
/** `manifest.toa.yaml` */
|
|
18
|
-
export type Declaration =
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
export type Declaration =
|
|
21
|
+
| false
|
|
22
|
+
| {
|
|
23
|
+
samples?: boolean
|
|
24
|
+
}
|
|
21
25
|
|
|
22
26
|
/** What `deployment()` encodes into the environment. */
|
|
23
27
|
export interface Options {
|
|
@@ -35,7 +39,7 @@ export interface Settings {
|
|
|
35
39
|
|
|
36
40
|
export const DISABLED: Settings = { enabled: false, samples: false }
|
|
37
41
|
|
|
38
|
-
export function options
|
|
42
|
+
export function options(annotation?: Annotation): Options {
|
|
39
43
|
const declaration = annotation === undefined || annotation === false ? {} : annotation
|
|
40
44
|
|
|
41
45
|
return {
|
|
@@ -47,22 +51,19 @@ export function options (annotation?: Annotation): Options {
|
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/** Reads what `deployment()` has put into the environment. */
|
|
50
|
-
export function environment
|
|
54
|
+
export function environment(): Options | null {
|
|
51
55
|
const value = process.env[ENV]
|
|
52
56
|
|
|
53
|
-
if (value === undefined)
|
|
54
|
-
return null
|
|
57
|
+
if (value === undefined) return null
|
|
55
58
|
|
|
56
59
|
return JSON.parse(value) as Options
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
export function component
|
|
60
|
-
if (declaration === false)
|
|
61
|
-
return false
|
|
62
|
+
export function component(declaration: Declaration | null | undefined): Declaration {
|
|
63
|
+
if (declaration === false) return false
|
|
62
64
|
|
|
63
65
|
// predefined extensions arrive as null for components that say nothing
|
|
64
|
-
if (declaration === null || declaration === undefined)
|
|
65
|
-
return {}
|
|
66
|
+
if (declaration === null || declaration === undefined) return {}
|
|
66
67
|
|
|
67
68
|
return declaration.samples === undefined ? {} : { samples: declaration.samples }
|
|
68
69
|
}
|
|
@@ -72,9 +73,12 @@ export function component (declaration: Declaration | null | undefined): Declara
|
|
|
72
73
|
* ceiling, the manifest is the component's own call. A component handling
|
|
73
74
|
* personal data opts out for good, and no context flag overrides that.
|
|
74
75
|
*/
|
|
75
|
-
export function settings
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
export function settings(
|
|
77
|
+
namespace: string,
|
|
78
|
+
declaration: Declaration,
|
|
79
|
+
opts: Options | null
|
|
80
|
+
): Settings {
|
|
81
|
+
if (opts === null || declaration === false) return DISABLED
|
|
78
82
|
|
|
79
83
|
const samples = opts.samples && declaration.samples !== false && !DENIED.has(namespace)
|
|
80
84
|
|
package/source/const.ts
CHANGED
|
@@ -10,6 +10,18 @@ export const EDGES = 'edges'
|
|
|
10
10
|
export const UI_PATH = '/.introspection'
|
|
11
11
|
export const UI_PORT = 8002
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The port the UI is served on. `UI_PORT` is what a deployment publishes and what the chart
|
|
15
|
+
* renders; the variable is for a machine that runs more than one Toa process — a Toa checkout
|
|
16
|
+
* beside an application, or two applications — where one of them has to give it up.
|
|
17
|
+
*/
|
|
18
|
+
export const UI_PORT_ENV = 'TOA_INTROSPECTION_UI_PORT'
|
|
19
|
+
export function uiPort(): number {
|
|
20
|
+
const value = process.env[UI_PORT_ENV]
|
|
21
|
+
|
|
22
|
+
return value === undefined ? UI_PORT : Number(value)
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
export const DEFAULT_INTERVAL = 300
|
|
14
26
|
export const DEFAULT_THRESHOLD = 1024
|
|
15
27
|
|
package/source/describe.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { Manifest } from '@toa.io/norm'
|
|
|
7
7
|
* Everything here is already normalized by norm: the prototype chain is collapsed,
|
|
8
8
|
* so this is the shape the runtime actually runs.
|
|
9
9
|
*/
|
|
10
|
-
export function describe
|
|
10
|
+
export function describe(manifest: Manifest): Node {
|
|
11
11
|
return {
|
|
12
12
|
namespace: manifest.namespace,
|
|
13
13
|
component: manifest.name,
|
|
@@ -22,20 +22,23 @@ export function describe (manifest: Manifest): Node {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function entity
|
|
26
|
-
if (manifest.entity === undefined)
|
|
27
|
-
return null
|
|
25
|
+
function entity(manifest: Manifest): Node['entity'] {
|
|
26
|
+
if (manifest.entity === undefined) return null
|
|
28
27
|
|
|
28
|
+
const { properties, required } = manifest.entity
|
|
29
|
+
|
|
30
|
+
// the map publishes a schema, which is what a tool reading it can validate against;
|
|
31
|
+
// a manifest declares the properties and assembles no envelope
|
|
29
32
|
return {
|
|
30
|
-
schema:
|
|
33
|
+
schema: { type: 'object', properties, required },
|
|
31
34
|
storage: manifest.entity.storage,
|
|
32
35
|
associated: manifest.entity.associated === true
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
function operations
|
|
37
|
-
return Object.entries(manifest.operations ?? {})
|
|
38
|
-
|
|
39
|
+
function operations(manifest: Manifest): Operation[] {
|
|
40
|
+
return Object.entries(manifest.operations ?? {}).map(
|
|
41
|
+
([endpoint, definition]: [string, any]) => ({
|
|
39
42
|
endpoint,
|
|
40
43
|
type: definition.type,
|
|
41
44
|
description: definition.description,
|
|
@@ -44,26 +47,29 @@ function operations (manifest: Manifest): Operation[] {
|
|
|
44
47
|
input: definition.input ?? null,
|
|
45
48
|
output: definition.output ?? null,
|
|
46
49
|
errors: definition.errors ?? []
|
|
47
|
-
})
|
|
50
|
+
})
|
|
51
|
+
)
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
function events
|
|
51
|
-
return Object.entries(manifest.events ?? {})
|
|
52
|
-
|
|
54
|
+
function events(manifest: Manifest): Event[] {
|
|
55
|
+
return Object.entries(manifest.events ?? {}).map(
|
|
56
|
+
([label, definition]: [string, any]) => ({ label, binding: definition.binding })
|
|
57
|
+
)
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
/*
|
|
56
61
|
* Receiver labels contain dots (`identity.bans.created`), which is why nodes keep
|
|
57
62
|
* these as arrays rather than maps — a dot in a document key is a hazard in Mongo.
|
|
58
63
|
*/
|
|
59
|
-
function receivers
|
|
60
|
-
return Object.entries(manifest.receivers ?? {})
|
|
61
|
-
|
|
64
|
+
function receivers(manifest: Manifest): Receiver[] {
|
|
65
|
+
return Object.entries(manifest.receivers ?? {}).map(
|
|
66
|
+
([label, definition]: [string, any]) => ({
|
|
62
67
|
label,
|
|
63
68
|
source: definition.source ?? label.split('.').slice(0, 2).join('.'),
|
|
64
69
|
event: label.split('.').pop()!,
|
|
65
70
|
operation: definition.operation,
|
|
66
71
|
conditioned: definition.conditioned === true,
|
|
67
72
|
adaptive: definition.adaptive === true
|
|
68
|
-
})
|
|
73
|
+
})
|
|
74
|
+
)
|
|
69
75
|
}
|
package/source/extension.ts
CHANGED
|
@@ -6,6 +6,10 @@ import * as schemas from './schemas.js'
|
|
|
6
6
|
import type { Annotation } from './annotation.js'
|
|
7
7
|
import type { Dependency, Instances, Service } from '@toa.io/operations'
|
|
8
8
|
|
|
9
|
+
/** Where Toa's release publishes this service's image. An application takes it
|
|
10
|
+
* instead of building one when its context says `registry.services: published`. */
|
|
11
|
+
export const image = 'ghcr.io/toa-io/extension-introspection-explorer'
|
|
12
|
+
|
|
9
13
|
export const standalone = true
|
|
10
14
|
|
|
11
15
|
/**
|
|
@@ -15,19 +19,19 @@ export const standalone = true
|
|
|
15
19
|
* with the service — never on its own, or tasks would pile up in a queue
|
|
16
20
|
* nothing consumes.
|
|
17
21
|
*/
|
|
18
|
-
export function deployment
|
|
19
|
-
if (annotation === false)
|
|
20
|
-
return {}
|
|
22
|
+
export function deployment(_: Instances<unknown>, annotation?: Annotation): Dependency {
|
|
23
|
+
if (annotation === false) return {}
|
|
21
24
|
|
|
22
|
-
if (annotation !== undefined)
|
|
23
|
-
schemas.annotation.validate(annotation)
|
|
25
|
+
if (annotation !== undefined) schemas.annotation.validate(annotation)
|
|
24
26
|
|
|
25
27
|
const opts = options(annotation)
|
|
26
28
|
|
|
27
29
|
const service: Service = {
|
|
28
30
|
group: 'introspection',
|
|
29
31
|
name: 'explorer',
|
|
30
|
-
|
|
32
|
+
image,
|
|
33
|
+
version: JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
34
|
+
.version,
|
|
31
35
|
components: components().labels,
|
|
32
36
|
resources: annotation?.resources,
|
|
33
37
|
variables: []
|
package/source/keys.ts
CHANGED
|
Binary file
|
package/source/manifest.ts
CHANGED
|
@@ -9,11 +9,10 @@ import type { Declaration } from './annotation.js'
|
|
|
9
9
|
* declaration arrives as `null` — which still has to produce a value,
|
|
10
10
|
* or norm rejects the extension.
|
|
11
11
|
*/
|
|
12
|
-
export function manifest
|
|
12
|
+
export function manifest(declaration: Declaration | null | undefined): Declaration {
|
|
13
13
|
const normalized = component(declaration)
|
|
14
14
|
|
|
15
|
-
if (normalized !== false)
|
|
16
|
-
schemas.declaration.validate(normalized)
|
|
15
|
+
if (normalized !== false) schemas.declaration.validate(normalized)
|
|
17
16
|
|
|
18
17
|
return normalized
|
|
19
18
|
}
|
package/source/model.ts
CHANGED
package/source/sample.ts
CHANGED
|
@@ -6,20 +6,19 @@ import type { Outcome, Sample } from './model.js'
|
|
|
6
6
|
* Captures the payload of a call. Only reached when both the context and the
|
|
7
7
|
* component have opted in, and never for a denied namespace.
|
|
8
8
|
*/
|
|
9
|
-
export function capture
|
|
9
|
+
export function capture(input: unknown, outcome: Outcome): Sample {
|
|
10
10
|
return { at: Date.now(), input: redact(input), outcome }
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function samplable
|
|
13
|
+
export function samplable(input: unknown): boolean {
|
|
14
14
|
return !(input instanceof Readable)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function redact
|
|
17
|
+
function redact(value: unknown): unknown {
|
|
18
18
|
if (value === null || value === undefined || typeof value !== 'object')
|
|
19
19
|
return truncate(value)
|
|
20
20
|
|
|
21
|
-
if (Array.isArray(value))
|
|
22
|
-
return truncate(value.map(redact))
|
|
21
|
+
if (Array.isArray(value)) return truncate(value.map(redact))
|
|
23
22
|
|
|
24
23
|
const result: Record<string, unknown> = {}
|
|
25
24
|
|
|
@@ -33,7 +32,7 @@ function redact (value: unknown): unknown {
|
|
|
33
32
|
* The map holds one sample per edge indefinitely, so a single oversized
|
|
34
33
|
* payload must not become a permanent tenant of the collection.
|
|
35
34
|
*/
|
|
36
|
-
function truncate
|
|
35
|
+
function truncate(value: unknown): unknown {
|
|
37
36
|
let serialized: string
|
|
38
37
|
|
|
39
38
|
try {
|
|
@@ -42,8 +41,7 @@ function truncate (value: unknown): unknown {
|
|
|
42
41
|
return '[unserializable]'
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
if (serialized.length <= SAMPLE_LIMIT)
|
|
46
|
-
return value
|
|
44
|
+
if (serialized.length <= SAMPLE_LIMIT) return value
|
|
47
45
|
|
|
48
46
|
return '[truncated]'
|
|
49
47
|
}
|
package/source/schemas.ts
CHANGED
|
@@ -6,5 +6,7 @@ import type { Annotation, Declaration } from './annotation.js'
|
|
|
6
6
|
const path = resolve(import.meta.dirname, '../schemas')
|
|
7
7
|
const namespace = schemas.namespace(path)
|
|
8
8
|
|
|
9
|
-
export const annotation: Schema<Exclude<Annotation, false>> =
|
|
10
|
-
|
|
9
|
+
export const annotation: Schema<Exclude<Annotation, false>> =
|
|
10
|
+
namespace.schema('annotation')
|
|
11
|
+
export const declaration: Schema<Exclude<Declaration, false>> =
|
|
12
|
+
namespace.schema('declaration')
|