@toa.io/boot 1.0.0-alpha.306 → 1.0.0-alpha.308
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 +16 -0
- package/package.json +6 -6
- package/src/composition.js +59 -75
- package/src/context.js +1 -1
- package/src/host.js +1 -1
- package/src/index.js +0 -1
- package/src/map.js +59 -14
- package/src/receivers.js +10 -4
- package/src/remote.js +13 -25
- package/types/index.d.ts +20 -0
- package/src/discovery.js +0 -75
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [1.0.0-alpha.308](https://github.com/toa-io/toa/compare/v1.0.0-alpha.307...v1.0.0-alpha.308) (2026-09-15)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @toa.io/boot
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# [1.0.0-alpha.307](https://github.com/toa-io/toa/compare/v1.0.0-alpha.306...v1.0.0-alpha.307) (2026-09-15)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @toa.io/boot
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# [1.0.0-alpha.306](https://github.com/toa-io/toa/compare/v1.0.0-alpha.305...v1.0.0-alpha.306) (2026-09-14)
|
|
7
23
|
|
|
8
24
|
* refactor(core)!: name the quiesce hooks pause and unpause ([6e4e5f5](https://github.com/toa-io/toa/commit/6e4e5f55d43f03ea95ca8bbdaae57b949564dfa2))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/boot",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.308",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa Boot",
|
|
6
6
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@toa.io/atomicity": "1.0.0-alpha.
|
|
25
|
-
"@toa.io/core": "1.0.0-alpha.
|
|
26
|
-
"@toa.io/definitions": "1.0.0-alpha.
|
|
24
|
+
"@toa.io/atomicity": "1.0.0-alpha.308",
|
|
25
|
+
"@toa.io/core": "1.0.0-alpha.308",
|
|
26
|
+
"@toa.io/definitions": "1.0.0-alpha.308",
|
|
27
27
|
"@toa.io/generic": "1.0.0-alpha.304",
|
|
28
|
-
"@toa.io/norm": "1.0.0-alpha.
|
|
28
|
+
"@toa.io/norm": "1.0.0-alpha.308",
|
|
29
29
|
"@toa.io/schemas": "1.0.0-alpha.304",
|
|
30
30
|
"clone-deep": "4.0.1",
|
|
31
31
|
"dotenv": "17.4.2",
|
|
32
32
|
"openspan": "1.0.0-alpha.305"
|
|
33
33
|
},
|
|
34
|
-
"gitHead": "
|
|
34
|
+
"gitHead": "66eb0d54032f60e6ead9e13fd9e21fb60c611277"
|
|
35
35
|
}
|
package/src/composition.js
CHANGED
|
@@ -24,104 +24,88 @@ export async function composition(paths, options) {
|
|
|
24
24
|
components: manifests.map((manifest) => manifest.locator.id)
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
+
// what this process composes it knows, and the map it was given says the same
|
|
28
|
+
await boot.map.compose(manifests)
|
|
29
|
+
|
|
27
30
|
const tenants = await span(
|
|
28
31
|
'create tenants',
|
|
29
32
|
async () => await Promise.all(manifests.map(boot.extensions.tenants))
|
|
30
33
|
)
|
|
31
34
|
|
|
32
|
-
const
|
|
33
|
-
'
|
|
34
|
-
async () => await Promise.all(manifests.map(boot.
|
|
35
|
+
const components = await span(
|
|
36
|
+
'create components',
|
|
37
|
+
async () => await Promise.all(manifests.map(boot.component))
|
|
35
38
|
)
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
const groups = await Promise.all(
|
|
44
|
-
components.map(
|
|
45
|
-
async (component, index) =>
|
|
46
|
-
await boot.bindings.produce(component, manifests[index].operations)
|
|
47
|
-
)
|
|
40
|
+
const groups = await Promise.all(
|
|
41
|
+
components.map(
|
|
42
|
+
async (component, index) =>
|
|
43
|
+
await boot.bindings.produce(component, manifests[index].operations)
|
|
48
44
|
)
|
|
45
|
+
)
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
47
|
+
const receivers = await span(
|
|
48
|
+
'create receivers',
|
|
49
|
+
async () =>
|
|
50
|
+
await Promise.all(
|
|
51
|
+
components.map((component, index) =>
|
|
52
|
+
boot.receivers(manifests[index], component)
|
|
57
53
|
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const producers = []
|
|
61
|
-
const settles = []
|
|
62
|
-
const readies = []
|
|
54
|
+
)
|
|
55
|
+
)
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const serving = [...local, ...other]
|
|
57
|
+
const producers = []
|
|
58
|
+
const settles = []
|
|
59
|
+
const readies = []
|
|
68
60
|
|
|
69
|
-
|
|
61
|
+
for (let i = 0; i < components.length; i++) {
|
|
62
|
+
const { local, other } = groups[i]
|
|
63
|
+
const { settle, ready } = components[i]
|
|
64
|
+
const serving = [...local, ...other]
|
|
70
65
|
|
|
71
|
-
|
|
72
|
-
* A receiver turns an event into a call to an operation of its own component, and
|
|
73
|
-
* that call is served by these — the loop producer first, the broker's if it has
|
|
74
|
-
* already gone. They are siblings under the composition otherwise, so a receiver
|
|
75
|
-
* still draining a delivery would be left calling something already torn down.
|
|
76
|
-
*/
|
|
77
|
-
// one at a time: `depends` given an array links the group it makes rather than its
|
|
78
|
-
// members, and it is the members whose teardown has to wait for this one
|
|
79
|
-
for (const receiver of receivers[i])
|
|
80
|
-
for (const producer of serving) receiver.depends(producer)
|
|
66
|
+
producers.push(...serving)
|
|
81
67
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
68
|
+
/*
|
|
69
|
+
* A receiver turns an event into a call to an operation of its own component, and
|
|
70
|
+
* that call is served by these — the loop producer first, the broker's if it has
|
|
71
|
+
* already gone. They are siblings under the composition otherwise, so a receiver
|
|
72
|
+
* still draining a delivery would be left calling something already torn down.
|
|
73
|
+
*/
|
|
74
|
+
// one at a time: `depends` given an array links the group it makes rather than its
|
|
75
|
+
// members, and it is the members whose teardown has to wait for this one
|
|
76
|
+
for (const receiver of receivers[i])
|
|
77
|
+
for (const producer of serving) receiver.depends(producer)
|
|
89
78
|
|
|
90
|
-
|
|
79
|
+
/*
|
|
80
|
+
* The broker producers serve the stateful endpoints under the process's name, and the
|
|
81
|
+
* name is held once they are open — before then a call to it is refused. `ready` is where
|
|
82
|
+
* a component hands the name out, so it waits for every producer and receiver of its own.
|
|
83
|
+
*/
|
|
84
|
+
if (ready !== undefined) {
|
|
85
|
+
for (const producer of serving) ready.depends(producer)
|
|
91
86
|
|
|
92
|
-
|
|
93
|
-
}
|
|
87
|
+
for (const receiver of receivers[i]) ready.depends(receiver)
|
|
94
88
|
|
|
95
|
-
|
|
89
|
+
readies.push(ready)
|
|
90
|
+
}
|
|
96
91
|
|
|
97
|
-
|
|
92
|
+
if (settle === undefined) continue
|
|
98
93
|
|
|
99
|
-
|
|
94
|
+
if (local.length > 0) settle.depends(local)
|
|
100
95
|
|
|
101
|
-
|
|
96
|
+
for (const producer of other) producer.depends(settle)
|
|
102
97
|
|
|
103
|
-
|
|
104
|
-
}
|
|
98
|
+
for (const receiver of receivers[i]) receiver.depends(settle)
|
|
105
99
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
producers.concat(settles, readies),
|
|
109
|
-
receivers.flat(),
|
|
110
|
-
tenants.flat()
|
|
111
|
-
)
|
|
100
|
+
settles.push(settle)
|
|
101
|
+
}
|
|
112
102
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
*/
|
|
119
|
-
composition.depends(await boot.discovery.discovery())
|
|
103
|
+
const composition = new Composition(
|
|
104
|
+
producers.concat(settles, readies),
|
|
105
|
+
receivers.flat(),
|
|
106
|
+
tenants.flat()
|
|
107
|
+
)
|
|
120
108
|
|
|
121
|
-
|
|
122
|
-
} catch (exception) {
|
|
123
|
-
await Promise.all(expositions.flat().map((connector) => connector.disconnect(true)))
|
|
124
|
-
throw exception
|
|
125
|
-
}
|
|
109
|
+
return boot.extensions.manage(composition)
|
|
126
110
|
})
|
|
127
111
|
}
|
package/src/context.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Context, Locator } from '@toa.io/core'
|
|
|
3
3
|
import * as boot from './index.js'
|
|
4
4
|
|
|
5
5
|
export const context = async (manifest) => {
|
|
6
|
-
const local = await boot.remote(manifest.locator, undefined, { manifest })
|
|
6
|
+
const local = await boot.remote(manifest.locator, undefined, { contract: manifest })
|
|
7
7
|
const aspects = boot.extensions.aspects(manifest)
|
|
8
8
|
|
|
9
9
|
// a system aspect: it is there in every component, declared by none of them
|
package/src/host.js
CHANGED
|
@@ -12,7 +12,7 @@ import * as boot from './index.js'
|
|
|
12
12
|
*/
|
|
13
13
|
export const host = (workload) => ({
|
|
14
14
|
// an extension knows a version only where something told it one; see `Remotes` in exposition
|
|
15
|
-
remote: (locator, source,
|
|
15
|
+
remote: (locator, source, contract) => boot.remote(locator, source, { contract }),
|
|
16
16
|
broadcast: boot.bindings.broadcast,
|
|
17
17
|
composition: boot.composition,
|
|
18
18
|
receive: boot.receive,
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,6 @@ export { load as env } from './env.js'
|
|
|
4
4
|
export * as bindings from './bindings/index.js'
|
|
5
5
|
export * as bridge from './bridge.js'
|
|
6
6
|
export * as contract from './contract.js'
|
|
7
|
-
export * as discovery from './discovery.js'
|
|
8
7
|
export * as extensions from './extensions/index.js'
|
|
9
8
|
|
|
10
9
|
export { call } from './call.js'
|
package/src/map.js
CHANGED
|
@@ -1,40 +1,85 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
1
2
|
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import * as core from '@toa.io/core'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
6
|
+
* What this process was given about the components it calls: the contract of the version each of
|
|
7
|
+
* them runs.
|
|
5
8
|
*
|
|
6
|
-
* Held as the file it was given rather than as what the file said, and read at every
|
|
7
|
-
* deployment rewrites it under a running process, and a process may first call a peer
|
|
8
|
-
* that peer was replaced. A
|
|
9
|
+
* Held as the file it was given rather than as what the file said, and read at every call for
|
|
10
|
+
* one: a deployment rewrites it under a running process, and a process may first call a peer
|
|
11
|
+
* long after that peer was replaced. A contract is read once per peer, so this is a read per
|
|
12
|
+
* peer.
|
|
9
13
|
*
|
|
10
|
-
* @type {string | Record<string,
|
|
14
|
+
* @type {string | Record<string, import('@toa.io/core').Contract> | undefined}
|
|
11
15
|
*/
|
|
12
16
|
let source
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
|
-
*
|
|
19
|
+
* What this process composes, which it knows from the sources it booted from. What the map
|
|
20
|
+
* states wins: two compositions of one locator run in one process while a rollout is staged,
|
|
21
|
+
* and the map is what says which of them a caller is held to.
|
|
22
|
+
*
|
|
23
|
+
* @type {Record<string, import('@toa.io/core').Contract>}
|
|
24
|
+
*/
|
|
25
|
+
let composed = {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string | Record<string, import('@toa.io/core').Contract> | undefined} value the file, or the map itself
|
|
16
29
|
*/
|
|
17
30
|
export const use = (value) => {
|
|
18
31
|
source = value
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
35
|
+
* Forgets what this process composed, which is what a test does between one composition and
|
|
36
|
+
* the next: a component that is no longer composed is one nothing states.
|
|
37
|
+
*/
|
|
38
|
+
export const forget = () => {
|
|
39
|
+
composed = {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What this process composes, stated by the boot that composes it — and held to the map it was
|
|
44
|
+
* given a file of, which one `toa deploy` writes with the images it deploys: the two differ only
|
|
45
|
+
* where the map was not written again. What a test states instead is what that test means, and
|
|
46
|
+
* is not checked.
|
|
47
|
+
*
|
|
48
|
+
* @param {toa.norm.Component[]} manifests
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
export const compose = async (manifests) => {
|
|
52
|
+
const file = typeof source === 'string' ? await read(source) : undefined
|
|
53
|
+
|
|
54
|
+
for (const manifest of manifests) {
|
|
55
|
+
const stated = file?.[manifest.locator.id]
|
|
56
|
+
|
|
57
|
+
assert.ok(
|
|
58
|
+
stated === undefined || stated.version === manifest.version,
|
|
59
|
+
`'${manifest.locator.id}' is composed at a version the component map does not state. ` +
|
|
60
|
+
'Run `toa map`.'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
composed[manifest.locator.id] = core.contract.component(manifest)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What a component provides: what the map states of it, or what this process composes.
|
|
24
69
|
*
|
|
25
70
|
* @param {string} id
|
|
26
|
-
* @returns {Promise<
|
|
71
|
+
* @returns {Promise<import('@toa.io/core').Contract | undefined>}
|
|
27
72
|
*/
|
|
28
|
-
export const
|
|
29
|
-
if (source === undefined) return
|
|
30
|
-
if (typeof source !== 'string') return source[id]
|
|
73
|
+
export const contract = async (id) => {
|
|
74
|
+
if (source === undefined) return composed[id]
|
|
75
|
+
if (typeof source !== 'string') return source[id] ?? composed[id]
|
|
31
76
|
|
|
32
|
-
return (await read(source))[id]
|
|
77
|
+
return (await read(source))[id] ?? composed[id]
|
|
33
78
|
}
|
|
34
79
|
|
|
35
80
|
/**
|
|
36
81
|
* @param {string} path
|
|
37
|
-
* @returns {Promise<Record<string,
|
|
82
|
+
* @returns {Promise<Record<string, import('@toa.io/core').Contract>>}
|
|
38
83
|
*/
|
|
39
84
|
async function read(path) {
|
|
40
85
|
let contents
|
package/src/receivers.js
CHANGED
|
@@ -8,7 +8,7 @@ export const receivers = async (manifest, component) => {
|
|
|
8
8
|
if (manifest.receivers === undefined) return []
|
|
9
9
|
|
|
10
10
|
const receivers = []
|
|
11
|
-
const local = await boot.remote(manifest.locator, undefined, { manifest })
|
|
11
|
+
const local = await boot.remote(manifest.locator, undefined, { contract: manifest })
|
|
12
12
|
|
|
13
13
|
for (const [label, definition] of Object.entries(manifest.receivers)) {
|
|
14
14
|
const locator = Locator.parse(label)
|
|
@@ -67,9 +67,15 @@ export async function receive(label, group, callback) {
|
|
|
67
67
|
*/
|
|
68
68
|
async function resolveBinding(locator, label) {
|
|
69
69
|
const event = label.split('.').pop()
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
const contract = await boot.map.contract(locator.id)
|
|
71
|
+
|
|
72
|
+
assert.ok(
|
|
73
|
+
contract !== undefined,
|
|
74
|
+
`The component map states nothing of '${locator.id}', whose event '${label}' receives. ` +
|
|
75
|
+
'Run `toa map`.'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const { events, version } = contract
|
|
73
79
|
|
|
74
80
|
assert.ok(
|
|
75
81
|
events?.[event] !== undefined,
|
package/src/remote.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Remote } from '@toa.io/core'
|
|
1
|
+
import { Remote, exceptions } from '@toa.io/core'
|
|
3
2
|
import { remap } from '@toa.io/generic'
|
|
4
3
|
|
|
5
4
|
import * as boot from './index.js'
|
|
@@ -7,40 +6,29 @@ import * as boot from './index.js'
|
|
|
7
6
|
/**
|
|
8
7
|
* @param {import('@toa.io/core').Locator} locator
|
|
9
8
|
* @param {import('@toa.io/core/types').Source} [source] the origin stamped on every call made through this remote
|
|
10
|
-
* @param {{
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
* Absent, the map this process was given says, and absent that too, whichever version answers.
|
|
9
|
+
* @param {{ contract?: import('@toa.io/core').Contract }} [options]
|
|
10
|
+
* `contract` is what the caller already has — a manifest of its own is one, and so is what a
|
|
11
|
+
* tenant announced — and absent it, what the map this process was given states.
|
|
14
12
|
*/
|
|
15
13
|
export const remote = async (locator, source, options = {}) => {
|
|
16
|
-
|
|
17
|
-
let discovery
|
|
14
|
+
const contract = options.contract ?? (await boot.map.contract(locator.id))
|
|
18
15
|
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
discovery = await boot.discovery.discovery()
|
|
25
|
-
manifest = await discovery.lookup(locator, version)
|
|
26
|
-
}
|
|
16
|
+
if (contract === undefined)
|
|
17
|
+
throw new exceptions.UnstatedException(
|
|
18
|
+
`Cannot call '${locator.id}': the component map states nothing of it. Run \`toa map\`.`
|
|
19
|
+
)
|
|
27
20
|
|
|
28
21
|
// a call binds its consumers, which are loaded rather than required
|
|
29
22
|
const calls =
|
|
30
|
-
|
|
23
|
+
contract.operations === undefined
|
|
31
24
|
? {}
|
|
32
25
|
: await settle(
|
|
33
|
-
remap(
|
|
34
|
-
boot.call(locator, endpoint, definition,
|
|
26
|
+
remap(contract.operations, (definition, endpoint) =>
|
|
27
|
+
boot.call(locator, endpoint, definition, contract.entity, source)
|
|
35
28
|
)
|
|
36
29
|
)
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// ensure discovery shutdown
|
|
41
|
-
if (discovery !== undefined) remote.depends(discovery)
|
|
42
|
-
|
|
43
|
-
return remote
|
|
31
|
+
return new Remote(locator, calls)
|
|
44
32
|
}
|
|
45
33
|
|
|
46
34
|
/** An object whose values are promises, as an object of what they resolve to. */
|
package/types/index.d.ts
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import type * as core from '@toa.io/core'
|
|
2
2
|
import type * as types from '@toa.io/core/types'
|
|
3
|
+
import type * as norm from '@toa.io/norm/types'
|
|
3
4
|
import * as composition from './composition.d.ts'
|
|
4
5
|
|
|
5
6
|
export * as bindings from './bindings.d.ts'
|
|
6
7
|
|
|
8
|
+
export async function manifest(
|
|
9
|
+
path: string,
|
|
10
|
+
options?: composition.Options
|
|
11
|
+
): Promise<norm.Manifest>
|
|
12
|
+
|
|
13
|
+
/** What this process was given about the components it calls. */
|
|
14
|
+
export namespace map {
|
|
15
|
+
/** the file to read it from, or the map itself */
|
|
16
|
+
function use(value: string | Record<string, core.Contract> | undefined): void
|
|
17
|
+
|
|
18
|
+
/** what this process composes, stated by the boot that composes it */
|
|
19
|
+
function compose(manifests: norm.Manifest[]): Promise<void>
|
|
20
|
+
|
|
21
|
+
/** forgets what this process composed */
|
|
22
|
+
function forget(): void
|
|
23
|
+
|
|
24
|
+
function contract(id: string): Promise<core.Contract | undefined>
|
|
25
|
+
}
|
|
26
|
+
|
|
7
27
|
export async function composition(
|
|
8
28
|
paths: string[],
|
|
9
29
|
options?: composition.Options
|
package/src/discovery.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { Discovery, Exposition } from '@toa.io/core'
|
|
2
|
-
|
|
3
|
-
import * as boot from './index.js'
|
|
4
|
-
|
|
5
|
-
let promise
|
|
6
|
-
let instance = null
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The process's discovery, for as long as it is connected.
|
|
10
|
-
*
|
|
11
|
-
* A disconnected one is replaced rather than reused. A lookup becomes a dependency of the
|
|
12
|
-
* discovery that made it and nothing takes one back, so an instance carried across a
|
|
13
|
-
* disconnection would reconnect every call it has ever made beside the ones the composition now
|
|
14
|
-
* booting asks for — and every disconnection after that would walk all of them again.
|
|
15
|
-
*/
|
|
16
|
-
export const discovery = async () => {
|
|
17
|
-
while (true) {
|
|
18
|
-
if (instance === null) {
|
|
19
|
-
instance = new Discovery(lookup)
|
|
20
|
-
promise = instance.connect()
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const current = instance
|
|
24
|
-
|
|
25
|
-
await promise
|
|
26
|
-
|
|
27
|
-
if (current.connected) return current
|
|
28
|
-
|
|
29
|
-
// whoever replaced it first wins, and the loop reads what they left
|
|
30
|
-
if (instance === current) instance = null
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const lookup = async (locator, version) => {
|
|
35
|
-
const call = await boot.call(locator, endpoint(version), { bindings: BINDINGS })
|
|
36
|
-
|
|
37
|
-
await call.connect()
|
|
38
|
-
|
|
39
|
-
return call
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Where a lookup of a named version of a component is answered. Absent a version, the name every
|
|
44
|
-
* version of it answers on, which is what a caller with no map asks.
|
|
45
|
-
*
|
|
46
|
-
* @param {string} [version]
|
|
47
|
-
* @returns {string}
|
|
48
|
-
*/
|
|
49
|
-
export const endpoint = (version) =>
|
|
50
|
-
version === undefined ? ENDPOINT : ENDPOINT + SEPARATOR + version
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Both names, always: the versioned one is what a caller that knows which version it wants asks,
|
|
54
|
-
* and the shared one is what everything else asks — a caller outside the deployment, a process of
|
|
55
|
-
* a runtime that has never heard of this.
|
|
56
|
-
*/
|
|
57
|
-
export const expose = async (manifest) => {
|
|
58
|
-
const exposition = new Exposition(manifest.locator, manifest)
|
|
59
|
-
const operations = {
|
|
60
|
-
[ENDPOINT]: { bindings: BINDINGS },
|
|
61
|
-
[endpoint(manifest.version)]: { bindings: BINDINGS }
|
|
62
|
-
}
|
|
63
|
-
const { local, other } = await boot.bindings.produce(exposition, operations)
|
|
64
|
-
const producers = local.concat(other)
|
|
65
|
-
|
|
66
|
-
await Promise.all(producers.map((producer) => producer.connect()))
|
|
67
|
-
|
|
68
|
-
return producers
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const BINDINGS = ['@toa.io/bindings.amqp']
|
|
72
|
-
const ENDPOINT = '.lookup'
|
|
73
|
-
|
|
74
|
-
/** the runtime's own, as `..tasks` and `..instances` are */
|
|
75
|
-
const SEPARATOR = '..'
|