@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/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # @zakkster/lite-pick
2
2
 
3
- Version: 1.0.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.0 -- the ROSTER-COMPLETE release -- ships the substrate seams + TEN strategies: RoundRobin,
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`: `pick(keyHash)` sticks a key to its
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: when using
65
- BoundedLoad the mirrored counter is mutated ONLY through `note()` / /pool -- direct mutation desyncs
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 SHARED read-only `Uint8Array` (1 = pickable, 0 = down), WRITTEN by
105
- @zakkster/lite-di-health / circuit breakers, only READ by `pick()`. Zero-copy.
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 shared `Uint8Array`
124
- (length >= capacity) owned/written externally; throws `RangeError` on a bad capacity
125
- or an undersized / non-Uint8Array view.
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; keeps `live` exact and is idempotent; throws
130
- `RangeError` on an out-of-range index.
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 (lowest index on a tie), or `PICK_NONE` when the whole pool is down. 0 B/op. In a
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), lowest index on a tie,
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 time-constant /
186
- half-life in nanoseconds (finite, > 0). The per-endpoint EWMA state (`_ewma` / `_stamp`,
187
- Float64) is BALANCER-OWNED and written ONLY by `recordRtt`. Validates typeof-first BEFORE
188
- allocating (RangeError/TypeError). Cold start seeds the EWMA to 1.0 with an UNSAMPLED sentinel
189
- (`_stamp = -1`), so an unsampled node scores at its undecayed baseline -> graceful
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) and returns the lower `cost = (inflight + 1) * ewmaAt(now)`; a tie goes to the first
193
- draw. `now` is caller-supplied nanoseconds. Decays ON READ (never writes) -> O(d)=O(1), 0 B/op.
194
- `PICK_NONE` when the whole pool is down. A slow-but-up node (high EWMA rtt) is avoided even while
195
- idle -- the difference from P2C-over-inflight (latency anchor in balance.mjs).
196
- - `ewmaAt(i, now)` -> number. The EWMA rtt estimate for endpoint i at `now`. Pure read. An
197
- unsampled node (`_stamp < 0`) returns the baseline 1.0 undecayed; otherwise exponential decay.
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 nanoseconds, consistent with `pick(now)`. Validates
202
- typeof-first; 0 B/op on the success path. Anti-flap = the half-life, no extra dwell (ADR 0002,
203
- ADR 0009).
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, the mirrored inflight counter is mutated ONLY through `note` / /pool
241
- (direct mutation desyncs `_total` -- UB, the SmoothWRR-weights asymmetry).
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 / live`
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 in-flight on DISPATCH, awaits
307
- `fn(endpoint, signal)`, decrements on SETTLE (in a finally -- net-zero per run, even on throw).
308
- On a thrown error it keeps the failed endpoint's count ELEVATED and re-picks, so a load-aware
309
- strategy (P2C/LeastConn/SED/NQ) steers the next attempt to a DIFFERENT endpoint -- up to
310
- `opts.tries` attempts (default 1 = no failover), then rejects with the LAST error. Rejects with
311
- a `code:'LITE_PICK_NONE'` error when no endpoint is eligible. `opts.signal` is passed to `fn`;
312
- once aborted after a failure, failover stops and the abort propagates. NOT a 0 B/op path (the
313
- kernel `pick()` is): a normal async wrapper adding O(1) counter ops + one small per-run array.
314
- BOUNDARY: Pool owns SPATIAL failover (across the pool); the caller / query cache owns TEMPORAL
315
- retry (backoff, staleness). Never double-owned (decisions/0007). OPT-IN latency feedback: when
316
- `opts.clock` (a caller-owned nanosecond source) is supplied AND the balancer duck-types
317
- `recordRtt` (PeakEwmaBalancer), Pool drives `pick(now)` and records the settled rtt on success;
318
- otherwise the hook is inert -- Pool stays generic, in-flight stays net-zero, abort/failover
319
- unchanged. OPT-IN occupancy feedback: when the balancer duck-types `note` (BoundedLoadBalancer),
320
- Pool mirrors each dispatch as `note(i, +1)` and each settle as `note(i, -1)` so its owned mean
321
- stays current; inert otherwise. The hooks are independent -- a balancer may duck-type neither,
322
- one, or both; a BoundedLoad + Pool round is net-zero on BOTH the inflight array and `_total`.
323
- OPT-IN keyed routing: `opts.key` (a caller INTEGER) drives `pick(key)` for a keyed balancer
324
- (ConsistentHash / BoundedLoad -- CHBL); failover re-picks with the SAME key, and because the failed
325
- backend's occupancy stays elevated a CHBL re-pick naturally OVERFLOWS to the next backend. Without
326
- `opts.key`, `pick()` / `pick(now)` behaviour is unchanged.
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
- Imports nothing from lite-query -- duck-typed, so `peerDependencies` stays empty.
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 on the hot path.
336
- - `npm run test:perf` -- `node --expose-gc --max-semi-space-size=4 --test test/perf/PerfGate.test.mjs`:
337
- lite-perf-gate `zgcSuite` HARD zero-alloc gate + a `mustFail` teeth-check.
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 (eligibility writer), lite-statechart (breaker), lite-o1
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 HARD
354
- dependency -- each is an OPTIONAL PEER dep (peerDependenciesMeta.optional, the LiteQuery
355
- model), every seam is duck-typed over a shared TypedArray, and the kernel runs with zero
356
- peers installed. A peer is declared only when a shipped code path imports it.
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.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/*.test.js",
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=4 --test test/perf/PerfGate.test.mjs",
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",