@toa.io/boot 1.0.0-alpha.27 → 1.0.0-alpha.272
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 +136 -0
- package/package.json +10 -8
- package/src/atom.js +41 -0
- package/src/atomicity.js +15 -0
- package/src/bindings/produce.js +27 -12
- package/src/bridge.js +15 -2
- package/src/call.js +3 -2
- package/src/cascade.js +4 -4
- package/src/component.js +53 -25
- package/src/composition.js +78 -9
- package/src/context.js +4 -1
- package/src/contract.js +6 -6
- package/src/discovery.js +2 -1
- package/src/emission.js +2 -2
- package/src/events.js +29 -0
- package/src/extensions/emitter.js +3 -2
- package/src/extensions/index.js +2 -0
- package/src/extensions/load.js +4 -2
- package/src/extensions/manage.js +20 -0
- package/src/extensions/receiver.js +2 -1
- package/src/extensions/tenants.js +1 -1
- package/src/guards.js +23 -0
- package/src/index.js +12 -0
- package/src/manifest.js +5 -4
- package/src/operation.js +14 -4
- package/src/outbox.js +21 -0
- package/src/rc.js +9 -0
- package/src/receivers.js +12 -6
- package/src/remote.js +18 -5
- package/src/span.js +21 -0
- package/src/storage.js +7 -2
- package/types/index.d.ts +3 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
# [1.0.0-alpha.272](https://github.com/toa-io/toa/compare/v1.0.0-alpha.271...v1.0.0-alpha.272) (2026-09-01)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **amqp:** keep a receiver apart from the producers it delivers into ([4c2f43e](https://github.com/toa-io/toa/commit/4c2f43ebde68ba76a1fd277fe2a3305be1b622ef))
|
|
12
|
+
* **amqp:** tear a receiver down before its producers, and drop the split ([e6c1154](https://github.com/toa-io/toa/commit/e6c1154024df22f7a470c47c1a79bd170fe7e3b2))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
* refactor(atomicity)!: take the lock manager from the stash ([4fd91eb](https://github.com/toa-io/toa/commit/4fd91eb2fdb68f7b87aaf16d182794bbc567ba57))
|
|
16
|
+
* refactor(atomicity)!: make the connector a family, not a partitioner ([06fd63a](https://github.com/toa-io/toa/commit/06fd63ad6296b724ef83afccf4e4e25d0bcaf080))
|
|
17
|
+
* refactor(atomicity)!: rename the connector and free it of the outbox ([21f1a41](https://github.com/toa-io/toa/commit/21f1a41aceafcfd35c7620014062fe46b54afa95))
|
|
18
|
+
* feat(core)!: commit events with the state that produced them ([1eb68cc](https://github.com/toa-io/toa/commit/1eb68cc435dbfa03faa16009fceb866693d22e1a)), closes [#20](https://github.com/toa-io/toa/issues/20)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* **core:** give every component an atom aspect ([b501b9c](https://github.com/toa-io/toa/commit/b501b9cd3d5f5408d9faa71213e953fe2792cf9f))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### BREAKING CHANGES
|
|
27
|
+
|
|
28
|
+
* `context.stash.lock` is gone; lock through `context.atom` instead.
|
|
29
|
+
A stash pointer resolving to several addresses now uses the first.
|
|
30
|
+
|
|
31
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
32
|
+
* `Factory.partition(group)` is `Factory.atom(group)`, and the
|
|
33
|
+
`Partition` it returned is an `Atom`. `slots(total)` is unchanged.
|
|
34
|
+
|
|
35
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
36
|
+
* `@toa.io/partitions.redis` is now `@toa.io/atomicity`, and its
|
|
37
|
+
`lanes(total)` is `slots(total)`. Redis is declared as `atomicity` in the context
|
|
38
|
+
rather than `outbox.redis`, and read from `TOA_ATOMICITY_REDIS`.
|
|
39
|
+
`TOA_OUTBOX_PARTITION_INTERVAL` is `TOA_ATOMICITY_INTERVAL`.
|
|
40
|
+
|
|
41
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
42
|
+
* `event.changeset` is removed; use `origin` and `state`. `State`
|
|
43
|
+
takes an `Outbox` in place of an `Emission`. `difference` is dropped from
|
|
44
|
+
`@toa.io/generic` along with its last caller.
|
|
45
|
+
|
|
46
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# [1.0.0-alpha.270](https://github.com/toa-io/toa/compare/v1.0.0-alpha.269...v1.0.0-alpha.270) (2026-08-31)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
### Performance Improvements
|
|
56
|
+
|
|
57
|
+
* **boot:** stop paying for what a composition does not need to start ([1619c27](https://github.com/toa-io/toa/commit/1619c2743072f4706939ce72f3074e615d26a91e))
|
|
58
|
+
* **openspan:** create a span only when something records it ([a6efb0e](https://github.com/toa-io/toa/commit/a6efb0eed38393f4c169b067f23d7447f0169bca))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# [1.0.0-alpha.266](https://github.com/toa-io/toa/compare/v1.0.0-alpha.265...v1.0.0-alpha.266) (2026-08-29)
|
|
65
|
+
|
|
66
|
+
**Note:** Version bump only for package @toa.io/boot
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# [1.0.0-alpha.265](https://github.com/toa-io/toa/compare/v1.0.0-alpha.264...v1.0.0-alpha.265) (2026-08-29)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
### Features
|
|
76
|
+
|
|
77
|
+
* **bridges.node:** add the dispose run command phase ([00df715](https://github.com/toa-io/toa/commit/00df7158f638b976ab09d4007102a4ead3c696c3))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# [1.0.0-alpha.264](https://github.com/toa-io/toa/compare/v1.0.0-alpha.263...v1.0.0-alpha.264) (2026-08-29)
|
|
84
|
+
|
|
85
|
+
**Note:** Version bump only for package @toa.io/boot
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# [1.0.0-alpha.263](https://github.com/toa-io/toa/compare/v1.0.0-alpha.262...v1.0.0-alpha.263) (2026-08-29)
|
|
92
|
+
|
|
93
|
+
**Note:** Version bump only for package @toa.io/boot
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# [1.0.0-alpha.262](https://github.com/toa-io/toa/compare/v1.0.0-alpha.261...v1.0.0-alpha.262) (2026-08-28)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
### Bug Fixes
|
|
103
|
+
|
|
104
|
+
* **boot:** let the composition own the process discovery ([2201e88](https://github.com/toa-io/toa/commit/2201e8852517fd1d2822c775c9b126eb60399580))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
### Features
|
|
108
|
+
|
|
109
|
+
* **core:** identify the origin of a call with `request.source` ([9da6b66](https://github.com/toa-io/toa/commit/9da6b66df852690351a1e82c7b8cc176fd89774a))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# [1.0.0-alpha.259](https://github.com/toa-io/toa/compare/v1.0.0-alpha.258...v1.0.0-alpha.259) (2026-08-24)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
### Features
|
|
119
|
+
|
|
120
|
+
* **runtime:** split component RC into preflight and settle ([0d648b3](https://github.com/toa-io/toa/commit/0d648b314e4aac226c254bf01a86affa04b59b34))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# [1.0.0-alpha.257](https://github.com/toa-io/toa/compare/v1.0.0-alpha.256...v1.0.0-alpha.257) (2026-08-24)
|
|
127
|
+
|
|
128
|
+
**Note:** Version bump only for package @toa.io/boot
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# [1.0.0-alpha.256](https://github.com/toa-io/toa/compare/v1.0.0-alpha.255...v1.0.0-alpha.256) (2026-08-23)
|
|
135
|
+
|
|
136
|
+
**Note:** Version bump only for package @toa.io/boot
|
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.272",
|
|
4
4
|
"description": "Toa Boot",
|
|
5
5
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
@@ -20,13 +20,15 @@
|
|
|
20
20
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@toa.io/
|
|
24
|
-
"@toa.io/
|
|
25
|
-
"@toa.io/
|
|
26
|
-
"@toa.io/
|
|
27
|
-
"@toa.io/
|
|
23
|
+
"@toa.io/atomicity": "1.0.0-alpha.272",
|
|
24
|
+
"@toa.io/core": "1.0.0-alpha.272",
|
|
25
|
+
"@toa.io/filesystem": "1.0.0-alpha.272",
|
|
26
|
+
"@toa.io/generic": "1.0.0-alpha.272",
|
|
27
|
+
"@toa.io/norm": "1.0.0-alpha.272",
|
|
28
|
+
"@toa.io/schemas": "1.0.0-alpha.272",
|
|
28
29
|
"clone-deep": "4.0.1",
|
|
29
|
-
"dotenv": "16.1.1"
|
|
30
|
+
"dotenv": "16.1.1",
|
|
31
|
+
"openspan": "1.0.0-alpha.272"
|
|
30
32
|
},
|
|
31
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "1a451755445935c04cf5b373bcc73942cac0bb10"
|
|
32
34
|
}
|
package/src/atom.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Connector } = require('@toa.io/core')
|
|
4
|
+
|
|
5
|
+
const boot = require('./index')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The atom as a system aspect. Every component has `context.atom`, with nothing declared in its
|
|
9
|
+
* manifest and nothing of its own to configure.
|
|
10
|
+
*
|
|
11
|
+
* @implements {toa.core.extensions.Aspect}
|
|
12
|
+
*/
|
|
13
|
+
class Aspect extends Connector {
|
|
14
|
+
name = 'atom'
|
|
15
|
+
|
|
16
|
+
#atom
|
|
17
|
+
|
|
18
|
+
constructor (atom) {
|
|
19
|
+
super()
|
|
20
|
+
|
|
21
|
+
this.#atom = atom
|
|
22
|
+
|
|
23
|
+
this.depends(atom)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// the methods are named rather than forwarded, so that the connector's lifecycle is not
|
|
27
|
+
// reachable from an algorithm
|
|
28
|
+
invoke (method, ...args) {
|
|
29
|
+
switch (method) {
|
|
30
|
+
case 'slots': return this.#atom.slots(...args)
|
|
31
|
+
case 'meter': return this.#atom.meter(...args)
|
|
32
|
+
case 'lock': return this.#atom.lock(...args)
|
|
33
|
+
default: throw new Error(`Atom aspect has no '${method}' method`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @param {string} group */
|
|
39
|
+
const atom = (group) => new Aspect(boot.atomicity(group))
|
|
40
|
+
|
|
41
|
+
exports.atom = atom
|
package/src/atomicity.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Factory } = require('@toa.io/atomicity')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What the replicas of one group decide together. Every component has one, whether or not there
|
|
7
|
+
* is anything to coordinate through: without a Redis nothing is owned and nothing is metered —
|
|
8
|
+
* see the connector's readme.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} group
|
|
11
|
+
* @returns {toa.core.atomicity.Atom}
|
|
12
|
+
*/
|
|
13
|
+
const atomicity = (group) => new Factory().atom(group)
|
|
14
|
+
|
|
15
|
+
exports.atomicity = atomicity
|
package/src/bindings/produce.js
CHANGED
|
@@ -3,26 +3,41 @@
|
|
|
3
3
|
const { LOOP } = require('./constants')
|
|
4
4
|
const { factory } = require('./factory')
|
|
5
5
|
|
|
6
|
-
const produce = (component, operations) =>
|
|
7
|
-
|
|
6
|
+
const produce = (component, operations) => {
|
|
7
|
+
const local = []
|
|
8
|
+
const other = []
|
|
9
|
+
|
|
10
|
+
group(operations, (binding, endpoints) => {
|
|
11
|
+
const producer = factory(binding).producer(component.locator, endpoints, component)
|
|
12
|
+
|
|
13
|
+
if (require(binding).properties.local === true)
|
|
14
|
+
local.push(producer)
|
|
15
|
+
else
|
|
16
|
+
other.push(producer)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
return { local, other }
|
|
20
|
+
}
|
|
8
21
|
|
|
9
22
|
const group = (operations, callback) => {
|
|
10
23
|
const map = {}
|
|
11
24
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
if (operations !== undefined)
|
|
26
|
+
for (const [endpoint, operation] of Object.entries(operations)) {
|
|
27
|
+
// noinspection JSUnresolvedVariable
|
|
28
|
+
const bindings = global.TOA_INTEGRATION_BINDINGS_LOOP_DISABLED
|
|
29
|
+
? operation.bindings
|
|
30
|
+
: [LOOP].concat(operation.bindings)
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
for (const binding of bindings) {
|
|
33
|
+
if (!map[binding]) map[binding] = []
|
|
20
34
|
|
|
21
|
-
|
|
35
|
+
map[binding].push(endpoint)
|
|
36
|
+
}
|
|
22
37
|
}
|
|
23
|
-
}
|
|
24
38
|
|
|
25
|
-
|
|
39
|
+
for (const [binding, endpoints] of Object.entries(map))
|
|
40
|
+
callback(binding, endpoints)
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
exports.produce = produce
|
package/src/bridge.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
const
|
|
3
|
+
async function algorithm (bridge, path, endpoint, context) {
|
|
4
|
+
const factory = resolve(bridge)
|
|
5
|
+
const algorithm = await factory.algorithm(path, endpoint, context)
|
|
5
6
|
|
|
6
7
|
algorithm.depends(context)
|
|
7
8
|
|
|
@@ -10,6 +11,16 @@ const algorithm = (bridge, path, endpoint, context) => {
|
|
|
10
11
|
|
|
11
12
|
const event = (bridge, path, label, context) => resolve(bridge).event(path, label, context)
|
|
12
13
|
const receiver = (bridge, path, label) => resolve(bridge).receiver(path, label)
|
|
14
|
+
const guard = (bridge, path, label, context) => resolve(bridge).guard(path, label, context)
|
|
15
|
+
|
|
16
|
+
async function rc (bridge, path, context) {
|
|
17
|
+
const factory = resolve(bridge)
|
|
18
|
+
|
|
19
|
+
if (factory.rc === undefined)
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
return factory.rc(path, context)
|
|
23
|
+
}
|
|
13
24
|
|
|
14
25
|
const factories = {}
|
|
15
26
|
|
|
@@ -26,3 +37,5 @@ const resolve = (bridge) => {
|
|
|
26
37
|
exports.algorithm = algorithm
|
|
27
38
|
exports.event = event
|
|
28
39
|
exports.receiver = receiver
|
|
40
|
+
exports.guard = guard
|
|
41
|
+
exports.rc = rc
|
package/src/call.js
CHANGED
|
@@ -4,12 +4,13 @@ const { Call, Transmission } = require('@toa.io/core')
|
|
|
4
4
|
|
|
5
5
|
const boot = require('./index')
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// eslint-disable-next-line max-params
|
|
8
|
+
const call = (locator, endpoint, definition, entity, source) => {
|
|
8
9
|
const consumers = boot.bindings.consume(locator, endpoint, definition.bindings)
|
|
9
10
|
const transmission = new Transmission(consumers)
|
|
10
11
|
const contract = boot.contract.request(definition, entity)
|
|
11
12
|
|
|
12
|
-
return new Call(transmission, contract)
|
|
13
|
+
return new Call(transmission, contract, source)
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
exports.call = call
|
package/src/cascade.js
CHANGED
|
@@ -4,13 +4,13 @@ const { Cascade } = require('@toa.io/core')
|
|
|
4
4
|
|
|
5
5
|
const boot = require('./index')
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
async function cascade (manifest, endpoint, definition, context, preflight) {
|
|
8
8
|
const bridges = []
|
|
9
9
|
|
|
10
10
|
if (definition.forward) endpoint = definition.forward
|
|
11
11
|
|
|
12
12
|
if (definition.bridge) {
|
|
13
|
-
const bridge = boot.bridge.algorithm(definition.bridge, manifest.path, endpoint, context)
|
|
13
|
+
const bridge = await boot.bridge.algorithm(definition.bridge, manifest.path, endpoint, context)
|
|
14
14
|
|
|
15
15
|
bridges.unshift(bridge)
|
|
16
16
|
}
|
|
@@ -22,12 +22,12 @@ const cascade = (manifest, endpoint, definition, context) => {
|
|
|
22
22
|
|
|
23
23
|
if (operation === undefined) continue
|
|
24
24
|
|
|
25
|
-
const bridge = boot.bridge.algorithm(operation.bridge, prototype.path, endpoint, context)
|
|
25
|
+
const bridge = await boot.bridge.algorithm(operation.bridge, prototype.path, endpoint, context)
|
|
26
26
|
|
|
27
27
|
bridges.unshift(bridge)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
return new Cascade(bridges)
|
|
30
|
+
return new Cascade(bridges, preflight)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
exports.cascade = cascade
|
package/src/component.js
CHANGED
|
@@ -1,48 +1,76 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
const
|
|
5
|
-
Component,
|
|
6
|
-
Locator,
|
|
7
|
-
State,
|
|
8
|
-
entities
|
|
9
|
-
} = require('@toa.io/core')
|
|
10
|
-
const { Schema } = require('@toa.io/schema')
|
|
3
|
+
const { Component, Locator, State, entities } = require('@toa.io/core')
|
|
4
|
+
const schemas = require('@toa.io/schemas')
|
|
11
5
|
|
|
12
6
|
const boot = require('./index')
|
|
7
|
+
const { span } = require('./span')
|
|
13
8
|
|
|
14
|
-
/**
|
|
15
|
-
* @param {toa.norm.Component} manifest
|
|
16
|
-
* @returns {Promise<toa.core.Component>}
|
|
17
|
-
*/
|
|
18
9
|
const component = async (manifest) => {
|
|
10
|
+
const locator = new Locator(manifest.name, manifest.namespace)
|
|
11
|
+
|
|
12
|
+
return span(`component ${locator.id}`, () => create(manifest, locator))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const create = async (manifest, locator) => {
|
|
19
16
|
boot.extensions.load(manifest)
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
const
|
|
18
|
+
// the storage is told whether there will be an outbox, so the events come first
|
|
19
|
+
const events = boot.events(manifest)
|
|
20
|
+
const storage = boot.storage(manifest, events !== undefined)
|
|
23
21
|
const context = await boot.context(manifest)
|
|
24
|
-
const emission = boot.emission(
|
|
22
|
+
const emission = boot.emission(events, locator, context)
|
|
23
|
+
const outbox = boot.outbox(manifest, storage, emission)
|
|
25
24
|
|
|
26
25
|
let state
|
|
27
26
|
|
|
28
27
|
if (manifest.entity !== undefined) {
|
|
29
|
-
const schema =
|
|
30
|
-
const
|
|
28
|
+
const schema = schemas.schema(manifest.entity.schema)
|
|
29
|
+
const guards = await boot.guards(manifest, context)
|
|
30
|
+
const entity = new entities.Factory(schema, guards)
|
|
31
31
|
|
|
32
|
-
state = new State(storage, entity,
|
|
32
|
+
state = new State(storage, entity, outbox, manifest.entity.associated)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
: remap(manifest.operations, (definition, endpoint) =>
|
|
38
|
-
boot.operation(manifest, endpoint, definition, context, state))
|
|
39
|
-
|
|
35
|
+
const phases = await boot.rc(manifest, context)
|
|
36
|
+
const operations = await bootOperations(manifest, context, state, phases?.preflight)
|
|
40
37
|
const component = new Component(locator, operations)
|
|
41
38
|
|
|
42
39
|
if (storage) component.depends(storage)
|
|
43
|
-
if (emission) component.depends(emission)
|
|
44
40
|
|
|
45
|
-
|
|
41
|
+
// the outbox owns the emission and the storage, so it drains before either goes down
|
|
42
|
+
if (outbox) component.depends(outbox)
|
|
43
|
+
else if (emission) component.depends(emission)
|
|
44
|
+
|
|
45
|
+
const decorated = boot.extensions.component(component)
|
|
46
|
+
|
|
47
|
+
if (phases?.settle !== undefined)
|
|
48
|
+
decorated.settle = phases.settle
|
|
49
|
+
|
|
50
|
+
// a dependency closes after its dependant, so the component is already closed
|
|
51
|
+
// when the RC releases what it opened
|
|
52
|
+
if (phases?.dispose !== undefined)
|
|
53
|
+
decorated.depends(phases.dispose)
|
|
54
|
+
|
|
55
|
+
return decorated
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function bootOperations (manifest, context, state, preflight) {
|
|
59
|
+
if (manifest.operations === undefined)
|
|
60
|
+
return {}
|
|
61
|
+
|
|
62
|
+
const entries = Object.entries(manifest.operations)
|
|
63
|
+
|
|
64
|
+
// each one loads its algorithm from disk and compiles its contracts
|
|
65
|
+
const booted = await Promise.all(entries.map(([endpoint, definition]) =>
|
|
66
|
+
boot.operation(manifest, endpoint, definition, context, state, preflight)))
|
|
67
|
+
|
|
68
|
+
const operations = {}
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < entries.length; i++)
|
|
71
|
+
operations[entries[i][0]] = booted[i]
|
|
72
|
+
|
|
73
|
+
return operations
|
|
46
74
|
}
|
|
47
75
|
|
|
48
76
|
exports.component = component
|
package/src/composition.js
CHANGED
|
@@ -1,24 +1,93 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { console } = require('openspan')
|
|
3
4
|
const { Composition } = require('@toa.io/core')
|
|
5
|
+
const { version } = require('@toa.io/runtime/package.json')
|
|
4
6
|
|
|
5
7
|
const boot = require('./index')
|
|
8
|
+
const { span } = require('./span')
|
|
6
9
|
|
|
7
10
|
async function composition (paths, options) {
|
|
8
11
|
options = Object.assign({}, options)
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const components = await Promise.all(manifests.map(boot.component))
|
|
13
|
+
return span('boot composition', async () => {
|
|
14
|
+
const manifests = await span('load manifests',
|
|
15
|
+
async () => await Promise.all(paths.map((path) => boot.manifest(path, options))))
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
console.info('Starting composition', {
|
|
18
|
+
runtime: version,
|
|
19
|
+
components: manifests.map((manifest) => manifest.locator.id)
|
|
20
|
+
})
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const tenants = await span('create tenants',
|
|
23
|
+
async () => await Promise.all(manifests.map(boot.extensions.tenants)))
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
const expositions = await span('expose discovery',
|
|
26
|
+
async () => await Promise.all(manifests.map(boot.discovery.expose)))
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const components = await span('create components',
|
|
30
|
+
async () => await Promise.all(manifests.map(boot.component)))
|
|
31
|
+
|
|
32
|
+
const groups = components.map((component, index) =>
|
|
33
|
+
boot.bindings.produce(component, manifests[index].operations))
|
|
34
|
+
|
|
35
|
+
const receivers = await span('create receivers',
|
|
36
|
+
async () => await Promise.all(components.map((component, index) =>
|
|
37
|
+
boot.receivers(manifests[index], component))))
|
|
38
|
+
|
|
39
|
+
const producers = []
|
|
40
|
+
const settles = []
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < components.length; i++) {
|
|
43
|
+
const { local, other } = groups[i]
|
|
44
|
+
const settle = components[i].settle
|
|
45
|
+
const serving = [...local, ...other]
|
|
46
|
+
|
|
47
|
+
producers.push(...serving)
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* A receiver turns an event into a call to an operation of its own component, and
|
|
51
|
+
* that call is served by these — the loop producer first, the broker's if it has
|
|
52
|
+
* already gone. They are siblings under the composition otherwise, so a receiver
|
|
53
|
+
* still draining a delivery would be left calling something already torn down.
|
|
54
|
+
*/
|
|
55
|
+
// one at a time: `depends` given an array links the group it makes rather than its
|
|
56
|
+
// members, and it is the members whose teardown has to wait for this one
|
|
57
|
+
for (const receiver of receivers[i])
|
|
58
|
+
for (const producer of serving) receiver.depends(producer)
|
|
59
|
+
|
|
60
|
+
if (settle === undefined)
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
if (local.length > 0)
|
|
64
|
+
settle.depends(local)
|
|
65
|
+
|
|
66
|
+
for (const producer of other)
|
|
67
|
+
producer.depends(settle)
|
|
68
|
+
|
|
69
|
+
for (const receiver of receivers[i])
|
|
70
|
+
receiver.depends(settle)
|
|
71
|
+
|
|
72
|
+
settles.push(settle)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const composition = new Composition(expositions.flat(), producers.concat(settles), receivers.flat(), tenants.flat())
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
* A lookup that is never answered holds a connection open, and whoever
|
|
79
|
+
* awaits it has nothing to disconnect — the remote it would belong to does
|
|
80
|
+
* not exist yet. The composition owns the process's discovery instead, so
|
|
81
|
+
* an unanswered lookup cannot outlive it.
|
|
82
|
+
*/
|
|
83
|
+
composition.depends(await boot.discovery.discovery())
|
|
84
|
+
|
|
85
|
+
return boot.extensions.manage(composition)
|
|
86
|
+
} catch (exception) {
|
|
87
|
+
await Promise.all(expositions.flat().map((connector) => connector.disconnect(true)))
|
|
88
|
+
throw exception
|
|
89
|
+
}
|
|
90
|
+
})
|
|
22
91
|
}
|
|
23
92
|
|
|
24
93
|
exports.composition = composition
|
package/src/context.js
CHANGED
|
@@ -5,9 +5,12 @@ const { Context, Locator } = require('@toa.io/core')
|
|
|
5
5
|
const boot = require('./index')
|
|
6
6
|
|
|
7
7
|
const context = async (manifest) => {
|
|
8
|
-
const local = await boot.remote(manifest.locator, manifest)
|
|
8
|
+
const local = await boot.remote(manifest.locator, undefined, manifest)
|
|
9
9
|
const aspects = boot.extensions.aspects(manifest)
|
|
10
10
|
|
|
11
|
+
// a system aspect: it is there in every component, declared by none of them
|
|
12
|
+
aspects.push(boot.atom(manifest.locator.id))
|
|
13
|
+
|
|
11
14
|
const lookup = async (namespace, name) => {
|
|
12
15
|
const locator = new Locator(name, namespace)
|
|
13
16
|
const remote = await boot.remote(locator)
|
package/src/contract.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { contract: { Request, Reply } } = require('@toa.io/core')
|
|
4
|
-
const
|
|
4
|
+
const schemas = require('@toa.io/schemas')
|
|
5
5
|
|
|
6
6
|
const request = (definition, entity) => {
|
|
7
7
|
const request = Request.schema(definition, entity)
|
|
8
|
-
const schema =
|
|
8
|
+
const schema = schemas.schema(request, { removeAdditional: true })
|
|
9
9
|
|
|
10
|
-
return new Request(schema)
|
|
10
|
+
return new Request(schema, definition)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const reply = (output,
|
|
14
|
-
const reply = Reply.schema(output,
|
|
15
|
-
const schema =
|
|
13
|
+
const reply = (output, errors) => {
|
|
14
|
+
const reply = Reply.schema(output, errors)
|
|
15
|
+
const schema = schemas.schema(reply)
|
|
16
16
|
|
|
17
17
|
return new Reply(schema)
|
|
18
18
|
}
|
package/src/discovery.js
CHANGED
|
@@ -29,7 +29,8 @@ const lookup = async (locator) => {
|
|
|
29
29
|
const expose = async (manifest) => {
|
|
30
30
|
const exposition = new Exposition(manifest.locator, manifest)
|
|
31
31
|
const operations = { [ENDPOINT]: { bindings: BINDINGS } }
|
|
32
|
-
const
|
|
32
|
+
const { local, other } = boot.bindings.produce(exposition, operations)
|
|
33
|
+
const producers = local.concat(other)
|
|
33
34
|
|
|
34
35
|
await Promise.all(producers.map((producer) => producer.connect()))
|
|
35
36
|
|
package/src/emission.js
CHANGED
|
@@ -10,10 +10,10 @@ const emission = (definitions, locator, context) => {
|
|
|
10
10
|
|
|
11
11
|
const events = Object.entries(definitions).map(([label, definition]) => {
|
|
12
12
|
const emitter = boot.bindings.emit(definition.binding, locator, label)
|
|
13
|
-
const decorator = extensions.emitter(emitter, label)
|
|
13
|
+
const decorator = extensions.emitter(emitter, label, locator)
|
|
14
14
|
const bridge = boot.bridge.event(definition.bridge, definition.path, label, context)
|
|
15
15
|
|
|
16
|
-
return new Event(definition, decorator, bridge)
|
|
16
|
+
return new Event({ ...definition, label: `${locator.id}.${label}` }, decorator, bridge)
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
return new Emission(events)
|
package/src/events.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The events of this component that something consumes. An event nobody consumes is not
|
|
5
|
+
* published, so it gets no emitter and no exchange, and a component none of whose events are
|
|
6
|
+
* consumed gets no emission — and therefore no outbox and no transaction.
|
|
7
|
+
*
|
|
8
|
+
* The deployment computes this from the whole context. Without one — a local run, `toa mono`,
|
|
9
|
+
* a composition booted in a test — the variable is absent and every event is published.
|
|
10
|
+
*
|
|
11
|
+
* @param {toa.norm.Component} manifest
|
|
12
|
+
* @returns {toa.norm.Events | undefined}
|
|
13
|
+
*/
|
|
14
|
+
const events = (manifest) => {
|
|
15
|
+
if (manifest.events === undefined) return
|
|
16
|
+
|
|
17
|
+
const value = process.env[VARIABLE + manifest.locator.uppercase]
|
|
18
|
+
|
|
19
|
+
if (value === undefined) return manifest.events
|
|
20
|
+
|
|
21
|
+
const consumed = new Set(value.split(' ').filter(Boolean))
|
|
22
|
+
const entries = Object.entries(manifest.events).filter(([label]) => consumed.has(label))
|
|
23
|
+
|
|
24
|
+
return entries.length === 0 ? undefined : Object.fromEntries(entries)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const VARIABLE = 'TOA_EVENTS_'
|
|
28
|
+
|
|
29
|
+
exports.events = events
|
|
@@ -5,13 +5,14 @@ const { instances } = require('./instances')
|
|
|
5
5
|
/**
|
|
6
6
|
* @param {toa.core.bindings.Emitter} emitter
|
|
7
7
|
* @param {string} label
|
|
8
|
+
* @param {toa.core.Locator} locator
|
|
8
9
|
* @returns {toa.core.bindings.Emitter}
|
|
9
10
|
*/
|
|
10
|
-
const emitter = (emitter, label) => {
|
|
11
|
+
const emitter = (emitter, label, locator) => {
|
|
11
12
|
let decorated = emitter
|
|
12
13
|
|
|
13
14
|
for (const factory of Object.values(instances)) {
|
|
14
|
-
if (factory.emitter !== undefined) decorated = factory.emitter(decorated, label)
|
|
15
|
+
if (factory.emitter !== undefined) decorated = factory.emitter(decorated, label, locator)
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
return decorated
|
package/src/extensions/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const { component } = require('./component')
|
|
|
5
5
|
const { context } = require('./context')
|
|
6
6
|
const { emitter } = require('./emitter')
|
|
7
7
|
const { load } = require('./load')
|
|
8
|
+
const { manage } = require('./manage')
|
|
8
9
|
const { receiver } = require('./receiver')
|
|
9
10
|
const { storage } = require('./storage')
|
|
10
11
|
const { tenants } = require('./tenants')
|
|
@@ -14,6 +15,7 @@ exports.component = component
|
|
|
14
15
|
exports.context = context
|
|
15
16
|
exports.emitter = emitter
|
|
16
17
|
exports.load = load
|
|
18
|
+
exports.manage = manage
|
|
17
19
|
exports.receiver = receiver
|
|
18
20
|
exports.storage = storage
|
|
19
21
|
exports.tenants = tenants
|
package/src/extensions/load.js
CHANGED
|
@@ -6,9 +6,11 @@ const { resolve } = require('./resolve')
|
|
|
6
6
|
* @param {toa.norm.Component} manifest
|
|
7
7
|
*/
|
|
8
8
|
const load = (manifest) => {
|
|
9
|
-
if (manifest.extensions === undefined)
|
|
9
|
+
if (manifest.extensions === undefined)
|
|
10
|
+
return
|
|
10
11
|
|
|
11
|
-
for (const name of Object.keys(manifest.extensions))
|
|
12
|
+
for (const name of Object.keys(manifest.extensions))
|
|
13
|
+
resolve(name, manifest.path)
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
exports.load = load
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { instances } = require('./instances')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {toa.core.Connector} composition
|
|
7
|
+
* @returns {toa.core.Connector}
|
|
8
|
+
*/
|
|
9
|
+
const manage = (composition) => {
|
|
10
|
+
let managed = composition
|
|
11
|
+
|
|
12
|
+
for (const factory of Object.values(instances)) {
|
|
13
|
+
if (factory.manage !== undefined)
|
|
14
|
+
managed = factory.manage(managed)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return managed
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
exports.manage = manage
|
|
@@ -11,7 +11,8 @@ const receiver = (receiver, locator) => {
|
|
|
11
11
|
let decorated = receiver
|
|
12
12
|
|
|
13
13
|
for (const factory of Object.values(instances)) {
|
|
14
|
-
if (factory.receiver !== undefined)
|
|
14
|
+
if (factory.receiver !== undefined)
|
|
15
|
+
decorated = factory.receiver(decorated, locator)
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
return decorated
|
package/src/guards.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Guard } = require('@toa.io/core')
|
|
4
|
+
const boot = require('./index')
|
|
5
|
+
|
|
6
|
+
async function guards(manifest, context) {
|
|
7
|
+
if (manifest.guards === undefined)
|
|
8
|
+
return
|
|
9
|
+
|
|
10
|
+
const entries = Object.entries(manifest.guards)
|
|
11
|
+
|
|
12
|
+
return await Promise.all(entries.map(async ([name, guard]) => {
|
|
13
|
+
const bridge = await load(guard, name, context)
|
|
14
|
+
|
|
15
|
+
return new Guard(name, bridge)
|
|
16
|
+
}))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function load(guard, name, context) {
|
|
20
|
+
return await boot.bridge.guard(guard.bridge, guard.path, name, context)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
exports.guards = guards
|
package/src/index.js
CHANGED
|
@@ -8,12 +8,18 @@ const { component } = require('./component')
|
|
|
8
8
|
const { composition } = require('./composition')
|
|
9
9
|
const { context } = require('./context')
|
|
10
10
|
const { emission } = require('./emission')
|
|
11
|
+
const { events } = require('./events')
|
|
12
|
+
const { outbox } = require('./outbox')
|
|
13
|
+
const { atomicity } = require('./atomicity')
|
|
14
|
+
const { atom } = require('./atom')
|
|
11
15
|
const { exposition } = require('./exposition')
|
|
12
16
|
const { manifest } = require('./manifest')
|
|
13
17
|
const { operation } = require('./operation')
|
|
14
18
|
const { receivers, receive } = require('./receivers')
|
|
15
19
|
const { remote } = require('./remote')
|
|
16
20
|
const { storage } = require('./storage')
|
|
21
|
+
const { guards } = require('./guards')
|
|
22
|
+
const { rc } = require('./rc')
|
|
17
23
|
|
|
18
24
|
exports.bindings = require('./bindings')
|
|
19
25
|
exports.bridge = require('./bridge')
|
|
@@ -27,6 +33,10 @@ exports.component = component
|
|
|
27
33
|
exports.composition = composition
|
|
28
34
|
exports.context = context
|
|
29
35
|
exports.emission = emission
|
|
36
|
+
exports.events = events
|
|
37
|
+
exports.outbox = outbox
|
|
38
|
+
exports.atomicity = atomicity
|
|
39
|
+
exports.atom = atom
|
|
30
40
|
exports.exposition = exposition
|
|
31
41
|
exports.manifest = manifest
|
|
32
42
|
exports.operation = operation
|
|
@@ -34,3 +44,5 @@ exports.receivers = receivers
|
|
|
34
44
|
exports.receive = receive
|
|
35
45
|
exports.remote = remote
|
|
36
46
|
exports.storage = storage
|
|
47
|
+
exports.guards = guards
|
|
48
|
+
exports.rc = rc
|
package/src/manifest.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const clone = require('clone-deep')
|
|
4
|
+
const { basename } = require('node:path')
|
|
4
5
|
const { merge } = require('@toa.io/generic')
|
|
5
6
|
const { component: load } = require('@toa.io/norm')
|
|
6
7
|
const { Locator } = require('@toa.io/core')
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*/
|
|
9
|
+
const { span } = require('./span')
|
|
10
|
+
|
|
11
11
|
const manifest = async (path, options = {}) => {
|
|
12
12
|
options = merge(clone(options), DEFAULTS)
|
|
13
13
|
|
|
14
|
-
const manifest = await
|
|
14
|
+
const manifest = await span({ name: `manifest ${basename(path)}`, attributes: { path } },
|
|
15
|
+
async () => await load(path))
|
|
15
16
|
|
|
16
17
|
if (options?.bindings !== undefined) {
|
|
17
18
|
if ('operations' in manifest) {
|
package/src/operation.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
Transition,
|
|
5
|
+
Observation,
|
|
6
|
+
Assignment,
|
|
7
|
+
Operation,
|
|
8
|
+
Query,
|
|
9
|
+
Effect,
|
|
10
|
+
Unmanaged
|
|
11
|
+
} = require('@toa.io/core')
|
|
4
12
|
|
|
5
13
|
const boot = require('./index')
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
const cascade = boot.cascade(manifest, endpoint, definition, context)
|
|
15
|
+
async function operation (manifest, endpoint, definition, context, scope, preflight) {
|
|
16
|
+
const cascade = await boot.cascade(manifest, endpoint, definition, context, preflight)
|
|
9
17
|
const reply = boot.contract.reply(definition.output, definition.error)
|
|
10
18
|
const input = definition.input
|
|
11
19
|
const request = boot.contract.request({ input }, manifest.entity)
|
|
12
20
|
const contracts = { reply, request }
|
|
21
|
+
|
|
13
22
|
const query = manifest.entity === undefined
|
|
14
23
|
? undefined
|
|
15
24
|
: new Query(manifest.entity.schema.properties)
|
|
@@ -24,7 +33,8 @@ const TYPES = {
|
|
|
24
33
|
observation: Observation,
|
|
25
34
|
assignment: Assignment,
|
|
26
35
|
computation: Operation,
|
|
27
|
-
effect:
|
|
36
|
+
effect: Effect,
|
|
37
|
+
unmanaged: Unmanaged
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
exports.operation = operation
|
package/src/outbox.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Outbox } = require('@toa.io/core')
|
|
4
|
+
|
|
5
|
+
const boot = require('./index')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A component with no declared events has nothing to publish and therefore no outbox — the
|
|
9
|
+
* static check that keeps the common case free of a transaction.
|
|
10
|
+
*
|
|
11
|
+
* @param {toa.norm.Component} manifest
|
|
12
|
+
* @param {toa.core.Storage} [storage]
|
|
13
|
+
* @param {toa.core.Emission} [emission]
|
|
14
|
+
*/
|
|
15
|
+
const outbox = (manifest, storage, emission) => {
|
|
16
|
+
if (emission === undefined) return
|
|
17
|
+
|
|
18
|
+
return new Outbox(emission, storage, boot.atomicity(manifest.locator.id), {})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
exports.outbox = outbox
|
package/src/rc.js
ADDED
package/src/receivers.js
CHANGED
|
@@ -5,23 +5,29 @@ const { Receiver, Locator } = require('@toa.io/core')
|
|
|
5
5
|
const boot = require('./index')
|
|
6
6
|
const extensions = require('./extensions')
|
|
7
7
|
|
|
8
|
-
const receivers = async (manifest,
|
|
8
|
+
const receivers = async (manifest, component) => {
|
|
9
9
|
if (manifest.receivers === undefined) return []
|
|
10
10
|
|
|
11
11
|
const receivers = []
|
|
12
|
-
const local = await boot.remote(manifest.locator, manifest)
|
|
12
|
+
const local = await boot.remote(manifest.locator, undefined, manifest)
|
|
13
13
|
|
|
14
14
|
for (const [label, definition] of Object.entries(manifest.receivers)) {
|
|
15
|
+
const locator = Locator.parse(label)
|
|
16
|
+
const source = definition.source ? Locator.parse(definition.source) : locator
|
|
17
|
+
const event = label.split('.').pop()
|
|
18
|
+
const destination = `${source.id}.${event}`
|
|
19
|
+
|
|
20
|
+
// the origin of the calls this receiver makes to its local operation
|
|
21
|
+
const origin = { namespace: source.namespace, component: source.name, event }
|
|
22
|
+
|
|
15
23
|
const bridge = definition.bridge !== undefined ? boot.bridge.receiver(definition.bridge, manifest.path, label) : undefined
|
|
16
|
-
const receiver = new Receiver(definition, local, bridge)
|
|
24
|
+
const receiver = new Receiver({ ...definition, label, destination, origin }, local, bridge)
|
|
17
25
|
const decorator = extensions.receiver(receiver, manifest.locator)
|
|
18
26
|
|
|
19
|
-
const locator = Locator.parse(label)
|
|
20
27
|
const transport = definition.binding ?? await resolveBinding(locator, label)
|
|
21
|
-
const source = definition.source ? Locator.parse(definition.source) : locator
|
|
22
28
|
const binding = boot.bindings.receive(transport, source, label, manifest.locator.id, decorator)
|
|
23
29
|
|
|
24
|
-
binding.depends(
|
|
30
|
+
binding.depends(component)
|
|
25
31
|
receivers.push(binding)
|
|
26
32
|
}
|
|
27
33
|
|
package/src/remote.js
CHANGED
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { console } = require('openspan')
|
|
3
4
|
const { Remote } = require('@toa.io/core')
|
|
4
5
|
const { remap } = require('@toa.io/generic')
|
|
5
6
|
|
|
6
7
|
const boot = require('./index')
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* @param {toa.core.Locator} locator
|
|
11
|
+
* @param {toa.core.Source} [source] the origin stamped on every call made through this remote
|
|
12
|
+
* @param {toa.norm.Component} [manifest] skips discovery when the manifest is already known
|
|
13
|
+
*/
|
|
14
|
+
const remote = async (locator, source, manifest) => {
|
|
15
|
+
let discovery
|
|
10
16
|
|
|
11
|
-
if (manifest === undefined)
|
|
17
|
+
if (manifest === undefined) {
|
|
18
|
+
console.debug('Lookup', { locator: locator.id })
|
|
19
|
+
|
|
20
|
+
discovery = await boot.discovery.discovery(locator)
|
|
21
|
+
manifest = await discovery.lookup(locator)
|
|
22
|
+
}
|
|
12
23
|
|
|
13
24
|
const calls = manifest.operations === undefined
|
|
14
25
|
? {}
|
|
15
|
-
: remap(manifest.operations,
|
|
26
|
+
: remap(manifest.operations,
|
|
27
|
+
(definition, endpoint) => boot.call(locator, endpoint, definition, manifest.entity, source))
|
|
16
28
|
|
|
17
29
|
const remote = new Remote(locator, calls)
|
|
18
30
|
|
|
19
31
|
// ensure discovery shutdown
|
|
20
|
-
|
|
32
|
+
if (discovery !== undefined)
|
|
33
|
+
remote.depends(discovery)
|
|
21
34
|
|
|
22
35
|
return remote
|
|
23
36
|
}
|
package/src/span.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { console, traces } = require('openspan')
|
|
4
|
+
|
|
5
|
+
const enabled = process.env.TOA_BOOT_TRACE === '1'
|
|
6
|
+
|
|
7
|
+
// Tracing is off until an exporter is configured, and the CLI opens its own span before
|
|
8
|
+
// any extension gets to configure one — so asking for a boot trace turns the console
|
|
9
|
+
// exporter on here. Extensions keep it: see `development()` in the telemetry factory.
|
|
10
|
+
if (enabled) traces({ exporters: { console: {} } })
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a span if boot tracing is enabled (TOA_BOOT_TRACE=1), otherwise just runs the task
|
|
14
|
+
*
|
|
15
|
+
* @param {string | object} options
|
|
16
|
+
* @param {() => Promise<any>} task
|
|
17
|
+
* @returns {Promise<any>}
|
|
18
|
+
*/
|
|
19
|
+
const span = async (options, task) => enabled ? console.span(options, task) : task()
|
|
20
|
+
|
|
21
|
+
exports.span = span
|
package/src/storage.js
CHANGED
|
@@ -3,14 +3,19 @@
|
|
|
3
3
|
const { join } = require('node:path')
|
|
4
4
|
const extensions = require('./extensions')
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* @param {toa.norm.Component} manifest
|
|
8
|
+
* @param {boolean} outbox whether this component publishes anything, and so needs a place to
|
|
9
|
+
* commit it with the entity
|
|
10
|
+
*/
|
|
11
|
+
const storage = (manifest, outbox) => {
|
|
7
12
|
if (manifest.entity === undefined) return
|
|
8
13
|
|
|
9
14
|
const Factory = load(manifest)
|
|
10
15
|
|
|
11
16
|
/** @type {toa.core.storages.Factory} */
|
|
12
17
|
const factory = new Factory()
|
|
13
|
-
const storage = factory.storage(manifest.locator, manifest.entity)
|
|
18
|
+
const storage = factory.storage(manifest.locator, manifest.entity, { outbox })
|
|
14
19
|
|
|
15
20
|
return extensions.storage(storage)
|
|
16
21
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -5,17 +5,17 @@ export * as bindings from './bindings'
|
|
|
5
5
|
|
|
6
6
|
export async function composition (paths: string[], options?: composition.Options): Promise<core.Connector>
|
|
7
7
|
|
|
8
|
-
export async function remote (locator: core.Locator): Promise<core.
|
|
8
|
+
export async function remote (locator: core.Locator, source?: core.Source): Promise<core.Remote>
|
|
9
9
|
|
|
10
10
|
export async function receive<T = any> (
|
|
11
11
|
label: string,
|
|
12
|
-
receiver: Receiver
|
|
12
|
+
receiver: Receiver
|
|
13
13
|
): Promise<core.Connector>
|
|
14
14
|
|
|
15
15
|
export async function receive<T = any> (
|
|
16
16
|
label: string,
|
|
17
17
|
group: string | undefined,
|
|
18
|
-
receiver: Receiver
|
|
18
|
+
receiver: Receiver
|
|
19
19
|
): Promise<core.Connector>
|
|
20
20
|
|
|
21
21
|
type Receiver = { receive: (message: core.Message<T>) => void | Promise<void> }
|