@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
@@ -30,7 +30,7 @@ export class Reporter extends Connector {
30
30
  private acquiring = false
31
31
  private dropped = 0
32
32
 
33
- public constructor (host: Host, options: Options) {
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 (node: Node): void {
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 (observed: Edge): void {
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 (): Promise<void> {
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 (): Promise<void> {
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 (): boolean {
98
+ private ready(): boolean {
101
99
  return NODES in this.remotes && EDGES in this.remotes
102
100
  }
103
101
 
104
- private async flush (): Promise<void> {
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 (reason: string): void {
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 (): Promise<void> {
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', { dropped: this.dropped, limit: MAX_EDGES })
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 (name: string, property: string,
169
- observed: Array<[string, Node | Edge]>): Promise<void> {
170
- if (observed.length === 0)
171
- return
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 (): void {
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 (): Promise<void> {
200
- await Promise.all([NODES, EDGES].map(async (name) => {
201
- const remote = await this.host.remote(new Locator(name, NAMESPACE))
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
- this.depends(remote)
201
+ this.depends(remote)
204
202
 
205
- await remote.connect()
203
+ await remote.connect()
206
204
 
207
- this.remotes[name] = remote
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
- * `_updated` fresh, which is how a removed component fades off the map.
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 (reporter: Reporter, node: Node) {
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 (): Promise<void> {
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 (): void {
33
+ protected override dispose(): void {
34
34
  this.stopped = true
35
35
  }
36
36
 
37
- private async announce (): Promise<void> {
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 (port: number, root: string = SITE) {
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 (): Promise<void> {
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 (): Promise<void> {
58
- const closed = new Promise<void>((resolve) => this.server.once('close', () => {
59
- resolve()
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 (request: http.IncomingMessage, response: http.ServerResponse): void {
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 (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> {
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
- response.writeHead(404).end()
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 (pathname: string): string | null {
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 (file: string, request: http.IncomingMessage, response: http.ServerResponse): Promise<void> {
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.writeHead(503, { 'content-type': 'text/plain' })
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
- response.end()
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 (url: string): string | null {
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 (file: string): boolean {
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 (relative: string): string {
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'
@@ -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 = false | {
6
- /** Capture real payloads. Off by default: this is production data. */
7
- samples?: boolean
8
- /** Flush period, seconds. */
9
- interval?: number
10
- /** Flush as soon as this many distinct edges are buffered. */
11
- threshold?: number
12
- /** Publish the UI. On by default. */
13
- ui?: boolean
14
- resources?: Resources
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 = false | {
19
- samples?: boolean
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 (annotation?: Annotation): 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 (): Options | null {
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 (declaration: Declaration | null | undefined): Declaration {
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 (namespace: string, declaration: Declaration, opts: Options | null): Settings {
76
- if (opts === null || declaration === false)
77
- return DISABLED
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
 
@@ -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 (manifest: Manifest): Node {
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 (manifest: Manifest): Node['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: manifest.entity.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 (manifest: Manifest): Operation[] {
37
- return Object.entries(manifest.operations ?? {})
38
- .map(([endpoint, definition]: [string, any]) => ({
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 (manifest: Manifest): Event[] {
51
- return Object.entries(manifest.events ?? {})
52
- .map(([label, definition]: [string, any]) => ({ label, binding: definition.binding }))
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 (manifest: Manifest): Receiver[] {
60
- return Object.entries(manifest.receivers ?? {})
61
- .map(([label, definition]: [string, any]) => ({
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
  }
@@ -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 (_: Instances<unknown>, annotation?: Annotation): Dependency {
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
- version: JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version,
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
@@ -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 (declaration: Declaration | null | undefined): Declaration {
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
@@ -1,4 +1,4 @@
1
- import type { Source } from '@toa.io/core'
1
+ import type { Source } from '@toa.io/core/types'
2
2
 
3
3
  export type { Source }
4
4
 
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 (input: unknown, outcome: Outcome): Sample {
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 (input: unknown): boolean {
13
+ export function samplable(input: unknown): boolean {
14
14
  return !(input instanceof Readable)
15
15
  }
16
16
 
17
- function redact (value: unknown): unknown {
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 (value: unknown): unknown {
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>> = namespace.schema('annotation')
10
- export const declaration: Schema<Exclude<Declaration, false>> = namespace.schema('declaration')
9
+ export const annotation: Schema<Exclude<Annotation, false>> =
10
+ namespace.schema('annotation')
11
+ export const declaration: Schema<Exclude<Declaration, false>> =
12
+ namespace.schema('declaration')