@toa.io/extensions.configuration 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 (92) hide show
  1. package/components/configuration.values/manifest.toa.yaml +14 -21
  2. package/components/configuration.values/migrations/0001-system-properties.yaml +14 -0
  3. package/components/configuration.values/migrations/0002-indexes.yaml +7 -0
  4. package/components/configuration.values/operations/create.js +1 -2
  5. package/components/configuration.values/operations/fetch.js +1 -1
  6. package/components/configuration.values/operations/get.js +1 -1
  7. package/components/configuration.values/operations/lib/map.js +1 -1
  8. package/components/configuration.values/operations/lib/resolve.js +3 -3
  9. package/components/configuration.values/operations/list.js +1 -1
  10. package/components/configuration.values/source/create.ts +3 -5
  11. package/components/configuration.values/source/fetch.ts +12 -10
  12. package/components/configuration.values/source/get.ts +2 -3
  13. package/components/configuration.values/source/lib/map.test.ts +7 -2
  14. package/components/configuration.values/source/lib/map.ts +3 -3
  15. package/components/configuration.values/source/lib/resolve.ts +9 -6
  16. package/components/configuration.values/source/list.ts +8 -6
  17. package/components/configuration.values/tsconfig.tsbuildinfo +1 -1
  18. package/components/configuration.values/types/toa.d.ts +5 -5
  19. package/cucumber.mjs +3 -0
  20. package/features/site/index.html +6 -2
  21. package/features/steps/UI.ts +20 -13
  22. package/notes/consistency.md +10 -9
  23. package/package.json +6 -6
  24. package/readme.md +15 -14
  25. package/schemas/annotation.cos.yaml +1 -1
  26. package/source/Aspect.test.ts +32 -8
  27. package/source/Aspect.ts +28 -19
  28. package/source/Client.test.ts +31 -14
  29. package/source/Client.ts +41 -48
  30. package/source/Composition.ts +6 -6
  31. package/source/Factory.ts +7 -7
  32. package/source/Secret.test.ts +1 -1
  33. package/source/Secret.ts +5 -5
  34. package/source/UI.ts +33 -32
  35. package/source/configuration.test.ts +14 -8
  36. package/source/configuration.ts +11 -15
  37. package/source/const.ts +13 -1
  38. package/source/context.ts +2 -2
  39. package/source/deployment.test.ts +37 -12
  40. package/source/deployment.ts +45 -19
  41. package/source/epoch.test.ts +8 -2
  42. package/source/epoch.ts +6 -5
  43. package/source/index.ts +1 -1
  44. package/source/manifest.test.ts +12 -6
  45. package/source/manifest.ts +1 -1
  46. package/transpiled/Aspect.d.ts +2 -1
  47. package/transpiled/Aspect.js +15 -3
  48. package/transpiled/Aspect.js.map +1 -1
  49. package/transpiled/Client.d.ts +1 -1
  50. package/transpiled/Client.js +8 -2
  51. package/transpiled/Client.js.map +1 -1
  52. package/transpiled/Composition.js +2 -2
  53. package/transpiled/Composition.js.map +1 -1
  54. package/transpiled/Factory.d.ts +2 -1
  55. package/transpiled/Factory.js.map +1 -1
  56. package/transpiled/Secret.js.map +1 -1
  57. package/transpiled/UI.js +2 -1
  58. package/transpiled/UI.js.map +1 -1
  59. package/transpiled/configuration.js.map +1 -1
  60. package/transpiled/const.d.ts +8 -1
  61. package/transpiled/const.js +10 -0
  62. package/transpiled/const.js.map +1 -1
  63. package/transpiled/context.d.ts +1 -1
  64. package/transpiled/context.js.map +1 -1
  65. package/transpiled/deployment.d.ts +3 -0
  66. package/transpiled/deployment.js +19 -5
  67. package/transpiled/deployment.js.map +1 -1
  68. package/transpiled/epoch.js +3 -1
  69. package/transpiled/epoch.js.map +1 -1
  70. package/transpiled/index.d.ts +1 -1
  71. package/transpiled/index.js +1 -1
  72. package/transpiled/index.js.map +1 -1
  73. package/transpiled/manifest.js.map +1 -1
  74. package/tsconfig.tsbuildinfo +1 -1
  75. package/ui/dist/_app/immutable/chunks/{mvspXr3J.js → 6fUKYVLr.js} +2 -2
  76. package/ui/dist/_app/immutable/chunks/{p9RyiXQ-.js → BGxZNgfH.js} +1 -1
  77. package/ui/dist/_app/immutable/chunks/{Bxde3uE_.js → By3hKJVT.js} +1 -1
  78. package/ui/dist/_app/immutable/chunks/DamfpXnK.js +1 -0
  79. package/ui/dist/_app/immutable/chunks/{_I_MUoRn.js → vHown6GG.js} +1 -1
  80. package/ui/dist/_app/immutable/entry/{app.C1VIepN7.js → app.cjzrCTnf.js} +2 -2
  81. package/ui/dist/_app/immutable/entry/start.C18omPso.js +1 -0
  82. package/ui/dist/_app/immutable/nodes/{0.D_0dtFej.js → 0.DjKXI3st.js} +1 -1
  83. package/ui/dist/_app/immutable/nodes/{1.DFlgHh-H.js → 1.Dh71z_Aw.js} +1 -1
  84. package/ui/dist/_app/immutable/nodes/{2.BIHgAxXR.js → 2.TzrOmdn2.js} +1 -1
  85. package/ui/dist/_app/immutable/nodes/3.DlvWZctZ.js +1 -0
  86. package/ui/dist/_app/immutable/nodes/{4.BZMuJGfA.js → 4.D8kjaqXl.js} +1 -1
  87. package/ui/dist/_app/version.json +1 -1
  88. package/ui/dist/index.html +28 -23
  89. package/CHANGELOG.md +0 -372
  90. package/ui/dist/_app/immutable/chunks/BzG-gka8.js +0 -1
  91. package/ui/dist/_app/immutable/entry/start.BQWCs_Pq.js +0 -1
  92. package/ui/dist/_app/immutable/nodes/3.R-_yCt5X.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.configuration",
