@toa.io/extensions.configuration 1.0.0-alpha.291 → 1.0.0-alpha.293
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/package.json +16 -6
- package/transpiled/Aspect.d.ts +1 -1
- package/transpiled/Aspect.js +1 -1
- package/transpiled/Aspect.js.map +1 -1
- package/transpiled/Client.js +1 -1
- package/transpiled/Client.js.map +1 -1
- package/transpiled/Composition.js +1 -1
- package/transpiled/Composition.js.map +1 -1
- package/transpiled/Factory.d.ts +1 -1
- package/transpiled/UI.js +1 -1
- package/transpiled/UI.js.map +1 -1
- package/transpiled/configuration.d.ts +1 -1
- package/transpiled/configuration.js +1 -1
- package/transpiled/configuration.js.map +1 -1
- package/transpiled/index.d.ts +0 -3
- package/transpiled/index.js +0 -4
- package/transpiled/index.js.map +1 -1
- package/ui/dist/_app/immutable/chunks/BVp7FTPj.js +1 -0
- package/ui/dist/_app/immutable/chunks/{CNZAv2Es.js → Ba25-8aA.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{BV5cDXGB.js → D5h0LnT-.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{vl8APhvf.js → DJFe2eVw.js} +2 -2
- package/ui/dist/_app/immutable/chunks/{B_CH3nFk.js → iBEDxyKc.js} +1 -1
- package/ui/dist/_app/immutable/entry/{app.CK-db-gz.js → app.BaUwlvvj.js} +2 -2
- package/ui/dist/_app/immutable/entry/start.BQIMr3E0.js +1 -0
- package/ui/dist/_app/immutable/nodes/{0.vPwVABRs.js → 0.Bdnf_m1Q.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{1.DS63PME5.js → 1.ChFvQfy2.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{2.KRpWYBfz.js → 2.DoZuKZSw.js} +1 -1
- package/ui/dist/_app/immutable/nodes/3.BalX8tPE.js +1 -0
- package/ui/dist/_app/immutable/nodes/{4.D8lluH3u.js → 4.w2QdhCXS.js} +1 -1
- package/ui/dist/_app/version.json +1 -1
- package/ui/dist/index.html +10 -10
- package/CHANGELOG.md +0 -35
- package/components/configuration.values/source/lib/map.test.ts +0 -42
- package/components/configuration.values/tsconfig.tsbuildinfo +0 -1
- package/cucumber.mjs +0 -8
- package/features/site/_app/immutable/asset.js +0 -1
- package/features/site/favicon.ico +0 -0
- package/features/site/index.html +0 -10
- package/features/steps/UI.ts +0 -98
- package/features/steps/tsconfig.json +0 -9
- package/features/ui.feature +0 -93
- package/notes/consistency.md +0 -70
- package/schemas/annotation.cos.yaml +0 -29
- package/schemas/manifest.cos.yaml +0 -9
- package/source/Aspect.test.ts +0 -119
- package/source/Aspect.ts +0 -91
- package/source/Client.test.ts +0 -125
- package/source/Client.ts +0 -251
- package/source/Composition.ts +0 -57
- package/source/Factory.ts +0 -34
- package/source/Secret.test.ts +0 -32
- package/source/Secret.ts +0 -32
- package/source/UI.ts +0 -213
- package/source/configuration.test.ts +0 -152
- package/source/configuration.ts +0 -74
- package/source/const.ts +0 -30
- package/source/context.ts +0 -13
- package/source/deployment.test.ts +0 -112
- package/source/deployment.ts +0 -160
- package/source/epoch.test.ts +0 -35
- package/source/epoch.ts +0 -22
- package/source/index.ts +0 -10
- package/source/manifest.test.ts +0 -24
- package/source/manifest.ts +0 -13
- package/source/schemas.ts +0 -9
- package/transpiled/const.d.ts +0 -18
- package/transpiled/const.js +0 -22
- package/transpiled/const.js.map +0 -1
- package/transpiled/context.d.ts +0 -4
- package/transpiled/context.js +0 -9
- package/transpiled/context.js.map +0 -1
- package/transpiled/deployment.d.ts +0 -17
- package/transpiled/deployment.js +0 -107
- package/transpiled/deployment.js.map +0 -1
- package/transpiled/epoch.d.ts +0 -2
- package/transpiled/epoch.js +0 -19
- package/transpiled/epoch.js.map +0 -1
- package/transpiled/manifest.d.ts +0 -6
- package/transpiled/manifest.js +0 -6
- package/transpiled/manifest.js.map +0 -1
- package/transpiled/schemas.d.ts +0 -3
- package/transpiled/schemas.js +0 -7
- package/transpiled/schemas.js.map +0 -1
- package/tsconfig.json +0 -13
- package/tsconfig.tsbuildinfo +0 -1
- package/ui/dist/_app/immutable/chunks/3Kbn_1Vv.js +0 -1
- package/ui/dist/_app/immutable/entry/start.Bs7xr9Ne.js +0 -1
- package/ui/dist/_app/immutable/nodes/3.DCxORePS.js +0 -1
package/features/ui.feature
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
@ui
|
|
2
|
-
Feature: Publishing the UI
|
|
3
|
-
|
|
4
|
-
The values service serves the UI on its own port, out of the directory `ui` builds. The
|
|
5
|
-
mount path is baked into the bundle and forwarded by the ingress, so the server routes
|
|
6
|
-
relative to it.
|
|
7
|
-
|
|
8
|
-
Scenario: The root is the way in
|
|
9
|
-
Given the UI is published
|
|
10
|
-
When "/" is requested
|
|
11
|
-
Then the status is 302
|
|
12
|
-
And the "location" header is "/.configuration/"
|
|
13
|
-
|
|
14
|
-
Scenario: The UI answers at its mount path
|
|
15
|
-
Given the UI is published
|
|
16
|
-
When "/.configuration" is requested
|
|
17
|
-
Then the status is 200
|
|
18
|
-
And the body contains "the page"
|
|
19
|
-
|
|
20
|
-
Scenario: A trailing slash is the same resource
|
|
21
|
-
Given the UI is published
|
|
22
|
-
When "/.configuration/" is requested
|
|
23
|
-
Then the status is 200
|
|
24
|
-
And the body contains "the page"
|
|
25
|
-
|
|
26
|
-
Scenario: A query string does not change the resource
|
|
27
|
-
Given the UI is published
|
|
28
|
-
When "/.configuration?epoch=e1" is requested
|
|
29
|
-
Then the status is 200
|
|
30
|
-
And the body contains "the page"
|
|
31
|
-
|
|
32
|
-
Scenario: An unknown route is the page
|
|
33
|
-
The client router knows what routes there are; this server does not.
|
|
34
|
-
|
|
35
|
-
Given the UI is published
|
|
36
|
-
When "/.configuration/whatever" is requested
|
|
37
|
-
Then the status is 200
|
|
38
|
-
And the body contains "the page"
|
|
39
|
-
|
|
40
|
-
Scenario: A route that looks like a file is still a route
|
|
41
|
-
A component is named `namespace.component`, and the address of its configuration
|
|
42
|
-
carries that name — so an extension is no way to tell an asset from a route.
|
|
43
|
-
|
|
44
|
-
Given the UI is published
|
|
45
|
-
When "/.configuration/dummies.dummy/" is requested
|
|
46
|
-
Then the status is 200
|
|
47
|
-
And the body contains "the page"
|
|
48
|
-
|
|
49
|
-
Scenario: And with no trailing slash to give it away
|
|
50
|
-
Given the UI is published
|
|
51
|
-
When "/.configuration/dummies.dummy" is requested
|
|
52
|
-
Then the status is 200
|
|
53
|
-
And the body contains "the page"
|
|
54
|
-
|
|
55
|
-
Scenario: A hashed asset is served forever
|
|
56
|
-
Given the UI is published
|
|
57
|
-
When "/.configuration/_app/immutable/asset.js" is requested
|
|
58
|
-
Then the status is 200
|
|
59
|
-
And the "content-type" header is "text/javascript; charset=utf-8"
|
|
60
|
-
And the "cache-control" header is "public, max-age=31536000, immutable"
|
|
61
|
-
|
|
62
|
-
Scenario: An icon is held, but not forever
|
|
63
|
-
It is named for what it is rather than for its content, so it is asked about again.
|
|
64
|
-
|
|
65
|
-
Given the UI is published
|
|
66
|
-
When "/.configuration/favicon.ico" is requested
|
|
67
|
-
Then the status is 200
|
|
68
|
-
And the "cache-control" header is "public, max-age=86400"
|
|
69
|
-
|
|
70
|
-
Scenario: The page itself is never cached
|
|
71
|
-
Given the UI is published
|
|
72
|
-
When "/.configuration/" is requested
|
|
73
|
-
Then the "cache-control" header is "no-cache"
|
|
74
|
-
|
|
75
|
-
Scenario: A missing asset is missing
|
|
76
|
-
Given the UI is published
|
|
77
|
-
When "/.configuration/_app/immutable/absent.js" is requested
|
|
78
|
-
Then the status is 404
|
|
79
|
-
|
|
80
|
-
Scenario: The path of the values API is not served by the UI
|
|
81
|
-
Given the UI is published
|
|
82
|
-
When "/configuration/values/" is requested
|
|
83
|
-
Then the status is 404
|
|
84
|
-
|
|
85
|
-
Scenario: A path that only looks like the mount path is absent
|
|
86
|
-
Given the UI is published
|
|
87
|
-
When "/.configurationable" is requested
|
|
88
|
-
Then the status is 404
|
|
89
|
-
|
|
90
|
-
Scenario: Nothing outside the site is reachable
|
|
91
|
-
Given the UI is published
|
|
92
|
-
When "/.configuration/%2e%2e/%2e%2e/package.json" is requested
|
|
93
|
-
Then the status is 404
|
package/notes/consistency.md
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# Configuration Consistency
|
|
2
|
-
|
|
3
|
-

|
|
4
|
-
|
|
5
|
-
## Problem Definition
|
|
6
|
-
|
|
7
|
-
At arbitrary moments when the configuration has changed, some distributed operations have started
|
|
8
|
-
but haven't yet
|
|
9
|
-
finished. This leads to operations that start with certain configuration values but will finish with
|
|
10
|
-
another, which may
|
|
11
|
-
result in logical problems.
|
|
12
|
-
|
|
13
|
-
Given that distributed operations may have arbitrary participants and last for an arbitrary period,
|
|
14
|
-
there is no such
|
|
15
|
-
single moment when configuration can be safely changed. Thus, it must be a process.
|
|
16
|
-
|
|
17
|
-
## ~~Not a~~ Solution
|
|
18
|
-
|
|
19
|
-
1. Configuration is a single versioned object.
|
|
20
|
-
2. Configuration version value is included in the Uniform Communication Interface.
|
|
21
|
-
3. The first distributed operation participant (which is the one who has received a UCI without the
|
|
22
|
-
configuration
|
|
23
|
-
version value) must include in the outgoing UCI the configuration version value that it considers
|
|
24
|
-
as current.
|
|
25
|
-
4. Non-first distributed operations participants (which is those who has received a UCI with the
|
|
26
|
-
configuration version
|
|
27
|
-
value) must use the configuration version specified in the UCI.
|
|
28
|
-
5. Non-first distributed operation participants must include in the outgoing UCI the same
|
|
29
|
-
configuration version value as
|
|
30
|
-
they received in the incoming.
|
|
31
|
-
|
|
32
|
-
## Restrictions
|
|
33
|
-
|
|
34
|
-
1. Configuration updates are always backward compatible.
|
|
35
|
-
2. Configuration updates are always being delivered before algorithm updates.
|
|
36
|
-
1. Including federated deployment, that is: first deliver configuration to all facilities (data
|
|
37
|
-
centers, zones,
|
|
38
|
-
whatever), then deliver algorithm updates.
|
|
39
|
-
3. Configuration storage and access solution must provide the transactional updates, that is if any
|
|
40
|
-
arbitrary
|
|
41
|
-
participant observed a certain configuration version, then any other participant is guaranteed to
|
|
42
|
-
be able to
|
|
43
|
-
subsequently observe that version.
|
|
44
|
-
1. Including federated deployment[^1].
|
|
45
|
-
|
|
46
|
-
## Solution
|
|
47
|
-
|
|
48
|
-
The configuration compatibility problem described above is a particular case of a common
|
|
49
|
-
compatibility problem for
|
|
50
|
-
distributed operation participants. It is not the only configuration that may change while a
|
|
51
|
-
distributed operation is
|
|
52
|
-
running, but participant algorithms themselves. This leads to the conclusion that this particular
|
|
53
|
-
solution must be
|
|
54
|
-
propagated to algorithm versions, that is: which system (as a set of algorithms and configuration)
|
|
55
|
-
version was used to
|
|
56
|
-
start the operation, and that system version must be used to finish the operation.
|
|
57
|
-
|
|
58
|
-
This kind of solution results in the need to run all versions of the system with appropriate message
|
|
59
|
-
routing. The
|
|
60
|
-
implementation complexity of this solution and its operations costs are considered unreasonable.
|
|
61
|
-
However, an attempt
|
|
62
|
-
will be made to implement this solution with a certain
|
|
63
|
-
constraints [#147](https://github.com/toa-io/toa/issues/147).
|
|
64
|
-
|
|
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”.
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# context.toa.yaml configuration annotation
|
|
2
|
-
#
|
|
3
|
-
# Every key names a component and holds that component's values, except `resources`, which
|
|
4
|
-
# is the values service's own: it deploys like any other and has to state what it may take.
|
|
5
|
-
# A component actually named `resources` is written with its namespace, `default.resources`
|
|
6
|
-
# — the bare form is only shorthand for that.
|
|
7
|
-
properties:
|
|
8
|
-
resources:
|
|
9
|
-
# `null` deploys the service without any, which every deployment has to state one way
|
|
10
|
-
# or the other — see `migrations/262.md`.
|
|
11
|
-
type: object
|
|
12
|
-
nullable: true
|
|
13
|
-
properties:
|
|
14
|
-
cpu:
|
|
15
|
-
type: array
|
|
16
|
-
items:
|
|
17
|
-
type: string
|
|
18
|
-
minItems: 2
|
|
19
|
-
maxItems: 2
|
|
20
|
-
memory:
|
|
21
|
-
type: array
|
|
22
|
-
items:
|
|
23
|
-
type: string
|
|
24
|
-
minItems: 2
|
|
25
|
-
maxItems: 2
|
|
26
|
-
patternProperties:
|
|
27
|
-
^(?!resources$).+$:
|
|
28
|
-
type: object
|
|
29
|
-
type: object
|
package/source/Aspect.test.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { it, beforeEach, afterEach, mock } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { isDeepStrictEqual } from 'node:util'
|
|
4
|
-
|
|
5
|
-
import { Connector, Locator } from '@toa.io/core'
|
|
6
|
-
import { generate } from 'randomstring'
|
|
7
|
-
import { Aspect } from './Aspect.js'
|
|
8
|
-
import type { Client, Listener } from './Client.js'
|
|
9
|
-
import type { Manifest } from './manifest.js'
|
|
10
|
-
|
|
11
|
-
class Fake extends Connector {
|
|
12
|
-
public readonly fetch = mock.fn(async () => ({
|
|
13
|
-
configuration: { foo: 'served' },
|
|
14
|
-
created: 5
|
|
15
|
-
}))
|
|
16
|
-
public readonly subscribe = mock.fn()
|
|
17
|
-
public readonly unsubscribe = mock.fn()
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const manifest: Manifest = {
|
|
21
|
-
schema: {
|
|
22
|
-
type: 'object',
|
|
23
|
-
properties: {
|
|
24
|
-
foo: { type: 'string' },
|
|
25
|
-
bar: { type: 'object', properties: { baz: { type: 'string' } } }
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
defaults: { bar: { baz: 'quux' } }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let locator: Locator
|
|
32
|
-
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
locator = new Locator(generate(), generate())
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
afterEach(() => {
|
|
38
|
-
delete process.env['TOA_CONFIGURATION_' + locator.uppercase]
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('should be named', async () => {
|
|
42
|
-
assert.deepStrictEqual(new Aspect(locator, manifest, null).name, 'configuration')
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('should resolve locally without a client', async () => {
|
|
46
|
-
process.env['TOA_CONFIGURATION_' + locator.uppercase] = JSON.stringify({ foo: 'local' })
|
|
47
|
-
|
|
48
|
-
const aspect = new Aspect(locator, manifest, null)
|
|
49
|
-
|
|
50
|
-
await aspect.connect()
|
|
51
|
-
|
|
52
|
-
assert.deepStrictEqual(aspect.invoke(), { foo: 'local', bar: { baz: 'quux' } })
|
|
53
|
-
assert.deepStrictEqual(aspect.invoke(['foo']), 'local')
|
|
54
|
-
assert.deepStrictEqual(aspect.invoke(['bar', 'baz']), 'quux')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('should fetch from the client and follow it', async () => {
|
|
58
|
-
const client = new Fake()
|
|
59
|
-
const aspect = new Aspect(locator, manifest, client as unknown as Client)
|
|
60
|
-
|
|
61
|
-
await aspect.connect()
|
|
62
|
-
|
|
63
|
-
assert.deepStrictEqual(client.connected, true)
|
|
64
|
-
assert.strictEqual(client.fetch.mock.callCount(), 1)
|
|
65
|
-
|
|
66
|
-
const [component, epoch] = client.fetch.mock.calls[0].arguments as unknown as [
|
|
67
|
-
string,
|
|
68
|
-
string
|
|
69
|
-
]
|
|
70
|
-
|
|
71
|
-
assert.deepStrictEqual(component, locator.id)
|
|
72
|
-
assert.match(epoch, /^[a-f0-9]{64}$/)
|
|
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
|
-
)
|
|
85
|
-
|
|
86
|
-
const listener = client.subscribe.mock.calls[0].arguments[2] as Listener
|
|
87
|
-
|
|
88
|
-
listener({ configuration: { foo: 'updated' }, created: 6 })
|
|
89
|
-
|
|
90
|
-
assert.deepStrictEqual(aspect.invoke(['foo']), 'updated')
|
|
91
|
-
|
|
92
|
-
// what is not newer than the held value is left alone
|
|
93
|
-
listener({ configuration: { foo: 'stale' }, created: 6 })
|
|
94
|
-
listener({ configuration: { foo: 'older' }, created: 4 })
|
|
95
|
-
|
|
96
|
-
assert.deepStrictEqual(aspect.invoke(['foo']), 'updated')
|
|
97
|
-
|
|
98
|
-
// a value that does not fit keeps the previous one
|
|
99
|
-
listener({ configuration: { foo: { nested: true } }, created: 7 })
|
|
100
|
-
|
|
101
|
-
assert.deepStrictEqual(aspect.invoke(['foo']), 'updated')
|
|
102
|
-
|
|
103
|
-
// and the one after it still applies
|
|
104
|
-
listener({ configuration: { foo: 'latest' }, created: 8 })
|
|
105
|
-
|
|
106
|
-
assert.deepStrictEqual(aspect.invoke(['foo']), 'latest')
|
|
107
|
-
|
|
108
|
-
await aspect.disconnect()
|
|
109
|
-
|
|
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
|
-
)
|
|
119
|
-
})
|
package/source/Aspect.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { console } from 'openspan'
|
|
2
|
-
import { Connector, type Locator } from '@toa.io/core'
|
|
3
|
-
import type { extensions } from '@toa.io/core/types'
|
|
4
|
-
import { fit, local, type Node } from './configuration.js'
|
|
5
|
-
import { epoch } from './epoch.js'
|
|
6
|
-
import type { Client, Value } from './Client.js'
|
|
7
|
-
import type { Manifest } from './manifest.js'
|
|
8
|
-
|
|
9
|
-
export class Aspect extends Connector implements extensions.Aspect {
|
|
10
|
-
public readonly name = 'configuration'
|
|
11
|
-
|
|
12
|
-
private readonly locator: Locator
|
|
13
|
-
private readonly manifest: Manifest
|
|
14
|
-
private readonly client: Client | null
|
|
15
|
-
private readonly epoch: string
|
|
16
|
-
private value: Node = {}
|
|
17
|
-
private created = 0
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Without a client the value is local: the variable, the defaults and the schema.
|
|
21
|
-
* With one, the value is what the service holds, and it follows the service.
|
|
22
|
-
*/
|
|
23
|
-
public constructor(locator: Locator, manifest: Manifest, client: Client | null) {
|
|
24
|
-
super()
|
|
25
|
-
|
|
26
|
-
this.locator = locator
|
|
27
|
-
this.manifest = manifest
|
|
28
|
-
this.client = client
|
|
29
|
-
this.epoch = epoch(manifest.schema)
|
|
30
|
-
|
|
31
|
-
if (client !== null) this.depends(client)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
public invoke(path?: string[]): any {
|
|
35
|
-
let cursor: any = this.value
|
|
36
|
-
|
|
37
|
-
if (path !== undefined) for (const segment of path) cursor = cursor[segment]
|
|
38
|
-
|
|
39
|
-
return cursor
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
protected override async open(): Promise<void> {
|
|
43
|
-
if (this.client === null) {
|
|
44
|
-
this.value = local(this.locator, this.manifest)
|
|
45
|
-
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const { configuration, created } = await this.client.fetch(
|
|
50
|
-
this.locator.id,
|
|
51
|
-
this.epoch
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
this.value = fit(configuration, this.manifest)
|
|
55
|
-
this.created = created
|
|
56
|
-
this.client.subscribe(this.locator.id, this.epoch, this.listener)
|
|
57
|
-
|
|
58
|
-
console.info('Configuration resolved', {
|
|
59
|
-
component: this.locator.id,
|
|
60
|
-
epoch: this.epoch,
|
|
61
|
-
created
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
protected override async close(): Promise<void> {
|
|
66
|
-
this.client?.unsubscribe(this.locator.id, this.epoch, this.listener)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
private readonly listener = ({ configuration, created }: Value): void => {
|
|
70
|
-
// deliveries may repeat or cross: only what is newer than the held value replaces it
|
|
71
|
-
if (created <= this.created) return
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
this.value = fit(configuration, this.manifest)
|
|
75
|
-
this.created = created
|
|
76
|
-
|
|
77
|
-
console.info('Configuration updated', {
|
|
78
|
-
component: this.locator.id,
|
|
79
|
-
epoch: this.epoch,
|
|
80
|
-
created
|
|
81
|
-
})
|
|
82
|
-
} catch (error) {
|
|
83
|
-
// the service validated it against the schema of its epoch, so the two schemas differ
|
|
84
|
-
console.error('Configuration value does not match the schema', {
|
|
85
|
-
component: this.locator.id,
|
|
86
|
-
epoch: this.epoch,
|
|
87
|
-
error
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
package/source/Client.test.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { it, beforeEach, afterEach, mock } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { isDeepStrictEqual } from 'node:util'
|
|
4
|
-
|
|
5
|
-
import { Connector, type Locator, type Receiver } from '@toa.io/core'
|
|
6
|
-
import type { Message } from '@toa.io/core/types'
|
|
7
|
-
import { timeout } from '@toa.io/generic'
|
|
8
|
-
import { Client, type Fetched } from './Client.js'
|
|
9
|
-
import { EVENT } from './const.js'
|
|
10
|
-
import type { Host } from './Factory.js'
|
|
11
|
-
|
|
12
|
-
class Remote extends Connector {
|
|
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
|
-
)
|
|
24
|
-
|
|
25
|
-
public values: Record<string, object | null> = {}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let remote: Remote
|
|
29
|
-
let receiver: Receiver | null
|
|
30
|
-
let host: Host
|
|
31
|
-
let client: Client
|
|
32
|
-
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
remote = new Remote()
|
|
35
|
-
receiver = null
|
|
36
|
-
|
|
37
|
-
host = {
|
|
38
|
-
remote: mock.fn(async (locator: Locator) => {
|
|
39
|
-
assert.deepStrictEqual(locator.id, 'configuration.values')
|
|
40
|
-
|
|
41
|
-
return remote
|
|
42
|
-
}),
|
|
43
|
-
receive: mock.fn(async (label: string, consumer: Receiver) => {
|
|
44
|
-
assert.deepStrictEqual(label, EVENT)
|
|
45
|
-
|
|
46
|
-
receiver = consumer
|
|
47
|
-
|
|
48
|
-
return new Connector()
|
|
49
|
-
})
|
|
50
|
-
} as unknown as Host
|
|
51
|
-
|
|
52
|
-
client = new Client(host, { base: 10, max: 20, warn: 2 })
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
afterEach(async () => {
|
|
56
|
-
await client.disconnect()
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('should send the requests of one tick as one call', async () => {
|
|
60
|
-
remote.values = { 'a.one': { foo: 1 }, 'a.two': { foo: 2 } }
|
|
61
|
-
|
|
62
|
-
await client.connect()
|
|
63
|
-
|
|
64
|
-
const [one, two] = await Promise.all([
|
|
65
|
-
client.fetch('a.one', 'e1'),
|
|
66
|
-
client.fetch('a.two', 'e2')
|
|
67
|
-
])
|
|
68
|
-
|
|
69
|
-
assert.deepStrictEqual(one, { configuration: { foo: 1 }, created: 7 })
|
|
70
|
-
assert.deepStrictEqual(two, { configuration: { foo: 2 }, created: 7 })
|
|
71
|
-
assert.strictEqual(remote.invoke.mock.callCount(), 1)
|
|
72
|
-
assert.deepStrictEqual(remote.invoke.mock.calls[0].arguments[1], {
|
|
73
|
-
input: [
|
|
74
|
-
{ component: 'a.one', epoch: 'e1' },
|
|
75
|
-
{ component: 'a.two', epoch: 'e2' }
|
|
76
|
-
]
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('should keep asking until served', async () => {
|
|
81
|
-
await client.connect()
|
|
82
|
-
|
|
83
|
-
const fetching = client.fetch('a.one', 'e1')
|
|
84
|
-
const deadline = Date.now() + 1000
|
|
85
|
-
|
|
86
|
-
while (remote.invoke.mock.calls.length < 2 && Date.now() < deadline) await timeout(5)
|
|
87
|
-
|
|
88
|
-
assert.ok(remote.invoke.mock.calls.length >= 2)
|
|
89
|
-
|
|
90
|
-
remote.values = { 'a.one': { foo: 1 } }
|
|
91
|
-
|
|
92
|
-
assert.deepStrictEqual((await fetching).configuration, { foo: 1 })
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('should hand a created object to its subscribers', async () => {
|
|
96
|
-
await client.connect()
|
|
97
|
-
|
|
98
|
-
const listener = mock.fn()
|
|
99
|
-
const other = mock.fn()
|
|
100
|
-
|
|
101
|
-
client.subscribe('a.one', 'e1', listener)
|
|
102
|
-
client.subscribe('a.one', 'e0', other)
|
|
103
|
-
|
|
104
|
-
await receiver!.receive({
|
|
105
|
-
payload: { component: 'a.one', epoch: 'e1', configuration: { foo: 2 }, CREATED: 12 }
|
|
106
|
-
} satisfies Message)
|
|
107
|
-
|
|
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
|
-
)
|
|
115
|
-
assert.strictEqual(other.mock.callCount(), 0)
|
|
116
|
-
assert.strictEqual(remote.invoke.mock.callCount(), 0)
|
|
117
|
-
|
|
118
|
-
client.unsubscribe('a.one', 'e1', listener)
|
|
119
|
-
|
|
120
|
-
await receiver!.receive({
|
|
121
|
-
payload: { component: 'a.one', epoch: 'e1', configuration: { foo: 3 }, CREATED: 13 }
|
|
122
|
-
} satisfies Message)
|
|
123
|
-
|
|
124
|
-
assert.strictEqual(listener.mock.callCount(), 1)
|
|
125
|
-
})
|