@toa.io/atomicity 1.0.0-alpha.282 → 1.0.0-alpha.284
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 +15 -0
- package/package.json +3 -3
- package/readme.md +20 -0
- package/src/atom.js +42 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
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
|
+
|
|
6
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)
|
|
7
22
|
|
|
8
23
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/atomicity",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.284",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa Atomicity Connector",
|
|
6
6
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@sesamecare-oss/redlock": "1.4.0",
|
|
21
|
-
"@toa.io/core": "1.0.0-alpha.
|
|
21
|
+
"@toa.io/core": "1.0.0-alpha.284",
|
|
22
22
|
"ioredis": "6.0.0",
|
|
23
23
|
"n-and-i": "0.3.2",
|
|
24
24
|
"openspan": "1.0.0-alpha.277"
|
|
@@ -26,5 +26,5 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "006755556879e91e8d68fa86753aa3b475bdf4bf"
|
|
30
30
|
}
|
package/readme.md
CHANGED
|
@@ -18,6 +18,26 @@ Returns `null` while this replica owns nothing — at startup, during a rollout,
|
|
|
18
18
|
|
|
19
19
|
Uses [n-and-i](https://github.com/temich/nandi).
|
|
20
20
|
|
|
21
|
+
### Being told
|
|
22
|
+
|
|
23
|
+
`slots` answers whoever asks. A consumer that asks rarely — once an hour, say — samples one
|
|
24
|
+
moment, and a rollout passing through that moment costs it the whole hour. `onassigned` tells it
|
|
25
|
+
instead.
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
const off = atom.onassigned((assignment) => { … }) // { i, n } — or null, owning nothing
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Called with the assignment this replica holds, and again whenever it changes: one arrived, was
|
|
32
|
+
lost, or the group resized. A change and not a heartbeat — a group that stays as it is never
|
|
33
|
+
calls back. It is called once as it is added, with the assignment as it stands, so a listener
|
|
34
|
+
arriving after the group settled is not left waiting for something that has already happened.
|
|
35
|
+
|
|
36
|
+
The answer removes the listener. A listener that raises ends the loop it was told through, and
|
|
37
|
+
that loop is what keeps this replica registered — so this replica owns nothing from that moment,
|
|
38
|
+
`slots` answers `null`, and it stands down. What to do about the exception is the runtime's;
|
|
39
|
+
nothing here catches it.
|
|
40
|
+
|
|
21
41
|
## Metering
|
|
22
42
|
|
|
23
43
|
Debt the group has run up under each key, in milliseconds. A call adds its own deltas and answers
|
package/src/atom.js
CHANGED
|
@@ -15,6 +15,9 @@ export class Atom extends Connector {
|
|
|
15
15
|
/** @type {{ i: number, n: number } | null} */
|
|
16
16
|
#assignment = null
|
|
17
17
|
|
|
18
|
+
/** @type {Set<(assignment: { i: number, n: number } | null) => void>} */
|
|
19
|
+
#listeners = new Set()
|
|
20
|
+
|
|
18
21
|
/** @type {AbortController} */
|
|
19
22
|
#abort
|
|
20
23
|
|
|
@@ -60,6 +63,29 @@ export class Atom extends Connector {
|
|
|
60
63
|
return owned
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Calls `listener` with the assignment this replica holds, and again whenever it changes —
|
|
68
|
+
* one arrived, was lost, or the group resized. Answers with what removes the listener again.
|
|
69
|
+
*
|
|
70
|
+
* A change and not a heartbeat: a group that stays as it is never calls back. It is called
|
|
71
|
+
* once as it is added, with the claim as it stands, so that a listener arriving after the
|
|
72
|
+
* group settled is not left waiting for something that has already happened.
|
|
73
|
+
*
|
|
74
|
+
* This is what `slots` cannot say. Reading answers a consumer that asks often, where a stale
|
|
75
|
+
* answer costs one cycle; a consumer that asks rarely needs to be told, or it samples the
|
|
76
|
+
* one moment a rollout was passing through and stands down for a whole cycle.
|
|
77
|
+
*
|
|
78
|
+
* @param listener {(assignment: { i: number, n: number } | null) => void}
|
|
79
|
+
* @returns {() => void}
|
|
80
|
+
*/
|
|
81
|
+
onassigned (listener) {
|
|
82
|
+
this.#listeners.add(listener)
|
|
83
|
+
|
|
84
|
+
listener(this.#assignment)
|
|
85
|
+
|
|
86
|
+
return () => this.#listeners.delete(listener)
|
|
87
|
+
}
|
|
88
|
+
|
|
63
89
|
/**
|
|
64
90
|
* Debt the group has run up under each key, in milliseconds: every call adds its own deltas
|
|
65
91
|
* and reads back where the group stands, so a replica reports only what it has spent and
|
|
@@ -146,9 +172,22 @@ export class Atom extends Connector {
|
|
|
146
172
|
console: this.#console
|
|
147
173
|
})
|
|
148
174
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
175
|
+
try {
|
|
176
|
+
// the loop yields when ownership changes, which is exactly when work has to be handed over
|
|
177
|
+
for await (const { i, n } of loop) {
|
|
178
|
+
this.#assignment = i === null ? null : { i, n }
|
|
179
|
+
|
|
180
|
+
for (const listener of this.#listeners) listener(this.#assignment)
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
/*
|
|
184
|
+
* The loop is what keeps this replica registered, so once it has ended — because it was
|
|
185
|
+
* aborted, or because a listener raised through it — this replica owns nothing. Left as
|
|
186
|
+
* it was, the last assignment would answer `slots` for as long as the process ran, and
|
|
187
|
+
* a claim nothing supports is what the whole of this exists to prevent.
|
|
188
|
+
*/
|
|
189
|
+
this.#assignment = null
|
|
190
|
+
}
|
|
152
191
|
}
|
|
153
192
|
}
|
|
154
193
|
|