@zakkster/lite-pick 1.0.0 → 1.0.1
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 +138 -0
- package/GUIDE.md +14 -2
- package/Pick.d.ts +51 -25
- package/Pick.js +171 -57
- package/Pool.d.ts +63 -17
- package/Pool.js +275 -62
- package/README.md +35 -30
- package/RECIPES.md +132 -36
- package/llms.txt +101 -65
- package/package.json +18 -4
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-pick
|
|
2
2
|
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
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,7 +14,8 @@ 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
|
-
1.0.
|
|
17
|
+
1.0.1 -- the audit bug-fix release (CHANGELOG [1.0.1], decisions/0013) on top of
|
|
18
|
+
1.0.0 -- the ROSTER-COMPLETE release -- which ships the substrate seams + TEN strategies: RoundRobin,
|
|
18
19
|
SmoothWRR (the weighted default), P2C (power-of-two-choices -- also the O(1) least-connections
|
|
19
20
|
APPROXIMATION), the EXACT LeastConn family (LeastConn, SED, NQ), PeakEWMA (latency-aware P2C),
|
|
20
21
|
ConsistentHash (a Maglev lookup table -- sticky/affinity routing), BoundedLoad (Consistent Hashing
|
|
@@ -53,7 +54,9 @@ table -- lite-pick's WeightedRandom returns an endpoint INDEX, honours the share
|
|
|
53
54
|
|
|
54
55
|
M9 (0.9.0) adds BoundedLoadBalancer (decisions/0011): Consistent Hashing with Bounded Loads (CHBL --
|
|
55
56
|
Mirrokni et al. / Google Research; Vimeo eps=0.25). It is `ConsistentHashBalancer` (the Maglev table)
|
|
56
|
-
PLUS a per-backend occupancy cap `cap = (1 + eps) x _total / live
|
|
57
|
+
PLUS a per-backend occupancy cap `cap = ceil((1 + eps) x (_total + 1) / live)` (the +1 counts the INCOMING
|
|
58
|
+
request -- Mirrokni-Thorup-Zadimoghaddam per-bin capacity, so cap >= 1; NOT HAProxy's global-slot-by-weight
|
|
59
|
+
definition, which is stricter): `pick(keyHash)` sticks a key to its
|
|
57
60
|
hashed home UNLESS that backend is over cap, in which case the request OVERFLOWS along the same bounded
|
|
58
61
|
probe to the next eligible under-cap backend -- consistent hashing's stickiness + minimal disruption
|
|
59
62
|
PLUS the HOTSPOT protection plain CH lacks. If none in the window is under cap it FALLS BACK to the
|
|
@@ -61,8 +64,9 @@ first eligible (sticky wins; PICK_NONE is pool-down ONLY, never for over-cap); `
|
|
|
61
64
|
cap -> pure ConsistentHash. It extends ConsistentHashBalancer (reusing the Maglev build + probe +
|
|
62
65
|
setWeight/rebuild/tableSize VERBATIM) and OWNS a running `_total` whose SOLE writer is the warm
|
|
63
66
|
`note(i, delta)` seam (dispatch +1 / settle -1); `inflight` is the caller's Uint32Array read LIVE as
|
|
64
|
-
the per-backend occupancy. `pick()` and `note()` are both O(1) / 0 B/op. CONTRACT:
|
|
65
|
-
|
|
67
|
+
the per-backend occupancy. `pick()` and `note()` are both O(1) / 0 B/op. CONTRACT: update `inflight[i]`
|
|
68
|
+
AND call `note(i, +/-1)` in LOCKSTEP (or drive it through /pool, which does both). `note` maintains
|
|
69
|
+
`_total`; it does NOT write `inflight`. A direct `inflight` write without the matching `note` desyncs
|
|
66
70
|
`_total` (UB, the SmoothWRR-weights asymmetry). THE PIVOT (decisions/0011): P2C-over-inflight with a
|
|
67
71
|
`(1+eps) x mean` cap is byte-identical to plain P2C (an under-cap draw always has lower inflight than an
|
|
68
72
|
over-cap one), so the cap is only LOAD-BEARING when the primary choice is a HASH -- CHBL is that. The
|
|
@@ -101,8 +105,12 @@ the contract + balance + tail -- never an "N times faster" headline (decisions/0
|
|
|
101
105
|
## Design ownership (decisions/0001, 0002)
|
|
102
106
|
|
|
103
107
|
- IN-PROCESS first (workers, DI services); remote HTTP is a thin optional adapter.
|
|
104
|
-
- Eligibility is a
|
|
105
|
-
|
|
108
|
+
- Eligibility is a `Uint8Array` (1 = pickable, 0 = down) READ by `pick()`. At runtime it is
|
|
109
|
+
flipped ONLY through `setEligible(i, up)` (the sole supported writer, which keeps the cached
|
|
110
|
+
`live` count -- and SmoothWRR's eligible-weight total -- exact); @zakkster/lite-di-health /
|
|
111
|
+
circuit breakers DRIVE that call. A direct byte write desyncs the cache (fail-closed picks,
|
|
112
|
+
wrong ratios) -- UB. Each balancer needs its OWN eligibility array (a shared `Eligibility`
|
|
113
|
+
value object is deferred to 2.0; ADR 0001, amended 1.0.1). No second internal copy.
|
|
106
114
|
- Load counters are CALLER-OWNED typed arrays (`Uint32Array` inflight, `Float64Array`
|
|
107
115
|
rtt/EWMA); the kernel holds no request state. `pick()` is pure-read.
|
|
108
116
|
- The circuit breaker is CONSUMED (@zakkster/lite-statechart), never built in.
|
|
@@ -120,14 +128,15 @@ the contract + balance + tail -- never an "N times faster" headline (decisions/0
|
|
|
120
128
|
- `nextBelow(n)` -> uint32 in [0, n).
|
|
121
129
|
- `reset()` -> void. Replays the original seed's stream (reproducible benches).
|
|
122
130
|
- `BalancerBase` -- class. The shared eligibility seam; strategies (M1+) subclass it.
|
|
123
|
-
- `new BalancerBase(capacity, eligible)` -- `eligible` is a
|
|
124
|
-
(
|
|
125
|
-
|
|
131
|
+
- `new BalancerBase(capacity, eligible)` -- `eligible` is a `Uint8Array` (length >= capacity),
|
|
132
|
+
per-balancer (not shared across balancers); throws `RangeError` on a bad capacity or an
|
|
133
|
+
undersized / non-Uint8Array view. Flip it ONLY through `setEligible` after construction.
|
|
126
134
|
- `capacity` -- readonly number. Fixed endpoint count.
|
|
127
135
|
- `live` -- readonly number. Currently eligible endpoints (O(1), cold-path maintained).
|
|
128
|
-
- `isEligible(i)` -> boolean. O(1); out-of-range is `false`, never a throw.
|
|
129
|
-
- `setEligible(i, up)` -> void. Cold path;
|
|
130
|
-
`RangeError` on
|
|
136
|
+
- `isEligible(i)` -> boolean. O(1); out-of-range OR a non-integer (1.5, NaN) is `false`, never a throw.
|
|
137
|
+
- `setEligible(i, up)` -> void. Cold path; the ONLY supported eligibility writer -- keeps `live`
|
|
138
|
+
exact and is idempotent; throws `RangeError` on a non-integer or out-of-range index (incl. a
|
|
139
|
+
numeric string like '2'). A direct `eligible[i]` write desyncs `live` (UB).
|
|
131
140
|
- `pick()` -> number. ABSTRACT in the base (throws); overridden by each strategy.
|
|
132
141
|
- `RoundRobinBalancer extends BalancerBase` -- class. The baseline strategy (M1).
|
|
133
142
|
- `new RoundRobinBalancer(capacity, eligible)` -- same shared-eligibility contract as the base.
|
|
@@ -162,7 +171,8 @@ the contract + balance + tail -- never an "N times faster" headline (decisions/0
|
|
|
162
171
|
(length >= capacity), read LIVE each pick. No `setWeight`, no derived aggregate: the caller may
|
|
163
172
|
mutate inflight directly between picks (that is the shared-counter seam).
|
|
164
173
|
- `pick()` -> number. Full O(cap) scan returning the eligible node with the fewest in-flight
|
|
165
|
-
requests (
|
|
174
|
+
requests (tie order UNSPECIFIED in 1.0.1 -- deterministic but do not depend on it; a rotating
|
|
175
|
+
tie-break is planned for 1.1.0), or `PICK_NONE` when the whole pool is down. 0 B/op. In a
|
|
166
176
|
feedback loop (increment on dispatch, decrement on settle) it is greedy-optimal: max-minus-min
|
|
167
177
|
load stays within 1 (balance.mjs). Without feedback it returns the same lowest-load index --
|
|
168
178
|
correct by contract; the M5 lite-query adapter provides the increment/decrement.
|
|
@@ -171,7 +181,8 @@ the contract + balance + tail -- never an "N times faster" headline (decisions/0
|
|
|
171
181
|
caller-owned Uint32Arrays (length >= capacity), read LIVE (no `setWeight`, no derived total --
|
|
172
182
|
the caller may retune weights directly, UNLIKE SmoothWRR).
|
|
173
183
|
- `pick()` -> number. O(cap) scan returning the eligible, positive-weight node minimizing
|
|
174
|
-
`(inflight + 1) / weight` (the new request's marginal expected delay),
|
|
184
|
+
`(inflight + 1) / weight` (the new request's marginal expected delay), tie order UNSPECIFIED
|
|
185
|
+
(1.0.1; rotating tie-break planned for 1.1.0),
|
|
175
186
|
or `PICK_NONE` when no eligible node has a positive weight. A weight-0 eligible node is NOT a
|
|
176
187
|
candidate. Converges to load proportional-to-weight (balance.mjs). 0 B/op.
|
|
177
188
|
- `NqBalancer extends BalancerBase` -- class. Never-queue (M4, IPVS `nq`); the worker-pool fit.
|
|
@@ -182,25 +193,33 @@ the contract + balance + tail -- never an "N times faster" headline (decisions/0
|
|
|
182
193
|
O(cap) worst case, O(1) when an early node is idle. 0 B/op.
|
|
183
194
|
- `PeakEwmaBalancer extends BalancerBase` -- class. Latency-aware P2C (M7, Finagle peak-EWMA).
|
|
184
195
|
- `new PeakEwmaBalancer(capacity, eligible, inflight, tauNs, seed?=0x9e3779b9)` -- `inflight` is a
|
|
185
|
-
caller-owned Uint32Array (length >= capacity) read LIVE; `tauNs` is the EWMA
|
|
186
|
-
|
|
187
|
-
Float64)
|
|
188
|
-
|
|
189
|
-
(`
|
|
190
|
-
least-connections regardless of clock magnitude (never underflows to 0).
|
|
196
|
+
caller-owned Uint32Array (length >= capacity) read LIVE; `tauNs` is the EWMA TIME CONSTANT in
|
|
197
|
+
nanoseconds (finite, > 0; half-life = tauNs x ln2). The per-endpoint EWMA state (`_ewma` /
|
|
198
|
+
`_stamp`, Float64) plus a lifetime running-mean-of-samples pair are BALANCER-OWNED and written
|
|
199
|
+
ONLY by `recordRtt`. Cold start seeds `_ewma` to 1.0 with an UNSAMPLED sentinel (`_stamp = -1`).
|
|
200
|
+
LATENCY marker (`static LATENCY = true`): /pool REQUIRES `opts.clock` for this strategy.
|
|
191
201
|
- `pick(now)` -> number. Draws two DISTINCT eligible endpoints (ADR 0005's rejection sampling,
|
|
192
|
-
reused)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
reused), lower COST wins; a tie goes to the first draw. Per-candidate cost (pure read, 0 B/op):
|
|
203
|
+
an unsampled node costs 0 WHILE IDLE (graceful least-connections; holds one probe in flight
|
|
204
|
+
until its first sample) and `(inflight+1) x lifetime-mean-sampled-rtt ONCE BUSY` (so a
|
|
205
|
+
cold-but-busy node is NOT the old 1.0 ns black hole); a sampled node costs
|
|
206
|
+
`(inflight+1) x max(decayedEWMA, dt)` while busy (a hung node -- dt grows, no completion -- gets
|
|
207
|
+
MORE expensive), else `(inflight+1) x decayedEWMA`. `dt = max(now - stamp, 0)` (clamped, L6).
|
|
208
|
+
`now` is caller-supplied nanoseconds. Decays ON READ -> O(d)=O(1), 0 B/op. `PICK_NONE` when the
|
|
209
|
+
whole pool is down. CAVEAT: an idle-then-busy node is priced by time-since-last-response until
|
|
210
|
+
that response completes (exact busy-since stamp = 1.1.0). A node that fails fast and records
|
|
211
|
+
nothing keeps winning while idle -- record failures (the /pool failure penalty does).
|
|
212
|
+
- `ewmaAt(i, now)` -> number. The decayed EWMA rtt estimate for endpoint i at `now`. Pure read. An
|
|
213
|
+
unsampled node (`_stamp < 0`) returns the baseline 1.0 undecayed; otherwise
|
|
214
|
+
`_ewma[i] x exp(-max(now - stamp, 0)/tau)`.
|
|
198
215
|
- `recordRtt(i, sampleNs, now)` -> void. WARM feedback path (not the hot pick path): the FIRST
|
|
199
216
|
sample initializes the EWMA EXACTLY to `sampleNs` (clock-magnitude-independent); thereafter the
|
|
200
|
-
Finagle peak rule -- the cost SNAPS UP to a larger sample instantly and DECAYS DOWN over ~tau
|
|
201
|
-
`now` / `sampleNs` are caller-supplied
|
|
202
|
-
|
|
203
|
-
|
|
217
|
+
Finagle peak rule -- the cost SNAPS UP to a larger sample instantly and DECAYS DOWN over ~tau --
|
|
218
|
+
and it folds the sample into the lifetime mean. `now` / `sampleNs` are caller-supplied
|
|
219
|
+
nanoseconds, consistent with `pick(now)`. THROWS `RangeError` on a non-integer / out-of-range
|
|
220
|
+
index (incl. a numeric string like '2' -- which used to work; a string index now throws
|
|
221
|
+
RangeError, not TypeError) or a non-finite/negative `sampleNs`; 0 B/op on the success path.
|
|
222
|
+
Anti-flap = the time constant, no extra dwell (ADR 0002, ADR 0009).
|
|
204
223
|
- CONTRACT: `now` and `sampleNs` MUST be FINITE numbers. `recordRtt` THROWS on a non-finite
|
|
205
224
|
argument (warm path); `pick(now)` NEVER throws (fail-closed contract), so a non-finite `now`
|
|
206
225
|
yields P2C-random selection, not an error.
|
|
@@ -237,10 +256,13 @@ the contract + balance + tail -- never an "N times faster" headline (decisions/0
|
|
|
237
256
|
BEFORE super() allocates the Maglev table (TypeError non-number eps, RangeError non-finite / <= 0).
|
|
238
257
|
`weights` / `m` / `seed` are the ConsistentHash args (copied weights, prime m >= capacity, COLD
|
|
239
258
|
build). The running occupancy sum `_total` is BALANCER-OWNED (starts at 0) and written SOLELY by
|
|
240
|
-
`note`; when using BoundedLoad,
|
|
241
|
-
|
|
259
|
+
`note`; when using BoundedLoad, update `inflight[i]` AND call `note(i, +/-1)` in LOCKSTEP (or drive
|
|
260
|
+
it through /pool). `note` maintains `_total`; it does NOT write `inflight`. A direct `inflight` write
|
|
261
|
+
without the matching `note` desyncs `_total` -- UB, the SmoothWRR-weights asymmetry.
|
|
242
262
|
- `pick(keyHash)` -> number. slot = (keyHash >>> 0) % M; walk the probe window (home + CH_PROBE_LIMIT
|
|
243
|
-
slots) and return the FIRST backend that is ELIGIBLE AND under `cap = (1 + eps) * _total
|
|
263
|
+
slots) and return the FIRST backend that is ELIGIBLE AND under `cap = ceil((1 + eps) * (_total + 1)
|
|
264
|
+
/ live)` (the +1 counts the incoming request -- MTZ per-bin capacity, cap >= 1; NOT HAProxy's
|
|
265
|
+
stricter global-slot-by-weight definition)
|
|
244
266
|
(a hot home OVERFLOWS to a neighbour). If none in the window is under cap, fall back to the FIRST
|
|
245
267
|
eligible seen (sticky wins; the cap is a soft preference). `_total === 0` skips the cap -> pure
|
|
246
268
|
ConsistentHash. O(1), 0 B/op, NEVER throws. `PICK_NONE` ONLY when no eligible backend is reachable
|
|
@@ -303,38 +325,52 @@ duck-typed and imports NOTHING from lite-query.
|
|
|
303
325
|
live }` -- every lite-pick strategy qualifies); `inflight` is the SAME caller-owned Uint32Array
|
|
304
326
|
the balancer reads (length >= balancer.capacity). Throws on a bad balancer / undersized view.
|
|
305
327
|
- `balancer` / `inflight` -- readonly getters.
|
|
306
|
-
- `run(fn, opts?)` -> Promise. Picks an endpoint, increments its
|
|
307
|
-
`fn(endpoint, signal)`, decrements on SETTLE (in a finally --
|
|
308
|
-
On a thrown error it keeps the failed endpoint's count ELEVATED
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
328
|
+
- `run(fn, opts?)` -> Promise. `opts` may be omitted or `null`. Picks an endpoint, increments its
|
|
329
|
+
in-flight on DISPATCH, awaits `fn(endpoint, signal)`, decrements on SETTLE (in a finally --
|
|
330
|
+
net-zero per run, even on throw). On a thrown error it keeps the failed endpoint's count ELEVATED
|
|
331
|
+
and fails over to a GENUINELY DISTINCT endpoint (M2): re-pick while the strategy repeats a tried
|
|
332
|
+
endpoint (bounded), then scan for an eligible UNTRIED one (key-derived start for a keyed run,
|
|
333
|
+
cursor-rotated otherwise). It STOPS with the last error as soon as no untried eligible endpoint
|
|
334
|
+
remains -- so a 1-node pool with `tries: 3` makes ONE attempt (Pool owns SPATIAL failover, not
|
|
335
|
+
temporal retry). `opts.tries` default 1 = no failover. Rejects `code:'LITE_PICK_NONE'` when no
|
|
336
|
+
endpoint is eligible.
|
|
337
|
+
ABORT (M-Item5): `opts.signal` is checked before EVERY attempt -- an already-aborted signal
|
|
338
|
+
dispatches NOTHING and rejects with the signal's `reason`, else `code:'LITE_PICK_ABORTED'`; an
|
|
339
|
+
abort after a failure stops failover and propagates.
|
|
340
|
+
CHANNELS (M3), read off static markers (Pool imports nothing new): a KEYED balancer
|
|
341
|
+
(`constructor.KEYED === true`: ConsistentHash/BoundedLoad) REQUIRES a numeric `opts.key`
|
|
342
|
+
(`code:'LITE_PICK_KEY_REQUIRED'` otherwise) and Pool drives `pick(key)`; a LATENCY balancer
|
|
343
|
+
(`constructor.LATENCY === true`: PeakEWMA) REQUIRES an `opts.clock`
|
|
344
|
+
(`code:'LITE_PICK_CLOCK_REQUIRED'` otherwise), read (validated finite) BEFORE dispatch and passed
|
|
345
|
+
as `pick(now)`. The KEY reaches ONLY a keyed pick; the CLOCK reading NEVER reaches a keyed pick; a
|
|
346
|
+
non-keyed clocked run passes the reading to `pick(now)`.
|
|
347
|
+
FEEDBACK is loud and never re-runs fn (M4): occupancy `note(i, +/-1)` mirrors dispatch/settle for
|
|
348
|
+
a note-duck-typed balancer; latency `recordRtt` is fed on settle when a clock is in use. A SUCCESS
|
|
349
|
+
whose settle feedback fails (clock throws / non-finite, or `recordRtt` throws) rejects
|
|
350
|
+
`code:'LITE_PICK_FEEDBACK'` carrying `.cause` and `.result` (fn's value -- fn ran once, nothing
|
|
351
|
+
lost). A FAILURE feeds a penalty `recordRtt(i, max(elapsed, failurePenaltyNs), done)` (H1); if THAT
|
|
352
|
+
feedback fails, Pool throws fn's original error unchanged (identity) with a non-enumerable
|
|
353
|
+
`liteFeedbackError` attached. A non-finite clock reading throws BEFORE dispatch; a
|
|
354
|
+
backwards-stepping finite reading records NO sample and resolves normally. `opts.failurePenaltyNs`
|
|
355
|
+
(finite > 0, default 1e9) is the minimum penalty rtt; a penalized node is re-probed roughly every
|
|
356
|
+
`tauNs x ln(failurePenaltyNs / healthyRttNs)`. NOT a 0 B/op path (the kernel `pick()` is): a normal
|
|
357
|
+
async wrapper adding O(1) counter ops + one small per-run array. BOUNDARY: Pool owns SPATIAL
|
|
358
|
+
failover; the caller / query cache owns TEMPORAL retry (decisions/0007).
|
|
327
359
|
- `liteQueryFetcher(pool, perEndpoint, opts?)` -> a `({ key, signal }) => Promise` fetcher for a
|
|
328
360
|
query cache (lite-query's `fetcher`, or any fetcher-shaped consumer). `perEndpoint({ endpoint,
|
|
329
|
-
key, signal })` -> the per-endpoint work. `opts.tries` (default 1) is the spatial failover count
|
|
330
|
-
|
|
361
|
+
key, signal })` -> the per-endpoint work. `opts.tries` (default 1) is the spatial failover count;
|
|
362
|
+
`opts.clock` and `opts.failurePenaltyNs` are forwarded to `pool.run` (they feed a latency-aware
|
|
363
|
+
balancer). `ctx.key` is the query-cache key (arbitrary), NOT the integer routing key -- so this
|
|
364
|
+
generic adapter does not drive `pick(key)`; supply routing keys via `pool.run` directly. Imports
|
|
365
|
+
nothing from lite-query -- duck-typed, so `peerDependencies` stays empty.
|
|
331
366
|
|
|
332
367
|
## Gates (every session)
|
|
333
368
|
|
|
334
369
|
- `npm run torture` -- `node --expose-gc test/torture.mjs`: lite-leak retention +
|
|
335
|
-
lite-gc-profiler 0 B/op
|
|
336
|
-
- `npm run test:perf` -- `node --expose-gc --max-semi-space-size=
|
|
337
|
-
lite-perf-gate `zgcSuite`
|
|
370
|
+
lite-gc-profiler -- proves `pick()` RETAINS 0 B/op (reports the retained B/op).
|
|
371
|
+
- `npm run test:perf` -- `node --expose-gc --min-semi-space-size=1 --max-semi-space-size=1 --test
|
|
372
|
+
test/perf/PerfGate.test.mjs`: lite-perf-gate `zgcSuite` -- proves `pick()` ALLOCATES 0 B/op by
|
|
373
|
+
scavenge counting at a pinned 1 MB semi-space + a `mustFail` teeth-check.
|
|
338
374
|
- `npm run witness` -- pick throughput flatness across a pool-size sweep.
|
|
339
375
|
- `npm run balance` -- peak-to-average load vs the strategy ceiling + random foil (the anchor).
|
|
340
376
|
- `npm run fuzz` -- the seeded invariant fuzzer (the state-machine attack): strict-mode
|
|
@@ -343,14 +379,14 @@ duck-typed and imports NOTHING from lite-query.
|
|
|
343
379
|
|
|
344
380
|
## Composes with
|
|
345
381
|
|
|
346
|
-
@zakkster/lite-di-health (
|
|
382
|
+
@zakkster/lite-di-health (drives setEligible), lite-statechart (breaker), lite-o1
|
|
347
383
|
(RandomSet / AliasTable / RingLog substrate), lite-logn (exact least-conn heap / Fenwick
|
|
348
384
|
dynamic weights -- the mutable-weight complement to WeightedRandom's static alias table),
|
|
349
385
|
lite-random (a SEPARATE domain -- a GAME RNG for loot tables / particles that returns an ITEM,
|
|
350
386
|
NOT an eligibility-aware LB index selector; use lite-pick WeightedRandom for load balancing),
|
|
351
387
|
lite-lru (sticky affinity), lite-fastbit32 (optional small-pool bitset peer),
|
|
352
388
|
lite-query (the fetcher adapter), lite-await (hedging), lite-worker-pool (in-process
|
|
353
|
-
consumer), lite-di-signal / lite-signal-decorators (observability). None is a
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
389
|
+
consumer), lite-di-signal / lite-signal-decorators (observability). None is a dependency of
|
|
390
|
+
any kind -- `dependencies`, `peerDependencies` AND `peerDependenciesMeta` are all `{}`. Every
|
|
391
|
+
seam is duck-typed over a shared TypedArray, and the kernel runs with nothing else installed.
|
|
392
|
+
A peer would be declared only if a shipped code path imported it (none does today).
|
package/package.json
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-pick",
|
|
3
3
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.1",
|
|
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 of ten strategies: RoundRobin, SmoothWRR, P2C, LeastConn, SED, NQ, PeakEWMA (latency-aware peak-EWMA), ConsistentHash (Maglev sticky/affinity routing, minimal disruption), BoundedLoad (consistent hashing with bounded loads -- sticky routing with a per-backend occupancy cap that overflows a hotspot to neighbours), and WeightedRandom (O(1) Vose alias-table sampling with rejection-sampling eligibility); the /pool subpath adds dispatch/settle counters + failover and a duck-typed query-cache fetcher. See GUIDE.md to choose a strategy.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./Pick.js",
|
|
8
8
|
"module": "./Pick.js",
|
|
9
9
|
"types": "./Pick.d.ts",
|
|
10
|
+
"typesVersions": {
|
|
11
|
+
"*": {
|
|
12
|
+
"pool": [
|
|
13
|
+
"./Pool.d.ts"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
10
17
|
"exports": {
|
|
11
18
|
".": {
|
|
12
19
|
"types": "./Pick.d.ts",
|
|
@@ -34,13 +41,13 @@
|
|
|
34
41
|
"LICENSE"
|
|
35
42
|
],
|
|
36
43
|
"scripts": {
|
|
37
|
-
"test": "node --test test
|
|
38
|
-
"test:types": "tsc -p test/types/tsconfig.json",
|
|
44
|
+
"test": "node --test test/Base.test.js test/BoundedLoad.test.js test/ConsistentHash.test.js test/LeastConn.test.js test/NQ.test.js test/P2C.test.js test/PeakEWMA.test.js test/Pool.test.js test/RoundRobin.test.js test/SED.test.js test/SmoothWRR.test.js test/WeightedRandom.test.js test/suite-list.test.js",
|
|
45
|
+
"test:types": "tsc -p test/types/tsconfig.json && tsc -p test/types/tsconfig.es2022.json",
|
|
39
46
|
"torture": "node --expose-gc test/torture.mjs",
|
|
40
47
|
"witness": "node test/witness.mjs",
|
|
41
48
|
"balance": "node test/balance.mjs",
|
|
42
49
|
"fuzz": "node test/fuzz.mjs",
|
|
43
|
-
"test:perf": "node --expose-gc --max-semi-space-size=
|
|
50
|
+
"test:perf": "node --expose-gc --min-semi-space-size=1 --max-semi-space-size=1 --test test/perf/PerfGate.test.mjs",
|
|
44
51
|
"bench": "node benchmark/Matrix.mjs",
|
|
45
52
|
"bench:gc": "node --expose-gc benchmark/GcBlastRadius.mjs",
|
|
46
53
|
"bench:fairness": "node benchmark/Fairness.mjs",
|
|
@@ -49,6 +56,9 @@
|
|
|
49
56
|
"bench:verify": "node benchmark/Report.mjs --verify",
|
|
50
57
|
"soak": "node --expose-gc benchmark/Soak.mjs",
|
|
51
58
|
"demo": "node demo/fanout.mjs",
|
|
59
|
+
"scope": "node --expose-gc demo/pool-scope/tui.mjs",
|
|
60
|
+
"scope:frames": "node --expose-gc demo/pool-scope/tui.mjs --frames 40 --scenario flapstorm",
|
|
61
|
+
"scope:web": "node demo/pool-scope/web/serve.mjs",
|
|
52
62
|
"verify": "npm test && npm run test:types && npm run torture && npm run witness && npm run balance && npm run fuzz && npm run test:perf"
|
|
53
63
|
},
|
|
54
64
|
"keywords": [
|
|
@@ -111,9 +121,13 @@
|
|
|
111
121
|
"access": "public"
|
|
112
122
|
},
|
|
113
123
|
"devDependencies": {
|
|
124
|
+
"@zakkster/lite-adaptive": "^1.0.0",
|
|
125
|
+
"@zakkster/lite-charts": "^1.24.0",
|
|
114
126
|
"@zakkster/lite-gc-profiler": "^1.16.0",
|
|
115
127
|
"@zakkster/lite-leak": "^1.10.0",
|
|
116
128
|
"@zakkster/lite-perf-gate": "^1.4.2",
|
|
129
|
+
"@zakkster/lite-signal": "^1.5.2",
|
|
130
|
+
"@zakkster/lite-sketch": "^1.1.2",
|
|
117
131
|
"load-balancers": "1.3.52",
|
|
118
132
|
"loadbalance": "1.0.0",
|
|
119
133
|
"typescript": "^7.0.2",
|