@toa.io/extensions.configuration 1.0.0-alpha.284 → 1.0.0-alpha.287

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 (91) hide show
  1. package/CHANGELOG.md +1 -365
  2. package/components/configuration.values/manifest.toa.yaml +14 -21
  3. package/components/configuration.values/migrations/0001-system-properties.yaml +14 -0
  4. package/components/configuration.values/migrations/0002-indexes.yaml +7 -0
  5. package/components/configuration.values/operations/create.js +1 -2
  6. package/components/configuration.values/operations/fetch.js +1 -1
  7. package/components/configuration.values/operations/get.js +1 -1
  8. package/components/configuration.values/operations/lib/map.js +1 -1
  9. package/components/configuration.values/operations/lib/resolve.js +3 -3
  10. package/components/configuration.values/operations/list.js +1 -1
  11. package/components/configuration.values/source/create.ts +3 -5
  12. package/components/configuration.values/source/fetch.ts +12 -10
  13. package/components/configuration.values/source/get.ts +2 -3
  14. package/components/configuration.values/source/lib/map.test.ts +7 -2
  15. package/components/configuration.values/source/lib/map.ts +3 -3
  16. package/components/configuration.values/source/lib/resolve.ts +9 -6
  17. package/components/configuration.values/source/list.ts +8 -6
  18. package/components/configuration.values/tsconfig.tsbuildinfo +1 -1
  19. package/components/configuration.values/types/toa.d.ts +5 -5
  20. package/cucumber.mjs +3 -0
  21. package/features/site/index.html +6 -2
  22. package/features/steps/UI.ts +20 -13
  23. package/notes/consistency.md +10 -9
  24. package/package.json +6 -6
  25. package/readme.md +15 -14
  26. package/schemas/annotation.cos.yaml +1 -1
  27. package/source/Aspect.test.ts +32 -8
  28. package/source/Aspect.ts +28 -19
  29. package/source/Client.test.ts +31 -14
  30. package/source/Client.ts +41 -48
  31. package/source/Composition.ts +6 -6
  32. package/source/Factory.ts +7 -7
  33. package/source/Secret.test.ts +1 -1
  34. package/source/Secret.ts +5 -5
  35. package/source/UI.ts +33 -32
  36. package/source/configuration.test.ts +14 -8
  37. package/source/configuration.ts +11 -15
  38. package/source/const.ts +13 -1
  39. package/source/context.ts +2 -2
  40. package/source/deployment.test.ts +37 -12
  41. package/source/deployment.ts +45 -19
  42. package/source/epoch.test.ts +8 -2
  43. package/source/epoch.ts +6 -5
  44. package/source/index.ts +1 -1
  45. package/source/manifest.test.ts +12 -6
  46. package/source/manifest.ts +1 -1
  47. package/transpiled/Aspect.d.ts +2 -1
  48. package/transpiled/Aspect.js +15 -3
  49. package/transpiled/Aspect.js.map +1 -1
  50. package/transpiled/Client.d.ts +1 -1
  51. package/transpiled/Client.js +8 -2
  52. package/transpiled/Client.js.map +1 -1
  53. package/transpiled/Composition.js +2 -2
  54. package/transpiled/Composition.js.map +1 -1
  55. package/transpiled/Factory.d.ts +2 -1
  56. package/transpiled/Factory.js.map +1 -1
  57. package/transpiled/Secret.js.map +1 -1
  58. package/transpiled/UI.js +2 -1
  59. package/transpiled/UI.js.map +1 -1
  60. package/transpiled/configuration.js.map +1 -1
  61. package/transpiled/const.d.ts +8 -1
  62. package/transpiled/const.js +10 -0
  63. package/transpiled/const.js.map +1 -1
  64. package/transpiled/context.d.ts +1 -1
  65. package/transpiled/context.js.map +1 -1
  66. package/transpiled/deployment.d.ts +3 -0
  67. package/transpiled/deployment.js +19 -5
  68. package/transpiled/deployment.js.map +1 -1
  69. package/transpiled/epoch.js +3 -1
  70. package/transpiled/epoch.js.map +1 -1
  71. package/transpiled/index.d.ts +1 -1
  72. package/transpiled/index.js +1 -1
  73. package/transpiled/index.js.map +1 -1
  74. package/transpiled/manifest.js.map +1 -1
  75. package/tsconfig.tsbuildinfo +1 -1
  76. package/ui/dist/_app/immutable/chunks/{BzG-gka8.js → BW57cOwz.js} +1 -1
  77. package/ui/dist/_app/immutable/chunks/{_I_MUoRn.js → Bhnl1Avs.js} +1 -1
  78. package/ui/dist/_app/immutable/chunks/{Bxde3uE_.js → BxOvOexd.js} +1 -1
  79. package/ui/dist/_app/immutable/chunks/{mvspXr3J.js → CkX_FNYa.js} +2 -2
  80. package/ui/dist/_app/immutable/chunks/{p9RyiXQ-.js → Dmv4yvb8.js} +1 -1
  81. package/ui/dist/_app/immutable/entry/{app.C1VIepN7.js → app.Cr7od9uH.js} +2 -2
  82. package/ui/dist/_app/immutable/entry/start.DWf-Tl0X.js +1 -0
  83. package/ui/dist/_app/immutable/nodes/{0.D_0dtFej.js → 0.CcGZUFKo.js} +1 -1
  84. package/ui/dist/_app/immutable/nodes/{1.DFlgHh-H.js → 1.CR6En12y.js} +1 -1
  85. package/ui/dist/_app/immutable/nodes/{2.BIHgAxXR.js → 2.BhyO8bGT.js} +1 -1
  86. package/ui/dist/_app/immutable/nodes/3.DcbMtJtb.js +1 -0
  87. package/ui/dist/_app/immutable/nodes/{4.BZMuJGfA.js → 4.qVnVQBNq.js} +1 -1
  88. package/ui/dist/_app/version.json +1 -1
  89. package/ui/dist/index.html +28 -23
  90. package/ui/dist/_app/immutable/entry/start.BQWCs_Pq.js +0 -1
  91. package/ui/dist/_app/immutable/nodes/3.R-_yCt5X.js +0 -1
