@zakkster/lite-pick 0.1.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 +76 -0
- package/LICENSE +21 -0
- package/Pick.d.ts +70 -0
- package/Pick.js +204 -0
- package/README.md +140 -0
- package/llms.txt +80 -0
- package/package.json +117 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@zakkster/lite-pick` are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-09-23
|
|
8
|
+
|
|
9
|
+
M1: the first strategy, RoundRobin (ROADMAP.md M1). The M0 harness stubs become real,
|
|
10
|
+
strategy-bearing gates.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `RoundRobinBalancer extends BalancerBase` -- the baseline strategy. A wrapping cursor
|
|
15
|
+
that forward-scans the shared eligibility view, skipping down nodes, to hand each LIVE
|
|
16
|
+
endpoint an equal share in index order (true round-robin over the live set, not the raw
|
|
17
|
+
index space). Owns only its cursor; O(1) amortized (O(cap) worst case under sparse
|
|
18
|
+
eligibility); 0 B/op on `pick()`; fail-closed `PICK_NONE` when the pool is down.
|
|
19
|
+
- `test/RoundRobin.test.js` -- boundary + behaviour suite (11 tests): perfect fairness,
|
|
20
|
+
round-robin order, skips down nodes, fail-closed, single-node, recovery, and a
|
|
21
|
+
never-returns-a-down-index proof under 200k picks of adversarial eligibility churn.
|
|
22
|
+
- Gates wired for RoundRobin: `torture.mjs` (retention + 0 B/op `pick()` phase),
|
|
23
|
+
`test/perf/PerfGate.test.mjs` (`zgcSuite` scenario + a `mustFail` teeth-check),
|
|
24
|
+
`test/witness.mjs` (throughput flatness across the pool sweep), `test/balance.mjs`
|
|
25
|
+
(imbalance 1.0000 on all-up, zero dead picks under partial eligibility vs the
|
|
26
|
+
`i++ % n` foil's dead-pick trap).
|
|
27
|
+
- `benchmark/Matrix.mjs` -- the SUBJECTS registration point (RoundRobin + the `i++ % n`
|
|
28
|
+
foil), a parity-check throughput slice ahead of the full M6 suite.
|
|
29
|
+
- `decisions/0003-roundrobin-bitmap-scan.md` -- the stateless bitmap-scan (Option A) vs
|
|
30
|
+
maintained eligible-set (Option B) fork; A now, B revisited at M3 with the lite-o1 peer.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Version 0.0.1 -> 0.1.0 across `package.json`, `Pick.js` `VERSION`, and `llms.txt`.
|
|
35
|
+
- Documented the composition model as OPTIONAL PEER dependencies (`peerDependenciesMeta`,
|
|
36
|
+
the LiteQuery model) -- zero HARD deps, never inlined/forked; kernel runs with zero peers.
|
|
37
|
+
|
|
38
|
+
## [0.0.1] - 2026-09-22
|
|
39
|
+
|
|
40
|
+
M0 scaffold: the substrate seams only, no strategy yet (ROADMAP.md M0). This release
|
|
41
|
+
establishes the shared machinery every strategy (M1+) will ride and locks the ratified
|
|
42
|
+
ownership boundary in place before any `pick()` is written.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- `Pick.js` single-file ESM kernel with:
|
|
47
|
+
- `VERSION` -- the source-of-truth version stamp (three-place sync with package.json + llms.txt).
|
|
48
|
+
- `PICK_NONE` (-1) -- the fail-closed sentinel: no endpoint is ever a dead pick.
|
|
49
|
+
- `Prng` -- an instance-local, deterministic xorshift32 (`next` / `nextBelow` / `reset`),
|
|
50
|
+
so a strategy can draw on the hot path without `Math.random` and the balance
|
|
51
|
+
benchmark stays reproducible.
|
|
52
|
+
- `BalancerBase` -- the shared eligibility seam: a fixed-capacity pool over a SHARED,
|
|
53
|
+
read-only `Uint8Array` eligibility view (written by `@zakkster/lite-di-health` /
|
|
54
|
+
circuit breakers, read by `pick()`), an O(1) `live` count, `isEligible` / `setEligible`,
|
|
55
|
+
and an abstract `pick()` that throws until a strategy overrides it.
|
|
56
|
+
- `Pick.d.ts` -- TypeScript declarations for the substrate surface.
|
|
57
|
+
- `llms.txt` -- LLM-oriented API + design summary.
|
|
58
|
+
- Test harness: `test/Base.test.js` (substrate boundary suite), `test/torture.mjs`
|
|
59
|
+
(lite-leak retention + lite-gc-profiler 0 B/op on the substrate hot path),
|
|
60
|
+
`test/perf/PerfGate.test.mjs` (lite-perf-gate `zgcSuite` hard gate + a `mustFail`
|
|
61
|
+
teeth-check), `test/witness.mjs` (throughput flatness), `test/balance.mjs` (the
|
|
62
|
+
random-foil baseline the strategy ceilings assert against from M1), and the
|
|
63
|
+
`test/types` tsc surface check.
|
|
64
|
+
- `decisions/0001-selection-kernel-boundary.md` -- the five ratified ownership forks.
|
|
65
|
+
- `decisions/0002-anti-flapping.md` -- hysteresis/dwell/backoff on routing-state transitions.
|
|
66
|
+
- `RESEARCH.md`, `ROADMAP.md`, `README.md`, `LICENSE`.
|
|
67
|
+
|
|
68
|
+
### Notes
|
|
69
|
+
|
|
70
|
+
- Zero runtime dependencies. ESM only. `sideEffects: false`. Node >= 18.
|
|
71
|
+
- Published metadata points at `PeshoVurtoleta/lite-pick`.
|
|
72
|
+
- Next: **M1 RoundRobin** (0.1.0) -- the first strategy, landing the throughput witness
|
|
73
|
+
and the balance gate.
|
|
74
|
+
|
|
75
|
+
[0.1.0]: https://github.com/PeshoVurtoleta/lite-pick/releases/tag/v0.1.0
|
|
76
|
+
[0.0.1]: https://github.com/PeshoVurtoleta/lite-pick/releases/tag/v0.0.1
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Pick.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zakkster/lite-pick -- TypeScript declarations.
|
|
3
|
+
*
|
|
4
|
+
* M1 (0.1.0): the substrate seams + the first strategy, RoundRobin. The remaining
|
|
5
|
+
* strategy classes (SmoothWRR, P2C, LeastConn/SED/NQ, PeakEWMA, ConsistentHash,
|
|
6
|
+
* BoundedLoad, WeightedRandom) are added one per session.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The single source-of-truth version stamp. */
|
|
10
|
+
export const VERSION: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fail-closed sentinel returned by `pick()` when no endpoint is eligible.
|
|
14
|
+
* A strategy never returns a down index; `PICK_NONE` (-1) means "no endpoint".
|
|
15
|
+
*/
|
|
16
|
+
export const PICK_NONE: -1;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Instance-local, deterministic xorshift32 PRNG. Zero-alloc per step; seedable and
|
|
20
|
+
* `reset()`-able so the balance benchmark stays reproducible.
|
|
21
|
+
*/
|
|
22
|
+
export class Prng {
|
|
23
|
+
/** @param seed 32-bit seed; 0 is remapped to the default. */
|
|
24
|
+
constructor(seed?: number);
|
|
25
|
+
/** One xorshift32 step -> a uint32 in [1, 2^32). */
|
|
26
|
+
next(): number;
|
|
27
|
+
/** A uint32 in [0, n). */
|
|
28
|
+
nextBelow(n: number): number;
|
|
29
|
+
/** Restore the original seed. */
|
|
30
|
+
reset(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The shared eligibility seam for every strategy. Owns the fixed capacity, a reference
|
|
35
|
+
* to a shared read-only eligibility `Uint8Array` (written by @zakkster/lite-di-health /
|
|
36
|
+
* circuit breakers, read by `pick()`), and an O(1) live count. Subclasses implement
|
|
37
|
+
* `pick()`; the base `pick()` throws.
|
|
38
|
+
*/
|
|
39
|
+
export class BalancerBase {
|
|
40
|
+
/**
|
|
41
|
+
* @param capacity endpoint count (fixed).
|
|
42
|
+
* @param eligible shared view: 1 = pickable, 0 = down (length >= capacity).
|
|
43
|
+
*/
|
|
44
|
+
constructor(capacity: number, eligible: Uint8Array);
|
|
45
|
+
/** Endpoint count (fixed at construction). */
|
|
46
|
+
readonly capacity: number;
|
|
47
|
+
/** Number of currently eligible endpoints (O(1)). */
|
|
48
|
+
readonly live: number;
|
|
49
|
+
/** True iff endpoint `i` is currently pickable. */
|
|
50
|
+
isEligible(i: number): boolean;
|
|
51
|
+
/** Cold path: mark endpoint `i` up/down, keeping the live count exact. */
|
|
52
|
+
setEligible(i: number, up: boolean): void;
|
|
53
|
+
/** Choose an endpoint index, or `PICK_NONE`. Abstract in the base (throws). */
|
|
54
|
+
pick(): number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* RoundRobinBalancer -- the baseline strategy (M1). A wrapping cursor that forward-scans
|
|
59
|
+
* the shared eligibility view, skipping down nodes, to hand each LIVE endpoint an equal
|
|
60
|
+
* share in index order. Owns only its cursor; O(1) amortized, 0 B/op on `pick()`.
|
|
61
|
+
*/
|
|
62
|
+
export class RoundRobinBalancer extends BalancerBase {
|
|
63
|
+
/**
|
|
64
|
+
* @param capacity endpoint count (fixed).
|
|
65
|
+
* @param eligible shared view: 1 = pickable, 0 = down (length >= capacity).
|
|
66
|
+
*/
|
|
67
|
+
constructor(capacity: number, eligible: Uint8Array);
|
|
68
|
+
/** Next eligible index in round-robin order, or `PICK_NONE` when the pool is down. */
|
|
69
|
+
pick(): number;
|
|
70
|
+
}
|
package/Pick.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zakkster/lite-pick -- zero-GC load-balancing SELECTION KERNEL.
|
|
3
|
+
*
|
|
4
|
+
* M1 (0.1.0): the substrate seams + the FIRST strategy, RoundRobin. This file ships:
|
|
5
|
+
*
|
|
6
|
+
* - VERSION the single source-of-truth version stamp (3-place sync).
|
|
7
|
+
* - PICK_NONE the fail-closed sentinel (-1): "no endpoint", never a dead pick.
|
|
8
|
+
* - Prng an instance-local, deterministic xorshift32 (seeded, reset()).
|
|
9
|
+
* - BalancerBase the eligibility seam: a fixed-capacity pool over a SHARED,
|
|
10
|
+
* read-only Uint8Array eligibility view (1 = pickable, 0 = down)
|
|
11
|
+
* written by @zakkster/lite-di-health / circuit breakers and only
|
|
12
|
+
* READ here, plus an O(1) live count and a cold-path setEligible().
|
|
13
|
+
* It does NOT implement pick() -- strategies subclass it.
|
|
14
|
+
* - RoundRobinBalancer the baseline strategy: a wrapping cursor that forward-scans
|
|
15
|
+
* the eligibility view, skipping down nodes, O(1) amortized, 0 B/op.
|
|
16
|
+
*
|
|
17
|
+
* The identity (decisions/0001): lite-pick OWNS NO mutable state it can avoid owning.
|
|
18
|
+
* It reads pre-allocated views (eligibility, inflight, weights, scores) that siblings or
|
|
19
|
+
* the caller write, and returns an integer index. Health, circuit state, and load
|
|
20
|
+
* counters live OUTSIDE the kernel. The steady-state pick path allocates 0 B/op.
|
|
21
|
+
*
|
|
22
|
+
* Roster (one strategy per session -- see ROADMAP.md): RoundRobin [shipped M1],
|
|
23
|
+
* SmoothWRR, P2C, LeastConn/SED/NQ, PeakEWMA, ConsistentHash, BoundedLoad,
|
|
24
|
+
* WeightedRandom [planned].
|
|
25
|
+
*
|
|
26
|
+
* Zero runtime dependencies. node:test only. ESM, single file, tree-shakeable.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Version stamp. Synced across package.json and llms.txt (three-place rule). */
|
|
30
|
+
export const VERSION = '0.1.0';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fail-closed sentinel returned by pick() when no endpoint is eligible.
|
|
34
|
+
* null is not zero: a strategy never picks a down node "to be safe".
|
|
35
|
+
*/
|
|
36
|
+
export const PICK_NONE = -1;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Prng -- instance-local, deterministic xorshift32.
|
|
40
|
+
*
|
|
41
|
+
* One PRNG step is a few integer ops and allocates nothing, so a strategy can draw
|
|
42
|
+
* on the hot path without touching Math.random (which is neither seedable nor
|
|
43
|
+
* gate-friendly) and the balance benchmark (the anchor) stays reproducible.
|
|
44
|
+
*
|
|
45
|
+
* Marsaglia's xorshift32: full period 2^32 - 1, never yields 0 once seeded non-zero.
|
|
46
|
+
*/
|
|
47
|
+
export class Prng {
|
|
48
|
+
/**
|
|
49
|
+
* @param {number} [seed=0x9e3779b9] 32-bit seed (0 is remapped to the default,
|
|
50
|
+
* since xorshift stuck at 0 stays 0).
|
|
51
|
+
*/
|
|
52
|
+
constructor(seed = 0x9e3779b9) {
|
|
53
|
+
const s = seed >>> 0;
|
|
54
|
+
this._seed = s === 0 ? 0x9e3779b9 : s;
|
|
55
|
+
this._s = this._seed;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One xorshift32 step -> a uint32 in [1, 2^32). Zero-alloc, deterministic. */
|
|
59
|
+
next() {
|
|
60
|
+
let x = this._s;
|
|
61
|
+
x ^= x << 13;
|
|
62
|
+
x ^= x >>> 17;
|
|
63
|
+
x ^= x << 5;
|
|
64
|
+
this._s = x >>> 0;
|
|
65
|
+
return this._s;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A uint32 in [0, n) via multiply-shift (no modulo bias for the balance gate). */
|
|
69
|
+
nextBelow(n) {
|
|
70
|
+
// (rand * n) >>> 32 -- unbiased enough for selection, one Math.imul-free mul.
|
|
71
|
+
return Math.floor((this.next() / 4294967296) * n);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Restore the original seed, so a benchmark run is byte-for-byte repeatable. */
|
|
75
|
+
reset() {
|
|
76
|
+
this._s = this._seed;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* BalancerBase -- the shared eligibility seam for every strategy.
|
|
82
|
+
*
|
|
83
|
+
* It owns ONLY: the fixed capacity, a reference to the caller/sibling-owned eligibility
|
|
84
|
+
* Uint8Array (never copied), and an O(1) `_live` count maintained on the cold setEligible()
|
|
85
|
+
* path so a strategy can fail closed in O(1). It never allocates after construction and
|
|
86
|
+
* never calls into a health source -- writers mutate `eligible` at their own cadence; pick()
|
|
87
|
+
* only reads it.
|
|
88
|
+
*
|
|
89
|
+
* Subclasses (M1+) implement pick(). BalancerBase.pick() throws, so an unfinished strategy
|
|
90
|
+
* fails loudly rather than silently returning a dead index.
|
|
91
|
+
*/
|
|
92
|
+
export class BalancerBase {
|
|
93
|
+
/**
|
|
94
|
+
* @param {number} capacity endpoint count (fixed; add/remove is a cold rebuild).
|
|
95
|
+
* @param {Uint8Array} eligible 1 = pickable, 0 = down. SHARED, read-only to pick().
|
|
96
|
+
* Written by @zakkster/lite-di-health probes / circuit breakers / admin.
|
|
97
|
+
*/
|
|
98
|
+
constructor(capacity, eligible) {
|
|
99
|
+
if (!Number.isInteger(capacity) || capacity < 1) {
|
|
100
|
+
throw new RangeError('[lite-pick] capacity must be an integer >= 1');
|
|
101
|
+
}
|
|
102
|
+
if (!(eligible instanceof Uint8Array) || eligible.length < capacity) {
|
|
103
|
+
throw new RangeError('[lite-pick] eligible must be a Uint8Array of length >= capacity');
|
|
104
|
+
}
|
|
105
|
+
this._cap = capacity;
|
|
106
|
+
this._eligible = eligible;
|
|
107
|
+
this._live = 0;
|
|
108
|
+
for (let i = 0; i < capacity; i++) if (eligible[i]) this._live++;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Endpoint count (fixed at construction). */
|
|
112
|
+
get capacity() {
|
|
113
|
+
return this._cap;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Number of currently eligible endpoints (O(1), cold-path maintained). */
|
|
117
|
+
get live() {
|
|
118
|
+
return this._live;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** True iff endpoint i is currently pickable. O(1), zero-alloc. */
|
|
122
|
+
isEligible(i) {
|
|
123
|
+
return i >= 0 && i < this._cap && this._eligible[i] !== 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Cold path: mark endpoint i up/down (delegated FROM lite-di-health), keeping the
|
|
128
|
+
* shared view and the O(1) `_live` count in lockstep. Idempotent. Zero-alloc.
|
|
129
|
+
* @param {number} i
|
|
130
|
+
* @param {boolean} up
|
|
131
|
+
*/
|
|
132
|
+
setEligible(i, up) {
|
|
133
|
+
if (i < 0 || i >= this._cap) {
|
|
134
|
+
throw new RangeError('[lite-pick] index out of range: ' + i);
|
|
135
|
+
}
|
|
136
|
+
const was = this._eligible[i];
|
|
137
|
+
const now = up ? 1 : 0;
|
|
138
|
+
if (was !== now) {
|
|
139
|
+
this._eligible[i] = now;
|
|
140
|
+
this._live += now ? 1 : -1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Choose an endpoint index, or PICK_NONE when the whole pool is down (fail closed).
|
|
146
|
+
* Not implemented in the base -- M1+ strategies override this.
|
|
147
|
+
* @returns {number}
|
|
148
|
+
*/
|
|
149
|
+
pick() {
|
|
150
|
+
throw new Error('[lite-pick] BalancerBase.pick() is abstract -- use a strategy (M1+)');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* RoundRobinBalancer -- the baseline strategy (M1).
|
|
156
|
+
*
|
|
157
|
+
* A single wrapping cursor over the shared eligibility view. `pick()` advances the
|
|
158
|
+
* cursor and forward-scans, skipping down nodes (`eligible[i] === 0`), until it lands
|
|
159
|
+
* on the next pickable endpoint. Over a run of picks this hands each eligible endpoint
|
|
160
|
+
* an equal share, in index order -- true round-robin over the LIVE set, not the raw
|
|
161
|
+
* index space (the distinction from a naive `i++ % n`, which would return down nodes).
|
|
162
|
+
*
|
|
163
|
+
* Ownership (ADR 0001): it owns ONLY the integer cursor. Eligibility is the shared,
|
|
164
|
+
* read-only Uint8Array from BalancerBase; `pick()` reads it and returns an index. No
|
|
165
|
+
* SparseSet of eligibles is maintained -- ADR 0003 chose the stateless bitmap-scan path
|
|
166
|
+
* (Option A) for M1; the lite-o1 RandomSet/SparseSet substrate (Option B, an optional
|
|
167
|
+
* PEER dep) arrives at M3 when P2C needs a random eligible draw.
|
|
168
|
+
*
|
|
169
|
+
* Bound: O(1) amortized (one step when the next index is eligible), worst case O(cap)
|
|
170
|
+
* when eligibility is sparse (bounded by a single wrap -- `_live > 0` guarantees a hit
|
|
171
|
+
* within `cap` steps). Steady-state pick(): a compare-wrap loop over the view, one
|
|
172
|
+
* cursor write. No object, closure, string, or array is created -- proven 0 B/op by
|
|
173
|
+
* test/torture.mjs and test/perf/PerfGate.test.mjs.
|
|
174
|
+
*/
|
|
175
|
+
export class RoundRobinBalancer extends BalancerBase {
|
|
176
|
+
/**
|
|
177
|
+
* @param {number} capacity endpoint count (fixed; add/remove is a cold rebuild).
|
|
178
|
+
* @param {Uint8Array} eligible shared view: 1 = pickable, 0 = down (length >= capacity).
|
|
179
|
+
*/
|
|
180
|
+
constructor(capacity, eligible) {
|
|
181
|
+
super(capacity, eligible);
|
|
182
|
+
// Last index returned. -1 so the first pick starts the scan at index 0.
|
|
183
|
+
this._cursor = -1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Next eligible endpoint index in round-robin order, or PICK_NONE when the whole
|
|
188
|
+
* pool is down (fail closed). O(1) amortized, O(cap) worst case, zero-alloc.
|
|
189
|
+
* @returns {number}
|
|
190
|
+
*/
|
|
191
|
+
pick() {
|
|
192
|
+
if (this._live === 0) return PICK_NONE; // whole pool down: fail closed
|
|
193
|
+
const cap = this._cap, el = this._eligible;
|
|
194
|
+
let i = this._cursor;
|
|
195
|
+
for (let steps = 0; steps < cap; steps++) {
|
|
196
|
+
i++;
|
|
197
|
+
if (i >= cap) i = 0; // wrap (cap is caller-given, not power-of-2)
|
|
198
|
+
if (el[i]) { this._cursor = i; return i; }
|
|
199
|
+
}
|
|
200
|
+
// Unreachable while `_live` is exact (setEligible maintains it): a positive live
|
|
201
|
+
// count guarantees a set bit within one wrap. Fail closed rather than loop.
|
|
202
|
+
return PICK_NONE;
|
|
203
|
+
}
|
|
204
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# @zakkster/lite-pick
|
|
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.1.0 ships the first strategy, `RoundRobinBalancer`**, on the substrate seams (`VERSION`, `PICK_NONE`, a deterministic `Prng`, and `BalancerBase`'s shared read-only eligibility view). The rest of the roster -- SmoothWRR, P2C, LeastConn/SED/NQ, PeakEWMA, ConsistentHash, BoundedLoad, WeightedRandom -- lands one per session.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@zakkster/lite-pick)
|
|
6
|
+
[](https://github.com/sponsors/PeshoVurtoleta)
|
|
7
|
+

|
|
8
|
+
[](https://bundlephobia.com/result?p=@zakkster/lite-pick)
|
|
9
|
+
[](https://www.npmjs.com/package/@zakkster/lite-pick)
|
|
10
|
+
[](https://www.npmjs.com/package/@zakkster/lite-pick)
|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
14
|
+
[](./LICENSE)
|
|
15
|
+
|
|
16
|
+
## The load balancer the ecosystem was missing
|
|
17
|
+
|
|
18
|
+
The npm landscape has old algorithm libraries (`load-balancers`, `loadbalance`, `wrr`) and heavy full proxies -- but **no package that ships a provably zero-GC `pick()` path with a measured balance-quality anchor.** Most algorithm libraries use ordinary objects and arrays and quietly allocate under sustained high call rates (millions of picks/sec in worker fan-out, high-QPS internal services, client-side routing). `lite-pick` fills that gap: a small, dependency-free, ESM-first selection **kernel** you drop into an HTTP client, a worker pool, or a custom proxy -- and it proves its two claims instead of asserting them.
|
|
19
|
+
|
|
20
|
+
- **The `pick()` is the product.** One hot-path primitive: given a pool of endpoints, return the index of the one to use. Every strategy (round-robin, weighted, power-of-two-choices, least-connections, latency-aware, consistent-hash, bounded-load) is a different `pick()` over the same substrate.
|
|
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
|
+
- **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
|
+
|
|
24
|
+
> **Status: M1 (v0.1.0).** Ships the substrate seams **plus the first strategy, `RoundRobinBalancer`**. The M0 harness stubs are now real gates: `pick()` is proven **0 B/op** (torture + PerfGate), **perfectly fair** (balance: imbalance 1.0000, and zero dead picks vs the naive `i++ % n` foil), and **flat** across the pool sweep (witness). See [ROADMAP.md](./ROADMAP.md) for the M1 -> M10 path to 1.0.0, and [decisions/](./decisions) for the ownership boundary (ADR 0001), anti-flapping (ADR 0002), and the RoundRobin design fork (ADR 0003).
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @zakkster/lite-pick
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## RoundRobin (v0.1.0)
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { RoundRobinBalancer, PICK_NONE } from '@zakkster/lite-pick';
|
|
34
|
+
|
|
35
|
+
// A pool of 4 endpoints. The eligibility view is SHARED and read-only to pick():
|
|
36
|
+
// lite-di-health probes / circuit breakers write it; the balancer only reads it.
|
|
37
|
+
const eligible = Uint8Array.from([1, 1, 0, 1]); // endpoint 2 is down
|
|
38
|
+
|
|
39
|
+
const rr = new RoundRobinBalancer(4, eligible);
|
|
40
|
+
|
|
41
|
+
rr.pick(); // -> 0
|
|
42
|
+
rr.pick(); // -> 1
|
|
43
|
+
rr.pick(); // -> 3 (skips the down endpoint 2, never returns it)
|
|
44
|
+
rr.pick(); // -> 0 (wraps)
|
|
45
|
+
|
|
46
|
+
// A health source marks endpoint 2 back up (cold path; live count stays exact).
|
|
47
|
+
rr.setEligible(2, true);
|
|
48
|
+
rr.pick(); // -> 1, then 2, then 3, ... now that 2 is eligible
|
|
49
|
+
|
|
50
|
+
// Whole pool down -> fail closed, never a dead pick.
|
|
51
|
+
for (let i = 0; i < 4; i++) rr.setEligible(i, false);
|
|
52
|
+
rr.pick() === PICK_NONE; // -> true (-1)
|
|
53
|
+
```
|
|
54
|
+
|
|
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
|
+
|
|
57
|
+
## The substrate (under every strategy)
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
import { BalancerBase, Prng, PICK_NONE, VERSION } from '@zakkster/lite-pick';
|
|
61
|
+
|
|
62
|
+
// A pool of 4 endpoints. The eligibility view is SHARED and read-only to pick():
|
|
63
|
+
// lite-di-health probes / circuit breakers write it; the balancer only reads it.
|
|
64
|
+
const eligible = Uint8Array.from([1, 1, 0, 1]); // endpoint 2 is down
|
|
65
|
+
|
|
66
|
+
const base = new BalancerBase(4, eligible);
|
|
67
|
+
base.capacity; // -> 4
|
|
68
|
+
base.live; // -> 3 (O(1), cold-path maintained)
|
|
69
|
+
base.isEligible(2); // -> false (out-of-range is false too, never a throw)
|
|
70
|
+
|
|
71
|
+
// Cold path: a health source marks endpoint 2 back up. live stays exact.
|
|
72
|
+
base.setEligible(2, true);
|
|
73
|
+
base.live; // -> 4
|
|
74
|
+
|
|
75
|
+
// A deterministic PRNG so the balance benchmark is reproducible (no Math.random
|
|
76
|
+
// on a gated path). Strategies draw from this on the hot path, zero-alloc.
|
|
77
|
+
const rng = new Prng(0x1234abcd);
|
|
78
|
+
rng.nextBelow(4); // -> a uint32 in [0, 4)
|
|
79
|
+
rng.reset(); // replays the exact stream
|
|
80
|
+
|
|
81
|
+
PICK_NONE; // -> -1 (fail-closed sentinel: no endpoint, never a dead pick)
|
|
82
|
+
VERSION; // -> '0.1.0'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`BalancerBase.pick()` is **abstract** -- it throws, so an unfinished strategy fails loudly rather than returning a dead index; `RoundRobinBalancer` (above) overrides it. Here is the shape the headline **P2C** strategy will take (M3), for orientation:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
// SHAPE ONLY -- not shipped until M3. Two random eligible draws, return the lower
|
|
89
|
+
// in-flight load. One extra probe over random buys the ln ln n balance ceiling.
|
|
90
|
+
class P2cBalancer extends BalancerBase {
|
|
91
|
+
constructor(capacity, eligible, inflight, seed) {
|
|
92
|
+
super(capacity, eligible);
|
|
93
|
+
this._inflight = inflight; // caller-owned Uint32Array; pick() only reads it
|
|
94
|
+
this._rng = new Prng(seed);
|
|
95
|
+
}
|
|
96
|
+
pick() {
|
|
97
|
+
if (this.live === 0) return PICK_NONE; // fail closed
|
|
98
|
+
const a = this._draw(), b = this._draw(); // two distinct eligible draws
|
|
99
|
+
return this._inflight[b] < this._inflight[a] ? b : a;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Design ownership (ratified before any strategy)
|
|
105
|
+
|
|
106
|
+
lite-pick owns **no mutable state it can avoid owning** ([ADR 0001](./decisions/0001-selection-kernel-boundary.md)):
|
|
107
|
+
|
|
108
|
+
| Concern | Owner | lite-pick's role |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| Liveness / eligibility | `@zakkster/lite-di-health` writes a shared `Uint8Array` | **reads** it, zero-copy |
|
|
111
|
+
| Circuit state | `@zakkster/lite-statechart` (consumed) | never built in; sees only the bit |
|
|
112
|
+
| In-flight / rtt counters | caller-owned `Uint32Array` / `Float64Array` | **reads** them; pure `pick()` |
|
|
113
|
+
| Whole pool down | -- | fail-closed: returns `PICK_NONE` (-1) |
|
|
114
|
+
| Routing flap | the layer that writes the shared view | hysteresis/dwell ([ADR 0002](./decisions/0002-anti-flapping.md)); `pick()` stays greedy |
|
|
115
|
+
|
|
116
|
+
**In-process first** (workers, DI services, ECS-style systems); remote HTTP is served by a thin optional adapter that owns the observation loop and writes the same shared views. The zero-GC contract stays strict because the kernel never touches the request lifecycle.
|
|
117
|
+
|
|
118
|
+
## Composes with
|
|
119
|
+
|
|
120
|
+
The moat is not the algorithms -- it is that lite-pick wires already-proven zero-GC parts of the suite: [`lite-di-health`](https://www.npmjs.com/package/@zakkster/lite-di-health) (liveness), [`lite-o1`](https://www.npmjs.com/package/@zakkster/lite-o1) (`RandomSet` / `AliasTable` / `RingLog` substrate), [`lite-logn`](https://www.npmjs.com/package/@zakkster/lite-logn) (exact least-conn heap / Fenwick weights), [`lite-lru`](https://www.npmjs.com/package/@zakkster/lite-lru) (sticky affinity), [`lite-statechart`](https://www.npmjs.com/package/@zakkster/lite-statechart) (breaker), [`lite-query`](https://www.npmjs.com/package/@zakkster/lite-query) (the fetcher adapter), and [`lite-await`](https://www.npmjs.com/package/@zakkster/lite-await) (hedging). **None is a hard dependency** -- each is an *optional peer* (`peerDependenciesMeta.optional`), every seam is duck-typed over a shared TypedArray, and the kernel runs with zero peers installed.
|
|
121
|
+
|
|
122
|
+
## Gates
|
|
123
|
+
|
|
124
|
+
Every strategy session must pass, no exceptions:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm test # node:test boundary suite
|
|
128
|
+
npm run test:types # tsc type-surface check (Pick.d.ts vs runtime)
|
|
129
|
+
npm run torture # lite-leak retention + lite-gc-profiler 0 B/op (needs --expose-gc)
|
|
130
|
+
npm run test:perf # lite-perf-gate HARD zero-alloc gate + a mustFail teeth-check
|
|
131
|
+
npm run witness # pick throughput flatness across a pool-size sweep
|
|
132
|
+
npm run balance # peak-to-average vs the strategy ceiling + random foil (the anchor)
|
|
133
|
+
npm run verify # all of the above
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The zero-GC proof is two complementary tools kept separate (the suite's torture-harness discipline): a soak tester (`torture.mjs`, [`@zakkster/lite-leak`](https://www.npmjs.com/package/@zakkster/lite-leak) + [`@zakkster/lite-gc-profiler`](https://www.npmjs.com/package/@zakkster/lite-gc-profiler)) and a node:test-native hard gate (`test/perf/PerfGate.test.mjs`, [`@zakkster/lite-perf-gate`](https://www.npmjs.com/package/@zakkster/lite-perf-gate)), which includes a `mustFail` scenario proving the instrument has teeth.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT (c) Zahary Shinikchiev
|
package/llms.txt
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @zakkster/lite-pick
|
|
2
|
+
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
License: MIT (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
|
|
5
|
+
Runtime dependencies: none. ESM only. ASCII-only source. sideEffects: false.
|
|
6
|
+
Node: >= 18.
|
|
7
|
+
|
|
8
|
+
## What it is
|
|
9
|
+
|
|
10
|
+
A zero-GC load-balancing SELECTION KERNEL: one hot `pick()` that returns an endpoint
|
|
11
|
+
INDEX over a fixed pool and allocates zero bytes on the steady-state path. It is a pure
|
|
12
|
+
selector, never a proxy -- it CONSUMES health and circuit state (it never owns them),
|
|
13
|
+
reading pre-allocated views that siblings or the caller write, and returning an integer.
|
|
14
|
+
The complementary evidence lite-pick ships is a measured balance-quality anchor (peak-to-
|
|
15
|
+
average load vs the strategy's theoretical ceiling) alongside the 0 B/op pick witness.
|
|
16
|
+
|
|
17
|
+
0.1.0 ships the substrate seams + the FIRST strategy, RoundRobin. It exports `VERSION`, the
|
|
18
|
+
fail-closed sentinel `PICK_NONE` (-1), a deterministic `Prng` (xorshift32), `BalancerBase`
|
|
19
|
+
(the shared read-only eligibility seam + O(1) live count), and `RoundRobinBalancer`. The
|
|
20
|
+
remaining strategies land one per session (see ROADMAP.md): SmoothWRR, P2C, LeastConn/SED/NQ,
|
|
21
|
+
PeakEWMA, ConsistentHash, BoundedLoad, WeightedRandom.
|
|
22
|
+
|
|
23
|
+
## Design ownership (decisions/0001, 0002)
|
|
24
|
+
|
|
25
|
+
- IN-PROCESS first (workers, DI services); remote HTTP is a thin optional adapter.
|
|
26
|
+
- Eligibility is a SHARED read-only `Uint8Array` (1 = pickable, 0 = down), WRITTEN by
|
|
27
|
+
@zakkster/lite-di-health / circuit breakers, only READ by `pick()`. Zero-copy.
|
|
28
|
+
- Load counters are CALLER-OWNED typed arrays (`Uint32Array` inflight, `Float64Array`
|
|
29
|
+
rtt/EWMA); the kernel holds no request state. `pick()` is pure-read.
|
|
30
|
+
- The circuit breaker is CONSUMED (@zakkster/lite-statechart), never built in.
|
|
31
|
+
- Fail-closed: whole pool down -> `pick()` returns `PICK_NONE` (-1), never a dead pick.
|
|
32
|
+
- Anti-flapping is a first-class rule: routing-STATE transitions apply hysteresis/dwell;
|
|
33
|
+
`pick()` itself stays greedy and stateless.
|
|
34
|
+
|
|
35
|
+
## Exports (from the single main file Pick.js)
|
|
36
|
+
|
|
37
|
+
- `VERSION` -- string. The source-of-truth version stamp (synced with package.json + this file).
|
|
38
|
+
- `PICK_NONE` -- the -1 fail-closed sentinel returned by `pick()` when no endpoint is eligible.
|
|
39
|
+
- `Prng` -- class. Instance-local deterministic xorshift32.
|
|
40
|
+
- `new Prng(seed = 0x9e3779b9)` -- 0 is remapped to the default.
|
|
41
|
+
- `next()` -> uint32 in [1, 2^32). One xorshift32 step, zero-alloc.
|
|
42
|
+
- `nextBelow(n)` -> uint32 in [0, n).
|
|
43
|
+
- `reset()` -> void. Replays the original seed's stream (reproducible benches).
|
|
44
|
+
- `BalancerBase` -- class. The shared eligibility seam; strategies (M1+) subclass it.
|
|
45
|
+
- `new BalancerBase(capacity, eligible)` -- `eligible` is a shared `Uint8Array`
|
|
46
|
+
(length >= capacity) owned/written externally; throws `RangeError` on a bad capacity
|
|
47
|
+
or an undersized / non-Uint8Array view.
|
|
48
|
+
- `capacity` -- readonly number. Fixed endpoint count.
|
|
49
|
+
- `live` -- readonly number. Currently eligible endpoints (O(1), cold-path maintained).
|
|
50
|
+
- `isEligible(i)` -> boolean. O(1); out-of-range is `false`, never a throw.
|
|
51
|
+
- `setEligible(i, up)` -> void. Cold path; keeps `live` exact and is idempotent; throws
|
|
52
|
+
`RangeError` on an out-of-range index.
|
|
53
|
+
- `pick()` -> number. ABSTRACT in the base (throws); overridden by each strategy.
|
|
54
|
+
- `RoundRobinBalancer extends BalancerBase` -- class. The baseline strategy (M1).
|
|
55
|
+
- `new RoundRobinBalancer(capacity, eligible)` -- same shared-eligibility contract as the base.
|
|
56
|
+
- `pick()` -> number. Next eligible index in round-robin order (a wrapping cursor that
|
|
57
|
+
forward-scans the eligibility view, skipping down nodes), or `PICK_NONE` when the pool is
|
|
58
|
+
down. O(1) amortized, O(cap) worst case under sparse eligibility, 0 B/op. Owns only its
|
|
59
|
+
cursor; no eligible-set structure (ADR 0003 chose the stateless bitmap-scan path for M1).
|
|
60
|
+
|
|
61
|
+
## Gates (every session)
|
|
62
|
+
|
|
63
|
+
- `npm run torture` -- `node --expose-gc test/torture.mjs`: lite-leak retention +
|
|
64
|
+
lite-gc-profiler 0 B/op on the hot path.
|
|
65
|
+
- `npm run test:perf` -- `node --expose-gc --max-semi-space-size=4 --test test/perf/PerfGate.test.mjs`:
|
|
66
|
+
lite-perf-gate `zgcSuite` HARD zero-alloc gate + a `mustFail` teeth-check.
|
|
67
|
+
- `npm run witness` -- pick throughput flatness across a pool-size sweep.
|
|
68
|
+
- `npm run balance` -- peak-to-average load vs the strategy ceiling + random foil (the anchor).
|
|
69
|
+
- `npm test` / `npm run test:types` -- node:test boundary suite + tsc type-surface check.
|
|
70
|
+
|
|
71
|
+
## Composes with
|
|
72
|
+
|
|
73
|
+
@zakkster/lite-di-health (eligibility writer), lite-statechart (breaker), lite-o1
|
|
74
|
+
(RandomSet / AliasTable / RingLog substrate), lite-logn (exact least-conn heap / Fenwick
|
|
75
|
+
weights), lite-lru (sticky affinity), lite-fastbit32 (optional small-pool bitset peer),
|
|
76
|
+
lite-query (the fetcher adapter), lite-await (hedging), lite-worker-pool (in-process
|
|
77
|
+
consumer), lite-di-signal / lite-signal-decorators (observability). None is a HARD
|
|
78
|
+
dependency -- each is an OPTIONAL PEER dep (peerDependenciesMeta.optional, the LiteQuery
|
|
79
|
+
model), every seam is duck-typed over a shared TypedArray, and the kernel runs with zero
|
|
80
|
+
peers installed. A peer is declared only when a shipped code path imports it.
|
package/package.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zakkster/lite-pick",
|
|
3
|
+
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
+
"version": "0.1.0",
|
|
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
|
+
"type": "module",
|
|
7
|
+
"main": "./Pick.js",
|
|
8
|
+
"module": "./Pick.js",
|
|
9
|
+
"types": "./Pick.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./Pick.d.ts",
|
|
13
|
+
"node": "./Pick.js",
|
|
14
|
+
"import": "./Pick.js",
|
|
15
|
+
"default": "./Pick.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"Pick.js",
|
|
20
|
+
"Pick.d.ts",
|
|
21
|
+
"llms.txt",
|
|
22
|
+
"README.md",
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test test/*.test.js",
|
|
28
|
+
"test:types": "tsc -p test/types/tsconfig.json",
|
|
29
|
+
"torture": "node --expose-gc test/torture.mjs",
|
|
30
|
+
"witness": "node test/witness.mjs",
|
|
31
|
+
"balance": "node test/balance.mjs",
|
|
32
|
+
"test:perf": "node --expose-gc --max-semi-space-size=4 --test test/perf/PerfGate.test.mjs",
|
|
33
|
+
"bench": "node benchmark/Matrix.mjs",
|
|
34
|
+
"bench:report": "node benchmark/Matrix.mjs && node benchmark/Report.mjs",
|
|
35
|
+
"verify": "npm test && npm run test:types && npm run torture && npm run witness && npm run balance && npm run test:perf"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"load-balancer",
|
|
39
|
+
"load-balancing",
|
|
40
|
+
"load-balance",
|
|
41
|
+
"balancer",
|
|
42
|
+
"pick",
|
|
43
|
+
"selection",
|
|
44
|
+
"endpoint-selection",
|
|
45
|
+
"server-selection",
|
|
46
|
+
"round-robin",
|
|
47
|
+
"weighted-round-robin",
|
|
48
|
+
"smooth-weighted-round-robin",
|
|
49
|
+
"wrr",
|
|
50
|
+
"power-of-two-choices",
|
|
51
|
+
"p2c",
|
|
52
|
+
"two-random-choices",
|
|
53
|
+
"least-connections",
|
|
54
|
+
"least-conn",
|
|
55
|
+
"shortest-expected-delay",
|
|
56
|
+
"sed",
|
|
57
|
+
"never-queue",
|
|
58
|
+
"peak-ewma",
|
|
59
|
+
"latency-aware",
|
|
60
|
+
"consistent-hashing",
|
|
61
|
+
"maglev",
|
|
62
|
+
"bounded-load",
|
|
63
|
+
"weighted-random",
|
|
64
|
+
"sticky-routing",
|
|
65
|
+
"client-side-load-balancing",
|
|
66
|
+
"in-process",
|
|
67
|
+
"worker-pool",
|
|
68
|
+
"health-check",
|
|
69
|
+
"circuit-breaker",
|
|
70
|
+
"fail-closed",
|
|
71
|
+
"anti-flapping",
|
|
72
|
+
"hysteresis",
|
|
73
|
+
"deterministic",
|
|
74
|
+
"xorshift32",
|
|
75
|
+
"prng",
|
|
76
|
+
"zero-gc",
|
|
77
|
+
"zero-allocation",
|
|
78
|
+
"gc",
|
|
79
|
+
"garbage-collection",
|
|
80
|
+
"typed-array",
|
|
81
|
+
"performance",
|
|
82
|
+
"high-throughput",
|
|
83
|
+
"lightweight",
|
|
84
|
+
"tree-shakeable",
|
|
85
|
+
"esm",
|
|
86
|
+
"zero-dependency"
|
|
87
|
+
],
|
|
88
|
+
"license": "MIT",
|
|
89
|
+
"publishConfig": {
|
|
90
|
+
"access": "public"
|
|
91
|
+
},
|
|
92
|
+
"devDependencies": {
|
|
93
|
+
"@zakkster/lite-gc-profiler": "^1.16.0",
|
|
94
|
+
"@zakkster/lite-leak": "^1.10.0",
|
|
95
|
+
"@zakkster/lite-perf-gate": "^1.4.2",
|
|
96
|
+
"typescript": "^7.0.2"
|
|
97
|
+
},
|
|
98
|
+
"peerDependencies": {},
|
|
99
|
+
"peerDependenciesMeta": {},
|
|
100
|
+
"homepage": "https://github.com/PeshoVurtoleta/lite-pick#readme",
|
|
101
|
+
"repository": {
|
|
102
|
+
"type": "git",
|
|
103
|
+
"url": "git+https://github.com/PeshoVurtoleta/lite-pick.git"
|
|
104
|
+
},
|
|
105
|
+
"bugs": {
|
|
106
|
+
"url": "https://github.com/PeshoVurtoleta/lite-pick/issues",
|
|
107
|
+
"email": "shinikchiev@yahoo.com"
|
|
108
|
+
},
|
|
109
|
+
"engines": {
|
|
110
|
+
"node": ">=18"
|
|
111
|
+
},
|
|
112
|
+
"funding": {
|
|
113
|
+
"type": "github",
|
|
114
|
+
"url": "https://github.com/sponsors/PeshoVurtoleta"
|
|
115
|
+
},
|
|
116
|
+
"sideEffects": false
|
|
117
|
+
}
|