@toa.io/boot 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 +46 -0
- package/package.json +9 -8
- package/src/atom.js +41 -0
- package/src/atomicity.js +15 -0
- package/src/component.js +11 -4
- package/src/composition.js +14 -2
- package/src/context.js +3 -0
- package/src/events.js +29 -0
- package/src/index.js +8 -0
- package/src/outbox.js +21 -0
- package/src/storage.js +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,52 @@
|
|
|
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
|
+
### 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
|
+
|
|
6
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)
|
|
7
53
|
|
|
8
54
|
|
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,14 +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
30
|
"dotenv": "16.1.1",
|
|
30
|
-
"openspan": "1.0.0-alpha.
|
|
31
|
+
"openspan": "1.0.0-alpha.272"
|
|
31
32
|
},
|
|
32
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "1a451755445935c04cf5b373bcc73942cac0bb10"
|
|
33
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/component.js
CHANGED
|
@@ -14,9 +14,13 @@ const component = async (manifest) => {
|
|
|
14
14
|
|
|
15
15
|
const create = async (manifest, locator) => {
|
|
16
16
|
boot.extensions.load(manifest)
|
|
17
|
-
|
|
17
|
+
|
|
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)
|
|
18
21
|
const context = await boot.context(manifest)
|
|
19
|
-
const emission = boot.emission(
|
|
22
|
+
const emission = boot.emission(events, locator, context)
|
|
23
|
+
const outbox = boot.outbox(manifest, storage, emission)
|
|
20
24
|
|
|
21
25
|
let state
|
|
22
26
|
|
|
@@ -25,7 +29,7 @@ const create = async (manifest, locator) => {
|
|
|
25
29
|
const guards = await boot.guards(manifest, context)
|
|
26
30
|
const entity = new entities.Factory(schema, guards)
|
|
27
31
|
|
|
28
|
-
state = new State(storage, entity,
|
|
32
|
+
state = new State(storage, entity, outbox, manifest.entity.associated)
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
const phases = await boot.rc(manifest, context)
|
|
@@ -33,7 +37,10 @@ const create = async (manifest, locator) => {
|
|
|
33
37
|
const component = new Component(locator, operations)
|
|
34
38
|
|
|
35
39
|
if (storage) component.depends(storage)
|
|
36
|
-
|
|
40
|
+
|
|
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)
|
|
37
44
|
|
|
38
45
|
const decorated = boot.extensions.component(component)
|
|
39
46
|
|
package/src/composition.js
CHANGED
|
@@ -42,8 +42,20 @@ async function composition (paths, options) {
|
|
|
42
42
|
for (let i = 0; i < components.length; i++) {
|
|
43
43
|
const { local, other } = groups[i]
|
|
44
44
|
const settle = components[i].settle
|
|
45
|
-
|
|
46
|
-
|
|
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)
|
|
47
59
|
|
|
48
60
|
if (settle === undefined)
|
|
49
61
|
continue
|
package/src/context.js
CHANGED
|
@@ -8,6 +8,9 @@ const context = async (manifest) => {
|
|
|
8
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/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
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,10 @@ 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')
|
|
@@ -29,6 +33,10 @@ exports.component = component
|
|
|
29
33
|
exports.composition = composition
|
|
30
34
|
exports.context = context
|
|
31
35
|
exports.emission = emission
|
|
36
|
+
exports.events = events
|
|
37
|
+
exports.outbox = outbox
|
|
38
|
+
exports.atomicity = atomicity
|
|
39
|
+
exports.atom = atom
|
|
32
40
|
exports.exposition = exposition
|
|
33
41
|
exports.manifest = manifest
|
|
34
42
|
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/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
|
}
|