package/source/Client.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { console } from 'openspan'
2
2
  import { Connector, Locator } from '@toa.io/core'
3
3
  import { EVENT, SOURCE } from './const.js'
4
- import type { Message, Remote } from '@toa.io/core'
4
+ import type { Remote } from '@toa.io/core'
5
+ import type { Message } from '@toa.io/core/types'
5
6
  import type { Host } from './Factory.js'
6
7
 
7
8
  /**
@@ -23,7 +24,7 @@ export class Client extends Connector {
23
24
  private fresh = false
24
25
  private round = 0
25
26
 
26
- public constructor (host: Host, options: Partial<Options> = {}) {
27
+ public constructor(host: Host, options: Partial<Options> = {}) {
27
28
  super()
28
29
 
29
30
  this.host = host
@@ -31,7 +32,7 @@ export class Client extends Connector {
31
32
  }
32
33
 
33
34
  /** The configuration of a component for an epoch, once the service has one. */
34
- public async fetch (component: string, epoch: string): Promise<Value> {
35
+ public async fetch(component: string, epoch: string): Promise<Value> {
35
36
  const key = id(component, epoch)
36
37
 
37
38
  let entry = this.pending.get(key)
@@ -45,28 +46,25 @@ export class Client extends Connector {
45
46
  entry!.waiters.push(resolve)
46
47
  })
47
48
 
48
- if (this.flushing)
49
- this.fresh = true
50
- else
51
- this.schedule(0)
49
+ if (this.flushing) this.fresh = true
50
+ else this.schedule(0)
52
51
 
53
52
  return await promise
54
53
  }
55
54
 