3
- "version": "1.0.0-alpha.284",
3
+ "version": "1.0.0-alpha.286",
4
4
  "type": "module",
5
5
  "description": "Toa Configuration",
6
6
  "author": "temich <tema.gurtovoy@gmail.com>",
@@ -23,11 +23,11 @@
23
23
  "prepack": "npm run build:ui",
24
24
  "features": "TSX_TSCONFIG_PATH=features/steps/tsconfig.json NODE_OPTIONS=--import=tsx cucumber-js"
25
25
  },
26
- "gitHead": "006755556879e91e8d68fa86753aa3b475bdf4bf",
26
+ "gitHead": "81f3d0688f17668b902be336d1749cc715edcdb6",
27
27
  "dependencies": {
28
- "@toa.io/core": "1.0.0-alpha.284",
29
- "@toa.io/generic": "1.0.0-alpha.283",
30
- "@toa.io/schemas": "1.0.0-alpha.283",
31
- "openspan": "1.0.0-alpha.277"
28
+ "@toa.io/core": "1.0.0-alpha.286",
29
+ "@toa.io/generic": "1.0.0-alpha.286",
30
+ "@toa.io/schemas": "1.0.0-alpha.286",
31
+ "openspan": "1.0.0-alpha.286"
32
32
  }
33
33
  }
package/readme.md CHANGED
@@ -25,7 +25,7 @@ configuration:
25
25
  ### Use
26
26
 
27
27
  ```javascript
28
- function transition (input, entity, context) {
28
+ function transition(input, entity, context) {
29
29
  const { foo, bar } = context.configuration
30
30
 
31
31
  // ...
@@ -38,9 +38,9 @@ function transition (input, entity, context) {
38
38
  # context.toa.yaml
39
39
  configuration:
40
40
  dummies.dummy:
41
- foo: qux # override default value
41
+ foo: qux # override default value
42
42
  foo@staging: quux # deployment environment discriminator
43
- bar: $BAZ_VALUE # secret
43
+ bar: $BAZ_VALUE # secret
44
44
  ```
