Skip to content

feat: redesign @coji/durably-react — createDurably, fullstack-first, automated doc checks#82

Merged
coji merged 15 commits into
mainfrom
feat/api-redesign
Mar 6, 2026
Merged

feat: redesign @coji/durably-react — createDurably, fullstack-first, automated doc checks#82
coji merged 15 commits into
mainfrom
feat/api-redesign

Conversation

@coji
Copy link
Copy Markdown
Owner

@coji coji commented Mar 6, 2026

Summary

@coji/durably-react の API を全面リデザイン。tRPC/Convex パターンを参考に DX を大幅改善。

API Changes

  • createDurably — サーバーと同名。api オプションでクライアントを区別
    • Per-job hooks: durably.importCsv.useJob(), durably.importCsv.useRun(id)
    • Built-in cross-job hooks: durably.useRuns(), durably.useRunActions()
    • ExtractJobs<T>createDurably<typeof serverDurably>() が動く
  • Root import (@coji/durably-react) → Fullstack hooks(旧 SPA)
  • SPA hooks@coji/durably-react/spa subpath
  • jobs optioncreateDurably() (core) の 1-step 初期化

Documentation Overhaul

  • Website 全面更新: init() 推奨、jobs オプション推奨、createDurably パターン
  • ファイルリネーム: browser.mdspa.md, client.mdfullstack.md
  • サイドバー: Fullstack first、Auth Middleware / deleteRun / subscribe 追加
  • 全ガイド・examples・README・CLAUDE.md・llms.md を新 API に統一

Skills Overhaul

  • scripts/find-stale.sh — 古い API パターンを自動検出するスクリプト追加
  • doc-check: 6-phase オーケストレーター化(検出 → 巡回 → 再生成 → 検証)
  • release-check: 10-phase ワークフロー、同スクリプト共有
  • Why-driven 設計(各ファイルになぜ確認が必要か記載)

Bug Fixes

  • triggerAndWait の interval リーク修正(unmount 時にクリア)
  • 後方互換エイリアス全削除

Test plan

  • pnpm validate passes (22/22 tasks)
  • find-stale.sh reports clean
  • Fullstack example works with SSR
  • Website sidebar links resolve correctly

Closes #81, closes #78

🤖 Generated with Claude Code

…paths

- Root import (`@coji/durably-react`) now exports fullstack hooks (was SPA)
- SPA hooks moved to `@coji/durably-react/spa` subpath
- Rename `createDurablyClient` → `createDurablyHooks`
- Add `jobs` option to `createDurably()` for 1-step initialization
- Rename example dirs: `browser-*` → `spa-*` to match mode names
- Update all documentation, guides, examples, and skills
- Fix DurablyProvider to accept `Durably<any, any>` (was too restrictive)

Closes #81

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
durably-demo Ready Ready Preview Mar 6, 2026 3:20pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR redesigns Durably and durably-react: it adds inline job registration to createDurably (jobs option and overloads), splits React exports into SPA vs Fullstack entrypoints (new /spa export and spa.ts), updates package exports/build, rewrites docs/examples/site navigation, and updates examples/tests to the new API and import paths.

Changes

