@toa.io/operations 1.0.0-alpha.270 → 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 +44 -0
- package/package.json +6 -6
- package/src/deployment/.deployment/.describe/events.js +67 -0
- package/src/deployment/.deployment/describe.js +46 -0
- package/src/deployment/.deployment/merge.js +5 -1
- package/types/_deployment/dependency.d.ts +2 -0
- package/types/dependency.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,50 @@
|
|
|
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.272](https://github.com/toa-io/toa/compare/v1.0.0-alpha.271...v1.0.0-alpha.272) (2026-09-01)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
* feat(atomicity)!: take a quorum of independent servers ([862562f](https://github.com/toa-io/toa/commit/862562f24d77ec00127dbbd88d43802b208c643b))
|
|
10
|
+
* refactor(atomicity)!: take one Redis, not a cluster ([b0a4770](https://github.com/toa-io/toa/commit/b0a4770dff5dfe8fee4b9ae637a4dd297454fcd4))
|
|
11
|
+
* refactor(core)!: pump the outbox in one cycle ([bf590fe](https://github.com/toa-io/toa/commit/bf590fe8ed59c701b5fd1a91ba4874685b79242f))
|
|
12
|
+
* refactor(atomicity)!: rename the connector and free it of the outbox ([21f1a41](https://github.com/toa-io/toa/commit/21f1a41aceafcfd35c7620014062fe46b54afa95))
|
|
13
|
+
* 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)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **atomicity:** make the interval a setting, and stop repeating n-and-i ([5c3a6d1](https://github.com/toa-io/toa/commit/5c3a6d1533751a21b7eb7d9c737f1a270bf5a5ae))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### BREAKING CHANGES
|
|
22
|
+
|
|
23
|
+
* `atomicity.redis` accepts a list again, of independent servers rather
|
|
24
|
+
than cluster nodes, and refuses an even number of them.
|
|
25
|
+
|
|
26
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
27
|
+
* `atomicity.redis` is a string. A list is refused at export.
|
|
28
|
+
|
|
29
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
30
|
+
* `Storage.outbox.pending` takes a fourth argument, the id to
|
|
31
|
+
continue from.
|
|
32
|
+
|
|
33
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
34
|
+
* `@toa.io/partitions.redis` is now `@toa.io/atomicity`, and its
|
|
35
|
+
`lanes(total)` is `slots(total)`. Redis is declared as `atomicity` in the context
|
|
36
|
+
rather than `outbox.redis`, and read from `TOA_ATOMICITY_REDIS`.
|
|
37
|
+
`TOA_OUTBOX_PARTITION_INTERVAL` is `TOA_ATOMICITY_INTERVAL`.
|
|
38
|
+
|
|
39
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
40
|
+
* `event.changeset` is removed; use `origin` and `state`. `State`
|
|
41
|
+
takes an `Outbox` in place of an `Emission`. `difference` is dropped from
|
|
42
|
+
`@toa.io/generic` along with its last caller.
|
|
43
|
+
|
|
44
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
6
50
|
# [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)
|
|
7
51
|
|
|
8
52
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/operations",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.272",
|
|
4
4
|
"description": "Toa Deployment",
|
|
5
5
|
"homepage": "https://toa.io",
|
|
6
6
|
"author": {
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@toa.io/filesystem": "1.0.0-alpha.
|
|
31
|
-
"@toa.io/generic": "1.0.0-alpha.
|
|
32
|
-
"@toa.io/norm": "1.0.0-alpha.
|
|
33
|
-
"@toa.io/yaml": "1.0.0-alpha.
|
|
30
|
+
"@toa.io/filesystem": "1.0.0-alpha.272",
|
|
31
|
+
"@toa.io/generic": "1.0.0-alpha.272",
|
|
32
|
+
"@toa.io/norm": "1.0.0-alpha.272",
|
|
33
|
+
"@toa.io/yaml": "1.0.0-alpha.272",
|
|
34
34
|
"execa": "5.1.1",
|
|
35
35
|
"fs-extra": "11.1.1"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "1a451755445935c04cf5b373bcc73942cac0bb10"
|
|
38
38
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tells each component which of its events something consumes. An event nobody consumes gets
|
|
5
|
+
* no emitter, no exchange and no outbox row, and a component none of whose events are consumed
|
|
6
|
+
* gets no outbox at all.
|
|
7
|
+
*
|
|
8
|
+
* A component that declares events always gets the variable, empty when nothing is consumed —
|
|
9
|
+
* an absent variable means every event, which is what a run without a deployment gets.
|
|
10
|
+
*
|
|
11
|
+
* @param {toa.norm.Context} context
|
|
12
|
+
* @param {toa.deployment.Dependency} dependency
|
|
13
|
+
*/
|
|
14
|
+
function events (context, dependency) {
|
|
15
|
+
const components = deployed(context)
|
|
16
|
+
const consumed = collect(components, context.events, dependency.events)
|
|
17
|
+
|
|
18
|
+
for (const { locator, events } of components) {
|
|
19
|
+
if (events === undefined) continue
|
|
20
|
+
|
|
21
|
+
const labels = Object.keys(events)
|
|
22
|
+
.filter((label) => consumed.has(locator.id + '.' + label))
|
|
23
|
+
|
|
24
|
+
dependency.variables[locator.label] ??= []
|
|
25
|
+
|
|
26
|
+
dependency.variables[locator.label].push({
|
|
27
|
+
name: VARIABLE + locator.uppercase,
|
|
28
|
+
value: labels.join(' ')
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Every component this context deploys, its own and those its extensions bring. `components`
|
|
35
|
+
* holds only the former; the latter reach a deployment as instances of what they depend on,
|
|
36
|
+
* which is also how they are given the rest of their variables.
|
|
37
|
+
*/
|
|
38
|
+
function deployed (context) {
|
|
39
|
+
const components = new Map()
|
|
40
|
+
|
|
41
|
+
for (const instances of Object.values(context.dependencies ?? {}))
|
|
42
|
+
for (const { component } of instances)
|
|
43
|
+
components.set(component.locator.id, component)
|
|
44
|
+
|
|
45
|
+
return [...components.values()]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Everything that consumes an event of this context: the receivers of its own components, what
|
|
50
|
+
* a dependency declares it consumes, and what the context says is consumed outside it.
|
|
51
|
+
*/
|
|
52
|
+
function collect (components, declared, contributed) {
|
|
53
|
+
const consumed = new Set(declared ?? [])
|
|
54
|
+
|
|
55
|
+
for (const label of contributed ?? []) consumed.add(label)
|
|
56
|
+
|
|
57
|
+
for (const { receivers } of components)
|
|
58
|
+
for (const [label, receiver] of Object.entries(receivers ?? {}))
|
|
59
|
+
// a receiver with a source consumes from a foreign broker, not from this context
|
|
60
|
+
if (receiver.source === undefined) consumed.add(label)
|
|
61
|
+
|
|
62
|
+
return consumed
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const VARIABLE = 'TOA_EVENTS_'
|
|
66
|
+
|
|
67
|
+
exports.events = events
|
|
@@ -4,6 +4,7 @@ const desc = require('./.describe')
|
|
|
4
4
|
const { addVariables } = require('./.describe/variables')
|
|
5
5
|
const { addMounts } = require('./.describe/mounts')
|
|
6
6
|
const { resources } = require('./.describe/resources')
|
|
7
|
+
const { events } = require('./.describe/events')
|
|
7
8
|
|
|
8
9
|
const describe = (context, compositions, dependency, image) => {
|
|
9
10
|
const { services } = dependency
|
|
@@ -20,6 +21,51 @@ const describe = (context, compositions, dependency, image) => {
|
|
|
20
21
|
}
|
|
21
22
|
)
|
|
22
23
|
|
|
24
|
+
const atomicity = context.atomicity
|
|
25
|
+
|
|
26
|
+
if (atomicity?.redis !== undefined) {
|
|
27
|
+
const addresses = Array.isArray(atomicity.redis) ? atomicity.redis : [atomicity.redis]
|
|
28
|
+
|
|
29
|
+
// a lock is taken on `floor(n / 2) + 1` of them, so an even number tolerates no more
|
|
30
|
+
// losses than the odd number below it, and two tolerate fewer than one does
|
|
31
|
+
if (addresses.length % 2 === 0)
|
|
32
|
+
throw new Error(`'atomicity.redis' takes an odd number of addresses, ` +
|
|
33
|
+
`${addresses.length} given`)
|
|
34
|
+
|
|
35
|
+
dependency.variables.global.push({
|
|
36
|
+
name: 'TOA_ATOMICITY_REDIS',
|
|
37
|
+
value: addresses.join(' ')
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (atomicity?.interval !== undefined)
|
|
42
|
+
dependency.variables.global.push({
|
|
43
|
+
name: 'TOA_ATOMICITY_INTERVAL',
|
|
44
|
+
value: String(atomicity.interval)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const outbox = context.outbox
|
|
48
|
+
|
|
49
|
+
if (outbox?.interval !== undefined)
|
|
50
|
+
dependency.variables.global.push({
|
|
51
|
+
name: 'TOA_OUTBOX_INTERVAL',
|
|
52
|
+
value: String(outbox.interval)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (outbox?.batch !== undefined)
|
|
56
|
+
dependency.variables.global.push({
|
|
57
|
+
name: 'TOA_OUTBOX_BATCH',
|
|
58
|
+
value: String(outbox.batch)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
if (outbox?.retention !== undefined)
|
|
62
|
+
dependency.variables.global.push({
|
|
63
|
+
name: 'TOA_OUTBOX_RETENTION',
|
|
64
|
+
value: String(outbox.retention)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
events(context, dependency)
|
|
68
|
+
|
|
23
69
|
const credentials = context.registry?.credentials
|
|
24
70
|
|
|
25
71
|
if (image !== undefined) {
|
|
@@ -13,6 +13,9 @@ const merge = (dependencies) => {
|
|
|
13
13
|
/** @type {toa.deployment.dependency.Variables} */
|
|
14
14
|
const variables = {}
|
|
15
15
|
|
|
16
|
+
/** event labels consumed by something other than a component's own receivers */
|
|
17
|
+
const events = []
|
|
18
|
+
|
|
16
19
|
const mounts = {}
|
|
17
20
|
|
|
18
21
|
/** @type {toa.deployment.dependency.Probe | false | undefined} */
|
|
@@ -23,13 +26,14 @@ const merge = (dependencies) => {
|
|
|
23
26
|
if (dependency.services !== undefined) services.push(...dependency.services)
|
|
24
27
|
if (dependency.proxies !== undefined) proxies.push(...dependency.proxies)
|
|
25
28
|
if (dependency.variables !== undefined) append(variables, dependency.variables)
|
|
29
|
+
if (dependency.events !== undefined) events.push(...dependency.events)
|
|
26
30
|
if (dependency.mounts !== undefined) append(mounts, dependency.mounts)
|
|
27
31
|
if (dependency.probe !== undefined) probe = dependency.probe
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
reserve(services, probe)
|
|
31
35
|
|
|
32
|
-
return { references, services, proxies, variables, mounts, probe }
|
|
36
|
+
return { references, services, proxies, variables, mounts, events, probe }
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
/**
|
|
@@ -50,6 +50,7 @@ declare namespace toa.deployment {
|
|
|
50
50
|
services?: Service[] // dependency.Service
|
|
51
51
|
proxies?: Proxy[]
|
|
52
52
|
variables?: Variables
|
|
53
|
+
events?: string[]
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
}
|
|
@@ -59,6 +60,7 @@ declare namespace toa.deployment {
|
|
|
59
60
|
services?: _service.Service[] // deployment.Service
|
|
60
61
|
proxies?: dependency.Proxy[]
|
|
61
62
|
variables?: dependency.Variables
|
|
63
|
+
events?: string[]
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
}
|
package/types/dependency.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface Dependency {
|
|
|
38
38
|
services?: Service[]
|
|
39
39
|
variables?: Variables
|
|
40
40
|
mounts?: Mounts
|
|
41
|
+
/** Event labels (`namespace.name.event`) this dependency consumes. */
|
|
42
|
+
events?: string[]
|
|
41
43
|
/** Default probe for compositions and services without their own probe. `false` disables. */
|
|
42
44
|
probe?: Probe | false
|
|
43
45
|
}
|