45
45
 
46
46
  ### Deploy secrets
@@ -176,7 +176,7 @@ In the component, a secret is a `Secret` object: `unwrap()` returns the string,
176
176
  `toString()`, JSON and `util.inspect` give `<REDACTED>`.
177
177
 
178
178
  ```javascript
179
- function transition (input, entity, context) {
179
+ function transition(input, entity, context) {
180
180
  const key = context.configuration.apiKey.unwrap()
181
181
 
182
182
  // ...
@@ -231,11 +231,11 @@ object as stored.
231
231
 
232
232
  ### Resources
233
233
 
234
- | Method | Path | Role |
235
- |--------|---------------------------------------|-------------------------------|
236
- | `GET` | `/configuration/values/` | `system:configuration:get` |
237
- | `GET` | `/configuration/values/:component/` | `system:configuration:get` |
238
- | `POST` | `/configuration/values/:component/` | `system:configuration:create` |
234
+ | Method | Path | Role |
235
+ | ------ | ----------------------------------- | ----------------------------- |
236
+ | `GET` | `/configuration/values/` | `system:configuration:get` |
237
+ | `GET` | `/configuration/values/:component/` | `system:configuration:get` |
238
+ | `POST` | `/configuration/values/:component/` | `system:configuration:create` |
239
239
 
240
240
  `GET /configuration/values/` lists every component's configuration for its deployed epoch, by
241
241
  component name, as `[{ component, epoch, schema, configuration }]`.
@@ -261,7 +261,7 @@ annotation is the per-component values map and has nowhere to carry a switch.
261
261
  Component's configuration values are available as a well-known Aspect `configuration`.
262
262
 
263
263
  ```javascript
