@toa.io/norm 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 ADDED
@@ -0,0 +1,143 @@
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
+ * **atomicity:** honour a cluster, and let it be unreachable ([c475d66](https://github.com/toa-io/toa/commit/c475d663ddf7329d86da517ccceee4cb03280e95))
12
+
13
+
14
+ * feat(atomicity)!: take a quorum of independent servers ([862562f](https://github.com/toa-io/toa/commit/862562f24d77ec00127dbbd88d43802b208c643b))
15
+ * refactor(atomicity)!: take one Redis, not a cluster ([b0a4770](https://github.com/toa-io/toa/commit/b0a4770dff5dfe8fee4b9ae637a4dd297454fcd4))
16
+ * refactor(atomicity)!: take the lock manager from the stash ([4fd91eb](https://github.com/toa-io/toa/commit/4fd91eb2fdb68f7b87aaf16d182794bbc567ba57))
17
+ * refactor(core)!: pump the outbox in one cycle ([bf590fe](https://github.com/toa-io/toa/commit/bf590fe8ed59c701b5fd1a91ba4874685b79242f))
18
+ * refactor(atomicity)!: rename the connector and free it of the outbox ([21f1a41](https://github.com/toa-io/toa/commit/21f1a41aceafcfd35c7620014062fe46b54afa95))
19
+ * 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)
20
+
21
+
22
+ ### Features
23
+
24
+ * **atomicity:** make the interval a setting, and stop repeating n-and-i ([5c3a6d1](https://github.com/toa-io/toa/commit/5c3a6d1533751a21b7eb7d9c737f1a270bf5a5ae))
25
+
26
+
27
+ ### BREAKING CHANGES
28
+
29
+ * `atomicity.redis` accepts a list again, of independent servers rather
30
+ than cluster nodes, and refuses an even number of them.
31
+
32
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
33
+ * `atomicity.redis` is a string. A list is refused at export.
34
+
35
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
36
+ * `context.stash.lock` is gone; lock through `context.atom` instead.
37
+ A stash pointer resolving to several addresses now uses the first.
38
+
39
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
40
+ * `Storage.outbox.pending` takes a fourth argument, the id to
41
+ continue from.
42
+
43
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
44
+ * `@toa.io/partitions.redis` is now `@toa.io/atomicity`, and its
45
+ `lanes(total)` is `slots(total)`. Redis is declared as `atomicity` in the context
46
+ rather than `outbox.redis`, and read from `TOA_ATOMICITY_REDIS`.
47
+ `TOA_OUTBOX_PARTITION_INTERVAL` is `TOA_ATOMICITY_INTERVAL`.
48
+
49
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
50
+ * `event.changeset` is removed; use `origin` and `state`. `State`
51
+ takes an `Outbox` in place of an `Emission`. `difference` is dropped from
52
+ `@toa.io/generic` along with its last caller.
53
+
54
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
55
+
56
+
57
+
58
+
59
+
60
+ # [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)
61
+
62
+
63
+ ### Bug Fixes
64
+
65
+ * **norm:** resolve annotations keyed by a dependency id ([e2860cd](https://github.com/toa-io/toa/commit/e2860cd2cc93dd3a135c092db1d943f80648ee56))
66
+ * **norm:** return the hash after hashing a file ([35058bd](https://github.com/toa-io/toa/commit/35058bdaae19f367197ea5b800742349e507ab11))
67
+
68
+
69
+ ### Performance Improvements
70
+
71
+ * **boot:** stop paying for what a composition does not need to start ([1619c27](https://github.com/toa-io/toa/commit/1619c2743072f4706939ce72f3074e615d26a91e))
72
+
73
+
74
+
75
+
76
+
77
+ # [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)
78
+
79
+ **Note:** Version bump only for package @toa.io/norm
80
+
81
+
82
+
83
+
84
+
85
+ # [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)
86
+
87
+ **Note:** Version bump only for package @toa.io/norm
88
+
89
+
90
+
91
+
92
+
93
+ # [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)
94
+
95
+ **Note:** Version bump only for package @toa.io/norm
96
+
97
+
98
+
99
+
100
+
101
+ # [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)
102
+
103
+ **Note:** Version bump only for package @toa.io/norm
104
+
105
+
106
+
107
+
108
+
109
+ # [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)
110
+
111
+
112
+ ### Features
113
+
114
+ * **introspection:** add the introspection extension ([a07ed86](https://github.com/toa-io/toa/commit/a07ed8651404258d544f3b6d8236e58cf4da09ac))
115
+ * **operations:** let a service claim a path prefix on the host ([02677d1](https://github.com/toa-io/toa/commit/02677d17fcd4222b07c3be458d93bcc496a016a8))
116
+
117
+
118
+
119
+
120
+
121
+ # [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)
122
+
123
+ **Note:** Version bump only for package @toa.io/norm
124
+
125
+
126
+
127
+
128
+
129
+ # [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)
130
+
131
+ **Note:** Version bump only for package @toa.io/norm
132
+
133
+
134
+
135
+
136
+
137
+ # [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)
138
+
139
+
140
+ ### Features
141
+
142
+ * add toa mono and discover components at components/* ([787a111](https://github.com/toa-io/toa/commit/787a111c8f83e56ebc734801c97106c585d034ea))
143
+ * deploy a single mono image with toa deploy --mono ([a7f14f9](https://github.com/toa-io/toa/commit/a7f14f9877a280e9c74d16c195028b35d74fb15a))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/norm",
3
- "version": "1.0.0-alpha.27",
3
+ "version": "1.0.0-alpha.272",
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,11 +20,10 @@
20
20
  "test": "echo \"Error: run tests from root\" && exit 1"
21
21
  },
22
22
  "dependencies": {
23
- "@toa.io/core": "1.0.0-alpha.27",
24
- "@toa.io/generic": "1.0.0-alpha.27",
25
- "@toa.io/schema": "1.0.0-alpha.27",
26
- "@toa.io/schemas": "1.0.0-alpha.27",
27
- "@toa.io/yaml": "1.0.0-alpha.27"
23
+ "@toa.io/core": "1.0.0-alpha.272",
24
+ "@toa.io/generic": "1.0.0-alpha.272",
25
+ "@toa.io/schemas": "1.0.0-alpha.272",
26
+ "@toa.io/yaml": "1.0.0-alpha.272"
28
27
  },
29
- "gitHead": "9bc4bdb919688c5272791020746f70d51a520750"
28
+ "gitHead": "1a451755445935c04cf5b373bcc73942cac0bb10"
30
29
  }
@@ -10,11 +10,11 @@ function extensions (manifest) {
10
10
  const SHORTCUTS = {
11
11
  exposition: '@toa.io/extensions.exposition',
12
12
  realtime: '@toa.io/extensions.realtime',
13
- origins: '@toa.io/extensions.origins',
14
13
  configuration: '@toa.io/extensions.configuration',
15
14
  state: '@toa.io/extensions.state',
16
15
  stash: '@toa.io/extensions.stash',
17
- storages: '@toa.io/extensions.storages'
16
+ storages: '@toa.io/extensions.storages',
17
+ introspection: '@toa.io/extensions.introspection'
18
18
  }
19
19
 
20
20
  exports.extensions = extensions
@@ -3,43 +3,59 @@
3
3
  const { join } = require('node:path')
4
4
  const { createHash } = require('node:crypto')
5
5
  const fs = require('node:fs/promises')
6
- const { createReadStream } = require('node:fs')
7
- const { once } = require('node:events')
8
6
 
9
7
  async function version (manifest) {
10
8
  manifest.version ??= await hash(manifest.path)
11
9
  }
12
10
 
11
+ /**
12
+ * Identifies a build of a component: it becomes the tag of its image, so it must be the
13
+ * same for the same sources and different for different ones — on any machine.
14
+ *
15
+ * What the image excludes cannot change it: `node_modules` is in the `.dockerignore` the
16
+ * build writes (see `operations/src/deployment/images/image.js`), so hashing it would only
17
+ * retag an identical image after a local install.
18
+ */
13
19
  async function hash (path) {
14
- const hash = await hashd(path)
20
+ const files = (await list(path)).sort()
21
+ const digests = await Promise.all(files.map((file) => digest(join(path, file))))
22
+ const total = createHash('sha256')
15
23
 
16
- return hash.digest('hex').slice(0, 8)
24
+ // the path is part of it: moving a file changes the build even if no content did
25
+ for (let i = 0; i < files.length; i++)
26
+ total.update(files[i]).update(digests[i])
27
+
28
+ return total.digest('hex').slice(0, 8)
17
29
  }
18
30
 
19
31
  /**
20
- * @param {string} path
21
- * @param {import('node:crypto').Hash} hash
32
+ * @param {string} root
33
+ * @param {string} [path] relative
34
+ * @param {string[]} [acc]
35
+ * @returns {Promise<string[]>} paths relative to `root`
22
36
  */
23
- async function hashd (path, hash = createHash('sha256')) {
24
- const stat = await fs.stat(path)
37
+ async function list (root, path = '', acc = []) {
38
+ const entries = await fs.readdir(join(root, path), { withFileTypes: true })
25
39
 
26
- if (stat.isFile()) {
27
- const stream = createReadStream(path)
40
+ for (const entry of entries) {
41
+ if (EXCLUDED.has(entry.name))
42
+ continue
28
43
 
29
- stream.pipe(hash, { end: false })
44
+ const relative = path === '' ? entry.name : `${path}/${entry.name}`
30
45
 
31
- return await once(stream, 'end')
46
+ if (entry.isDirectory())
47
+ await list(root, relative, acc)
48
+ else if (entry.isFile())
49
+ acc.push(relative)
32
50
  }
33
51
 
34
- if (stat.isDirectory()) {
35
- const entries = await fs.opendir(path)
36
-
37
- for await (const entry of entries) {
38
- await hashd(join(path, entry.name), hash)
39
- }
52
+ return acc
53
+ }
40
54
 
41
- return hash
42
- }
55
+ async function digest (path) {
56
+ return createHash('sha256').update(await fs.readFile(path)).digest()
43
57
  }
44
58
 
59
+ const EXCLUDED = new Set(['node_modules', '.git'])
60
+
45
61
  exports.version = version
@@ -4,7 +4,7 @@ const operations = (component) => {
4
4
  if (component.operations === undefined) return
5
5
 
6
6
  for (const [endpoint, operation] of Object.entries(component.operations)) {
7
- if (operation.type === 'computation' || operation.type === 'effect')
7
+ if (operation.scope === 'none')
8
8
  operation.query = false
9
9
 
10
10
  if (operation.bindings === undefined) operation.bindings = component.bindings
@@ -42,6 +42,11 @@ const collapse = (manifest, prototype) => {
42
42
  delete prototype.entity.schema.properties.id
43
43
  }
44
44
 
45
+ if (prototype.events !== undefined && manifest.events !== undefined)
46
+ for (const event of Object.keys(prototype.events))
47
+ if (event in manifest.events)
48
+ delete prototype.events[event]
49
+
45
50
  merge(manifest, { entity, events, extensions })
46
51
  }
47
52
 
@@ -1,11 +1,13 @@
1
1
  'use strict'
2
2
 
3
3
  const { hash } = require('@toa.io/generic')
4
- const assert = require('node:assert')
5
4
 
6
5
  // these defaults are required before validation
7
- const defaults = (manifest) => {
8
- if (manifest.name === undefined) manifest.name = protoName(manifest)
6
+ const defaults = (manifest, proto) => {
7
+ if (manifest.name === undefined)
8
+ if (proto) manifest.name = protoName(manifest)
9
+ else nameAfterDir(manifest)
10
+
9
11
  if (manifest.bindings === undefined) manifest.bindings = ['@toa.io/bindings.amqp']
10
12
  if (manifest.bridge === undefined) manifest.bridge = '@toa.io/bridges.node'
11
13
 
@@ -24,4 +26,13 @@ function protoName (manifest) {
24
26
  return 'proto' + hash(manifest.path)
25
27
  }
26
28
 
29
+ function nameAfterDir (manifest) {
30
+ const parts = manifest.path.split('/')
31
+ const dirname = parts[parts.length - 1]
32
+ const [name, namespace] = dirname.split('.').reverse()
33
+
34
+ manifest.name = name
35
+ manifest.namespace = namespace
36
+ }
37
+
27
38
  exports.defaults = defaults
@@ -25,7 +25,12 @@ const createResolver = (properties) => (property) => {
25
25
  function operations (manifest, resolver) {
26
26
  for (const operation of Object.values(manifest.operations)) {
27
27
  if (operation.input !== undefined) operation.input = schema(operation.input, resolver)
28
- if (operation.output !== undefined) operation.output = schema(operation.output, resolver)
28
+
29
+ if (operation.output !== undefined)
30
+ if (Array.isArray(operation.output) && operation.output.length === 1)
31
+ operation.output = [schema(operation.output[0], resolver)]
32
+ else
33
+ operation.output = schema(operation.output, resolver)
29
34
  }
30
35
 
31
36
  // forwarding
@@ -3,8 +3,10 @@
3
3
  const { directory: { find } } = require('@toa.io/filesystem')
4
4
  const { resolve } = require('../shortcuts')
5
5
 
6
+ const cache = {}
7
+
6
8
  const extensions = (manifest) => {
7
- if (manifest.extensions === undefined) return
9
+ manifest.extensions = Object.assign({}, PREDEFINED, manifest.extensions)
8
10
 
9
11
  const extensions = manifest.extensions
10
12
 
@@ -14,7 +16,8 @@ const extensions = (manifest) => {
14
16
  // relative path
15
17
  if (key[0] === '.') key = find(key, manifest.path)
16
18
 
17
- const extension = require(key)
19
+ cache[key] ??= require(key)
20
+ const extension = cache[key]
18
21
 
19
22
  if (extension.manifest !== undefined) {
20
23
  declaration = extension.manifest(declaration, manifest)
@@ -29,4 +32,10 @@ const extensions = (manifest) => {
29
32
  }
30
33
  }
31
34
 
35
+ const PREDEFINED = {
36
+ '@toa.io/extensions.telemetry': null,
37
+ '@toa.io/extensions.fetch': null,
38
+ '@toa.io/extensions.introspection': null
39
+ }
40
+
32
41
  exports.extensions = extensions
@@ -6,7 +6,8 @@ const bridge = async (root, manifest) => {
6
6
  await Promise.all([
7
7
  define(root, manifest, 'operations'),
8
8
  define(root, manifest, 'events'),
9
- define(root, manifest, 'receivers')
9
+ define(root, manifest, 'receivers'),
10
+ define(root, manifest, 'guards')
10
11
  ])
11
12
  }
12
13
 
@@ -19,7 +20,7 @@ const define = async (root, manifest, property) => {
19
20
  if (item.bridge === undefined || item.bridge === manifest.bridge) continue // default bridge later
20
21
 
21
22
  const bridge = item.bridge || manifest.bridge
22
- const { define } = require(bridge)
23
+ const define = req(bridge).define
23
24
  const definition = await define[singular](root, endpoint)
24
25
 
25
26
  merge(item, definition)
@@ -44,14 +45,27 @@ const define = async (root, manifest, property) => {
44
45
 
45
46
  const declared = manifest[property][endpoint]
46
47
 
47
- if (declared === undefined) manifest[property][endpoint] = item
48
- else merge(declared, item)
48
+ try {
49
+ if (declared === undefined) manifest[property][endpoint] = item
50
+ else merge(declared, item)
51
+ } catch (error) {
52
+ console.error(`Error merging ${singular} '${endpoint}' in ${root}`)
53
+ throw error
54
+ }
49
55
  }
50
56
  }
51
57
  }
52
58
 
59
+ const cache = {}
60
+
61
+ function req(mod) {
62
+ cache[mod] ??= require(mod)
63
+
64
+ return cache[mod]
65
+ }
66
+
53
67
  const scan = async (bridge, root, property) => {
54
- const { define } = require(bridge)
68
+ const define = req(bridge).define
55
69
 
56
70
  if (property in define)
57
71
  return define[property](root)