feat: redesign @coji/durably-react — createDurably, fullstack-first, automated doc checks#82
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
- 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>
There was a problem hiding this comment.
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 | 🟡 MinorUpdate 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../../srchere.♻️ 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
durablyshadows the imported typetypeof 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 likedurablyHooks.📝 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 addcommand only installs@coji/durably-react. For fullstack mode, developers also need the server-side@coji/durablypackage. 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.mdaround 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-reactand
the server needs@coji/durably(reference the package names@coji/durably-react
and@coji/durablyfrom 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 onRun/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 directorieshooks/andclient/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
durablyshadows the imported
type referencetypeof 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 usedurablyHooks, 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 variabledurably
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-reactand the server needs@coji/durably(reference
the package names@coji/durably-reactand@coji/durablyfrom 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 -->
| import { useRuns, TypedClientRun } from '@coji/durably-react' | ||
| import { defineJob } from '@coji/durably' |
There was a problem hiding this comment.
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.
| export type DurablyHooks<TJobs extends Record<string, unknown>> = { | ||
| [K in keyof TJobs]: JobHooks<InferInput<TJobs[K]>, InferOutput<TJobs[K]>> |
There was a problem hiding this comment.
🧩 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.tsRepository: 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.
There was a problem hiding this comment.
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 | 🟠 MajorReject pending
triggerAndWait()calls on unmount.The polling loop in
triggerAndWaithas a reliability leak. If the component unmounts while waiting for a job to complete, thesetIntervalcontinues running indefinitely becausesubscriptionRef.currentstops updating after unmount (whenuse-subscription.tsunsubscribes). The loop never finds a terminal status, soclearIntervalis 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 | 🟠 MajorExtract
jobsbefore mapping the generic.The example documents
createDurablyHooks<typeof durably>(...), butDurablyHooks<TJobs>currently maps overkeyof typeof durably, so consumers get hooks for instance members likeinit/dbinstead ofimportCsv/syncUsers. Accept the full Durably instance shape and extract itsjobsrecord 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
📒 Files selected for processing (5)
packages/durably-react/src/client/create-durably-hooks.tspackages/durably-react/src/client/index.tspackages/durably-react/src/client/use-job.tspackages/durably-react/src/index.tspackages/durably/src/durably.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/durably-react/src/client/index.ts
…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>
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>
There was a problem hiding this comment.
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 | 🟠 MajorPin
triggerAndWait()to the run it started.
subscriptionRef.currentalways follows the latestcurrentRunId. Because Lines 176-208 can switchcurrentRunIdto a newer run for the same job, this promise can resolve/reject from that other run while still returning the originalrunId. Please keep a dedicated wait target for the started run, or suppressfollowLatesthandoffs 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 | 🟡 MinorUpdate 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 | 🟡 MinorMove 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 publicgetRuns()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 | 🟡 MinorAdd the missing
zodimport to thisuseRunsexample.This snippet uses
z.object(...)but never importsz, 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 | 🟡 MinorPrefer
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 useinit()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 | 🟡 MinorThese
useRunssnippets still misszimports.Both examples call
z.object(...)without importingz, 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 theExtractJobs<T extends { jobs: ... }>path used bytypeof 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-reactsource code and examples useimport 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
📒 Files selected for processing (23)
.claude/skills/doc-check/SKILL.md.claude/skills/release-check/SKILL.mdexamples/fullstack-react-router/app/lib/durably.hooks.tsexamples/fullstack-react-router/app/routes/_index/dashboard.tsxexamples/fullstack-react-router/app/routes/_index/data-sync-progress.tsxexamples/fullstack-react-router/app/routes/_index/image-processing-progress.tsxpackages/durably-react/README.mdpackages/durably-react/docs/llms.mdpackages/durably-react/src/client/create-durably.tspackages/durably-react/src/client/index.tspackages/durably-react/src/client/use-job.tspackages/durably-react/src/index.tspackages/durably-react/tests/client/create-durably.test.tsxwebsite/.vitepress/config.tswebsite/api/create-durably.mdwebsite/api/durably-react/fullstack.mdwebsite/api/durably-react/index.mdwebsite/api/durably-react/spa.mdwebsite/api/durably-react/types.mdwebsite/api/index.mdwebsite/guide/csv-import.mdwebsite/guide/getting-started.mdwebsite/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
| function createDurably<TLabels, TJobs>( | ||
| options: DurablyOptions<TLabels, TJobs> & { jobs: TJobs }, | ||
| ): Durably<TransformToHandles<TJobs, TLabels>, TLabels> |
There was a problem hiding this comment.
🧩 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 -B4Repository: 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.
| 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.
| // 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, | ||
| }) |
There was a problem hiding this comment.
🧩 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 -20Repository: coji/durably
Length of output: 236
🏁 Script executed:
head -60 website/public/llms.txt | tail -30Repository: 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.mdRepository: coji/durably
Length of output: 823
🏁 Script executed:
# Check the generate script to understand relationship
head -50 website/scripts/generate-llms.jsRepository: 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.mdRepository: 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.
| // 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.
| import type { durably } from '~/lib/durably.server' | ||
| import { createDurably } from '@coji/durably-react' | ||
|
|
||
| export const durably = createDurably<typeof durably>({ | ||
| api: '/api/durably', | ||
| }) |
There was a problem hiding this comment.
🧩 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 fRepository: 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.txtRepository: 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 -5Repository: 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.mdRepository: 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.
| 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>
There was a problem hiding this comment.
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 | 🟡 MinorMake the quick-start snippet self-contained.
This now shows the new
jobsoption, butdialectis 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 | 🟡 MinorAdd
useRunActionsto the SPA sidebar.
useRunActionsis 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 | 🟡 MinorUse the proxy client for cross-job hooks here.
This dashboard snippet is still using the old standalone fullstack hooks. With the redesigned API,
useRuns()anduseRunActions()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 | 🔴 CriticalFix useDurably hook signature and async pattern.
Line 65 destructures
isReadyanderrorproperties thatuseDurably()does not return—it only returns{ durably }. Additionally, line 71 usesawaitdirectly in the component body, which is invalid React. Either useuseRuns()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 | 🟡 MinorFix the
register()example to read handles from.jobs.Line 48 currently destructures
syncUsersfrom the return value ofregister(), butregister()returns a new Durably instance. The handle is available under.jobs, so this example is still incorrect as written. As per coding guidelines, "Updatepackages/durably/docs/llms.mdwhen 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 | 🟡 MinorThe duplicate
const durablydeclaration 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 | 🟡 MinorThe 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-sidecreateDurablywith ajobsparameter. This section documents server setup, not client hook creation. However, the same function name (createDurably) exists in both@coji/durablyand@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
📒 Files selected for processing (20)
.claude/skills/doc-check/SKILL.md.claude/skills/release-check/SKILL.mdCLAUDE.mdexamples/fullstack-react-router/app/lib/durably.tsexamples/fullstack-react-router/app/routes/_index/dashboard.tsxexamples/fullstack-react-router/app/routes/_index/data-sync-progress.tsxexamples/fullstack-react-router/app/routes/_index/image-processing-progress.tsxpackages/durably/README.mdpackages/durably/docs/llms.mdwebsite/.vitepress/config.tswebsite/api/durably-react/fullstack.mdwebsite/api/durably-react/index.mdwebsite/api/durably-react/spa.mdwebsite/api/index.mdwebsite/guide/background-sync.mdwebsite/guide/concepts.mdwebsite/guide/csv-import.mdwebsite/guide/getting-started.mdwebsite/guide/offline-app.mdwebsite/public/llms.txt
✅ Files skipped from review due to trivial changes (1)
- CLAUDE.md
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
.claude/skills/doc-check/scripts/find-stale.sh (1)
17-21: Remove unusedDIMvariable.The
DIMvariable 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 serveras a placeholder, but the PR objectives and other examples in the codebase usetypeof 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
📒 Files selected for processing (4)
.claude/skills/doc-check/SKILL.md.claude/skills/doc-check/scripts/find-stale.sh.claude/skills/release-check/SKILL.mdpackages/durably-react/README.md
| 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', | ||
| }) |
There was a problem hiding this comment.
🧩 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 2Repository: 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 -30Repository: 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 -3Repository: 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:26packages/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.
| 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>
Summary
@coji/durably-reactの API を全面リデザイン。tRPC/Convex パターンを参考に DX を大幅改善。API Changes
createDurably— サーバーと同名。apiオプションでクライアントを区別durably.importCsv.useJob(),durably.importCsv.useRun(id)durably.useRuns(),durably.useRunActions()ExtractJobs<T>でcreateDurably<typeof serverDurably>()が動く@coji/durably-react) → Fullstack hooks(旧 SPA)@coji/durably-react/spasubpathjobsoption →createDurably()(core) の 1-step 初期化Documentation Overhaul
init()推奨、jobsオプション推奨、createDurablyパターンbrowser.md→spa.md,client.md→fullstack.mdSkills Overhaul
scripts/find-stale.sh— 古い API パターンを自動検出するスクリプト追加Bug Fixes
triggerAndWaitの interval リーク修正(unmount 時にクリア)Test plan
pnpm validatepasses (22/22 tasks)find-stale.shreports cleanCloses #81, closes #78
🤖 Generated with Claude Code