Skip to content

feat: classify events and add waitForRun polling fallback (#162 step 2/4)#169

Merged
coji merged 4 commits into
mainfrom
feat/classify-events-waitforrun-polling
Mar 28, 2026
Merged

feat: classify events and add waitForRun polling fallback (#162 step 2/4)#169
coji merged 4 commits into
mainfrom
feat/classify-events-waitforrun-polling

Conversation

@coji
Copy link
Copy Markdown
Owner

@coji coji commented Mar 28, 2026

Summary

  • Classify 16 event types into Domain (state transitions: trigger, complete, fail, cancel, coalesced, delete) and Operational (diagnostics: leased, lease-renewed, progress, worker:error, step:*, log:write) with compile-time type helpers and isDomainEvent() type guard
  • Add polling fallback to waitForRunCompletion() for cross-process observation (feat: polling fallback for waitForRun in multi-worker setups #152) — when no in-process event fires, polls store.getRun() at configurable interval (default: pollingIntervalMs from createDurably() config, default 1000ms)
  • Same-process events still resolve immediately (fast path unchanged)

Changes

Area Files What
Event classification events.ts, index.ts DomainEvent, OperationalEvent types, isDomainEvent() guard
Polling fallback job.ts, durably.ts pollingIntervalMs option in WaitForRunOptions and DurablyOptions
Tests wait-for-run.test.ts (new), events.shared.ts, run-api.shared.ts, types.test.ts Cross-process polling, fast path, event classification
Docs llms.md, website/api/, website/guide/, examples Updated API reference and guides

Context

Step 2/4 of #162 (core internal restructuring). Supersedes #152.

Test plan

  • pnpm validate passes (format, lint, typecheck, tests)
  • New wait-for-run.test.ts covers: cross-process detection via polling, same-process fast path, timeout with polling active, custom interval
  • Event classification tests validate isDomainEvent() for all 16 event types
  • CodeRabbit review

🤖 Generated with Claude Code

Summary by CodeRabbit

リリースノート

  • 新機能

    • イベント分類ガイダンス機能を追加。ドメインイベント検出の新しいヘルパー関数を提供。
    • waitForRun()triggerAndWait() に、カスタマイズ可能なストレージポーリング間隔オプションを追加。
  • ドキュメント

    • イベント分類と使用パターンに関するドキュメント拡張。
    • ストレージポーリング動作と設定オプションの説明を追加。
  • テスト

    • クロスランタイムのイベント待機動作テストを追加。
    • イベント分類機能の包括的なテストを追加。

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 28, 2026

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

Project Deployment Actions Updated (UTC)
durably-demo Ready Ready Preview Mar 28, 2026 11:30am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
durably-demo-vercel-turso Ignored Ignored Preview Mar 28, 2026 11:30am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 28, 2026

Warning

Rate limit exceeded

@coji has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 38 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 1 minutes and 38 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 636378f7-be89-454d-b2d2-ce0bbe7b65e1

📥 Commits

Reviewing files that changed from the base of the PR and between 3ff1e9f and 33143a4.

📒 Files selected for processing (1)
  • packages/durably/src/job.ts
📝 Walkthrough

Walkthrough

DurablyEventを「ドメイン」と「オペレーショナル」に分類する型/ランタイム判定(isDomainEvent)を追加し、waitForRun/triggerAndWaitにプロセス外のストレージポーリング用のpollingIntervalMsを導入、関連ドキュメント・例・テストを更新しました。

Changes

Cohort / File(s) Summary
イベント分類の追加
packages/durably/src/events.ts, packages/durably/src/index.ts
DomainEventType/OperationalEventType/DomainEvent/OperationalEventの型を追加。DOMAIN_EVENT_TYPES集合とisDomainEvent(event)型ガードを実装・再エクスポート。
waitForRunポーリングと状態伝搬
packages/durably/src/durably.ts, packages/durably/src/job.ts
DurablyStatepollingIntervalMsを追加。waitForRunCompletionでストレージポーリング(intervalベース)を導入し、WaitForRunOptionspollingIntervalMs?: numberを追加。createJobHandlepollingIntervalMsを渡すよう変更。
例とイベントリスナー更新
examples/server-node-postgres/basic.ts, examples/server-node/basic.ts
isDomainEventをインポートし、durably.on('run:complete', ...)内の出力/期間ログをisDomainEvent(event)で型ナローイングして条件付きで記録するよう変更。
テスト追加/更新
packages/durably/tests/node/wait-for-run.test.ts, packages/durably/tests/shared/run-api.shared.ts, packages/durably/tests/shared/events.shared.ts, packages/durably/tests/node/types.test.ts
クロスランタイムのwait/trigger振る舞い(完了/失敗/キャンセル/削除検知)とpollingIntervalMsの動作を検証する統合テストを追加。isDomainEventの分類と型エクスポートを検証する型テストを追加。
ドキュメント更新
website/api/create-durably.md, website/api/define-job.md, website/api/events.md, website/api/index.md, website/guide/server-mode.md, website/public/llms.txt, packages/durably-react/docs/llms.md, packages/durably/docs/llms.md, website/api/durably-react/types.md
pollingIntervalMsオプションと「コアイベント分類(Domain vs Operational)」の説明、durably-reactのSSEワイヤ形状とコア分類ヘルパーの違いに関する注記を追加。

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RuntimeA as Runtime A
  participant RuntimeB as Runtime B
  participant DB as Storage(DB)

  Client->>RuntimeA: triggerAndWait(runId, {pollingIntervalMs})
  RuntimeA->>RuntimeA: start in-process listener for events
  RuntimeA->>RuntimeB: (no direct call) RuntimeB may lease and process run
  RuntimeB->>DB: lease run / update status (pending -> leased -> completed/failed/cancelled)
  DB-->>RuntimeA: (no push) RuntimeA polls storage at pollingIntervalMs if no terminal event seen
  RuntimeB->>RuntimeA: emit in-process event (e.g., run:complete) -> RuntimeA listeners settle immediately
  alt another process completed run
    RuntimeA->>DB: poll -> sees terminal state -> resolve wait/reject accordingly
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇✨
走るイベントを分けてみたよ、ドメインと式(しき)とで
ポーリングの鼓動は静かに、遠い仲間の終わりを待つ
型ガードでぴょんと跳び越え、安全に分けるよ
小さな変更で世界が少し整う、にんじん一つどうぞ 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed タイトルは主な変更内容をよく反映しており、イベント分類機能とwaitForRunのポーリング機能の追加を明確に示しています。

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/classify-events-waitforrun-polling

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

❤️ Share

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

@coji coji marked this pull request as ready for review March 28, 2026 10:27
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
website/api/events.md (1)

14-22: コード例が冗長 — run:fail は常にドメインイベントです

durably.on('run:fail', ...) のリスナー内で isDomainEvent(event) を呼び出していますが、run:fail はすでにドメインイベントとして定義されているため、この条件分岐は常に true になります。

より実用的な例としては、全イベントを購読するケースや、イベント配列をフィルタリングするケースが考えられます:

📝 より実用的な例の提案
-durably.on('run:fail', (event) => {
-  if (isDomainEvent(event)) {
-    // narrowed to domain union
-  }
-})
+// Filter domain events from a mixed event stream
+const events: DurablyEvent[] = getEventStream()
+const domainEvents = events.filter(isDomainEvent)
+
+// Or use with a catch-all listener pattern
+function handleEvent(event: DurablyEvent) {
+  if (isDomainEvent(event)) {
+    // handle lifecycle events (trigger, complete, fail, cancel, delete, coalesced)
+  } else {
+    // handle operational events (leased, progress, step:*, log:write, worker:error)
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/api/events.md` around lines 14 - 22, 例示コードでは durably.on('run:fail',
...) 内で isDomainEvent(event) を呼んでいますが "run:fail"
は常にドメインイベントなのでそのチェックは不要です。修正案:該当チェックを削除して単純にイベントを処理するコードにするか、より実用的な例に差し替える(全イベント購読例:durably.on('*',
handler) 内で isDomainEvent(event) を使ってフィルタする、あるいはイベント配列を filter(isDomainEvent)
してから処理するなど)。変更箇所の目印は durably.on(...) 呼び出しと isDomainEvent の使用箇所です。
packages/durably/docs/llms.md (1)

356-364: コード例が冗長 — run:complete は常にドメインイベントです

website/api/events.md と同様に、run:complete リスナー内での isDomainEvent() チェックは常に true になります。一貫性のために両方のドキュメントで例を更新することを検討してください。

🤖 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 356 - 364, The example
unnecessarily checks isDomainEvent() inside the durably.on('run:complete', ...)
handler even though 'run:complete' is always a domain event; remove the import
and the conditional so the handler can directly use event.runId. Specifically,
update the code that references isDomainEvent and the durably.on listener (the
import of isDomainEvent, the if-check, and the branch) to a simple handler that
logs event.runId, keeping the durably.on('run:complete', ...) and runId
reference.
packages/durably/src/job.ts (1)

304-325: settleFromStoragecancelledステータス処理について

321-324行目でcancelledステータスの処理にreturn文がありません。これは現在の実装では問題ありませんが(関数の最後なので)、将来のコード追加時に意図しない動作を引き起こす可能性があります。

♻️ 明示的なreturnを追加する提案
       if (run.status === 'cancelled') {
         cleanup()
         reject(new CancelledError(runId))
+        return
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably/src/job.ts` around lines 304 - 325, In settleFromStorage,
after handling the 'cancelled' branch where you call cleanup() and reject(new
CancelledError(runId)), add an explicit return; i.e., ensure the 'cancelled'
case in the settleFromStorage function exits the function just like the other
status branches (use the same pattern as the 'completed'/'failed' branches that
call cleanup() then return) to avoid accidental fall-through when future code is
appended.
packages/durably/tests/node/types.test.ts (1)

178-201: 型ガードの戻り値型アサーションについて

181行目でisDomainEventの戻り値型がbooleanとしてテストされていますが、型ガードは通常 event is DomainEvent という型述語を返します。183-185行目のナローイングテストでは型ガードの動作が正しく検証されていますが、181行目のアサーションは実行時の戻り値型(boolean)をテストしており、TypeScriptの型述語シグネチャではありません。

これは意図的かもしれませんが、より正確にするには型述語の振る舞いのみをテストすることを検討してください(183-185行目で既に行われているように)。

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

In `@packages/durably/tests/node/types.test.ts` around lines 178 - 201, Replace
the incorrect return-type assertion that checks isDomainEvent returns boolean
with an assertion that its function signature is a type predicate; specifically,
update the test referencing isDomainEvent (the parameter/return assertions
around isDomainEvent) to assert the function type equals (e: DurablyEvent) => e
is DomainEvent (or simply remove the .returns.toEqualTypeOf<boolean>() line),
keeping the existing narrowing check (the if (isDomainEvent(e)) block) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/durably/docs/llms.md`:
- Around line 356-364: The example unnecessarily checks isDomainEvent() inside
the durably.on('run:complete', ...) handler even though 'run:complete' is always
a domain event; remove the import and the conditional so the handler can
directly use event.runId. Specifically, update the code that references
isDomainEvent and the durably.on listener (the import of isDomainEvent, the
if-check, and the branch) to a simple handler that logs event.runId, keeping the
durably.on('run:complete', ...) and runId reference.

In `@packages/durably/src/job.ts`:
- Around line 304-325: In settleFromStorage, after handling the 'cancelled'
branch where you call cleanup() and reject(new CancelledError(runId)), add an
explicit return; i.e., ensure the 'cancelled' case in the settleFromStorage
function exits the function just like the other status branches (use the same
pattern as the 'completed'/'failed' branches that call cleanup() then return) to
avoid accidental fall-through when future code is appended.

In `@packages/durably/tests/node/types.test.ts`:
- Around line 178-201: Replace the incorrect return-type assertion that checks
isDomainEvent returns boolean with an assertion that its function signature is a
type predicate; specifically, update the test referencing isDomainEvent (the
parameter/return assertions around isDomainEvent) to assert the function type
equals (e: DurablyEvent) => e is DomainEvent (or simply remove the
.returns.toEqualTypeOf<boolean>() line), keeping the existing narrowing check
(the if (isDomainEvent(e)) block) intact.

In `@website/api/events.md`:
- Around line 14-22: 例示コードでは durably.on('run:fail', ...) 内で isDomainEvent(event)
を呼んでいますが "run:fail"
は常にドメインイベントなのでそのチェックは不要です。修正案:該当チェックを削除して単純にイベントを処理するコードにするか、より実用的な例に差し替える(全イベント購読例:durably.on('*',
handler) 内で isDomainEvent(event) を使ってフィルタする、あるいはイベント配列を filter(isDomainEvent)
してから処理するなど)。変更箇所の目印は durably.on(...) 呼び出しと isDomainEvent の使用箇所です。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 107893e8-ee3a-403c-abd8-cf3935197986

📥 Commits

Reviewing files that changed from the base of the PR and between 3db5f9e and c6bfabd.

⛔ Files ignored due to path filters (1)
  • packages/durably-react/.vitest-attachments/29a9f1aed8057f830dcb6e50abcda0377e0b2912.png is excluded by !**/*.png
📒 Files selected for processing (19)
  • examples/server-node-postgres/basic.ts
  • examples/server-node/basic.ts
  • packages/durably-react/docs/llms.md
  • packages/durably/docs/llms.md
  • packages/durably/src/durably.ts
  • packages/durably/src/events.ts
  • packages/durably/src/index.ts
  • packages/durably/src/job.ts
  • packages/durably/tests/node/types.test.ts
  • packages/durably/tests/node/wait-for-run.test.ts
  • packages/durably/tests/shared/events.shared.ts
  • packages/durably/tests/shared/run-api.shared.ts
  • website/api/create-durably.md
  • website/api/define-job.md
  • website/api/durably-react/types.md
  • website/api/events.md
  • website/api/index.md
  • website/guide/server-mode.md
  • website/public/llms.txt

@coji coji changed the title classify-events-waitforrun-polling feat: classify events and add waitForRun polling fallback (#162 step 2/4) Mar 28, 2026
- Fix isDomainEvent() examples in docs to show practical usage with
  DurablyEvent type instead of redundant check inside typed handler
- Add explicit return after cancelled case in settleFromStorage
- Remove redundant boolean return type assertion for isDomainEvent
  type predicate (narrowing test already covers this)
- Regenerate llms.txt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/durably/src/job.ts (1)

12-13: 1000 の既定値は 1 箇所に寄せたいです。

packages/durably/src/durably.ts:87-92 にも同じ既定値があるので、ここに別定数を置くと片方だけ更新されたときに createDurably() 経由と waitForRunCompletion() 直呼び出しで挙動がズレます。共有定数を切り出すか、この helper には常に解決済みの値を渡す形にしておく方が安全です。

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

In `@packages/durably/src/job.ts` around lines 12 - 13,
DEFAULT_WAIT_POLLING_INTERVAL_MS is duplicated (also defined in createDurably)
causing divergent behavior; unify the value by exporting a single shared
constant and using it everywhere or by changing callers to pass an
already-resolved pollingIntervalMs into waitForRunCompletion. Concretely, remove
the local DEFAULT_WAIT_POLLING_INTERVAL_MS, add a single exported constant
(e.g., WAIT_POLLING_INTERVAL_MS) in a shared module and update createDurably and
waitForRunCompletion to import and use that constant (or alter
waitForRunCompletion to require pollingIntervalMs as an argument and update
createDurably to pass its resolved value).
🤖 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/src/job.ts`:
- Around line 111-115: Validate the pollingIntervalMs option (ensure it's a
positive, finite number and fall back to a safe default or throw) before passing
it to setInterval; do not call setInterval with 0, negative, NaN, or Infinity.
Add an in-flight guard (e.g., a pollInFlight boolean) around the getRun()
polling call: check pollInFlight and skip starting a new getRun() if true, set
pollInFlight = true before awaiting getRun(), and reset pollInFlight = false in
a finally block to guarantee cleanup; update references in the polling logic
that currently call setInterval(...) and invoke getRun() so they use this
validation and the pollInFlight guard.

---

Nitpick comments:
In `@packages/durably/src/job.ts`:
- Around line 12-13: DEFAULT_WAIT_POLLING_INTERVAL_MS is duplicated (also
defined in createDurably) causing divergent behavior; unify the value by
exporting a single shared constant and using it everywhere or by changing
callers to pass an already-resolved pollingIntervalMs into waitForRunCompletion.
Concretely, remove the local DEFAULT_WAIT_POLLING_INTERVAL_MS, add a single
exported constant (e.g., WAIT_POLLING_INTERVAL_MS) in a shared module and update
createDurably and waitForRunCompletion to import and use that constant (or alter
waitForRunCompletion to require pollingIntervalMs as an argument and update
createDurably to pass its resolved value).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6d144caa-9f77-4add-8b12-1b92a3e15a77

📥 Commits

Reviewing files that changed from the base of the PR and between c6bfabd and 3ff1e9f.

📒 Files selected for processing (5)
  • packages/durably/docs/llms.md
  • packages/durably/src/job.ts
  • packages/durably/tests/node/types.test.ts
  • website/api/events.md
  • website/public/llms.txt
✅ Files skipped from review due to trivial changes (1)
  • packages/durably/docs/llms.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • website/api/events.md
  • packages/durably/tests/node/types.test.ts
  • website/public/llms.txt

Comment thread packages/durably/src/job.ts
- Validate pollingIntervalMs is a positive finite number
- Add pollInFlight guard to prevent concurrent getRun() requests when
  storage responses are slower than the polling interval

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coji coji merged commit 9b2629a into main Mar 28, 2026
5 checks passed
@coji coji deleted the feat/classify-events-waitforrun-polling branch March 28, 2026 11:31
coji added a commit that referenced this pull request Mar 28, 2026
- Add step 5 to spec-review: check for invalid input tests on new public
  API options, async in-flight guards, doc example correctness, and
  explicit behavior preservation criteria
- Add matching items to spec-draft quality checklist
- Remove remaining vitest-attachments PNG

Learned from #169 CodeRabbit review: pollingIntervalMs lacked input
validation and in-flight guard, isDomainEvent examples were redundant.
These should have been caught at spec stage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
coji added a commit that referenced this pull request Mar 28, 2026
…171)

- Add step 5 to spec-review: check for invalid input tests on new public
  API options, async in-flight guards, doc example correctness, and
  explicit behavior preservation criteria
- Add matching items to spec-draft quality checklist
- Remove remaining vitest-attachments PNG

Learned from #169 CodeRabbit review: pollingIntervalMs lacked input
validation and in-flight guard, isDomainEvent examples were redundant.
These should have been caught at spec stage.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant