@zakkster/lite-pick 0.1.0 → 0.2.0
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 +34 -0
- package/Pick.d.ts +22 -3
- package/Pick.js +101 -5
- package/README.md +24 -2
- package/llms.txt +18 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@ All notable changes to `@zakkster/lite-pick` are documented here. The format fol
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-09-23
|
|
8
|
+
|
|
9
|
+
M2: SmoothWRR, the weighted default (ROADMAP.md M2).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `SmoothWRRBalancer extends BalancerBase` -- nginx-style smooth weighted round-robin
|
|
14
|
+
(`current += weight; pick max; current -= total`). Distributes picks by caller-configured
|
|
15
|
+
integer weights, interleaved SMOOTHLY (weights [5,1,1] -> A,A,B,A,C,A,A), not in the
|
|
16
|
+
bursts of naive weight-expansion WRR. Owns its Float64Array smoothing accumulators; the
|
|
17
|
+
sole writer of the weights via the cold `setWeight(i, w)`. O(cap) per pick, 0 B/op. Fails
|
|
18
|
+
closed (`PICK_NONE`) when the eligible-weight sum is 0 (all down, or all eligible weights 0).
|
|
19
|
+
- `setWeight(i, w)` (cold) -- reconfigure a weight, keeping the eligible-weight total exact.
|
|
20
|
+
`setEligible` is overridden to maintain the total and reset the toggled node's accumulator
|
|
21
|
+
(no stale credit across an eligibility epoch).
|
|
22
|
+
- `test/SmoothWRR.test.js` -- 12 tests: the documented [5,1,1] sequence, exact fairness over
|
|
23
|
+
k cycles, smoothness (max-run strictly below the bursty foil), skips-down / redistribution,
|
|
24
|
+
fail-closed (all down AND all-zero-weight), setWeight/setEligible invariants + accumulator
|
|
25
|
+
reset, uint32 validation, and a never-returns-a-down-index proof under 200k churned picks.
|
|
26
|
+
- Gates extended for SmoothWRR: torture (retention + 0 B/op `pick()`), PerfGate (`zgcSuite`
|
|
27
|
+
scenario at a realistic 256-endpoint pool -- SmoothWRR is O(cap) -- with a `grows` counter
|
|
28
|
+
over all three backing arrays, plus a `mustFail` teeth-check), witness (a per-strategy
|
|
29
|
+
complexity flag: `linear` asserts flat WORK RATE `ops/ms * n`), balance (exact weighted
|
|
30
|
+
fairness + max-run < bursty foil).
|
|
31
|
+
- `benchmark/Matrix.mjs` -- SmoothWRR subject + the naive expand-WRR bursty foil.
|
|
32
|
+
- `decisions/0004-smoothwrr-weight-ownership.md` -- weight ownership (cold setWeight),
|
|
33
|
+
Float64 accumulators, and the epoch-reset-on-eligibility-transition enrichment.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Version 0.1.0 -> 0.2.0 across `package.json`, `Pick.js` `VERSION`, and `llms.txt`.
|
|
38
|
+
- Corrected the SmoothWRR complexity: O(cap) per pick, not "O(1) amortized" (ROADMAP).
|
|
39
|
+
|
|
7
40
|
## [0.1.0] - 2026-09-23
|
|
8
41
|
|
|
9
42
|
M1: the first strategy, RoundRobin (ROADMAP.md M1). The M0 harness stubs become real,
|
|
@@ -72,5 +105,6 @@ ownership boundary in place before any `pick()` is written.
|
|
|
72
105
|
- Next: **M1 RoundRobin** (0.1.0) -- the first strategy, landing the throughput witness
|
|
73
106
|
and the balance gate.
|
|
74
107
|
|
|
108
|
+
[0.2.0]: https://github.com/PeshoVurtoleta/lite-pick/releases/tag/v0.2.0
|
|
75
109
|
[0.1.0]: https://github.com/PeshoVurtoleta/lite-pick/releases/tag/v0.1.0
|
|
76
110
|
[0.0.1]: https://github.com/PeshoVurtoleta/lite-pick/releases/tag/v0.0.1
|
package/Pick.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zakkster/lite-pick -- TypeScript declarations.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* strategy classes (
|
|
6
|
-
*
|
|
4
|
+
* M2 (0.2.0): substrate seams + RoundRobin + SmoothWRR (weighted). The remaining
|
|
5
|
+
* strategy classes (P2C, LeastConn/SED/NQ, PeakEWMA, ConsistentHash, BoundedLoad,
|
|
6
|
+
* WeightedRandom) are added one per session.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/** The single source-of-truth version stamp. */
|
|
@@ -68,3 +68,22 @@ export class RoundRobinBalancer extends BalancerBase {
|
|
|
68
68
|
/** Next eligible index in round-robin order, or `PICK_NONE` when the pool is down. */
|
|
69
69
|
pick(): number;
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* SmoothWRRBalancer -- nginx-style smooth weighted round-robin (M2). Distributes picks by
|
|
74
|
+
* caller-configured integer weights, interleaved smoothly (weights [5,1,1] -> A,A,B,A,C,A,A).
|
|
75
|
+
* Owns its smoothing accumulators; the sole writer of the weights via `setWeight`. O(cap)
|
|
76
|
+
* per pick, 0 B/op. Fails closed (`PICK_NONE`) when the eligible-weight sum is 0.
|
|
77
|
+
*/
|
|
78
|
+
export class SmoothWRRBalancer extends BalancerBase {
|
|
79
|
+
/**
|
|
80
|
+
* @param capacity endpoint count (fixed).
|
|
81
|
+
* @param eligible shared view: 1 = pickable, 0 = down (length >= capacity).
|
|
82
|
+
* @param weights per-endpoint weights (length >= capacity); mutate only via setWeight.
|
|
83
|
+
*/
|
|
84
|
+
constructor(capacity: number, eligible: Uint8Array, weights: Uint32Array);
|
|
85
|
+
/** Cold path: reconfigure endpoint `i`'s weight, keeping the eligible-weight total exact. */
|
|
86
|
+
setWeight(i: number, w: number): void;
|
|
87
|
+
/** Next endpoint by smooth weighting, or `PICK_NONE` when the eligible-weight sum is 0. */
|
|
88
|
+
pick(): number;
|
|
89
|
+
}
|
package/Pick.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zakkster/lite-pick -- zero-GC load-balancing SELECTION KERNEL.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* M2 (0.2.0): substrate seams + RoundRobin + SmoothWRR (weighted). This file ships:
|
|
5
5
|
*
|
|
6
6
|
* - VERSION the single source-of-truth version stamp (3-place sync).
|
|
7
7
|
* - PICK_NONE the fail-closed sentinel (-1): "no endpoint", never a dead pick.
|
|
@@ -13,21 +13,22 @@
|
|
|
13
13
|
* It does NOT implement pick() -- strategies subclass it.
|
|
14
14
|
* - RoundRobinBalancer the baseline strategy: a wrapping cursor that forward-scans
|
|
15
15
|
* the eligibility view, skipping down nodes, O(1) amortized, 0 B/op.
|
|
16
|
+
* - SmoothWRRBalancer the weighted default: nginx smooth weighted round-robin over
|
|
17
|
+
* caller-configured integer weights, O(cap)/pick, 0 B/op.
|
|
16
18
|
*
|
|
17
19
|
* The identity (decisions/0001): lite-pick OWNS NO mutable state it can avoid owning.
|
|
18
20
|
* It reads pre-allocated views (eligibility, inflight, weights, scores) that siblings or
|
|
19
21
|
* the caller write, and returns an integer index. Health, circuit state, and load
|
|
20
22
|
* counters live OUTSIDE the kernel. The steady-state pick path allocates 0 B/op.
|
|
21
23
|
*
|
|
22
|
-
* Roster (one strategy per session -- see ROADMAP.md): RoundRobin [
|
|
23
|
-
*
|
|
24
|
-
* WeightedRandom [planned].
|
|
24
|
+
* Roster (one strategy per session -- see ROADMAP.md): RoundRobin [M1], SmoothWRR [M2],
|
|
25
|
+
* P2C, LeastConn/SED/NQ, PeakEWMA, ConsistentHash, BoundedLoad, WeightedRandom [planned].
|
|
25
26
|
*
|
|
26
27
|
* Zero runtime dependencies. node:test only. ESM, single file, tree-shakeable.
|
|
27
28
|
*/
|
|
28
29
|
|
|
29
30
|
/** Version stamp. Synced across package.json and llms.txt (three-place rule). */
|
|
30
|
-
export const VERSION = '0.
|
|
31
|
+
export const VERSION = '0.2.0';
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Fail-closed sentinel returned by pick() when no endpoint is eligible.
|
|
@@ -202,3 +203,98 @@ export class RoundRobinBalancer extends BalancerBase {
|
|
|
202
203
|
return PICK_NONE;
|
|
203
204
|
}
|
|
204
205
|
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* SmoothWRRBalancer -- nginx-style smooth weighted round-robin (M2).
|
|
209
|
+
*
|
|
210
|
+
* Distributes picks by caller-configured integer weights, spreading them SMOOTHLY over
|
|
211
|
+
* time rather than in bursts: weights [5, 1, 1] yield A, A, B, A, C, A, A -- not the
|
|
212
|
+
* A A A A A B C clumping of naive weight-expansion WRR. Each pick adds every eligible
|
|
213
|
+
* node's weight to its accumulator, takes the node with the highest accumulator, and
|
|
214
|
+
* subtracts the total eligible weight from it (the nginx `current += weight; pick max;
|
|
215
|
+
* current -= total` algorithm).
|
|
216
|
+
*
|
|
217
|
+
* Ownership (ADR 0001, ADR 0004): this is the first strategy that owns ALGORITHM state --
|
|
218
|
+
* the per-endpoint smoothing accumulators (`_current`, a Float64Array; Float64 absorbs the
|
|
219
|
+
* sum of uint32 weights without overflow and is 0 B/op on the hot path). Weights live in
|
|
220
|
+
* the caller's Uint32Array, but the balancer is the SOLE writer via the cold `setWeight()`,
|
|
221
|
+
* which keeps `_totalEligibleWeight` exact; mutating the weights array directly desyncs the
|
|
222
|
+
* total (documented UB). An eligibility toggle maintains the total AND resets the toggled
|
|
223
|
+
* node's accumulator (ADR 0004: no stale credit across an eligibility epoch -- anti-flap
|
|
224
|
+
* aligned, ADR 0002).
|
|
225
|
+
*
|
|
226
|
+
* Bound: O(cap) per pick (one scan of the pool -- SmoothWRR is inherently linear in the
|
|
227
|
+
* pool size, negligible at real endpoint counts), zero-alloc. Fails closed (PICK_NONE)
|
|
228
|
+
* when the eligible-weight sum is 0 -- whole pool down, or every eligible node's weight 0.
|
|
229
|
+
*/
|
|
230
|
+
export class SmoothWRRBalancer extends BalancerBase {
|
|
231
|
+
/**
|
|
232
|
+
* @param {number} capacity endpoint count (fixed).
|
|
233
|
+
* @param {Uint8Array} eligible shared view: 1 = pickable, 0 = down (length >= capacity).
|
|
234
|
+
* @param {Uint32Array} weights per-endpoint weights (length >= capacity); the balancer
|
|
235
|
+
* is the sole writer via setWeight() -- direct mutation desyncs the total (UB).
|
|
236
|
+
*/
|
|
237
|
+
constructor(capacity, eligible, weights) {
|
|
238
|
+
super(capacity, eligible);
|
|
239
|
+
if (!(weights instanceof Uint32Array) || weights.length < capacity) {
|
|
240
|
+
throw new RangeError('[lite-pick] weights must be a Uint32Array of length >= capacity');
|
|
241
|
+
}
|
|
242
|
+
this._weights = weights;
|
|
243
|
+
this._current = new Float64Array(capacity);
|
|
244
|
+
let total = 0;
|
|
245
|
+
for (let i = 0; i < capacity; i++) if (eligible[i]) total += weights[i];
|
|
246
|
+
this._totalEligibleWeight = total;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Cold path: mark endpoint i up/down, maintaining the eligible-weight total and
|
|
251
|
+
* RESETTING the toggled node's accumulator (no stale credit across an eligibility
|
|
252
|
+
* epoch, ADR 0004). Zero-alloc.
|
|
253
|
+
* @param {number} i
|
|
254
|
+
* @param {boolean} up
|
|
255
|
+
*/
|
|
256
|
+
setEligible(i, up) {
|
|
257
|
+
const was = this.isEligible(i);
|
|
258
|
+
super.setEligible(i, up); // validates range, flips the bit, updates _live
|
|
259
|
+
const now = this._eligible[i] !== 0;
|
|
260
|
+
if (was !== now) {
|
|
261
|
+
this._totalEligibleWeight += now ? this._weights[i] : -this._weights[i];
|
|
262
|
+
this._current[i] = 0; // reset on every eligibility transition
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Cold path: reconfigure endpoint i's weight, keeping the eligible-weight total exact.
|
|
268
|
+
* @param {number} i
|
|
269
|
+
* @param {number} w new weight (uint32)
|
|
270
|
+
*/
|
|
271
|
+
setWeight(i, w) {
|
|
272
|
+
if (i < 0 || i >= this._cap) throw new RangeError('[lite-pick] index out of range: ' + i);
|
|
273
|
+
const nw = w >>> 0;
|
|
274
|
+
if (nw !== w) throw new RangeError('[lite-pick] weight must be a uint32: ' + w);
|
|
275
|
+
const old = this._weights[i];
|
|
276
|
+
if (nw === old) return;
|
|
277
|
+
this._weights[i] = nw;
|
|
278
|
+
if (this._eligible[i]) this._totalEligibleWeight += nw - old;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Next endpoint by smooth weighting, or PICK_NONE (fail closed). O(cap), zero-alloc.
|
|
283
|
+
* @returns {number}
|
|
284
|
+
*/
|
|
285
|
+
pick() {
|
|
286
|
+
const total = this._totalEligibleWeight;
|
|
287
|
+
if (total <= 0) return PICK_NONE; // whole pool down, or all eligible weights 0
|
|
288
|
+
const cap = this._cap, el = this._eligible, wt = this._weights, cur = this._current;
|
|
289
|
+
let best = -1, bestCur = -Infinity;
|
|
290
|
+
for (let i = 0; i < cap; i++) {
|
|
291
|
+
if (el[i]) {
|
|
292
|
+
const c = cur[i] + wt[i];
|
|
293
|
+
cur[i] = c;
|
|
294
|
+
if (c > bestCur) { bestCur = c; best = i; }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
cur[best] -= total; // best >= 0 guaranteed while total > 0
|
|
298
|
+
return best;
|
|
299
|
+
}
|
|
300
|
+
}
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-pick
|
|
2
2
|
|
|
3
|
-
> Zero-GC load-balancing **selection kernel**: one hot `pick()` that returns an endpoint **index** over a fixed pool and allocates **0 B/op** on the steady-state path. A pure selector, never a proxy -- it consumes health and circuit state, it never owns them. **v0.
|
|
3
|
+
> Zero-GC load-balancing **selection kernel**: one hot `pick()` that returns an endpoint **index** over a fixed pool and allocates **0 B/op** on the steady-state path. A pure selector, never a proxy -- it consumes health and circuit state, it never owns them. **v0.2.0 ships two strategies -- `RoundRobinBalancer` and `SmoothWRRBalancer`** (the weighted default) -- on the substrate seams (`VERSION`, `PICK_NONE`, a deterministic `Prng`, and `BalancerBase`'s shared read-only eligibility view). The rest of the roster -- P2C, LeastConn/SED/NQ, PeakEWMA, ConsistentHash, BoundedLoad, WeightedRandom -- lands one per session.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@zakkster/lite-pick)
|
|
6
6
|
[](https://github.com/sponsors/PeshoVurtoleta)
|
|
@@ -21,7 +21,7 @@ The npm landscape has old algorithm libraries (`load-balancers`, `loadbalance`,
|
|
|
21
21
|
- **Two pieces of evidence, both shipped.** A **0 B/op** witness on the pick path (no object, closure, string, or array created per pick), and a measured **balance-quality anchor** -- peak-to-average load within the strategy's theoretical ceiling (for P2C, the Azar-Broder-Karlin-Upfal `ln ln n / ln 2` bound) and strictly better than a random foil.
|
|
22
22
|
- **A pure selector, not a proxy.** It **consumes** health and circuit state; it never owns them. Health is a shared read-only bitmap written by [`@zakkster/lite-di-health`](https://www.npmjs.com/package/@zakkster/lite-di-health); circuit state comes from [`@zakkster/lite-statechart`](https://www.npmjs.com/package/@zakkster/lite-statechart); load counters are caller-owned typed arrays. `pick()` only reads.
|
|
23
23
|
|
|
24
|
-
> **Status:
|
|
24
|
+
> **Status: M2 (v0.2.0).** Ships the substrate seams **plus `RoundRobinBalancer` and `SmoothWRRBalancer`**. Every strategy is gated: `pick()` proven **0 B/op** (torture + PerfGate), RoundRobin **perfectly fair** with **zero dead picks** vs the naive `i++ % n` foil, SmoothWRR **exactly weighted** and **smooth** (max-run far below bursty weight-expansion WRR), and each scales as advertised (witness). See [ROADMAP.md](./ROADMAP.md) for the M2 -> M10 path to 1.0.0, and [decisions/](./decisions) for the ownership boundary (ADR 0001), anti-flapping (ADR 0002), and the RoundRobin (ADR 0003) and SmoothWRR (ADR 0004) design forks.
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
npm install @zakkster/lite-pick
|
|
@@ -54,6 +54,28 @@ rr.pick() === PICK_NONE; // -> true (-1)
|
|
|
54
54
|
|
|
55
55
|
Every `pick()` above allocates **0 bytes**, owns only an integer cursor, and reads the one shared eligibility view (no second copy to drift). Over a run it hands each *live* endpoint an equal share -- true round-robin over the eligible set, not the raw index space.
|
|
56
56
|
|
|
57
|
+
## SmoothWRR (v0.2.0)
|
|
58
|
+
|
|
59
|
+
The weighted default -- nginx's *smooth* weighted round-robin. Weighted picks are **interleaved evenly** instead of clumped, so a heavy endpoint doesn't get a burst of consecutive requests.
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { SmoothWRRBalancer } from '@zakkster/lite-pick';
|
|
63
|
+
|
|
64
|
+
const eligible = Uint8Array.from([1, 1, 1]);
|
|
65
|
+
const weights = Uint32Array.from([5, 1, 1]); // A is 5x
|
|
66
|
+
|
|
67
|
+
const wrr = new SmoothWRRBalancer(3, eligible, weights);
|
|
68
|
+
|
|
69
|
+
const seq = Array.from({ length: 7 }, () => wrr.pick());
|
|
70
|
+
// -> [0, 0, 1, 0, 2, 0, 0] smooth: A A B A C A A (not A A A A A B C)
|
|
71
|
+
// over 7 picks: A=5, B=1, C=1 -- exactly the weights
|
|
72
|
+
|
|
73
|
+
// Reweight on the cold path (the balancer stays the sole writer of the weights):
|
|
74
|
+
wrr.setWeight(1, 4); // B is now 4x
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`pick()` is **0 B/op** and **O(cap)** (one scan of the pool -- negligible at real endpoint counts). It owns its smoothing accumulators; weights live in your `Uint32Array` but you mutate them only through `setWeight`, which keeps the internal eligible-weight total exact. Marking a node down/up resets its accumulator, so a recovered node rejoins neutral -- no stale burst or starvation ([ADR 0004](./decisions/0004-smoothwrr-weight-ownership.md)).
|
|
78
|
+
|
|
57
79
|
## The substrate (under every strategy)
|
|
58
80
|
|
|
59
81
|
```js
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-pick
|
|
2
2
|
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
License: MIT (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
|
|
5
5
|
Runtime dependencies: none. ESM only. ASCII-only source. sideEffects: false.
|
|
6
6
|
Node: >= 18.
|
|
@@ -14,11 +14,11 @@ reading pre-allocated views that siblings or the caller write, and returning an
|
|
|
14
14
|
The complementary evidence lite-pick ships is a measured balance-quality anchor (peak-to-
|
|
15
15
|
average load vs the strategy's theoretical ceiling) alongside the 0 B/op pick witness.
|
|
16
16
|
|
|
17
|
-
0.
|
|
18
|
-
fail-closed sentinel `PICK_NONE` (-1), a deterministic
|
|
19
|
-
(the shared read-only eligibility seam + O(1) live count),
|
|
20
|
-
remaining strategies land one per session
|
|
21
|
-
PeakEWMA, ConsistentHash, BoundedLoad, WeightedRandom.
|
|
17
|
+
0.2.0 ships the substrate seams + two strategies: RoundRobin and SmoothWRR (the weighted
|
|
18
|
+
default). It exports `VERSION`, the fail-closed sentinel `PICK_NONE` (-1), a deterministic
|
|
19
|
+
`Prng` (xorshift32), `BalancerBase` (the shared read-only eligibility seam + O(1) live count),
|
|
20
|
+
`RoundRobinBalancer`, and `SmoothWRRBalancer`. The remaining strategies land one per session
|
|
21
|
+
(see ROADMAP.md): P2C, LeastConn/SED/NQ, PeakEWMA, ConsistentHash, BoundedLoad, WeightedRandom.
|
|
22
22
|
|
|
23
23
|
## Design ownership (decisions/0001, 0002)
|
|
24
24
|
|
|
@@ -57,6 +57,18 @@ PeakEWMA, ConsistentHash, BoundedLoad, WeightedRandom.
|
|
|
57
57
|
forward-scans the eligibility view, skipping down nodes), or `PICK_NONE` when the pool is
|
|
58
58
|
down. O(1) amortized, O(cap) worst case under sparse eligibility, 0 B/op. Owns only its
|
|
59
59
|
cursor; no eligible-set structure (ADR 0003 chose the stateless bitmap-scan path for M1).
|
|
60
|
+
- `SmoothWRRBalancer extends BalancerBase` -- class. The weighted default (M2).
|
|
61
|
+
- `new SmoothWRRBalancer(capacity, eligible, weights)` -- `weights` is a Uint32Array
|
|
62
|
+
(length >= capacity); the balancer is the sole writer via setWeight (direct mutation
|
|
63
|
+
desyncs the total -- UB).
|
|
64
|
+
- `setWeight(i, w)` -> void. COLD. Reconfigure endpoint i's weight (uint32), keeping the
|
|
65
|
+
eligible-weight total exact.
|
|
66
|
+
- `setEligible(i, up)` -> void. COLD. Overrides the base to maintain the eligible-weight
|
|
67
|
+
total and reset the toggled node's accumulator (no stale credit across an epoch, ADR 0004).
|
|
68
|
+
- `pick()` -> number. Next endpoint by nginx smooth weighted round-robin (`current +=
|
|
69
|
+
weight; pick max; current -= total`), interleaved smoothly (weights [5,1,1] ->
|
|
70
|
+
A,A,B,A,C,A,A), or `PICK_NONE` when the eligible-weight sum is 0. O(cap) per pick, 0 B/op.
|
|
71
|
+
Owns its Float64Array smoothing accumulators (ADR 0004).
|
|
60
72
|
|
|
61
73
|
## Gates (every session)
|
|
62
74
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-pick",
|
|
3
3
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "Zero-dependency, zero-GC load-balancing selection kernel: one hot pick() -> endpoint index over a fixed pool, 0 B/op steady-state. A pure selector (consumes health/circuit state, never a proxy) for the in-process hop, complementary to AWS NLB/ALB. Tree-shakeable ESM roster: RoundRobin, SmoothWRR, P2C, least-conn, PeakEWMA, consistent hashing.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./Pick.js",
|