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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +1 -365
  2. package/components/configuration.values/manifest.toa.yaml +14 -21
  3. package/components/configuration.values/migrations/0001-system-properties.yaml +14 -0
  4. package/components/configuration.values/migrations/0002-indexes.yaml +7 -0
  5. package/components/configuration.values/operations/create.js +1 -2
  6. package/components/configuration.values/operations/fetch.js +1 -1
  7. package/components/configuration.values/operations/get.js +1 -1
  8. package/components/configuration.values/operations/lib/map.js +1 -1
  9. package/components/configuration.values/operations/lib/resolve.js +3 -3
  10. package/components/configuration.values/operations/list.js +1 -1
  11. package/components/configuration.values/source/create.ts +3 -5
  12. package/components/configuration.values/source/fetch.ts +12 -10
  13. package/components/configuration.values/source/get.ts +2 -3
  14. package/components/configuration.values/source/lib/map.test.ts +7 -2
  15. package/components/configuration.values/source/lib/map.ts +3 -3
  16. package/components/configuration.values/source/lib/resolve.ts +9 -6
  17. package/components/configuration.values/source/list.ts +8 -6
  18. package/components/configuration.values/tsconfig.tsbuildinfo +1 -1
  19. package/components/configuration.values/types/toa.d.ts +5 -5
  20. package/cucumber.mjs +3 -0
  21. package/features/site/index.html +6 -2
  22. package/features/steps/UI.ts +20 -13
  23. package/notes/consistency.md +10 -9
  24. package/package.json +6 -6
  25. package/readme.md +15 -14
  26. package/schemas/annotation.cos.yaml +1 -1
  27. package/source/Aspect.test.ts +32 -8
  28. package/source/Aspect.ts +28 -19
  29. package/source/Client.test.ts +31 -14
  30. package/source/Client.ts +41 -48
  31. package/source/Composition.ts +6 -6
  32. package/source/Factory.ts +7 -7
  33. package/source/Secret.test.ts +1 -1
  34. package/source/Secret.ts +5 -5
  35. package/source/UI.ts +33 -32
  36. package/source/configuration.test.ts +14 -8
  37. package/source/configuration.ts +11 -15
  38. package/source/const.ts +13 -1
  39. package/source/context.ts +2 -2
  40. package/source/deployment.test.ts +37 -12
  41. package/source/deployment.ts +45 -19
  42. package/source/epoch.test.ts +8 -2
  43. package/source/epoch.ts +6 -5
  44. package/source/index.ts +1 -1
  45. package/source/manifest.test.ts +12 -6
  46. package/source/manifest.ts +1 -1
  47. package/transpiled/Aspect.d.ts +2 -1
  48. package/transpiled/Aspect.js +15 -3
  49. package/transpiled/Aspect.js.map +1 -1
  50. package/transpiled/Client.d.ts +1 -1
  51. package/transpiled/Client.js +8 -2
  52. package/transpiled/Client.js.map +1 -1
  53. package/transpiled/Composition.js +2 -2
  54. package/transpiled/Composition.js.map +1 -1
  55. package/transpiled/Factory.d.ts +2 -1
  56. package/transpiled/Factory.js.map +1 -1
  57. package/transpiled/Secret.js.map +1 -1
  58. package/transpiled/UI.js +2 -1
  59. package/transpiled/UI.js.map +1 -1
  60. package/transpiled/configuration.js.map +1 -1
  61. package/transpiled/const.d.ts +8 -1
  62. package/transpiled/const.js +10 -0
  63. package/transpiled/const.js.map +1 -1
  64. package/transpiled/context.d.ts +1 -1
  65. package/transpiled/context.js.map +1 -1
  66. package/transpiled/deployment.d.ts +3 -0
  67. package/transpiled/deployment.js +19 -5
  68. package/transpiled/deployment.js.map +1 -1
  69. package/transpiled/epoch.js +3 -1
  70. package/transpiled/epoch.js.map +1 -1
  71. package/transpiled/index.d.ts +1 -1
  72. package/transpiled/index.js +1 -1
  73. package/transpiled/index.js.map +1 -1
  74. package/transpiled/manifest.js.map +1 -1
  75. package/tsconfig.tsbuildinfo +1 -1
  76. package/ui/dist/_app/immutable/chunks/{BzG-gka8.js → BW57cOwz.js} +1 -1
  77. package/ui/dist/_app/immutable/chunks/{_I_MUoRn.js → Bhnl1Avs.js} +1 -1
  78. package/ui/dist/_app/immutable/chunks/{Bxde3uE_.js → BxOvOexd.js} +1 -1
  79. package/ui/dist/_app/immutable/chunks/{mvspXr3J.js → CkX_FNYa.js} +2 -2
  80. package/ui/dist/_app/immutable/chunks/{p9RyiXQ-.js → Dmv4yvb8.js} +1 -1
  81. package/ui/dist/_app/immutable/entry/{app.C1VIepN7.js → app.Cr7od9uH.js} +2 -2
  82. package/ui/dist/_app/immutable/entry/start.DWf-Tl0X.js +1 -0
  83. package/ui/dist/_app/immutable/nodes/{0.D_0dtFej.js → 0.CcGZUFKo.js} +1 -1
  84. package/ui/dist/_app/immutable/nodes/{1.DFlgHh-H.js → 1.CR6En12y.js} +1 -1
  85. package/ui/dist/_app/immutable/nodes/{2.BIHgAxXR.js → 2.BhyO8bGT.js} +1 -1
  86. package/ui/dist/_app/immutable/nodes/3.DcbMtJtb.js +1 -0
  87. package/ui/dist/_app/immutable/nodes/{4.BZMuJGfA.js → 4.qVnVQBNq.js} +1 -1
  88. package/ui/dist/_app/version.json +1 -1
  89. package/ui/dist/index.html +28 -23
  90. package/ui/dist/_app/immutable/entry/start.BQWCs_Pq.js +0 -1
  91. package/ui/dist/_app/immutable/nodes/3.R-_yCt5X.js +0 -1
@@ -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 (): Promise<void> {
22
- this.server = new UI(UI_PORT, resolve(import.meta.dirname, '..', 'site'))
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 (path: string): Promise<void> {
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 (status: number): void {
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 (text: string): void {
39
- assert.ok(this.response?.body.includes(text),
40
- `Expected the body to contain '${text}', got '${this.response?.body}'`)
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 (): void {
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 (name: string, value: string): void {
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 (): Promise<void> {
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 (path: string): Promise<Response> {
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: UI_PORT, path, agent: false }, (response) => {
76
+ const request = http.get({ port: PORT, path, agent: false }, (response) => {
70
77
  let body = ''
71
78
 
72
79
  response.setEncoding('utf8')
@@ -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
- 1. Including federated deployment, that is: first deliver configuration to all facilities (data
37
- centers, zones,
38
- whatever), then deliver 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
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
- 1. Including federated deployment[^1].
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]: Since it looks like there is no reasonable way to provide this kind of guarantee without
66
- significant performance
67
- and/or availability impact, it may be implemented as a mechanism with read retries and with a
68
- timeout considered as
69
- “enough for the most of fail-over scenarios”.
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.284",
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": "006755556879e91e8d68fa86753aa3b475bdf4bf",
26
+ "gitHead": "5c7944a70c16065ab6372072385cc1f02f5db77c",
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.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 (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)