@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.
- package/CHANGELOG.md +1 -365
- 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/{BzG-gka8.js → BW57cOwz.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{_I_MUoRn.js → Bhnl1Avs.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{Bxde3uE_.js → BxOvOexd.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{mvspXr3J.js → CkX_FNYa.js} +2 -2
- package/ui/dist/_app/immutable/chunks/{p9RyiXQ-.js → Dmv4yvb8.js} +1 -1
- package/ui/dist/_app/immutable/entry/{app.C1VIepN7.js → app.Cr7od9uH.js} +2 -2
- package/ui/dist/_app/immutable/entry/start.DWf-Tl0X.js +1 -0
- package/ui/dist/_app/immutable/nodes/{0.D_0dtFej.js → 0.CcGZUFKo.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{1.DFlgHh-H.js → 1.CR6En12y.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{2.BIHgAxXR.js → 2.BhyO8bGT.js} +1 -1
- package/ui/dist/_app/immutable/nodes/3.DcbMtJtb.js +1 -0
- package/ui/dist/_app/immutable/nodes/{4.BZMuJGfA.js → 4.qVnVQBNq.js} +1 -1
- package/ui/dist/_app/version.json +1 -1
- package/ui/dist/index.html +28 -23
- 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/features/steps/UI.ts
CHANGED
|
@@ -4,10 +4,15 @@ import { resolve } from 'node:path'
|
|
|
4
4
|
import tsflow from 'cucumber-tsflow'
|
|
5
5
|
|
|
6
6
|
import { UI } from '../../source/UI.js'
|
|
7
|
-
import { UI_PORT } from '../../source/const.js'
|
|
8
7
|
|
|
9
8
|
const { after, binding, given, then, when } = tsflow
|
|
10
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Not `UI_PORT`, which a deployment serves the page on: an application built on Toa is served
|
|
12
|
+
* on this machine too, and its own explorer holds that one. See CONTRIBUTING.md.
|
|
13
|
+
*/
|
|
14
|
+
const PORT = 31003
|
|
15
|
+
|
|
11
16
|
/**
|
|
12
17
|
* The scenarios are about the server, not about the page: it is pointed at a fixture
|
|
13
18
|
* directory, so that running them never needs a UI build.
|
|
@@ -18,40 +23,42 @@ export class Site {
|
|
|
18
23
|
private response: Response | null = null
|
|
19
24
|
|
|
20
25
|
@given('the UI is published')
|
|
21
|
-
public async publish
|
|
22
|
-
this.server = new UI(
|
|
26
|
+
public async publish(): Promise<void> {
|
|
27
|
+
this.server = new UI(PORT, resolve(import.meta.dirname, '..', 'site'))
|
|
23
28
|
|
|
24
29
|
await this.server.connect()
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
@when('{string} is requested')
|
|
28
|
-
public async request
|
|
33
|
+
public async request(path: string): Promise<void> {
|
|
29
34
|
this.response = await get(path)
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
@then('the status is {int}')
|
|
33
|
-
public statusIs
|
|
38
|
+
public statusIs(status: number): void {
|
|
34
39
|
assert.equal(this.response?.status, status)
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
@then('the body contains {string}')
|
|
38
|
-
public bodyContains
|
|
39
|
-
assert.ok(
|
|
40
|
-
|
|
43
|
+
public bodyContains(text: string): void {
|
|
44
|
+
assert.ok(
|
|
45
|
+
this.response?.body.includes(text),
|
|
46
|
+
`Expected the body to contain '${text}', got '${this.response?.body}'`
|
|
47
|
+
)
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
@then('the body is empty')
|
|
44
|
-
public bodyIsEmpty
|
|
51
|
+
public bodyIsEmpty(): void {
|
|
45
52
|
assert.equal(this.response?.body, '')
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
@then('the {string} header is {string}')
|
|
49
|
-
public headerIs
|
|
56
|
+
public headerIs(name: string, value: string): void {
|
|
50
57
|
assert.equal(this.response?.headers[name], value)
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
@after()
|
|
54
|
-
public async shutdown
|
|
61
|
+
public async shutdown(): Promise<void> {
|
|
55
62
|
await this.server?.disconnect()
|
|
56
63
|
|
|
57
64
|
this.server = null
|
|
@@ -63,10 +70,10 @@ export class Site {
|
|
|
63
70
|
* Raw `http`, not `fetch`: the URL parser normalizes `%2e%2e` away, and one of the
|
|
64
71
|
* scenarios is about exactly that segment reaching the server.
|
|
65
72
|
*/
|
|
66
|
-
async function get
|
|
73
|
+
async function get(path: string): Promise<Response> {
|
|
67
74
|
return await new Promise((resolve, reject) => {
|
|
68
75
|
// `agent: false` — a keep-alive socket outlives the scenario that opened it
|
|
69
|
-
const request = http.get({ port:
|
|
76
|
+
const request = http.get({ port: PORT, path, agent: false }, (response) => {
|
|
70
77
|
let body = ''
|
|
71
78
|
|
|
72
79
|
response.setEncoding('utf8')
|
package/notes/consistency.md
CHANGED
|
@@ -33,15 +33,15 @@ single moment when configuration can be safely changed. Thus, it must be a proce
|
|
|
33
33
|
|
|
34
34
|
1. Configuration updates are always backward compatible.
|
|
35
35
|
2. Configuration updates are always being delivered before algorithm updates.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
1. Including federated deployment, that is: first deliver configuration to all facilities (data
|
|
37
|
+
centers, zones,
|
|
38
|
+
whatever), then deliver algorithm updates.
|
|
39
39
|
3. Configuration storage and access solution must provide the transactional updates, that is if any
|
|
40
40
|
arbitrary
|
|
41
41
|
participant observed a certain configuration version, then any other participant is guaranteed to
|
|
42
42
|
be able to
|
|
43
43
|
subsequently observe that version.
|
|
44
|
-
|
|
44
|
+
1. Including federated deployment[^1].
|
|
45
45
|
|
|
46
46
|
## Solution
|
|
47
47
|
|
|
@@ -62,8 +62,9 @@ However, an attempt
|
|
|
62
62
|
will be made to implement this solution with a certain
|
|
63
63
|
constraints [#147](https://github.com/toa-io/toa/issues/147).
|
|
64
64
|
|
|
65
|
-
[^1]:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
[^1]:
|
|
66
|
+
Since it looks like there is no reasonable way to provide this kind of guarantee without
|
|
67
|
+
significant performance
|
|
68
|
+
and/or availability impact, it may be implemented as a mechanism with read retries and with a
|
|
69
|
+
timeout considered as
|
|
70
|
+
“enough for the most of fail-over scenarios”.
|
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.287",
|
|
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": "5c7944a70c16065ab6372072385cc1f02f5db77c",
|
|
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.287",
|
|
29
|
+
"@toa.io/generic": "1.0.0-alpha.286",
|
|
30
|
+
"@toa.io/schemas": "1.0.0-alpha.287",
|
|
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)
|