Cohort / File(s) Summary
Core Durably
packages/durably/src/durably.ts, packages/durably/README.md, packages/durably/docs/llms.md
Add jobs option and overloads to createDurably, register inline when provided, unify event subscription/cleanup, update README and docs to show inline jobs pattern.
durably-react public surface
packages/durably-react/src/index.ts, packages/durably-react/package.json, packages/durably-react/tsup.config.ts
Root now exposes shared types and server/client factory APIs; removed client re-export, changed package exports to introduce ./spa entry.
SPA entrypoint
packages/durably-react/src/spa.ts, packages/durably-react/src/context.tsx
New spa.ts re-exports provider and SPA hooks; loosened context types (AnyDurably) and adjusted provider props.
Fullstack client factory & hooks
packages/durably-react/src/client/create-durably.ts, packages/durably-react/src/client/index.ts, packages/durably-react/src/client/create-job-hooks.ts
Added createDurably client factory (proxy-based) returning per-job typed hooks and built-in cross-job hooks (useRuns, useRunActions); adjusted client index exports.
Client hook logic updates
packages/durably-react/src/client/use-job.ts, packages/durably-react/src/client/use-job-run.ts
Improved triggerAndWait/wait logic with subscriptionRef and waitIntervalRef, auto-resume changes, and cleanup on unmount.
Remove legacy client re-exports
packages/durably-react/src/client.ts
Deleted legacy client re-export module.
Examples: server & fullstack
examples/server-node/lib/durably.ts, examples/fullstack-react-router/app/lib/durably.server.ts, examples/fullstack-react-router/app/lib/durably.ts
Register payload now nests jobs under { jobs: { ... } }; added client createDurably usage in example client file.
Examples: SPA projects
examples/spa-react-router/..., examples/spa-vite-react/..., examples/spa-react-router/package.json, examples/spa-vite-react/package.json
Switch imports to @coji/durably-react/spa, update register/createDurably usages, reorder Tailwind classes (no semantic change), and rename example package names.
React components / runtime usage
examples/*/routes/*, examples/*/src/components/*
Updated use of hook APIs to namespaced durably.<job>.useRun/useJob or durably.useRuns and replaced direct useRuns/useRunActions with SPA/Fullstack-specific imports where applicable.
Docs & website
website/api/*, website/guide/*, website/.vitepress/config.ts, website/public/llms.txt
Comprehensive docs rewrite: Browser/Server → SPA/Fullstack renames, updated examples to new createDurably/jobs or spa imports, sidebar/navigation updates, new create-durably API doc with overloads.
Scripts / doc-check
.claude/skills/doc-check/SKILL.md, .claude/skills/doc-check/scripts/find-stale.sh
Rewrote doc-check workflow into phased Doc Check; added/expanded allowed tools and artifact tiers; added find-stale.sh script to detect stale docs and renamed patterns.
Tests
packages/durably-react/tests/*, packages/durably-react/tests/client/create-durably.test.tsx
Updated tests to use new import paths (../../src/spa), renamed test references to createDurably, and adjusted mocks/examples accordingly.

Sequence Diagram(s)

sequenceDiagram
  participant Dev as Developer
  participant Core as `@coji/durably` (createDurably)
  participant Durably as DurablyInstance
  participant SPA as `@coji/durably-react/spa` (DurablyProvider/hooks)
  participant Full as durably-client (createDurably client factory)
  Dev->>Core: call createDurably({ dialect, jobs? })
  alt jobs provided
    Core->>Core: register(options.jobs)
    Core-->>Durably: return typed instance with jobs
  else no jobs
    Core-->>Durably: return instance (no jobs)
  end
  Dev->>Full: createDurably({ api }) // client factory for fullstack UI
  Full-->>Dev: proxy client with per-job hooks & useRuns
  Dev->>SPA: import { DurablyProvider, useJob } from `@coji/durably-react/spa`
  SPA->>Durably: consume Durably instance (provider) or client hooks
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hop through docs and code with cheer,

Jobs now nest so everything's clear,
/spa and /fullstack find their place,
createDurably sets the base,
Little rabbit dances, exports near. 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR title clearly describes the main change: API redesign of @coji/durably-react with createDurably factory and fullstack-first approach.
Linked Issues check ✅ Passed All key requirements from issue #81 are implemented: jobs option added [packages/durably], createDurably factory created [packages/durably-react], /spa and /fullstack paths established, examples updated.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #81 objectives: API redesign, docs/examples migration, terminology updates (browser→spa, server→fullstack), and new doc-check/release-check tooling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/api-redesign

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Remove duplicate JobHooks interface from create-durably-hooks.ts,
  import from create-job-hooks.ts instead
- Delegate proxy handler to createJobHooks() instead of re-implementing
- Cache job hooks per jobKey to avoid allocations on every property access
- Guard proxy get trap against Symbol keys (React DevTools compatibility)
- Fix incorrect import path in docstring (@coji/durably-react/fullstack
  → @coji/durably-react)
- Remove unnecessary SingleJobHooks alias from index.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…cate subscribe

- Fix triggerAndWait stale closure bug: use ref to track latest
  subscription state instead of capturing React state in closure
- Parallelize autoResume fetches: running and pending status checks
  now run concurrently with Promise.all (halves latency)
- Deduplicate subscribe() event listeners: replace 11 near-identical
  blocks with data-driven loop over event types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.claude/skills/release-check/SKILL.md (1)

20-22: ⚠️ Potential issue | 🟡 Minor

Update checklist items to match new SPA/Fullstack terminology.

The implementation checklist still references "Browser hooks" and "Client hooks" with old directory names (hooks/, client/), but the PR renames these to SPA and Fullstack. This is inconsistent with the updated section at lines 92-99 and could cause confusion during release verification.

📝 Suggested update to align with new terminology
   - [ ] **@coji/durably-react** (`packages/durably-react/src/`)
-    - [ ] Browser hooks (`hooks/`)
-    - [ ] Client hooks (`client/`)
+    - [ ] SPA hooks (`spa/`)
+    - [ ] Fullstack hooks (root exports or `fullstack/`)
     - [ ] Shared utilities (`shared/`)
     - [ ] Type definitions (`types.ts`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/release-check/SKILL.md around lines 20 - 22, Update the
checklist entries that still read "Browser hooks" and "Client hooks" and
reference directories `hooks/` and `client/` to use the new SPA/Fullstack
terminology and directory names to match the rest of the file (e.g., change
"Browser hooks (`hooks/`)" → "SPA hooks (`spa/`)" and "Client hooks (`client/`)"
→ "Fullstack hooks (`fullstack/`)"), ensuring consistency with the other section
that already uses the new terms.
🧹 Nitpick comments (4)
website/guide/offline-app.md (1)

151-152: Consolidate duplicate imports.

Both lines import from the same module '@coji/durably-react/spa'. Consider merging them for cleaner documentation.

📝 Suggested consolidation
-import { DurablyProvider, useDurably } from '@coji/durably-react/spa'
-import { useJob } from '@coji/durably-react/spa'
+import { DurablyProvider, useDurably, useJob } from '@coji/durably-react/spa'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/guide/offline-app.md` around lines 151 - 152, Two separate import
statements pull from the same module; consolidate them into one import from
'@coji/durably-react/spa' that includes DurablyProvider, useDurably and useJob
together (look for the existing import lines that reference DurablyProvider,
useDurably and useJob) to remove the duplicate import and keep the doc example
concise and clean.
packages/durably-react/tests/client/create-durably-client.test.tsx (1)

11-11: Import the factory through the root entrypoint.

Line 11 still reaches into the implementation file, so this suite will stay green even if the new root export stops re-exporting createDurablyHooks. Since the rename is a public API change, it’s better to exercise ../../src here.

♻️ Suggested tweak
-import { createDurablyHooks } from '../../src/client/create-durably-hooks'
+import { createDurablyHooks } from '../../src'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably-react/tests/client/create-durably-client.test.tsx` at line
11, The test imports createDurablyHooks directly from the implementation; update
the import to use the public root entrypoint so the suite exercises the exported
API instead of the internal file — replace the import that references
createDurablyHooks from the implementation path with an import from the package
root (import { createDurablyHooks } from '../../src') so the test fails if the
root export is removed or renamed.
website/api/durably-react/client.md (1)

85-90: Pattern: Variable name shadows the type import.

The variable durably shadows the imported type typeof durably. While this is a common pattern for creating typed client instances, it may cause confusion for developers. Consider mentioning this pattern explicitly in the documentation or using a different variable name like durablyHooks.

📝 Alternative naming pattern
 // app/lib/durably.client.ts
 import { createDurablyHooks } from '@coji/durably-react'
 import type { durably } from './durably.server'

-export const durably = createDurablyHooks<typeof durably>({
+export const durablyHooks = createDurablyHooks<typeof durably>({
   api: '/api/durably',
 })

Then usage becomes:

const { trigger, status } = durablyHooks.importCsv.useJob()

Alternatively, add a note in the docs explaining this pattern is intentional for ergonomics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/api/durably-react/client.md` around lines 85 - 90, The exported
variable name `durably` shadows the imported type reference `typeof durably`,
which can confuse readers; either rename the runtime export (e.g., change export
const durably -> export const durablyHooks) and update all usage examples to use
`durablyHooks`, or add a short inline doc comment above the export explaining
that using the same name intentionally pairs the runtime instance with its type
via createDurablyHooks and is ergonomic; refer to createDurablyHooks, typeof
durably, and the exported variable `durably` when making the change.
website/api/durably-react/index.md (1)

50-55: Consider clarifying the installation instructions.

The comment on line 50 says "Fullstack mode - connects to Durably server" but the pnpm add command only installs @coji/durably-react. For fullstack mode, developers also need the server-side @coji/durably package. Consider adding a note about server-side dependencies.

📝 Suggested clarification
 ```bash
 # Fullstack mode - connects to Durably server
+# Client side:
 pnpm add `@coji/durably-react`
+# Server side:
+pnpm add `@coji/durably`

 # SPA mode - runs Durably in the browser
 pnpm add `@coji/durably` `@coji/durably-react` kysely zod sqlocal
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @website/api/durably-react/index.md around lines 50 - 55, Update the
installation instructions so Fullstack mode clearly installs both client and
server packages: explicitly show that the client needs @coji/durably-react and
the server needs @coji/durably (reference the package names @coji/durably-react
and @coji/durably from the diff) and add a brief note that the server-side
package must be installed for Fullstack mode while SPA mode keeps the existing
single-line install for @coji/durably, @coji/durably-react, kysely, zod, and
sqlocal.


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude/skills/doc-check/SKILL.md:

  • Around line 88-99: The checklist table in SKILL.md still references the old
    React docs (browser.md and client.md); update the affected rows (e.g., the "New
    field on Run / RunFilter" and both "React hook change" rows) to point to the
    new SPA/fullstack React documentation pages instead of browser.md and client.md
    so reviewers are directed to the redesigned React docs; search for occurrences
    of "browser.md" and "client.md" in the table and replace them with the correct
    new doc names/paths for the SPA/fullstack React docs.

In @packages/durably-react/docs/llms.md:

  • Around line 187-188: The examples using useRuns and TypedClientRun call
    z.object(...) but do not import z; update each affected code fence (examples
    near useRuns / TypedClientRun and defineJob) to add the missing z import by
    importing z from 'zod' so the z.object(...) expressions typecheck; search for
    occurrences of useRuns, TypedClientRun, and defineJob in the document (including
    the noted ranges) and add "import { z } from 'zod'" to the top of each snippet
    that uses z.object.

In @packages/durably-react/README.md:

  • Around line 95-96: Replace the two outdated README links that point to
    /durably-react/browser and /durably-react/client so they match the renamed
    labels (SPA/fullstack); update the hrefs for the "SPA Hooks" link to the new
    /durably-react/spa page and the "Fullstack Hooks" link to the new
    /durably-react/fullstack page so the README landing links point to the current
    API docs.

In @packages/durably-react/src/client/create-durably-hooks.ts:

  • Around line 42-43: The DurablyHooks generic currently treats its type
    parameter as a job map directly, causing it to iterate over instance properties
    instead of the instance.jobs map; change the generic to extract the jobs map
    when given a full Durably instance while still accepting a plain jobs map for
    backward compatibility by adding a conditional helper like ExtractJobs = T
    extends { jobs: infer J } ? J : T and then defining DurablyHooks = { [K in
    keyof ExtractJobs]: JobHooks<InferInput<ExtractJobs[K]>,
    InferOutput<ExtractJobs[K]>> }; update usages of DurablyHooks to use the new
    helper so createDurablyHooks(...) works as documented.

In @packages/durably/docs/llms.md:

  • Around line 46-50: The docs show destructuring syncUsers directly from the
    return of register() but register() returns a Durably instance whose job handles
    live under the .jobs property; update the example using createDurably and
    register (functions: createDurably, register) to access the job handle from the
    returned instance's .jobs (e.g., use durably.jobs.syncUsers) and adjust the
    explanatory text to state that register() returns a Durably instance rather than
    a handle map, referencing the syncUsersJob symbol as the job implementation.

Outside diff comments:
In @.claude/skills/release-check/SKILL.md:

  • Around line 20-22: Update the checklist entries that still read "Browser
    hooks" and "Client hooks" and reference directories hooks/ and client/ to
    use the new SPA/Fullstack terminology and directory names to match the rest of
    the file (e.g., change "Browser hooks (hooks/)" → "SPA hooks (spa/)" and
    "Client hooks (client/)" → "Fullstack hooks (fullstack/)"), ensuring
    consistency with the other section that already uses the new terms.

Nitpick comments:
In @packages/durably-react/tests/client/create-durably-client.test.tsx:

  • Line 11: The test imports createDurablyHooks directly from the implementation;
    update the import to use the public root entrypoint so the suite exercises the
    exported API instead of the internal file — replace the import that references
    createDurablyHooks from the implementation path with an import from the package
    root (import { createDurablyHooks } from '../../src') so the test fails if the
    root export is removed or renamed.

In @website/api/durably-react/client.md:

  • Around line 85-90: The exported variable name durably shadows the imported
    type reference typeof durably, which can confuse readers; either rename the
    runtime export (e.g., change export const durably -> export const durablyHooks)
    and update all usage examples to use durablyHooks, or add a short inline doc
    comment above the export explaining that using the same name intentionally pairs
    the runtime instance with its type via createDurablyHooks and is ergonomic;
    refer to createDurablyHooks, typeof durably, and the exported variable durably
    when making the change.

In @website/api/durably-react/index.md:

  • Around line 50-55: Update the installation instructions so Fullstack mode
    clearly installs both client and server packages: explicitly show that the
    client needs @coji/durably-react and the server needs @coji/durably (reference
    the package names @coji/durably-react and @coji/durably from the diff) and add a
    brief note that the server-side package must be installed for Fullstack mode
    while SPA mode keeps the existing single-line install for @coji/durably,
    @coji/durably-react, kysely, zod, and sqlocal.

In @website/guide/offline-app.md:

  • Around line 151-152: Two separate import statements pull from the same module;
    consolidate them into one import from '@coji/durably-react/spa' that includes
    DurablyProvider, useDurably and useJob together (look for the existing import
    lines that reference DurablyProvider, useDurably and useJob) to remove the
    duplicate import and keep the doc example concise and clean.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `2be0159e-640f-4be4-90ce-1eda956083e8`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e59df0c763c499d39dd5c4104a6cb02c8cb83ede and f6ba38cfc6c9a856ce8024c2eadcc1cff56afb3e.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `examples/spa-react-router/public/favicon.ico` is excluded by `!**/*.ico`
* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (84)</summary>

* `.claude/skills/doc-check/SKILL.md`
* `.claude/skills/release-check/SKILL.md`
* `examples/fullstack-react-router/app/lib/durably.server.ts`
* `examples/fullstack-react-router/app/routes/_index/dashboard.tsx`
* `examples/fullstack-react-router/app/routes/_index/data-sync-progress.tsx`
* `examples/fullstack-react-router/app/routes/_index/image-processing-progress.tsx`
* `examples/fullstack-react-router/app/routes/_index/run-progress.tsx`
* `examples/server-node/lib/durably.ts`
* `examples/spa-react-router/.gitignore`
* `examples/spa-react-router/README.md`
* `examples/spa-react-router/app/app.css`
* `examples/spa-react-router/app/entry.client.tsx`
* `examples/spa-react-router/app/jobs/data-sync.ts`
* `examples/spa-react-router/app/jobs/import-csv.ts`
* `examples/spa-react-router/app/jobs/index.ts`
* `examples/spa-react-router/app/jobs/process-image.ts`
* `examples/spa-react-router/app/lib/database.ts`
* `examples/spa-react-router/app/lib/durably.ts`
* `examples/spa-react-router/app/root.tsx`
* `examples/spa-react-router/app/routes.ts`
* `examples/spa-react-router/app/routes/_index.tsx`
* `examples/spa-react-router/app/routes/_index/dashboard.tsx`
* `examples/spa-react-router/app/routes/_index/data-sync-form.tsx`
* `examples/spa-react-router/app/routes/_index/data-sync-progress.tsx`
* `examples/spa-react-router/app/routes/_index/image-processing-form.tsx`
* `examples/spa-react-router/app/routes/_index/image-processing-progress.tsx`
* `examples/spa-react-router/app/routes/_index/run-progress.tsx`
* `examples/spa-react-router/biome.json`
* `examples/spa-react-router/package.json`
* `examples/spa-react-router/prettier.config.js`
* `examples/spa-react-router/react-router.config.ts`
* `examples/spa-react-router/tsconfig.json`
* `examples/spa-react-router/vite.config.ts`
* `examples/spa-vite-react/.gitignore`
* `examples/spa-vite-react/biome.json`
* `examples/spa-vite-react/index.html`
* `examples/spa-vite-react/package.json`
* `examples/spa-vite-react/prettier.config.js`
* `examples/spa-vite-react/src/App.tsx`
* `examples/spa-vite-react/src/app.css`
* `examples/spa-vite-react/src/components/dashboard.tsx`
* `examples/spa-vite-react/src/components/data-sync-form.tsx`
* `examples/spa-vite-react/src/components/data-sync-progress.tsx`
* `examples/spa-vite-react/src/components/image-processing-form.tsx`
* `examples/spa-vite-react/src/components/image-processing-progress.tsx`
* `examples/spa-vite-react/src/components/index.ts`
* `examples/spa-vite-react/src/components/run-progress.tsx`
* `examples/spa-vite-react/src/jobs/data-sync.ts`
* `examples/spa-vite-react/src/jobs/index.ts`
* `examples/spa-vite-react/src/jobs/process-image.ts`
* `examples/spa-vite-react/src/lib/database.ts`
* `examples/spa-vite-react/src/lib/durably.ts`
* `examples/spa-vite-react/src/main.tsx`
* `examples/spa-vite-react/tsconfig.json`
* `examples/spa-vite-react/vercel.json`
* `examples/spa-vite-react/vite.config.ts`
* `packages/durably-react/README.md`
* `packages/durably-react/docs/llms.md`
* `packages/durably-react/package.json`
* `packages/durably-react/src/client.ts`
* `packages/durably-react/src/client/create-durably-hooks.ts`
* `packages/durably-react/src/client/create-job-hooks.ts`
* `packages/durably-react/src/client/index.ts`
* `packages/durably-react/src/context.tsx`
* `packages/durably-react/src/index.ts`
* `packages/durably-react/src/spa.ts`
* `packages/durably-react/tests/browser/provider.test.tsx`
* `packages/durably-react/tests/browser/use-job-logs.test.tsx`
* `packages/durably-react/tests/browser/use-job-run.test.tsx`
* `packages/durably-react/tests/browser/use-job.test.tsx`
* `packages/durably-react/tests/browser/use-runs.test.tsx`
* `packages/durably-react/tests/client/create-durably-client.test.tsx`
* `packages/durably-react/tsup.config.ts`
* `packages/durably/docs/llms.md`
* `packages/durably/src/durably.ts`
* `website/.vitepress/config.ts`
* `website/api/durably-react/browser.md`
* `website/api/durably-react/client.md`
* `website/api/durably-react/index.md`
* `website/api/index.md`
* `website/guide/csv-import.md`
* `website/guide/getting-started.md`
* `website/guide/offline-app.md`
* `website/public/llms.txt`

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* packages/durably-react/src/client.ts

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread .claude/skills/doc-check/SKILL.md Outdated
Comment on lines +187 to +188
import { useRuns, TypedClientRun } from '@coji/durably-react'
import { defineJob } from '@coji/durably'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add the missing zod import to both useRuns examples.

Both snippets call z.object(...), but neither code fence imports z. As written, these copy-paste examples do not typecheck.

✏️ Proposed doc fix
 import { useRuns, TypedClientRun } from '@coji/durably-react'
 import { defineJob } from '@coji/durably'
+import { z } from 'zod'
 import { useRuns, TypedRun } from '@coji/durably-react/spa'
 import { defineJob } from '@coji/durably'
+import { z } from 'zod'

Also applies to: 201-205, 520-521, 535-539

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably-react/docs/llms.md` around lines 187 - 188, The examples
using useRuns and TypedClientRun call z.object(...) but do not import z; update
each affected code fence (examples near useRuns / TypedClientRun and defineJob)
to add the missing z import by importing z from 'zod' so the z.object(...)
expressions typecheck; search for occurrences of useRuns, TypedClientRun, and
defineJob in the document (including the noted ranges) and add "import { z }
from 'zod'" to the top of each snippet that uses z.object.

