跳到正文
misoto22 design

模板 · Content

Documentation

A reference page: a sidebar of sections, an article column with a props table and a keyboard note, and a contents rail beside it.

Three columns at once. A sidebar and a contents rail leave the middle narrower than any other template’s, which is the width at which a table, a long code identifier and a callout stop fitting and have to decide which of them scrolls.

queue.enqueue()

stableasync

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.

queueswritesince 1.0

Signature

await queue.enqueue(name: string, payload: Json, options?: EnqueueOptions): Promise<JobId>

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.

Options

EnqueueOptions
OptionTypeDefaultDescription
delaynumber0Milliseconds to hold the job before any worker may claim it.
attemptsnumber3Total tries, not retries. Setting 1 disables retrying.
backoff'fixed' | 'exponential''exponential'How the delay grows between attempts. Exponential starts at one second.
dedupeKeystringTwo jobs with the same key inside the window collapse into one.
prioritynumber0Higher is claimed first. Equal priorities are FIFO.

Ordering guarantees

Jobs of equal priority are claimed in the order they were accepted by the broker, which is not necessarily the order you called enqueue in — two calls in the same tick can reach different brokers. If the order matters, it belongs in the payload.

What happens after

  1. AcceptedThe broker has it; enqueue resolves with the id.
  2. ClaimedA worker leases it for the visibility timeout.
  3. RunningYour handler. Anything it throws counts as an attempt.
  4. SettledCompleted, or moved to the dead letter queue.

When it throws

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.

In the playground below, Enter runs the snippet and Esc restores the original.

由这些组件搭成

NavItemBreadcrumbTableKbdAlertStepsTagBadgeSeparatorLinkArrowButton

这里没有任何东西是为模板单独写的样式。只有这样它才能随着系统变化而保持诚实——带自己 CSS 的模板不再是对组件的检验,只是一张截图。

源码

TSX
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>
  )
}