@zakkster/lite-pick 0.7.1 → 0.7.2
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 +13 -0
- package/Pick.js +1 -1
- package/README.md +2 -0
- package/RECIPES.md +329 -0
- package/llms.txt +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to `@zakkster/lite-pick` are documented here. The format fol
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.7.2] - 2026-09-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `RECIPES.md` -- a beginner-to-advanced usage guide that builds the selection kernel up into a
|
|
12
|
+
real load balancer (health/eligibility wiring, caller-owned in-flight counters, the
|
|
13
|
+
dispatch/settle loop, `/pool` failover, PeakEWMA rtt feedback, the FE profile, a strategy
|
|
14
|
+
decision table, suite composition, zero-GC discipline, and gotchas). Added to the published
|
|
15
|
+
package (`files[]`) and linked from the README.
|
|
16
|
+
|
|
17
|
+
Docs-only release: no source or behavior change from 0.7.1 (the `VERSION` stamp is bumped for the
|
|
18
|
+
three-place sync).
|
|
19
|
+
|
|
7
20
|
## [0.7.1] - 2026-09-23
|
|
8
21
|
|
|
9
22
|
### Fixed
|
package/Pick.js
CHANGED
package/README.md
CHANGED
|
@@ -300,6 +300,8 @@ VERSION; // -> '0.6.0'
|
|
|
300
300
|
|
|
301
301
|
## Wiring it up -- `@zakkster/lite-pick/pool` (v0.5.0)
|
|
302
302
|
|
|
303
|
+
> **New to lite-pick as a load balancer?** [**RECIPES.md**](./RECIPES.md) is a beginner-to-advanced guide: it builds the kernel up into a real balancer step by step -- health/eligibility, load counters, the dispatch/settle loop, failover, latency feedback (PeakEWMA), the FE profile, and choosing a strategy. Start there; the sections below are the reference.
|
|
304
|
+
|
|
303
305
|
The kernel gives you `pick() -> index`. Real callers also need the counter ergonomics: **increment in-flight on dispatch, decrement on settle, and re-pick a *different* endpoint on failure.** That layer is async (it wraps the request), so it lives in a separate subpath -- `@zakkster/lite-pick/pool` -- and the kernel stays 0 B/op.
|
|
304
306
|
|
|
305
307
|
```js
|
package/RECIPES.md
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# lite-pick recipes -- from a one-liner to a real load balancer
|
|
2
|
+
|
|
3
|
+
`@zakkster/lite-pick` is a SELECTION KERNEL, not a proxy. It answers one question --
|
|
4
|
+
"which endpoint should this request go to?" -- and returns an integer index (or
|
|
5
|
+
`PICK_NONE` = -1 when nothing is eligible). A *real* load balancer is that kernel plus
|
|
6
|
+
the wiring around it:
|
|
7
|
+
|
|
8
|
+
- **eligibility** -- who is up? (a health check writes a shared bitmap)
|
|
9
|
+
- **load counters** -- how busy is each endpoint? (you own an `inflight` array)
|
|
10
|
+
- **the dispatch/settle loop** -- increment on send, decrement on finish
|
|
11
|
+
- **failover** -- if a call fails, try a different endpoint
|
|
12
|
+
- **latency feedback** -- for latency-aware routing, feed measured rtt back
|
|
13
|
+
|
|
14
|
+
These recipes build that wiring up, one layer at a time. Every array is preallocated
|
|
15
|
+
once and reused -- the pick path allocates 0 bytes.
|
|
16
|
+
|
|
17
|
+
Install: `npm i @zakkster/lite-pick` (zero runtime dependencies; ESM; Node >= 18)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. The 30-second version -- round-robin over a fixed pool
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { RoundRobinBalancer, PICK_NONE } from '@zakkster/lite-pick';
|
|
25
|
+
|
|
26
|
+
const CAP = 4; // fixed pool size
|
|
27
|
+
const eligible = new Uint8Array(CAP).fill(1); // 1 = up, 0 = down (all up here)
|
|
28
|
+
const lb = new RoundRobinBalancer(CAP, eligible);
|
|
29
|
+
|
|
30
|
+
const endpoints = ['a.svc:8080', 'b.svc:8080', 'c.svc:8080', 'd.svc:8080'];
|
|
31
|
+
|
|
32
|
+
for (let r = 0; r < 6; r++) {
|
|
33
|
+
const i = lb.pick(); // -> next eligible index, round-robin
|
|
34
|
+
if (i === PICK_NONE) throw new Error('pool is down');
|
|
35
|
+
send(endpoints[i]); // your transport; lite-pick never opens a socket
|
|
36
|
+
}
|
|
37
|
+
// picks: a, b, c, d, a, b
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`pick()` is the whole kernel. Everything below adds a capability around it.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. Fail closed -- always handle PICK_NONE
|
|
45
|
+
|
|
46
|
+
lite-pick never returns a down endpoint and never guesses. When the whole pool is
|
|
47
|
+
ineligible, `pick()` returns `PICK_NONE` (-1). Treat it as a first-class outcome:
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
const i = lb.pick();
|
|
51
|
+
if (i === PICK_NONE) {
|
|
52
|
+
// shed load, return 503, or fall back -- your policy. Never index endpoints[-1].
|
|
53
|
+
return respond503();
|
|
54
|
+
}
|
|
55
|
+
send(endpoints[i]);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`lb.live` is an O(1) count of eligible endpoints if you want to check before picking.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 3. Wire health -> eligibility (the shared bitmap)
|
|
63
|
+
|
|
64
|
+
Eligibility is a shared `Uint8Array` (1 = pickable, 0 = down). Something else writes it
|
|
65
|
+
-- a health checker, a circuit breaker, or `@zakkster/lite-di-health` -- and `pick()`
|
|
66
|
+
only reads it. Two ways to flip a node:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
// (a) write the bitmap directly if you own it elsewhere (zero-copy, pick sees it live):
|
|
70
|
+
eligible[2] = 0; // endpoint c is down
|
|
71
|
+
|
|
72
|
+
// (b) go through the balancer so its O(1) `live` count stays exact (recommended):
|
|
73
|
+
lb.setEligible(2, false); // COLD path; idempotent; keeps `live` correct
|
|
74
|
+
lb.setEligible(2, true); // back up
|
|
75
|
+
|
|
76
|
+
lb.isEligible(2); // -> boolean, HOT, out-of-range is false (never throws)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Prefer `setEligible` when you rely on `live`. Health flapping is the writer's problem:
|
|
80
|
+
apply hysteresis/dwell in the health layer -- `pick()` stays greedy and stateless.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 4. Weighted pools -- SmoothWRR
|
|
85
|
+
|
|
86
|
+
When endpoints have different capacities, weight them. `SmoothWRRBalancer` owns its
|
|
87
|
+
weight state; it is the SOLE writer -- always go through `setWeight`, never mutate the
|
|
88
|
+
array directly (direct mutation desyncs the internal total = undefined behavior).
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
import { SmoothWRRBalancer } from '@zakkster/lite-pick';
|
|
92
|
+
|
|
93
|
+
const weights = new Uint32Array(CAP); // the balancer manages these
|
|
94
|
+
const lb = new SmoothWRRBalancer(CAP, eligible, weights);
|
|
95
|
+
lb.setWeight(0, 5); // a is 5x
|
|
96
|
+
lb.setWeight(1, 1);
|
|
97
|
+
lb.setWeight(2, 1);
|
|
98
|
+
lb.setWeight(3, 1);
|
|
99
|
+
// pick() interleaves smoothly (nginx smooth WRR): a a b a c a a d ... not a a a a a b c d
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use SmoothWRR when weights are known/config-driven and change rarely.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 5. Load-aware selection -- you own the `inflight` counters
|
|
107
|
+
|
|
108
|
+
P2C, LeastConn, SED, and NQ route by *current load*. That load lives in a
|
|
109
|
+
caller-owned `Uint32Array` you increment on dispatch and decrement on settle. If you
|
|
110
|
+
don't maintain it, these strategies are blind (they see every node at 0).
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
import { P2cBalancer } from '@zakkster/lite-pick';
|
|
114
|
+
|
|
115
|
+
const inflight = new Uint32Array(CAP); // YOURS to maintain
|
|
116
|
+
const lb = new P2cBalancer(CAP, eligible, inflight);
|
|
117
|
+
|
|
118
|
+
async function handle(req) {
|
|
119
|
+
const i = lb.pick();
|
|
120
|
+
if (i === PICK_NONE) return respond503();
|
|
121
|
+
inflight[i]++; // DISPATCH
|
|
122
|
+
try {
|
|
123
|
+
return await send(endpoints[i], req);
|
|
124
|
+
} finally {
|
|
125
|
+
inflight[i]--; // SETTLE (always, even on error)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- **P2C** -- two random draws, pick the lighter. O(1), the scalable default; peak load
|
|
131
|
+
hugs the `ln ln n` band. Great from ~8 endpoints up.
|
|
132
|
+
- **LeastConn** -- exact fewest-in-flight (O(cap) scan). Best balance for small pools.
|
|
133
|
+
- **SED** / **NQ** -- weighted least-conn: pass a `weights` Uint32Array too;
|
|
134
|
+
`new SedBalancer(CAP, eligible, inflight, weights)`. NQ sends to an idle node first.
|
|
135
|
+
|
|
136
|
+
Maintaining the dispatch/settle loop by hand is easy to get wrong. Recipe 6 does it for
|
|
137
|
+
you.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 6. The real request loop -- `@zakkster/lite-pick/pool`
|
|
142
|
+
|
|
143
|
+
The `/pool` subpath wraps the kernel with the async dispatch/settle ergonomics so you
|
|
144
|
+
don't hand-maintain `inflight`. The kernel `pick()` stays 0 B/op; `Pool.run` is a normal
|
|
145
|
+
async wrapper on top.
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { P2cBalancer } from '@zakkster/lite-pick';
|
|
149
|
+
import { Pool } from '@zakkster/lite-pick/pool';
|
|
150
|
+
|
|
151
|
+
const inflight = new Uint32Array(CAP);
|
|
152
|
+
const lb = new P2cBalancer(CAP, eligible, inflight);
|
|
153
|
+
const pool = new Pool(lb, inflight); // SAME inflight array the balancer reads
|
|
154
|
+
|
|
155
|
+
// Pool does pick -> inflight++ -> await fn -> inflight-- (in a finally) for you:
|
|
156
|
+
const body = await pool.run((i, signal) => fetchFrom(endpoints[i], { signal }));
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`run(fn, opts?)` rejects with a `code: 'LITE_PICK_NONE'` error when the pool is down.
|
|
160
|
+
`fn(endpoint, signal)` receives the chosen index and the (optional) AbortSignal.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 7. Failover -- try a different endpoint on error
|
|
165
|
+
|
|
166
|
+
Set `tries > 1`. On a thrown error, Pool keeps the failed node's in-flight count
|
|
167
|
+
elevated and re-picks -- so a load-aware strategy naturally steers to a DIFFERENT
|
|
168
|
+
endpoint -- up to `tries` attempts, then rejects with the last error.
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
const body = await pool.run(
|
|
172
|
+
(i, signal) => fetchFrom(endpoints[i], { signal }),
|
|
173
|
+
{ tries: 3, signal: req.signal } // up to 3 distinct endpoints
|
|
174
|
+
);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Boundary: Pool owns **spatial** failover (move across the pool, once each, in-process).
|
|
178
|
+
The caller or your query cache owns **temporal** retry (backoff, staleness, dedup).
|
|
179
|
+
Don't double-own them. If `signal` aborts after a failure, failover stops and the abort
|
|
180
|
+
propagates.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 8. Latency-aware routing -- PeakEWMA with rtt feedback
|
|
185
|
+
|
|
186
|
+
PeakEWMA (latency-aware P2C, Finagle's peak-EWMA) steers away from *slow* endpoints,
|
|
187
|
+
not just busy ones. It scores each candidate `(inflight + 1) x ewma(rtt)`, so a node
|
|
188
|
+
that got slow gets less traffic even if its connection count looks fine. It needs two
|
|
189
|
+
things you didn't need before: a **clock** (`now`, caller-supplied nanoseconds) and
|
|
190
|
+
**rtt feedback** (`recordRtt`).
|
|
191
|
+
|
|
192
|
+
Manual loop:
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
import { PeakEwmaBalancer } from '@zakkster/lite-pick';
|
|
196
|
+
|
|
197
|
+
const TAU_NS = 30_000_000; // 30ms half-life for the EWMA decay
|
|
198
|
+
const inflight = new Uint32Array(CAP);
|
|
199
|
+
const lb = new PeakEwmaBalancer(CAP, eligible, inflight, TAU_NS);
|
|
200
|
+
const nowNs = () => Number(process.hrtime.bigint());
|
|
201
|
+
|
|
202
|
+
async function handle(req) {
|
|
203
|
+
const now = nowNs();
|
|
204
|
+
const i = lb.pick(now); // decay-on-read, 0 B/op
|
|
205
|
+
if (i === PICK_NONE) return respond503();
|
|
206
|
+
inflight[i]++;
|
|
207
|
+
const start = nowNs();
|
|
208
|
+
try {
|
|
209
|
+
return await send(endpoints[i], req);
|
|
210
|
+
} finally {
|
|
211
|
+
inflight[i]--;
|
|
212
|
+
lb.recordRtt(i, nowNs() - start, nowNs()); // FEEDBACK: measured rtt, snaps up / decays down
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Or let Pool do the feedback for you -- pass a `clock`; Pool drives `pick(now)` and calls
|
|
218
|
+
`recordRtt` on a successful settle when the balancer supports it:
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
import { Pool } from '@zakkster/lite-pick/pool';
|
|
222
|
+
const pool = new Pool(lb, inflight);
|
|
223
|
+
const body = await pool.run(
|
|
224
|
+
(i, signal) => fetchFrom(endpoints[i], { signal }),
|
|
225
|
+
{ clock: () => Number(process.hrtime.bigint()), tries: 2 }
|
|
226
|
+
);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Notes:
|
|
230
|
+
- **Cold start** (no samples yet) degrades gracefully to least-connections -- never NaN.
|
|
231
|
+
- `now` must be a FINITE number. A non-finite `now` degrades to P2C-random (no throw).
|
|
232
|
+
- Pick `tauNs` around your p50-p90 rtt: smaller = reacts faster to a slowdown, larger =
|
|
233
|
+
steadier. It IS the anti-flap smoothing; no extra dwell needed.
|
|
234
|
+
- Measured effect: with one node at 10x latency, PeakEWMA sends it a tiny fraction of
|
|
235
|
+
the traffic P2C-over-inflight would, and cuts service p99 sharply.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 9. Wire it into a query cache (lite-query, or any fetcher)
|
|
240
|
+
|
|
241
|
+
`liteQueryFetcher` adapts a Pool into a `({ key, signal }) => Promise` fetcher -- the
|
|
242
|
+
shape lite-query (or any cache/route-loader) expects. It imports nothing from lite-query
|
|
243
|
+
(duck-typed), so it works with any fetcher-shaped consumer.
|
|
244
|
+
|
|
245
|
+
```js
|
|
246
|
+
import { Pool, liteQueryFetcher } from '@zakkster/lite-pick/pool';
|
|
247
|
+
|
|
248
|
+
const pool = new Pool(lb, inflight);
|
|
249
|
+
const fetcher = liteQueryFetcher(
|
|
250
|
+
pool,
|
|
251
|
+
({ endpoint, key, signal }) => fetchFrom(endpoints[endpoint], { key, signal }),
|
|
252
|
+
{ tries: 2 }
|
|
253
|
+
);
|
|
254
|
+
// hand `fetcher` to your query cache; each cache miss fans out across the pool with failover.
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 10. Choosing a strategy
|
|
260
|
+
|
|
261
|
+
| Strategy | Route by | Cost | Reach for it when |
|
|
262
|
+
|---------------|---------------------|-------------|-------------------|
|
|
263
|
+
| RoundRobin | position | O(1) amort. | uniform endpoints, no load signal |
|
|
264
|
+
| SmoothWRR | static weight | O(cap) | known/config capacities, smooth interleave |
|
|
265
|
+
| P2C | in-flight (approx) | O(1) | the scalable default from ~8 nodes up |
|
|
266
|
+
| LeastConn | in-flight (exact) | O(cap) | small pools, tightest connection balance |
|
|
267
|
+
| SED | in-flight / weight | O(cap) | weighted least-conn |
|
|
268
|
+
| NQ | idle-first else SED | O(cap)/O(1) | worker pools -- never queue while a worker is free |
|
|
269
|
+
| PeakEWMA | in-flight x ewma(rtt)| O(1) | heterogeneous / flaky backends; steer around slow nodes |
|
|
270
|
+
|
|
271
|
+
All read the SAME `inflight` array live, so you can swap strategies without rewiring.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 11. The "FE profile" -- a browser / front-end client
|
|
276
|
+
|
|
277
|
+
For a front-end client picking among origins a handful of times per second (not a
|
|
278
|
+
zero-GC hot loop), the recommended profile is **PeakEWMA + health/eligibility only** --
|
|
279
|
+
latency-aware choice across origins with a fail-closed eligibility view -- and skip the
|
|
280
|
+
bounded-load / AZ / occupancy machinery. Feed rtt from your `fetch` timings via
|
|
281
|
+
`recordRtt`.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## 12. Compose with the suite (all optional, all duck-typed)
|
|
286
|
+
|
|
287
|
+
lite-pick declares ZERO hard dependencies and an EMPTY `peerDependencies`. Each seam is
|
|
288
|
+
a shared TypedArray or a duck-typed shape, so you wire in a sibling only if you use it:
|
|
289
|
+
|
|
290
|
+
- `@zakkster/lite-di-health` -- writes the eligibility bitmap from health checks.
|
|
291
|
+
- `@zakkster/lite-statechart` -- a circuit breaker that flips eligibility.
|
|
292
|
+
- `@zakkster/lite-query` -- the cache behind `liteQueryFetcher` (recipe 9).
|
|
293
|
+
- `@zakkster/lite-sketch` -- `DDSketch` for a p99-aware PeakEWMA variant (deferred).
|
|
294
|
+
- `@zakkster/lite-await` -- hedging (race the P2C second choice past a percentile).
|
|
295
|
+
|
|
296
|
+
None is required; the kernel runs over raw TypedArrays with nothing installed.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## 13. Zero-GC discipline (why the pick path stays 0 B/op)
|
|
301
|
+
|
|
302
|
+
- Allocate `eligible` / `inflight` / `weights` ONCE at startup and reuse them. Never
|
|
303
|
+
build arrays per pick.
|
|
304
|
+
- The counters are YOURS -- mutate them in place (`inflight[i]++/--`), don't replace them.
|
|
305
|
+
- `pick()` / `pick(now)` and `recordRtt` allocate nothing. The only async allocation is
|
|
306
|
+
the promise your own `fn` already creates (disclosed; `Pool.run` adds O(1) integer ops
|
|
307
|
+
plus one small per-run array).
|
|
308
|
+
- Fixed capacity: the pool size is set at construction and the backing arrays never
|
|
309
|
+
reallocate.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## 14. Gotchas
|
|
314
|
+
|
|
315
|
+
- **PICK_NONE (-1)** is always possible -- handle it before indexing (recipe 2).
|
|
316
|
+
- **SmoothWRR weights** must go through `setWeight`; direct array mutation is UB.
|
|
317
|
+
- **Load-aware strategies need the dispatch/settle loop** -- forget the `inflight--` in
|
|
318
|
+
a `finally` and load leaks upward forever. Use `/pool` (recipe 6) to avoid it.
|
|
319
|
+
- **PeakEWMA needs a finite `now`** and rtt feedback -- without `recordRtt` it behaves
|
|
320
|
+
like LeastConn (cold-start baseline).
|
|
321
|
+
- **Eligibility is read-only to `pick()`** -- the health layer writes it; the balancer
|
|
322
|
+
only reads (or maintains `live` via `setEligible`).
|
|
323
|
+
- **lite-pick is not a proxy** -- it returns an index; you own transport, retries/backoff
|
|
324
|
+
(temporal), health checking, and the socket.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
See also: `README.md` (overview + gates), `llms.txt` (full API surface),
|
|
329
|
+
`decisions/` (the ADRs behind each design call), `ROADMAP.md` (what's next).
|
package/llms.txt
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-pick",
|
|
3
3
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.2",
|
|
5
5
|
"description": "Zero-dependency, zero-GC load-balancing selection kernel: one hot pick() -> endpoint index over a fixed pool, 0 B/op steady-state. A pure selector (consumes health/circuit state, never a proxy) for the in-process hop, complementary to AWS NLB/ALB. Tree-shakeable ESM roster: RoundRobin, SmoothWRR, P2C, LeastConn, SED, NQ, PeakEWMA (latency-aware peak-EWMA), plus consistent hashing; the /pool subpath adds dispatch/settle counters + failover and a duck-typed query-cache fetcher.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./Pick.js",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"Pool.d.ts",
|
|
29
29
|
"llms.txt",
|
|
30
30
|
"README.md",
|
|
31
|
+
"RECIPES.md",
|
|
31
32
|
"CHANGELOG.md",
|
|
32
33
|
"LICENSE"
|
|
33
34
|
],
|