Comment thread packages/durably-react/README.md Outdated
Comment on lines +42 to +43
export type DurablyHooks<TJobs extends Record<string, unknown>> = {
[K in keyof TJobs]: JobHooks<InferInput<TJobs[K]>, InferOutput<TJobs[K]>>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n=== documented call sites ===\n'
rg -n -C2 'createDurablyHooks<\s*typeof durably' packages/durably-react website examples || true

printf '\n=== current createDurablyHooks typing ===\n'
sed -n '1,140p' packages/durably-react/src/client/create-durably-hooks.ts

printf '\n=== Durably instance shape ===\n'
rg -n -A12 -B4 'export interface Durably' packages/durably/src/durably.ts

Repository: coji/durably

Length of output: 7063


Extract the jobs property from the Durably instance type in the generic.

The documented API requires createDurablyHooks<typeof durably>(...) with the full Durably instance type, but the current generic DurablyHooks<TJobs> treats its parameter as a direct job record. This causes the mapped type to iterate over instance properties (jobs, init, db) instead of job names (importCsv, syncUsers), breaking the advertised type-safe hooks API and autocomplete.

All nine documented examples across website/ and packages/durably-react/ pass the full Durably type, confirming the intended usage. The fix should accept either the full instance or a direct jobs map for backward compatibility:

🛠️ Proposed fix
+type ExtractJobMap<T> =
+  T extends { jobs: infer TJobs extends Record<string, unknown> }
+    ? TJobs
+    : T extends Record<string, unknown>
+      ? T
+      : never
+
-export type DurablyHooks<TJobs extends Record<string, unknown>> = {
-  [K in keyof TJobs]: JobHooks<InferInput<TJobs[K]>, InferOutput<TJobs[K]>>
+export type DurablyHooks<
+  TDurablyOrJobs extends
+    | { jobs: Record<string, unknown> }
+    | Record<string, unknown>,
+> = {
+  [K in keyof ExtractJobMap<TDurablyOrJobs>]: JobHooks<
+    InferInput<ExtractJobMap<TDurablyOrJobs>[K]>,
+    InferOutput<ExtractJobMap<TDurablyOrJobs>[K]>
+  >
 }
 ...
-export function createDurablyHooks<TJobs extends Record<string, unknown>>(
+export function createDurablyHooks<
+  TDurablyOrJobs extends
+    | { jobs: Record<string, unknown> }
+    | Record<string, unknown>,
+>(
   options: CreateDurablyHooksOptions,
-): DurablyHooks<TJobs> {
+): DurablyHooks<TDurablyOrJobs> {

Affects lines 16–17, 44, 38 (JSDoc example).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably-react/src/client/create-durably-hooks.ts` around lines 42 -
43, The DurablyHooks generic currently treats its type parameter as a job map
directly, causing it to iterate over instance properties instead of the
instance.jobs map; change the generic to extract the jobs map when given a full
Durably instance while still accepting a plain jobs map for backward
compatibility by adding a conditional helper like ExtractJobs<T> = T extends {
jobs: infer J } ? J : T and then defining DurablyHooks<T> = { [K in keyof
ExtractJobs<T>]: JobHooks<InferInput<ExtractJobs<T>[K]>,
InferOutput<ExtractJobs<T>[K]>> }; update usages of DurablyHooks to use the new
helper so createDurablyHooks<typeof durably>(...) works as documented.

Comment thread packages/durably/docs/llms.md
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/durably-react/src/client/use-job.ts (1)

240-260: ⚠️ Potential issue | 🟠 Major

Reject pending triggerAndWait() calls on unmount.

The polling loop in triggerAndWait has a reliability leak. If the component unmounts while waiting for a job to complete, the setInterval continues running indefinitely because subscriptionRef.current stops updating after unmount (when use-subscription.ts unsubscribes). The loop never finds a terminal status, so clearInterval is never called and the promise never settles. Track outstanding waiters and clear/reject them from a cleanup effect.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably-react/src/client/use-job.ts` around lines 240 - 260,
triggerAndWait can leak intervals and unresolved promises when the component
unmounts; modify triggerAndWait to register each waiter (store its runId,
interval id, and reject callback) in a pendingWaitersRef (or similar) before
starting setInterval, and ensure you clearInterval and remove the waiter from
pendingWaitersRef when resolving or rejecting inside the interval; add a
useEffect cleanup that iterates pendingWaitersRef on unmount, clears each stored
interval and calls each stored reject with an appropriate Error (e.g.,
"Component unmounted"), and make sure references used are
subscriptionRef.current and trigger so the cleanup reliably cancels outstanding
waiters.
♻️ Duplicate comments (1)
packages/durably-react/src/client/create-durably-hooks.ts (1)

17-18: ⚠️ Potential issue | 🟠 Major

Extract jobs before mapping the generic.

The example documents createDurablyHooks<typeof durably>(...), but DurablyHooks<TJobs> currently maps over keyof typeof durably, so consumers get hooks for instance members like init/db instead of importCsv/syncUsers. Accept the full Durably instance shape and extract its jobs record before building the mapped type.

🛠️ Proposed fix
+type ExtractJobMap<T> =
+  T extends { jobs: infer TJobs extends Record<string, unknown> }
+    ? TJobs
+    : T extends Record<string, unknown>
+      ? T
+      : never
+
-export type DurablyHooks<TJobs extends Record<string, unknown>> = {
-  [K in keyof TJobs]: JobHooks<InferInput<TJobs[K]>, InferOutput<TJobs[K]>>
+export type DurablyHooks<
+  TDurablyOrJobs extends { jobs: Record<string, unknown> } | Record<string, unknown>,
+> = {
+  [K in keyof ExtractJobMap<TDurablyOrJobs>]: JobHooks<
+    InferInput<ExtractJobMap<TDurablyOrJobs>[K]>,
+    InferOutput<ExtractJobMap<TDurablyOrJobs>[K]>
+  >
 }
 ...
-export function createDurablyHooks<TJobs extends Record<string, unknown>>(
+export function createDurablyHooks<
+  TDurablyOrJobs extends { jobs: Record<string, unknown> } | Record<string, unknown>,
+>(
   options: CreateDurablyHooksOptions,
-): DurablyHooks<TJobs> {
+): DurablyHooks<TDurablyOrJobs> {

Also applies to: 38-39, 54-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably-react/src/client/create-durably-hooks.ts` around lines 17 -
18, The DurablyHooks generic currently maps over the wrong shape; change the
type to accept the full Durably instance and extract its jobs record before
mapping. Specifically, update the declaration DurablyHooks<TJobs extends
Record<string, unknown>> to take a Durably-like type (e.g., TDurably extends {
jobs: Record<string, unknown> }) and then map over keyof TDurably['jobs'] using
JobHooks<InferInput<...>, InferOutput<...>>; apply the same extraction fix to
the other similar declarations/usages around the symbols createDurablyHooks,
DurablyHooks (the other occurrences), JobHooks, InferInput and InferOutput
referenced in the file so consumers using createDurablyHooks<typeof
durably>(...) get hooks only for the jobs (importCsv/syncUsers) not instance
members (init/db).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/durably-react/src/index.ts`:
- Around line 8-15: Index entrypoint is missing re-exports for the deprecated
root aliases causing breaks for existing imports; add exports for
createDurablyClient and the associated types/aliases
(CreateDurablyClientOptions, DurablyClient, JobClient) to
packages/durably-react/src/index.ts so the aliases defined in
create-durably-hooks.ts remain publicly available. Locate the existing exports
for createDurablyHooks/createJobHooks in this file and add corresponding export
statements that re-export the deprecated symbols (createDurablyClient,
CreateDurablyClientOptions, DurablyClient, JobClient) from
'./client/create-durably-hooks' so old imports continue to work.

---

Outside diff comments:
In `@packages/durably-react/src/client/use-job.ts`:
- Around line 240-260: triggerAndWait can leak intervals and unresolved promises
when the component unmounts; modify triggerAndWait to register each waiter
(store its runId, interval id, and reject callback) in a pendingWaitersRef (or
similar) before starting setInterval, and ensure you clearInterval and remove
the waiter from pendingWaitersRef when resolving or rejecting inside the
interval; add a useEffect cleanup that iterates pendingWaitersRef on unmount,
clears each stored interval and calls each stored reject with an appropriate
Error (e.g., "Component unmounted"), and make sure references used are
subscriptionRef.current and trigger so the cleanup reliably cancels outstanding
waiters.

---

Duplicate comments:
In `@packages/durably-react/src/client/create-durably-hooks.ts`:
- Around line 17-18: The DurablyHooks generic currently maps over the wrong
shape; change the type to accept the full Durably instance and extract its jobs
record before mapping. Specifically, update the declaration DurablyHooks<TJobs
extends Record<string, unknown>> to take a Durably-like type (e.g., TDurably
extends { jobs: Record<string, unknown> }) and then map over keyof
TDurably['jobs'] using JobHooks<InferInput<...>, InferOutput<...>>; apply the
same extraction fix to the other similar declarations/usages around the symbols
createDurablyHooks, DurablyHooks (the other occurrences), JobHooks, InferInput
and InferOutput referenced in the file so consumers using
createDurablyHooks<typeof durably>(...) get hooks only for the jobs
(importCsv/syncUsers) not instance members (init/db).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: faeefa5b-6651-4073-8309-8c04e6f10f0d

📥 Commits

Reviewing files that changed from the base of the PR and between f6ba38c and 2cfba97.

📒 Files selected for processing (5)
  • packages/durably-react/src/client/create-durably-hooks.ts
  • packages/durably-react/src/client/index.ts
  • packages/durably-react/src/client/use-job.ts
  • packages/durably-react/src/index.ts
  • packages/durably/src/durably.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/durably-react/src/client/index.ts

Comment thread packages/durably-react/src/index.ts Outdated
…docs

- Remove deprecated createDurablyClient/DurablyClient/JobClient aliases
- Fix triggerAndWait interval leak on unmount via waitIntervalRef
- Rename test file to match source (create-durably-hooks.test.tsx)
- Add jobs option to create-durably.md signature and options table
- Fix SPA trigger example with incorrect labels parameter in llms.md
- Add UseRunsClientOptions (realtime option) to fullstack useRuns docs
- Fix stale "Browser-Complete/Server-Connected" terminology in types.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Runs/useRunActions

- Rename `createDurablyHooks` to `createDurably` (mirrors server-side name, tRPC-inspired)
- Add `useRuns` and `useRunActions` as built-in cross-job methods on DurablyClient Proxy
- Add `ExtractJobs<T>` type to support `createDurably<typeof serverDurably>()`
- Convert fullstack example to use `createDurably` pattern throughout
- Rename website docs: browser.md → spa.md, client.md → fullstack.md
- Use framework-agnostic `durably.ts` in doc examples
- Regenerate llms.txt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coji coji changed the title feat: redesign @coji/durably-react API — fullstack-first, new import paths feat: redesign @coji/durably-react API — createDurably, fullstack-first Mar 6, 2026
Match the framework-agnostic file name used in documentation.
The `.hooks.ts` suffix was unnecessary — only `.client.ts` triggers
React Router's client-only behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use `jobs: {}` option instead of `.register()` chain in all guides
- Use `await durably.init()` instead of `migrate()` + `start()`
- Rewrite fullstack.md: createDurably section first with per-job
  and cross-job hook examples (durably.useRuns(), durably.useRunActions())
- Update spa.md: jobs option, init()
- Update index.md: Dashboard example uses durably.useRuns()
- Update sidebar: "CSV Import (Fullstack)", "Offline App (SPA)"
- Update concepts.md, background-sync.md to use jobs option

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fullstack is the primary use case; SPA (OPFS) is specialized.
Reorder both Guide and API sidebars accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ribe

Important sections that existed in page content but were missing
from sidebar navigation:
- HTTP Handler: Auth Middleware (replaces generic "Security")
- createDurably: deleteRun, subscribe

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Key improvements:
- Add "grep for old patterns" as Step 2 (most effective at catching stale docs)
- Add missing file tiers: README (root), CLAUDE.md, skills, sidebar config
- Add API pattern consistency table (jobs option, init(), createDurably)
- Add "final grep" step to confirm nothing was missed
- Reorganize release-check into sequential workflow (diff → grep → check → validate → grep)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
packages/durably-react/src/client/use-job.ts (1)

241-271: ⚠️ Potential issue | 🟠 Major

Pin triggerAndWait() to the run it started.

subscriptionRef.current always follows the latest currentRunId. Because Lines 176-208 can switch currentRunId to a newer run for the same job, this promise can resolve/reject from that other run while still returning the original runId. Please keep a dedicated wait target for the started run, or suppress followLatest handoffs until that run reaches a terminal state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably-react/src/client/use-job.ts` around lines 241 - 271,
triggerAndWait is racing with followLatest because subscriptionRef.current
tracks the latest currentRunId; fix by pinning the wait to the run you started:
capture the returned runId from trigger (already stored) and when inspecting
subscriptionRef.current only act if subscriptionRef.current.runId === runId (or
create a per-run subscriptionRef/targetRun variable) so resolve/reject logic in
triggerAndWait only fires for the same runId; ensure waitIntervalRef management
(clear/set) remains unchanged so the interval is tied to that pinned run.
.claude/skills/release-check/SKILL.md (1)

92-117: ⚠️ Potential issue | 🟡 Minor

Update Section 1 to the new SPA/fullstack names too.

This file now uses SPA/fullstack terminology, but Lines 20-21 still list “Browser hooks” and “Client hooks”. The release checklist should use the new names there as well so reviewers do not audit the pre-rename surfaces.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/release-check/SKILL.md around lines 92 - 117, Update Section
1 headings that still use the old names "Browser hooks" and "Client hooks" to
the new SPA/fullstack terminology so the checklist is consistent; locate the
top-level section (Section 1) and replace those label strings with "SPA hooks"
and "Fullstack hooks" respectively, and verify any cross-references or lists in
the same section now use "SPA/fullstack" consistently (also check any nearby
mentions in the same file like the "SPA/Fullstack Mode Consistency" heading and
the example table to ensure all labels match).
website/api/durably-react/spa.md (1)

61-71: ⚠️ Potential issue | 🟡 Minor

Move the async call out of render and use the public getRuns() API.

Line 70 shows await durably.storage.getRuns() inside component body, which is not a valid client-component pattern and relies on internal storage API instead of the public getRuns() method.

🛠️ Proposed docs fix
 function Component() {
   const { durably, isReady, error } = useDurably()

   if (!isReady) return <div>Loading...</div>
   if (error) return <div>Error: {error.message}</div>

-  // Use durably instance directly
-  const runs = await durably.storage.getRuns()
+  const handleLoadRuns = async () => {
+    const runs = await durably.getRuns()
+    console.log(runs)
+  }
+
+  return <button onClick={handleLoadRuns}>Load runs</button>
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/api/durably-react/spa.md` around lines 61 - 71, The component is
awaiting durably.storage.getRuns() inside the render body and using an internal
storage API; move this async work into an effect or callback and call the public
API durably.getRuns() instead. In Component, after confirming isReady and no
error (from useDurably), start an async call in useEffect (or an event handler)
that calls durably.getRuns(), stores the result in state, and handles
loading/error states; remove any use of durably.storage.getRuns() and the
top-level await so rendering remains synchronous.
website/api/durably-react/fullstack.md (2)

280-286: ⚠️ Potential issue | 🟡 Minor

Add the missing zod import to this useRuns example.

This snippet uses z.object(...) but never imports z, so it does not copy-paste cleanly.

✏️ Proposed doc fix
 import { defineJob } from '@coji/durably'
 import { useRuns } from '@coji/durably-react'
+import { z } from 'zod'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/api/durably-react/fullstack.md` around lines 280 - 286, The example
uses z.object(...) but never imports z; add an import for z from the zod package
at the top of the snippet so the example is copy-pasteable (e.g., import { z }
from 'zod') and ensure it sits alongside the existing imports for defineJob and
useRuns referenced in the snippet.

32-33: ⚠️ Potential issue | 🟡 Minor

Prefer init() in the primary setup example.

Line 32-33 still recommends migrate() + start(), which undercuts the new API guidance in this PR. The main example should use init() and leave the split lifecycle calls to advanced docs.

✏️ Proposed doc fix
-await durably.migrate()
-durably.start()
+await durably.init()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/api/durably-react/fullstack.md` around lines 32 - 33, Update the
primary setup example to use the new combined lifecycle method by replacing the
separate calls to durably.migrate() and durably.start() with a single
durably.init() call; locate the example that currently invokes durably.migrate()
and durably.start() and change it to call durably.init() so the docs reflect the
new API, leaving the migrate/start split only in advanced sections.
♻️ Duplicate comments (1)
packages/durably-react/docs/llms.md (1)

199-217: ⚠️ Potential issue | 🟡 Minor

These useRuns snippets still miss z imports.

Both examples call z.object(...) without importing z, so they still do not typecheck as written.

✏️ Proposed doc fix
 import { useRuns, TypedClientRun } from '@coji/durably-react'
 import { defineJob } from '@coji/durably'
+import { z } from 'zod'
 import { useRuns, TypedRun } from '@coji/durably-react/spa'
 import { defineJob } from '@coji/durably'
+import { z } from 'zod'

Also applies to: 523-542

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably-react/docs/llms.md` around lines 199 - 217, The examples
reference z.object(...) but never import z; update the snippets that use
useRuns/TypedClientRun and the defineJob example to import z from 'zod' (add an
import statement like "import { z } from 'zod'") so symbols used in
TypedClientRun types and in defineJob (e.g., syncDataJob) type-check correctly;
ensure the import appears at the top of each snippet where useRuns,
TypedClientRun, or defineJob are used.
🧹 Nitpick comments (2)
packages/durably-react/tests/client/create-durably.test.tsx (1)

50-59: Add coverage for the new cross-job helpers.

These cases only exercise per-job proxy accessors. The new durably.useRuns() / durably.useRunActions() built-ins, plus the ExtractJobs<T extends { jobs: ... }> path used by typeof serverDurably, should have at least one happy-path assertion here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably-react/tests/client/create-durably.test.tsx` around lines 50
- 59, Add assertions to this test to cover the new cross-job helpers and the
ExtractJobs path: after creating client with createDurably<MockJobs>, assert
that durably.useRuns and durably.useRunActions (or client.useRuns /
client.useRunActions depending on exported shape) are defined and callable (use
toBeDefined and toBeTypeOf('function')), and also exercise the ExtractJobs path
by constructing/typing a serverDurably-like value (using typeof serverDurably /
a stub with a jobs property) and asserting that its extracted job clients (e.g.,
importCsv, syncUsers) are present and their .useJob/.useRun/.useLogs helpers
exist; reference createDurably, useRuns, useRunActions, and ExtractJobs to
locate where to add these assertions.
website/api/index.md (1)

145-148: Use the alias pattern from the official examples for consistency.

The official durably-react source code and examples use import type { durably as serverDurably } to avoid reusing the same identifier for both type and value. This improves clarity in the snippet.

🛠️ Proposed docs refactor
 import { createDurably } from '@coji/durably-react'
-import type { durably } from './durably.server'
+import type { durably as serverDurably } from './durably.server'

-const durably = createDurably<typeof durably>({
+const durably = createDurably<typeof serverDurably>({
   api: '/api/durably',
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/api/index.md` around lines 145 - 148, The snippet reuses the
identifier "durably" for both the imported type and the runtime value; update
the type import to use the alias from the examples (import type { durably as
serverDurably }) and then pass that aliased type into createDurably
(createDurably<typeof serverDurably>({ api: '/api/durably' })) so the type name
and value name are distinct and the example matches official patterns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude/skills/doc-check/SKILL.md:
- Around line 88-99: Replace the outdated phrase "Browser and Client mode" in
the checklist with the updated terminology used in the scope guide (e.g.,
"Browser and Server mode" or the exact new label the scope guide uses) so the
checklist is consistent; specifically update the checklist row that currently
instructs reviewers to update “Browser and Client mode” hooks to use the new
term and scan the surrounding "React hook change" / "React hook change
(examples...)" entries to ensure the same replacement is applied everywhere in
SKILL.md.

In `@packages/durably-react/README.md`:
- Around line 95-96: Update the description text next to the "[SPA Hooks]" link:
replace the phrase "Browser mode with OPFS" with "SPA mode with OPFS" so the
README uses the new SPA/fullstack terminology consistently; locate the line
containing the "[SPA Hooks]" link and change its adjacent label accordingly.

In `@packages/durably-react/src/client/create-durably.ts`:
- Around line 102-115: The proxy currently reserves the names useRuns and
useRunActions in the builtins map, which shadows any job named the same; update
the implementation around the Proxy get handler and the builtins object so
cross-job helpers are not in the same top-level namespace as job hooks: either
move these helpers off the root DurablyClient (e.g., under a dedicated property
like cross or helpers) or explicitly detect reserved keys (inspect builtins,
useRuns, useRunActions, DurablyClient, Proxy get) and throw or document a clear
error; modify call sites and typings accordingly so job keys remain reachable
and types don’t collapse into intersections.

In `@website/api/create-durably.md`:
- Around line 14-16: The function and related docs use unconstrained generics
and bare JobDefinition types that don't match the implementation; update the
createDurably signature to constrain TLabels and TJobs to the actual types used
(e.g., TLabels extends Record<string, string> or the concrete label constraint
used in code, and TJobs extends Record<string, JobDefinition<string, any,
any>>), and update DurablyOptions and the register() method signatures to use
JobDefinition<string, any, any> (or the exact JobDefinition generic shape the
implementation expects) so the docs mirror the implementation and preserve
correct type inference; locate and change the signatures for createDurably,
DurablyOptions, and register to include these stricter generic constraints and
replace bare JobDefinition references with the fully-parameterized form.

In `@website/public/llms.txt`:
- Around line 32-50: The example defines const durably twice which is a
duplicate declaration; change the second variable name (the one returned from
createDurably without jobs) to a unique identifier (e.g., durablyNoJobs or
durablyRegistered) and update the subsequent destructuring line to use that new
name when calling register({ syncUsers: syncUsersJob }); ensure references to
durably.jobs.syncUsers remain for the first Option 1 example and that the Option
2 example uses the new identifier when extracting syncUsers.
- Around line 673-678: The exported const durably is colliding with the imported
type name; update the source docs file (packages/durably-react/docs/llms.md) to
alias the imported type (e.g., import type { durably as DurablyType } from
'...') and then use that alias in the generic for createDurably
(createDurably<typeof DurablyType>({ api: '/api/durably' })), keeping the
exported const durably name unchanged and leaving createDurably invocation
as-is.

---

Outside diff comments:
In @.claude/skills/release-check/SKILL.md:
- Around line 92-117: Update Section 1 headings that still use the old names
"Browser hooks" and "Client hooks" to the new SPA/fullstack terminology so the
checklist is consistent; locate the top-level section (Section 1) and replace
those label strings with "SPA hooks" and "Fullstack hooks" respectively, and
verify any cross-references or lists in the same section now use "SPA/fullstack"
consistently (also check any nearby mentions in the same file like the
"SPA/Fullstack Mode Consistency" heading and the example table to ensure all
labels match).

In `@packages/durably-react/src/client/use-job.ts`:
- Around line 241-271: triggerAndWait is racing with followLatest because
subscriptionRef.current tracks the latest currentRunId; fix by pinning the wait
to the run you started: capture the returned runId from trigger (already stored)
and when inspecting subscriptionRef.current only act if
subscriptionRef.current.runId === runId (or create a per-run
subscriptionRef/targetRun variable) so resolve/reject logic in triggerAndWait
only fires for the same runId; ensure waitIntervalRef management (clear/set)
remains unchanged so the interval is tied to that pinned run.

In `@website/api/durably-react/fullstack.md`:
- Around line 280-286: The example uses z.object(...) but never imports z; add
an import for z from the zod package at the top of the snippet so the example is
copy-pasteable (e.g., import { z } from 'zod') and ensure it sits alongside the
existing imports for defineJob and useRuns referenced in the snippet.
- Around line 32-33: Update the primary setup example to use the new combined
lifecycle method by replacing the separate calls to durably.migrate() and
durably.start() with a single durably.init() call; locate the example that
currently invokes durably.migrate() and durably.start() and change it to call
durably.init() so the docs reflect the new API, leaving the migrate/start split
only in advanced sections.

In `@website/api/durably-react/spa.md`:
- Around line 61-71: The component is awaiting durably.storage.getRuns() inside
the render body and using an internal storage API; move this async work into an
effect or callback and call the public API durably.getRuns() instead. In
Component, after confirming isReady and no error (from useDurably), start an
async call in useEffect (or an event handler) that calls durably.getRuns(),
stores the result in state, and handles loading/error states; remove any use of
durably.storage.getRuns() and the top-level await so rendering remains
synchronous.

---

Duplicate comments:
In `@packages/durably-react/docs/llms.md`:
- Around line 199-217: The examples reference z.object(...) but never import z;
update the snippets that use useRuns/TypedClientRun and the defineJob example to
import z from 'zod' (add an import statement like "import { z } from 'zod'") so
symbols used in TypedClientRun types and in defineJob (e.g., syncDataJob)
type-check correctly; ensure the import appears at the top of each snippet where
useRuns, TypedClientRun, or defineJob are used.

---

Nitpick comments:
In `@packages/durably-react/tests/client/create-durably.test.tsx`:
- Around line 50-59: Add assertions to this test to cover the new cross-job
helpers and the ExtractJobs path: after creating client with
createDurably<MockJobs>, assert that durably.useRuns and durably.useRunActions
(or client.useRuns / client.useRunActions depending on exported shape) are
defined and callable (use toBeDefined and toBeTypeOf('function')), and also
exercise the ExtractJobs path by constructing/typing a serverDurably-like value
(using typeof serverDurably / a stub with a jobs property) and asserting that
its extracted job clients (e.g., importCsv, syncUsers) are present and their
.useJob/.useRun/.useLogs helpers exist; reference createDurably, useRuns,
useRunActions, and ExtractJobs to locate where to add these assertions.

In `@website/api/index.md`:
- Around line 145-148: The snippet reuses the identifier "durably" for both the
imported type and the runtime value; update the type import to use the alias
from the examples (import type { durably as serverDurably }) and then pass that
aliased type into createDurably (createDurably<typeof serverDurably>({ api:
'/api/durably' })) so the type name and value name are distinct and the example
matches official patterns.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7663d00e-c5d0-4664-b5e8-e3a1fecdd936

📥 Commits

Reviewing files that changed from the base of the PR and between 2cfba97 and 410cdc0.

📒 Files selected for processing (23)
  • .claude/skills/doc-check/SKILL.md
  • .claude/skills/release-check/SKILL.md
  • examples/fullstack-react-router/app/lib/durably.hooks.ts
  • examples/fullstack-react-router/app/routes/_index/dashboard.tsx
  • examples/fullstack-react-router/app/routes/_index/data-sync-progress.tsx
  • examples/fullstack-react-router/app/routes/_index/image-processing-progress.tsx
  • packages/durably-react/README.md
  • packages/durably-react/docs/llms.md
  • packages/durably-react/src/client/create-durably.ts
  • packages/durably-react/src/client/index.ts
  • packages/durably-react/src/client/use-job.ts
  • packages/durably-react/src/index.ts
  • packages/durably-react/tests/client/create-durably.test.tsx
  • website/.vitepress/config.ts
  • website/api/create-durably.md
  • website/api/durably-react/fullstack.md
  • website/api/durably-react/index.md
  • website/api/durably-react/spa.md
  • website/api/durably-react/types.md
  • website/api/index.md
  • website/guide/csv-import.md
  • website/guide/getting-started.md
  • website/public/llms.txt
✅ Files skipped from review due to trivial changes (1)
  • website/api/durably-react/types.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/durably-react/src/client/index.ts
  • examples/fullstack-react-router/app/routes/_index/data-sync-progress.tsx

Comment thread .claude/skills/doc-check/SKILL.md Outdated
Comment thread packages/durably-react/README.md Outdated
Comment thread packages/durably-react/src/client/create-durably.ts Outdated
Comment on lines +14 to +16
function createDurably<TLabels, TJobs>(
options: DurablyOptions<TLabels, TJobs> & { jobs: TJobs },
): Durably<TransformToHandles<TJobs, TLabels>, TLabels>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify canonical signatures in source
echo "=== Checking implementation source ==="
rg -n "export function createDurably<" packages/durably/src/durably.ts -A20
echo ""
echo "=== Checking DurablyOptions interface ==="
rg -n "interface DurablyOptions<" packages/durably/src/durably.ts -A20
echo ""
echo "=== Checking current docs signature ==="
rg -n "function createDurably<TLabels, TJobs>|TJobs extends Record<string, JobDefinition>" website/api/create-durably.md -A8 -B4

Repository: coji/durably

Length of output: 3640


Add strict generic constraints to match implementation signatures.

The docs show unconstrained generic parameters and bare JobDefinition types that diverge from the actual implementation. This can lead to incorrect type inference when developers copy signatures.

The createDurably function signature (line 14–16) is missing the TLabels constraint entirely, and TJobs has no constraint. The DurablyOptions interface (line 24) and register() method (line 83) use bare JobDefinition instead of the strict JobDefinition<string, any, any> used in the implementation.

🛠️ Proposed docs fixes
 // With jobs (1-step initialization)
-function createDurably<TLabels, TJobs>(
+function createDurably<
+  TLabels extends Record<string, string> = Record<string, string>,
+  TJobs extends Record<string, JobDefinition<string, any, any>> = Record<
+    string,
+    never
+  >,
+>(
   options: DurablyOptions<TLabels, TJobs> & { jobs: TJobs },
 ): Durably<TransformToHandles<TJobs, TLabels>, TLabels>
 interface DurablyOptions<
   TLabels extends Record<string, string> = Record<string, string>,
-  TJobs extends Record<string, JobDefinition> = Record<string, never>,
+  TJobs extends Record<string, JobDefinition<string, any, any>> = Record<
+    string,
+    never
+  >,
 > {
-durably.register<TJobs extends Record<string, JobDefinition>>(
+durably.register<TJobs extends Record<string, JobDefinition<string, any, any>>>(
   jobs: TJobs
 ): { [K in keyof TJobs]: JobHandle }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function createDurably<TLabels, TJobs>(
options: DurablyOptions<TLabels, TJobs> & { jobs: TJobs },
): Durably<TransformToHandles<TJobs, TLabels>, TLabels>
function createDurably<
TLabels extends Record<string, string> = Record<string, string>,
TJobs extends Record<string, JobDefinition<string, any, any>> = Record<
string,
never
>,
>(
options: DurablyOptions<TLabels, TJobs> & { jobs: TJobs },
): Durably<TransformToHandles<TJobs, TLabels>, TLabels>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/api/create-durably.md` around lines 14 - 16, The function and related
docs use unconstrained generics and bare JobDefinition types that don't match
the implementation; update the createDurably signature to constrain TLabels and
TJobs to the actual types used (e.g., TLabels extends Record<string, string> or
the concrete label constraint used in code, and TJobs extends Record<string,
JobDefinition<string, any, any>>), and update DurablyOptions and the register()
method signatures to use JobDefinition<string, any, any> (or the exact
JobDefinition generic shape the implementation expects) so the docs mirror the
implementation and preserve correct type inference; locate and change the
signatures for createDurably, DurablyOptions, and register to include these
stricter generic constraints and replace bare JobDefinition references with the
fully-parameterized form.

Comment thread website/public/llms.txt
Comment on lines +32 to 50
// Option 1: With jobs (1-step initialization, returns typed instance)
const durably = createDurably({
dialect,
pollingInterval: 1000, // Job polling interval (ms)
heartbeatInterval: 5000, // Heartbeat update interval (ms)
staleThreshold: 30000, // When to consider a job abandoned (ms)
// Optional: type-safe labels with Zod schema
// labels: z.object({ organizationId: z.string(), env: z.string() }),
jobs: {
syncUsers: syncUsersJob,
},
})
// durably.jobs.syncUsers is immediately available and type-safe

// Option 2: Without jobs (register later)
const durably = createDurably({ dialect })
const { syncUsers } = durably.register({
syncUsers: syncUsersJob,
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -type f -name "llms.*"

Repository: coji/durably

Length of output: 226


🏁 Script executed:

# Search for both potential file locations
git ls-files | grep -E "(llms|docs)" | head -20

Repository: coji/durably

Length of output: 236


🏁 Script executed:

head -60 website/public/llms.txt | tail -30

Repository: coji/durably

Length of output: 899


🏁 Script executed:

# Check if packages/durably/docs/llms.md has similar content
sed -n '30,55p' packages/durably/docs/llms.md

Repository: coji/durably

Length of output: 823


🏁 Script executed:

# Check the generate script to understand relationship
head -50 website/scripts/generate-llms.js

Repository: coji/durably

Length of output: 845


🏁 Script executed:

# Check for duplicate const declarations in both source files
rg -n "const durably =" packages/durably/docs/llms.md packages/durably-react/docs/llms.md

Repository: coji/durably

Length of output: 615


The Option 1/Option 2 example has a duplicate const durably declaration in the same scope, preventing the code from running as written.

This is a syntax error in the source documentation (packages/durably/docs/llms.md lines 33–50). Rename the second declaration:

🛠️ Proposed fix
// Option 2: Without jobs (register later)
-const durably = createDurably({ dialect })
-const { syncUsers } = durably.register({
+const durablyWithoutJobs = createDurably({ dialect })
+const { syncUsers } = durablyWithoutJobs.register({
   syncUsers: syncUsersJob,
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Option 1: With jobs (1-step initialization, returns typed instance)
const durably = createDurably({
dialect,
pollingInterval: 1000, // Job polling interval (ms)
heartbeatInterval: 5000, // Heartbeat update interval (ms)
staleThreshold: 30000, // When to consider a job abandoned (ms)
// Optional: type-safe labels with Zod schema
// labels: z.object({ organizationId: z.string(), env: z.string() }),
jobs: {
syncUsers: syncUsersJob,
},
})
// durably.jobs.syncUsers is immediately available and type-safe
// Option 2: Without jobs (register later)
const durably = createDurably({ dialect })
const { syncUsers } = durably.register({
syncUsers: syncUsersJob,
})
// Option 1: With jobs (1-step initialization, returns typed instance)
const durably = createDurably({
dialect,
pollingInterval: 1000, // Job polling interval (ms)
heartbeatInterval: 5000, // Heartbeat update interval (ms)
staleThreshold: 30000, // When to consider a job abandoned (ms)
// Optional: type-safe labels with Zod schema
// labels: z.object({ organizationId: z.string(), env: z.string() }),
jobs: {
syncUsers: syncUsersJob,
},
})
// durably.jobs.syncUsers is immediately available and type-safe
// Option 2: Without jobs (register later)
const durablyWithoutJobs = createDurably({ dialect })
const { syncUsers } = durablyWithoutJobs.register({
syncUsers: syncUsersJob,
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/public/llms.txt` around lines 32 - 50, The example defines const
durably twice which is a duplicate declaration; change the second variable name
(the one returned from createDurably without jobs) to a unique identifier (e.g.,
durablyNoJobs or durablyRegistered) and update the subsequent destructuring line
to use that new name when calling register({ syncUsers: syncUsersJob }); ensure
references to durably.jobs.syncUsers remain for the first Option 1 example and
that the Option 2 example uses the new identifier when extracting syncUsers.

Comment thread website/public/llms.txt
Comment on lines +673 to +678
import type { durably } from '~/lib/durably.server'
import { createDurably } from '@coji/durably-react'

export const durably = createDurably<typeof durably>({
api: '/api/durably',
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if website/public/llms.txt exists and find similar files
echo "=== Checking for website/public/llms.txt ==="
if [ -f "website/public/llms.txt" ]; then
  echo "File exists"
  wc -l "website/public/llms.txt"
else
  echo "File does not exist at website/public/llms.txt"
fi

echo -e "\n=== Checking for packages/durably/docs/llms.md ==="
if [ -f "packages/durably/docs/llms.md" ]; then
  echo "File exists"
  wc -l "packages/durably/docs/llms.md"
else
  echo "File does not exist at packages/durably/docs/llms.md"
fi

echo -e "\n=== Listing files named llms.* ==="
fd "llms\." --type f

Repository: coji/durably

Length of output: 398


🏁 Script executed:

#!/bin/bash
# Read lines 673-678 from website/public/llms.txt
sed -n '673,678p' website/public/llms.txt
echo "---"
# Also show more context
sed -n '670,685p' website/public/llms.txt

Repository: coji/durably

Length of output: 701


🏁 Script executed:

#!/bin/bash
# Check the generate script
echo "=== Checking generate-llms.js ==="
head -50 website/scripts/generate-llms.js

echo -e "\n=== Checking packages/durably/docs/llms.md for similar code ==="
sed -n '1,100p' packages/durably/docs/llms.md | grep -A10 -B5 "import type { durably }" || echo "Pattern not found in first 100 lines"

# Search more broadly
grep -n "import type { durably } from '~/lib/durably.server'" packages/durably/docs/llms.md | head -5

Repository: coji/durably

Length of output: 981


🏁 Script executed:

#!/bin/bash
# Search both source files for the pattern
echo "=== Searching packages/durably/docs/llms.md ==="
grep -n "import type { durably } from '~/lib/durably.server'" packages/durably/docs/llms.md

echo -e "\n=== Searching packages/durably-react/docs/llms.md ==="
grep -n "import type { durably } from '~/lib/durably.server'" packages/durably-react/docs/llms.md

# If not found, try broader search
echo -e "\n=== Broader search for 'createDurably<typeof durably>' ==="
grep -rn "createDurably<typeof durably>" packages/durably/docs/ packages/durably-react/docs/

Repository: coji/durably

Length of output: 367


🏁 Script executed:

#!/bin/bash
# Read context around lines 53-56 in packages/durably-react/docs/llms.md
sed -n '48,65p' packages/durably-react/docs/llms.md

Repository: coji/durably

Length of output: 556


Update the source file packages/durably-react/docs/llms.md (lines 53-56), not the generated website/public/llms.txt.

Use a type alias to avoid the name collision between the imported type and exported variable.

🛠️ Proposed docs refactor
-import type { durably } from '~/lib/durably.server'
+import type { durably as serverDurably } from '~/lib/durably.server'
 import { createDurably } from '@coji/durably-react'

-export const durably = createDurably<typeof durably>({
+export const durably = createDurably<typeof serverDurably>({
   api: '/api/durably',
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { durably } from '~/lib/durably.server'
import { createDurably } from '@coji/durably-react'
export const durably = createDurably<typeof durably>({
api: '/api/durably',
})
import type { durably as serverDurably } from '~/lib/durably.server'
import { createDurably } from '@coji/durably-react'
export const durably = createDurably<typeof serverDurably>({
api: '/api/durably',
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/public/llms.txt` around lines 673 - 678, The exported const durably
is colliding with the imported type name; update the source docs file
(packages/durably-react/docs/llms.md) to alias the imported type (e.g., import
type { durably as DurablyType } from '...') and then use that alias in the
generic for createDurably (createDurably<typeof DurablyType>({ api:
'/api/durably' })), keeping the exported const durably name unchanged and
leaving createDurably invocation as-is.

Inspired by Anthropic's skill-creator design patterns:

- Add `scripts/find-stale.sh` — automated grep for old API patterns,
  import paths, directory names, and terminology across docs/examples
- Rewrite doc-check as 6-phase orchestrator: detect → understand →
  walk docs → regenerate → validate → final check
- Rewrite release-check as 10-phase orchestrator sharing the same script
- Why-driven: every Tier/file explains WHY it needs checking
- Description optimized for trigger accuracy ("prevents follow-up PRs")
- Fix "Browser mode" → "SPA mode" in durably-react README (caught by script)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/durably/README.md (1)

19-33: ⚠️ Potential issue | 🟡 Minor

Make the quick-start snippet self-contained.

This now shows the new jobs option, but dialect is still never declared inside the code block. A tiny placeholder or import would make the README copy-pasteable for first-time users.

💡 Suggested tweak
 import { createDurably, defineJob } from '@coji/durably'
 import { z } from 'zod'
+
+// Create a Kysely dialect for your SQLite backend first.
+const dialect = /* ... */

 const myJob = defineJob({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably/README.md` around lines 19 - 33, The README quick-start
snippet references an undeclared variable `dialect`; update the code block so it
is self-contained by adding a minimal placeholder or import for `dialect` used
when calling `createDurably({ dialect, jobs: { myJob } })`. Specifically, add a
short declaration (e.g., a comment or dummy value) for `dialect` near the top of
the example before `createDurably` so readers can copy-paste and run the
snippet; ensure the identifiers `createDurably`, `defineJob`, `myJob`, and
`dialect` are consistently referenced and documented in the block.
website/.vitepress/config.ts (1)

183-241: ⚠️ Potential issue | 🟡 Minor

Add useRunActions to the SPA sidebar.

useRunActions is exported from the main package entry point (packages/durably-react/src/index.ts) and available via the client exports, but it's missing from both the SPA documentation (website/api/durably-react/spa.md) and the sidebar navigation. Add it as a sidebar item under the SPA Hooks section to match the fullstack section coverage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/.vitepress/config.ts` around lines 183 - 241, The SPA sidebar is
missing the useRunActions entry; add an item for useRunActions to the SPA Hooks
items array (mirroring the Fullstack entry) with text 'useRunActions' and link
'/api/durably-react/spa#userunactions' so it appears under the SPA Hooks
section, and also update the SPA doc (api/durably-react/spa.md) to include the
useRunActions documentation (exported symbol: useRunActions) so the sidebar link
has a matching anchor target.
website/guide/csv-import.md (1)

218-223: ⚠️ Potential issue | 🟡 Minor

Use the proxy client for cross-job hooks here.

This dashboard snippet is still using the old standalone fullstack hooks. With the redesigned API, useRuns() and useRunActions() should come from the created client object (durablyClient.useRuns(), durablyClient.useRunActions()), not direct imports from @coji/durably-react.

✏️ Proposed doc fix
-import { useRuns, useRunActions } from '@coji/durably-react'
+import { durablyClient } from '~/lib/durably'

 function Dashboard() {
-  const { runs, refresh } = useRuns({ api: '/api/durably' })
-  const { retry, cancel, deleteRun } = useRunActions({ api: '/api/durably' })
+  const { runs, refresh } = durablyClient.useRuns()
+  const { retry, cancel, deleteRun } = durablyClient.useRunActions()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/guide/csv-import.md` around lines 218 - 223, The snippet imports
useRuns and useRunActions directly from `@coji/durably-react` but should use the
proxy client created earlier; replace direct imports and calls so the component
calls durablyClient.useRuns(...) and durablyClient.useRunActions(...)
(referencing the existing durablyClient instance) instead of useRuns(...) and
useRunActions(...), and pass the same options ({ api: '/api/durably' }) to those
client methods to preserve behavior.
website/api/durably-react/spa.md (1)

61-72: ⚠️ Potential issue | 🔴 Critical

Fix useDurably hook signature and async pattern.

Line 65 destructures isReady and error properties that useDurably() does not return—it only returns { durably }. Additionally, line 71 uses await directly in the component body, which is invalid React. Either use useRuns() hook for automatic loading state management, or move async calls into an event handler/effect.

✏️ Proposed fix using useRuns hook
-import { useDurably } from '@coji/durably-react/spa'
+import { useRuns } from '@coji/durably-react/spa'

 function Component() {
-  const { durably, isReady, error } = useDurably()
+  const { runs, isLoading, error } = useRuns({})

-  if (!isReady) return <div>Loading...</div>
+  if (isLoading) return <div>Loading...</div>
   if (error) return <div>Error: {error}</div>

-  // Use durably instance directly
-  const runs = await durably.storage.getRuns()
+  return <div>{runs.length} runs</div>
 }

Or if you need direct durably access, use an effect:

+import { useEffect, useState } from 'react'
 import { useDurably } from '@coji/durably-react/spa'

 function Component() {
   const { durably } = useDurably()
+  const [runs, setRuns] = useState([])
+
+  useEffect(() => {
+    if (!durably) return
+    durably.storage.getRuns().then(setRuns)
+  }, [durably])

-  if (!isReady) return <div>Loading...</div>
-  if (error) return <div>Error: {error.message}</div>
-
-  // Use durably instance directly
-  const runs = await durably.storage.getRuns()
+  return <div>{runs.length} runs</div>
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/api/durably-react/spa.md` around lines 61 - 72, The component
incorrectly destructures isReady and error from useDurably (it only returns {
durably }) and also uses await in the component body; fix by only extracting
durably via const { durably } = useDurably() and then either (A) replace the
direct call with the provided useRuns hook to get runs, loading and error state
(useRuns() returns runs/isLoading/error), or (B) if you need raw durably, move
the async call durably.storage.getRuns() into a useEffect or an event handler,
set local state for runs/loading/error, and avoid top-level await in the
Component; update references to runs, loading/isReady and error accordingly.
♻️ Duplicate comments (3)
packages/durably/docs/llms.md (1)

47-50: ⚠️ Potential issue | 🟡 Minor

Fix the register() example to read handles from .jobs.

Line 48 currently destructures syncUsers from the return value of register(), but register() returns a new Durably instance. The handle is available under .jobs, so this example is still incorrect as written. As per coding guidelines, "Update packages/durably/docs/llms.md when API changes are made to keep it in sync with the codebase".

✏️ Proposed doc fix
-const durably = createDurably({ dialect })
-const { syncUsers } = durably.register({
+const durablyLater = createDurably({ dialect }).register({
   syncUsers: syncUsersJob,
 })
+const { syncUsers } = durablyLater.jobs
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably/docs/llms.md` around lines 47 - 50, The example incorrectly
destructures syncUsers from register() — update the docs to show that register
returns a Durably instance and that job handles live under the .jobs property:
after creating durably with createDurably({ dialect }) call durably.register({
syncUsers: syncUsersJob }) and then read the handle from durably.jobs.syncUsers
(or const { syncUsers } = durably.jobs) so the example references createDurably,
register, .jobs, and syncUsersJob correctly.
website/public/llms.txt (2)

32-50: ⚠️ Potential issue | 🟡 Minor

The duplicate const durably declaration remains unfixed.

This issue was previously identified. The two initialization options are shown in the same scope with identical variable names, which is a syntax error.

🛠️ Proposed fix
 // Option 2: Without jobs (register later)
-const durably = createDurably({ dialect })
-const { syncUsers } = durably.register({
+const durablyLater = createDurably({ dialect })
+const { syncUsers } = durablyLater.register({
   syncUsers: syncUsersJob,
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/public/llms.txt` around lines 32 - 50, The snippet declares const
durably twice which is a syntax error; fix by ensuring only one binding is
used—either show the two options separately or rename the second example
variable. For example, keep the first example as const durably =
createDurably({... , jobs: { syncUsers: syncUsersJob }}), and in the second
example rename the variable or show it as a separate isolated snippet (e.g.,
const durablyNoJobs = createDurably({ dialect }) and then const { syncUsers } =
durablyNoJobs.register({ syncUsers: syncUsersJob })); update references to
durably accordingly and ensure createDurably, syncUsersJob, and register remain
the same.

670-678: ⚠️ Potential issue | 🟡 Minor

The type/value name collision remains unfixed.

This issue was previously identified. While TypeScript allows a type and value to share the same name (separate namespaces), it creates confusion in documentation.

🛠️ Proposed docs refactor
-import type { durably } from '~/lib/durably.server'
+import type { durably as ServerDurably } from '~/lib/durably.server'
 import { createDurably } from '@coji/durably-react'
 
-export const durably = createDurably<typeof durably>({
+export const durably = createDurably<typeof ServerDurably>({
   api: '/api/durably',
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/public/llms.txt` around lines 670 - 678, The exported constant name
collides with the imported type name "durably" (type { durably } from
'~/lib/durably.server') which causes confusion in docs; rename the runtime
export (currently "export const durably = createDurably...") to a distinct
identifier such as "durablyClient" or "createDurablyClient", update the call
site that invokes createDurably (symbol: createDurably) accordingly, and if
desired alias the imported type (e.g., import type { durably as DurablyType })
to keep clear separation between the type and value namespaces in documentation
and examples.
🧹 Nitpick comments (1)
website/api/durably-react/fullstack.md (1)

22-28: Clear delineation needed: This is server-side code.

The code imports from @coji/durably (server package) and uses the server-side createDurably with a jobs parameter. This section documents server setup, not client hook creation. However, the same function name (createDurably) exists in both @coji/durably and @coji/durably-react, which could confuse readers.

Consider adding a comment in the example to clarify:

📝 Suggested clarification
 // app/lib/durably.server.ts
 import { createDurably, createDurablyHandler } from '@coji/durably'
 import { LibsqlDialect } from '@libsql/kysely-libsql'
 import { createClient } from '@libsql/client'
 
 const client = createClient({ url: 'file:local.db' })
 const dialect = new LibsqlDialect({ client })
 
+// Server-side Durably instance with job registration
 export const durably = createDurably({
   dialect,
   jobs: {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/api/durably-react/fullstack.md` around lines 22 - 28, The example
using createDurably to export durably with a jobs map (importCsvJob,
syncUsersJob) is server-only and can be confused with the similarly named client
function in `@coji/durably-react`; update the snippet to include a short inline
comment above the code clarifying this is server-side setup using createDurably
from `@coji/durably` (not `@coji/durably-react`) and mention that client-side usage
uses the hook from `@coji/durably-react` instead; reference the exported symbol
durably and the jobs keys importCsv and syncUsers (or importCsvJob,
syncUsersJob) so readers know which identifiers are server-only.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude/skills/doc-check/SKILL.md:
- Around line 21-30: Step 2's example uses raw grep which isn't permitted by
this skill's frontmatter allowlist (it currently only allows Bash(pnpm:*),
Bash(node:*), and Bash(git:*)); either add Bash(grep:*) to the SKILL.md
frontmatter allowlist or rewrite the Step 2 text to use the skill's native Grep
tool syntax (replace the raw `grep` example with a Grep-tool invocation and
example search patterns), and update the wording in the "Step 2: Grep for Old
Patterns" section accordingly so the example matches the allowed tools.

---

Outside diff comments:
In `@packages/durably/README.md`:
- Around line 19-33: The README quick-start snippet references an undeclared
variable `dialect`; update the code block so it is self-contained by adding a
minimal placeholder or import for `dialect` used when calling `createDurably({
dialect, jobs: { myJob } })`. Specifically, add a short declaration (e.g., a
comment or dummy value) for `dialect` near the top of the example before
`createDurably` so readers can copy-paste and run the snippet; ensure the
identifiers `createDurably`, `defineJob`, `myJob`, and `dialect` are
consistently referenced and documented in the block.

In `@website/.vitepress/config.ts`:
- Around line 183-241: The SPA sidebar is missing the useRunActions entry; add
an item for useRunActions to the SPA Hooks items array (mirroring the Fullstack
entry) with text 'useRunActions' and link '/api/durably-react/spa#userunactions'
so it appears under the SPA Hooks section, and also update the SPA doc
(api/durably-react/spa.md) to include the useRunActions documentation (exported
symbol: useRunActions) so the sidebar link has a matching anchor target.

In `@website/api/durably-react/spa.md`:
- Around line 61-72: The component incorrectly destructures isReady and error
from useDurably (it only returns { durably }) and also uses await in the
component body; fix by only extracting durably via const { durably } =
useDurably() and then either (A) replace the direct call with the provided
useRuns hook to get runs, loading and error state (useRuns() returns
runs/isLoading/error), or (B) if you need raw durably, move the async call
durably.storage.getRuns() into a useEffect or an event handler, set local state
for runs/loading/error, and avoid top-level await in the Component; update
references to runs, loading/isReady and error accordingly.

In `@website/guide/csv-import.md`:
- Around line 218-223: The snippet imports useRuns and useRunActions directly
from `@coji/durably-react` but should use the proxy client created earlier;
replace direct imports and calls so the component calls
durablyClient.useRuns(...) and durablyClient.useRunActions(...) (referencing the
existing durablyClient instance) instead of useRuns(...) and useRunActions(...),
and pass the same options ({ api: '/api/durably' }) to those client methods to
preserve behavior.

---

Duplicate comments:
In `@packages/durably/docs/llms.md`:
- Around line 47-50: The example incorrectly destructures syncUsers from
register() — update the docs to show that register returns a Durably instance
and that job handles live under the .jobs property: after creating durably with
createDurably({ dialect }) call durably.register({ syncUsers: syncUsersJob })
and then read the handle from durably.jobs.syncUsers (or const { syncUsers } =
durably.jobs) so the example references createDurably, register, .jobs, and
syncUsersJob correctly.

In `@website/public/llms.txt`:
- Around line 32-50: The snippet declares const durably twice which is a syntax
error; fix by ensuring only one binding is used—either show the two options
separately or rename the second example variable. For example, keep the first
example as const durably = createDurably({... , jobs: { syncUsers: syncUsersJob
}}), and in the second example rename the variable or show it as a separate
isolated snippet (e.g., const durablyNoJobs = createDurably({ dialect }) and
then const { syncUsers } = durablyNoJobs.register({ syncUsers: syncUsersJob }));
update references to durably accordingly and ensure createDurably, syncUsersJob,
and register remain the same.
- Around line 670-678: The exported constant name collides with the imported
type name "durably" (type { durably } from '~/lib/durably.server') which causes
confusion in docs; rename the runtime export (currently "export const durably =
createDurably...") to a distinct identifier such as "durablyClient" or
"createDurablyClient", update the call site that invokes createDurably (symbol:
createDurably) accordingly, and if desired alias the imported type (e.g., import
type { durably as DurablyType }) to keep clear separation between the type and
value namespaces in documentation and examples.

---

Nitpick comments:
In `@website/api/durably-react/fullstack.md`:
- Around line 22-28: The example using createDurably to export durably with a
jobs map (importCsvJob, syncUsersJob) is server-only and can be confused with
the similarly named client function in `@coji/durably-react`; update the snippet
to include a short inline comment above the code clarifying this is server-side
setup using createDurably from `@coji/durably` (not `@coji/durably-react`) and
mention that client-side usage uses the hook from `@coji/durably-react` instead;
reference the exported symbol durably and the jobs keys importCsv and syncUsers
(or importCsvJob, syncUsersJob) so readers know which identifiers are
server-only.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1e33641f-a8a7-4f77-afda-df08557b07de

📥 Commits

Reviewing files that changed from the base of the PR and between 410cdc0 and 9579325.

📒 Files selected for processing (20)
  • .claude/skills/doc-check/SKILL.md
  • .claude/skills/release-check/SKILL.md
  • CLAUDE.md
  • examples/fullstack-react-router/app/lib/durably.ts
  • examples/fullstack-react-router/app/routes/_index/dashboard.tsx
  • examples/fullstack-react-router/app/routes/_index/data-sync-progress.tsx
  • examples/fullstack-react-router/app/routes/_index/image-processing-progress.tsx
  • packages/durably/README.md
  • packages/durably/docs/llms.md
  • website/.vitepress/config.ts
  • website/api/durably-react/fullstack.md
  • website/api/durably-react/index.md
  • website/api/durably-react/spa.md
  • website/api/index.md
  • website/guide/background-sync.md
  • website/guide/concepts.md
  • website/guide/csv-import.md
  • website/guide/getting-started.md
  • website/guide/offline-app.md
  • website/public/llms.txt
✅ Files skipped from review due to trivial changes (1)
  • CLAUDE.md

Comment thread .claude/skills/doc-check/SKILL.md Outdated
@coji coji changed the title feat: redesign @coji/durably-react API — createDurably, fullstack-first feat: redesign @coji/durably-react — createDurably, fullstack-first, automated doc checks Mar 6, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
.claude/skills/doc-check/scripts/find-stale.sh (1)

17-21: Remove unused DIM variable.

The DIM variable is defined but never used in the script.

🧹 Proposed fix
 RED='\033[0;31m'
 GREEN='\033[0;32m'
 YELLOW='\033[1;33m'
-DIM='\033[2m'
 NC='\033[0m'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/doc-check/scripts/find-stale.sh around lines 17 - 21, Remove
the unused DIM variable from the color definitions in the find-stale.sh script:
delete the line that defines DIM='\033[2m' (leaving RED, GREEN, YELLOW, NC
intact) so there are no unused variables; ensure no other references to DIM
exist elsewhere in the script and run a quick shellcheck/lint to confirm.
.claude/skills/release-check/SKILL.md (1)

10-10: Consider security implications of permissive script execution.

The Bash(./*) permission allows execution of any script in the repository. While this provides flexibility for the release-check workflow, consider whether it should be more restrictive (e.g., Bash(./.claude/*) to limit to the .claude directory only) to prevent accidental execution of untrusted scripts.

🔒 More restrictive alternative
- - Bash(./*)
+ - Bash(./.claude/*)

This would limit script execution to the .claude/ directory where your workflow scripts are located.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/release-check/SKILL.md at line 10, The permission entry
Bash(./*) is too permissive and allows execution of any repo script; change the
permission to a restrictive pattern such as Bash(./.claude/*) (or another vetted
subdirectory) so only workflow-owned scripts run. Locate the permission string
"Bash(./*)" in the release-check skill definition and replace it with the more
restrictive pattern (or document and validate any additional allowed paths)
while keeping the rest of the workflow unchanged.
.claude/skills/doc-check/SKILL.md (2)

148-148: Consider a more specific placeholder name.

The example uses typeof server as a placeholder, but the PR objectives and other examples in the codebase use typeof serverDurably, which more clearly indicates that this refers to the server-side Durably instance rather than a generic server export.

📝 Suggested refinement
-| Fullstack client   | `createDurably<typeof server>({})` | raw `useJob({ api, jobName })`   |
+| Fullstack client   | `createDurably<typeof serverDurably>({})` | raw `useJob({ api, jobName })`   |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/doc-check/SKILL.md at line 148, Update the placeholder type
in the example row so it specifically references the Durably server export:
replace the generic `typeof server` placeholder used with `createDurably<typeof
server>` by the clearer `typeof serverDurably` (so use `createDurably<typeof
serverDurably>({...})`), keeping the rest of the example (`raw useJob({ api,
jobName })`) unchanged; this targets the example around the createDurably helper
and the server export name to make it explicit that the Durably server instance
is intended.

96-96: Standardize "Fullstack" capitalization for consistency.

Line 96 uses lowercase "fullstack" while Lines 115 and 148 capitalize "Fullstack" when referring to the mode or client type. For consistency with how "SPA mode" is treated (always caps as an acronym), "Fullstack" should be consistently capitalized when used as a mode name or technical term.

📝 Suggested fix
-| `website/guide/csv-import.md`      | Complete fullstack example          |
+| `website/guide/csv-import.md`      | Complete Fullstack example          |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/doc-check/SKILL.md at line 96, The table entry using
"fullstack" should be capitalized to match the rest of the doc; update the
occurrence of "fullstack" (the table cell text '`website/guide/csv-import.md`   
| Complete fullstack example') to "Fullstack" so it reads "Complete Fullstack
example" and ensure other mentions use the same "Fullstack" capitalization to
match terms like "SPA mode".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/durably-react/README.md`:
- Around line 22-28: The README examples use a self-referential type import
causing TypeScript errors: replace the type-only import with a value import so
the type can be derived from the runtime export; specifically change the import
from `import type { durably } from './durably.server'` to a value import (so
`durably` is the actual exported server instance) and then call
`createDurably<typeof durably>({...})` using that value; update both usages
referenced (the example using `createDurably` and the `durably` identifier in
docs/llms.md) so `durably` refers to the exported server instance rather than a
type-only import.

---

Nitpick comments:
In @.claude/skills/doc-check/scripts/find-stale.sh:
- Around line 17-21: Remove the unused DIM variable from the color definitions
in the find-stale.sh script: delete the line that defines DIM='\033[2m' (leaving
RED, GREEN, YELLOW, NC intact) so there are no unused variables; ensure no other
references to DIM exist elsewhere in the script and run a quick shellcheck/lint
to confirm.

In @.claude/skills/doc-check/SKILL.md:
- Line 148: Update the placeholder type in the example row so it specifically
references the Durably server export: replace the generic `typeof server`
placeholder used with `createDurably<typeof server>` by the clearer `typeof
serverDurably` (so use `createDurably<typeof serverDurably>({...})`), keeping
the rest of the example (`raw useJob({ api, jobName })`) unchanged; this targets
the example around the createDurably helper and the server export name to make
it explicit that the Durably server instance is intended.
- Line 96: The table entry using "fullstack" should be capitalized to match the
rest of the doc; update the occurrence of "fullstack" (the table cell text
'`website/guide/csv-import.md`      | Complete fullstack example') to
"Fullstack" so it reads "Complete Fullstack example" and ensure other mentions
use the same "Fullstack" capitalization to match terms like "SPA mode".

In @.claude/skills/release-check/SKILL.md:
- Line 10: The permission entry Bash(./*) is too permissive and allows execution
of any repo script; change the permission to a restrictive pattern such as
Bash(./.claude/*) (or another vetted subdirectory) so only workflow-owned
scripts run. Locate the permission string "Bash(./*)" in the release-check skill
definition and replace it with the more restrictive pattern (or document and
validate any additional allowed paths) while keeping the rest of the workflow
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 927da913-6bdb-409b-839f-33ddbd1b56e4

📥 Commits

Reviewing files that changed from the base of the PR and between 9579325 and c6ed61c.

📒 Files selected for processing (4)
  • .claude/skills/doc-check/SKILL.md
  • .claude/skills/doc-check/scripts/find-stale.sh
  • .claude/skills/release-check/SKILL.md
  • packages/durably-react/README.md

Comment on lines +22 to +28
import { createDurably } from '@coji/durably-react'
import type { durably } from './durably.server'

// Create type-safe hooks from server's Durably type
export const durably = createDurably<typeof durably>({
api: '/api/durably',
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's verify the actual content of the README at the specified lines
cat -n packages/durably-react/README.md | sed -n '20,35p'

Repository: coji/durably

Length of output: 610


🏁 Script executed:

# Search for the problematic self-referential pattern in the codebase
rg -n "createDurably<\s*typeof\s+durably\s*>" packages/durably-react examples docs --no-heading 2>/dev/null || echo "Pattern not found (as expected if comment is about fixing it)"

Repository: coji/durably

Length of output: 240


🏁 Script executed:

# Check the actual import pattern in the README
rg -n "import type.*durably.*from.*durably\.server" packages/durably-react/README.md -A 5 -B 2

Repository: coji/durably

Length of output: 323


🏁 Script executed:

# Look at the createDurably client implementation to understand the proper typing pattern
cat -n packages/durably-react/src/client/create-durably.ts | head -30

Repository: coji/durably

Length of output: 1013


🏁 Script executed:

# Check if there are any example files demonstrating the proper typing pattern
fd -e tsx -e ts examples | xargs grep -l "createDurably" 2>/dev/null | head -3

Repository: coji/durably

Length of output: 38


Fix the self-referential typing in the fullstack quick-start example; it appears in README and docs.

The pattern createDurably<typeof durably>({...}) combined with import type { durably } is self-referential and will fail TypeScript type-checking. Using import type imports only the type, making typeof durably unable to resolve the value. This occurs in:

  • packages/durably-react/README.md:26
  • packages/durably-react/docs/llms.md:56

Use direct import syntax to reference the server instance:

Fix
 import { createDurably } from '@coji/durably-react'
-import type { durably } from './durably.server'
+import type { durably } from './durably.server'  // or: type ServerDurably = typeof import('./durably.server').durably
 
 // Create type-safe hooks from server's Durably type
-export const durably = createDurably<typeof durably>({
+export const durably = createDurably<typeof import('./durably.server').durably>({
   api: '/api/durably',
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createDurably } from '@coji/durably-react'
import type { durably } from './durably.server'
// Create type-safe hooks from server's Durably type
export const durably = createDurably<typeof durably>({
api: '/api/durably',
})
import { createDurably } from '@coji/durably-react'
import type { durably } from './durably.server'
// Create type-safe hooks from server's Durably type
export const durably = createDurably<typeof import('./durably.server').durably>({
api: '/api/durably',
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably-react/README.md` around lines 22 - 28, The README examples
use a self-referential type import causing TypeScript errors: replace the
type-only import with a value import so the type can be derived from the runtime
export; specifically change the import from `import type { durably } from
'./durably.server'` to a value import (so `durably` is the actual exported
server instance) and then call `createDurably<typeof durably>({...})` using that
value; update both usages referenced (the example using `createDurably` and the
`durably` identifier in docs/llms.md) so `durably` refers to the exported server
instance rather than a type-only import.

- Add missing `import { z } from 'zod'` to useRuns examples in llms.md
- Document `useRuns`/`useRunActions` as reserved proxy keys in:
  - source code comment
  - fullstack.md (tip box)
  - llms.md (cross-job hooks example + note)
- Regenerate llms.txt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coji coji merged commit b369860 into main Mar 6, 2026
4 checks passed
@coji coji deleted the feat/api-redesign branch March 6, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: API redesign - import paths, naming, and DX improvements docs: rename Browser/Server Hooks to Local/Client Mode

1 participant