@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.
- package/components/configuration.values/manifest.toa.yaml +14 -21
- package/components/configuration.values/migrations/0001-system-properties.yaml +14 -0
- package/components/configuration.values/migrations/0002-indexes.yaml +7 -0
- package/components/configuration.values/operations/create.js +1 -2
- package/components/configuration.values/operations/fetch.js +1 -1
- package/components/configuration.values/operations/get.js +1 -1
- package/components/configuration.values/operations/lib/map.js +1 -1
- package/components/configuration.values/operations/lib/resolve.js +3 -3
- package/components/configuration.values/operations/list.js +1 -1
- package/components/configuration.values/source/create.ts +3 -5
- package/components/configuration.values/source/fetch.ts +12 -10
- package/components/configuration.values/source/get.ts +2 -3
- package/components/configuration.values/source/lib/map.test.ts +7 -2
- package/components/configuration.values/source/lib/map.ts +3 -3
- package/components/configuration.values/source/lib/resolve.ts +9 -6
- package/components/configuration.values/source/list.ts +8 -6
- package/components/configuration.values/tsconfig.tsbuildinfo +1 -1
- package/components/configuration.values/types/toa.d.ts +5 -5
- package/cucumber.mjs +3 -0
- package/features/site/index.html +6 -2
- package/features/steps/UI.ts +20 -13
- package/notes/consistency.md +10 -9
- package/package.json +6 -6
- package/readme.md +15 -14
- package/schemas/annotation.cos.yaml +1 -1
- package/source/Aspect.test.ts +32 -8
- package/source/Aspect.ts +28 -19
- package/source/Client.test.ts +31 -14
- package/source/Client.ts +41 -48
- package/source/Composition.ts +6 -6
- package/source/Factory.ts +7 -7
- package/source/Secret.test.ts +1 -1
- package/source/Secret.ts +5 -5
- package/source/UI.ts +33 -32
- package/source/configuration.test.ts +14 -8
- package/source/configuration.ts +11 -15
- package/source/const.ts +13 -1
- package/source/context.ts +2 -2
- package/source/deployment.test.ts +37 -12
- package/source/deployment.ts +45 -19
- package/source/epoch.test.ts +8 -2
- package/source/epoch.ts +6 -5
- package/source/index.ts +1 -1
- package/source/manifest.test.ts +12 -6
- package/source/manifest.ts +1 -1
- package/transpiled/Aspect.d.ts +2 -1
- package/transpiled/Aspect.js +15 -3
- package/transpiled/Aspect.js.map +1 -1
- package/transpiled/Client.d.ts +1 -1
- package/transpiled/Client.js +8 -2
- package/transpiled/Client.js.map +1 -1
- package/transpiled/Composition.js +2 -2
- package/transpiled/Composition.js.map +1 -1
- package/transpiled/Factory.d.ts +2 -1
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Secret.js.map +1 -1
- package/transpiled/UI.js +2 -1
- package/transpiled/UI.js.map +1 -1
- package/transpiled/configuration.js.map +1 -1
- package/transpiled/const.d.ts +8 -1
- package/transpiled/const.js +10 -0
- package/transpiled/const.js.map +1 -1
- package/transpiled/context.d.ts +1 -1
- package/transpiled/context.js.map +1 -1
- package/transpiled/deployment.d.ts +3 -0
- package/transpiled/deployment.js +19 -5
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/epoch.js +3 -1
- package/transpiled/epoch.js.map +1 -1
- package/transpiled/index.d.ts +1 -1
- package/transpiled/index.js +1 -1
- package/transpiled/index.js.map +1 -1
- package/transpiled/manifest.js.map +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/ui/dist/_app/immutable/chunks/{mvspXr3J.js → 6fUKYVLr.js} +2 -2
- package/ui/dist/_app/immutable/chunks/{p9RyiXQ-.js → BGxZNgfH.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{Bxde3uE_.js → By3hKJVT.js} +1 -1
- package/ui/dist/_app/immutable/chunks/DamfpXnK.js +1 -0
- package/ui/dist/_app/immutable/chunks/{_I_MUoRn.js → vHown6GG.js} +1 -1
- package/ui/dist/_app/immutable/entry/{app.C1VIepN7.js → app.cjzrCTnf.js} +2 -2
- package/ui/dist/_app/immutable/entry/start.C18omPso.js +1 -0
- package/ui/dist/_app/immutable/nodes/{0.D_0dtFej.js → 0.DjKXI3st.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{1.DFlgHh-H.js → 1.Dh71z_Aw.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{2.BIHgAxXR.js → 2.TzrOmdn2.js} +1 -1
- package/ui/dist/_app/immutable/nodes/3.DlvWZctZ.js +1 -0
- package/ui/dist/_app/immutable/nodes/{4.BZMuJGfA.js → 4.D8kjaqXl.js} +1 -1
- package/ui/dist/_app/version.json +1 -1
- package/ui/dist/index.html +28 -23
- package/CHANGELOG.md +0 -372
- package/ui/dist/_app/immutable/chunks/BzG-gka8.js +0 -1
- package/ui/dist/_app/immutable/entry/start.BQWCs_Pq.js +0 -1
- 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.
|
|
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": "
|
|
26
|
+
"gitHead": "81f3d0688f17668b902be336d1749cc715edcdb6",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@toa.io/core": "1.0.0-alpha.
|
|
29
|
-
"@toa.io/generic": "1.0.0-alpha.
|
|
30
|
-
"@toa.io/schemas": "1.0.0-alpha.
|
|
31
|
-
"openspan": "1.0.0-alpha.
|
|
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
|
|
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
|
|
41
|
+
foo: qux # override default value
|
|
42
42
|
foo@staging: quux # deployment environment discriminator
|
|
43
|
-
bar: $BAZ_VALUE
|
|
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
|
|
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
|
|
235
|
-
|
|
236
|
-
| `GET` | `/configuration/values/`
|
|
237
|
-
| `GET` | `/configuration/values/:component/`
|
|
238
|
-
| `POST` | `/configuration/values/:component/`
|
|
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
|
|
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.
|
|
274
|
-
|
|
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
|
|
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
|
package/source/Aspect.test.ts
CHANGED
|
@@ -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 () => ({
|
|
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' } }
|
|
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 [
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/source/Client.test.ts
CHANGED
|
@@ -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
|
|
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(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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([
|
|
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: [
|
|
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 },
|
|
105
|
+
payload: { component: 'a.one', epoch: 'e1', configuration: { foo: 2 }, CREATED: 12 }
|
|
95
106
|
} satisfies Message)
|
|
96
107
|
|
|
97
|
-
assert.ok(
|
|
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 },
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
87
|
+
protected override async dispose(): Promise<void> {
|
|
90
88
|
this.disposed = true
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
private schedule
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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', {
|
|
164
|
+
console.warn('Waiting for configuration', {
|
|
165
|
+
components: waiting,
|
|
166
|
+
round: this.round
|
|
167
|
+
})
|
|
176
168
|
}
|
|
177
169
|
|
|
178
|
-
private backoff
|
|
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
|
|
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 = {
|
|
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
|
|
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
|
|
199
|
+
public async receive(message: Message<Created>): Promise<void> {
|
|
207
200
|
this.handler(message.payload)
|
|
208
201
|
}
|
|
209
202
|
}
|
|
210
203
|
|
|
211
|
-
function id
|
|
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
|
-
|
|
242
|
+
CREATED: number
|
|
250
243
|
}
|
|
251
244
|
|
|
252
245
|
export type Listener = (value: Value) => void
|
package/source/Composition.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
12
|
+
public constructor(host: Host) {
|
|
13
13
|
super()
|
|
14
14
|
this.host = host
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
protected override async open
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
13
|
+
public constructor(host: Host) {
|
|
13
14
|
this.host = host
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
public 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
|
|
23
|
+
public service(): Connector {
|
|
23
24
|
return new Composition(this.host)
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
private shared
|
|
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
|
}
|