import {
Alert,
Badge,
Breadcrumb,
Button,
Kbd,
LinkArrow,
NavItem,
Separator,
Steps,
TBody,
TD,
TH,
THead,
TR,
Table,
Tag,
} from '@misoto22/design'
import { BookOpen, Layers, Package, Terminal } from 'lucide-react'
const SIDEBAR = [
{ href: '#start', label: 'Getting started', icon: BookOpen },
{ href: '#cli', label: 'CLI', icon: Terminal },
{ href: '#queues', label: 'Queues', icon: Layers, active: true },
{ href: '#adapters', label: 'Adapters', icon: Package },
]
const CONTENTS = [
{ id: 'signature', text: 'Signature', level: 2 },
{ id: 'options', text: 'Options', level: 2 },
{ id: 'ordering', text: 'Ordering guarantees', level: 3 },
{ id: 'lifecycle', text: 'What happens after', level: 2 },
{ id: 'failure', text: 'When it throws', level: 2 },
]
const OPTIONS = [
{
name: 'delay',
type: 'number',
fallback: '0',
note: 'Milliseconds to hold the job before any worker may claim it.',
},
{
name: 'attempts',
type: 'number',
fallback: '3',
note: 'Total tries, not retries. Setting 1 disables retrying.',
},
{
name: 'backoff',
type: "'fixed' | 'exponential'",
fallback: "'exponential'",
note: 'How the delay grows between attempts. Exponential starts at one second.',
},
{
name: 'dedupeKey',
type: 'string',
fallback: '—',
note: 'Two jobs with the same key inside the window collapse into one.',
},
{
name: 'priority',
type: 'number',
fallback: '0',
note: 'Higher is claimed first. Equal priorities are FIFO.',
},
]
/**
* A reference page, assembled from the set.
*
* The three-column documentation layout, and the reason it earns a template of
* its own: it is the narrowest middle column in this whole set. A sidebar on
* one side and a contents rail on the other leave the article with less width
* than a blog post, a settings form or a detail page — and that is precisely
* the width at which the blocks a reference page is MADE of stop fitting.
*
* Three of them, and each has a different answer:
*
* the table scrolls on its own axis. `Table` wraps itself in a focusable
* scrolling region for exactly this, so the page never scrolls
* sideways as a whole while a five-column API table does.
* the code wraps rather than scrolls, because a signature broken across
* two lines is still readable and a signature you have to drag
* is not.
* the callout gets the full column width, since it is prose and the measure
* is already the constraint.
*
* The contents rail is `sticky`, one step of indent for an h3, and marked
* `aria-label="On this page"` so it is a second navigation landmark a reader
* can skip rather than an unnamed list of links.
*
* No state, so no `'use client'`. Every element is from the package.
*/
export function DocsShell() {
return (
<div className="grid min-h-[38rem] grid-cols-1 @3xl:grid-cols-[13rem_minmax(0,1fr)] @5xl:grid-cols-[13rem_minmax(0,1fr)_12rem]">
<aside className="hidden flex-col gap-1 border-e border-(--rule) p-3 @3xl:flex">
<div className="flex items-baseline gap-2 px-3 pb-3 pt-2">
<span className="font-heading text-[15px] text-(--ink)">Ferry</span>
<span className="mono-meta text-(--ink-3-aa)">v2.4</span>
</div>
{SIDEBAR.map((item) => (
<NavItem key={item.href} href={item.href} icon={item.icon} active={item.active}>
{item.label}
</NavItem>
))}
<Separator className="my-3" />
<p className="m-0 px-3 text-[13px] leading-relaxed text-(--ink-3-aa)">
Reference for 2.4. The 1.x pages are still published and marked as such.
</p>
</aside>
<article className="flex min-w-0 flex-col gap-7 px-6 py-8">
<div className="flex flex-col gap-3">
{/* Named, not left on the default: the page this renders inside has
a trail of its own, and two navigation landmarks both called
"Breadcrumb" are indistinguishable by ear. */}
<Breadcrumb
label="Reference"
items={[
{ label: 'Docs', href: '#docs' },
{ label: 'Queues', href: '#queues' },
{ label: 'enqueue' },
]}
/>
<div className="flex flex-wrap items-center gap-2">
<h1 className="m-0 font-heading text-[length:var(--fs-sub)] font-normal text-(--ink)">
queue.enqueue()
</h1>
<Badge tone="outline">stable</Badge>
<Badge>async</Badge>
</div>
<p className="m-0 max-w-(--w-reading) text-[15px] leading-relaxed text-(--ink-2)">
Puts one job on a queue and returns as soon as the broker has it. It does not wait for a
worker, and it is the only call in Ferry that writes.
</p>
<div className="flex flex-wrap gap-1.5">
<Tag>queues</Tag>
<Tag>write</Tag>
<Tag>since 1.0</Tag>
</div>
</div>
<Separator />
<section aria-labelledby="signature" className="flex min-w-0 flex-col gap-3">
<h2
id="signature"
className="m-0 font-heading text-[length:var(--fs-item)] font-normal text-(--ink)"
>
Signature
</h2>
{/* Wraps rather than scrolls. In the narrowest column in the set, a
signature you have to drag sideways is a signature nobody reads. */}
<pre className="m-0 overflow-hidden whitespace-pre-wrap break-words rounded-(--radius) border border-(--rule) bg-(--paper-2) p-4 font-mono text-xs leading-relaxed text-(--ink-2)">
{`await queue.enqueue(name: string, payload: Json, options?: EnqueueOptions): Promise<JobId>`}
</pre>
<p className="m-0 max-w-(--w-reading) text-sm leading-relaxed text-(--ink-2)">
The payload is serialised once, at the call site, so a value that cannot be represented
as JSON fails here rather than inside a worker forty minutes later.
</p>
</section>
<section aria-labelledby="options" className="flex min-w-0 flex-col gap-3">
<h2
id="options"
className="m-0 font-heading text-[length:var(--fs-item)] font-normal text-(--ink)"
>
Options
</h2>
{/* Five columns in the narrowest column of any template here. The
table takes its own scrollbar rather than making the page take
one. */}
<Table caption="EnqueueOptions" borders="grid" density="compact">
<THead>
<TR>
<TH>Option</TH>
<TH>Type</TH>
<TH>Default</TH>
<TH>Description</TH>
</TR>
</THead>
<TBody>
{OPTIONS.map((option) => (
<TR key={option.name}>
<TD className="whitespace-nowrap font-mono text-xs text-(--ink)">
{option.name}
</TD>
<TD className="whitespace-nowrap font-mono text-xs text-(--ink-3-aa)">
{option.type}
</TD>
<TD className="whitespace-nowrap font-mono text-xs text-(--ink-3-aa)">
{option.fallback}
</TD>
<TD className="text-(--ink-2)">{option.note}</TD>
</TR>
))}
</TBody>
</Table>
<h3
id="ordering"
className="m-0 mt-2 font-heading text-[15px] font-normal text-(--ink)"
>
Ordering guarantees
</h3>
<p className="m-0 max-w-(--w-reading) text-sm leading-relaxed text-(--ink-2)">
Jobs of equal priority are claimed in the order they were accepted by the broker, which
is not necessarily the order you called <code className="font-mono text-xs text-(--ink)">enqueue</code> in
— two calls in the same tick can reach different brokers. If the order matters, it
belongs in the payload.
</p>
</section>
<section aria-labelledby="lifecycle" className="flex min-w-0 flex-col gap-3">
<h2
id="lifecycle"
className="m-0 font-heading text-[length:var(--fs-item)] font-normal text-(--ink)"
>
What happens after
</h2>
<Steps
label="The life of a job"
marker="rule"
steps={[
{ title: 'Accepted', note: 'The broker has it; enqueue resolves with the id.' },
{ title: 'Claimed', note: 'A worker leases it for the visibility timeout.' },
{ title: 'Running', note: 'Your handler. Anything it throws counts as an attempt.' },
{ title: 'Settled', note: 'Completed, or moved to the dead letter queue.', current: true },
]}
/>
</section>
<section aria-labelledby="failure" className="flex min-w-0 flex-col gap-3">
<h2
id="failure"
className="m-0 font-heading text-[length:var(--fs-item)] font-normal text-(--ink)"
>
When it throws
</h2>
<Alert tone="warning" title="A rejected enqueue means the job does not exist">
There is no partial state and nothing to clean up. Catch it where you would catch a
failed database write, because that is what it is.
</Alert>
<p className="m-0 max-w-(--w-reading) text-sm leading-relaxed text-(--ink-2)">
In the playground below, <Kbd>⌘</Kbd> <Kbd>Enter</Kbd> runs the snippet and{' '}
<Kbd>Esc</Kbd> restores the original.
</p>
</section>
<Separator />
<nav aria-label="Page order" className="flex flex-wrap items-center justify-between gap-3">
<Button variant="ghost" size="sm">
Queues overview
</Button>
<a
href="#dead-letter"
className="text-sm text-(--ink) underline decoration-(--rule-2) underline-offset-4 transition-colors duration-(--duration-fast) hover:decoration-(--ink)"
>
Dead letter queues
<LinkArrow />
</a>
</nav>
</article>
<nav aria-label="On this page" className="max-@5xl:hidden border-s border-(--rule) p-6">
<div className="sticky top-6 flex flex-col gap-2">
<span className="eyebrow text-(--ink-3-aa)">On this page</span>
{CONTENTS.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
className={`text-[13px] leading-snug text-(--ink-3-aa) transition-colors duration-(--duration-fast) hover:text-(--ink) ${
item.level === 3 ? 'ps-3' : ''
}`}
>
{item.text}
</a>
))}
</div>
</nav>
</div>
)
}