diff --git a/.changeset/example-gateway.md b/.changeset/example-gateway.md new file mode 100644 index 0000000..4a0131f --- /dev/null +++ b/.changeset/example-gateway.md @@ -0,0 +1,17 @@ +--- +"resilix": patch +--- + +Adds a runnable example: `pnpm example:gateway`. + +A simulated LLM provider degrades from ~140ms to seconds **at a flat error rate**, starts +returning `429`s, then recovers, while ~25 requests/second flow through a full pipeline across +three tenants. It prints a per-second table so the adaptation is visible rather than described: +between 7s and 19s latency rises **23× while failures stay at 2**, and the limiter walks +concurrency from 25 down to 5 and sheds the excess before the timeouts start. + +Covers policy ordering, the verdict model, per-model isolation keys, tenant fairness, criticality +shedding, a shared retry budget, `ctx.mark()` for time-to-first-token, and `RejectedError.reason`. + +It is type-checked and run in CI (nine seconds at 4× speed) — an example that does not compile is +worse than no example, and it is the first code anyone reads. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f34109c..7978c86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,5 +27,10 @@ jobs: - run: pnpm test:paths - run: pnpm test:perf - run: pnpm test:compat + + # The example is the first code most people read, so a broken one is worse + # than none. Nine seconds at 4x speed traverses all four provider phases — + # enough to prove it runs, not enough to slow the build. + - run: RESILIX_EXAMPLE_MS=9000 RESILIX_EXAMPLE_SPEED=4 pnpm example:gateway - run: pnpm build - run: pnpm check:package diff --git a/README.md b/README.md index 7ecb9a9..1a87708 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,17 @@ because customers submitted bad input. resilix classifies outcomes into verdicts +## See it work + +```bash +pnpm example:gateway +``` + +A simulated provider degrades from ~140ms to seconds **at a flat error rate**, then recovers. +Watch the limiter walk concurrency down before any failures appear — +[examples/llm-gateway](examples/llm-gateway), written up at +[resilix.js.org/guide/example](https://resilix.js.org/guide/example). + ## Quick start diff --git a/biome.json b/biome.json index 9ebdec4..db07832 100644 --- a/biome.json +++ b/biome.json @@ -52,7 +52,7 @@ }, "overrides": [ { - "include": ["src/**/*.test.ts", "scripts/**"], + "include": ["src/**/*.test.ts", "scripts/**", "examples/**"], "linter": { "rules": { "style": { diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 3a947de..acc24dc 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -134,6 +134,7 @@ export default defineConfig({ items: [ { text: "What resilix is", link: "/guide/" }, { text: "Getting started", link: "/guide/getting-started" }, + { text: "A worked example", link: "/guide/example" }, { text: "The verdict model", link: "/guide/verdicts" }, ], }, diff --git a/docs/guide/example.md b/docs/guide/example.md new file mode 100644 index 0000000..ec8237e --- /dev/null +++ b/docs/guide/example.md @@ -0,0 +1,47 @@ +--- +description: "A runnable LLM gateway example: watch the adaptive limiter walk concurrency down as an upstream degrades at a flat error rate, then recover." +--- + +# A worked example + +Prose about adaptive concurrency limiting is hard to believe. This is the same thing as a program +you can run: + +```bash +git clone https://github.com/lintdeveloper/resilix +cd resilix && pnpm install +pnpm example:gateway +``` + +A simulated provider degrades from ~140ms to seconds **at a flat error rate**, starts returning +`429`s, then recovers. Around 25 requests/second flow through a full pipeline across three tenants. + +## The output + +``` + t phase limit inflight p90ms | ok 4xx shed fail + 7s healthy 25 0 143 | 148 27 0 0 +14s degrading 23 56 1186 | 190 34 68 2 +19s degrading 21 42 3358 | 203 38 190 2 +23s overloaded 5 36 7880 | 215 40 272 12 +``` + +Between 7s and 19s, **latency rises 23× while failures stay at 2**. A failure-rate circuit breaker +sees nothing in that window — it is watching errors, and there are none. The limiter is watching +latency, so it walks concurrency from 25 down to 5 and sheds the excess before the timeouts start. + +The `4xx` column is the [verdict model](./verdicts) doing its job: fifty healthy rejections from a +validating upstream, none of which counted against the breaker. + +## What each file shows + +| File | Use case | +|---|---| +| `gateway.ts` | policy ordering, verdicts, isolation key, tenant fairness, criticality, a shared retry budget | +| `run.ts` | `ctx.mark()` for time-to-first-token, and `RejectedError.reason` | +| `upstream.ts` | why concurrency is the right lever — latency there rises *with* concurrency | + +Source: [examples/llm-gateway](https://github.com/lintdeveloper/resilix/tree/main/examples/llm-gateway) + +It is a simulation with a seeded PRNG, tuned so the transitions are visible in under a minute — +the *shape* of the behaviour, not numbers to quote. diff --git a/examples/llm-gateway/README.md b/examples/llm-gateway/README.md new file mode 100644 index 0000000..c97b217 --- /dev/null +++ b/examples/llm-gateway/README.md @@ -0,0 +1,51 @@ +# Example — an LLM gateway + +```bash +pnpm example:gateway +``` + +A simulated provider that **degrades from ~140ms to seconds at a flat error rate**, starts pushing +back with `429`s, then recovers. Roughly 25 requests/second are offered through a resilix pipeline +across three tenants, one in five of them background work. + +It runs for 44 seconds. `RESILIX_EXAMPLE_MS` and `RESILIX_EXAMPLE_SPEED` compress it — CI runs it +for nine seconds at 4× purely to prove it still works. + +## What to watch + +The **`limit`** column against the **`p90ms`** and **`fail`** columns: + +``` + t phase limit inflight p90ms | ok 4xx shed fail + 7s healthy 25 0 143 | 148 27 0 0 +14s degrading 23 56 1186 | 190 34 68 2 ← 8x slower, still 2 failures +19s degrading 21 42 3358 | 203 38 190 2 ← 23x slower, still 2 failures +23s overloaded 5 36 7880 | 215 40 272 12 +``` + +Between 7s and 19s latency rises **23×** while failures stay at **2**. That is the incident this +library was built for, and a failure-rate circuit breaker sees nothing in that window — it is +watching errors, and there are none. The limiter is watching latency, so it walks concurrency down +from 25 to 21 to 5 and sheds the excess *before* the timeouts start. + +The **`4xx` column** is the other half. Fifty of those arrive across the run — a validating +upstream rejecting bad prompts. Every one is `answered`: healthy, never counted against the +breaker. A library whose failure predicate is *"did the promise reject?"* opens the circuit on +them, which is a self-inflicted outage on a working provider. + +## Which use case is where + +| File | What it demonstrates | +|---|---| +| `gateway.ts` | policy **ordering**, and why cheapest-refusal-first is not arbitrary | +| `gateway.ts` | **verdicts** — `classifyHttp`, so a `422` is `answered` and a `429` is `overload` | +| `gateway.ts` | **isolation key** per model, **tenant** fairness, **priority** so background sheds first | +| `gateway.ts` | a **shared retry budget** — one instance for the process, not one per pipeline | +| `run.ts` | **`ctx.mark()`** — latency is time to first token, not time to drain | +| `run.ts` | **`RejectedError.reason`**, so "why was I refused?" always has an answer | +| `upstream.ts` | why **concurrency** is the right lever: latency here rises with concurrency | + +## What it is not + +Not a benchmark. The provider is a simulation with a seeded PRNG, tuned so the transitions are +visible in under a minute. It shows the *shape* of the behaviour, not numbers you should quote. diff --git a/examples/llm-gateway/gateway.ts b/examples/llm-gateway/gateway.ts new file mode 100644 index 0000000..5112518 --- /dev/null +++ b/examples/llm-gateway/gateway.ts @@ -0,0 +1,74 @@ +/** + * The gateway: one pipeline, every policy resilix ships, wired the way you would + * actually wire them. + * + * Order matters and is not arbitrary. Cheapest and most decisive refusals go + * first, so an obviously-doomed call never occupies a slot it would only have to + * release: + * + * throttler → the upstream is refusing most of what we send; shed a fraction + * breaker → the upstream looks wholly down; fail fast + * limiter → it is up, but this is more concurrency than it can absorb + */ +import { + type Priority, + breaker, + budget, + classifyHttp, + limiter, + pipeline, + throttler, +} from "../../src/index.ts"; +import type { Reply } from "./upstream.ts"; + +export interface Job { + /** Which model — the isolation key. One bad model must not shed the others. */ + model: string; + /** Which customer, for fairness under pressure. */ + tenant: string; + /** Background work is shed before anything a user is waiting on. */ + background: boolean; +} + +/** + * ONE budget for the whole process. A per-pipeline cap cannot bound system-wide + * retry amplification, which is the entire point of having one. + */ +export const retryBudget = budget({ ratio: 0.1 }); + +export const gateway = pipeline({ + key: (job) => job.model, + tenant: (job) => job.tenant, + priority: (job): Priority => (job.background ? "bulk" : "critical"), + + // A Response is a value, not a throw, so the classifier must see it either + // way. This is what makes a 422 `answered` rather than a failure. + classify: classifyHttp, + + policies: [ + throttler(), + breaker({ + // ~3x the healthy p95. No default exists for this on purpose: "slow" is + // meaningless without your own baseline, and a wrong guess is worse than + // a required argument. + slowCallMs: 600, + slowCallRate: 0.5, + window: { calls: 60, minCalls: 8, maxAgeMs: 20_000 }, + openForMs: 4_000, + consecutiveBackstop: 12, + }), + limiter({ initialLimit: 16, minLimit: 4 }), + ], + + // Bounds the WHOLE sequence, not each attempt. Most libraries bound each + // attempt, so a caller asking for 12s can wait maxAttempts x (12s + backoff). + timeoutMs: 12_000, + retry: { maxAttempts: 3, jitter: "full", budget: retryBudget }, +}); + +/** Shape a provider reply the way the classifier expects to see it. */ +export const toResponse = (reply: Reply): Response => + new Response(null, { + status: reply.status, + headers: reply.retryAfterS ? { "retry-after": String(reply.retryAfterS) } : undefined, + }); diff --git a/examples/llm-gateway/run.ts b/examples/llm-gateway/run.ts new file mode 100644 index 0000000..7450426 --- /dev/null +++ b/examples/llm-gateway/run.ts @@ -0,0 +1,125 @@ +/** + * Drive traffic through the gateway and print what the policies are doing. + * + * pnpm example:gateway + * + * Watch the `limit` column. The provider degrades from ~120ms to seconds at a + * flat error rate; the limiter sees latency rise and walks the concurrency down + * before the errors start, and walks it back up on recovery. That adaptation is + * the thing that has no equivalent in npm, and it is hard to believe from prose. + */ +import { RejectedError, type RejectionReason } from "../../src/index.ts"; +import { type Job, gateway, toResponse } from "./gateway.ts"; +import { FakeProvider, sleep } from "./upstream.ts"; + +const MODEL = "gpt-oss-120b"; +const TENANTS = ["acme", "globex", "initech"] as const; +/** Default long enough to show degradation AND recovery; CI overrides it. */ +const RUN_MS = Number(process.env.RESILIX_EXAMPLE_MS ?? "44000"); + +const provider = new FakeProvider(); +const tally = { ok: 0, answered: 0, shed: 0, failed: 0 }; +const shedBy = new Map(); + +async function once(job: Job): Promise { + try { + const reply = await gateway.execute(job, async (ctx) => { + const r = await provider.call(); + // Time to first token, not time to drain. Judge a stream end-to-end and a + // healthy 45-second completion looks like saturation. + ctx.mark(); + return toResponse(r); + }); + if (reply.status === 200) tally.ok++; + else if (reply.status < 500) tally.answered++; + else tally.failed++; + } catch (error) { + if (error instanceof RejectedError) { + tally.shed++; + shedBy.set(error.reason, (shedBy.get(error.reason) ?? 0) + 1); + } else { + tally.failed++; + } + } +} + +function header(): void { + console.log( + "\n t phase limit inflight p90ms | ok 4xx shed fail | shed reason", + ); + console.log(` ${"─".repeat(88)}`); +} + +function row(second: number): void { + const m = gateway.metrics().find((x) => x.policy === "limiter")?.values ?? {}; + const top = [...shedBy.entries()].sort((a, b) => b[1] - a[1])[0]; + const cell = (v: number, w: number) => String(Math.round(v)).padStart(w); + const cols = [ + ` ${String(second).padStart(2)}s ${provider.phase().padEnd(11)}`, + cell(m.limit ?? 0, 6), + cell(m.inFlight ?? 0, 10), + cell(m.recentMs ?? 0, 8), + " |", + cell(tally.ok, 6), + cell(tally.answered, 5), + cell(tally.shed, 6), + cell(tally.failed, 6), + ` | ${top ? `${top[0]} x${top[1]}` : "—"}`, + ]; + console.log(cols.join("")); +} + +async function main(): Promise { + console.log(" resilix — LLM gateway example"); + console.log(" A provider that degrades at a FLAT error rate, then recovers."); + console.log(" Watch `limit` fall before the failures start, and rise again after."); + header(); + + const started = Date.now(); + const inFlight = new Set>(); + let tick = 0; + + const printer = setInterval(() => row(++tick), 1_000); + + while (Date.now() - started < RUN_MS) { + // ~25 requests/sec offered load, mixed tenants, one in five is background. + for (let i = 0; i < 5; i++) { + const job: Job = { + model: MODEL, + tenant: TENANTS[Math.floor(Math.random() * TENANTS.length)] ?? "acme", + background: Math.random() < 0.2, + }; + const p = once(job).finally(() => inFlight.delete(p)); + inFlight.add(p); + } + await sleep(200); + } + + clearInterval(printer); + await Promise.allSettled([...inFlight]); + + const total = tally.ok + tally.answered + tally.shed + tally.failed; + console.log(` ${"─".repeat(88)}`); + console.log(`\n ${total} requests offered\n`); + console.log(` ${String(tally.ok).padStart(5)} succeeded`); + console.log( + ` ${String(tally.answered).padStart(5)} answered 4xx — healthy, never opened the circuit`, + ); + console.log( + ` ${String(tally.shed).padStart(5)} shed by resilix, fast, without touching the provider`, + ); + console.log(` ${String(tally.failed).padStart(5)} failed`); + console.log("\n shed by reason:"); + for (const [reason, n] of [...shedBy.entries()].sort((a, b) => b[1] - a[1])) { + console.log(` ${String(n).padStart(5)} ${reason}`); + } + console.log(` + The 4xx column is the point of the verdict model: a boolean + 'did it reject?' breaker would have opened the circuit on those. +`); +} + +main().catch((error: unknown) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/examples/llm-gateway/upstream.ts b/examples/llm-gateway/upstream.ts new file mode 100644 index 0000000..35e9de1 --- /dev/null +++ b/examples/llm-gateway/upstream.ts @@ -0,0 +1,93 @@ +/** + * A simulated LLM provider that misbehaves the way real ones do. + * + * The point of the demo is the *transition*: a provider that stays healthy or + * stays broken teaches you nothing. This one degrades gradually, starts pushing + * back, then recovers — which is the shape resilix is built for and the shape a + * failure-rate breaker cannot see until it is too late. + */ + +export type Phase = "healthy" | "degrading" | "overloaded" | "recovering"; + +/** + * Compresses the whole scenario when the run is short. CI runs this for a few + * seconds purely to prove it does not crash; a human wants to watch it happen. + */ +export const SPEED = Number(process.env.RESILIX_EXAMPLE_SPEED ?? "1"); + +export interface Reply { + status: number; + /** Time to first token. The only latency that means anything for a stream. */ + ttfbMs: number; + retryAfterS?: number; +} + +/** Deterministic PRNG so two runs of the demo tell the same story. */ +const mulberry32 = (seed: number) => { + let state = seed | 0; + return () => { + state = (state + 0x6d2b79f5) | 0; + let t = Math.imul(state ^ (state >>> 15), 1 | state); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +}; + +export class FakeProvider { + private readonly rand = mulberry32(0xc0ffee); + private inFlight = 0; + + constructor(private readonly startedAt = Date.now()) {} + + phase(now = Date.now()): Phase { + const s = (now - this.startedAt) / 1000; + const t = s * SPEED; + if (t < 8) return "healthy"; + if (t < 18) return "degrading"; + if (t < 26) return "overloaded"; + return "recovering"; + } + + /** + * Serve one request. Latency rises with concurrency once degraded — which is + * what makes a concurrency limit the right lever rather than a rate limit. + */ + async call(now = Date.now()): Promise { + const phase = this.phase(now); + this.inFlight++; + try { + const queueing = Math.max(0, this.inFlight - 4); + let ttfb: number; + switch (phase) { + case "healthy": + ttfb = 90 + this.rand() * 60; + break; + case "degrading": + // The incident this library came from: ~25x slower at a FLAT error rate. + ttfb = 400 + queueing * 260 + this.rand() * 400; + break; + case "overloaded": + ttfb = 700 + queueing * 300 + this.rand() * 600; + break; + default: + ttfb = 140 + queueing * 40 + this.rand() * 120; + } + + await sleep(ttfb); + + // Healthy traffic contains a lot of 4xx — bad prompts, oversized inputs. + // A boolean "did it reject?" breaker opens on this. It must not. + if (this.rand() < 0.15) return { status: 422, ttfbMs: ttfb }; + if (phase === "overloaded" && this.rand() < 0.45) { + return { status: 429, ttfbMs: ttfb, retryAfterS: 2 }; + } + if (phase !== "healthy" && this.rand() < 0.05) return { status: 500, ttfbMs: ttfb }; + return { status: 200, ttfbMs: ttfb }; + } finally { + this.inFlight--; + } + } +} + +export const sleep = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/package.json b/package.json index 2091285..888074b 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "docs:check": "vitepress build docs && node scripts/check-links.mjs", "docs:preview": "vitepress preview docs", "dev": "tsup --watch", + "example:gateway": "tsx examples/llm-gateway/run.ts", "test": "vitest run", "test:paths": "node scripts/check-test-scripts.mjs", "test:watch": "vitest", @@ -127,6 +128,7 @@ "pg": "^8.23.0", "publint": "^0.3.2", "tsup": "^8.3.5", + "tsx": "^4.23.12", "typescript": "^5.7.2", "undici": "^8.10.0", "vitepress": "^1.6.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a371977..00f7d89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,7 +35,10 @@ importers: version: 0.3.23 tsup: specifier: ^8.3.5 - version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@5.9.3) + tsx: + specifier: ^4.23.12 + version: 4.23.12 typescript: specifier: ^5.7.2 version: 5.9.3 @@ -317,6 +320,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -329,6 +338,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -341,6 +356,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -353,6 +374,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -365,6 +392,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -377,6 +410,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -389,6 +428,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -401,6 +446,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -413,6 +464,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -425,6 +482,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -437,6 +500,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -449,6 +518,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -461,6 +536,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -473,6 +554,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -485,6 +572,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -497,6 +590,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -509,12 +608,24 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.27.7': resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -527,12 +638,24 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.27.7': resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -545,12 +668,24 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.27.7': resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -563,6 +698,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -575,6 +716,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -587,6 +734,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -599,6 +752,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@iconify-json/simple-icons@1.2.93': resolution: {integrity: sha512-/XhANjfGYOuqvSR3TmUnkQkINvQ4GVjVuukvymRbxtVFBvIq/yiXJqCDycKcQPT401OYT9H2vIY6ihAlz1QIAw==} @@ -1231,6 +1390,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1971,6 +2135,11 @@ packages: typescript: optional: true + tsx@4.23.12: + resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} + engines: {node: '>=18.0.0'} + hasBin: true + typescript@5.6.1-rc: resolution: {integrity: sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==} engines: {node: '>=14.17'} @@ -2512,147 +2681,225 @@ snapshots: '@esbuild/aix-ppc64@0.27.7': optional: true + '@esbuild/aix-ppc64@0.28.2': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true '@esbuild/android-arm64@0.27.7': optional: true + '@esbuild/android-arm64@0.28.2': + optional: true + '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-arm@0.27.7': optional: true + '@esbuild/android-arm@0.28.2': + optional: true + '@esbuild/android-x64@0.21.5': optional: true '@esbuild/android-x64@0.27.7': optional: true + '@esbuild/android-x64@0.28.2': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true '@esbuild/darwin-arm64@0.27.7': optional: true + '@esbuild/darwin-arm64@0.28.2': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true '@esbuild/darwin-x64@0.27.7': optional: true + '@esbuild/darwin-x64@0.28.2': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true '@esbuild/freebsd-arm64@0.27.7': optional: true + '@esbuild/freebsd-arm64@0.28.2': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true '@esbuild/freebsd-x64@0.27.7': optional: true + '@esbuild/freebsd-x64@0.28.2': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true '@esbuild/linux-arm64@0.27.7': optional: true + '@esbuild/linux-arm64@0.28.2': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true '@esbuild/linux-arm@0.27.7': optional: true + '@esbuild/linux-arm@0.28.2': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true '@esbuild/linux-ia32@0.27.7': optional: true + '@esbuild/linux-ia32@0.28.2': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true '@esbuild/linux-loong64@0.27.7': optional: true + '@esbuild/linux-loong64@0.28.2': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true '@esbuild/linux-mips64el@0.27.7': optional: true + '@esbuild/linux-mips64el@0.28.2': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true '@esbuild/linux-ppc64@0.27.7': optional: true + '@esbuild/linux-ppc64@0.28.2': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true '@esbuild/linux-riscv64@0.27.7': optional: true + '@esbuild/linux-riscv64@0.28.2': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true '@esbuild/linux-s390x@0.27.7': optional: true + '@esbuild/linux-s390x@0.28.2': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true '@esbuild/linux-x64@0.27.7': optional: true + '@esbuild/linux-x64@0.28.2': + optional: true + '@esbuild/netbsd-arm64@0.27.7': optional: true + '@esbuild/netbsd-arm64@0.28.2': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true '@esbuild/netbsd-x64@0.27.7': optional: true + '@esbuild/netbsd-x64@0.28.2': + optional: true + '@esbuild/openbsd-arm64@0.27.7': optional: true + '@esbuild/openbsd-arm64@0.28.2': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true '@esbuild/openbsd-x64@0.27.7': optional: true + '@esbuild/openbsd-x64@0.28.2': + optional: true + '@esbuild/openharmony-arm64@0.27.7': optional: true + '@esbuild/openharmony-arm64@0.28.2': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true '@esbuild/sunos-x64@0.27.7': optional: true + '@esbuild/sunos-x64@0.28.2': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true '@esbuild/win32-arm64@0.27.7': optional: true + '@esbuild/win32-arm64@0.28.2': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true '@esbuild/win32-ia32@0.27.7': optional: true + '@esbuild/win32-ia32@0.28.2': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true '@esbuild/win32-x64@0.27.7': optional: true + '@esbuild/win32-x64@0.28.2': + optional: true + '@iconify-json/simple-icons@1.2.93': dependencies: '@iconify/types': 2.0.0 @@ -3297,6 +3544,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} esprima@4.0.1: {} @@ -3730,12 +4006,13 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 - postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.26): + postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.7.0 postcss: 8.5.26 + tsx: 4.23.12 postcss@8.5.26: dependencies: @@ -3986,7 +4263,7 @@ snapshots: ts-interface-checker@0.1.13: {} - tsup@8.5.1(jiti@2.7.0)(postcss@8.5.26)(typescript@5.9.3): + tsup@8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12)(typescript@5.9.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 @@ -3997,7 +4274,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.26) + postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.12) resolve-from: 5.0.0 rollup: 4.62.4 source-map: 0.7.6 @@ -4014,6 +4291,12 @@ snapshots: - tsx - yaml + tsx@4.23.12: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + typescript@5.6.1-rc: {} typescript@5.9.3: {} diff --git a/tsconfig.json b/tsconfig.json index 77ac7ca..6d9fdc9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,5 +26,7 @@ "forceConsistentCasingInFileNames": true, "outDir": "dist" }, - "include": ["src"] + // examples/ is type-checked too: an example that does not compile is worse + // than no example, and it is the first code anyone reads. + "include": ["src", "examples"] }