From 0979fd7aa9b80e84c58366e70c0d35966d5a13d9 Mon Sep 17 00:00:00 2001 From: Alex Le Date: Fri, 26 Jun 2026 17:09:05 -0700 Subject: [PATCH 01/17] docs: added docs on circuit breaker Signed-off-by: Alex Le --- astro.config.mjs | 1 + .../how-to/connections/circuit-breaker.mdx | 249 ++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 src/content/docs/how-to/connections/circuit-breaker.mdx diff --git a/astro.config.mjs b/astro.config.mjs index 479c7035..323ea5b1 100755 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -176,6 +176,7 @@ export default defineConfig({ { label: "Connections", items: [ + "how-to/connections/circuit-breaker", "how-to/connections/configure-lazy-connection", "how-to/connections/limit-inflight-requests", "how-to/connections/read-strategy", diff --git a/src/content/docs/how-to/connections/circuit-breaker.mdx b/src/content/docs/how-to/connections/circuit-breaker.mdx new file mode 100644 index 00000000..435fed46 --- /dev/null +++ b/src/content/docs/how-to/connections/circuit-breaker.mdx @@ -0,0 +1,249 @@ +--- +title: Configure a Circuit Breaker +description: Protect your application from thread explosion during degraded conditions by enabling the client-side circuit breaker. +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +The circuit breaker is an opt-in feature that detects when the GLIDE core is unhealthy (sustained error rate) and rejects requests at the FFI boundary before threads stalls. This prevents thread explosion under degraded conditions by failing fast instead of queueing requests indefinitely. + +## Configuration + +The circuit breaker option is defined on the base client configuration, so it applies to both standalone and cluster clients. The examples below use the standalone client; pass the same configuration to the cluster client configuration to enable it for a cluster client. + + + + ```python + from glide import GlideClientConfiguration, ClientCircuitBreakerConfiguration, NodeAddress + + circuit_breaker = ClientCircuitBreakerConfiguration( + window_size_ms=10000, + failure_rate_threshold=0.5, + min_errors=50, + open_timeout_ms=5000, + count_timeouts=False, + consecutive_successes=3, + ) + + config = GlideClientConfiguration( + [NodeAddress("localhost", 6379)], + client_circuit_breaker=circuit_breaker, + ) + ``` + + + + ```java + import glide.api.models.configuration.ClientCircuitBreakerConfiguration; + import glide.api.models.configuration.GlideClientConfiguration; + import glide.api.models.configuration.NodeAddress; + + ClientCircuitBreakerConfiguration circuitBreaker = ClientCircuitBreakerConfiguration.builder() + .windowSizeMs(10000) + .failureRateThreshold(0.5f) + .minErrors(50) + .openTimeoutMs(5000) + .countTimeouts(false) + .consecutiveSuccesses(3) + .build(); + + GlideClientConfiguration config = GlideClientConfiguration.builder() + .address(NodeAddress.builder().host("localhost").port(6379).build()) + .clientCircuitBreakerConfiguration(circuitBreaker) + .build(); + ``` + + + + ```typescript + import { GlideClient, GlideClientConfiguration } from '@valkey/valkey-glide'; + + const config: GlideClientConfiguration = { + addresses: [{ host: 'localhost', port: 6379 }], + clientCircuitBreaker: { + windowSizeMs: 10000, + failureRateThreshold: 0.5, + minErrors: 50, + openTimeoutMs: 5000, + countTimeouts: false, + consecutiveSuccesses: 3, + }, + }; + ``` + + + + ```go + import "github.com/valkey-io/valkey-glide/go/v2/config" + + circuitBreaker := &config.ClientCircuitBreakerConfiguration{ + WindowSizeMs: 10000, + FailureRateThreshold: 0.5, + MinErrors: 50, + OpenTimeoutMs: 5000, + CountTimeouts: false, + ConsecutiveSuccesses: 3, + } + + clientConfig := config.NewClientConfiguration(). + WithAddress(&config.NodeAddress{Host: "localhost", Port: 6379}). + WithClientCircuitBreaker(circuitBreaker) + ``` + + + + :::note + The circuit breaker is not yet available in the PHP client. + ::: + + + + :::note + The circuit breaker is not yet available in the C# client. + ::: + + + +## How It Works + +The circuit breaker uses a state machine with three states: + +1. **Closed** (normal operation) — All requests pass through. The breaker monitors the error rate within a sliding window. If the error rate exceeds the threshold (and the minimum error count has been reached), the breaker trips to Open. + +2. **Open** (rejecting) — All requests are immediately rejected with a `CircuitBreakerException`/`CircuitBreakerError`. After the open timeout elapses, the breaker transitions to HalfOpen. + +3. **HalfOpen** (probing recovery) — All traffic is allowed through optimistically. If the configured number of consecutive successes is reached, the breaker closes. If a failure occurs, the breaker returns to Open. + +``` +Closed ──(error rate exceeds threshold)──► Open + ▲ │ + │ (timeout) + │ ▼ + └──(N consecutive successes)────────── HalfOpen +``` + +### What counts as a failure + +Only transport-level failures count toward tripping the breaker: + +* Connection failures (e.g. connection refused) and dropped connections +* Failures sending or receiving on the connection (pipeline send/receive failures) +* Request timeouts — but only when `countTimeouts` is enabled + +Application and server errors do **not** count, because the request reached the server and got a valid response. For example, `WRONGTYPE`, `MOVED`, and similar command-level errors leave the breaker closed. + +:::note +This failure classification reflects the current core behavior and may evolve in a future release (see valkey-glide [#6208](https://github.com/valkey-io/valkey-glide/issues/6208)). +::: + +## Handling Rejections + +When the circuit breaker is open, requests throw immediately. Catch these exceptions to implement fallback logic or surface appropriate errors to callers. + + + + ```python + from glide import CircuitBreakerError + + try: + value = await client.get("key") + except CircuitBreakerError: + # Circuit breaker is open — use fallback or back off + pass + ``` + + + + ```java + import glide.api.models.exceptions.CircuitBreakerException; + import java.util.concurrent.ExecutionException; + + try { + String value = client.get("key").get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof CircuitBreakerException) { + // Circuit breaker is open — use fallback or back off + } + } + ``` + + + + ```typescript + import { CircuitBreakerError } from '@valkey/valkey-glide'; + + try { + const value = await client.get("key"); + } catch (e) { + if (e instanceof CircuitBreakerError) { + // Circuit breaker is open — use fallback or back off + } + } + ``` + + + + ```go + import ( + "context" + "errors" + + glide "github.com/valkey-io/valkey-glide/go/v2" + ) + + result, err := client.Get(context.Background(), "key") + if err != nil { + var cbErr *glide.CircuitBreakerError + if errors.As(err, &cbErr) { + // Circuit breaker is open — use fallback or back off + } + } + ``` + + + + :::note + The circuit breaker is not yet available in the PHP client. + ::: + + + + :::note + The circuit breaker is not yet available in the C# client. + ::: + + + +## Configuration Parameters + +All parameters are optional. The circuit breaker uses sensible defaults if you provide no configuration beyond enabling it. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `windowSizeMs` / `window_size_ms` | integer | `10000` (10s) | Sliding window duration for error rate calculation. | +| `failureRateThreshold` / `failure_rate_threshold` | float | `0.5` (50%) | Error rate (0.0–1.0) that trips the breaker. | +| `minErrors` / `min_errors` | integer | `50` | Minimum errors within the window before the rate is evaluated. Prevents tripping on low traffic. | +| `openTimeoutMs` / `open_timeout_ms` | integer | `5000` (5s) | Time the breaker stays Open before transitioning to HalfOpen. | +| `countTimeouts` / `count_timeouts` | boolean | `false` | Whether request timeouts count as failures toward tripping the breaker. | +| `consecutiveSuccesses` / `consecutive_successes` | integer | `3` | Consecutive successful requests in HalfOpen needed to close the breaker. | + +## Best Practices + +**When to enable:** + +* Applications where thread exhaustion or resource starvation is a concern during server outages. +* Services that can surface a degraded response (cache miss fallback, stale data) rather than hanging. +* High-throughput workloads where queued requests would pile up faster than they drain. + +**Tuning guidance:** + +* Start with defaults and adjust based on observed behavior. +* Lower `failureRateThreshold` if you want faster detection of degraded backends. +* Increase `minErrors` in low-traffic environments to avoid false trips from small sample sizes. +* Increase `openTimeoutMs` if your server typically takes longer to recover (e.g., failover scenarios). +* Set `countTimeouts: true` if your timeout configuration is tight and timeouts reliably indicate a backend issue. +* Keep `consecutiveSuccesses` low (2–5) for faster recovery; increase it if premature recovery causes cascading failures. + +:::tip +Enable the circuit breaker with all defaults as a starting point — just pass an empty configuration object. This gives you protection with no tuning required. +::: From 8c50af8fd7e0b3ba045d37f8fcdd8278c5448996 Mon Sep 17 00:00:00 2001 From: Alex Le Date: Mon, 29 Jun 2026 09:06:25 -0700 Subject: [PATCH 02/17] addressed comments Signed-off-by: Alex Le --- src/content/docs/how-to/connections/circuit-breaker.mdx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/content/docs/how-to/connections/circuit-breaker.mdx b/src/content/docs/how-to/connections/circuit-breaker.mdx index 435fed46..59de73b0 100644 --- a/src/content/docs/how-to/connections/circuit-breaker.mdx +++ b/src/content/docs/how-to/connections/circuit-breaker.mdx @@ -5,7 +5,7 @@ description: Protect your application from thread explosion during degraded cond import { Tabs, TabItem } from '@astrojs/starlight/components'; -The circuit breaker is an opt-in feature that detects when the GLIDE core is unhealthy (sustained error rate) and rejects requests at the FFI boundary before threads stalls. This prevents thread explosion under degraded conditions by failing fast instead of queueing requests indefinitely. +The circuit breaker is an opt-in feature that detects when the GLIDE core is unhealthy (sustained error rate) and rejects requests at the FFI boundary before threads . This prevents thread explosion under degraded conditions by failing fast instead of queueing requests indefinitely. ## Configuration @@ -132,10 +132,6 @@ Only transport-level failures count toward tripping the breaker: Application and server errors do **not** count, because the request reached the server and got a valid response. For example, `WRONGTYPE`, `MOVED`, and similar command-level errors leave the breaker closed. -:::note -This failure classification reflects the current core behavior and may evolve in a future release (see valkey-glide [#6208](https://github.com/valkey-io/valkey-glide/issues/6208)). -::: - ## Handling Rejections When the circuit breaker is open, requests throw immediately. Catch these exceptions to implement fallback logic or surface appropriate errors to callers. From d401df706b8d72341ad4622ea50b3d22f18c7e66 Mon Sep 17 00:00:00 2001 From: Alex Le Date: Mon, 29 Jun 2026 09:11:16 -0700 Subject: [PATCH 03/17] fixed typo Signed-off-by: Alex Le --- src/content/docs/how-to/connections/circuit-breaker.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/how-to/connections/circuit-breaker.mdx b/src/content/docs/how-to/connections/circuit-breaker.mdx index 59de73b0..546a9186 100644 --- a/src/content/docs/how-to/connections/circuit-breaker.mdx +++ b/src/content/docs/how-to/connections/circuit-breaker.mdx @@ -5,7 +5,7 @@ description: Protect your application from thread explosion during degraded cond import { Tabs, TabItem } from '@astrojs/starlight/components'; -The circuit breaker is an opt-in feature that detects when the GLIDE core is unhealthy (sustained error rate) and rejects requests at the FFI boundary before threads . This prevents thread explosion under degraded conditions by failing fast instead of queueing requests indefinitely. +The circuit breaker is an opt-in feature that detects when the GLIDE core is unhealthy (sustained error rate) and rejects requests at the FFI boundary before threads stalls. This prevents thread explosion under degraded conditions by failing fast instead of queueing requests indefinitely. ## Configuration From 8b2b0d40726bc002fa96de4138e1137957591ad0 Mon Sep 17 00:00:00 2001 From: Alex Le Date: Mon, 29 Jun 2026 09:13:02 -0700 Subject: [PATCH 04/17] fixed typo Signed-off-by: Alex Le --- src/content/docs/how-to/connections/circuit-breaker.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/how-to/connections/circuit-breaker.mdx b/src/content/docs/how-to/connections/circuit-breaker.mdx index 546a9186..311ecebc 100644 --- a/src/content/docs/how-to/connections/circuit-breaker.mdx +++ b/src/content/docs/how-to/connections/circuit-breaker.mdx @@ -5,7 +5,7 @@ description: Protect your application from thread explosion during degraded cond import { Tabs, TabItem } from '@astrojs/starlight/components'; -The circuit breaker is an opt-in feature that detects when the GLIDE core is unhealthy (sustained error rate) and rejects requests at the FFI boundary before threads stalls. This prevents thread explosion under degraded conditions by failing fast instead of queueing requests indefinitely. +The circuit breaker is an opt-in feature that detects when the GLIDE core is unhealthy (sustained error rate) and rejects requests at the FFI boundary before threads stall. This prevents thread explosion under degraded conditions by failing fast instead of queueing requests indefinitely. ## Configuration From b52e90eb19805c821e5b57832adf11187cc3e27b Mon Sep 17 00:00:00 2001 From: Alex Le Date: Sun, 21 Jun 2026 16:13:38 -0700 Subject: [PATCH 05/17] Implemented Node example validator Signed-off-by: Alex Le --- .github/workflows/check-code-examples.yml | 12 + .github/workflows/check-csharp-examples.yml | 4 +- .github/workflows/check-node-examples.yml | 61 ++++ scripts/validators/node.py | 342 ++++++++++++++++++++ 4 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/check-code-examples.yml create mode 100644 .github/workflows/check-node-examples.yml create mode 100644 scripts/validators/node.py diff --git a/.github/workflows/check-code-examples.yml b/.github/workflows/check-code-examples.yml new file mode 100644 index 00000000..d021a471 --- /dev/null +++ b/.github/workflows/check-code-examples.yml @@ -0,0 +1,12 @@ +name: Check Code Examples + +on: + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + csharp: + uses: ./.github/workflows/check-csharp-examples.yml + node: + uses: ./.github/workflows/check-node-examples.yml diff --git a/.github/workflows/check-csharp-examples.yml b/.github/workflows/check-csharp-examples.yml index ae6a2641..19665e14 100644 --- a/.github/workflows/check-csharp-examples.yml +++ b/.github/workflows/check-csharp-examples.yml @@ -1,8 +1,8 @@ name: Check C# Examples on: - pull_request: - branches: [main] + workflow_call: + workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/check-node-examples.yml b/.github/workflows/check-node-examples.yml new file mode 100644 index 00000000..bbb63074 --- /dev/null +++ b/.github/workflows/check-node-examples.yml @@ -0,0 +1,61 @@ +name: Check Node Examples + +# Full-compilation validates the Node.js (TypeScript) code examples in the +# docs against the real @valkey/valkey-glide type definitions built from source. +# See scripts/validators/node.py. + +on: + pull_request: + branches: [main] + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + check-node-examples: + runs-on: ubuntu-latest + steps: + - name: Checkout valkey-glide-docs + uses: actions/checkout@v4 + + - name: Checkout valkey-glide + uses: actions/checkout@v4 + with: + repository: valkey-io/valkey-glide + path: valkey-glide + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # stable + with: + toolchain: stable + targets: x86_64-unknown-linux-gnu + + - name: Install protoc + uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3 + with: + version: "25.1" + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev + + - name: Build Node client + working-directory: valkey-glide/node + run: | + npm ci + npm run build:release + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Validate Node examples + run: python scripts/validators/node.py --glide-path valkey-glide/node diff --git a/scripts/validators/node.py b/scripts/validators/node.py new file mode 100644 index 00000000..ef0d5bfc --- /dev/null +++ b/scripts/validators/node.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +"""Full-compilation validator for Node (TypeScript) documentation examples. + +Extracts TypeScript/JavaScript code blocks from the MDX docs and compiles +each snippet against the real ``@valkey/valkey-glide`` type definitions +using ``tsc --noEmit``. + +Self-contained: no external Python dependencies beyond the standard library. + +Usage: + python scripts/validators/node.py --glide-path ../valkey-glide/node + +Requires a pre-built Node client (run ``npm ci && npm run build:release`` +in the valkey-glide/node directory first). +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +import textwrap + +# --------------------------------------------------------------------------- +# Extraction +# --------------------------------------------------------------------------- + +_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +_DOCS_DIR = os.path.join(_REPO_ROOT, "src", "content", "docs") + +_TS_BLOCK_RE = re.compile( + r"^\s*```(?:typescript|ts|javascript|js)\s*\n(.*?)^\s*```\s*$", + re.MULTILINE | re.DOTALL, +) + + +def _extract_examples() -> dict[str, str]: + """Extract TypeScript/JS code blocks from all MDX files.""" + examples: dict[str, str] = {} + for root, _dirs, files in os.walk(_DOCS_DIR): + # Skip migration guides — they contain comparison snippets from other clients + rel_root = os.path.relpath(root, _DOCS_DIR) + if rel_root.startswith("migration"): + continue + for fname in sorted(files): + if not fname.endswith(".mdx"): + continue + # Skip IAM integration guides — they import AWS SDK packages + if fname.startswith("iam-"): + continue + filepath = os.path.join(root, fname) + with open(filepath, encoding="utf-8") as fh: + content = fh.read() + for match in _TS_BLOCK_RE.finditer(content): + key_path = os.path.relpath(filepath, _REPO_ROOT) + line_number = content[: match.start()].count("\n") + 1 + examples[f"{key_path}:{line_number}"] = match.group(1) + return examples + + +# --------------------------------------------------------------------------- +# Wrapping +# --------------------------------------------------------------------------- + +_IMPORT_LINE = re.compile(r"^\s*import\s+") + +_DEFAULT_IMPORTS = ( + "GlideClient, GlideClusterClient, GlideClientConfiguration, " + "GlideClusterClientConfiguration, Batch, ClusterBatch, BatchOptions, " + "ClusterBatchOptions, ClusterBatchRetryStrategy, Script, Transaction, " + "Routes, Logger, RequestError, ServerCredentials, GlideFt, " + "GlideJson, OpenTelemetry, ClientSideCache, EvictionPolicy, TimeUnit, " + "Field, FtSearchOptions, FtSearchReturnType, Decoder, " + "AdvancedGlideClusterClientConfiguration, CompressionBackend, " + "OpenTelemetryConfig, OpenTelemetryMetricsConfig, OpenTelemetryTracesConfig, " + "ClusterScanCursor, ReadFrom, GlideString, PubSubMsg, ALL_CHANNELS, ALL_PATTERNS" +) + +_CLIENT_DECLARATIONS = ( + "declare const client: GlideClient;\n" + "declare const clusterClient: GlideClusterClient;\n" +) + + +def _split_imports(code: str) -> tuple[list[str], list[str]]: + """Separate import statements (including multi-line) from the rest.""" + imports: list[str] = [] + body: list[str] = [] + lines = code.splitlines() + i = 0 + while i < len(lines): + line = lines[i] + if _IMPORT_LINE.match(line): + import_lines = [line] + while not re.search(r"""from\s+["'][^"']+["']\s*;?\s*$""", line) and i + 1 < len(lines): + i += 1 + line = lines[i] + import_lines.append(line) + imports.append("\n".join(import_lines)) + else: + body.append(line) + i += 1 + return imports, body + + +def _wrap_snippet(code: str) -> str: + """Wrap a snippet into a compilable .ts file.""" + imports, body_lines = _split_imports(code) + + parts: list[str] = [] + + # Inject default imports, deduped against snippet's own + existing_names: set[str] = set() + for imp in imports: + m = re.search(r"\{([^}]+)\}", imp) + if m: + for name in m.group(1).split(","): + existing_names.add(name.strip()) + defaults = [ + n.strip() for n in _DEFAULT_IMPORTS.split(",") + if n.strip() not in existing_names + ] + if defaults: + parts.append(f"import {{ {', '.join(defaults)} }} from \"@valkey/valkey-glide\";\n") + + # Hoisted imports from snippet + if imports: + parts.append("\n".join(imports) + "\n") + + # Client declarations + parts.append("\n" + _CLIENT_DECLARATIONS) + + # Async wrapper for the body + body = "\n".join(body_lines).strip() + if body: + parts.append(f"\nasync function __run() {{\n{textwrap.indent(body, ' ')}\n}}\n") + + return "\n".join(parts) + + +# --------------------------------------------------------------------------- +# Compilation +# --------------------------------------------------------------------------- + + +def _require_tool(name: str) -> None: + if shutil.which(name) is None: + print(f"Error: '{name}' is not installed or not on PATH.", file=sys.stderr) + sys.exit(1) + + +def _setup_project(tmp_dir: str, glide_path: str) -> None: + """Create a temp TypeScript project referencing the local GLIDE build.""" + glide_abs = os.path.abspath(glide_path) + + package_json = json.dumps( + { + "name": "glide-doc-validator", + "private": True, + "type": "module", + "dependencies": { + "@valkey/valkey-glide": f"file:{glide_abs}", + }, + }, + indent=2, + ) + tsconfig = json.dumps( + { + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "bundler", + "target": "ESNext", + "noEmit": True, + "strict": False, + "skipLibCheck": True, + "esModuleInterop": True, + "types": ["node"], + }, + "include": ["*.ts"], + }, + indent=2, + ) + + with open(os.path.join(tmp_dir, "package.json"), "w") as f: + f.write(package_json) + with open(os.path.join(tmp_dir, "tsconfig.json"), "w") as f: + f.write(tsconfig) + + proc = subprocess.run( + ["npm", "install", "typescript", "@types/node", "--save"], + cwd=tmp_dir, + capture_output=True, + text=True, + ) + if proc.returncode != 0: + detail = (proc.stderr or proc.stdout).strip() + print(f"Error: npm install failed:\n{detail}", file=sys.stderr) + sys.exit(1) + + +def _run_tsc(tmp_dir: str) -> str: + """Run tsc --noEmit and return combined output.""" + tsc_path = os.path.join(tmp_dir, "node_modules", ".bin", "tsc") + proc = subprocess.run( + [tsc_path, "--noEmit", "--pretty", "false"], + cwd=tmp_dir, + capture_output=True, + text=True, + ) + return proc.stdout + proc.stderr + + +_TSC_ERROR = re.compile(r"^(example_\d+\.ts)\((\d+),(\d+)\):\s+error\s+TS\d+:\s+(.+)$") + + +def _parse_tsc_errors(output: str) -> dict[str, list[str]]: + """Parse tsc output into {filename: [messages]}.""" + errors: dict[str, list[str]] = {} + for line in output.splitlines(): + m = _TSC_ERROR.match(line) + if m: + filename, line_no, _col, message = m.groups() + errors.setdefault(filename, []).append(f"line {line_no}: {message}") + return errors + + +def validate( + examples: dict[str, str], + *, + glide_path: str, + keep_project: bool = False, +) -> dict[str, list[str]]: + """Compile all snippets and return {source: [errors]}.""" + tmp_dir = tempfile.mkdtemp(prefix="glide_node_validate_") + try: + print("Setting up TypeScript project...", flush=True) + _setup_project(tmp_dir, glide_path) + + file_to_source: dict[str, str] = {} + for idx, (source, code) in enumerate(examples.items()): + filename = f"example_{idx:04d}.ts" + file_to_source[filename] = source + wrapped = _wrap_snippet(code) + with open(os.path.join(tmp_dir, filename), "w", encoding="utf-8") as f: + f.write(wrapped) + + print(f"Running tsc --noEmit on {len(examples)} file(s)...", flush=True) + output = _run_tsc(tmp_dir) + + file_errors = _parse_tsc_errors(output) + result: dict[str, list[str]] = {} + for filename, msgs in file_errors.items(): + source = file_to_source.get(filename) + if source: + result[source] = msgs + finally: + if keep_project: + print(f"\nProject kept at: {tmp_dir}", flush=True) + else: + shutil.rmtree(tmp_dir, ignore_errors=True) + + return result + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Full-compilation validator for Node.js (TypeScript) doc examples." + ) + parser.add_argument( + "--glide-path", required=True, + help="Path to the built valkey-glide/node directory.", + ) + parser.add_argument( + "--keep-project", action="store_true", + help="Keep the temp project directory for inspection.", + ) + args = parser.parse_args() + + glide_path = os.path.abspath(args.glide_path) + + # Validate the glide path + if not os.path.isdir(glide_path): + print(f"Error: --glide-path not found: {glide_path}", file=sys.stderr) + sys.exit(1) + if not os.path.isfile(os.path.join(glide_path, "build-ts", "index.d.ts")): + print( + f"Error: No build-ts/index.d.ts in {glide_path}. " + f"Build the client first: cd {glide_path} && npm ci && npm run build:release", + file=sys.stderr, + ) + sys.exit(1) + + _require_tool("node") + _require_tool("npm") + + if not os.path.isdir(_DOCS_DIR): + print(f"Error: docs directory not found: {_DOCS_DIR}", file=sys.stderr) + sys.exit(1) + + print("Extracting TypeScript examples from MDX docs...", flush=True) + examples = _extract_examples() + print(f"Extracted {len(examples)} example(s).", flush=True) + + if not examples: + sys.exit(0) + + dedented = {source: textwrap.dedent(code) for source, code in examples.items()} + + errors = validate( + dedented, + glide_path=glide_path, + keep_project=args.keep_project, + ) + errors = {s: msgs for s, msgs in errors.items() if msgs} + + if errors: + bar = "=" * 60 + print(f"\n{bar}\nFAILURES ({len(errors)} of {len(examples)} examples)\n{bar}\n") + for source, messages in errors.items(): + print(f" FAIL: {source}") + for message in messages: + print(f" {message}") + print() + print(f"{len(examples) - len(errors)} passed, {len(errors)} failed") + sys.exit(1) + + print(f"\nAll {len(examples)} node examples compiled successfully.") + sys.exit(0) + + +if __name__ == "__main__": + main() From 810f3710e432821e28014709f41cd1cef9227cd0 Mon Sep 17 00:00:00 2001 From: Alex Le Date: Sun, 21 Jun 2026 18:23:09 -0700 Subject: [PATCH 06/17] Fixed examples Signed-off-by: Alex Le --- .github/workflows/check-node-examples.yml | 2 -- src/content/docs/commands/valkey-string.mdx | 17 ++++++---- .../client-features/batch-commands.mdx | 18 +++++++---- .../client-features/valkey-scripting.mdx | 12 ++++--- .../connections/limit-inflight-requests.mdx | 1 + .../timeouts-and-reconnect-strategy.mdx | 4 +-- .../docs/how-to/execute-custom-scripts.mdx | 26 ++------------- .../how-to/load-and-execute-functions.mdx | 32 +++---------------- .../how-to/monitoring/tracking-resources.mdx | 2 +- .../how-to/publish-and-subscribe-messages.mdx | 6 +++- src/content/docs/how-to/scan-cluster.mdx | 23 +++++++------ .../docs/reference/connection-options.mdx | 4 +-- .../docs/reference/scripting-reference.mdx | 4 ++- 13 files changed, 65 insertions(+), 86 deletions(-) diff --git a/.github/workflows/check-node-examples.yml b/.github/workflows/check-node-examples.yml index bbb63074..a739035e 100644 --- a/.github/workflows/check-node-examples.yml +++ b/.github/workflows/check-node-examples.yml @@ -5,8 +5,6 @@ name: Check Node Examples # See scripts/validators/node.py. on: - pull_request: - branches: [main] workflow_call: workflow_dispatch: diff --git a/src/content/docs/commands/valkey-string.mdx b/src/content/docs/commands/valkey-string.mdx index 338bba21..d117b2fb 100644 --- a/src/content/docs/commands/valkey-string.mdx +++ b/src/content/docs/commands/valkey-string.mdx @@ -135,7 +135,7 @@ Valkey strings store sequences of bytes, which may include text, serialized obje Here is an example of command implementation to outline arguments using [`DecoderOption`](https://github.com/valkey-io/valkey-glide/blob/79fdec50bbef4310cb12e46a69f14c18378afb44/node/src/BaseClient.ts#L295) and response type: - ```typescript + ```text public getdel( key: GlideString, options?: DecoderOption, @@ -145,14 +145,17 @@ Valkey strings store sequences of bytes, which may include text, serialized obje Here's a simple example demonstrating how to use `Decoder` in command API and `Buffer.from()` to encode binary data: ```typescript - expect(await client.set(key, value)).toEqual("OK"); - expect(await client.getdel(key)).toEqual(value); + const key = "mykey"; + const value = "hello"; + + await client.set(key, value); + const result = await client.getdel(key); + console.log(result); // "hello" const valueEncoded = Buffer.from(value); - expect(await client.set(key, value)).toEqual("OK"); - expect(await client.getdel(key, { decoder: Decoder.Bytes })).toEqual( - valueEncoded, - ); + await client.set(key, value); + const resultBytes = await client.getdel(key, { decoder: Decoder.Bytes }); + console.log(resultBytes); // ``` diff --git a/src/content/docs/concepts/client-features/batch-commands.mdx b/src/content/docs/concepts/client-features/batch-commands.mdx index 633614ef..be74622e 100644 --- a/src/content/docs/concepts/client-features/batch-commands.mdx +++ b/src/content/docs/concepts/client-features/batch-commands.mdx @@ -68,9 +68,9 @@ For **standalone** (non-cluster, cluster mode disabled) clients. import {Batch} from "@valkey/valkey-glide"; // Create an atomic batch (transaction) - const batch = new Batch(true) + const atomicBatch = new Batch(true) // Create a non-atomic batch (pipeline) - const batch = new Batch(false) + const nonAtomicBatch = new Batch(false) ``` @@ -134,9 +134,9 @@ splitting into sub-pipelines if needed, [Read more in Multi-Node support](#multi import {ClusterBatch} from "@valkey/valkey-glide"; // Create an atomic cluster batch (must use keys mapping to same slot) - const batch = new ClusterBatch(true) + const atomicBatch = new ClusterBatch(true) // Create a non-atomic cluster batch (pipeline may span multiple slots) - const batch = new ClusterBatch(false) + const nonAtomicBatch = new ClusterBatch(false) ``` @@ -206,7 +206,7 @@ Determines how errors are surfaced when calling `exec(...)`. It is passed direct - ```typescript + ```text // Standalone Mode public async exec( batch: Batch, @@ -324,19 +324,23 @@ Behavior: ```typescript // Cluster pipeline with raiseOnError = false + const key = "mykey"; + const key2 = "newkey"; const batch = new ClusterBatch(false); batch.set(key, "hello") // OK .lpop(key) // WRONGTYPE error (not a list) .del([key]) // 1 .rename(key, key2); // NO SUCH KEY error - const result = await GlideClusterClient.exec(batch, false); + const result = await clusterClient.exec(batch, false); console.log("Result is:", result); // Output: Result is: [OK, RequestError: WRONGTYPE: Operation against a key holding the wrong kind of value, 1, RequestError: An error was signalled by the server: - ResponseError: no such key]) ``` ```typescript // Transaction with raiseOnError = true + const key = "mykey"; + const key2 = "newkey"; const batch = new Batch(true); batch.set(key, "hello") // OK .lpop(key) // WRONGTYPE error (not a list) @@ -344,7 +348,7 @@ Behavior: .rename(key, key2); // NO SUCH KEY error try { - await GlideClient.exec(batch, true); + await client.exec(batch, true); } catch (error) { console.log("Batch execution aborted: ", error); } diff --git a/src/content/docs/concepts/client-features/valkey-scripting.mdx b/src/content/docs/concepts/client-features/valkey-scripting.mdx index 9fda79b3..35ec2361 100644 --- a/src/content/docs/concepts/client-features/valkey-scripting.mdx +++ b/src/content/docs/concepts/client-features/valkey-scripting.mdx @@ -175,8 +175,9 @@ Valkey provides `KEYS` and `ARGV` to handle input parameters: ```typescript const productKey = "product:shoe:stock"; const buyQuantity = "3"; + const script = new Script("return redis.call('DECRBY', KEYS[1], ARGV[1])"); - const result = await client.invokeScript(purchaseScript, { + const result = await client.invokeScript(script, { keys: [productKey], // Maps to KEYS[1] in Lua args: [buyQuantity] // Maps to ARGV[1] in Lua }); @@ -267,6 +268,9 @@ values directly into the script would create unique scripts each time, preventin ```typescript + const productKey = "product:shoe:stock"; + const buyQuantity = "3"; + // Bad: Creates a new hash for each quantity const badScript = new Script(`return redis.call('DECRBY', ${productKey}, ${buyQuantity})`); @@ -590,10 +594,10 @@ GLIDE provide options for explicit control over script routing in cluster mode. ```typescript - import {Routes, SlotType} from "@valkey/valkey-glide"; + const script = new Script("return redis.call('PING')"); // Route to the node holding a specific key's slot - const route = {type: "routeByAddress", host: "user:1000", slotType: SlotType.Primary}; + const route = {type: "routeByAddress" as const, host: "localhost", port: 6379}; // Route to all primary nodes await clusterClient.invokeScriptWithRoute(script, {route: "allPrimaries"}); @@ -737,7 +741,7 @@ To use Lua scripts within an atomic batch (MULTI/EXEC transaction), you must use ]); transaction.get("script-key"); - const results = await client.exec(transaction); + const results = await client.exec(transaction, true); console.log(`EVAL result: ${results[0]}`); // OK console.log(`GET result: ${results[1]}`); // script-value ``` diff --git a/src/content/docs/how-to/connections/limit-inflight-requests.mdx b/src/content/docs/how-to/connections/limit-inflight-requests.mdx index 5670b050..2710fae4 100644 --- a/src/content/docs/how-to/connections/limit-inflight-requests.mdx +++ b/src/content/docs/how-to/connections/limit-inflight-requests.mdx @@ -37,6 +37,7 @@ Inflight requests limit can be configured through the general client configurati ```typescript // Limit to 1000 inflight requests per connection const clusterConfig: GlideClusterClientConfiguration = { + addresses: [{ host: "localhost", port: 6379 }], inflightRequestsLimit: 1000, }; ``` diff --git a/src/content/docs/how-to/connections/timeouts-and-reconnect-strategy.mdx b/src/content/docs/how-to/connections/timeouts-and-reconnect-strategy.mdx index 7074974b..d8145e49 100644 --- a/src/content/docs/how-to/connections/timeouts-and-reconnect-strategy.mdx +++ b/src/content/docs/how-to/connections/timeouts-and-reconnect-strategy.mdx @@ -271,8 +271,8 @@ The connection timeout controls how long the client waits for a TCP/TLS connecti const client = await GlideClusterClient.createClient({ addresses: addresses, - reconnectStrategy: { - numOfRetries: 3, + connectionBackoff: { + numberOfRetries: 3, factor: 2, exponentBase: 2 } diff --git a/src/content/docs/how-to/execute-custom-scripts.mdx b/src/content/docs/how-to/execute-custom-scripts.mdx index 874a30d9..48cd7022 100644 --- a/src/content/docs/how-to/execute-custom-scripts.mdx +++ b/src/content/docs/how-to/execute-custom-scripts.mdx @@ -147,29 +147,9 @@ The following steps shows how to run a simple a custom Lua script using GLIDE. - 1. Define the lua script - - ```typescript - const lua = ` - server.call('SET', KEYS[1], ARGV[1]) - return KEYS[1] .. ': ' .. server.call('GET', KEYS[1]) - `; - ``` - - 2. Create a `Script` object with the Lua code - - ```typescript - const script = new Script(lua); - ``` - - 3. Execute script with keys and arguments - - ```typescript - const keys = ["username"]; - const args = ["John Doe"]; - const result = await client.invokeScript(script, {keys, args}); - console.log(result); // username: John Doe - ``` + 1. Define the Lua script as a string. + 2. Create a `Script` object with the Lua code. + 3. Execute the script using `invokeScript`.
diff --git a/src/content/docs/how-to/load-and-execute-functions.mdx b/src/content/docs/how-to/load-and-execute-functions.mdx index 9bab4d8a..dd8466e0 100644 --- a/src/content/docs/how-to/load-and-execute-functions.mdx +++ b/src/content/docs/how-to/load-and-execute-functions.mdx @@ -158,31 +158,9 @@ The following example shows a simple example of loading and executing a Valkey F - 1. Define the lua script. - - ```typescript - const luaCode = `#!lua name=example_module - server.register_function('set_then_get', function(key, value) - server.call('SET', key, value) - return server.call('GET', key) - end) - `; - ``` - + 1. Define the Lua script. 2. Load the function to Valkey. - - ```typescript - await client.functionLoad(luaCode, {replace: true}); - ``` - 3. Call the function. - - ```typescript - await client.set("page:home:visits", "0"); - const result = await client.fcall("set_then_get", - ["page:home:visits"], ["1"]); - console.log(result); // 1 - ```
@@ -458,20 +436,20 @@ GLIDE clients implement `FCALL_RO` command to execute read-only functions. ```typescript - const result = await client.fcallReadonly("get_value", ["mykey"]); + const result = await client.fcallReadonly("get_value", ["mykey"], []); ``` **With Routing (Cluster Mode)** ```typescript // Route to all nodes - const result = await client.fcallReadonlyWithRoute("get_value", [], { route: "allNodes" }); + const allNodesResult = await clusterClient.fcallReadonlyWithRoute("get_value", [], { route: "allNodes" }); // Route to all primary nodes - const result = await client.fcallReadonlyWithRoute("get_value", [], { route: "allPrimaries" }); + const primaryResult = await clusterClient.fcallReadonlyWithRoute("get_value", [], { route: "allPrimaries" }); // Route to a random node - const result = await client.fcallReadonlyWithRoute("get_value", [], { route: "randomNode" }); + const randomResult = await clusterClient.fcallReadonlyWithRoute("get_value", [], { route: "randomNode" }); ``` diff --git a/src/content/docs/how-to/monitoring/tracking-resources.mdx b/src/content/docs/how-to/monitoring/tracking-resources.mdx index 5bcf26de..97a0303c 100644 --- a/src/content/docs/how-to/monitoring/tracking-resources.mdx +++ b/src/content/docs/how-to/monitoring/tracking-resources.mdx @@ -73,7 +73,7 @@ This is not supported by the Go client. }); // Retrieve statistics - const stats = await client.getStatistics(); + const stats = await client.getStatistics() as Record; // Example: Accessing and printing statistics console.log(`Total Connections: ${stats.total_connections}`); diff --git a/src/content/docs/how-to/publish-and-subscribe-messages.mdx b/src/content/docs/how-to/publish-and-subscribe-messages.mdx index 005e57f5..4537a9a8 100644 --- a/src/content/docs/how-to/publish-and-subscribe-messages.mdx +++ b/src/content/docs/how-to/publish-and-subscribe-messages.mdx @@ -535,7 +535,7 @@ To use callback-based delivery, provide a subscription configuration with a call ```typescript - const received: string[] = []; + const received: GlideString[] = []; const callback = (msg: PubSubMsg, context: any) => { received.push(msg.message); console.log(`Received '${msg.message}' on '${msg.channel}'`); @@ -555,12 +555,16 @@ To use callback-based delivery, provide a subscription configuration with a call await client.subscribe(new Set(["news"]), 5000); // Publish a message (from another client) + const publishingClient = await GlideClient.createClient({ + addresses: [{ host: "localhost", port: 6379 }], + }); await publishingClient.publish("Hello!", "news"); await new Promise((r) => setTimeout(r, 500)); // Verify the callback received the message console.assert(received.includes("Hello!")); + publishingClient.close(); client.close(); ``` diff --git a/src/content/docs/how-to/scan-cluster.mdx b/src/content/docs/how-to/scan-cluster.mdx index c633b5bc..88222601 100644 --- a/src/content/docs/how-to/scan-cluster.mdx +++ b/src/content/docs/how-to/scan-cluster.mdx @@ -52,11 +52,11 @@ For a detailed explanation of Cluster Scan, see our [article](/concepts/client-f ```typescript let cursor = new ClusterScanCursor(); - const allKeys: string[] = []; - let keys: string[] = []; + const allKeys: GlideString[] = []; while (!cursor.isFinished()) { - [cursor, keys] = await client.scan(cursor); + let keys; + [cursor, keys] = await clusterClient.scan(cursor); allKeys.push(...keys); } ``` @@ -162,10 +162,11 @@ For a detailed explanation of Cluster Scan, see our [article](/concepts/client-f { key: "something_else", value: "value4" } ]); let cursor = new ClusterScanCursor(); - const matchedKeys: string[] = []; + const matchedKeys: GlideString[] = []; while (!cursor.isFinished()) { - [cursor, keys] = await client.scan(cursor, { match: "*key*" }); + let keys; + [cursor, keys] = await clusterClient.scan(cursor, { match: "*key*" }); matchedKeys.push(...keys); } // Returns matching keys such as ["my_key1", "my_key2", "not_my_key"] @@ -288,10 +289,11 @@ For a detailed explanation of Cluster Scan, see our [article](/concepts/client-f { key: "something_else", value: "value4" } ]); let cursor = new ClusterScanCursor(); - const allKeys: string[] = []; + const allKeys: GlideString[] = []; while (!cursor.isFinished()) { - [cursor, keys] = await client.scan(cursor, { count: 10 }); + let keys; + [cursor, keys] = await clusterClient.scan(cursor, { count: 10 }); allKeys.push(...keys); } // Returns around `count` keys per iteration @@ -405,6 +407,8 @@ For a detailed explanation of Cluster Scan, see our [article](/concepts/client-f ```typescript + import { ObjectType } from "@valkey/valkey-glide"; + await client.mset([ { key: "key1", value: "value1" }, { key: "key2", value: "value2" }, @@ -412,10 +416,11 @@ For a detailed explanation of Cluster Scan, see our [article](/concepts/client-f ]); await client.sadd("thisIsASet", ["value4"]); let cursor = new ClusterScanCursor(); - const stringKeys: string[] = []; + const stringKeys: GlideString[] = []; while (!cursor.isFinished()) { - [cursor, keys] = await client.scan(cursor, { type: ObjectType.STRING }); + let keys; + [cursor, keys] = await clusterClient.scan(cursor, { type: ObjectType.STRING }); stringKeys.push(...keys); } // Output: ["key1", "key2", "key3"] diff --git a/src/content/docs/reference/connection-options.mdx b/src/content/docs/reference/connection-options.mdx index 47e32dbf..bd4e0102 100644 --- a/src/content/docs/reference/connection-options.mdx +++ b/src/content/docs/reference/connection-options.mdx @@ -42,9 +42,9 @@ The following are the configuration references for each Glide clients: ```typescript - import { GlideClientOptions } from "@valkey/valkey-glide"; + import { GlideClientConfiguration } from "@valkey/valkey-glide"; - const config: GlideClientOptions = { + const config: GlideClientConfiguration = { addresses: [{ host: "localhost", port: 6379 }], useTLS: false, requestTimeout: 1000, diff --git a/src/content/docs/reference/scripting-reference.mdx b/src/content/docs/reference/scripting-reference.mdx index a0f54b55..f9438ded 100644 --- a/src/content/docs/reference/scripting-reference.mdx +++ b/src/content/docs/reference/scripting-reference.mdx @@ -1170,7 +1170,9 @@ When a client timeout occurs, the client stops waiting for a response, but the s ```typescript - import {RequestError} from "@valkey/valkey-glide"; + import {RequestError, Script} from "@valkey/valkey-glide"; + + const script = new Script("return redis.call('MGET', KEYS[1], KEYS[2])"); // Handle cluster routing errors try { From 062a72dc8cda7235cb225ccedd27f31cd51d153b Mon Sep 17 00:00:00 2001 From: Alex Le Date: Mon, 22 Jun 2026 14:25:59 -0700 Subject: [PATCH 07/17] Use separate workflows Signed-off-by: Alex Le --- .github/workflows/check-code-examples.yml | 12 ------------ .github/workflows/check-csharp-examples.yml | 4 ++-- .github/workflows/check-node-examples.yml | 3 ++- 3 files changed, 4 insertions(+), 15 deletions(-) delete mode 100644 .github/workflows/check-code-examples.yml diff --git a/.github/workflows/check-code-examples.yml b/.github/workflows/check-code-examples.yml deleted file mode 100644 index d021a471..00000000 --- a/.github/workflows/check-code-examples.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: Check Code Examples - -on: - pull_request: - branches: [main] - workflow_dispatch: - -jobs: - csharp: - uses: ./.github/workflows/check-csharp-examples.yml - node: - uses: ./.github/workflows/check-node-examples.yml diff --git a/.github/workflows/check-csharp-examples.yml b/.github/workflows/check-csharp-examples.yml index 19665e14..ae6a2641 100644 --- a/.github/workflows/check-csharp-examples.yml +++ b/.github/workflows/check-csharp-examples.yml @@ -1,8 +1,8 @@ name: Check C# Examples on: - workflow_call: - workflow_dispatch: + pull_request: + branches: [main] permissions: contents: read diff --git a/.github/workflows/check-node-examples.yml b/.github/workflows/check-node-examples.yml index a739035e..c80f9d42 100644 --- a/.github/workflows/check-node-examples.yml +++ b/.github/workflows/check-node-examples.yml @@ -5,7 +5,8 @@ name: Check Node Examples # See scripts/validators/node.py. on: - workflow_call: + pull_request: + branches: [main] workflow_dispatch: permissions: From 29c542a0ed357969c328a7aecde87d7672fdd403 Mon Sep 17 00:00:00 2001 From: Alex Le Date: Mon, 22 Jun 2026 14:31:53 -0700 Subject: [PATCH 08/17] renamed and moved validator scripts Signed-off-by: Alex Le --- .github/workflows/check-csharp-examples.yml | 2 +- .github/workflows/check-node-examples.yml | 4 ++-- .../check-csharp-examples.py} | 7 ++++--- scripts/validators/{node.py => check-node-examples.py} | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) rename scripts/{check_csharp_examples.py => validators/check-csharp-examples.py} (94%) rename scripts/validators/{node.py => check-node-examples.py} (99%) diff --git a/.github/workflows/check-csharp-examples.yml b/.github/workflows/check-csharp-examples.yml index ae6a2641..bf873658 100644 --- a/.github/workflows/check-csharp-examples.yml +++ b/.github/workflows/check-csharp-examples.yml @@ -44,6 +44,6 @@ jobs: - name: Run C# Example Validation run: | - python scripts/check_csharp_examples.py \ + python scripts/validators/check-csharp-examples.py \ --validator valkey-glide-csharp/dev/scripts/validate_examples.py \ --glide-dll valkey-glide-csharp/sources/Valkey.Glide/bin/Release/net8.0/Valkey.Glide.dll diff --git a/.github/workflows/check-node-examples.yml b/.github/workflows/check-node-examples.yml index c80f9d42..4339c72f 100644 --- a/.github/workflows/check-node-examples.yml +++ b/.github/workflows/check-node-examples.yml @@ -2,7 +2,7 @@ name: Check Node Examples # Full-compilation validates the Node.js (TypeScript) code examples in the # docs against the real @valkey/valkey-glide type definitions built from source. -# See scripts/validators/node.py. +# See scripts/validators/check-node-examples.py. on: pull_request: @@ -57,4 +57,4 @@ jobs: python-version: "3.x" - name: Validate Node examples - run: python scripts/validators/node.py --glide-path valkey-glide/node + run: python scripts/validators/check-node-examples.py --glide-path valkey-glide/node diff --git a/scripts/check_csharp_examples.py b/scripts/validators/check-csharp-examples.py similarity index 94% rename from scripts/check_csharp_examples.py rename to scripts/validators/check-csharp-examples.py index 42757e4a..7352db58 100644 --- a/scripts/check_csharp_examples.py +++ b/scripts/validators/check-csharp-examples.py @@ -7,7 +7,7 @@ 4. Cleans up and propagates the exit code. Usage: - python scripts/check_csharp_examples.py + python scripts/validators/check-csharp-examples.py --validator --glide-dll @@ -24,8 +24,9 @@ import sys import tempfile -# Repository root is one level up from this script's directory (scripts/). -_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# Repository root is two levels up from this script's directory +# (scripts/validators/). +_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) _DOCS_DIR = os.path.join(_REPO_ROOT, "src", "content", "docs") # Matches a ```csharp ... ``` fenced code block, capturing the content. diff --git a/scripts/validators/node.py b/scripts/validators/check-node-examples.py similarity index 99% rename from scripts/validators/node.py rename to scripts/validators/check-node-examples.py index ef0d5bfc..a761a724 100644 --- a/scripts/validators/node.py +++ b/scripts/validators/check-node-examples.py @@ -8,7 +8,7 @@ Self-contained: no external Python dependencies beyond the standard library. Usage: - python scripts/validators/node.py --glide-path ../valkey-glide/node + python scripts/validators/check-node-examples.py --glide-path ../valkey-glide/node Requires a pre-built Node client (run ``npm ci && npm run build:release`` in the valkey-glide/node directory first). From 255b9b119417c35bb09a6f2c4bfb90722f7ed620 Mon Sep 17 00:00:00 2001 From: Alex Le Date: Wed, 1 Jul 2026 16:28:35 -0700 Subject: [PATCH 09/17] fix: added workflow_dispatch to c# validator Signed-off-by: Alex Le --- .github/workflows/check-csharp-examples.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/check-csharp-examples.yml b/.github/workflows/check-csharp-examples.yml index bf873658..f894aaaf 100644 --- a/.github/workflows/check-csharp-examples.yml +++ b/.github/workflows/check-csharp-examples.yml @@ -3,6 +3,7 @@ name: Check C# Examples on: pull_request: branches: [main] + workflow_dispatch: permissions: contents: read From 244a62909f5958846e31af492cf57f4985857389 Mon Sep 17 00:00:00 2001 From: Alex Le Date: Wed, 1 Jul 2026 16:30:02 -0700 Subject: [PATCH 10/17] added common.py with extract_all() Signed-off-by: Alex Le --- scripts/validators/_common.py | 66 +++++++++++++++++++++ scripts/validators/check-csharp-examples.py | 41 +------------ 2 files changed, 69 insertions(+), 38 deletions(-) create mode 100644 scripts/validators/_common.py diff --git a/scripts/validators/_common.py b/scripts/validators/_common.py new file mode 100644 index 00000000..098017b5 --- /dev/null +++ b/scripts/validators/_common.py @@ -0,0 +1,66 @@ +"""Shared helpers for the per-language example validators. + +Every validator (C#, Node, and any future language) extracts fenced code +blocks from the MDX docs the same way — only the language tag(s) and the +files to skip differ. Centralizing that logic keeps future validators +consistent and makes it easy to fix bugs (e.g. in the extraction regex) +in one place. +""" + +from __future__ import annotations + +import os +import re + +# Repository root is two levels up from this script's directory +# (scripts/validators/). +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +DOCS_DIR = os.path.join(REPO_ROOT, "src", "content", "docs") + + +def extract_all( + languages: list[str], + *, + skip_patterns: list[str] | None = None, +) -> dict[str, str]: + """Recursively walk the docs directory and extract fenced code blocks. + + Args: + languages: Fence language tags to match, e.g. ``["csharp"]`` or + ``["typescript", "ts", "javascript", "js"]``. + skip_patterns: Regexes matched against the path of each MDX file + (relative to the docs directory). Any file matching one of + these patterns is skipped entirely. For example, Node skips + migration guides (``r"^migration"``) and IAM guides + (``r"iam-"``) because they reference APIs outside GLIDE. + + Returns: + A dict mapping ``":"`` to the + extracted code string. + """ + fence_re = re.compile( + r"^\s*```(?:" + "|".join(languages) + r")\s*\n(.*?)^\s*```\s*$", + re.MULTILINE | re.DOTALL, + ) + compiled_skips = [re.compile(p) for p in (skip_patterns or [])] + + examples: dict[str, str] = {} + for root, _dirs, files in os.walk(DOCS_DIR): + for fname in sorted(files): + if not fname.endswith(".mdx"): + continue + + filepath = os.path.join(root, fname) + rel_path = os.path.relpath(filepath, DOCS_DIR) + if any(skip.search(rel_path) for skip in compiled_skips): + continue + + with open(filepath, encoding="utf-8") as fh: + content = fh.read() + + for match in fence_re.finditer(content): + key_path = os.path.relpath(filepath, REPO_ROOT) + line_number = content[: match.start()].count("\n") + 1 + examples[f"{key_path}:{line_number}"] = match.group(1) + + return examples diff --git a/scripts/validators/check-csharp-examples.py b/scripts/validators/check-csharp-examples.py index 7352db58..87bf5eeb 100644 --- a/scripts/validators/check-csharp-examples.py +++ b/scripts/validators/check-csharp-examples.py @@ -19,47 +19,12 @@ import argparse import json import os -import re import subprocess import sys import tempfile -# Repository root is two levels up from this script's directory -# (scripts/validators/). -_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -_DOCS_DIR = os.path.join(_REPO_ROOT, "src", "content", "docs") - -# Matches a ```csharp ... ``` fenced code block, capturing the content. -# Allows leading whitespace before fences (common in MDX tab components). -_CSHARP_BLOCK_RE = re.compile( - r"^\s*```csharp\s*\n(.*?)^\s*```\s*$", - re.MULTILINE | re.DOTALL, -) - - -def extract_all(docs_dir: str) -> dict[str, str]: - """Recursively walk docs_dir for .mdx files and extract C# blocks. - - Returns a dict mapping ":" to the - code string. - """ - examples: dict[str, str] = {} - - for root, _dirs, files in os.walk(docs_dir): - for fname in sorted(files): - if not fname.endswith(".mdx"): - continue - - filepath = os.path.join(root, fname) - with open(filepath, encoding="utf-8") as fh: - content = fh.read() - - for match in _CSHARP_BLOCK_RE.finditer(content): - key_path = os.path.relpath(filepath, _REPO_ROOT) - line_number = content[: match.start()].count("\n") + 1 - examples[f"{key_path}:{line_number}"] = match.group(1) - - return examples +from _common import DOCS_DIR as _DOCS_DIR +from _common import extract_all as _extract_all def main() -> None: @@ -104,7 +69,7 @@ def main() -> None: sys.exit(1) # Step 1: Extract examples from MDX files - examples = extract_all(_DOCS_DIR) + examples = _extract_all(["csharp"]) print(f"Extracted {len(examples)} C# code example(s).") if not examples: From 0761d6aa076cbcfecdc739e876971b100010518e Mon Sep 17 00:00:00 2001 From: Alex Le Date: Wed, 1 Jul 2026 16:31:58 -0700 Subject: [PATCH 11/17] refactored node example validator Signed-off-by: Alex Le --- scripts/validators/check-node-examples.py | 131 ++++++++++++++-------- 1 file changed, 83 insertions(+), 48 deletions(-) diff --git a/scripts/validators/check-node-examples.py b/scripts/validators/check-node-examples.py index a761a724..3243a64e 100644 --- a/scripts/validators/check-node-examples.py +++ b/scripts/validators/check-node-examples.py @@ -1,9 +1,14 @@ #!/usr/bin/env python3 """Full-compilation validator for Node (TypeScript) documentation examples. -Extracts TypeScript/JavaScript code blocks from the MDX docs and compiles -each snippet against the real ``@valkey/valkey-glide`` type definitions -using ``tsc --noEmit``. +Follows the same overall pattern as ``check-csharp-examples.py``: + +1. Extracts TypeScript/JavaScript code blocks from the MDX docs. +2. Wraps each snippet into a compilable ``.ts`` file (injecting default + imports and ambient client declarations). +3. Compiles everything in one pass with ``tsc --noEmit`` against the real + ``@valkey/valkey-glide`` package built from source. +4. Parses the compiler output and reports failures per source location. Self-contained: no external Python dependencies beyond the standard library. @@ -12,6 +17,16 @@ Requires a pre-built Node client (run ``npm ci && npm run build:release`` in the valkey-glide/node directory first). + +Options: + --glide-path Path to the built valkey-glide/node directory. We point + at the directory (rather than directly at + build-ts/index.d.ts) so npm can resolve the package via + its package.json "exports"/"types"/"main" fields — + this also correctly follows any relative imports + across the package's other .d.ts files. + --keep-project Preserve the temporary TypeScript project directory + instead of deleting it, for local debugging. """ from __future__ import annotations @@ -26,48 +41,35 @@ import tempfile import textwrap +from _common import extract_all as _extract_all + # --------------------------------------------------------------------------- # Extraction # --------------------------------------------------------------------------- -_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -_DOCS_DIR = os.path.join(_REPO_ROOT, "src", "content", "docs") +# Fence language tags this validator extracts. +_LANGUAGES = ["typescript", "ts", "javascript", "js"] -_TS_BLOCK_RE = re.compile( - r"^\s*```(?:typescript|ts|javascript|js)\s*\n(.*?)^\s*```\s*$", - re.MULTILINE | re.DOTALL, -) - - -def _extract_examples() -> dict[str, str]: - """Extract TypeScript/JS code blocks from all MDX files.""" - examples: dict[str, str] = {} - for root, _dirs, files in os.walk(_DOCS_DIR): - # Skip migration guides — they contain comparison snippets from other clients - rel_root = os.path.relpath(root, _DOCS_DIR) - if rel_root.startswith("migration"): - continue - for fname in sorted(files): - if not fname.endswith(".mdx"): - continue - # Skip IAM integration guides — they import AWS SDK packages - if fname.startswith("iam-"): - continue - filepath = os.path.join(root, fname) - with open(filepath, encoding="utf-8") as fh: - content = fh.read() - for match in _TS_BLOCK_RE.finditer(content): - key_path = os.path.relpath(filepath, _REPO_ROOT) - line_number = content[: match.start()].count("\n") + 1 - examples[f"{key_path}:{line_number}"] = match.group(1) - return examples +# Files/directories skipped during extraction: +# - migration guides contain comparison snippets from other clients +# - IAM integration guides import AWS SDK packages we don't install +_SKIP_PATTERNS = [r"^migration", r"iam-"] # --------------------------------------------------------------------------- # Wrapping # --------------------------------------------------------------------------- -_IMPORT_LINE = re.compile(r"^\s*import\s+") +# Matches the start of an import statement. +_IMPORT_LINE_RE = re.compile(r"^\s*import\s+") + +# Matches the closing `from "..."` of an import statement, to detect where +# a (possibly multi-line) import statement ends. +_IMPORT_END_RE = re.compile(r"""from\s+["'][^"']+["']\s*;?\s*$""") + +# Matches the `{ Named, Imports }` portion of an import statement, so we can +# tell which names a snippet already imports for itself. +_NAMED_IMPORTS_RE = re.compile(r"\{([^}]+)\}") _DEFAULT_IMPORTS = ( "GlideClient, GlideClusterClient, GlideClientConfiguration, " @@ -88,16 +90,30 @@ def _extract_examples() -> dict[str, str]: def _split_imports(code: str) -> tuple[list[str], list[str]]: - """Separate import statements (including multi-line) from the rest.""" + """Separate import statements (including multi-line) from the rest. + + Import statements can span multiple lines, e.g.: + + import { + GlideClient, + GlideClusterClient, + } from "@valkey/valkey-glide"; + + We scan line by line: once a line starts an import, we keep consuming + lines until we see the closing `from "...";` (via ``_IMPORT_END_RE``), + which marks the end of that statement. Every other line is treated as + part of the snippet's body. + """ imports: list[str] = [] body: list[str] = [] lines = code.splitlines() i = 0 while i < len(lines): line = lines[i] - if _IMPORT_LINE.match(line): + if _IMPORT_LINE_RE.match(line): import_lines = [line] - while not re.search(r"""from\s+["'][^"']+["']\s*;?\s*$""", line) and i + 1 < len(lines): + # Keep consuming lines until this import statement closes. + while not _IMPORT_END_RE.search(line) and i + 1 < len(lines): i += 1 line = lines[i] import_lines.append(line) @@ -117,7 +133,7 @@ def _wrap_snippet(code: str) -> str: # Inject default imports, deduped against snippet's own existing_names: set[str] = set() for imp in imports: - m = re.search(r"\{([^}]+)\}", imp) + m = _NAMED_IMPORTS_RE.search(imp) if m: for name in m.group(1).split(","): existing_names.add(name.strip()) @@ -215,14 +231,22 @@ def _run_tsc(tmp_dir: str) -> str: return proc.stdout + proc.stderr -_TSC_ERROR = re.compile(r"^(example_\d+\.ts)\((\d+),(\d+)\):\s+error\s+TS\d+:\s+(.+)$") +# Matches a single tsc error line, e.g.: +# example_0001.ts(12,5): error TS2304: Cannot find name 'foo'. +_TSC_ERROR_RE = re.compile(r"^(example_\d+\.ts)\((\d+),(\d+)\):\s+error\s+TS\d+:\s+(.+)$") def _parse_tsc_errors(output: str) -> dict[str, list[str]]: - """Parse tsc output into {filename: [messages]}.""" + """Parse tsc output into per-file error messages. + + Returns: + A dict mapping each generated example filename (e.g. + ``"example_0001.ts"``) to a list of human-readable error strings, + one per compiler diagnostic raised against that file. + """ errors: dict[str, list[str]] = {} for line in output.splitlines(): - m = _TSC_ERROR.match(line) + m = _TSC_ERROR_RE.match(line) if m: filename, line_no, _col, message = m.groups() errors.setdefault(filename, []).append(f"line {line_no}: {message}") @@ -235,7 +259,21 @@ def validate( glide_path: str, keep_project: bool = False, ) -> dict[str, list[str]]: - """Compile all snippets and return {source: [errors]}.""" + """Compile all snippets and collect any errors. + + Args: + examples: Mapping of ``":"`` to snippet code, as + produced by ``_common.extract_all``. + glide_path: Path to the built valkey-glide/node directory. + keep_project: If True, don't delete the temp project afterwards. + + Returns: + A dict mapping each ``source`` key from ``examples`` to the list of + compiler error messages raised against that snippet. Sources that + compiled cleanly are omitted entirely (never mapped to an empty + list), since only failing example filenames appear in the tsc + output that ``_parse_tsc_errors`` parses. + """ tmp_dir = tempfile.mkdtemp(prefix="glide_node_validate_") try: print("Setting up TypeScript project...", flush=True) @@ -282,7 +320,8 @@ def main() -> None: ) parser.add_argument( "--keep-project", action="store_true", - help="Keep the temp project directory for inspection.", + help="Keep the temp project directory for inspection (useful when " + "debugging a failing snippet locally).", ) args = parser.parse_args() @@ -303,12 +342,8 @@ def main() -> None: _require_tool("node") _require_tool("npm") - if not os.path.isdir(_DOCS_DIR): - print(f"Error: docs directory not found: {_DOCS_DIR}", file=sys.stderr) - sys.exit(1) - print("Extracting TypeScript examples from MDX docs...", flush=True) - examples = _extract_examples() + examples = _extract_all(_LANGUAGES, skip_patterns=_SKIP_PATTERNS) print(f"Extracted {len(examples)} example(s).", flush=True) if not examples: From 8a7a4fb2f4a335596eb8b4000ba6ebe2a3859b9b Mon Sep 17 00:00:00 2001 From: Alex Le Date: Wed, 1 Jul 2026 16:56:25 -0700 Subject: [PATCH 12/17] refactored _wrap_snippet Signed-off-by: Alex Le --- scripts/validators/check-node-examples.py | 81 +++++++++++++++-------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/scripts/validators/check-node-examples.py b/scripts/validators/check-node-examples.py index 3243a64e..37041441 100644 --- a/scripts/validators/check-node-examples.py +++ b/scripts/validators/check-node-examples.py @@ -124,39 +124,64 @@ def _split_imports(code: str) -> tuple[list[str], list[str]]: return imports, body -def _wrap_snippet(code: str) -> str: - """Wrap a snippet into a compilable .ts file.""" - imports, body_lines = _split_imports(code) - - parts: list[str] = [] - - # Inject default imports, deduped against snippet's own - existing_names: set[str] = set() - for imp in imports: - m = _NAMED_IMPORTS_RE.search(imp) - if m: - for name in m.group(1).split(","): - existing_names.add(name.strip()) - defaults = [ - n.strip() for n in _DEFAULT_IMPORTS.split(",") - if n.strip() not in existing_names +def _collect_imported_names(imports: list[str]) -> set[str]: + """Collect all named imports (`{ A, B }`) already present in a snippet.""" + names: set[str] = set() + for import_statement in imports: + match = _NAMED_IMPORTS_RE.search(import_statement) + if not match: + continue + names.update(name.strip() for name in match.group(1).split(",")) + return names + + +def _build_default_import_statement(imported_names: set[str]) -> str: + """Build the injected `import { ... } from "@valkey/valkey-glide"` line. + + Names the snippet already imports itself are excluded to avoid + duplicate-import compiler errors. + """ + missing_names = [ + name.strip() for name in _DEFAULT_IMPORTS.split(",") + if name.strip() not in imported_names ] - if defaults: - parts.append(f"import {{ {', '.join(defaults)} }} from \"@valkey/valkey-glide\";\n") + if not missing_names: + return "" + return f"import {{ {', '.join(missing_names)} }} from \"@valkey/valkey-glide\";\n" - # Hoisted imports from snippet - if imports: - parts.append("\n".join(imports) + "\n") - # Client declarations - parts.append("\n" + _CLIENT_DECLARATIONS) - - # Async wrapper for the body +def _build_async_wrapper(body_lines: list[str]) -> str: + """Wrap the snippet's body lines in an async function, if non-empty.""" body = "\n".join(body_lines).strip() - if body: - parts.append(f"\nasync function __run() {{\n{textwrap.indent(body, ' ')}\n}}\n") + if not body: + return "" + return f"\nasync function __run() {{\n{textwrap.indent(body, ' ')}\n}}\n" + - return "\n".join(parts) +def _wrap_snippet(code: str) -> str: + """Wrap a snippet into a compilable .ts file. + + Steps: + 1. Split the snippet into its own imports and body. + 2. Build the injected default-import line, deduplicating against the + snippet's imports. + 3. Reassemble: default imports, snippet imports, client + declarations, then the snippet body wrapped in an async function. + """ + snippet_imports, snippet_body_lines = _split_imports(code) + imported_names = _collect_imported_names(snippet_imports) + + default_import_statement = _build_default_import_statement(imported_names) + snippet_import_block = "\n".join(snippet_imports) + "\n" if snippet_imports else "" + async_wrapper = _build_async_wrapper(snippet_body_lines) + + file_parts = [ + default_import_statement, + snippet_import_block, + "\n" + _CLIENT_DECLARATIONS, + async_wrapper, + ] + return "\n".join(part for part in file_parts if part) # --------------------------------------------------------------------------- From 7ac52e65e5061831ba6a317c6b158e51c0aa7468 Mon Sep 17 00:00:00 2001 From: Alex Le Date: Wed, 1 Jul 2026 17:06:30 -0700 Subject: [PATCH 13/17] removed validate check Signed-off-by: Alex Le --- scripts/validators/check-node-examples.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/scripts/validators/check-node-examples.py b/scripts/validators/check-node-examples.py index 37041441..6e3875c2 100644 --- a/scripts/validators/check-node-examples.py +++ b/scripts/validators/check-node-examples.py @@ -295,9 +295,8 @@ def validate( Returns: A dict mapping each ``source`` key from ``examples`` to the list of compiler error messages raised against that snippet. Sources that - compiled cleanly are omitted entirely (never mapped to an empty - list), since only failing example filenames appear in the tsc - output that ``_parse_tsc_errors`` parses. + compiled cleanly are omitted entirely, since ``_parse_tsc_errors`` + only returns entries for files tsc actually reported errors on. """ tmp_dir = tempfile.mkdtemp(prefix="glide_node_validate_") try: @@ -316,11 +315,9 @@ def validate( output = _run_tsc(tmp_dir) file_errors = _parse_tsc_errors(output) - result: dict[str, list[str]] = {} - for filename, msgs in file_errors.items(): - source = file_to_source.get(filename) - if source: - result[source] = msgs + result: dict[str, list[str]] = { + file_to_source[filename]: msgs for filename, msgs in file_errors.items() + } finally: if keep_project: print(f"\nProject kept at: {tmp_dir}", flush=True) From 04cbc86a535839aba3c22466d66c65f609d72624 Mon Sep 17 00:00:00 2001 From: Alex Le Date: Wed, 1 Jul 2026 17:58:10 -0700 Subject: [PATCH 14/17] fixed node code example Signed-off-by: Alex Le --- src/content/docs/how-to/connections/address-resolver.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/content/docs/how-to/connections/address-resolver.mdx b/src/content/docs/how-to/connections/address-resolver.mdx index fa8053d4..00fccc2a 100644 --- a/src/content/docs/how-to/connections/address-resolver.mdx +++ b/src/content/docs/how-to/connections/address-resolver.mdx @@ -329,6 +329,11 @@ The cluster client supports the same resolver signature. ```typescript import { GlideClusterClient } from "@valkey/valkey-glide"; + // Your custom host resolution logic + function getActualHost(host: string): string { + return host; + } + const client = await GlideClusterClient.createClient({ addresses: [{ host: "internal-dns.service", port: 7000 }], addressResolver: (host: string, port: number): [string, number] => { From 17eb384ca50282c62613535d956e7534fd18edee Mon Sep 17 00:00:00 2001 From: Alex Le Date: Wed, 1 Jul 2026 17:58:42 -0700 Subject: [PATCH 15/17] Changed to node-index instead Signed-off-by: Alex Le --- .github/workflows/check-node-examples.yml | 2 +- scripts/validators/check-node-examples.py | 54 +++++++++++------------ 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/check-node-examples.yml b/.github/workflows/check-node-examples.yml index 4339c72f..a1156fb9 100644 --- a/.github/workflows/check-node-examples.yml +++ b/.github/workflows/check-node-examples.yml @@ -57,4 +57,4 @@ jobs: python-version: "3.x" - name: Validate Node examples - run: python scripts/validators/check-node-examples.py --glide-path valkey-glide/node + run: python scripts/validators/check-node-examples.py --glide-index valkey-glide/node/build-ts/index.d.ts diff --git a/scripts/validators/check-node-examples.py b/scripts/validators/check-node-examples.py index 6e3875c2..4269ec97 100644 --- a/scripts/validators/check-node-examples.py +++ b/scripts/validators/check-node-examples.py @@ -13,18 +13,16 @@ Self-contained: no external Python dependencies beyond the standard library. Usage: - python scripts/validators/check-node-examples.py --glide-path ../valkey-glide/node + python scripts/validators/check-node-examples.py --glide-index ../valkey-glide/node/build-ts/index.d.ts Requires a pre-built Node client (run ``npm ci && npm run build:release`` in the valkey-glide/node directory first). Options: - --glide-path Path to the built valkey-glide/node directory. We point - at the directory (rather than directly at - build-ts/index.d.ts) so npm can resolve the package via - its package.json "exports"/"types"/"main" fields — - this also correctly follows any relative imports - across the package's other .d.ts files. + --glide-index Path to the built valkey-glide/node/build-ts/index.d.ts + file. Mapped directly to the `@valkey/valkey-glide` + import via a tsconfig `paths` entry, mirroring the + C# validator's `--glide-dll`. --keep-project Preserve the temporary TypeScript project directory instead of deleting it, for local debugging. """ @@ -195,18 +193,18 @@ def _require_tool(name: str) -> None: sys.exit(1) -def _setup_project(tmp_dir: str, glide_path: str) -> None: - """Create a temp TypeScript project referencing the local GLIDE build.""" - glide_abs = os.path.abspath(glide_path) +def _setup_project(tmp_dir: str, glide_index: str) -> None: + """Create a temp TypeScript project referencing the local GLIDE build. + Maps the `@valkey/valkey-glide` import directly to the built + `index.d.ts` via a `paths` entry, so no `npm install` of the GLIDE + package itself is needed — only the TypeScript compiler is installed. + """ package_json = json.dumps( { "name": "glide-doc-validator", "private": True, "type": "module", - "dependencies": { - "@valkey/valkey-glide": f"file:{glide_abs}", - }, }, indent=2, ) @@ -221,6 +219,10 @@ def _setup_project(tmp_dir: str, glide_path: str) -> None: "skipLibCheck": True, "esModuleInterop": True, "types": ["node"], + "baseUrl": ".", + "paths": { + "@valkey/valkey-glide": [glide_index], + }, }, "include": ["*.ts"], }, @@ -281,7 +283,7 @@ def _parse_tsc_errors(output: str) -> dict[str, list[str]]: def validate( examples: dict[str, str], *, - glide_path: str, + glide_index: str, keep_project: bool = False, ) -> dict[str, list[str]]: """Compile all snippets and collect any errors. @@ -289,7 +291,8 @@ def validate( Args: examples: Mapping of ``":"`` to snippet code, as produced by ``_common.extract_all``. - glide_path: Path to the built valkey-glide/node directory. + glide_index: Path to the built valkey-glide/node + build-ts/index.d.ts file. keep_project: If True, don't delete the temp project afterwards. Returns: @@ -301,7 +304,7 @@ def validate( tmp_dir = tempfile.mkdtemp(prefix="glide_node_validate_") try: print("Setting up TypeScript project...", flush=True) - _setup_project(tmp_dir, glide_path) + _setup_project(tmp_dir, glide_index) file_to_source: dict[str, str] = {} for idx, (source, code) in enumerate(examples.items()): @@ -337,8 +340,8 @@ def main() -> None: description="Full-compilation validator for Node.js (TypeScript) doc examples." ) parser.add_argument( - "--glide-path", required=True, - help="Path to the built valkey-glide/node directory.", + "--glide-index", required=True, + help="Path to the built valkey-glide/node/build-ts/index.d.ts file.", ) parser.add_argument( "--keep-project", action="store_true", @@ -347,16 +350,13 @@ def main() -> None: ) args = parser.parse_args() - glide_path = os.path.abspath(args.glide_path) + glide_index = os.path.abspath(args.glide_index) - # Validate the glide path - if not os.path.isdir(glide_path): - print(f"Error: --glide-path not found: {glide_path}", file=sys.stderr) - sys.exit(1) - if not os.path.isfile(os.path.join(glide_path, "build-ts", "index.d.ts")): + # Validate the glide index path + if not os.path.isfile(glide_index): print( - f"Error: No build-ts/index.d.ts in {glide_path}. " - f"Build the client first: cd {glide_path} && npm ci && npm run build:release", + f"Error: --glide-index not found: {glide_index}. " + f"Build the client first: cd /node && npm ci && npm run build:release", file=sys.stderr, ) sys.exit(1) @@ -375,7 +375,7 @@ def main() -> None: errors = validate( dedented, - glide_path=glide_path, + glide_index=glide_index, keep_project=args.keep_project, ) errors = {s: msgs for s, msgs in errors.items() if msgs} From 934eb202f6ff2034152ebbaba1b88ee03db61497 Mon Sep 17 00:00:00 2001 From: Alex Le Date: Wed, 1 Jul 2026 17:58:49 -0700 Subject: [PATCH 16/17] added readme Signed-off-by: Alex Le --- scripts/validators/README.md | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 scripts/validators/README.md diff --git a/scripts/validators/README.md b/scripts/validators/README.md new file mode 100644 index 00000000..bfb11015 --- /dev/null +++ b/scripts/validators/README.md @@ -0,0 +1,50 @@ +# Example Validators + +Compile the code examples in the docs against the real GLIDE client libraries, to catch broken syntax, wrong method names, and outdated APIs. + +## How they work + +1. Extract fenced code blocks (e.g. ` ```csharp `, ` ```typescript `) from the MDX docs. +2. Wrap each snippet into a compilable file, injecting common imports/client declarations. +3. Compile everything against a GLIDE client you've already built and point the script at. +4. Report any compiler errors per source file/line. + +`_common.py` holds the shared extraction logic used by every language-specific validator. + +## Usage + +**C#** + +You must build `Valkey.Glide.dll` yourself first — the script does not do this for you: + +```bash +cd && dotnet build sources/Valkey.Glide/ --configuration Release /p:SkipCargo=true +``` + +Then run the validator: + +```bash +python scripts/validators/check-csharp-examples.py \ + --validator /dev/scripts/validate_examples.py \ + --glide-dll /sources/Valkey.Glide/bin/Release/net8.0/Valkey.Glide.dll +``` + +**Node.js** + +You must build the client yourself first — the script does not do this for you: + +```bash +cd /node && npm ci && npm run build:release +``` + +Then run the validator: + +```bash +python scripts/validators/check-node-examples.py --glide-index /node/build-ts/index.d.ts +``` + +Both scripts run automatically in CI on every PR (see `.github/workflows/check-csharp-examples.yml` and `.github/workflows/check-node-examples.yml`). + +## Adding a new language + +See `DESIGN.md` for the full pattern to follow when adding a validator for another language. From 2dda23187563bc8b39ca0e1a11c178806d8b8435 Mon Sep 17 00:00:00 2001 From: Alex Le Date: Wed, 1 Jul 2026 18:01:37 -0700 Subject: [PATCH 17/17] cleanup Readme Signed-off-by: Alex Le --- scripts/validators/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/validators/README.md b/scripts/validators/README.md index bfb11015..f9885482 100644 --- a/scripts/validators/README.md +++ b/scripts/validators/README.md @@ -44,7 +44,3 @@ python scripts/validators/check-node-examples.py --glide-index