@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 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.270",
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/core": "1.0.0-alpha.270",
24
- "@toa.io/filesystem": "1.0.0-alpha.254",
25
- "@toa.io/generic": "1.0.0-alpha.254",
26
- "@toa.io/norm": "1.0.0-alpha.270",
27
- "@toa.io/schemas": "1.0.0-alpha.270",
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.270"
31
+ "openspan": "1.0.0-alpha.272"
31
32
  },
32
- "gitHead": "94400bdd411b4c25074ffcf58184580ddd060268"
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
@@ -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
- const storage = boot.storage(manifest)
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(manifest.events, locator, context)
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, emission, manifest.entity.associated)
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
- if (emission) component.depends(emission)
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
 
@@ -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
- producers.push(...local, ...other)
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
- const storage = (manifest) => {
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
  }