@toa.io/norm 1.0.0-alpha.270 → 1.0.0-alpha.273

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,68 @@
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.273](https://github.com/toa-io/toa/compare/v1.0.0-alpha.272...v1.0.0-alpha.273) (2026-09-02)
7
+
8
+ **Note:** Version bump only for package @toa.io/norm
9
+
10
+
11
+
12
+
13
+
14
+ # [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)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **atomicity:** honour a cluster, and let it be unreachable ([c475d66](https://github.com/toa-io/toa/commit/c475d663ddf7329d86da517ccceee4cb03280e95))
20
+
21
+
22
+ * feat(atomicity)!: take a quorum of independent servers ([862562f](https://github.com/toa-io/toa/commit/862562f24d77ec00127dbbd88d43802b208c643b))
23
+ * refactor(atomicity)!: take one Redis, not a cluster ([b0a4770](https://github.com/toa-io/toa/commit/b0a4770dff5dfe8fee4b9ae637a4dd297454fcd4))
24
+ * refactor(atomicity)!: take the lock manager from the stash ([4fd91eb](https://github.com/toa-io/toa/commit/4fd91eb2fdb68f7b87aaf16d182794bbc567ba57))
25
+ * refactor(core)!: pump the outbox in one cycle ([bf590fe](https://github.com/toa-io/toa/commit/bf590fe8ed59c701b5fd1a91ba4874685b79242f))
26
+ * refactor(atomicity)!: rename the connector and free it of the outbox ([21f1a41](https://github.com/toa-io/toa/commit/21f1a41aceafcfd35c7620014062fe46b54afa95))
27
+ * 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)
28
+
29
+
30
+ ### Features
31
+
32
+ * **atomicity:** make the interval a setting, and stop repeating n-and-i ([5c3a6d1](https://github.com/toa-io/toa/commit/5c3a6d1533751a21b7eb7d9c737f1a270bf5a5ae))
33
+
34
+
35
+ ### BREAKING CHANGES
36
+
37
+ * `atomicity.redis` accepts a list again, of independent servers rather
38
+ than cluster nodes, and refuses an even number of them.
39
+
40
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
41
+ * `atomicity.redis` is a string. A list is refused at export.
42
+
43
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
44
+ * `context.stash.lock` is gone; lock through `context.atom` instead.
45
+ A stash pointer resolving to several addresses now uses the first.
46
+
47
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
48
+ * `Storage.outbox.pending` takes a fourth argument, the id to
49
+ continue from.
50
+
51
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
52
+ * `@toa.io/partitions.redis` is now `@toa.io/atomicity`, and its
53
+ `lanes(total)` is `slots(total)`. Redis is declared as `atomicity` in the context
54
+ rather than `outbox.redis`, and read from `TOA_ATOMICITY_REDIS`.
55
+ `TOA_OUTBOX_PARTITION_INTERVAL` is `TOA_ATOMICITY_INTERVAL`.
56
+
57
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
58
+ * `event.changeset` is removed; use `origin` and `state`. `State`
59
+ takes an `Outbox` in place of an `Emission`. `difference` is dropped from
60
+ `@toa.io/generic` along with its last caller.
61
+
62
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
63
+
64
+
65
+
66
+
67
+
6
68
  # [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
69
 
8
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/norm",
3
- "version": "1.0.0-alpha.270",
3
+ "version": "1.0.0-alpha.273",
4
4
  "description": "Toa declarations normalization and validation",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -20,10 +20,11 @@
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/generic": "1.0.0-alpha.254",
25
- "@toa.io/schemas": "1.0.0-alpha.270",
26
- "@toa.io/yaml": "1.0.0-alpha.254"
23
+ "@toa.io/core": "1.0.0-alpha.273",
24
+ "@toa.io/generic": "1.0.0-alpha.273",
25
+ "@toa.io/schemas": "1.0.0-alpha.273",
26
+ "fast-glob": "3.2.12",
27
+ "js-yaml": "4.3.1"
27
28
  },
28
- "gitHead": "94400bdd411b4c25074ffcf58184580ddd060268"
29
+ "gitHead": "ad3284850ece95ffd5325fd0995707fc144c9c3d"
29
30
  }
@@ -1,12 +1,9 @@
1
1
  'use strict'
2
2
 
3
- const { expand } = require('@toa.io/schemas')
4
-
5
3
  const { resolve } = require('../../shortcuts')
6
4
 
7
5
  function entity (manifest) {
8
6
  if (!('entity' in manifest)) return
9
- if ('schema' in manifest.entity) manifest.entity.schema = expand(manifest.entity.schema)
10
7
 
11
8
  manifest.entity.storage = resolve(manifest.entity.storage)
12
9
  }
@@ -1,14 +1,11 @@
1
1
  'use strict'
2
2
 
3
- const { expand } = require('@toa.io/schemas')
4
3
  const { resolve } = require('../../shortcuts')
5
4
 
6
5
  function operations (manifest) {
7
6
  if (manifest.operations === undefined) return
8
7
 
9
8
  for (const operation of Object.values(manifest.operations)) {
10
- if (operation.input !== undefined) operation.input = expand(operation.input)
11
- if (operation.output !== undefined) operation.output = expand(operation.output)
12
9
  if (operation.bridge !== undefined) operation.bridge = resolve(operation.bridge)
13
10
 
14
11
  if (operation.bindings !== undefined && operation.bindings !== null) {
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { directory: { find } } = require('@toa.io/filesystem')
3
+ const { find } = require('@toa.io/generic')
4
4
  const { resolve } = require('../shortcuts')
5
5
 
6
6
  const cache = {}
@@ -2,10 +2,11 @@
2
2
 
3
3
  const path = require('node:path')
4
4
 
5
- const { load } = require('@toa.io/yaml')
5
+ const { readFileSync } = require('node:fs')
6
+ const { load: parseYAML } = require('js-yaml')
6
7
  const schemas = require('@toa.io/schemas')
7
8
 
8
- const object = load.sync(path.resolve(__dirname, 'schema.yaml'))
9
+ const object = parseYAML(readFileSync(path.resolve(__dirname, 'schema.yaml'), 'utf8'))
9
10
  const schema = schemas.schema(object)
10
11
 
11
12
  const validate = (manifest) => {
@@ -65,6 +65,65 @@ properties:
65
65
  resources:
66
66
  $ref: '#/definitions/resources'
67
67
  required: [name, components]
68
+ atomicity:
69
+ description: >
70
+ Where the replicas of a component decide together: who owns what, what they have spent
71
+ between them, and which of them holds a name while it works. Absent or unreachable,
72
+ nothing is owned and whoever asked stands down; metering and locking are refused.
73
+ type: object
74
+ additionalProperties: false
75
+ properties:
76
+ redis:
77
+ description: >
78
+ One address, or an odd number of them — independent servers, not the nodes of a
79
+ cluster. A lock is taken on a quorum of them; the registry and the meter use the
80
+ first, because each of them counts on a single key.
81
+ oneOf:
82
+ - type: string
83
+ - type: array
84
+ minItems: 1
85
+ items:
86
+ type: string
87
+ interval:
88
+ description: >
89
+ How often a replica registers, in milliseconds. It has to comfortably exceed a round
90
+ trip to Redis and the jitter around it, and nothing is owned until two consecutive
91
+ intervals have agreed — so this is also how long a replica owns nothing after it
92
+ joins, restarts or stalls.
93
+ type: integer
94
+ minimum: 100
95
+ required: [redis]
96
+ outbox:
97
+ type: object
98
+ additionalProperties: false
99
+ properties:
100
+ interval:
101
+ description: >
102
+ The pump's cycle, in milliseconds. One cycle reads what is unpublished, publishes it
103
+ and marks it. In steady state it reads nothing, so this is budgeted for how fast a rare
104
+ failure heals rather than for how fast events flow.
105
+ type: integer
106
+ minimum: 100
107
+ batch:
108
+ description: >
109
+ How many rows one read brings back. A page that comes back full is read on from,
110
+ so this bounds a read rather than a cycle.
111
+ type: integer
112
+ minimum: 1
113
+ retention:
114
+ description: >
115
+ Seconds a published row is kept as a change log before it expires. A row that never
116
+ made it out has no expiry and is never reaped.
117
+ type: integer
118
+ minimum: 0
119
+ events:
120
+ description: >
121
+ Events of this context that are consumed outside it. An event is otherwise published only
122
+ where something in the context receives it.
123
+ type: array
124
+ items:
125
+ type: string
126
+ pattern: ^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*$
68
127
  mono:
69
128
  type: object
70
129
  additionalProperties: false
@@ -2,11 +2,12 @@
2
2
 
3
3
  const { resolve } = require('node:path')
4
4
 
5
- const { load } = require('@toa.io/yaml')
5
+ const { readFileSync } = require('node:fs')
6
+ const { load: parseYAML } = require('js-yaml')
6
7
  const schemas = require('@toa.io/schemas')
7
8
 
8
9
  const path = resolve(__dirname, 'schema.yaml')
9
- const object = load.sync(path)
10
+ const object = parseYAML(readFileSync(path, 'utf8'))
10
11
  const schema = schemas.schema(object)
11
12
 
12
13
  const validate = (context) => {
package/src/component.js CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  const { join } = require('node:path')
4
4
 
5
- const { load: yaml } = require('@toa.io/yaml')
6
- const { directory: { find } } = require('@toa.io/filesystem')
5
+ const { readFile } = require('node:fs/promises')
6
+ const jsyaml = require('js-yaml')
7
+ const { find } = require('@toa.io/generic')
7
8
  const { Locator } = require('@toa.io/core')
8
9
 
9
10
  const {
@@ -33,7 +34,7 @@ const load = async (path, base, proto = false) => {
33
34
  if (base !== undefined) path = find(path, base, MANIFEST)
34
35
 
35
36
  const file = join(path, MANIFEST)
36
- const manifest = await yaml(file) ?? {}
37
+ const manifest = await read(file) ?? {}
37
38
 
38
39
  manifest.path = path
39
40
 
@@ -57,3 +58,16 @@ const load = async (path, base, proto = false) => {
57
58
  const MANIFEST = 'manifest.toa.yaml'
58
59
 
59
60
  exports.component = component
61
+
62
+ /**
63
+ * Reads a YAML file, resolving anchors into distinct objects so that
64
+ * mutating one node cannot reach another.
65
+ *
66
+ * @param {string} path
67
+ * @return {Promise<object>}
68
+ */
69
+ async function read (path) {
70
+ const object = jsyaml.load(await readFile(path, 'utf8'))
71
+
72
+ return jsyaml.load(jsyaml.dump(object, { noRefs: true, lineWidth: -1 }))
73
+ }
package/src/context.js CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  const { resolve } = require('node:path')
4
4
  const { convolve } = require('@toa.io/generic')
5
- const { directory: { glob } } = require('@toa.io/filesystem')
6
- const { load } = require('@toa.io/yaml')
5
+ const glob = require('fast-glob')
6
+ const { readFile } = require('node:fs/promises')
7
+ const jsyaml = require('js-yaml')
7
8
 
8
9
  const { component } = require('./component')
9
10
 
@@ -18,7 +19,7 @@ const {
18
19
 
19
20
  const context = async (root, environment = process.env.TOA_ENV) => {
20
21
  const path = resolve(root, CONTEXT)
21
- const context = /** @type {toa.norm.Context} */ await load(path)
22
+ const context = /** @type {toa.norm.Context} */ await read(path)
22
23
 
23
24
  context.environment = environment
24
25
 
@@ -28,7 +29,7 @@ const context = async (root, environment = process.env.TOA_ENV) => {
28
29
 
29
30
  validate(context)
30
31
 
31
- const paths = await glob(resolve(root, COMPONENTS))
32
+ const paths = await glob(resolve(root, COMPONENTS), GLOB)
32
33
 
33
34
  context.components = await Promise.all(paths.map(component))
34
35
  context.dependencies = await dependencies(context)
@@ -42,4 +43,20 @@ const context = async (root, environment = process.env.TOA_ENV) => {
42
43
  const CONTEXT = 'context.toa.yaml'
43
44
  const COMPONENTS = 'components/*'
44
45
 
46
+
47
+ const GLOB = { onlyDirectories: true, absolute: true }
48
+
45
49
  exports.context = context
50
+
51
+ /**
52
+ * Reads a YAML file, resolving anchors into distinct objects so that
53
+ * mutating one node cannot reach another.
54
+ *
55
+ * @param {string} path
56
+ * @return {Promise<object>}
57
+ */
58
+ async function read (path) {
59
+ const object = jsyaml.load(await readFile(path, 'utf8'))
60
+
61
+ return jsyaml.load(jsyaml.dump(object, { noRefs: true, lineWidth: -1 }))
62
+ }
@@ -4,8 +4,9 @@ const source = {
4
4
  path: __dirname, // `version` hashes the component directory
5
5
  entity: {
6
6
  schema: {
7
+ type: 'object',
7
8
  properties: {
8
- foo: 'string'
9
+ foo: { type: 'string' }
9
10
  }
10
11
  },
11
12
  storage: 'mongodb'
@@ -16,26 +17,30 @@ const source = {
16
17
  bridge: 'node',
17
18
  bindings: ['amqp'],
18
19
  input: {
20
+ type: 'object',
19
21
  properties: {
20
- foo: 'integer',
22
+ foo: { type: 'integer' },
21
23
  bar: {
22
24
  type: 'array',
23
25
  items: {
26
+ type: 'object',
24
27
  properties: {
25
- baz: 'string'
28
+ baz: { type: 'string' }
26
29
  }
27
30
  }
28
31
  }
29
32
  }
30
33
  },
31
34
  output: {
35
+ type: 'object',
32
36
  properties: {
33
- foo: 'integer',
37
+ foo: { type: 'integer' },
34
38
  bar: {
35
39
  type: 'array',
36
40
  items: {
41
+ type: 'object',
37
42
  properties: {
38
- baz: 'string'
43
+ baz: { type: 'string' }
39
44
  }
40
45
  }
41
46
  }