diff --git a/apps/docs/app/docs/[[...slug]]/page.tsx b/apps/docs/app/docs/[[...slug]]/page.tsx
index 840c88a..3fff59d 100644
--- a/apps/docs/app/docs/[[...slug]]/page.tsx
+++ b/apps/docs/app/docs/[[...slug]]/page.tsx
@@ -1,10 +1,10 @@
-import { createRelativeLink } from "fumadocs-ui/mdx";
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
} from "fumadocs-ui/layouts/docs/page";
+import { createRelativeLink } from "fumadocs-ui/mdx";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getMDXComponents } from "@/components/mdx";
diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css
index 423e5cf..d3428a8 100644
--- a/apps/docs/app/global.css
+++ b/apps/docs/app/global.css
@@ -2,6 +2,70 @@
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";
+@theme {
+ --font-sans: "Space Grotesk", system-ui, sans-serif;
+ --font-mono: "JetBrains Mono", ui-monospace, monospace;
+}
+
+:root {
+ --accent: #c6f94f;
+ /* Light theme: same character as the landing palette, readable on white. */
+ --color-fd-background: hsl(240, 6%, 97%);
+ --color-fd-foreground: hsl(240, 8%, 8%);
+ --color-fd-muted: hsl(240, 6%, 93%);
+ --color-fd-muted-foreground: hsl(240, 5%, 42%);
+ --color-fd-card: hsl(240, 6%, 95%);
+ --color-fd-card-foreground: hsl(240, 8%, 8%);
+ --color-fd-border: hsla(240, 8%, 60%, 0.35);
+ --color-fd-primary: hsl(78, 85%, 26%);
+ --color-fd-primary-foreground: hsl(240, 8%, 98%);
+ --color-fd-ring: hsl(78, 85%, 30%);
+}
+
+/* Dark theme: the landing page palette. */
+.dark {
+ --color-fd-background: #0b0b0e;
+ --color-fd-foreground: #ededf2;
+ --color-fd-muted: #101016;
+ --color-fd-muted-foreground: #8b8b98;
+ --color-fd-popover: #0e0e13;
+ --color-fd-popover-foreground: #b9b9c4;
+ --color-fd-card: #101016;
+ --color-fd-card-foreground: #ededf2;
+ --color-fd-border: #23232b;
+ --color-fd-primary: var(--accent);
+ --color-fd-primary-foreground: #0b0b0e;
+ --color-fd-secondary: #101016;
+ --color-fd-secondary-foreground: #ededf2;
+ --color-fd-accent: #1a1a22;
+ --color-fd-accent-foreground: #ededf2;
+ --color-fd-ring: var(--accent);
+}
+
+.dark #nd-sidebar {
+ --color-fd-muted: #101016;
+ --color-fd-secondary: #15151c;
+ --color-fd-muted-foreground: #8b8b98;
+}
+
+.dark ::selection {
+ background: var(--accent);
+ color: #0b0b0e;
+}
+
html {
scrollbar-gutter: stable;
}
+
+.docs-wordmark {
+ font: 700 16px var(--font-mono);
+ letter-spacing: -0.02em;
+}
+
+.docs-wordmark-accent {
+ color: var(--accent);
+}
+
+:root:not(.dark) .docs-wordmark-accent {
+ color: var(--color-fd-primary);
+}
diff --git a/apps/docs/app/landing.css b/apps/docs/app/landing.css
new file mode 100644
index 0000000..6d46497
--- /dev/null
+++ b/apps/docs/app/landing.css
@@ -0,0 +1,722 @@
+/* Landing page styles — recreated from the EffectMQ landing design handoff. */
+
+.lp {
+ background: #0b0b0e;
+ min-height: 100vh;
+ color: #ededf2;
+ font-family: "Space Grotesk", system-ui, sans-serif;
+}
+
+.lp ::selection {
+ background: var(--accent);
+ color: #0b0b0e;
+}
+
+.lp a {
+ color: var(--accent);
+ text-decoration: none;
+}
+
+.lp a:hover {
+ text-decoration: underline;
+}
+
+@keyframes lp-blink {
+ 0%,
+ 49% {
+ opacity: 1;
+ }
+ 50%,
+ 100% {
+ opacity: 0;
+ }
+}
+
+.lp-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding-left: 32px;
+ padding-right: 32px;
+}
+
+/* ---- nav ---- */
+
+.lp-nav {
+ padding-top: 22px;
+ padding-bottom: 22px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.lp-wordmark {
+ font:
+ 700 18px "JetBrains Mono",
+ monospace;
+ letter-spacing: -0.02em;
+ color: #ededf2;
+}
+
+.lp-wordmark .lp-accent {
+ color: var(--accent);
+}
+
+.lp-cursor {
+ color: var(--accent);
+ animation: lp-blink 1.1s step-end infinite;
+}
+
+.lp-nav-links {
+ display: flex;
+ align-items: center;
+ gap: 26px;
+ font:
+ 500 14px "Space Grotesk",
+ sans-serif;
+}
+
+.lp a.lp-nav-docs {
+ color: #b9b9c4;
+}
+
+.lp a.lp-nav-github {
+ color: #0b0b0e;
+ background: var(--accent);
+ padding: 8px 16px;
+ border-radius: 8px;
+ font-weight: 600;
+}
+
+/* ---- hero ---- */
+
+.lp-hero {
+ position: relative;
+ overflow: hidden;
+}
+
+.lp-hero-grid {
+ position: absolute;
+ inset: 0;
+ background-image: radial-gradient(
+ rgba(255, 255, 255, 0.05) 1px,
+ transparent 1px
+ );
+ background-size: 28px 28px;
+ pointer-events: none;
+}
+
+.lp-hero-glow {
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(
+ 700px 340px at 72% -10%,
+ color-mix(in oklab, var(--accent) 13%, transparent),
+ transparent
+ );
+ pointer-events: none;
+}
+
+.lp-hero-inner {
+ position: relative;
+ padding-top: 88px;
+ padding-bottom: 72px;
+}
+
+.lp-chips {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 28px;
+ flex-wrap: wrap;
+}
+
+.lp-chip {
+ font:
+ 500 11px "JetBrains Mono",
+ monospace;
+ letter-spacing: 0.1em;
+ color: #8b8b98;
+ border: 1px solid #26262e;
+ border-radius: 999px;
+ padding: 4px 12px;
+}
+
+.lp-chip-accent {
+ color: var(--accent);
+ border-color: color-mix(in oklab, var(--accent) 40%, transparent);
+}
+
+.lp-hero h1 {
+ margin: 0 0 26px;
+ font:
+ 700 clamp(46px, 6.4vw, 84px) / 1.02 "Space Grotesk",
+ sans-serif;
+ letter-spacing: -0.035em;
+ max-width: 900px;
+ text-wrap: balance;
+}
+
+.lp-hero h1 .lp-accent,
+.lp-h2 .lp-accent {
+ color: var(--accent);
+}
+
+.lp-hero-sub {
+ margin: 0 0 40px;
+ font:
+ 400 19px / 1.6 "Space Grotesk",
+ sans-serif;
+ color: #a7a7b3;
+ max-width: 640px;
+ text-wrap: pretty;
+}
+
+.lp-cta-row {
+ display: flex;
+ gap: 14px;
+ align-items: stretch;
+ flex-wrap: wrap;
+}
+
+.lp-install {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ background: #101016;
+ border: 1px solid #2a2a33;
+ border-radius: 10px;
+ padding: 14px 18px;
+ font:
+ 500 14px "JetBrains Mono",
+ monospace;
+}
+
+.lp-install .lp-prompt {
+ color: #8b8b98;
+ user-select: none;
+}
+
+.lp-install .lp-pkg {
+ white-space: nowrap;
+}
+
+.lp-install .lp-pkg .lp-accent {
+ color: var(--accent);
+}
+
+.lp-copy-btn {
+ font:
+ 600 12px "JetBrains Mono",
+ monospace;
+ color: #0b0b0e;
+ background: var(--accent);
+ border: none;
+ border-radius: 6px;
+ padding: 7px 13px;
+ cursor: pointer;
+}
+
+.lp-copy-btn:hover {
+ filter: brightness(1.1);
+}
+
+.lp a.lp-ghost-btn {
+ display: flex;
+ align-items: center;
+ color: #ededf2;
+ border: 1px solid #2a2a33;
+ border-radius: 10px;
+ padding: 14px 22px;
+ font:
+ 500 15px "Space Grotesk",
+ sans-serif;
+}
+
+.lp a.lp-ghost-btn:hover {
+ border-color: #3d3d49;
+ text-decoration: none;
+}
+
+/* ---- sections ---- */
+
+.lp-eyebrow {
+ margin: 0 0 14px;
+ font:
+ 600 12px "JetBrains Mono",
+ monospace;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ color: var(--accent);
+}
+
+.lp-h2 {
+ margin: 0 0 16px;
+ font:
+ 700 clamp(30px, 3.6vw, 46px) / 1.1 "Space Grotesk",
+ sans-serif;
+ letter-spacing: -0.02em;
+}
+
+.lp-lede {
+ margin: 0 0 36px;
+ font:
+ 400 16px / 1.6 "Space Grotesk",
+ sans-serif;
+ color: #a7a7b3;
+ max-width: 620px;
+ text-wrap: pretty;
+}
+
+.lp-band {
+ border-top: 1px solid #1a1a22;
+ background: #0d0d12;
+}
+
+/* ---- panels / canvases ---- */
+
+.lp-panel {
+ background: #0e0e13;
+ border: 1px solid #23232b;
+ border-radius: 14px;
+}
+
+.lp-canvas-panel {
+ padding: 8px 8px 0;
+}
+
+.lp-canvas-panel canvas,
+.lp-sem-panel canvas {
+ width: 100%;
+ display: block;
+}
+
+.lp-sem-panel {
+ padding: 8px;
+}
+
+.lp-legend {
+ display: flex;
+ gap: 26px;
+ margin-top: 16px;
+ flex-wrap: wrap;
+ font:
+ 400 12px "JetBrains Mono",
+ monospace;
+ color: #8b8b98;
+}
+
+.lp-legend span {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.lp-legend .lp-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+}
+
+/* ---- code ---- */
+
+.lp-tabs {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-bottom: 14px;
+}
+
+.lp-tab {
+ font:
+ 500 13px "JetBrains Mono",
+ monospace;
+ padding: 9px 16px;
+ border: 1px solid #2a2a33;
+ background: transparent;
+ color: #8b8b98;
+ border-radius: 8px;
+ cursor: pointer;
+}
+
+.lp-tab[data-active="true"] {
+ border-color: var(--accent);
+ background: var(--accent);
+ color: #0b0b0e;
+}
+
+.lp-tab-sm {
+ font-size: 12px;
+ padding: 7px 14px;
+ border-radius: 7px;
+}
+
+.lp-code-panel {
+ padding: 26px 30px;
+ overflow: auto;
+}
+
+.lp-code-panel pre {
+ margin: 0;
+ font:
+ 400 13.5px / 1.75 "JetBrains Mono",
+ monospace;
+ color: #c9c9d4;
+}
+
+.lp-code-sm {
+ border-radius: 12px;
+ padding: 18px 20px;
+}
+
+.lp-code-sm pre {
+ font-size: 12.5px;
+ line-height: 1.7;
+}
+
+.lp-code-panel .tk-k {
+ color: #b8a7ff;
+}
+
+.lp-code-panel .tk-t {
+ color: #7ee0da;
+}
+
+.lp-code-panel .tk-s {
+ color: var(--accent);
+}
+
+.lp-code-panel .tk-c {
+ color: #61616e;
+ font-style: italic;
+}
+
+.lp-code-panel .tk-n {
+ color: #ffb86b;
+}
+
+/* ---- concurrency ---- */
+
+.lp-conc-grid {
+ padding-top: 96px;
+ padding-bottom: 96px;
+ display: grid;
+ grid-template-columns: minmax(320px, 5fr) 6fr;
+ gap: 56px;
+ align-items: center;
+}
+
+.lp-conc-body {
+ margin: 0 0 22px;
+ font:
+ 400 16px / 1.65 "Space Grotesk",
+ sans-serif;
+ color: #a7a7b3;
+ text-wrap: pretty;
+}
+
+.lp-conc-body .lp-inline-code {
+ font-family: "JetBrains Mono", monospace;
+ font-size: 14px;
+ color: #ededf2;
+}
+
+.lp-conc-tabs {
+ margin-bottom: 12px;
+}
+
+/* ---- benchmarks ---- */
+
+.lp-stats {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 20px;
+ margin-bottom: 22px;
+}
+
+.lp-stat-card {
+ background: #101016;
+ border: 1px solid #23232b;
+ border-radius: 12px;
+ padding: 22px 24px;
+}
+
+.lp-stat-value {
+ margin: 0;
+ font:
+ 700 34px "Space Grotesk",
+ sans-serif;
+ color: var(--accent);
+ letter-spacing: -0.02em;
+}
+
+.lp-stat-unit {
+ font-size: 17px;
+ color: #8b8b98;
+ font-weight: 500;
+}
+
+.lp-stat-caption {
+ margin: 6px 0 0;
+ font:
+ 400 13px "Space Grotesk",
+ sans-serif;
+ color: #8b8b98;
+}
+
+.lp-chart-panel {
+ padding: 34px 34px 26px;
+}
+
+.lp-chart-title {
+ margin: 0 0 26px;
+ font:
+ 500 12px "JetBrains Mono",
+ monospace;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: #8b8b98;
+}
+
+.lp-chart-groups {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 40px;
+}
+
+.lp-bars {
+ display: flex;
+ align-items: flex-end;
+ gap: 14px;
+ height: 210px;
+}
+
+.lp-bar-col {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ height: 100%;
+ gap: 8px;
+}
+
+.lp-bar-value {
+ margin: 0;
+ text-align: center;
+ font:
+ 500 12px "JetBrains Mono",
+ monospace;
+ color: #ededf2;
+}
+
+.lp-bar {
+ background: linear-gradient(
+ to top,
+ color-mix(in oklab, var(--accent) 55%, #0e0e13),
+ var(--accent)
+ );
+ border-radius: 6px 6px 2px 2px;
+ transition: height 0.9s cubic-bezier(0.2, 0.7, 0.2, 1);
+ min-height: 3px;
+}
+
+.lp-bar-labels {
+ display: flex;
+ gap: 14px;
+ margin-top: 10px;
+}
+
+.lp-bar-labels p {
+ margin: 0;
+ flex: 1;
+ text-align: center;
+ font:
+ 400 11px "JetBrains Mono",
+ monospace;
+ color: #63636e;
+}
+
+.lp-group-label {
+ margin: 14px 0 0;
+ text-align: center;
+ font:
+ 500 13px "JetBrains Mono",
+ monospace;
+ color: #b9b9c4;
+}
+
+.lp-footnote {
+ margin: 16px 2px 0;
+ font:
+ 400 12px / 1.7 "JetBrains Mono",
+ monospace;
+ color: #63636e;
+}
+
+/* ---- feature cards ---- */
+
+.lp-cards {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 18px;
+}
+
+.lp-card {
+ background: #101016;
+ border: 1px solid #23232b;
+ border-radius: 12px;
+ padding: 26px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.lp-card:hover {
+ border-color: #34343f;
+}
+
+.lp-card h3 {
+ margin: 0;
+ font:
+ 600 19px "Space Grotesk",
+ sans-serif;
+ letter-spacing: -0.01em;
+}
+
+.lp-card p {
+ margin: 0;
+ font:
+ 400 14px / 1.6 "Space Grotesk",
+ sans-serif;
+ color: #8b8b98;
+ flex: 1;
+ text-wrap: pretty;
+}
+
+.lp-card code {
+ font:
+ 400 12px "JetBrains Mono",
+ monospace;
+ color: var(--accent);
+}
+
+/* ---- CTA ---- */
+
+.lp-cta {
+ border-top: 1px solid #1a1a22;
+ position: relative;
+ overflow: hidden;
+}
+
+.lp-cta-glow {
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(
+ 640px 300px at 50% 120%,
+ color-mix(in oklab, var(--accent) 12%, transparent),
+ transparent
+ );
+ pointer-events: none;
+}
+
+.lp-cta-inner {
+ position: relative;
+ padding-top: 110px;
+ padding-bottom: 120px;
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 30px;
+}
+
+.lp-cta h2 {
+ margin: 0;
+ font:
+ 700 clamp(34px, 4.4vw, 58px) / 1.1 "Space Grotesk",
+ sans-serif;
+ letter-spacing: -0.025em;
+ max-width: 760px;
+ text-wrap: balance;
+}
+
+.lp-cta h2 .lp-accent {
+ color: var(--accent);
+}
+
+.lp a.lp-star-link {
+ font:
+ 500 15px "Space Grotesk",
+ sans-serif;
+}
+
+/* ---- footer ---- */
+
+.lp-footer {
+ border-top: 1px solid #1a1a22;
+ background: #0a0a0d;
+}
+
+.lp-footer-row {
+ padding-top: 48px;
+ padding-bottom: 28px;
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: 40px;
+ align-items: center;
+}
+
+.lp-footer-mark {
+ font:
+ 700 16px "JetBrains Mono",
+ monospace;
+}
+
+.lp-footer-mark .lp-accent {
+ color: var(--accent);
+}
+
+.lp-footer-links {
+ display: flex;
+ gap: 28px;
+ align-items: center;
+ justify-self: end;
+ font:
+ 400 14px "Space Grotesk",
+ sans-serif;
+}
+
+.lp a.lp-footer-link {
+ color: #b9b9c4;
+}
+
+.lp-footer-fine {
+ padding-bottom: 36px;
+}
+
+.lp-footer-fine p {
+ margin: 0;
+ font:
+ 400 12px "JetBrains Mono",
+ monospace;
+ color: #4a4a54;
+}
+
+.lp .lp-footer-fine a {
+ color: #63636e;
+}
+
+/* ---- responsive ---- */
+
+@media (max-width: 960px) {
+ .lp-conc-grid {
+ grid-template-columns: 1fr;
+ gap: 36px;
+ }
+ .lp-stats,
+ .lp-cards,
+ .lp-chart-groups {
+ grid-template-columns: 1fr;
+ }
+ .lp-chart-groups {
+ gap: 32px;
+ }
+}
diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx
index e39c2c3..d3e6d69 100644
--- a/apps/docs/app/layout.tsx
+++ b/apps/docs/app/layout.tsx
@@ -4,15 +4,31 @@ import type { ReactNode } from "react";
import "./global.css";
export const metadata: Metadata = {
- title: "effectmq",
- description: "Documentation for effectmq.",
+ title: {
+ template: "%s | effectmq",
+ default: "effectmq",
+ },
+ description:
+ "A typed, Redis-backed task queue built on Effect: schemas for payloads, results and errors, with retries, delays, idempotency and cron schedules.",
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
+
+
+
+
+
- {children}
+ {children}
);
diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx
index f8c9409..92aa34f 100644
--- a/apps/docs/app/page.tsx
+++ b/apps/docs/app/page.tsx
@@ -1,5 +1,270 @@
-import { redirect } from "next/navigation";
+import type { Metadata } from "next";
+import Link from "next/link";
+import { ApiTabs } from "@/components/landing/api-tabs";
+import { BenchChart } from "@/components/landing/bench-chart";
+import { ConcurrencyTabs } from "@/components/landing/concurrency-tabs";
+import { InstallCommand } from "@/components/landing/install-command";
+import { LifecycleCanvas } from "@/components/landing/lifecycle-canvas";
+import "./landing.css";
+
+export const metadata: Metadata = {
+ title: "effectmq — typed task queue built on Effect",
+ description:
+ "A task queue built on Effect. Describe work as a schema, process it with a handler that is just an Effect. Retries, delays, idempotency, cron schedules: handled. Redis keeps unfinished work recoverable.",
+};
+
+const GITHUB_URL = "https://github.com/julia-script/effectmq";
+
+const FEATURES = [
+ {
+ title: "Retries are Schedules",
+ body: "Declare retry on the task as an Effect Schedule — exponential, jittered, whatever composes. Exhausted schedule → your failure policy.",
+ code: 'retry: Schedule.exponential("1 second")',
+ },
+ {
+ title: "Idempotency is the id",
+ body: "The idempotency key is the task id. Offer the same key twice and you get one task, not two — replacement is an explicit new generation.",
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: displayed as literal code
+ code: "idempotencyKey: (p) => `email:${p.to}`",
+ },
+ {
+ title: "Durable cron",
+ body: "Scheduler.make materializes a real queue task per tick — competing schedulers and crash recovery can safely re-offer it.",
+ code: 'missed: { _tag: "coalesce" }',
+ },
+ {
+ title: "Typed lifecycle events",
+ body: "task.completed carries your success type; task.failed carries your typed error. Streams, wait and execute all decode against your schemas.",
+ code: "TaskQueue.stream(emails)",
+ },
+ {
+ title: "At-least-once, fenced",
+ body: "Every attempt is fenced with a unique lease token; stalled workers are separated from failed handlers. Unfinished work stays recoverable.",
+ code: "lease: unique token per attempt",
+ },
+ {
+ title: "Swappable engine",
+ body: "The Redis engine is a Layer built on atomic Lua scripts. Provide it and forget it — or swap it for a different implementation, like anything in Effect.",
+ code: "TaskEngine.layer({ redis })",
+ },
+];
export default function HomePage() {
- redirect("/docs");
+ return (
+
+
+
+ effect
+ mq
+ ▮
+
+
+
+
+
+
+
+
+
+ BUILT ON EFFECT 4
+ REDIS-BACKED
+ MIT
+
+
+ Typed payloads.
+
+ Typed results.
+
+ Typed errors. All the way down.
+
+
+ A task queue built on Effect .
+ Describe work as a schema, process it with a handler that is just an
+ Effect. Retries, delays, idempotency, cron schedules: handled. Redis
+ keeps unfinished work recoverable.
+
+
+
+
+ Read the docs →
+
+
+
+
+
+
+ 01 — Lifecycle
+ How a task lives.
+
+ The state machine every task moves through: offered, queued, leased
+ under a unique fence token, and acknowledged — or routed back through
+ its retry Schedule. Unfinished work survives worker loss. (An
+ illustration of the engine's internals, not a product dashboard.)
+
+
+
+
+
+
+
+ waiting / scheduled
+
+
+
+ active (fenced attempt)
+
+
+
+ retry / failed
+
+
+
+ success
+
+
+
+
+
+ 02 — The API
+ The whole loop, in thirty seconds.
+
+ A task is a schema, not a function. Your handler receives a fully
+ decoded payload — the real object, not a JSON string — and its
+ failures are pattern-matchable typed errors.
+
+
+
+
+
+
+
+ 04 — Receipts
+ Numbers, not vibes.
+
+ Every release ships reproducible performance evidence: full end-to-end
+ task lifecycles — atomic create, fenced acquire, acknowledge —
+ measured per payload size and concurrency.
+
+
+
+
+ 2,968 tasks/s
+
+
+ peak lifecycle throughput · 1 KiB @ c=32
+
+
+
+
+ 1.44 ms p50
+
+
+ single-task lifecycle latency · 1 KiB @ c=1
+
+
+
+
+ 39,097 items/s
+
+
+ due-backlog sweep, atomic Lua batches
+
+
+
+
+
+ baseline: Node 22 · Redis 8.0.6 (loopback) · Effect 4.0.0-beta.107 ·
+ macOS arm64 — committed as{" "}
+
+ regression evidence
+
+ , not a capacity claim.
+
+
+
+
+
+
05 — Batteries
+
+ Handled, so you don't.
+
+
+ {FEATURES.map((f) => (
+
+
{f.title}
+
{f.body}
+
{f.code}
+
+ ))}
+
+
+
+
+
+
+
+
+ );
}
diff --git a/apps/docs/components/landing/api-tabs.tsx b/apps/docs/components/landing/api-tabs.tsx
new file mode 100644
index 0000000..b107ada
--- /dev/null
+++ b/apps/docs/components/landing/api-tabs.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import { useState } from "react";
+import { apiSnippets, apiTabLabels } from "./code-snippets";
+
+export function ApiTabs() {
+ const [tab, setTab] = useState(0);
+
+ return (
+ <>
+
+ {apiTabLabels.map((label, i) => (
+ setTab(i)}
+ >
+ {label}
+
+ ))}
+
+ {apiSnippets[tab]}
+ >
+ );
+}
diff --git a/apps/docs/components/landing/bench-chart.tsx b/apps/docs/components/landing/bench-chart.tsx
new file mode 100644
index 0000000..52a1142
--- /dev/null
+++ b/apps/docs/components/landing/bench-chart.tsx
@@ -0,0 +1,68 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+
+const GROUPS: ReadonlyArray]> = [
+ ["64 B", [592.3, 2179.9, 2922.4]],
+ ["1 KiB", [640.8, 2346.9, 2968.0]],
+ ["16 KiB", [483.4, 1623.9, 2035.6]],
+];
+
+export function BenchChart() {
+ const [chartsIn, setChartsIn] = useState(false);
+ const panelRef = useRef(null);
+
+ useEffect(() => {
+ const el = panelRef.current;
+ if (!el) return;
+ const io = new IntersectionObserver(
+ (entries) => {
+ if (entries.some((e) => e.isIntersecting)) {
+ setChartsIn(true);
+ io.disconnect();
+ }
+ },
+ { threshold: 0.25 },
+ );
+ io.observe(el);
+ return () => io.disconnect();
+ }, []);
+
+ return (
+
+
+ Completed lifecycles per second — by payload × concurrency
+
+
+ {GROUPS.map(([label, values]) => (
+
+
+ {values.map((v, i) => (
+
+
+ {Math.round(v).toLocaleString("en-US")}
+
+
+
+ ))}
+
+
+
{label} payload
+
+ ))}
+
+
+ );
+}
diff --git a/apps/docs/components/landing/code-snippets.tsx b/apps/docs/components/landing/code-snippets.tsx
new file mode 100644
index 0000000..686354d
--- /dev/null
+++ b/apps/docs/components/landing/code-snippets.tsx
@@ -0,0 +1,270 @@
+import type { ReactNode } from "react";
+
+function K({ children }: { children: ReactNode }) {
+ return {children} ;
+}
+
+function T({ children }: { children: ReactNode }) {
+ return {children} ;
+}
+
+function S({ children }: { children: ReactNode }) {
+ return {children} ;
+}
+
+function C({ children }: { children: ReactNode }) {
+ return {children} ;
+}
+
+function N({ children }: { children: ReactNode }) {
+ return {children} ;
+}
+
+export const apiTabLabels = [
+ "01 define",
+ "02 queue",
+ "03 worker",
+ "04 concurrency",
+ "05 schedule",
+];
+
+export const apiSnippets: ReadonlyArray = [
+ // 01 define
+
+ import {"{ Schedule, Schema }"} from "effect"
+ {";\n"}
+ import {"{ Task, TaskQueue }"} from "@effectmq/core"
+ {";\n\n"}
+ class EmailRejected extends Schema
+ {".TaggedError<"}
+ EmailRejected
+ {">()(\n "}
+ "EmailRejected"
+ {",\n { reason: "}
+ Schema
+ {".String },\n) {}\n\n"}
+ const SendEmail = Task
+ {".make({\n name: "}
+ "send-email"
+ {",\n payload: { to: "}
+ Schema
+ {".String, subject: "}
+ Schema
+ {".String },\n success: "}
+ Schema
+ {".String,\n error: "}
+ EmailRejected
+ {", "}
+ {"// typed, pattern-matchable failure\n"}
+ {" idempotencyKey: (p) => "}
+ {/* biome-ignore lint/suspicious/noTemplateCurlyInString: displayed as literal code */}
+ {"`email:${p.to}:${p.subject}`"}
+ {",\n retry: "}
+ Schedule
+ {".exponential("}
+ "1 second"
+ {"), "}
+ {"// retries are just Schedules\n"}
+ {"});"}
+ ,
+ // 02 queue
+
+ {"// A queue binds a name to a task definition.\n"}
+ const emails = TaskQueue
+ {".make("}
+ "emails"
+ {", SendEmail);\n\n"}
+ {"// Enqueue a payload — idempotent, durable.\n"}
+ yield
+ {"* "}
+ TaskQueue
+ {".offer(emails, {\n to: "}
+ "ada@example.com"
+ {",\n subject: "}
+ "Welcome"
+ {",\n});\n\n"}
+ {"// Offer + await the outcome in one call.\n"}
+ const result = yield
+ {"* "}
+ TaskQueue
+ {".execute(emails, {\n to: "}
+ "grace@example.com"
+ {",\n subject: "}
+ "Welcome"
+ {",\n}); "}
+ {"// succeeds with your value — or fails with EmailRejected\n"}
+ {"\n"}
+ {"// Or await a task you already offered.\n"}
+ const task = yield
+ {"* "}
+ TaskQueue
+ {".offer(emails, payload);\n"}
+ const outcome = yield
+ {"* "}
+ TaskQueue
+ {".wait(emails, task.handle);"}
+ ,
+ // 03 worker
+
+ {"// Take one task, run it, report the outcome.\n"}
+ yield
+ {"* "}
+ TaskQueue
+ {".complete(emails, (task) =>\n "}
+ Effect
+ {".succeed("}
+ {/* biome-ignore lint/suspicious/noTemplateCurlyInString: displayed as literal code */}
+ {"`provider:${task.payload.to}`"}
+ {"),\n);\n\n"}
+ {"// Or name the handler once, and loop forever.\n"}
+ const
+ {" sendEmailWorker = "}
+ TaskQueue
+ {".complete(emails, handleSendEmail);\n"}
+ yield
+ {"* sendEmailWorker.pipe("}
+ Effect
+ {".repeat("}
+ Schedule
+ {".forever));\n\n"}
+ {"// The engine + its Redis layer: the only wiring you need.\n"}
+ const AppLayer = TaskEngine
+ {".layer({\n redis: { url: "}
+ "redis://localhost:6379"
+ {" },\n});\n\nprogram.pipe("}
+ Effect
+ {".provide(AppLayer), "}
+ NodeRuntime
+ {".runMain);"}
+ ,
+ // 04 concurrency
+
+ {"// No bespoke concurrency options. Just Effect.\n"}
+ const worker = Effect
+ {".gen("}
+ function
+ {"* () {\n "}
+ {"// At most 5 tasks in flight at any moment.\n"}
+ {" "}
+ const semaphore = yield
+ {"* "}
+ Semaphore
+ {".make("}
+ 5
+ {");\n\n "}
+ yield
+ {"* "}
+ Semaphore
+ {".withPermit(\n semaphore,\n "}
+ TaskQueue
+ {".complete(emails, handleSendEmail),\n ).pipe(\n "}
+ Effect
+ {".forkScoped, "}
+ {"// each worker is its own fiber\n"}
+ {" "}
+ Effect
+ {".repeat("}
+ Schedule
+ {".forever), "}
+ {"// ...that keeps pulling work\n"}
+ {" );\n});"}
+ ,
+ // 05 schedule
+
+ {"// A durable task, materialized for every cron tick.\n"}
+ const schedule = Scheduler
+ {".make({\n name: "}
+ "nightly-report"
+ {",\n cron: "}
+ Cron
+ {".parseUnsafe("}
+ "0 2 * * *"
+ {", "}
+ "UTC"
+ {
+ "),\n queue: reportQueue,\n payload: (tick) => ({ scheduledAt: tick.scheduledAt.toISOString() }),\n missed: { _tag: "
+ }
+ "coalesce"
+ {" }, "}
+ {"// skip | coalesce | bounded backfill\n"}
+ {"});\n\n"}
+ const worker = Worker
+ {".make(reportQueue, ({ payload }) =>\n "}
+ Effect
+ {".log("}
+ {/* biome-ignore lint/suspicious/noTemplateCurlyInString: displayed as literal code */}
+ {"`Building report for ${payload.scheduledAt}`"}
+ {"),\n);"}
+ ,
+];
+
+export const semTabLabels = ["bounded", "rate limit", "fan-out"];
+
+export const semSnippets: ReadonlyArray = [
+ // bounded
+
+ {"// At most 5 tasks in flight, across any number of runners.\n"}
+ const semaphore = yield
+ {"* "}
+ Semaphore
+ {".make("}
+ 5
+ {");\n\n"}
+ yield
+ {"* "}
+ Semaphore
+ {".withPermit(\n semaphore,\n "}
+ TaskQueue
+ {".complete(emails, handle),\n).pipe("}
+ Effect
+ {".forkScoped, "}
+ Effect
+ {".repeat("}
+ Schedule
+ {".forever));"}
+ ,
+ // rate limit
+
+ {"// A rate limit is just a Semaphore + a Schedule.\n"}
+ const permits = yield
+ {"* "}
+ Semaphore
+ {".make("}
+ 1
+ {");\n\n"}
+ yield
+ {"* "}
+ Semaphore
+ {".withPermit(\n permits,\n "}
+ TaskQueue
+ {".complete(emails, handle),\n).pipe(\n "}
+ Effect
+ {".repeat("}
+ Schedule
+ {".spaced("}
+ "100 millis"
+ {")), "}
+ {"// ≤ 10 tasks/s"}
+ {"\n);"}
+ ,
+ // fan-out
+
+ {"// Fan out: more fibers — or more processes. Same worker.\n"}
+ const worker = TaskQueue
+ {".complete(emails, handle).pipe(\n "}
+ Effect
+ {".repeat("}
+ Schedule
+ {".forever),\n);\n\n"}
+ yield
+ {"* "}
+ Effect
+ {".all(\n "}
+ Array
+ {".from({ length: "}
+ 6
+ {" }, () => worker),\n { concurrency: "}
+ "unbounded"
+ {" },\n);"}
+ ,
+];
diff --git a/apps/docs/components/landing/concurrency-tabs.tsx b/apps/docs/components/landing/concurrency-tabs.tsx
new file mode 100644
index 0000000..febd463
--- /dev/null
+++ b/apps/docs/components/landing/concurrency-tabs.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { useState } from "react";
+import { semSnippets, semTabLabels } from "./code-snippets";
+import { SemCanvas } from "./sem-canvas";
+
+export function ConcurrencyTabs() {
+ const [tab, setTab] = useState(0);
+
+ return (
+ <>
+
+
03 — Concurrency
+
+ Bring your own concurrency.
+
+
+ No builtin concurrency knobs, rate limiting or backpressure — it
+ doesn't need them. complete {" "}
+ does exactly one task; Semaphore, Schedule and fibers decide how many
+ run at once. None of it is our invention. All of it composes.
+
+
+ {semTabLabels.map((label, i) => (
+ setTab(i)}
+ >
+ {label}
+
+ ))}
+
+
+ {semSnippets[tab]}
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/docs/components/landing/install-command.tsx b/apps/docs/components/landing/install-command.tsx
new file mode 100644
index 0000000..486496b
--- /dev/null
+++ b/apps/docs/components/landing/install-command.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+
+const FULL_COMMAND =
+ "npm install @effectmq/core effect@4.0.0-beta.107 @effect/platform-node@4.0.0-beta.107";
+
+export function InstallCommand() {
+ const [copied, setCopied] = useState(false);
+ const timer = useRef | undefined>(undefined);
+
+ useEffect(() => () => clearTimeout(timer.current), []);
+
+ return (
+
+ $
+
+ npm install @effectmq/core
+
+ {
+ navigator.clipboard.writeText(FULL_COMMAND);
+ setCopied(true);
+ clearTimeout(timer.current);
+ timer.current = setTimeout(() => setCopied(false), 1600);
+ }}
+ >
+ {copied ? "copied!" : "copy"}
+
+
+ );
+}
diff --git a/apps/docs/components/landing/lifecycle-canvas.tsx b/apps/docs/components/landing/lifecycle-canvas.tsx
new file mode 100644
index 0000000..651e7f8
--- /dev/null
+++ b/apps/docs/components/landing/lifecycle-canvas.tsx
@@ -0,0 +1,368 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+
+interface Dot {
+ node: string;
+ edge: Edge | null;
+ t: number;
+ hold: number;
+ attempt: number;
+ alpha: number;
+ delayed: boolean;
+ x?: number;
+ y?: number;
+}
+
+interface Edge {
+ a: string;
+ b: string;
+ bow: number;
+ label: string;
+ warn?: boolean;
+}
+
+const accentColor = () =>
+ getComputedStyle(document.documentElement)
+ .getPropertyValue("--accent")
+ .trim() || "#C6F94F";
+
+/**
+ * Constellation state-machine animation: tasks travel the real
+ * storage-protocol lists (offer → scheduled/queue → worker → outcome).
+ */
+export function LifecycleCanvas() {
+ const canvasRef = useRef(null);
+
+ useEffect(() => {
+ const cv = canvasRef.current;
+ if (!cv) return;
+ const ctx = cv.getContext("2d");
+ if (!ctx) return;
+
+ const WORKERS = ["worker1", "worker2", "worker3"];
+ const NODES: Record<
+ string,
+ { x: number; y: number; label: string; flash: number }
+ > = {
+ offer: { x: 0.06, y: 0.5, label: "offer()", flash: 0 },
+ scheduled: { x: 0.36, y: 0.12, label: "scheduled", flash: 0 },
+ waiting: { x: 0.34, y: 0.64, label: "queue", flash: 0 },
+ worker1: { x: 0.62, y: 0.24, label: "worker · 1", flash: 0 },
+ worker2: { x: 0.66, y: 0.52, label: "worker · 2", flash: 0 },
+ worker3: { x: 0.6, y: 0.8, label: "worker · 3", flash: 0 },
+ success: { x: 0.9, y: 0.3, label: "success", flash: 0 },
+ failed: { x: 0.9, y: 0.74, label: "failed", flash: 0 },
+ };
+ const EDGES: Array = [
+ { a: "offer", b: "waiting", bow: 0, label: "offer" },
+ { a: "offer", b: "scheduled", bow: 0, label: "delay" },
+ { a: "scheduled", b: "waiting", bow: 0, label: "due" },
+ { a: "waiting", b: "worker1", bow: 0, label: "lease" },
+ { a: "waiting", b: "worker2", bow: 0, label: "" },
+ { a: "waiting", b: "worker3", bow: 0, label: "" },
+ { a: "worker1", b: "success", bow: 0, label: "" },
+ { a: "worker2", b: "success", bow: 0, label: "succeed" },
+ { a: "worker3", b: "success", bow: 0, label: "" },
+ { a: "worker1", b: "failed", bow: 0, label: "" },
+ { a: "worker2", b: "failed", bow: 0, label: "exhausted" },
+ { a: "worker3", b: "failed", bow: 0, label: "" },
+ { a: "worker1", b: "scheduled", bow: 0, label: "retry", warn: true },
+ { a: "worker2", b: "scheduled", bow: 0, label: "", warn: true },
+ { a: "worker3", b: "scheduled", bow: 0, label: "", warn: true },
+ ];
+ const edgeOf = (a: string, b: string) =>
+ EDGES.find((e) => e.a === a && e.b === b) ?? null;
+ const dots: Array = [];
+ const waitQ: Array = [];
+ const stars = Array.from({ length: 80 }, () => ({
+ x: Math.random(),
+ y: Math.random(),
+ r: Math.random() * 1.1 + 0.3,
+ ph: Math.random() * 6.28,
+ tw: 0.0006 + Math.random() * 0.0012,
+ }));
+ let last = performance.now();
+ let acc = 900;
+ let nSuccess = 0;
+ let nFailed = 0;
+ let nRetries = 0;
+ let raf = 0;
+
+ const step = (now: number) => {
+ raf = requestAnimationFrame(step);
+ const dt = Math.min(64, now - last);
+ last = now;
+ const W = cv.clientWidth;
+ const H = cv.clientHeight;
+ if (!W) return;
+ const dpr = window.devicePixelRatio || 1;
+ if (cv.width !== Math.round(W * dpr)) {
+ cv.width = Math.round(W * dpr);
+ cv.height = Math.round(H * dpr);
+ }
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+ ctx.clearRect(0, 0, W, H);
+ const AC = accentColor();
+ const P = (n: string): [number, number] => [
+ NODES[n].x * W,
+ NODES[n].y * H,
+ ];
+ const bez = (e: Edge, t: number): [number, number] => {
+ const [x0, y0] = P(e.a);
+ const [x1, y1] = P(e.b);
+ const dx = x1 - x0;
+ const dy = y1 - y0;
+ const len = Math.hypot(dx, dy) || 1;
+ const cx = (x0 + x1) / 2 + (-dy / len) * e.bow * W;
+ const cyy = (y0 + y1) / 2 + (dx / len) * e.bow * W;
+ const v = 1 - t;
+ return [
+ v * v * x0 + 2 * v * t * cx + t * t * x1,
+ v * v * y0 + 2 * v * t * cyy + t * t * y1,
+ ];
+ };
+ ctx.font = "10px 'JetBrains Mono', monospace";
+ ctx.textAlign = "center";
+ ctx.lineWidth = 1;
+ // background starfield (twinkling)
+ for (const s of stars) {
+ ctx.globalAlpha =
+ 0.14 + 0.16 * (0.5 + 0.5 * Math.sin(now * s.tw + s.ph));
+ ctx.fillStyle = "#EDEDF2";
+ ctx.beginPath();
+ ctx.arc(s.x * W, s.y * H, s.r, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ ctx.globalAlpha = 1;
+ // constellation lines: straight, dashed, stopping short of each star
+ for (const e of EDGES) {
+ const col = e.warn ? "rgba(255,138,92,0.32)" : "rgba(237,237,242,0.18)";
+ ctx.strokeStyle = col;
+ ctx.setLineDash([4, 5]);
+ ctx.beginPath();
+ const [sx, sy] = bez(e, 0.06);
+ ctx.moveTo(sx, sy);
+ const [ex, ey] = bez(e, 0.94);
+ ctx.lineTo(ex, ey);
+ ctx.stroke();
+ ctx.setLineDash([]);
+ // small directional tick at 0.85
+ const [ax, ay] = bez(e, 0.82);
+ const [bx2, by2] = bez(e, 0.87);
+ const ang = Math.atan2(by2 - ay, bx2 - ax);
+ ctx.fillStyle = col;
+ ctx.beginPath();
+ ctx.moveTo(bx2, by2);
+ ctx.lineTo(
+ bx2 - 6 * Math.cos(ang - 0.5),
+ by2 - 6 * Math.sin(ang - 0.5),
+ );
+ ctx.lineTo(
+ bx2 - 6 * Math.cos(ang + 0.5),
+ by2 - 6 * Math.sin(ang + 0.5),
+ );
+ ctx.fill();
+ // label
+ const [lx, ly] = bez(e, 0.5);
+ ctx.fillStyle = e.warn
+ ? "rgba(255,138,92,0.75)"
+ : "rgba(139,139,152,0.7)";
+ ctx.fillText(e.label, lx, ly - 7);
+ }
+ // spawn
+ acc += dt;
+ if (acc > 900 && dots.length < 24) {
+ acc = 0;
+ NODES.offer.flash = 200;
+ dots.push({
+ node: "offer",
+ edge: null,
+ t: 0,
+ hold: 220,
+ attempt: 0,
+ alpha: 1,
+ delayed: Math.random() < 0.2,
+ });
+ }
+ // waiting -> workers dispatch (one fenced attempt per worker)
+ for (const w of WORKERS) {
+ if (!waitQ.length) break;
+ const busy = dots.some(
+ (d) => (d.node === w && !d.edge) || (d.edge && d.edge.b === w),
+ );
+ if (!busy) {
+ const d = waitQ.shift();
+ if (d) {
+ d.edge = edgeOf("waiting", w);
+ d.t = 0;
+ }
+ }
+ }
+ // step dots
+ for (let i = dots.length - 1; i >= 0; i--) {
+ const d = dots[i];
+ if (d.edge) {
+ d.t += dt / (d.edge.warn ? 950 : 780);
+ const [x, y] = bez(d.edge, Math.min(1, d.t));
+ d.x = x;
+ d.y = y;
+ if (d.t >= 1) {
+ d.node = d.edge.b;
+ d.edge = null;
+ NODES[d.node].flash = 220;
+ if (d.node === "waiting") {
+ waitQ.push(d);
+ d.hold = -1;
+ } else if (d.node === "scheduled")
+ d.hold = 900 + Math.random() * 900;
+ else if (d.node.indexOf("worker") === 0)
+ d.hold = 650 + Math.random() * 950;
+ else {
+ d.hold = 300;
+ if (d.node === "success") nSuccess++;
+ else nFailed++;
+ }
+ }
+ } else if (d.hold >= 0) {
+ d.hold -= dt;
+ if (d.hold < 0) {
+ if (d.node === "offer") {
+ d.edge = edgeOf("offer", d.delayed ? "scheduled" : "waiting");
+ d.t = 0;
+ } else if (d.node === "scheduled") {
+ d.edge = edgeOf("scheduled", "waiting");
+ d.t = 0;
+ } else if (d.node.indexOf("worker") === 0) {
+ const r = Math.random();
+ if (d.attempt === 0 && r < 0.3) {
+ d.attempt = 1;
+ d.edge = edgeOf(d.node, "scheduled");
+ nRetries++;
+ } else if (r < (d.attempt === 0 ? 0.36 : 0.15))
+ d.edge = edgeOf(d.node, "failed");
+ else d.edge = edgeOf(d.node, "success");
+ d.t = 0;
+ } else if (d.node === "success" || d.node === "failed") {
+ d.alpha -= dt * 0.002;
+ d.hold = 0;
+ if (d.alpha <= 0) {
+ dots.splice(i, 1);
+ }
+ }
+ }
+ }
+ }
+ // resting dots orbit their star like satellites
+ const rest: Record = {};
+ for (const d of dots) {
+ if (d.edge) continue;
+ const n = d.node;
+ rest[n] = rest[n] || 0;
+ const idx = rest[n]++;
+ const [nx, ny] = P(n);
+ const ring = 17 + 7 * Math.floor(idx / 7);
+ const ang = idx * 2.4 + now * 0.00035;
+ d.x = nx + ring * Math.cos(ang);
+ d.y = ny + ring * Math.sin(ang);
+ }
+ // stars (nodes)
+ for (const k in NODES) {
+ const n = NODES[k];
+ n.flash = Math.max(0, n.flash - dt);
+ const [x, y] = P(k);
+ const isWorker = k.indexOf("worker") === 0;
+ const base =
+ k === "success"
+ ? AC
+ : k === "failed"
+ ? "#FF8A5C"
+ : k === "scheduled"
+ ? "#B8A7FF"
+ : "#EDEDF2";
+ const col = n.flash > 0 ? AC : base;
+ // sparkle cross
+ ctx.strokeStyle = col;
+ ctx.globalAlpha = 0.35;
+ ctx.beginPath();
+ ctx.moveTo(x - 9, y);
+ ctx.lineTo(x + 9, y);
+ ctx.moveTo(x, y - 9);
+ ctx.lineTo(x, y + 9);
+ ctx.stroke();
+ ctx.globalAlpha = 1;
+ // core
+ ctx.shadowColor = col;
+ ctx.shadowBlur = 14;
+ ctx.fillStyle = col;
+ ctx.beginPath();
+ ctx.arc(x, y, isWorker ? 4.5 : 3.5, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.shadowBlur = 0;
+ // arrival pulse ring
+ if (n.flash > 0) {
+ const u = 1 - n.flash / 220;
+ ctx.strokeStyle = AC;
+ ctx.globalAlpha = 0.5 * (1 - u);
+ ctx.beginPath();
+ ctx.arc(x, y, 8 + u * 16, 0, Math.PI * 2);
+ ctx.stroke();
+ ctx.globalAlpha = 1;
+ }
+ // label
+ ctx.font = "11px 'JetBrains Mono', monospace";
+ ctx.fillStyle = n.flash > 0 ? AC : "rgba(185,185,196,0.85)";
+ const count =
+ k === "success"
+ ? ` · ${nSuccess}`
+ : k === "failed"
+ ? ` · ${nFailed}`
+ : "";
+ const ly =
+ k === "scheduled" || k === "success" || k === "worker1"
+ ? y - 22
+ : y + 32;
+ ctx.fillText(n.label + count, x, ly);
+ ctx.font = "10px 'JetBrains Mono', monospace";
+ }
+ // dots on top
+ for (const d of dots) {
+ const inActive = d.node.indexOf("worker") === 0 && !d.edge;
+ const col =
+ d.edge?.warn || (d.attempt > 0 && d.node === "scheduled" && !d.edge)
+ ? "#FF8A5C"
+ : d.node === "success" || d.edge?.b === "success"
+ ? AC
+ : d.node === "failed" || d.edge?.b === "failed"
+ ? "#FF8A5C"
+ : inActive
+ ? "#EDEDF2"
+ : "#8A8A96";
+ ctx.globalAlpha = Math.max(0, d.alpha);
+ ctx.shadowColor = col;
+ ctx.shadowBlur = inActive || d.node === "success" ? 9 : 0;
+ ctx.fillStyle = col;
+ ctx.beginPath();
+ ctx.arc(d.x ?? 0, d.y ?? 0, 4.5, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.shadowBlur = 0;
+ if (inActive) {
+ ctx.strokeStyle = "rgba(237,237,242,0.35)";
+ ctx.beginPath();
+ ctx.arc(d.x ?? 0, d.y ?? 0, 8, 0, Math.PI * 2);
+ ctx.stroke();
+ }
+ ctx.globalAlpha = 1;
+ }
+ // counters
+ ctx.textAlign = "right";
+ ctx.fillStyle = "rgba(139,139,152,0.9)";
+ ctx.fillText(`retries · ${nRetries}`, W - 16, 22);
+ ctx.textAlign = "center";
+ };
+ raf = requestAnimationFrame(step);
+ return () => cancelAnimationFrame(raf);
+ }, []);
+
+ return ;
+}
diff --git a/apps/docs/components/landing/sem-canvas.tsx b/apps/docs/components/landing/sem-canvas.tsx
new file mode 100644
index 0000000..14b45c9
--- /dev/null
+++ b/apps/docs/components/landing/sem-canvas.tsx
@@ -0,0 +1,381 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+
+interface Dot {
+ node: string;
+ edge: Edge | null;
+ t: number;
+ hold: number;
+ alpha: number;
+ x?: number;
+ y?: number;
+}
+
+interface Edge {
+ a: string;
+ b: string;
+ label: string;
+}
+
+const accentColor = () =>
+ getComputedStyle(document.documentElement)
+ .getPropertyValue("--accent")
+ .trim() || "#C6F94F";
+
+/**
+ * Semaphore constellation: runners offer into a queue whose head is pulled
+ * through a column of permit stars. Three modes — bounded (5 permits),
+ * rate limit (1 permit on a clock), fan-out (6 permits).
+ */
+export function SemCanvas({ mode }: { readonly mode: number }) {
+ const canvasRef = useRef(null);
+ const modeRef = useRef(mode);
+ modeRef.current = mode;
+
+ useEffect(() => {
+ const cv = canvasRef.current;
+ if (!cv) return;
+ const ctx = cv.getContext("2d");
+ if (!ctx) return;
+
+ let NODES: Record<
+ string,
+ { x: number; y: number; label: string; flash: number }
+ > = {};
+ let EDGES: Array = [];
+ let PERMITS: Array = [];
+ let currentMode = -1;
+ let cooldown = 0;
+ let rateMs = 0;
+ let gateLabel = "";
+ let dots: Array = [];
+ let waitQ: Array = [];
+ let doneCount = 0;
+ const edgeOf = (a: string, b: string) =>
+ EDGES.find((e) => e.a === a && e.b === b) ?? null;
+ const build = (m: number) => {
+ currentMode = m;
+ dots = [];
+ waitQ = [];
+ doneCount = 0;
+ cooldown = 0;
+ const n = m === 0 ? 5 : m === 1 ? 1 : 6;
+ rateMs = m === 1 ? 620 : 0;
+ gateLabel =
+ m === 0
+ ? "Semaphore.make(5)"
+ : m === 1
+ ? 'Schedule.spaced("100 millis")'
+ : "6 fibers · Effect.all";
+ NODES = {
+ runnerA: { x: 0.09, y: 0.28, label: "runner A", flash: 0 },
+ runnerB: { x: 0.09, y: 0.72, label: "runner B", flash: 0 },
+ queue: { x: 0.38, y: 0.5, label: "queue", flash: 0 },
+ done: { x: 0.92, y: 0.5, label: "done", flash: 0 },
+ };
+ PERMITS = [];
+ for (let j = 0; j < n; j++) {
+ const k = `permit${j}`;
+ PERMITS.push(k);
+ NODES[k] = {
+ x: 0.68,
+ y: n === 1 ? 0.5 : 0.14 + (0.72 * j) / (n - 1),
+ label: "",
+ flash: 0,
+ };
+ }
+ EDGES = [
+ { a: "runnerA", b: "queue", label: "offer()" },
+ { a: "runnerB", b: "queue", label: "" },
+ ];
+ const midIdx = Math.floor(n / 2);
+ PERMITS.forEach((k, j) => {
+ EDGES.push({
+ a: "queue",
+ b: k,
+ label: j === midIdx ? (m === 2 ? "complete" : "withPermit") : "",
+ });
+ EDGES.push({ a: k, b: "done", label: "" });
+ });
+ };
+ build(modeRef.current);
+ const stars = Array.from({ length: 70 }, () => ({
+ x: Math.random(),
+ y: Math.random(),
+ r: Math.random() * 1.1 + 0.3,
+ ph: Math.random() * 6.28,
+ tw: 0.0006 + Math.random() * 0.0012,
+ }));
+ const runners = [
+ { node: "runnerA", acc: 0, next: 700 },
+ { node: "runnerB", acc: 420, next: 1100 },
+ ];
+ let last = performance.now();
+ let raf = 0;
+
+ const step = (now: number) => {
+ raf = requestAnimationFrame(step);
+ if (modeRef.current !== currentMode) build(modeRef.current);
+ const dt = Math.min(64, now - last);
+ last = now;
+ const W = cv.clientWidth;
+ const H = cv.clientHeight;
+ if (!W) return;
+ const dpr = window.devicePixelRatio || 1;
+ if (cv.width !== Math.round(W * dpr)) {
+ cv.width = Math.round(W * dpr);
+ cv.height = Math.round(H * dpr);
+ }
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+ ctx.clearRect(0, 0, W, H);
+ const AC = accentColor();
+ const P = (n: string): [number, number] => [
+ NODES[n].x * W,
+ NODES[n].y * H,
+ ];
+ const lin = (e: Edge, t: number): [number, number] => {
+ const [x0, y0] = P(e.a);
+ const [x1, y1] = P(e.b);
+ return [x0 + (x1 - x0) * t, y0 + (y1 - y0) * t];
+ };
+ ctx.font = "10px 'JetBrains Mono', monospace";
+ ctx.textAlign = "center";
+ ctx.lineWidth = 1;
+ // starfield
+ for (const s of stars) {
+ ctx.globalAlpha =
+ 0.14 + 0.16 * (0.5 + 0.5 * Math.sin(now * s.tw + s.ph));
+ ctx.fillStyle = "#EDEDF2";
+ ctx.beginPath();
+ ctx.arc(s.x * W, s.y * H, s.r, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ ctx.globalAlpha = 1;
+ // dashed constellation lines
+ for (const e of EDGES) {
+ const col = "rgba(237,237,242,0.18)";
+ ctx.strokeStyle = col;
+ ctx.setLineDash([4, 5]);
+ ctx.beginPath();
+ const [sx, sy] = lin(e, 0.08);
+ ctx.moveTo(sx, sy);
+ const [ex, ey] = lin(e, 0.92);
+ ctx.lineTo(ex, ey);
+ ctx.stroke();
+ ctx.setLineDash([]);
+ const [ax, ay] = lin(e, 0.8);
+ const [bx2, by2] = lin(e, 0.86);
+ const ang = Math.atan2(by2 - ay, bx2 - ax);
+ ctx.fillStyle = col;
+ ctx.beginPath();
+ ctx.moveTo(bx2, by2);
+ ctx.lineTo(
+ bx2 - 6 * Math.cos(ang - 0.5),
+ by2 - 6 * Math.sin(ang - 0.5),
+ );
+ ctx.lineTo(
+ bx2 - 6 * Math.cos(ang + 0.5),
+ by2 - 6 * Math.sin(ang + 0.5),
+ );
+ ctx.fill();
+ if (e.label) {
+ const [lx, ly] = lin(e, 0.5);
+ ctx.fillStyle = "rgba(139,139,152,0.7)";
+ ctx.fillText(e.label, lx, ly - 7);
+ }
+ }
+ // runners emit
+ const backlog =
+ waitQ.length +
+ dots.filter((d) => d.edge && d.edge.b === "queue").length;
+ for (const r of runners) {
+ r.acc += dt;
+ if (r.acc > r.next && backlog < 9) {
+ r.acc = 0;
+ r.next = 600 + Math.random() * 900;
+ NODES[r.node].flash = 220;
+ dots.push({
+ node: r.node,
+ edge: edgeOf(r.node, "queue"),
+ t: 0,
+ hold: 0,
+ alpha: 1,
+ });
+ }
+ }
+ // free permits pull from queue head (rate-limited mode releases on a clock)
+ const isBusy = (pk: string) =>
+ dots.some(
+ (d) => (d.node === pk && !d.edge) || (d.edge && d.edge.b === pk),
+ );
+ if (rateMs) {
+ cooldown -= dt;
+ if (cooldown <= 0 && waitQ.length) {
+ const pk = PERMITS.find((k) => !isBusy(k));
+ if (pk) {
+ const d = waitQ.shift();
+ if (d) {
+ d.edge = edgeOf("queue", pk);
+ d.t = 0;
+ cooldown = rateMs;
+ NODES[pk].flash = 220;
+ }
+ }
+ }
+ } else {
+ for (const pk of PERMITS) {
+ if (!waitQ.length) break;
+ if (!isBusy(pk)) {
+ const d = waitQ.shift();
+ if (d) {
+ d.edge = edgeOf("queue", pk);
+ d.t = 0;
+ }
+ }
+ }
+ }
+ // step dots
+ for (let i = dots.length - 1; i >= 0; i--) {
+ const d = dots[i];
+ if (d.edge) {
+ d.t += dt / 720;
+ const [x, y] = lin(d.edge, Math.min(1, d.t));
+ d.x = x;
+ d.y = y;
+ if (d.t >= 1) {
+ d.node = d.edge.b;
+ d.edge = null;
+ NODES[d.node].flash = 220;
+ if (d.node === "queue") {
+ waitQ.push(d);
+ d.hold = -1;
+ } else if (d.node === "done") {
+ doneCount++;
+ d.hold = 0;
+ } else
+ d.hold =
+ currentMode === 1
+ ? 320
+ : currentMode === 2
+ ? 400 + Math.random() * 800
+ : 600 + Math.random() * 1400;
+ }
+ } else if (d.node === "done") {
+ d.alpha -= dt * 0.002;
+ if (d.alpha <= 0) {
+ dots.splice(i, 1);
+ }
+ } else if (d.hold >= 0) {
+ d.hold -= dt;
+ if (d.hold < 0 && d.node.indexOf("permit") === 0) {
+ d.edge = edgeOf(d.node, "done");
+ d.t = 0;
+ }
+ }
+ }
+ // resting dots orbit their star
+ const rest: Record = {};
+ for (const d of dots) {
+ if (d.edge) continue;
+ rest[d.node] = rest[d.node] || 0;
+ const idx = rest[d.node]++;
+ const [nx, ny] = P(d.node);
+ const ring = 15 + 7 * Math.floor(idx / 7);
+ const ang = idx * 2.4 + now * 0.00035;
+ d.x = nx + ring * Math.cos(ang);
+ d.y = ny + ring * Math.sin(ang);
+ }
+ // stars
+ for (const k in NODES) {
+ const n = NODES[k];
+ n.flash = Math.max(0, n.flash - dt);
+ const [x, y] = P(k);
+ const isPermit = k.indexOf("permit") === 0;
+ const busy = isPermit && dots.some((d) => d.node === k && !d.edge);
+ const base =
+ k === "done"
+ ? AC
+ : busy
+ ? "#EDEDF2"
+ : isPermit
+ ? "rgba(184,167,255,0.9)"
+ : "#EDEDF2";
+ const col = n.flash > 0 ? AC : base;
+ ctx.strokeStyle = col;
+ ctx.globalAlpha = 0.35;
+ ctx.beginPath();
+ ctx.moveTo(x - 8, y);
+ ctx.lineTo(x + 8, y);
+ ctx.moveTo(x, y - 8);
+ ctx.lineTo(x, y + 8);
+ ctx.stroke();
+ ctx.globalAlpha = 1;
+ ctx.shadowColor = col;
+ ctx.shadowBlur = 14;
+ ctx.fillStyle = col;
+ ctx.beginPath();
+ ctx.arc(x, y, isPermit ? 3 : 3.5, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.shadowBlur = 0;
+ if (busy) {
+ ctx.strokeStyle = AC;
+ ctx.globalAlpha = 0.6;
+ ctx.beginPath();
+ ctx.arc(x, y, 8, 0, Math.PI * 2);
+ ctx.stroke();
+ ctx.globalAlpha = 1;
+ }
+ if (n.flash > 0) {
+ const u = 1 - n.flash / 220;
+ ctx.strokeStyle = AC;
+ ctx.globalAlpha = 0.5 * (1 - u);
+ ctx.beginPath();
+ ctx.arc(x, y, 8 + u * 16, 0, Math.PI * 2);
+ ctx.stroke();
+ ctx.globalAlpha = 1;
+ }
+ if (n.label) {
+ ctx.font = "11px 'JetBrains Mono', monospace";
+ ctx.fillStyle = n.flash > 0 ? AC : "rgba(185,185,196,0.85)";
+ const count = k === "done" ? ` · ${doneCount}` : "";
+ ctx.fillText(n.label + count, x, y + 30);
+ ctx.font = "10px 'JetBrains Mono', monospace";
+ }
+ }
+ // group label for the permit column
+ ctx.font = "11px 'JetBrains Mono', monospace";
+ ctx.fillStyle = "rgba(185,185,196,0.85)";
+ const p0 = NODES[PERMITS[0]];
+ ctx.fillText(
+ gateLabel,
+ p0.x * W,
+ p0.y * H - (PERMITS.length === 1 ? 28 : 22),
+ );
+ ctx.font = "10px 'JetBrains Mono', monospace";
+ // traveling + resting dots
+ for (const d of dots) {
+ const atPermit = d.node.indexOf("permit") === 0 && !d.edge;
+ const col =
+ d.node === "done" || d.edge?.b === "done"
+ ? AC
+ : atPermit
+ ? "#EDEDF2"
+ : "#8A8A96";
+ ctx.globalAlpha = Math.max(0, d.alpha);
+ ctx.shadowColor = col;
+ ctx.shadowBlur = atPermit || d.node === "done" ? 9 : 0;
+ ctx.fillStyle = col;
+ ctx.beginPath();
+ ctx.arc(d.x ?? 0, d.y ?? 0, 4.5, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.shadowBlur = 0;
+ ctx.globalAlpha = 1;
+ }
+ };
+ raf = requestAnimationFrame(step);
+ return () => cancelAnimationFrame(raf);
+ }, []);
+
+ return ;
+}
diff --git a/apps/docs/lib/layout.shared.tsx b/apps/docs/lib/layout.shared.tsx
index dd933e1..559998e 100644
--- a/apps/docs/lib/layout.shared.tsx
+++ b/apps/docs/lib/layout.shared.tsx
@@ -3,7 +3,14 @@ import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
export function baseOptions(): BaseLayoutProps {
return {
nav: {
- title: "effectmq",
+ title: (
+
+ effect
+ mq
+
+ ),
+ url: "/",
},
+ githubUrl: "https://github.com/julia-script/effectmq",
};
}