264
- function transition (input, entity, context) {
264
+ function transition(input, entity, context) {
265
265
  const foo = context.configuration.foo
266
266
 
267
267
  // ...
@@ -270,14 +270,15 @@ function transition (input, entity, context) {
270
270
 
271
271
  On start, a component requests its configuration for its epoch from the values service and
272
272
  waits until there is one, reporting every fifth attempt. The schema is applied, and secrets
273
- are substituted. After a configuration is created, the running component receives the new
274
- object and takes it when its `_created` is later than that of the value it holds.
273
+ are substituted. What is served is what was stored, whole: a `default` written into the schema
274
+ fills nothing, so a value every component is to have is declared in `defaults`. After a configuration is created, the running component receives the new
275
+ object and takes it when its `CREATED` is later than that of the value it holds.
275
276
 
276
277
  ### Local override
277
278
 
278
279
  When the variable `TOA_CONFIGURATION_<NAMESPACE>_<NAME>` is set, the component's
279
- configuration is the variable's value with the manifest `defaults` and the schema applied,
280
- and the values service is not used.
280
+ configuration is the variable's value with the manifest `defaults` filled in and the schema
281
+ applied, and the values service is not used.
281
282
 
282
283
  ```shell
283
284
  $ TOA_CONFIGURATION_DUMMIES_DUMMY='{"foo":"local"}' toa run components/dummy
@@ -7,7 +7,7 @@
7
7
  properties:
8
8
  resources:
9
9
  # `null` deploys the service without any, which every deployment has to state one way
10
- # or the other — see `migrations/263.md`.
10
+ # or the other — see `migrations/262.md`.
11
11
  type: object
12
12
  nullable: true
13
13
  properties:
@@ -9,7 +9,10 @@ import type { Client, Listener } from './Client.js'
9
9
  import type { Manifest } from './manifest.js'
10
10
 
11
11
  class Fake extends Connector {
12
- public readonly fetch = mock.fn(async () => ({ configuration: { foo: 'served' }, created: 5 }))
12
+ public readonly fetch = mock.fn(async () => ({
13
+ configuration: { foo: 'served' },
14
+ created: 5
15
+ }))
13
16
  public readonly subscribe = mock.fn()
14
17
  public readonly unsubscribe = mock.fn()
15
18
  }
@@ -19,9 +22,10 @@ const manifest: Manifest = {
19
22
  type: 'object',
20
23
  properties: {
21
24
  foo: { type: 'string' },
22
- bar: { type: 'object', properties: { baz: { type: 'string' } }, default: { baz: 'quux' } }
25
+ bar: { type: 'object', properties: { baz: { type: 'string' } } }
23
26
  }
24
- }
27
+ },
28
+ defaults: { bar: { baz: 'quux' } }
25
29
  }
26
30
 
27
31
  let locator: Locator
@@ -59,13 +63,25 @@ it('should fetch from the client and follow it', async () => {
59
63
  assert.deepStrictEqual(client.connected, true)
60
64
  assert.strictEqual(client.fetch.mock.callCount(), 1)
61
65
 
62
- const [component, epoch] = client.fetch.mock.calls[0].arguments as unknown as [string, string]
66
+ const [component, epoch] = client.fetch.mock.calls[0].arguments as unknown as [
67
+ string,
68
+ string
69
+ ]
63
70
 
64
71
  assert.deepStrictEqual(component, locator.id)
65
72
  assert.match(epoch, /^[a-f0-9]{64}$/)
66
- assert.deepStrictEqual(aspect.invoke(), { foo: 'served', bar: { baz: 'quux' } })
67
-
68
- assert.ok(client.subscribe.mock.calls.some((call: any) => call.arguments.length === 3 && isDeepStrictEqual(call.arguments[0], component) && isDeepStrictEqual(call.arguments[1], epoch) && typeof call.arguments[2] === 'function'))
73
+ // what the service serves is what was stored, and the manifest defaults are not applied to it
74
+ assert.deepStrictEqual(aspect.invoke(), { foo: 'served' })
75
+
76
+ assert.ok(
77
+ client.subscribe.mock.calls.some(
78
+ (call: any) =>
79
+ call.arguments.length === 3 &&
80
+ isDeepStrictEqual(call.arguments[0], component) &&
81
+ isDeepStrictEqual(call.arguments[1], epoch) &&
82
+ typeof call.arguments[2] === 'function'
83
+ )
84
+ )
69
85
 
70
86
  const listener = client.subscribe.mock.calls[0].arguments[2] as Listener
71
87
 
@@ -91,5 +107,13 @@ it('should fetch from the client and follow it', async () => {
91
107
 
92
108
  await aspect.disconnect()
93
109
 
94
- assert.ok(client.unsubscribe.mock.calls.some((call: any) => call.arguments.length === 3 && isDeepStrictEqual(call.arguments[0], component) && isDeepStrictEqual(call.arguments[1], epoch) && isDeepStrictEqual(call.arguments[2], listener)))
110
+ assert.ok(
111
+ client.unsubscribe.mock.calls.some(
112
+ (call: any) =>
113
+ call.arguments.length === 3 &&
114
+ isDeepStrictEqual(call.arguments[0], component) &&
115
+ isDeepStrictEqual(call.arguments[1], epoch) &&
116
+ isDeepStrictEqual(call.arguments[2], listener)
117
+ )
118
+ )
95
119
  })
package/source/Aspect.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { console } from 'openspan'
2
- import { Connector, type Locator, type extensions } from '@toa.io/core'
2
+ import { Connector, type Locator } from '@toa.io/core'
3
+ import type { extensions } from '@toa.io/core/types'
3
4
  import { fit, local, type Node } from './configuration.js'
4
5
  import { epoch } from './epoch.js'
5
6
  import type { Client, Value } from './Client.js'
@@ -19,7 +20,7 @@ export class Aspect extends Connector implements extensions.Aspect {
19
20
  * Without a client the value is local: the variable, the defaults and the schema.
20
21
  * With one, the value is what the service holds, and it follows the service.
21
22
  */
22
- public constructor (locator: Locator, manifest: Manifest, client: Client | null) {
23
+ public constructor(locator: Locator, manifest: Manifest, client: Client | null) {
23
24
  super()
24
25
 
25
26
  this.locator = locator
@@ -27,56 +28,64 @@ export class Aspect extends Connector implements extensions.Aspect {
27
28
  this.client = client
28
29
  this.epoch = epoch(manifest.schema)
29
30
 
30
- if (client !== null)
31
- this.depends(client)
31
+ if (client !== null) this.depends(client)
32
32
  }
33
33
 
34
- public invoke (path?: string[]): any {
34
+ public invoke(path?: string[]): any {
35
35
  let cursor: any = this.value
36
36
 
37
- if (path !== undefined)
38
- for (const segment of path)
39
- cursor = cursor[segment]
37
+ if (path !== undefined) for (const segment of path) cursor = cursor[segment]
40
38
 
41
39
  return cursor
42
40
  }
43
41
 
44
- protected override async open (): Promise<void> {
42
+ protected override async open(): Promise<void> {
45
43
  if (this.client === null) {
46
44
  this.value = local(this.locator, this.manifest)
47
45
 
48
46
  return
49
47
  }
50
48
 
51
- const { configuration, created } = await this.client.fetch(this.locator.id, this.epoch)
49
+ const { configuration, created } = await this.client.fetch(
50
+ this.locator.id,
51
+ this.epoch
52
+ )
52
53
 
53
54
  this.value = fit(configuration, this.manifest)
54
55
  this.created = created
55
56
  this.client.subscribe(this.locator.id, this.epoch, this.listener)
56
57
 
57
- console.info('Configuration resolved',
58
- { component: this.locator.id, epoch: this.epoch, created })
58
+ console.info('Configuration resolved', {
59
+ component: this.locator.id,
60
+ epoch: this.epoch,
61
+ created
62
+ })
59
63
  }
60
64
 
61
- protected override async close (): Promise<void> {
65
+ protected override async close(): Promise<void> {
62
66
  this.client?.unsubscribe(this.locator.id, this.epoch, this.listener)
63
67
  }
64
68
 
65
69
  private readonly listener = ({ configuration, created }: Value): void => {
66
70
  // deliveries may repeat or cross: only what is newer than the held value replaces it
67
- if (created <= this.created)
68
- return
71
+ if (created <= this.created) return
69
72
 
70
73
  try {
71
74
  this.value = fit(configuration, this.manifest)
72
75
  this.created = created
73
76
 
74
- console.info('Configuration updated',
75
- { component: this.locator.id, epoch: this.epoch, created })
77
+ console.info('Configuration updated', {
78
+ component: this.locator.id,
79
+ epoch: this.epoch,
80
+ created
81
+ })
76
82
  } catch (error) {
77
83
  // the service validated it against the schema of its epoch, so the two schemas differ
78
- console.error('Configuration value does not match the schema',
79
- { component: this.locator.id, epoch: this.epoch, error })
84
+ console.error('Configuration value does not match the schema', {
85
+ component: this.locator.id,
86
+ epoch: this.epoch,
87
+ error
88
+ })
80
89
  }
81
90
  }
82
91
  }
@@ -2,19 +2,25 @@ import { it, beforeEach, afterEach, mock } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
  import { isDeepStrictEqual } from 'node:util'
4
4
 
5
- import { Connector, type Locator, type Message, type Receiver } from '@toa.io/core'
5
+ import { Connector, type Locator, type Receiver } from '@toa.io/core'
6
+ import type { Message } from '@toa.io/core/types'
6
7
  import { timeout } from '@toa.io/generic'
7
8
  import { Client, type Fetched } from './Client.js'
8
9
  import { EVENT } from './const.js'
9
10
  import type { Host } from './Factory.js'
10
11
 
11
12
  class Remote extends Connector {
12
- public readonly invoke = mock.fn(async (_endpoint: string, request: { input: Array<{ component: string, epoch: string }> }) =>
13
- request.input.map((pair): Fetched => ({
14
- ...pair,
15
- configuration: this.values[pair.component] ?? null,
16
- created: this.values[pair.component] === undefined ? 0 : 7
17
- })))
13
+ public readonly invoke = mock.fn(
14
+ async (
15
+ _endpoint: string,
16
+ request: { input: Array<{ component: string; epoch: string }> }
17
+ ) =>
18
+ request.input.map((pair): Fetched => ({
19
+ ...pair,
20
+ configuration: this.values[pair.component] ?? null,
21
+ created: this.values[pair.component] === undefined ? 0 : 7
22
+ }))
23
+ )
18
24
 
19
25
  public values: Record<string, object | null> = {}
20
26
  }
@@ -55,13 +61,19 @@ it('should send the requests of one tick as one call', async () => {
55
61
 
56
62
  await client.connect()
57
63
 
58
- const [one, two] = await Promise.all([client.fetch('a.one', 'e1'), client.fetch('a.two', 'e2')])
64
+ const [one, two] = await Promise.all([
65
+ client.fetch('a.one', 'e1'),
66
+ client.fetch('a.two', 'e2')
67
+ ])
59
68
 
60
69
  assert.deepStrictEqual(one, { configuration: { foo: 1 }, created: 7 })
61
70
  assert.deepStrictEqual(two, { configuration: { foo: 2 }, created: 7 })
62
71
  assert.strictEqual(remote.invoke.mock.callCount(), 1)
63
72
  assert.deepStrictEqual(remote.invoke.mock.calls[0].arguments[1], {
64
- input: [{ component: 'a.one', epoch: 'e1' }, { component: 'a.two', epoch: 'e2' }]
73
+ input: [
74
+ { component: 'a.one', epoch: 'e1' },
75
+ { component: 'a.two', epoch: 'e2' }
76
+ ]
65
77
  })
66
78
  })
67
79
 
@@ -71,8 +83,7 @@ it('should keep asking until served', async () => {
71
83
  const fetching = client.fetch('a.one', 'e1')
72
84
  const deadline = Date.now() + 1000
73
85
 
74
- while (remote.invoke.mock.calls.length < 2 && Date.now() < deadline)
75
- await timeout(5)
86
+ while (remote.invoke.mock.calls.length < 2 && Date.now() < deadline) await timeout(5)
76
87
 
77
88
  assert.ok(remote.invoke.mock.calls.length >= 2)
78
89
 
@@ -91,17 +102,23 @@ it('should hand a created object to its subscribers', async () => {
91
102
  client.subscribe('a.one', 'e0', other)
92
103
 
93
104
  await receiver!.receive({
94
- payload: { component: 'a.one', epoch: 'e1', configuration: { foo: 2 }, _created: 12 }
105
+ payload: { component: 'a.one', epoch: 'e1', configuration: { foo: 2 }, CREATED: 12 }
95
106
  } satisfies Message)
96
107
 
97
- assert.ok(listener.mock.calls.some((call: any) => call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], { configuration: { foo: 2 }, created: 12 })))
108
+ assert.ok(
109
+ listener.mock.calls.some(
110
+ (call: any) =>
111
+ call.arguments.length === 1 &&
112
+ isDeepStrictEqual(call.arguments[0], { configuration: { foo: 2 }, created: 12 })
113
+ )
114
+ )
98
115
  assert.strictEqual(other.mock.callCount(), 0)
99
116
  assert.strictEqual(remote.invoke.mock.callCount(), 0)
100
117
 
101
118
  client.unsubscribe('a.one', 'e1', listener)
102
119
 
103
120
  await receiver!.receive({
104
- payload: { component: 'a.one', epoch: 'e1', configuration: { foo: 3 }, _created: 13 }
121
+ payload: { component: 'a.one', epoch: 'e1', configuration: { foo: 3 }, CREATED: 13 }
105
122
  } satisfies Message)
106
123
 
107
124
  assert.strictEqual(listener.mock.callCount(), 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
  }