@toa.io/atomicity 1.0.0-alpha.284 → 1.0.0-alpha.287

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,94 +3,6 @@
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.284](https://github.com/toa-io/toa/compare/v1.0.0-alpha.283...v1.0.0-alpha.284) (2026-09-05)
7
-
8
- ### Features
9
-
10
- * **atomicity:** a replica can be told what it owns, not only ask ([f7b56b9](https://github.com/toa-io/toa/commit/f7b56b99cc43313f3f855df658a80fdc0659689e))
11
-
12
-
13
- # [1.0.0-alpha.283](https://github.com/toa-io/toa/compare/v1.0.0-alpha.282...v1.0.0-alpha.283) (2026-09-04)
14
-
15
- **Note:** Version bump only for package @toa.io/atomicity
16
-
17
-
18
-
19
-
20
-
21
- # [1.0.0-alpha.282](https://github.com/toa-io/toa/compare/v1.0.0-alpha.281...v1.0.0-alpha.282) (2026-09-03)
22
-
23
- ### Bug Fixes
24
-
25
- * **atomicity:** nest every key under atom:<group>:<kind> ([348f39f](https://github.com/toa-io/toa/commit/348f39f72d805a24dd4fdbaa83aede47fe95b894))
26
-
27
-
28
- # [1.0.0-alpha.278](https://github.com/toa-io/toa/compare/v1.0.0-alpha.277...v1.0.0-alpha.278) (2026-09-03)
29
-
30
- **Note:** Version bump only for package @toa.io/atomicity
31
-
32
-
33
-
34
-
35
-
36
- # [1.0.0-alpha.277](https://github.com/toa-io/toa/compare/v1.0.0-alpha.276...v1.0.0-alpha.277) (2026-09-03)
37
-
38
- **Note:** Version bump only for package @toa.io/atomicity
39
-
40
-
41
-
42
-
43
-
44
- # [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)
6
+ # [1.0.0-alpha.287](https://github.com/toa-io/toa/compare/v1.0.0-alpha.286...v1.0.0-alpha.287) (2026-09-06)
45
7
 
46
8
  **Note:** Version bump only for package @toa.io/atomicity
47
-
48
-
49
-
50
-
51
-
52
- # [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)
53
-
54
-
55
- ### Bug Fixes
56
-
57
- * **atomicity:** honour a cluster, and let it be unreachable ([c475d66](https://github.com/toa-io/toa/commit/c475d663ddf7329d86da517ccceee4cb03280e95))
58
- * **atomicity:** stop swallowing faults from the discovery loop ([449f79a](https://github.com/toa-io/toa/commit/449f79a011611f2e14ad5a325a9f7f7161433060))
59
-
60
-
61
- * feat(atomicity)!: take a quorum of independent servers ([862562f](https://github.com/toa-io/toa/commit/862562f24d77ec00127dbbd88d43802b208c643b))
62
- * refactor(atomicity)!: take one Redis, not a cluster ([b0a4770](https://github.com/toa-io/toa/commit/b0a4770dff5dfe8fee4b9ae637a4dd297454fcd4))
63
- * refactor(atomicity)!: take the lock manager from the stash ([4fd91eb](https://github.com/toa-io/toa/commit/4fd91eb2fdb68f7b87aaf16d182794bbc567ba57))
64
- * refactor(atomicity)!: make the connector a family, not a partitioner ([06fd63a](https://github.com/toa-io/toa/commit/06fd63ad6296b724ef83afccf4e4e25d0bcaf080))
65
- * refactor(atomicity)!: rename the connector and free it of the outbox ([21f1a41](https://github.com/toa-io/toa/commit/21f1a41aceafcfd35c7620014062fe46b54afa95))
66
-
67
-
68
- ### Features
69
-
70
- * **atomicity:** make the interval a setting, and stop repeating n-and-i ([5c3a6d1](https://github.com/toa-io/toa/commit/5c3a6d1533751a21b7eb7d9c737f1a270bf5a5ae))
71
- * **atomicity:** meter what the group has spent ([bb1118a](https://github.com/toa-io/toa/commit/bb1118aa60bfeb43d3212f2495b7797445f003c3))
72
-
73
-
74
- ### BREAKING CHANGES
75
-
76
- * `atomicity.redis` accepts a list again, of independent servers rather
77
- than cluster nodes, and refuses an even number of them.
78
-
79
- Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
80
- * `atomicity.redis` is a string. A list is refused at export.
81
-
82
- Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
83
- * `context.stash.lock` is gone; lock through `context.atom` instead.
84
- A stash pointer resolving to several addresses now uses the first.
85
-
86
- Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87
- * `Factory.partition(group)` is `Factory.atom(group)`, and the
88
- `Partition` it returned is an `Atom`. `slots(total)` is unchanged.
89
-
90
- Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
91
- * `@toa.io/partitions.redis` is now `@toa.io/atomicity`, and its
92
- `lanes(total)` is `slots(total)`. Redis is declared as `atomicity` in the context
93
- rather than `outbox.redis`, and read from `TOA_ATOMICITY_REDIS`.
94
- `TOA_OUTBOX_PARTITION_INTERVAL` is `TOA_ATOMICITY_INTERVAL`.
95
-
96
- Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/atomicity",
3
- "version": "1.0.0-alpha.284",
3
+ "version": "1.0.0-alpha.287",
4
4
  "type": "module",
5
5
  "description": "Toa Atomicity Connector",
6
6
  "author": "temich <tema.gurtovoy@gmail.com>",
@@ -18,13 +18,13 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@sesamecare-oss/redlock": "1.4.0",
21
- "@toa.io/core": "1.0.0-alpha.284",
21
+ "@toa.io/core": "1.0.0-alpha.287",
22
22
  "ioredis": "6.0.0",
23
23
  "n-and-i": "0.3.2",
24
- "openspan": "1.0.0-alpha.277"
24
+ "openspan": "1.0.0-alpha.286"
25
25
  },
26
26
  "scripts": {
27
27
  "test": "echo \"Error: run tests from root\" && exit 1"
28
28
  },
29
- "gitHead": "006755556879e91e8d68fa86753aa3b475bdf4bf"
29
+ "gitHead": "5c7944a70c16065ab6372072385cc1f02f5db77c"
30
30
  }
package/readme.md CHANGED
@@ -62,7 +62,7 @@ Uses [redlock](https://github.com/sesamecare/redlock). The key is written to eve
62
62
  majority holding it is the lock, so a minority can be lost or failed over without invalidating one.
63
63
  Against a single address there is no majority to lose: a restart that drops the key, or a failover
64
64
  to a replica that has not received it, can leave two holders. Entity writes do not rest on either —
65
- they have `_version`.
65
+ they have `VERSION`.
66
66
 
67
67
  The routine is given an `AbortSignal` and a context. Extension can fail while the routine runs, and
68
68
  the signal is how it says so:
@@ -92,8 +92,8 @@ Requires Redis.
92
92
  ```yaml
93
93
  # context.toa.yaml
94
94
  atomicity:
95
- redis: redis://redis.example.com # one address, or an odd number of them
96
- interval: 5000 # how often a replica registers, milliseconds
95
+ redis: redis://redis.example.com # one address, or an odd number of them
96
+ interval: 5000 # how often a replica registers, milliseconds
97
97
  ```
98
98
 
99
99
  Several addresses are **independent servers**, not the nodes of a cluster. The lock is taken on a
package/src/atom.js CHANGED
@@ -5,7 +5,7 @@ import { Connector } from '@toa.io/core'
5
5
  * What one group of replicas decides together, in one place: which of them owns what, what they
6
6
  * have spent between them, and which of them holds a name while it works.
7
7
  *
8
- * @implements {toa.core.atomicity.Atom}
8
+ * @implements {import('@toa.io/core/types').atomicity.Atom}
9
9
  */
10
10
  export class Atom extends Connector {
11
11
  #connection
@@ -27,7 +27,7 @@ export class Atom extends Connector {
27
27
  /** the loop's own logger, carrying the group every line belongs to */
28
28
  #console
29
29
 
30
- constructor (connection, name, interval) {
30
+ constructor(connection, name, interval) {
31
31
  super()
32
32
 
33
33
  this.#connection = connection
@@ -50,7 +50,7 @@ export class Atom extends Connector {
50
50
  * @param total {number}
51
51
  * @returns {number[] | null} null while this replica owns nothing
52
52
  */
53
- slots (total) {
53
+ slots(total) {
54
54
  const assignment = this.#assignment
55
55
 
56
56
  if (assignment === null) return null
@@ -78,7 +78,7 @@ export class Atom extends Connector {
78
78
  * @param listener {(assignment: { i: number, n: number } | null) => void}
79
79
  * @returns {() => void}
80
80
  */
81
- onassigned (listener) {
81
+ onassigned(listener) {
82
82
  this.#listeners.add(listener)
83
83
 
84
84
  listener(this.#assignment)
@@ -95,7 +95,7 @@ export class Atom extends Connector {
95
95
  * @param {number[]} deltas
96
96
  * @returns {Promise<number[]>}
97
97
  */
98
- async meter (keys, deltas) {
98
+ async meter(keys, deltas) {
99
99
  const meter = this.#connection.meter
100
100
 
101
101
  if (meter === undefined)
@@ -112,7 +112,7 @@ export class Atom extends Connector {
112
112
  * @param {() => Promise<any>} routine
113
113
  * @returns {Promise<any>}
114
114
  */
115
- async lock (keys, routine) {
115
+ async lock(keys, routine) {
116
116
  const redlock = this.#connection.redlock
117
117
 
118
118
  if (redlock === undefined)
@@ -128,13 +128,13 @@ export class Atom extends Connector {
128
128
  *
129
129
  * @private
130
130
  */
131
- #keys (kind, keys) {
131
+ #keys(kind, keys) {
132
132
  if (typeof keys === 'string') keys = [keys]
133
133
 
134
134
  return keys.map((key) => `${ATOM}:${this.#name}:${kind}:${key}`)
135
135
  }
136
136
 
137
- async open () {
137
+ async open() {
138
138
  const redis = this.#connection.redis
139
139
 
140
140
  // without a Redis nothing can be owned exclusively, so nothing is claimed at all
@@ -144,7 +144,7 @@ export class Atom extends Connector {
144
144
  this.#discovering = this.#discover(redis)
145
145
  }
146
146
 
147
- async close () {
147
+ async close() {
148
148
  this.#assignment = null
149
149
 
150
150
  this.#abort?.abort()
@@ -160,7 +160,7 @@ export class Atom extends Connector {
160
160
  *
161
161
  * @private
162
162
  */
163
- async #discover (redis) {
163
+ async #discover(redis) {
164
164
  const { discover } = await import('n-and-i')
165
165
 
166
166
  const loop = discover({
@@ -191,7 +191,7 @@ export class Atom extends Connector {
191
191
  }
192
192
  }
193
193
 
194
- function override () {
194
+ function override() {
195
195
  const value = Number(process.env.TOA_ATOMICITY_INTERVAL)
196
196
 
197
197
  return Number.isNaN(value) || value <= 0 ? undefined : value
package/src/connection.js CHANGED
@@ -26,12 +26,14 @@ export class Connection extends Connector {
26
26
  /** @type {import('ioredis').Redis[]} */
27
27
  #clients = []
28
28
 
29
- async open () {
29
+ async open() {
30
30
  const urls = resolve()
31
31
 
32
32
  if (urls.length === 0) {
33
- console.warn('Atomicity is not configured, so nothing is owned and nothing is metered. ' +
34
- 'Set TOA_ATOMICITY_REDIS.')
33
+ console.warn(
34
+ 'Atomicity is not configured, so nothing is owned and nothing is metered. ' +
35
+ 'Set TOA_ATOMICITY_REDIS.'
36
+ )
35
37
 
36
38
  return
37
39
  }
@@ -56,8 +58,11 @@ export class Connection extends Connector {
56
58
  for (const client of this.#clients)
57
59
  // ioredis leaves `message` empty on a refused connection, where the code is the whole story
58
60
  client.on('error', (error) =>
59
- console.warn('Atomicity is unreachable, so nothing is owned',
60
- { host: client.options.host, error: error.code ?? error.message }))
61
+ console.warn('Atomicity is unreachable, so nothing is owned', {
62
+ host: client.options.host,
63
+ error: error.code ?? error.message
64
+ })
65
+ )
61
66
 
62
67
  this.redis = this.#clients[0]
63
68
 
@@ -75,7 +80,7 @@ export class Connection extends Connector {
75
80
  console.info('Atomicity connecting to redis', { nodes: this.#clients.length })
76
81
  }
77
82
 
78
- async close () {
83
+ async close() {
79
84
  for (const client of this.#clients) client.disconnect()
80
85
 
81
86
  // a closed connection holds nothing: it is opened again with whatever is configured then,
@@ -89,7 +94,7 @@ export class Connection extends Connector {
89
94
 
90
95
  const OPTIONS = { enableReadyCheck: true }
91
96
 
92
- function resolve () {
97
+ function resolve() {
93
98
  const value = process.env[VARIABLE]
94
99
 
95
100
  // an empty value is atomicity turned off, where an absent one in development is the
@@ -108,4 +113,8 @@ export const connection = () => (instance ??= new Connection())
108
113
  export const reset = () => (instance = undefined)
109
114
 
110
115
  export const VARIABLE = 'TOA_ATOMICITY_REDIS'
111
- const DEV = 'redis://localhost'
116
+ /**
117
+ * Toa's own development stack is not on the conventional ports: the applications built on
118
+ * Toa are, and they share the machine. See CONTRIBUTING.md.
119
+ */
120
+ const DEV = 'redis://localhost:31040'
package/src/factory.js CHANGED
@@ -9,7 +9,7 @@ export class Factory {
9
9
  * @param {string} group
10
10
  * @param {object} [options]
11
11
  */
12
- atom (group, options = {}) {
12
+ atom(group, options = {}) {
13
13
  return atom(group, options)
14
14
  }
15
15
  }
@@ -22,7 +22,7 @@ const atoms = new Map()
22
22
  * time in a group that has one replica, and every replica of that group would then be told it
23
23
  * owns half of what it does.
24
24
  */
25
- function atom (group, options) {
25
+ function atom(group, options) {
26
26
  let atom = atoms.get(group)
27
27
 
28
28
  if (atom === undefined) {
package/src/meter.js CHANGED
@@ -15,7 +15,7 @@
15
15
  export class Meter {
16
16
  #redis
17
17
 
18
- constructor (redis) {
18
+ constructor(redis) {
19
19
  this.#redis = redis
20
20
 
21
21
  redis.defineCommand(COMMAND, { lua: SCRIPT })
@@ -28,7 +28,7 @@ export class Meter {
28
28
  * @param {number[]} deltas
29
29
  * @returns {Promise<number[]>}
30
30
  */
31
- async meter (keys, deltas) {
31
+ async meter(keys, deltas) {
32
32
  if (keys.length <= CHUNK) return this.#call(keys, deltas)
33
33
 
34
34
  // arguments are spread into the call, so a batch is split rather than risking the
@@ -36,13 +36,15 @@ export class Meter {
36
36
  const debts = []
37
37
 
38
38
  for (let i = 0; i < keys.length; i += CHUNK)
39
- debts.push(...await this.#call(keys.slice(i, i + CHUNK), deltas.slice(i, i + CHUNK)))
39
+ debts.push(
40
+ ...(await this.#call(keys.slice(i, i + CHUNK), deltas.slice(i, i + CHUNK)))
41
+ )
40
42
 
41
43
  return debts
42
44
  }
43
45
 
44
46
  /** @private */
45
- async #call (keys, deltas) {
47
+ async #call(keys, deltas) {
46
48
  // `defineCommand` extends the client at runtime
47
49
  return this.#redis[COMMAND](keys.length, ...keys, ...deltas)
48
50
  }