56
- public subscribe (component: string, epoch: string, listener: Listener): void {
55
+ public subscribe(component: string, epoch: string, listener: Listener): void {
57
56
  const key = id(component, epoch)
58
57
 
59
- if (!this.listeners.has(key))
60
- this.listeners.set(key, new Set())
58
+ if (!this.listeners.has(key)) this.listeners.set(key, new Set())
61
59
 
62
60
  this.listeners.get(key)!.add(listener)
63
61
  }
64
62
 
65
- public unsubscribe (component: string, epoch: string, listener: Listener): void {
63
+ public unsubscribe(component: string, epoch: string, listener: Listener): void {
66
64
  this.listeners.get(id(component, epoch))?.delete(listener)
67
65
  }
68
66
 
69
- protected override async open (): Promise<void> {
67
+ protected override async open(): Promise<void> {
70
68
  this.remote = await this.host.remote(LOCATOR, SOURCE)
71
69
 
72
70
  this.depends(this.remote)
@@ -79,25 +77,23 @@ export class Client extends Connector {
79
77
  await consumer.connect()
80
78
  }
81
79
 
82
- protected override async close (): Promise<void> {
80
+ protected override async close(): Promise<void> {
83
81
  if (this.timer !== null) {
84
82
  clearTimeout(this.timer)
85
83
  this.timer = null
86
84
  }
87
85
  }
88
86
 
89
- protected override async dispose (): Promise<void> {
87
+ protected override async dispose(): Promise<void> {
90
88
  this.disposed = true
91
89
  }
92
90
 
93
- private schedule (delay: number): void {
94
- if (this.flushing)
95
- return
91
+ private schedule(delay: number): void {
92
+ if (this.flushing) return
96
93
 
97
94
  if (this.timer !== null) {
98
95
  // a request that has just arrived does not wait for the round already planned
99
- if (delay > 0)
100
- return
96
+ if (delay > 0) return
101
97
 
102
98
  clearTimeout(this.timer)
103
99
  }
@@ -107,7 +103,7 @@ export class Client extends Connector {
107
103
  }, delay)
108
104
  }
109
105
 
110
- private async flush (): Promise<void> {
106
+ private async flush(): Promise<void> {
111
107
  this.timer = null
112
108
  this.flushing = true
113
109
  this.fresh = false
@@ -118,8 +114,7 @@ export class Client extends Connector {
118
114
  try {
119
115
  const output = await this.remote!.invoke<Fetched[] | Error>('fetch', { input })
120
116
 
121
- if (output instanceof Error)
122
- throw output
117
+ if (output instanceof Error) throw output
123
118
 
124
119
  this.settle(output)
125
120
  } catch (error) {
@@ -130,35 +125,29 @@ export class Client extends Connector {
130
125
 
131
126
  this.report(batch)
132
127
 
133
- if (this.pending.size === 0)
134
- return
128
+ if (this.pending.size === 0) return
135
129
 
136
- if (this.fresh)
137
- this.schedule(0)
138
- else
139
- this.schedule(this.backoff())
130
+ if (this.fresh) this.schedule(0)
131
+ else this.schedule(this.backoff())
140
132
  }
141
133
 
142
134
  /** Those the service has served are told; the rest stay for the next round. */
143
- private settle (output: Fetched[]): void {
135
+ private settle(output: Fetched[]): void {
144
136
  for (const { component, epoch, configuration, created } of output) {
145
- if (configuration === null)
146
- continue
137
+ if (configuration === null) continue
147
138
 
148
139
  const key = id(component, epoch)
149
140
  const entry = this.pending.get(key)
150
141
 
151
- if (entry === undefined)
152
- continue
142
+ if (entry === undefined) continue
153
143
 
154
144
  this.pending.delete(key)
155
145
 
156
- for (const resolve of entry.waiters)
157
- resolve({ configuration, created })
146
+ for (const resolve of entry.waiters) resolve({ configuration, created })
158
147
  }
159
148
  }
160
149
 
161
- private report (batch: Pending[]): void {
150
+ private report(batch: Pending[]): void {
162
151
  const waiting = batch
163
152
  .filter(({ component, epoch }) => this.pending.has(id(component, epoch)))
164
153
  .map(({ component }) => component)
@@ -172,24 +161,28 @@ export class Client extends Connector {
172
161
  this.round++
173
162
 
174
163
  if (this.round % this.options.warn === 1)
175
- console.warn('Waiting for configuration', { components: waiting, round: this.round })
164
+ console.warn('Waiting for configuration', {
165
+ components: waiting,
166
+ round: this.round
167
+ })
176
168
  }
177
169
 
178
- private backoff (): number {
170
+ private backoff(): number {
179
171
  return Math.min(this.options.base * Math.pow(FACTOR, this.round), this.options.max)
180
172
  }
181
173
 
182
174
  /** A created object goes to the subscribers of its component and epoch, as it is. */
183
- private deliver (created: Created): void {
175
+ private deliver(created: Created): void {
184
176
  const listeners = this.listeners.get(id(created.component, created.epoch))
185
177
 
186
- if (listeners === undefined)
187
- return
178
+ if (listeners === undefined) return
188
179
 
189
- const value: Value = { configuration: created.configuration, created: created._created }
180
+ const value: Value = {
181
+ configuration: created.configuration,
182
+ created: created.CREATED
183
+ }
190
184
 
191
- for (const listener of listeners)
192
- listener(value)
185
+ for (const listener of listeners) listener(value)
193
186
  }
194
187
  }
195
188
 
@@ -197,18 +190,18 @@ export class Client extends Connector {
197
190
  class Subscription extends Connector {
198
191
  private readonly handler: (created: Created) => void
199
192
 
200
- public constructor (handler: (created: Created) => void) {
193
+ public constructor(handler: (created: Created) => void) {
201
194
  super()
202
195
 
203
196
  this.handler = handler
204
197
  }
205
198
 
206
- public async receive (message: Message<Created>): Promise<void> {
199
+ public async receive(message: Message<Created>): Promise<void> {
207
200
  this.handler(message.payload)
208
201
  }
209
202
  }
210
203
 
211
- function id (component: string, epoch: string): string {
204
+ function id(component: string, epoch: string): string {
212
205
  return component + '\0' + epoch
213
206
  }
214
207
 
@@ -246,7 +239,7 @@ export interface Created {
246
239
  component: string
247
240
  epoch: string
248
241
  configuration: object
249
- _created: number
242
+ CREATED: number
250
243
  }
251
244
 
252
245
  export type Listener = (value: Value) => void
@@ -1,7 +1,7 @@
1
1
  import { readdirSync, type Dirent } from 'node:fs'
2
2
  import { resolve } from 'node:path'
3
3
  import { Connector } from '@toa.io/core'
4
- import { UI_PORT } from './const.js'
4
+ import { uiPort } from './const.js'
5
5
  import { UI } from './UI.js'
6
6
  import type { Host } from './Factory.js'
7
7
 
@@ -9,12 +9,12 @@ import type { Host } from './Factory.js'
9
9
  export class Composition extends Connector {
10
10
  private readonly host: Host
11
11
 
12
- public constructor (host: Host) {
12
+ public constructor(host: Host) {
13
13
  super()
14
14
  this.host = host
15
15
  }
16
16
 
17
- protected override async open (): Promise<void> {
17
+ protected override async open(): Promise<void> {
18
18
  const composition = await this.host.composition(components().paths)
19
19
 
20
20
  await composition.connect()
@@ -23,7 +23,7 @@ export class Composition extends Connector {
23
23
 
24
24
  // connected here rather than declared as a dependency: dependencies are connected
25
25
  // before `open` runs, so one added from inside it would never be
26
- const ui = new UI(UI_PORT)
26
+ const ui = new UI(uiPort())
27
27
 
28
28
  await ui.connect()
29
29
 
@@ -31,7 +31,7 @@ export class Composition extends Connector {
31
31
  }
32
32
  }
33
33
 
34
- export function components (): Components {
34
+ export function components(): Components {
35
35
  const labels: string[] = []
36
36
  const paths: string[] = []
37
37
 
@@ -43,7 +43,7 @@ export function components (): Components {
43
43
  return { labels, paths }
44
44
  }
45
45
 
46
- function entries (): Dirent[] {
46
+ function entries(): Dirent[] {
47
47
  const entries = readdirSync(ROOT, { withFileTypes: true })
48
48
 
49
49
  return entries.filter((entry) => entry.isDirectory())
package/source/Factory.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { type Connector, type Locator, type extensions } from '@toa.io/core'
1
+ import { type Connector, type Locator } from '@toa.io/core'
2
+ import type { extensions } from '@toa.io/core/types'
2
3
  import { Aspect } from './Aspect.js'
3
4
  import { Client } from './Client.js'
4
5
  import { Composition } from './Composition.js'
@@ -9,23 +10,22 @@ export class Factory implements extensions.Factory {
9
10
  private readonly host: Host
10
11
  private client: Client | null = null
11
12
 
12
- public constructor (host: Host) {
13
+ public constructor(host: Host) {
13
14
  this.host = host
14
15
  }
15
16
 
16
- public aspect (locator: Locator, manifest: Manifest): extensions.Aspect {
17
+ public aspect(locator: Locator, manifest: Manifest): extensions.Aspect {
17
18
  const client = overridden(locator) ? null : this.shared()
18
19
 
19
20
  return new Aspect(locator, manifest, client)
20
21
  }
21
22
 
22
- public service (): Connector {
23
+ public service(): Connector {
23
24
  return new Composition(this.host)
24
25
  }
25
26
 
26
- private shared (): Client {
27
- if (this.client === null || this.client.disposed)
28
- this.client = new Client(this.host)
27
+ private shared(): Client {
28
+ if (this.client === null || this.client.disposed) this.client = new Client(this.host)
29
29
 
30
30
  return this.client
31
31
  }
@@ -23,7 +23,7 @@ it('should not show in JSON', () => {
23
23
  it('should not show when inspected', () => {
24
24
  assert.deepStrictEqual(inspect(secret), REDACTED)
25
25
  assert.ok(inspect({ key: secret }).includes(REDACTED))
26
- assert.ok(!(inspect({ key: secret }).includes('s3cret')))
26
+ assert.ok(!inspect({ key: secret }).includes('s3cret'))
27
27
  })
28
28
 
29
29
  it('should not expose the value as a property', () => {
package/source/Secret.ts CHANGED
@@ -8,23 +8,23 @@ import { inspect } from 'node:util'
8
8
  export class Secret {
9
9
  readonly #value: string
10
10
 
11
- public constructor (value: string) {
11
+ public constructor(value: string) {
12
12
  this.#value = value
13
13
  }
14
14
 
15
- public unwrap (): string {
15
+ public unwrap(): string {
16
16
  return this.#value
17
17
  }
18
18
 
19
- public toString (): string {
19
+ public toString(): string {
20
20
  return REDACTED
21
21
  }
22
22
 
23
- public toJSON (): string {
23
+ public toJSON(): string {
24
24
  return REDACTED
25
25
  }
26
26
 
27
- public [inspect.custom] (): string {
27
+ public [inspect.custom](): string {
28
28
  return REDACTED
29
29
  }
30
30
  }
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('Configuration 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('Configuration 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 configuration 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'
@@ -21,8 +21,7 @@ beforeEach(() => {
21
21
  })
22
22
 
23
23
  afterEach(() => {
24
- for (const name of used)
25
- delete process.env[name]
24
+ for (const name of used) delete process.env[name]
26
25
 
27
26
  used = []
28
27
  })
@@ -92,14 +91,21 @@ describe('local', () => {
92
91
  manifest.schema = {
93
92
  type: 'object',
94
93
  properties: {
95
- foo: { type: 'string', default: 'hello' },
94
+ foo: { type: 'string' },
96
95
  bar: { type: 'number' }
97
96
  }
98
97
  }
99
98
 
100
99
  set({ bar: 5 })
101
100
 
102
- assert.deepStrictEqual(local(locator, manifest), { foo: 'hello', bar: 5 })
101
+ assert.deepStrictEqual(local(locator, manifest), { bar: 5 })
102
+
103
+ set({ bar: 'five' })
104
+
105
+ assert.throws(
106
+ () => local(locator, manifest),
107
+ (error: Error) => /must be number/.test(error.message)
108
+ )
103
109
  })
104
110
  })
105
111
 
@@ -109,18 +115,18 @@ describe('fit', () => {
109
115
  type: 'object',
110
116
  properties: {
111
117
  foo: { type: 'string' },
112
- bar: { type: 'number', default: 1 }
118
+ bar: { type: 'number' }
113
119
  }
114
120
  }
115
121
 
116
122
  set('secret', '_FOO')
117
123
 
118
- const raw = { foo: '$FOO' }
124
+ const raw = { foo: '$FOO', bar: 1 }
119
125
  const values = fit(raw, manifest)
120
126
 
121
127
  assert.deepStrictEqual((values.foo as Secret).unwrap(), 'secret')
122
128
  assert.deepStrictEqual(values.bar, 1)
123
- assert.deepStrictEqual(raw, { foo: '$FOO' }) // untouched
129
+ assert.deepStrictEqual(raw, { foo: '$FOO', bar: 1 }) // untouched
124
130
  })
125
131
 
126
132
  it('should not apply the manifest defaults', async () => {
@@ -134,7 +140,7 @@ describe('fit', () => {
134
140
  })
135
141
  })
136
142
 
137
- function set (value: object | string, key = locator.uppercase): void {
143
+ function set(value: object | string, key = locator.uppercase): void {
138
144
  const string = typeof value === 'string' ? value : JSON.stringify(value)
139
145
  const name = 'TOA_CONFIGURATION_' + key
140
146
 
@@ -7,22 +7,21 @@ import type { Schema } from '@toa.io/schemas'
7
7
  import type { Manifest } from './manifest.js'
8
8
 
9
9
  /** The variable is set, so the values service is not consulted. */
10
- export function overridden (locator: Locator): boolean {
10
+ export function overridden(locator: Locator): boolean {
11
11
  return process.env[PREFIX + locator.uppercase] !== undefined
12
12
  }
13
13
 
14
14
  /** The variable, the manifest defaults, then the schema. */
15
- export function local (locator: Locator, manifest: Manifest): Node {
15
+ export function local(locator: Locator, manifest: Manifest): Node {
16
16
  const values = read(locator.uppercase)
17
17
 
18
- if (manifest.defaults !== undefined)
19
- add(values, manifest.defaults)
18
+ if (manifest.defaults !== undefined) add(values, manifest.defaults)
20
19
 
21
20
  return fit(values, manifest)
22
21
  }
23
22
 
24
23
  /** A copy of the value with the schema applied and the secrets substituted. */
25
- export function fit (raw: object, manifest: Manifest): Node {
24
+ export function fit(raw: object, manifest: Manifest): Node {
26
25
  // a copy of the caller's, and one of this realm: what came over the wire is JSON anyway
27
26
  const values = JSON.parse(JSON.stringify(raw)) as Node
28
27
 
@@ -33,26 +32,23 @@ export function fit (raw: object, manifest: Manifest): Node {
33
32
  return values
34
33
  }
35
34
 
36
- function validate (values: Node, manifest: Manifest): void {
35
+ function validate(values: Node, manifest: Manifest): void {
37
36
  const schema: Schema<any> = schemas.schema(manifest.schema)
38
37
 
39
38
  schema.validate(values)
40
39
  }
41
40
 
42
- function read (suffix: string): Node {
41
+ function read(suffix: string): Node {
43
42
  const variable = PREFIX + suffix
44
43
  const string = process.env[variable]
45
44
 
46
- if (string === undefined)
47
- return {}
48
- else
49
- return JSON.parse(string)
45
+ if (string === undefined) return {}
46
+ else return JSON.parse(string)
50
47
  }
51
48
 
52
- function substituteSecrets (configuration: Node): void {
49
+ function substituteSecrets(configuration: Node): void {
53
50
  for (const [key, value] of Object.entries(configuration)) {
54
- if (typeof value === 'object' && value !== null)
55
- substituteSecrets(value as Node)
51
+ if (typeof value === 'object' && value !== null) substituteSecrets(value as Node)
56
52
 
57
53
  if (typeof value !== 'string') continue
58
54
 
@@ -66,7 +62,7 @@ function substituteSecrets (configuration: Node): void {
66
62
  }
67
63
  }
68
64
 
69
- function getSecret (name: string): string {
65
+ function getSecret(name: string): string {
70
66
  const variable = PREFIX + '_' + name
71
67
  const value = process.env[variable]
72
68
 
package/source/const.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
  /** Per-component variables: the local override and the secrets. */
4
4
  export const PREFIX = 'TOA_CONFIGURATION_'
@@ -12,6 +12,18 @@ export const SECRET_RX = /^\$(?<variable>[A-Z0-9_]{1,32})$/
12
12
  export const UI_PATH = '/.configuration'
13
13
  export const UI_PORT = 8003
14
14
 
15
+ /**
16
+ * The port the UI is served on. `UI_PORT` is what a deployment publishes and what the chart
17
+ * renders; the variable is for a machine that runs more than one Toa process — a Toa checkout
18
+ * beside an application, or two applications — where one of them has to give it up.
19
+ */
20
+ export const UI_PORT_ENV = 'TOA_CONFIGURATION_UI_PORT'
21
+ export function uiPort(): number {
22
+ const value = process.env[UI_PORT_ENV]
23
+
24
+ return value === undefined ? UI_PORT : Number(value)
25
+ }
26
+
15
27
  export const EVENT = 'configuration.values.created'
16
28
 
17
29
  export const SOURCE: Source = { service: 'configuration' }
package/source/context.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { Manifest } from './manifest.js'
2
- import type { Contribution } from '@toa.io/core'
2
+ import type { Contribution } from '@toa.io/core/types'
3
3
 
4
4
  /** What this extension puts on the context of a component that declares it. */
5
- export function context (declaration: Manifest): Contribution {
5
+ export function context(declaration: Manifest): Contribution {
6
6
  const contribution: Contribution = { name: 'configuration', schema: declaration.schema }
7
7
 
8
8
  // a value the schema declares a secret is one on the context, not a string