From 86090313e7ab3182d2dc6fd1755b79a196ed0201 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:19:45 +0530 Subject: [PATCH 01/11] feat(docs): centralize SDKs in a single registry One SDK_PROFILES/SDK_IDS source drives the Sdk type, nav, sidebar, MDX tabs and the no-flash boot script, so adding a language is one entry. --- docs/app/components/docs/sidebar.tsx | 8 +-- docs/app/components/mdx/sdk.tsx | 5 +- docs/app/lib/index.ts | 1 + docs/app/lib/nav.ts | 51 +++++++------------ docs/app/lib/sdk-registry.ts | 76 ++++++++++++++++++++++++++++ docs/app/lib/sdk-store.ts | 10 ++-- docs/app/root.tsx | 9 ++-- 7 files changed, 110 insertions(+), 50 deletions(-) create mode 100644 docs/app/lib/sdk-registry.ts diff --git a/docs/app/components/docs/sidebar.tsx b/docs/app/components/docs/sidebar.tsx index 794e3556..a59d7261 100644 --- a/docs/app/components/docs/sidebar.tsx +++ b/docs/app/components/docs/sidebar.tsx @@ -6,6 +6,7 @@ import { type NavNode, navForSdk, type Sdk, + sdkLabels, sdkSwitchTarget, } from "@/lib"; @@ -16,10 +17,9 @@ function containsHref(node: NavNode, current: string): boolean { ); } -const SDK_LABELS: { id: Sdk; label: string }[] = [ - { id: "python", label: "Python" }, - { id: "node", label: "Node.js" }, -]; +// Switcher options come from the SDK registry, so a new language appears here +// automatically. +const SDK_LABELS = sdkLabels(); /** Global SDK toggle. Sets the shared store (flips inline variants + this nav); * on an SDK-specific page it also navigates to the counterpart page, on a shared diff --git a/docs/app/components/mdx/sdk.tsx b/docs/app/components/mdx/sdk.tsx index 5caa923e..f97fbe37 100644 --- a/docs/app/components/mdx/sdk.tsx +++ b/docs/app/components/mdx/sdk.tsx @@ -9,8 +9,7 @@ import { } from "react"; import { Link } from "react-router"; import { type Sdk, useSdk } from "@/hooks"; - -const LABELS: Record = { python: "Python", node: "Node.js" }; +import { SDK_PROFILES } from "@/lib"; /** Show children only under one SDK. All variants ship in the HTML; the inactive * one is hidden by CSS off `` — no flash, no hydration mismatch. */ @@ -87,7 +86,7 @@ export function CodeTabs({ children }: { children: ReactNode }) { onClick={() => setSdk(variant)} onKeyDown={(e) => onKeyDown(e, index)} > - {LABELS[variant]} + {SDK_PROFILES[variant].label} ); })} diff --git a/docs/app/lib/index.ts b/docs/app/lib/index.ts index 6f6530c7..14b0333e 100644 --- a/docs/app/lib/index.ts +++ b/docs/app/lib/index.ts @@ -1,2 +1,3 @@ export * from "./nav"; +export * from "./sdk-registry"; export * from "./sdk-store"; diff --git a/docs/app/lib/nav.ts b/docs/app/lib/nav.ts index 93e90e58..8ab131b1 100644 --- a/docs/app/lib/nav.ts +++ b/docs/app/lib/nav.ts @@ -1,5 +1,5 @@ import { docTitle, hasDoc } from "./manifest"; -import type { Sdk } from "./sdk-store"; +import { SDK_IDS, SDK_PROFILES, type Sdk } from "./sdk-registry"; interface Meta { title?: string; @@ -83,38 +83,22 @@ function buildTree(sections: string[]): NavNode[] { }); } -// `architecture` and `resources` are SDK-neutral (the engine is identical across -// SDKs); both live at top-level shared URLs and appear in every SDK's nav. -export const PYTHON_SECTIONS = [ - "python/getting-started", - "python/guides", - "architecture", - "python/api-reference", - "python/more/examples", - "resources", -]; -export const NODE_SECTIONS = [ - "node/getting-started", - "node/guides", - "architecture", - "node/api-reference", - "node/more/examples", - "resources", -]; - -export const PYTHON_NAV = buildTree(PYTHON_SECTIONS); -export const NODE_NAV = buildTree(NODE_SECTIONS); +// Each SDK's nav is its registry `navSections` built into a tree. `architecture` +// and `resources` are SDK-neutral (the engine is identical across SDKs); both +// appear in every SDK's section list at shared top-level URLs. +const NAV_BY_SDK = Object.fromEntries( + SDK_IDS.map((id) => [id, buildTree(SDK_PROFILES[id].navSections)]), +) as Record; export type { Sdk }; -/** The SDK forced by an explicit `/python`|`/node` URL prefix, or null on a - * shared page (where the active SDK comes from the global store instead). */ +/** The SDK forced by an explicit `/` URL prefix, or null on a shared page + * (where the active SDK comes from the global store instead). */ export function forcedSdkForPath(path: string): Sdk | null { - if (path === "/node" || path.startsWith("/node/")) { - return "node"; - } - if (path === "/python" || path.startsWith("/python/")) { - return "python"; + for (const id of SDK_IDS) { + if (path === `/${id}` || path.startsWith(`/${id}/`)) { + return id; + } } return null; } @@ -123,10 +107,9 @@ export function forcedSdkForPath(path: string): Sdk | null { * prefix if it exists, else that SDK's install landing. Shared pages stay put * (the caller skips navigation when the current path isn't SDK-prefixed). */ export function sdkSwitchTarget(path: string, target: Sdk): string { - const other: Sdk = target === "node" ? "python" : "node"; - const prefix = `/${other}`; - if (path === prefix || path.startsWith(`${prefix}/`)) { - const swapped = `/${target}${path.slice(prefix.length)}`; + const current = forcedSdkForPath(path); + if (current && current !== target) { + const swapped = `/${target}${path.slice(`/${current}`.length)}`; if (hasDoc(swapped)) { return swapped; } @@ -135,7 +118,7 @@ export function sdkSwitchTarget(path: string, target: Sdk): string { } export function navForSdk(sdk: Sdk): NavNode[] { - return sdk === "node" ? NODE_NAV : PYTHON_NAV; + return NAV_BY_SDK[sdk]; } /** Depth-first flattened links for the active SDK — drives prev/next. */ diff --git a/docs/app/lib/sdk-registry.ts b/docs/app/lib/sdk-registry.ts new file mode 100644 index 00000000..ada9706a --- /dev/null +++ b/docs/app/lib/sdk-registry.ts @@ -0,0 +1,76 @@ +// Single source of truth for every SDK the docs site supports. +// +// Adding a language (java, go, …) is a one-entry change: append its id to +// `SDK_IDS` and add a `SDK_PROFILES` row. Everything else — the `Sdk` type, the +// nav trees, the sidebar switcher, the no-flash boot script, the SDK-aware +// diagrams and prose primitives — derives from this file. Nothing else should +// hardcode the literals `"python"`/`"node"`. + +/** Every supported SDK id, in display order. Also the URL prefix (`/python/…`) + * and the `` value. Order drives the switcher + nav. */ +export const SDK_IDS = ["python", "node"] as const; + +export type Sdk = (typeof SDK_IDS)[number]; + +/** The SDK assumed before any user choice — used by the SSG server snapshot and + * the boot script, so prerendered HTML and first paint agree. */ +export const DEFAULT_SDK: Sdk = "python"; + +export interface SdkProfile { + /** Stable id; URL prefix and `data-sdk` value. */ + id: Sdk; + /** Switcher / breadcrumb label, e.g. "Node.js". */ + label: string; + /** Language name for prose ("a hybrid /Rust system"). */ + language: string; + /** The FFI boundary crossed into the shared Rust core, shown in the + * architecture stack (" boundary"), e.g. "PyO3", "N-API". */ + binding: string; + /** Section directories under `content/docs`, in nav order. `architecture` and + * `resources` are SDK-neutral and appear in every SDK's nav. */ + navSections: string[]; +} + +export const SDK_PROFILES: Record = { + python: { + id: "python", + label: "Python", + language: "Python", + binding: "PyO3", + navSections: [ + "python/getting-started", + "python/guides", + "architecture", + "python/api-reference", + "python/more/examples", + "resources", + ], + }, + node: { + id: "node", + label: "Node.js", + language: "Node.js", + binding: "N-API", + navSections: [ + "node/getting-started", + "node/guides", + "architecture", + "node/api-reference", + "node/more/examples", + "resources", + ], + }, +}; + +export function isSdk(value: string | null | undefined): value is Sdk { + return SDK_IDS.includes(value as Sdk); +} + +export function sdkProfile(sdk: Sdk): SdkProfile { + return SDK_PROFILES[sdk]; +} + +/** Ordered `{ id, label }` pairs for switcher UIs. */ +export function sdkLabels(): { id: Sdk; label: string }[] { + return SDK_IDS.map((id) => ({ id, label: SDK_PROFILES[id].label })); +} diff --git a/docs/app/lib/sdk-store.ts b/docs/app/lib/sdk-store.ts index 54a4d7f7..4752c94f 100644 --- a/docs/app/lib/sdk-store.ts +++ b/docs/app/lib/sdk-store.ts @@ -5,17 +5,15 @@ // without a hydration mismatch. The live value lives on ``; CSS // shows/hides each SDK's variants off that attribute. -export type Sdk = "python" | "node"; +import { DEFAULT_SDK, isSdk, type Sdk } from "./sdk-registry"; + +export type { Sdk }; const KEY = "taskito-sdk"; -const DEFAULT: Sdk = "python"; +const DEFAULT = DEFAULT_SDK; const listeners = new Set<() => void>(); -function isSdk(value: string | null | undefined): value is Sdk { - return value === "python" || value === "node"; -} - /** Resolve the active SDK: `?sdk=` query > localStorage > default. Used by the * no-flash bootstrap in root.tsx and as the first client read. */ export function readSdk(): Sdk { diff --git a/docs/app/root.tsx b/docs/app/root.tsx index 85ecef62..a16027c0 100644 --- a/docs/app/root.tsx +++ b/docs/app/root.tsx @@ -7,6 +7,7 @@ import { ScrollRestoration, } from "react-router"; import { usePrefetchDocs } from "@/hooks"; +import { DEFAULT_SDK, SDK_IDS } from "@/lib"; import type { Route } from "./+types/root"; import "./app.css"; @@ -27,15 +28,17 @@ export const links: Route.LinksFunction = () => [ const THEME_INIT = `(function(){try{var t=localStorage.getItem('taskito-theme')||'dark';document.documentElement.setAttribute('data-theme',t);}catch(e){}})();`; // Apply the active SDK before paint (query > localStorage > default) so the -// CSS show/hide picks the right variant with no flash. Mirrors readSdk(). -const SDK_INIT = `(function(){try{var u=new URLSearchParams(location.search).get('sdk');var s=(u==='python'||u==='node')?u:(localStorage.getItem('taskito-sdk')||'python');if(s!=='python'&&s!=='node'){s='python';}document.documentElement.setAttribute('data-sdk',s);}catch(e){document.documentElement.setAttribute('data-sdk','python');}})();`; +// CSS show/hide picks the right variant with no flash. Mirrors readSdk(). The +// valid-id list and default come from the SDK registry, so a new language needs +// no edit here. +const SDK_INIT = `(function(){try{var ids=${JSON.stringify([...SDK_IDS])},def='${DEFAULT_SDK}';var u=new URLSearchParams(location.search).get('sdk');var s=ids.indexOf(u)>=0?u:(localStorage.getItem('taskito-sdk')||def);if(ids.indexOf(s)<0){s=def;}document.documentElement.setAttribute('data-sdk',s);}catch(e){document.documentElement.setAttribute('data-sdk','${DEFAULT_SDK}');}})();`; export function Layout({ children }: { children: React.ReactNode }) { return ( From 95ca5d44b44158716af13aa12d29e8ccd613c32a Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:19:53 +0530 Subject: [PATCH 02/11] feat(docs): theme Mermaid diagrams from one palette applyDiagramTheme() strips author classDefs and injects a brand-aligned, dark/light-aware semantic palette; charts only name states. All inline classDefs dropped. --- docs/app/components/mermaid.tsx | 131 +-------- docs/app/lib/mermaid-theme.ts | 263 ++++++++++++++++++ .../docs/node/guides/workflows/conditions.mdx | 6 +- .../docs/node/guides/workflows/fan-out.mdx | 19 +- .../docs/node/guides/workflows/gates.mdx | 7 +- .../docs/node/guides/workflows/saga.mdx | 6 +- .../node/guides/workflows/sub-workflows.mdx | 6 +- .../docs/node/more/examples/data-pipeline.mdx | 4 +- .../docs/python/guides/workflows/building.mdx | 1 - .../docs/python/guides/workflows/caching.mdx | 7 +- .../python/guides/workflows/conditions.mdx | 14 +- .../docs/python/guides/workflows/fan-out.mdx | 5 +- .../docs/python/guides/workflows/gates.mdx | 4 +- 13 files changed, 295 insertions(+), 178 deletions(-) create mode 100644 docs/app/lib/mermaid-theme.ts diff --git a/docs/app/components/mermaid.tsx b/docs/app/components/mermaid.tsx index 4bb8e8c9..80e64a5a 100644 --- a/docs/app/components/mermaid.tsx +++ b/docs/app/components/mermaid.tsx @@ -1,121 +1,11 @@ import { useEffect, useId, useRef, useState } from "react"; +import { + applyDiagramTheme, + diagramThemeCss, + diagramThemeVariables, +} from "@/lib/mermaid-theme"; import { useThemeMode } from "@/lib/theme"; -const DARK_THEME_VARIABLES = { - darkMode: true, - background: "transparent", - primaryColor: "#1f2024", - primaryTextColor: "#f5f5f7", - primaryBorderColor: "#7a7d85", - lineColor: "#9b9ea6", - secondaryColor: "#2a2c31", - tertiaryColor: "#26282d", - mainBkg: "#1f2024", - nodeBkg: "#1f2024", - nodeBorder: "#7a7d85", - clusterBkg: "#1a1b1e", - clusterBorder: "#5a5d65", - edgeLabelBackground: "#2a2c31", - titleColor: "#f5f5f7", - labelTextColor: "#f5f5f7", - textColor: "#f5f5f7", - noteBkgColor: "#3a3c41", - noteTextColor: "#f5f5f7", - noteBorderColor: "#7a7d85", - errorBkgColor: "#4a1d1d", - errorTextColor: "#fca5a5", - // erDiagram-specific (entity attribute rows) — same color, no zebra stripe - attributeBackgroundColorOdd: "#1f2024", - attributeBackgroundColorEven: "#1f2024", - // stateDiagram-specific - altBackground: "#26282d", - // flowchart label colors - labelBackground: "#2a2c31", - // sequenceDiagram - actorBkg: "#1f2024", - actorBorder: "#7a7d85", - actorTextColor: "#f5f5f7", - actorLineColor: "#9b9ea6", - signalColor: "#f5f5f7", - signalTextColor: "#f5f5f7", - labelBoxBkgColor: "#2a2c31", - labelBoxBorderColor: "#7a7d85", - loopTextColor: "#f5f5f7", - activationBkgColor: "#3a3c41", - activationBorderColor: "#9b9ea6", -} as const; - -const DARK_THEME_CSS = ` - .er.entityBox { fill: #1f2024 !important; stroke: #7a7d85 !important; } - .er.entityLabel { fill: #f5f5f7 !important; } - .er.attributeBoxOdd { fill: #1f2024 !important; } - .er.attributeBoxEven { fill: #1f2024 !important; } - .er .er.attribute-text, - .er .attribute-text, - .er text { - fill: #f5f5f7 !important; - } - .er.relationshipLabel, - .er.relationshipLabelBox { - fill: #f5f5f7 !important; - } - .er.relationshipLabelBox + text { fill: #1a1a1a !important; } -`; - -const LIGHT_THEME_CSS = ` - .er.entityBox { fill: #ffffff !important; stroke: #3a3a3a !important; } - .er.entityLabel { fill: #1a1a1a !important; } - .er.attributeBoxOdd { fill: #ffffff !important; } - .er.attributeBoxEven { fill: #f4f4f5 !important; } - .er .er.attribute-text, - .er .attribute-text, - .er text { - fill: #1a1a1a !important; - } -`; - -const LIGHT_THEME_VARIABLES = { - darkMode: false, - background: "transparent", - primaryColor: "#ffffff", - primaryTextColor: "#1a1a1a", - primaryBorderColor: "#3a3a3a", - lineColor: "#4a4a4a", - secondaryColor: "#f4f4f5", - tertiaryColor: "#fafafa", - mainBkg: "#ffffff", - nodeBkg: "#ffffff", - nodeBorder: "#3a3a3a", - clusterBkg: "#f4f4f5", - clusterBorder: "#a1a1aa", - edgeLabelBackground: "#ffffff", - titleColor: "#1a1a1a", - labelTextColor: "#1a1a1a", - textColor: "#1a1a1a", - noteBkgColor: "#fef9c3", - noteTextColor: "#1a1a1a", - noteBorderColor: "#a1a1aa", - // erDiagram-specific (entity attribute rows) - attributeBackgroundColorOdd: "#ffffff", - attributeBackgroundColorEven: "#f4f4f5", - // stateDiagram-specific - altBackground: "#f4f4f5", - // flowchart label colors - labelBackground: "#ffffff", - // sequenceDiagram - actorBkg: "#ffffff", - actorBorder: "#3a3a3a", - actorTextColor: "#1a1a1a", - actorLineColor: "#4a4a4a", - signalColor: "#1a1a1a", - signalTextColor: "#1a1a1a", - labelBoxBkgColor: "#ffffff", - labelBoxBorderColor: "#3a3a3a", - loopTextColor: "#1a1a1a", - activationBkgColor: "#f4f4f5", - activationBorderColor: "#4a4a4a", -} as const; - export function Mermaid({ chart }: { chart: string }) { const id = useId(); const containerRef = useRef(null); @@ -129,12 +19,12 @@ export function Mermaid({ chart }: { chart: string }) { if (typeof document !== "undefined" && document.fonts?.ready) { await document.fonts.ready; } - const isDark = resolvedTheme === "dark"; + const theme = resolvedTheme === "dark" ? "dark" : "light"; mermaid.initialize({ startOnLoad: false, theme: "base", - themeVariables: isDark ? DARK_THEME_VARIABLES : LIGHT_THEME_VARIABLES, - themeCSS: isDark ? DARK_THEME_CSS : LIGHT_THEME_CSS, + themeVariables: diagramThemeVariables(theme), + themeCSS: diagramThemeCss(theme), securityLevel: "loose", fontFamily: '"IBM Plex Sans", "Inter", system-ui, sans-serif', flowchart: { padding: 18, htmlLabels: true, useMaxWidth: true }, @@ -146,7 +36,10 @@ export function Mermaid({ chart }: { chart: string }) { }); try { const renderId = `m${id.replace(/[^a-zA-Z0-9]/g, "")}`; - const { svg: rendered } = await mermaid.render(renderId, chart); + const { svg: rendered } = await mermaid.render( + renderId, + applyDiagramTheme(chart, theme), + ); if (!cancelled) { setSvg(rendered); } diff --git a/docs/app/lib/mermaid-theme.ts b/docs/app/lib/mermaid-theme.ts new file mode 100644 index 00000000..8b736907 --- /dev/null +++ b/docs/app/lib/mermaid-theme.ts @@ -0,0 +1,263 @@ +// Central Mermaid theming. One source of truth for how every diagram in the +// docs looks, so individual charts never hardcode colors. +// +// Two layers: +// 1. base theme variables — default node/edge/cluster colors, aligned to the +// Taskito tokens (tokens.css) for dark + light. Charts with no semantic +// classes still look on-brand. +// 2. a semantic class palette — `success`/`failed`/`skipped`/… mapped to +// brand-tinted, theme-aware fills. `applyDiagramTheme()` strips whatever +// `classDef`s a chart author wrote and injects these, so a chart only has +// to *name* a state (`A:::failed`) and theming stays consistent and +// theme-reactive (light/dark) everywhere. +// +// Hex values mirror tokens.css on purpose: Mermaid bakes `classDef` colors into +// the SVG as literal styles, so CSS vars can't be used reliably here. Keep the +// two in sync when the brand palette changes. + +export type DiagramTheme = "dark" | "light"; + +// --- base theme variables (default nodes, edges, clusters, sequences) -------- + +const DARK_VARIABLES = { + darkMode: true, + background: "transparent", + primaryColor: "#13131d", // panel2 — default node fill + primaryTextColor: "#ececf4", // txt + primaryBorderColor: "#33334a", // line3-ish + lineColor: "#5c5c70", // dim — edges + secondaryColor: "#181824", // panel3 + tertiaryColor: "#0f0f17", // panel + mainBkg: "#13131d", + nodeBkg: "#13131d", + nodeBorder: "#33334a", + nodeTextColor: "#ececf4", + clusterBkg: "#0c0c13", // bg-soft + clusterBorder: "#1f5e3c", // brand-green tint + edgeLabelBackground: "#0f0f17", + titleColor: "#ececf4", + labelTextColor: "#ececf4", + textColor: "#ececf4", + noteBkgColor: "#181824", + noteTextColor: "#ececf4", + noteBorderColor: "#3bbf72", + errorBkgColor: "#331a1c", + errorTextColor: "#ff9d9d", + attributeBackgroundColorOdd: "#13131d", + attributeBackgroundColorEven: "#0f0f17", + altBackground: "#181824", + labelBackground: "#0f0f17", + actorBkg: "#13131d", + actorBorder: "#3bbf72", + actorTextColor: "#ececf4", + actorLineColor: "#5c5c70", + signalColor: "#ececf4", + signalTextColor: "#ececf4", + labelBoxBkgColor: "#13131d", + labelBoxBorderColor: "#33334a", + loopTextColor: "#ececf4", + activationBkgColor: "#181824", + activationBorderColor: "#5c5c70", +} as const; + +const LIGHT_VARIABLES = { + darkMode: false, + background: "transparent", + primaryColor: "#ffffff", + primaryTextColor: "#1b1a15", // txt + primaryBorderColor: "#cfc9ba", + lineColor: "#8b877a", // dim + secondaryColor: "#f2efe7", // panel2 + tertiaryColor: "#eae5da", // panel3 + mainBkg: "#ffffff", + nodeBkg: "#ffffff", + nodeBorder: "#cfc9ba", + nodeTextColor: "#1b1a15", + clusterBkg: "#fbf9f3", // bg-soft + clusterBorder: "#10a152", // brand green + edgeLabelBackground: "#ffffff", + titleColor: "#1b1a15", + labelTextColor: "#1b1a15", + textColor: "#1b1a15", + noteBkgColor: "#f2efe7", + noteTextColor: "#1b1a15", + noteBorderColor: "#10a152", + errorBkgColor: "#fbe3e3", + errorTextColor: "#911b1b", + attributeBackgroundColorOdd: "#ffffff", + attributeBackgroundColorEven: "#f2efe7", + altBackground: "#f2efe7", + labelBackground: "#ffffff", + actorBkg: "#ffffff", + actorBorder: "#10a152", + actorTextColor: "#1b1a15", + actorLineColor: "#8b877a", + signalColor: "#1b1a15", + signalTextColor: "#1b1a15", + labelBoxBkgColor: "#ffffff", + labelBoxBorderColor: "#cfc9ba", + loopTextColor: "#1b1a15", + activationBkgColor: "#f2efe7", + activationBorderColor: "#8b877a", +} as const; + +export function diagramThemeVariables(theme: DiagramTheme) { + return theme === "dark" ? DARK_VARIABLES : LIGHT_VARIABLES; +} + +// --- erDiagram entity styling (not covered by theme variables) --------------- + +const DARK_THEME_CSS = ` + .er.entityBox { fill: #13131d !important; stroke: #33334a !important; } + .er.entityLabel { fill: #ececf4 !important; } + .er.attributeBoxOdd { fill: #13131d !important; } + .er.attributeBoxEven { fill: #0f0f17 !important; } + .er .er.attribute-text, + .er .attribute-text, + .er text { fill: #ececf4 !important; } + .er.relationshipLabel, + .er.relationshipLabelBox { fill: #ececf4 !important; } + .er.relationshipLabelBox + text { fill: #0f0f17 !important; } +`; + +const LIGHT_THEME_CSS = ` + .er.entityBox { fill: #ffffff !important; stroke: #cfc9ba !important; } + .er.entityLabel { fill: #1b1a15 !important; } + .er.attributeBoxOdd { fill: #ffffff !important; } + .er.attributeBoxEven { fill: #f2efe7 !important; } + .er .er.attribute-text, + .er .attribute-text, + .er text { fill: #1b1a15 !important; } +`; + +export function diagramThemeCss(theme: DiagramTheme): string { + return theme === "dark" ? DARK_THEME_CSS : LIGHT_THEME_CSS; +} + +// --- semantic node palette --------------------------------------------------- + +interface NodeStyle { + fill: string; + stroke: string; + color: string; + /** Extra `classDef` props, e.g. a dashed border for nested/terminal nodes. */ + extra?: string; +} + +/** Canonical states. Every chart should style nodes with one of these (or an + * alias below). Add a state here — not in an MDX file — to extend the palette. */ +const PALETTE: Record> = { + dark: { + success: { fill: "#14301e", stroke: "#3bbf72", color: "#86e3aa" }, + failed: { fill: "#331a1c", stroke: "#ff6b6b", color: "#ff9d9d" }, + skipped: { fill: "#181824", stroke: "#3a3a48", color: "#9a9ab0" }, + waiting: { fill: "#33270f", stroke: "#ffb86b", color: "#ffd29c" }, + active: { fill: "#0e2b29", stroke: "#29b8a8", color: "#6fd6cb" }, + gate: { fill: "#2f2410", stroke: "#f0a850", color: "#ffce9e" }, + sink: { fill: "#0e2b29", stroke: "#29b8a8", color: "#6fd6cb" }, + sub: { + fill: "#0e2b29", + stroke: "#29b8a8", + color: "#6fd6cb", + extra: "stroke-dasharray:5", + }, + terminal: { + fill: "#13131d", + stroke: "#5c5c70", + color: "#c3c2d4", + extra: "stroke-dasharray:4", + }, + }, + light: { + success: { fill: "#dcf5e6", stroke: "#10a152", color: "#0a5e30" }, + failed: { fill: "#fbe3e3", stroke: "#dc2626", color: "#911b1b" }, + skipped: { fill: "#ece9e1", stroke: "#bdb9ab", color: "#5c5849" }, + waiting: { fill: "#f7ead4", stroke: "#c2410c", color: "#7a2e0a" }, + active: { fill: "#d4efeb", stroke: "#0d9488", color: "#0a5a52" }, + gate: { fill: "#f9edd2", stroke: "#b45309", color: "#7a3d05" }, + sink: { fill: "#d4efeb", stroke: "#0d9488", color: "#0a5a52" }, + sub: { + fill: "#d4efeb", + stroke: "#0d9488", + color: "#0a5a52", + extra: "stroke-dasharray:5", + }, + terminal: { + fill: "#f2efe7", + stroke: "#8b877a", + color: "#35332c", + extra: "stroke-dasharray:4", + }, + }, +}; + +// Author-facing aliases → canonical state. Keeps existing charts working +// (Python used `failed`/`success`/`skipped`; Node used `fail`/`done`/`skip`/…) +// and lets new charts use whichever name reads best. +const ALIASES: Record = { + done: "success", + ok: "success", + complete: "success", + completed: "success", + source: "success", + pass: "success", + fail: "failed", + error: "failed", + dead: "failed", + skip: "skipped", + cancelled: "skipped", + canceled: "skipped", + pending: "waiting", + queued: "waiting", + run: "active", + running: "active", + current: "active", + approval: "gate", + info: "sink", + comp: "sub", + compensation: "sub", +}; + +function classDefLine(name: string, style: NodeStyle): string { + const props = [ + `fill:${style.fill}`, + `stroke:${style.stroke}`, + `color:${style.color}`, + style.extra, + ] + .filter(Boolean) + .join(","); + return `classDef ${name} ${props}`; +} + +/** Canonical + alias `classDef` lines for the theme. Appended to every + * flowchart; unused definitions are harmless. */ +function semanticClassDefs(theme: DiagramTheme): string { + const palette = PALETTE[theme]; + const lines = Object.entries(palette).map(([name, style]) => + classDefLine(name, style), + ); + for (const [alias, target] of Object.entries(ALIASES)) { + lines.push(classDefLine(alias, palette[target])); + } + return lines.join("\n"); +} + +// Only flowcharts support `classDef`/`:::`. Injecting into sequence/er/state/etc. +// would be a parse error, so those charts are returned untouched. +function isFlowchart(chart: string): boolean { + const first = chart.trim().split(/\s+/, 1)[0]?.toLowerCase(); + return first === "graph" || first === "flowchart"; +} + +const CLASSDEF_LINE = /^[ \t]*classDef[ \t].*$/gm; + +/** Normalize a chart to the central theme: drop author `classDef`s and inject + * the canonical, theme-aware palette. Non-flowchart diagrams pass through. */ +export function applyDiagramTheme(chart: string, theme: DiagramTheme): string { + if (!isFlowchart(chart)) { + return chart; + } + const stripped = chart.replace(CLASSDEF_LINE, "").replace(/\n{3,}/g, "\n\n"); + return `${stripped.trimEnd()}\n${semanticClassDefs(theme)}\n`; +} diff --git a/docs/content/docs/node/guides/workflows/conditions.mdx b/docs/content/docs/node/guides/workflows/conditions.mdx index 18b4b74e..76ba2a35 100644 --- a/docs/content/docs/node/guides/workflows/conditions.mdx +++ b/docs/content/docs/node/guides/workflows/conditions.mdx @@ -51,11 +51,7 @@ event, submit and execute may be different processes. celebrate["celebrate\n(on_success)"] - riskyStep --> recover["recover\n(on_failure)"] - - classDef skip fill:#fef9c3,color:#713f12,stroke:#ca8a04 - classDef run fill:#bbf7d0,color:#14532d,stroke:#16a34a - classDef fail fill:#fee2e2,color:#7f1d1d,stroke:#ef4444`} + riskyStep --> recover["recover\n(on_failure)"]`} /> When `riskyStep` fails: `celebrate` → `skipped`, `recover` → enqueued and runs. diff --git a/docs/content/docs/node/guides/workflows/fan-out.mdx b/docs/content/docs/node/guides/workflows/fan-out.mdx index 471334a9..6ead4d77 100644 --- a/docs/content/docs/node/guides/workflows/fan-out.mdx +++ b/docs/content/docs/node/guides/workflows/fan-out.mdx @@ -8,18 +8,15 @@ import { Callout } from "fumadocs-ui/components/callout"; Split a step's result into parallel child jobs, then collect all results into a downstream step. -```mermaid -graph LR + process_0["process[0]"] fetch --> process_1["process[1]"] fetch --> process_2["process[2]"] process_0 --> aggregate:::sink process_1 --> aggregate - process_2 --> aggregate - - classDef source fill:#bbf7d0,color:#14532d,stroke:#16a34a - classDef sink fill:#bfdbfe,color:#1e3a8a,stroke:#3b82f6 -``` + process_2 --> aggregate`} +/> ## Basic fan-out and fan-in @@ -71,8 +68,8 @@ const nodes = handle.nodes(); calls `createDeferredJob` for the fan-in step 6. `summarize` runs with the results array as its single argument -```mermaid -sequenceDiagram +>T: outcome(process[1], success) R->>T: outcome(process[2], success) T->>R: checkFanOutCompletion → all done - T->>R: createDeferredJob(collect) -``` + T->>R: createDeferredJob(collect)`} +/> The `WorkflowTracker` is driven entirely by the worker outcome stream and reconstructs the run plan from storage on each event, so submission and diff --git a/docs/content/docs/node/guides/workflows/gates.mdx b/docs/content/docs/node/guides/workflows/gates.mdx index 6307304e..385a14f7 100644 --- a/docs/content/docs/node/guides/workflows/gates.mdx +++ b/docs/content/docs/node/guides/workflows/gates.mdx @@ -31,12 +31,7 @@ queue.workflows.approveGate(handle.runId, "approve-deploy"); chart={`graph LR build:::done --> gate["approve-deploy\n(waiting_approval)"]:::waiting gate -- approved --> deploy:::run - gate -- rejected --> deploy_skip["deploy\n(skipped)"]:::skip - - classDef done fill:#bbf7d0,color:#14532d,stroke:#16a34a - classDef waiting fill:#fef9c3,color:#713f12,stroke:#ca8a04 - classDef run fill:#bfdbfe,color:#1e3a8a,stroke:#3b82f6 - classDef skip fill:#f3f4f6,color:#6b7280,stroke:#9ca3af`} + gate -- rejected --> deploy_skip["deploy\n(skipped)"]:::skip`} /> ## Gate node status diff --git a/docs/content/docs/node/guides/workflows/saga.mdx b/docs/content/docs/node/guides/workflows/saga.mdx index 5bb7eb34..e8951e14 100644 --- a/docs/content/docs/node/guides/workflows/saga.mdx +++ b/docs/content/docs/node/guides/workflows/saga.mdx @@ -45,11 +45,7 @@ const run = await handle.wait(); reserve --> charge --> ship ship -. "run fails" .-> refund - refund --> unreserve - - classDef done fill:#bbf7d0,color:#14532d,stroke:#16a34a - classDef fail fill:#fee2e2,color:#7f1d1d,stroke:#ef4444 - classDef comp fill:#ede9fe,color:#4c1d95,stroke:#7c3aed`} + refund --> unreserve`} /> ## How it works diff --git a/docs/content/docs/node/guides/workflows/sub-workflows.mdx b/docs/content/docs/node/guides/workflows/sub-workflows.mdx index 3af43774..a8ada562 100644 --- a/docs/content/docs/node/guides/workflows/sub-workflows.mdx +++ b/docs/content/docs/node/guides/workflows/sub-workflows.mdx @@ -47,11 +47,7 @@ console.log(run.state); // "completed" | "failed" v[validate] --> t[transform] --> s[store] end process -. spawns .-> child - process --> finish:::run - - classDef done fill:#bbf7d0,color:#14532d,stroke:#16a34a - classDef sub fill:#ede9fe,color:#4c1d95,stroke:#7c3aed - classDef run fill:#bfdbfe,color:#1e3a8a,stroke:#3b82f6`} + process --> finish:::run`} /> ## How it works diff --git a/docs/content/docs/node/more/examples/data-pipeline.mdx b/docs/content/docs/node/more/examples/data-pipeline.mdx index c56f1b22..a852ab52 100644 --- a/docs/content/docs/node/more/examples/data-pipeline.mdx +++ b/docs/content/docs/node/more/examples/data-pipeline.mdx @@ -12,9 +12,7 @@ dependency ordering, per-step retries, and a queryable run you can monitor. extract:::source --> clean extract --> enrich clean --> load:::sink - enrich --> load - classDef source fill:#1f6feb,color:#fff - classDef sink fill:#238636,color:#fff`} + enrich --> load`} /> ## Project structure diff --git a/docs/content/docs/python/guides/workflows/building.mdx b/docs/content/docs/python/guides/workflows/building.mdx index 7a54bf02..cd13a2fb 100644 --- a/docs/content/docs/python/guides/workflows/building.mdx +++ b/docs/content/docs/python/guides/workflows/building.mdx @@ -123,7 +123,6 @@ Each step transitions through these states: Running -- retries exhausted --> Failed([Failed]) WaitingApproval -- approved --> Completed WaitingApproval -- rejected --> Failed - classDef terminal stroke-dasharray: 4 3 class Completed,Failed,Skipped,CacheHit terminal`} /> diff --git a/docs/content/docs/python/guides/workflows/caching.mdx b/docs/content/docs/python/guides/workflows/caching.mdx index 4a7926cf..1177bd79 100644 --- a/docs/content/docs/python/guides/workflows/caching.mdx +++ b/docs/content/docs/python/guides/workflows/caching.mdx @@ -30,16 +30,13 @@ nodes are also dirty — even if they had cached results: B["b — dirty (propagated)"]:::failed - B --> C["c — dirty (propagated)"]:::failed - classDef failed fill:#fecaca,color:#7f1d1d,stroke:#dc2626`} + B --> C["c — dirty (propagated)"]:::failed`} /> B["b — dirty (failed in base)"]:::failed - B --> C["c — dirty (propagated)"]:::failed - classDef success fill:#bbf7d0,color:#14532d,stroke:#16a34a - classDef failed fill:#fecaca,color:#7f1d1d,stroke:#dc2626`} + B --> C["c — dirty (propagated)"]:::failed`} /> ## Cache TTL diff --git a/docs/content/docs/python/guides/workflows/conditions.mdx b/docs/content/docs/python/guides/workflows/conditions.mdx index b5974d84..97d3982b 100644 --- a/docs/content/docs/python/guides/workflows/conditions.mdx +++ b/docs/content/docs/python/guides/workflows/conditions.mdx @@ -63,9 +63,7 @@ One failure skips **all** pending steps. The workflow transitions to `FAILED`. chart={`graph LR A["a ✓"] --> B["b ✗"]:::failed B --> C["c ━ SKIPPED"]:::skipped - B --> D["d ━ SKIPPED"]:::skipped - classDef failed fill:#fecaca,color:#7f1d1d,stroke:#dc2626 - classDef skipped fill:#e5e7eb,color:#374151,stroke:#9ca3af`} + B --> D["d ━ SKIPPED"]:::skipped`} /> @@ -83,10 +81,7 @@ branches keep running**. root["root ✓"] --> fail_branch["fail ✗"]:::failed root --> ok_branch["ok ✓"] fail_branch --> after_fail["after_fail ━ SKIPPED"]:::skipped - ok_branch --> after_ok["after_ok ✓"]:::success - classDef failed fill:#fecaca,color:#7f1d1d,stroke:#dc2626 - classDef skipped fill:#e5e7eb,color:#374151,stroke:#9ca3af - classDef success fill:#bbf7d0,color:#14532d,stroke:#16a34a`} + ok_branch --> after_ok["after_ok ✓"]:::success`} /> @@ -110,10 +105,7 @@ wf.step("cleanup", cleanup, after="b", condition="always") # runs even if b is B["b ━ SKIPPED"]:::skipped - B --> C["cleanup ✓ ALWAYS"]:::success - classDef failed fill:#fecaca,color:#7f1d1d,stroke:#dc2626 - classDef skipped fill:#e5e7eb,color:#374151,stroke:#9ca3af - classDef success fill:#bbf7d0,color:#14532d,stroke:#16a34a`} + B --> C["cleanup ✓ ALWAYS"]:::success`} /> ## Combining conditions with fan-out diff --git a/docs/content/docs/python/guides/workflows/fan-out.mdx b/docs/content/docs/python/guides/workflows/fan-out.mdx index dbcd9007..5c6160fb 100644 --- a/docs/content/docs/python/guides/workflows/fan-out.mdx +++ b/docs/content/docs/python/guides/workflows/fan-out.mdx @@ -13,10 +13,7 @@ into a downstream step. fetch --> process_2["process[2]"] process_0 --> aggregate:::sink process_1 --> aggregate - process_2 --> aggregate - - classDef source fill:#bbf7d0,color:#14532d,stroke:#16a34a - classDef sink fill:#bfdbfe,color:#1e3a8a,stroke:#3b82f6`} + process_2 --> aggregate`} /> ## Fan-out with `"each"` diff --git a/docs/content/docs/python/guides/workflows/gates.mdx b/docs/content/docs/python/guides/workflows/gates.mdx index fe63aad2..1ae86445 100644 --- a/docs/content/docs/python/guides/workflows/gates.mdx +++ b/docs/content/docs/python/guides/workflows/gates.mdx @@ -11,9 +11,7 @@ status until explicitly approved or rejected — or until a timeout fires. train["train ✓"] --> eval["evaluate ✓"] eval --> gate["approve ⏸"]:::gate gate -->|approved| deploy["deploy"] - gate -->|rejected| skip["deploy ━ SKIPPED"]:::skipped - classDef gate fill:#fef3c7,color:#78350f,stroke:#d97706 - classDef skipped fill:#e5e7eb,color:#374151,stroke:#9ca3af`} + gate -->|rejected| skip["deploy ━ SKIPPED"]:::skipped`} /> ## Adding a gate From ac5257383b3cb0425005fece1e49fac1a4ac5bb0 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:19:58 +0530 Subject: [PATCH 03/11] feat(docs): make shared docs sections SDK-aware Architecture diagrams + shared prose adapt to the selected SDK via SdkName/SdkLang/SdkBinding/SdkSwap no-flash atoms. --- docs/app/components/diagrams/arch-stack.tsx | 50 ++++++++++--- docs/app/components/diagrams/worker-fork.tsx | 57 ++++++++++++--- docs/app/components/mdx/index.tsx | 5 ++ docs/app/components/sdk-text.tsx | 51 ++++++++++++++ docs/content/docs/architecture/index.mdx | 2 +- docs/content/docs/architecture/overview.mdx | 10 +-- docs/content/docs/architecture/resources.mdx | 29 +++++++- .../docs/architecture/serialization.mdx | 43 ++++++++++-- .../content/docs/architecture/worker-pool.mdx | 28 ++++++-- docs/content/docs/resources/comparison.mdx | 15 ++-- docs/content/docs/resources/faq.mdx | 70 +++++++++++++++---- 11 files changed, 305 insertions(+), 55 deletions(-) create mode 100644 docs/app/components/sdk-text.tsx diff --git a/docs/app/components/diagrams/arch-stack.tsx b/docs/app/components/diagrams/arch-stack.tsx index 8030b5f9..f0f3b1a7 100644 --- a/docs/app/components/diagrams/arch-stack.tsx +++ b/docs/app/components/diagrams/arch-stack.tsx @@ -1,12 +1,14 @@ import { type CSSProperties, Fragment, type ReactNode } from "react"; +import { SdkBinding, SdkName, SdkSwap } from "@/components/sdk-text"; // Layered architecture stack — exact port of the prototype's `.archstack` HTML -// (semantic layers + PyO3/store boundaries as direct children). +// (semantic layers + binding/store boundaries as direct children). The +// user-facing layer adapts to the active SDK via the SDK-text atoms. type Kind = "py" | "rust" | "store" | ""; interface Layer { - tag: string; + tag: ReactNode; /** Pill colour: `.ltag t-*`. */ tagKind?: Kind; /** `.layer` modifier; defaults to `tagKind`. The prototype sometimes pairs a @@ -68,13 +70,25 @@ export function ArchitectureStack() { } layers={[ { - tag: "Python", + tag: , tagKind: "py", title: "User-facing API", body: ( <> - Queue, @task, .delay(), - results, workflows, resources — the surface you write against. + Queue,{" "} + + @task, .delay() + + } + node={ + <> + .task(), .enqueue() + + } + /> + , results, workflows, resources — the surface you write against. ), role: "what you write", @@ -83,7 +97,12 @@ export function ArchitectureStack() { tag: "Rust", tagKind: "rust", title: "Engine", - body: "Scheduler, dispatcher, worker pool, rate limiter — Tokio runtime, OS-thread pool, near-zero Python overhead.", + body: ( + <> + Scheduler, dispatcher, worker pool, rate limiter — Tokio runtime, + OS-thread pool, near-zero overhead. + + ), role: "where the work runs", }, { @@ -97,7 +116,9 @@ export function ArchitectureStack() { bounds={[ <> - PyO3 boundary + + boundary + , <> @@ -135,8 +156,19 @@ export function ResourcePipeline() { <> ResourceRuntime initializes resources at worker startup in topological order, then injects requested ones (via{" "} - inject= or Inject["name"]) as kwargs. - Task-scoped resources come from a semaphore pool. + + inject= or Inject["name"] + + } + node={ + <> + inject: or useResource() + + } + /> + ). Task-scoped resources come from a semaphore pool. ), }, diff --git a/docs/app/components/diagrams/worker-fork.tsx b/docs/app/components/diagrams/worker-fork.tsx index 1820d8f0..d7118053 100644 --- a/docs/app/components/diagrams/worker-fork.tsx +++ b/docs/app/components/diagrams/worker-fork.tsx @@ -1,6 +1,8 @@ +import { SdkSwap } from "@/components/sdk-text"; + // Worker pool — exact port of the prototype's `.archstack` + `.fork-route` + // `.archfork` (scheduler routes by task type; sync OS-thread pool vs async pool). -// `architecture/worker-pool.mdx`. +// The per-runtime details adapt to the active SDK. `architecture/worker-pool.mdx`. export function WorkerDispatch() { return (
@@ -11,12 +13,18 @@ export function WorkerDispatch() {
Dequeues a job, applies rate limits, then routes it{" "} by task type — sync functions to the thread pool,{" "} - async def functions to the async runtime. + async def} + node={async} + />{" "} + functions to the async runtime.
routes by task type -
sync def  ·  async def
+
+ +
bounded mpsc · workers × 2
@@ -25,9 +33,23 @@ export function WorkerDispatch() {
OS-thread workers
- Each sync worker is a Rust std::thread. The GIL is - acquired per task via Python::with_gil() — - independent across workers. + + Each sync worker is a Rust std::thread. The + GIL is acquired per task via{" "} + Python::with_gil() — independent across + workers. + + } + node={ + <> + Sync handlers run on an OS-thread pool owned by the Rust + core — independent across workers, with no event loop to + block. + + } + />
@@ -37,11 +59,26 @@ export function WorkerDispatch() {
Async pool
-
NativeAsyncPool
+
+ +
- async def tasks are dispatched to an{" "} - AsyncTaskExecutor on a Python daemon thread;{" "} - PyResultSender bridges results back. + + async def tasks are dispatched to an{" "} + AsyncTaskExecutor on a Python daemon thread;{" "} + PyResultSender bridges results back. + + } + node={ + <> + async handlers run on a native async pool — + no thread per job; each runs on your Node event loop and + its promise is awaited back into the core. + + } + />
diff --git a/docs/app/components/mdx/index.tsx b/docs/app/components/mdx/index.tsx index 4f859a3f..bc46b8fa 100644 --- a/docs/app/components/mdx/index.tsx +++ b/docs/app/components/mdx/index.tsx @@ -2,6 +2,7 @@ import type { MDXComponents } from "mdx/types"; import type { ComponentProps } from "react"; import { Link } from "react-router"; import * as Diagrams from "@/components/diagrams"; +import { SdkBinding, SdkLang, SdkName, SdkSwap } from "@/components/sdk-text"; import { Callout } from "./callout"; import { Card, Cards } from "./card"; import { CodeBlock } from "./code-block"; @@ -43,6 +44,10 @@ export const mdxComponents: MDXComponents = { CodeTabs, SdkOnly, SdkLink, + SdkName, + SdkLang, + SdkBinding, + SdkSwap, Card, Cards, ...Diagrams, diff --git a/docs/app/components/sdk-text.tsx b/docs/app/components/sdk-text.tsx new file mode 100644 index 00000000..da19473c --- /dev/null +++ b/docs/app/components/sdk-text.tsx @@ -0,0 +1,51 @@ +import type { ReactNode } from "react"; +import { SDK_IDS, SDK_PROFILES, type Sdk, type SdkProfile } from "@/lib"; + +// Inline SDK-aware text atoms for shared pages (architecture, resources) and the +// architecture diagrams. They follow the site's no-flash rule: every SDK's text +// ships in the HTML and CSS hides all but the active one (off ``, +// the same mechanism as ``), so switching the SDK needs no re-render, +// there's no hydration flash, and every variant stays indexable. + +/** Render one span per SDK, picking a value from each profile; CSS shows only + * the active SDK's span. */ +function SdkVariants({ pick }: { pick: (profile: SdkProfile) => ReactNode }) { + return ( + <> + {SDK_IDS.map((id) => ( + + {pick(SDK_PROFILES[id])} + + ))} + + ); +} + +/** Active SDK's display name, e.g. "Python" / "Node.js". */ +export function SdkName() { + return p.label} />; +} + +/** Active SDK's language name for prose, e.g. "Python" / "Node.js". */ +export function SdkLang() { + return p.language} />; +} + +/** Active SDK's FFI boundary into the Rust core, e.g. "PyO3" / "N-API". */ +export function SdkBinding() { + return p.binding} />; +} + +/** Inline value that differs per SDK, e.g. + * `@task} node={.task()} />`. + * A missing SDK falls back to `fallback`, then the default SDK, then the first + * provided value — so adding a new SDK never breaks existing call sites. */ +export function SdkSwap({ + fallback, + ...bySdk +}: Partial> & { fallback?: ReactNode }) { + const provided = SDK_IDS.map((id) => bySdk[id]).find((v) => v !== undefined); + const resolve = (id: Sdk): ReactNode => + bySdk[id] ?? fallback ?? provided ?? null; + return resolve(p.id)} />; +} diff --git a/docs/content/docs/architecture/index.mdx b/docs/content/docs/architecture/index.mdx index cf1b7869..f0cd98be 100644 --- a/docs/content/docs/architecture/index.mdx +++ b/docs/content/docs/architecture/index.mdx @@ -23,7 +23,7 @@ high-level model, then drill into the layer that interests you. icon={} title="Overview" href="/architecture/overview" - description="The big picture — Python ↔ PyO3 ↔ Rust core, end to end." + description="The big picture — your language SDK ↔ the Rust core, end to end." /> } diff --git a/docs/content/docs/architecture/overview.mdx b/docs/content/docs/architecture/overview.mdx index b82c4ac4..733d0ccd 100644 --- a/docs/content/docs/architecture/overview.mdx +++ b/docs/content/docs/architecture/overview.mdx @@ -1,11 +1,11 @@ --- title: Overview -description: "Hybrid Python/Rust architecture: Python API on top, Rust engine underneath." +description: "Hybrid architecture: a thin language SDK on top, one Rust engine underneath." --- -taskito is a hybrid Python/Rust system. Python provides the user-facing API. Rust -handles all the heavy lifting: storage, scheduling, dispatch, rate limiting, and -worker management. +taskito is a hybrid /Rust system. provides the +user-facing API. Rust handles all the heavy lifting: storage, scheduling, +dispatch, rate limiting, and worker management. @@ -14,7 +14,7 @@ worker management. | Page | What it covers | |---|---| | [Job Lifecycle](/architecture/job-lifecycle) | State machine, status codes, transitions | -| [Worker Pool](/architecture/worker-pool) | Thread architecture, async dispatch, GIL management | +| [Worker Pool](/architecture/worker-pool) | Thread architecture, async dispatch, concurrency model | | [Storage Layer](/architecture/storage) | SQLite pragmas, schema, indexes, Postgres differences | | [Scheduler](/architecture/scheduler) | Poll loop, dispatch flow, periodic tasks | | [Resource System](/architecture/resources) | Argument interception, DI, proxy reconstruction | diff --git a/docs/content/docs/architecture/resources.mdx b/docs/content/docs/architecture/resources.mdx index b07352db..7f3fa310 100644 --- a/docs/content/docs/architecture/resources.mdx +++ b/docs/content/docs/architecture/resources.mdx @@ -1,12 +1,15 @@ --- title: Resource System -description: "The three-layer Python pipeline that intercepts arguments, injects DI, and reconstructs proxies." +description: "How external dependencies are registered, injected into tasks, and reconstructed on the worker." --- -The resource system is a three-layer Python pipeline that runs entirely outside Rust: +The resource system injects external dependencies into tasks and runs entirely +outside Rust. It is a three-layer pipeline: + + **Layer 1 — Argument Interception**: the `ArgumentInterceptor` walks every argument before serialization, applying the strategy registered for its type. CONVERT types are transformed to JSON-safe markers. REDIRECT types are replaced with a DI placeholder. @@ -23,3 +26,25 @@ returned after the task finishes. live objects (file handles, HTTP sessions, cloud clients) into a JSON-serializable recipe, and how to reconstruct them on the worker before the task function is called. Recipes are optionally HMAC-signed for tamper detection. + + + + + +**Layer 1 — Registration**: resources are registered once with +`queue.resource(name, factory, { scope })`. A factory may depend on another +resource through the context it receives (`ctx.use("other")`), so the runtime +builds them in topological dependency order. + +**Layer 2 — Worker Resource Runtime**: at worker startup the runtime initializes +every registered resource. At dispatch time it resolves the ones a task asked +for — declaratively via `inject:` (received as a trailing `deps` object) or +imperatively via `useResource()` inside the handler. The `scope` +(worker / task / …) decides each resource's lifetime. + +**Layer 3 — Resource Proxies**: handlers deconstruct live objects (file handles, +HTTP sessions, cloud clients) into a serializable recipe and reconstruct them on +the worker before the handler runs. Recipes are optionally HMAC-signed for +tamper detection. + + diff --git a/docs/content/docs/architecture/serialization.mdx b/docs/content/docs/architecture/serialization.mdx index 1d2dc63f..65d09b38 100644 --- a/docs/content/docs/architecture/serialization.mdx +++ b/docs/content/docs/architecture/serialization.mdx @@ -1,10 +1,13 @@ --- title: Serialization -description: "Pluggable serializers for task arguments and results, with Cloudpickle as the default." +description: "Pluggable serializers for task arguments and results, with a per-SDK default codec." --- -taskito uses a pluggable serializer for task arguments and results. The default is -`CloudpickleSerializer`, which supports lambdas, closures, and complex Python objects. +taskito uses a pluggable serializer for task arguments and results. The default +is CloudpickleSerializer} node={JsonSerializer} />, +which . + + ```python from taskito import Queue, JsonSerializer @@ -13,20 +16,48 @@ from taskito import Queue, JsonSerializer queue = Queue(serializer=JsonSerializer()) ``` + + + + +```ts +import { Queue, MsgpackSerializer } from "@byteveda/taskito"; + +// Use msgpack for compact, binary payloads +new Queue({ dbPath: "taskito.db", serializer: new MsgpackSerializer() }); +``` + + + ## Built-in serializers + + | Serializer | Format | Best for | |---|---|---| | `CloudpickleSerializer` (default) | Binary (pickle) | Complex Python objects, lambdas, closures | | `JsonSerializer` | JSON | Simple types, cross-language interop, debugging | + + + + +| Serializer | Format | Best for | +|---|---|---| +| `JsonSerializer` (default) | JSON | Simple types, cross-language interop, debugging | +| `MsgpackSerializer` | Binary (msgpack) | Compact payloads, smaller storage | +| `SignedSerializer` | Wraps a codec + HMAC-SHA256 | Tamper detection | +| `EncryptedSerializer` | Wraps a codec + AES-256-GCM | Encrypted, authenticated payloads | + + + ## Custom serializers -Implement the `Serializer` protocol (`dumps(obj) -> bytes`, `loads(data) -> Any`). +Implement the `Serializer` interface with dumps(obj) / loads(data)} node={<>serialize(value) / deserialize(bytes)} /> methods. ## What gets serialized -- **Arguments**: `serializer.dumps((args, kwargs))` — stored as BLOB in `payload` -- **Results**: `serializer.dumps(return_value)` — stored as BLOB in `result` +- **Arguments**: serialized and stored as a BLOB in `payload` +- **Results**: serialized and stored as a BLOB in `result` - **Periodic task args**: serialized at registration time, stored as BLOBs in `periodic_tasks.args` diff --git a/docs/content/docs/architecture/worker-pool.mdx b/docs/content/docs/architecture/worker-pool.mdx index d62ed5d0..15399066 100644 --- a/docs/content/docs/architecture/worker-pool.mdx +++ b/docs/content/docs/architecture/worker-pool.mdx @@ -1,18 +1,22 @@ --- title: Worker Pool -description: "How the scheduler dispatches jobs to Python task functions across threads and async runtimes." +description: "How the scheduler dispatches jobs to your task functions across threads and async runtimes." --- -The worker pool dispatches jobs from the scheduler to Python task functions. +The worker pool dispatches jobs from the scheduler to task +functions. ## Design decisions -- **OS threads, not Python threads**: sync workers are Rust `std::thread` threads. - The GIL is only acquired when calling Python task code. - **Bounded channels**: both job and result channels are bounded to `workers × 2` to provide backpressure. + + + +- **OS threads, not Python threads**: sync workers are Rust `std::thread` threads. + The GIL is only acquired when calling Python task code. - **GIL isolation**: each sync worker acquires the GIL independently using `Python::with_gil()`. The scheduler and result handler release the GIL via `py.allow_threads()`. @@ -23,3 +27,19 @@ The worker pool dispatches jobs from the scheduler to Python task functions. - **Context isolation**: sync tasks use `threading.local` for `current_job`; async tasks use `contextvars.ContextVar`, which is properly scoped across `await` boundaries and isolated between concurrent coroutines. + + + + + +- **OS threads for sync, no GIL**: sync handlers run on a Rust `std::thread` + pool owned by the core, independent across workers — there's no global lock to + coordinate. +- **Native async dispatch**: `async` handlers bypass the thread pool. They run + on your Node event loop as independent async invocations; each returned promise + is awaited and its result bridged back into the Rust scheduler. +- **Context isolation**: the running job's context (`currentJob()`) is carried + with `AsyncLocalStorage`, so it stays correct across `await` boundaries and + between concurrently dispatched jobs. + + diff --git a/docs/content/docs/resources/comparison.mdx b/docs/content/docs/resources/comparison.mdx index 531071fe..3bdcd4a8 100644 --- a/docs/content/docs/resources/comparison.mdx +++ b/docs/content/docs/resources/comparison.mdx @@ -3,16 +3,16 @@ title: Comparison description: "Taskito vs Celery, RQ, Dramatiq, Huey, TaskIQ — feature matrix and decision guide." --- -**TL;DR**: Taskito is Celery without the broker. Rust scheduler, no -Redis/RabbitMQ, lower latency, better concurrency. Start with SQLite, scale to -Postgres when needed. +**TL;DR**: Taskito is without the +broker. Rust scheduler, no Redis/RabbitMQ, lower latency, better concurrency. +Start with SQLite, scale to Postgres when needed. ## Feature matrix | Feature | taskito | Celery | RQ | Dramatiq | Huey | TaskIQ | |---|---|---|---|---|---|---| | Broker required | **No** | Redis / RabbitMQ | Redis | Redis / RabbitMQ | Redis | Redis / RabbitMQ / Nats | -| Core language | **Rust + Python** | Python | Python | Python | Python | Python | +| Core language | **Rust + ** | Python | Python | Python | Python | Python | | Priority queues | **Yes** | Yes | No | No | Yes | Yes | | Rate limiting | **Yes** | Yes | No | Yes | No | No | | Dead letter queue | **Yes** | No | Yes | No | No | No | @@ -77,8 +77,11 @@ widely adopted. **Choose taskito** if you want zero-infrastructure simplicity on a single machine. **Choose Celery** if you need distributed workers, complex routing, or enterprise features. -Looking to switch? See the Migrating from Celery guide for a -step-by-step walkthrough with side-by-side code examples. +Looking to switch? See the{" "} + + Migrating from +{" "} +guide for a step-by-step walkthrough with side-by-side code examples. ### vs RQ (Redis Queue) diff --git a/docs/content/docs/resources/faq.mdx b/docs/content/docs/resources/faq.mdx index c74251d2..378f65be 100644 --- a/docs/content/docs/resources/faq.mdx +++ b/docs/content/docs/resources/faq.mdx @@ -125,13 +125,22 @@ or RabbitMQ). taskito replaces **both** broker and backend with a single SQLite database. Additionally: - taskito's scheduler runs in Rust (faster polling, lower overhead) -- Worker threads are OS threads managed by Rust, not Python processes -- No external dependencies beyond `cloudpickle` +- Worker threads are OS threads managed by Rust, not{" "} + +- No external dependencies beyond{" "} + cloudpickle} node="the native addon" /> ## Can I use async tasks? -Yes. Define the task function with `async def` and the worker dispatches it -natively — no `asyncio.run()` wrapping, no thread-pool bridging: +Yes. Define the task function with{" "} +async def} node={async} /> and the +worker dispatches it natively —{" "} +: + + ```python @queue.task() @@ -151,16 +160,38 @@ result = await job.aresult(timeout=30) stats = await queue.astats() ``` + + + + +```ts +queue.task("fetchUrls", async (urls: string[]) => + Promise.all(urls.map((u) => fetch(u).then((r) => r.text()))), +); +``` + +Enqueue and await results from your application code: + +```ts +const id = queue.enqueue("fetchUrls", [urls]); +const result = await queue.result(id); +const stats = queue.stats(); +``` + + + Sync and async tasks can coexist in the same queue. The worker automatically -routes each job to the correct pool based on the task type. See the -Async Tasks guide for details including -`async_concurrency` tuning and `current_job` context in async tasks. +routes each job to the correct pool based on the task type. See the{" "} +Async Tasks guide for details +including concurrency tuning and{" "} +current_job} node={currentJob()} />{" "} +context in async tasks. ## What serialization format does taskito use? -By default, `CloudpickleSerializer` — which supports most Python objects -including lambdas and closures. You can switch to `JsonSerializer` for -simpler, cross-language payloads, or provide a custom serializer: +By default, CloudpickleSerializer} node={JsonSerializer} /> — — or provide a custom serializer: + + ```python from taskito import Queue, JsonSerializer @@ -171,8 +202,23 @@ queue = Queue(serializer=JsonSerializer()) Custom serializers implement the `Serializer` protocol with `dumps(obj) -> bytes` and `loads(data) -> Any` methods. -Regardless of serializer, avoid passing unpicklable/unserializable objects -like open file handles, database connections, or thread locks. + + + + +```ts +import { Queue, MsgpackSerializer } from "@byteveda/taskito"; + +new Queue({ dbPath: "taskito.db", serializer: new MsgpackSerializer() }); +``` + +Custom serializers implement the `Serializer` interface with +`serialize(value) -> bytes` and `deserialize(bytes) -> value` methods. + + + +Regardless of serializer, avoid passing unserializable objects like open file +handles, database connections, or thread locks. ## Can I run the dashboard and worker in the same process? From 816d002cd37312ea42d4a3857d851438de36a1a0 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:20:02 +0530 Subject: [PATCH 04/11] feat(docs): drive SDK from hero snippet tabs --- docs/app/components/landing/hero.tsx | 62 +++++++++++++--------------- docs/app/lib/landing-content.ts | 19 ++++++--- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/docs/app/components/landing/hero.tsx b/docs/app/components/landing/hero.tsx index 216e559f..1aed81d1 100644 --- a/docs/app/components/landing/hero.tsx +++ b/docs/app/components/landing/hero.tsx @@ -2,10 +2,9 @@ import { useState } from "react"; import { Link } from "react-router"; import { RawHtml } from "@/components/ui"; import { useSdk } from "@/hooks"; +import { sdkProfile } from "@/lib"; import { highlightPython, highlightTs } from "@/lib/highlight-lite"; -import { HERO_PANES } from "@/lib/landing-content"; - -type Lang = "py" | "ts"; +import { HERO_COMING_SOON, HERO_PANES } from "@/lib/landing-content"; function CopyButton({ text }: { text: string }) { const [copied, setCopied] = useState(false); @@ -26,27 +25,26 @@ function CopyButton({ text }: { text: string }) { export function Hero() { const { sdk, setSdk } = useSdk(); - // py/ts mirror the global SDK so the hero tab and the docs sidebar switch stay in sync. - const isNode = sdk === "node"; - const lang: Lang = isNode ? "ts" : "py"; - const active = HERO_PANES.find((p) => p.id === lang) ?? HERO_PANES[0]; + // The selected snippet IS the global SDK — clicking a tab sets it, so the hero + // copy, the install/quickstart links, and the docs sidebar switch all follow. + const active = HERO_PANES.find((p) => p.sdk === sdk) ?? HERO_PANES[0]; + const label = sdkProfile(active.sdk).label; const codeHtml = - lang === "ts" ? highlightTs(active.code) : highlightPython(active.code); + active.lang === "ts" + ? highlightTs(active.code) + : highlightPython(active.code); return (

One queue. - - Built for {isNode ? "Node.js." : "Python."} - + Built for {label}.

- A Rust-powered task queue with a first-class{" "} - {isNode ? "Node.js" : "Python"} SDK over one core and one store - — no broker. Start on SQLite, scale to{" "} - Postgres. + A Rust-powered task queue with a first-class {label} SDK over + one core and one store — no broker. Start on SQLite, + scale to Postgres.

@@ -62,7 +60,7 @@ export function Hero() {
Brokerless Rust core - {isNode ? "Node.js SDK" : "Python SDK"} + {label} SDK DAG workflows
@@ -86,18 +84,20 @@ export function Hero() {
{HERO_PANES.map((p) => ( + ))} + {HERO_COMING_SOON.map((name) => ( + ))} -
@@ -119,15 +119,11 @@ export function Hero() {
- - Read the Python quickstart → - - - Read the Node.js quickstart → - + {HERO_PANES.map((p) => ( + + {p.docLabel} → + + ))}
diff --git a/docs/app/lib/landing-content.ts b/docs/app/lib/landing-content.ts index 356d598f..b4b700ce 100644 --- a/docs/app/lib/landing-content.ts +++ b/docs/app/lib/landing-content.ts @@ -1,5 +1,7 @@ // Landing-page copy + code, ported verbatim from the prototype (index.html / landing.js). +import type { Sdk } from "./sdk-registry"; + /** A worker-output line: glyph + text, optionally a result value + timing. */ export interface OutLine { glyph: string; @@ -10,8 +12,10 @@ export interface OutLine { } export interface LangPane { - id: "py" | "ts"; - label: string; + /** Which SDK this snippet is for — selecting it sets the global SDK. */ + sdk: Sdk; + /** Highlighter dialect for the snippet. */ + lang: "py" | "ts"; filename: string; install: string; code: string; @@ -20,10 +24,13 @@ export interface LangPane { docLabel: string; } +/** SDKs shown in the hero tab strip as "Soon" (no pane yet). */ +export const HERO_COMING_SOON: string[] = ["Java"]; + export const HERO_PANES: LangPane[] = [ { - id: "py", - label: "Python", + sdk: "python", + lang: "py", filename: "tasks.py", install: "pip install taskito", code: `from taskito import Queue @@ -55,8 +62,8 @@ print(job.result()) # → 5`, docLabel: "Read the Python quickstart", }, { - id: "ts", - label: "Node.js", + sdk: "node", + lang: "ts", filename: "tasks.ts", install: "pnpm add taskito", code: `import { Queue } from "taskito"; From e5eb776e7d1dac5f7d219d01ae85f8c192f69e01 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:20:52 +0530 Subject: [PATCH 05/11] fix(docs): make top nav and CTA links follow the SDK SiteNav LINKS and the landing CTA hardcoded /python/...; they now prefix the active SDK like the footer, so e.g. API points to /node/api-reference under Node. --- docs/app/components/landing/sections.tsx | 3 ++- docs/app/components/ui/site-nav.tsx | 14 +++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/app/components/landing/sections.tsx b/docs/app/components/landing/sections.tsx index 404859f2..9e3c87d8 100644 --- a/docs/app/components/landing/sections.tsx +++ b/docs/app/components/landing/sections.tsx @@ -397,6 +397,7 @@ function InstallPill({ cmd }: { cmd: string }) { } export function CTA() { + const sdk = useActiveSdk(); return (
@@ -414,7 +415,7 @@ export function CTA() {
- + Start the quickstart → diff --git a/docs/app/components/ui/site-nav.tsx b/docs/app/components/ui/site-nav.tsx index f5400e43..c57d1c47 100644 --- a/docs/app/components/ui/site-nav.tsx +++ b/docs/app/components/ui/site-nav.tsx @@ -1,6 +1,7 @@ import { Menu, Search } from "lucide-react"; import { Link } from "react-router"; import { ThemeToggle } from "@/components/ui/theme-toggle"; +import { useActiveSdk } from "@/hooks"; // lucide dropped brand glyphs, so the GitHub mark is inlined. function GithubMark() { @@ -17,11 +18,13 @@ function GithubMark() { ); } -const LINKS = [ - { label: "Getting Started", href: "/python/getting-started/installation" }, - { label: "Guides", href: "/python/guides" }, +// `sdk` links are SDK-relative (prefixed with the active /python|/node); the rest +// are shared, SDK-neutral pages. +const LINKS: { label: string; href: string; sdk?: boolean }[] = [ + { label: "Getting Started", href: "getting-started/installation", sdk: true }, + { label: "Guides", href: "guides", sdk: true }, { label: "Architecture", href: "/architecture" }, - { label: "API", href: "/python/api-reference" }, + { label: "API", href: "api-reference", sdk: true }, { label: "Changelog", href: "/resources/changelog" }, ]; @@ -35,6 +38,7 @@ export function SiteNav({ onSearch?: () => void; onMenu?: () => void; }) { + const sdk = useActiveSdk(); return (
diff --git a/docs/app/styles/landing.css b/docs/app/styles/landing.css index f205af0e..114a9942 100644 --- a/docs/app/styles/landing.css +++ b/docs/app/styles/landing.css @@ -677,36 +677,6 @@ main { font-size: 11.5px; color: var(--cyan); } -.rspark { - position: absolute; - left: 0; - top: 50%; - width: 7px; - height: 7px; - margin-top: -3.5px; - border-radius: 50%; - background: var(--cyan); - box-shadow: 0 0 10px var(--cyan); - opacity: 0; - animation: rmove 2.6s ease-in-out infinite; - animation-delay: 1s; -} -@keyframes rmove { - 0% { - left: 2%; - opacity: 0; - } - 15% { - opacity: 1; - } - 85% { - opacity: 1; - } - 100% { - left: 98%; - opacity: 0; - } -} /* ===== features ===== */ .feat-grid { From b6b613762543f56558dd35611ea3912a8befa204 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:21:00 +0530 Subject: [PATCH 07/11] feat(docs): derive footer version from CHANGELOG sync-changelog.mjs now emits app/lib/version.ts from the latest CHANGELOG.md release; the footer reads it instead of a hardcoded v0.16. --- docs/app/components/landing/footer.tsx | 3 ++- docs/app/lib/version.ts | 2 ++ scripts/sync-changelog.mjs | 27 ++++++++++++++++++++++---- 3 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 docs/app/lib/version.ts diff --git a/docs/app/components/landing/footer.tsx b/docs/app/components/landing/footer.tsx index 84ae03c6..3cd61be0 100644 --- a/docs/app/components/landing/footer.tsx +++ b/docs/app/components/landing/footer.tsx @@ -1,5 +1,6 @@ import { Link } from "react-router"; import { useActiveSdk } from "@/hooks"; +import { VERSION } from "@/lib/version"; type FootLink = { label: string; @@ -95,7 +96,7 @@ export function Footer() {
© 2026 ByteVeda · taskito - MIT License · v0.16 + MIT License · v{VERSION}
); diff --git a/docs/app/lib/version.ts b/docs/app/lib/version.ts new file mode 100644 index 00000000..8425a83c --- /dev/null +++ b/docs/app/lib/version.ts @@ -0,0 +1,2 @@ +// AUTO-GENERATED from /CHANGELOG.md by scripts/sync-changelog.mjs — do not edit directly. +export const VERSION = "0.18.0"; diff --git a/scripts/sync-changelog.mjs b/scripts/sync-changelog.mjs index 15bc4e12..fe7611a8 100644 --- a/scripts/sync-changelog.mjs +++ b/scripts/sync-changelog.mjs @@ -1,9 +1,11 @@ #!/usr/bin/env node -// Generate the docs changelog page from the canonical root CHANGELOG.md. +// Generate the docs changelog page + version constant from the canonical root +// CHANGELOG.md. // // CHANGELOG.md (Keep a Changelog) is the single source of truth — edit it there. -// This script wraps its body in the frontmatter the docs site needs and writes -// docs/content/docs/resources/changelog.mdx. Run automatically before `docs` dev/build. +// This script wraps its body in the frontmatter the docs site needs (writing +// docs/content/docs/resources/changelog.mdx) and emits the latest version to +// docs/app/lib/version.ts. Run automatically before `docs` dev/build. import { readFileSync, writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; @@ -12,9 +14,26 @@ const source = fileURLToPath(new URL("CHANGELOG.md", repoRoot)); const target = fileURLToPath( new URL("docs/content/docs/resources/changelog.mdx", repoRoot), ); +const versionTarget = fileURLToPath( + new URL("docs/app/lib/version.ts", repoRoot), +); +const changelog = readFileSync(source, "utf8"); // Drop the top-level "# Changelog" heading — the frontmatter title renders it. -const body = readFileSync(source, "utf8").replace(/^#\s+Changelog\s*\n/, "").trimStart(); +const body = changelog.replace(/^#\s+Changelog\s*\n/, "").trimStart(); + +// The first `## x.y.z` heading is the latest release — the docs footer reads it. +const latest = body.match(/^##\s+(\d+\.\d+\.\d+[\w.-]*)/m)?.[1]; +if (!latest) { + throw new Error( + "sync-changelog: no `## x.y.z` release heading found in CHANGELOG.md", + ); +} +writeFileSync( + versionTarget, + `// AUTO-GENERATED from /CHANGELOG.md by scripts/sync-changelog.mjs — do not edit directly.\nexport const VERSION = "${latest}";\n`, +); +console.log(`synced ${versionTarget} (v${latest})`); const frontmatter = [ "---", From 25a3b687e56154c257038e39c03c993867b29629 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:32:50 +0530 Subject: [PATCH 08/11] fix(docs): build SDK bootstrap from data, not interpolated code Pass the registry id list as an inert data-sdk-config attribute and parse it in a static bootstrap, instead of interpolating values into the script (CodeQL code-construction). --- docs/app/root.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/app/root.tsx b/docs/app/root.tsx index a16027c0..5b9b7a98 100644 --- a/docs/app/root.tsx +++ b/docs/app/root.tsx @@ -27,11 +27,12 @@ export const links: Route.LinksFunction = () => [ // Apply the persisted theme before paint to avoid a light/dark flash. const THEME_INIT = `(function(){try{var t=localStorage.getItem('taskito-theme')||'dark';document.documentElement.setAttribute('data-theme',t);}catch(e){}})();`; -// Apply the active SDK before paint (query > localStorage > default) so the -// CSS show/hide picks the right variant with no flash. Mirrors readSdk(). The -// valid-id list and default come from the SDK registry, so a new language needs -// no edit here. -const SDK_INIT = `(function(){try{var ids=${JSON.stringify([...SDK_IDS])},def='${DEFAULT_SDK}';var u=new URLSearchParams(location.search).get('sdk');var s=ids.indexOf(u)>=0?u:(localStorage.getItem('taskito-sdk')||def);if(ids.indexOf(s)<0){s=def;}document.documentElement.setAttribute('data-sdk',s);}catch(e){document.documentElement.setAttribute('data-sdk','${DEFAULT_SDK}');}})();`; +// Registry ids + default as inert JSON on the data attribute below. +const SDK_CONFIG = JSON.stringify({ ids: [...SDK_IDS], def: DEFAULT_SDK }); + +// No-flash SDK bootstrap: query > localStorage > default, validated against the +// data-sdk-config above. Static string (no interpolation); keeps the SSR default on error. +const SDK_INIT = `(function(){try{var c=JSON.parse(document.documentElement.getAttribute('data-sdk-config')),ids=c.ids,def=c.def;var u=new URLSearchParams(location.search).get('sdk');var s=ids.indexOf(u)>=0?u:(localStorage.getItem('taskito-sdk')||def);if(ids.indexOf(s)<0){s=def;}document.documentElement.setAttribute('data-sdk',s);}catch(e){}})();`; export function Layout({ children }: { children: React.ReactNode }) { return ( @@ -39,6 +40,7 @@ export function Layout({ children }: { children: React.ReactNode }) { lang="en" data-theme="dark" data-sdk={DEFAULT_SDK} + data-sdk-config={SDK_CONFIG} suppressHydrationWarning > @@ -46,7 +48,7 @@ export function Layout({ children }: { children: React.ReactNode }) { {/* biome-ignore lint/security/noDangerouslySetInnerHtml: tiny no-flash theme bootstrap */}