refactor(workspaces): apply realtime item deltas - #774
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
React Doctor found 2 new issues in 2 files · 1 error & 1 warning · score 49 / 100 (Critical) · 2 fixed · vs Errors
1 warning
Reviewed by React Doctor for commit |
📝 WalkthroughWalkthroughWorkspace synchronization now uses revisioned workspace-page deltas. Persistence publishes canonical item changes, realtime clients apply them to cached pages, and refresh events trigger refetches. Workspace item facts and related count fields are removed from workspace data shapes. ChangesWorkspace page synchronization
AI service-tier observability
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Persistence
participant WorkspaceRoom
participant RealtimeHook
participant WorkspaceLayout
participant QueryCache
Persistence->>WorkspaceRoom: Publish WorkspacePageChange
WorkspaceRoom->>RealtimeHook: Deliver typed page change
RealtimeHook->>WorkspaceLayout: Route delta or desync
WorkspaceLayout->>QueryCache: Apply delta or invalidate page query
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 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 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 48106c5272
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (message.type !== "presence.snapshot" && message.workspaceId === workspaceId) { | ||
| if (message.revision <= cachedRevisionRef.current) { | ||
| return; | ||
| } | ||
|
|
||
| cachedRevisionRef.current = message.revision; |
There was a problem hiding this comment.
Recover when realtime deltas arrive out of revision order
When concurrent workspace mutations commit revisions in order but their post-commit room notifications arrive in the opposite order, accepting the higher revision here and then discarding the lower one permanently loses a non-cumulative create, move, rename, or delete delta. The affected client remains on an incomplete workspace page until a reconnect or unrelated full refresh, so notification delivery must be ordered or receipt of a late lower revision must trigger reconciliation rather than silently ignoring it.
Useful? React with 👍 / 👎.
| if (hasConnectedRef.current) { | ||
| onDesyncRef.current?.(); | ||
| } | ||
| hasConnectedRef.current = true; |
There was a problem hiding this comment.
Reconcile the page on the initial realtime connection
The page snapshot is fetched before this component opens its WebSocket, so a mutation committed after that fetch but before the first onOpen is neither in the snapshot nor delivered over the not-yet-connected socket. Because desync handling is skipped on the first connection, the client can remain stale indefinitely; the initial open also needs an authoritative reconciliation or a server-provided revision handshake.
Useful? React with 👍 / 👎.
| queryClient.setQueryData<WorkspacePage>(workspacePageQueryKey(workspaceId), (current) => | ||
| current | ||
| ? items.reduce((page, item) => upsertWorkspaceItemInPage(page, item, revision), current) | ||
| : current, |
There was a problem hiding this comment.
Prevent stale command results from overwriting newer items
When two mutations for the same item overlap—for example, repeated color changes on a slow connection or rapid drag moves—the newer command response or realtime delta can update the cache first, after which an older command response is unconditionally upserted here. upsertWorkspaceItemInPage preserves the higher page revision but still replaces the item with the older summary, leaving the cache marked current while showing stale canonical state; command-result updates need per-item revision ordering or reconciliation when their revision trails the cached page.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/features/workspaces/cache-page.ts`:
- Around line 47-67: Update upsertWorkspaceItemsInPageCache and
removeWorkspaceItemsFromPageCache to ignore mutation results when a supplied
revision is less than or equal to current.revision, matching
applyWorkspacePageDelta. Keep the revision-less removal behavior intact for
optimistic updates, and only apply versioned changes when they are newer.
In `@src/features/workspaces/use-workspace-kernel-items.ts`:
- Around line 141-148: Update the color commit flow around
upsertWorkspaceItemsInPageCache to track the latest color commit for each item
and ignore completion handlers whose commit has been superseded by a newer
optimistic update. Preserve applying results for the latest commit, and add a
race test covering request A resolving after request B updates the cache.
🪄 Autofix
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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 375bc40d-fa7b-49ab-948b-28c3e9d79fe3
📒 Files selected for processing (30)
docs/concepts/workspaces.mdxsrc/features/workspaces/cache-page.tssrc/features/workspaces/cache-workspace.tssrc/features/workspaces/components/WorkspaceFileUploadProvider.tsxsrc/features/workspaces/components/WorkspaceLayout.tsxsrc/features/workspaces/components/WorkspacePageRoute.tsxsrc/features/workspaces/contracts.tssrc/features/workspaces/kernel/workspace-kernel-access.tssrc/features/workspaces/kernel/workspace-kernel-list.tssrc/features/workspaces/kernel/workspace-kernel.tssrc/features/workspaces/model/workspace-ai-context-outline.tssrc/features/workspaces/model/workspace-ai-context-prompt.tssrc/features/workspaces/model/workspace-ai-context-types.tssrc/features/workspaces/model/workspace-ai-context-validation.tssrc/features/workspaces/model/workspace-page.test.tssrc/features/workspaces/model/workspace-page.tssrc/features/workspaces/operations/workspace-tool-schemas.tssrc/features/workspaces/persistence/workspace-postgres-documents.tssrc/features/workspaces/persistence/workspace-postgres-files.tssrc/features/workspaces/persistence/workspace-postgres-persistence.tssrc/features/workspaces/persistence/workspace-postgres-support.tssrc/features/workspaces/query-options.tssrc/features/workspaces/realtime/messages.tssrc/features/workspaces/realtime/use-workspace-presence.tssrc/features/workspaces/realtime/workspace-room-notifier.tssrc/features/workspaces/server/mutations.test.tssrc/features/workspaces/server/mutations.tssrc/features/workspaces/server/queries.tssrc/features/workspaces/use-create-workspace.tssrc/features/workspaces/use-workspace-kernel-items.ts
💤 Files with no reviewable changes (9)
- src/features/workspaces/cache-workspace.ts
- src/features/workspaces/use-create-workspace.ts
- src/features/workspaces/components/WorkspacePageRoute.tsx
- src/features/workspaces/operations/workspace-tool-schemas.ts
- src/features/workspaces/contracts.ts
- src/features/workspaces/server/queries.ts
- src/features/workspaces/model/workspace-ai-context-validation.ts
- src/features/workspaces/model/workspace-ai-context-types.ts
- src/features/workspaces/model/workspace-ai-context-outline.ts
| .then((command) => { | ||
| upsertWorkspaceItemsInPageCache( | ||
| queryClient, | ||
| input.workspaceId, | ||
| [command.result], | ||
| command.revision, | ||
| ); | ||
| }) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Prevent stale color commits from overwriting a newer optimistic color.
If color request A starts and color request B updates the cache before A resolves, Line 141 applies A's result over B's optimistic value. Track the latest color commit per item. Ignore completion handlers for superseded commits. Add a race test for this order.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/workspaces/use-workspace-kernel-items.ts` around lines 141 -
148, Update the color commit flow around upsertWorkspaceItemsInPageCache to
track the latest color commit for each item and ignore completion handlers whose
commit has been superseded by a newer optimistic update. Preserve applying
results for the latest commit, and add a race test covering request A resolving
after request B updates the cache.
Greptile SummaryThis change applies revision-gated workspace item updates directly to the client cache. The realtime handler was exercised with a missed revision-11 update followed by revision 12: it advanced the cached revision and applied revision 12 without restoring the missed item or requesting a refresh. The workspace can therefore remain incomplete after a dropped room notification. Merge safety: do not merge until revision gaps trigger resynchronization. Confidence Score: 4/5The change is not safe to merge because a lost realtime update can leave an open workspace page permanently stale until an unrelated refresh occurs. The failure was reproduced through the registered realtime handler and production cache updater with a deliberately omitted intermediate revision. The observed cache advanced past the omitted revision without calling the resynchronization callback. Files Needing Attention: src/features/workspaces/realtime/use-workspace-presence.ts needs contiguous revision handling and a resynchronization path for gaps.
What T-Rex did
|
| if (message.revision <= cachedRevisionRef.current) { | ||
| return; | ||
| } | ||
|
|
||
| cachedRevisionRef.current = message.revision; |
There was a problem hiding this comment.
Revision gaps leave stale workspace state
When the cached page is at revision N and the notification for N+1 is missed, a later N+2 delta is accepted and advances the cursor directly to N+2. The missed creation, update, or deletion is never applied, and no full refresh is requested. Room notifications are best-effort live broadcasts rather than a replayable sequence, so a transient delivery failure can permanently leave the workspace cache incomplete until another independent refresh occurs. Require the next delta revision to be exactly cachedRevisionRef.current + 1; on a larger revision, trigger onDesync without applying or advancing the delta.
Artifacts
- This authored Vitest source registers the actual realtime handler, seeds the production query cache at revision 10, and delivers revision 12 while omitting revision 11, demonstrating the precise candidate path.
- This captured command output shows the test passed with cache revision advancing from 10 to 12, the revision-11 item absent, and zero desync calls, confirming the missing delta is not repaired.
There was a problem hiding this comment.
2 issues found across 30 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/features/workspaces/kernel/workspace-kernel.ts">
<violation number="1" location="src/features/workspaces/kernel/workspace-kernel.ts:45">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
This behavior-change PR modifies the realtime broadcast protocol (`publishWorkspacePageChange` now broadcasts a raw `WorkspacePageChange` delta instead of a wrapped `workspace.changed` message), but no automated tests exercise the new broadcast format. Add a test that asserts the broadcast payload matches the incoming `WorkspacePageChange` delta to protect against regression.</violation>
</file>
<file name="src/features/workspaces/persistence/workspace-postgres-persistence.ts">
<violation number="1" location="src/features/workspaces/persistence/workspace-postgres-persistence.ts:290">
P1: Concurrent mutations can publish these deltas out of revision order. If revision N+1 reaches the room before revision N, clients accept N+1 and permanently discard N, leaving the item changed at N stale until a reconnect; serialize per-workspace publication or trigger a full refresh when a revision gap is observed.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| }); | ||
| if (outcome.status === "applied") await this.notify(outcome.command.revision); | ||
| if (outcome.status === "applied") { | ||
| await this.notifyItemsUpserted([outcome.command.result], outcome.command.revision); |
There was a problem hiding this comment.
P1: Concurrent mutations can publish these deltas out of revision order. If revision N+1 reaches the room before revision N, clients accept N+1 and permanently discard N, leaving the item changed at N stale until a reconnect; serialize per-workspace publication or trigger a full refresh when a revision gap is observed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/persistence/workspace-postgres-persistence.ts, line 290:
<comment>Concurrent mutations can publish these deltas out of revision order. If revision N+1 reaches the room before revision N, clients accept N+1 and permanently discard N, leaving the item changed at N stale until a reconnect; serialize per-workspace publication or trigger a full refresh when a revision gap is observed.</comment>
<file context>
@@ -293,7 +286,9 @@ export class PostgresWorkspacePersistence implements WorkspaceKernelClient {
});
- if (outcome.status === "applied") await this.notify(outcome.command.revision);
+ if (outcome.status === "applied") {
+ await this.notifyItemsUpserted([outcome.command.result], outcome.command.revision);
+ }
return outcome;
</file context>
| } | ||
|
|
||
| async publishWorkspaceChange(change: WorkspaceRevision): Promise<void> { | ||
| async publishWorkspacePageChange(change: WorkspacePageChange): Promise<void> { |
There was a problem hiding this comment.
P2: Custom agent: Flag AI Slop and Fabricated Changes
This behavior-change PR modifies the realtime broadcast protocol (publishWorkspacePageChange now broadcasts a raw WorkspacePageChange delta instead of a wrapped workspace.changed message), but no automated tests exercise the new broadcast format. Add a test that asserts the broadcast payload matches the incoming WorkspacePageChange delta to protect against regression.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/kernel/workspace-kernel.ts, line 45:
<comment>This behavior-change PR modifies the realtime broadcast protocol (`publishWorkspacePageChange` now broadcasts a raw `WorkspacePageChange` delta instead of a wrapped `workspace.changed` message), but no automated tests exercise the new broadcast format. Add a test that asserts the broadcast payload matches the incoming `WorkspacePageChange` delta to protect against regression.</comment>
<file context>
@@ -42,16 +42,12 @@ export class WorkspaceKernel extends Agent<Cloudflare.Env> {
}
- async publishWorkspaceChange(change: WorkspaceRevision): Promise<void> {
+ async publishWorkspacePageChange(change: WorkspacePageChange): Promise<void> {
if (change.workspaceId !== this.name) {
throw new Error("Workspace change was routed to the wrong room.");
</file context>
There was a problem hiding this comment.
1 issue found across 15 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/features/workspaces/use-workspace-kernel-items.ts">
<violation number="1" location="src/features/workspaces/use-workspace-kernel-items.ts:129">
P2: When a user selects colors quickly, every click now starts a mutation, so an earlier selection can commit after a later one and leave the item with the wrong final color. Preserve the per-item latest-only debounce or serialize/discard superseded color requests before calling `updateWorkspaceItemColor`.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
|
||
| return { mutate }; | ||
| return useMutation({ | ||
| mutationFn: (input: UpdateWorkspaceItemColorInput) => updateWorkspaceItemColor({ data: input }), |
There was a problem hiding this comment.
P2: When a user selects colors quickly, every click now starts a mutation, so an earlier selection can commit after a later one and leave the item with the wrong final color. Preserve the per-item latest-only debounce or serialize/discard superseded color requests before calling updateWorkspaceItemColor.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/use-workspace-kernel-items.ts, line 129:
<comment>When a user selects colors quickly, every click now starts a mutation, so an earlier selection can commit after a later one and leave the item with the wrong final color. Preserve the per-item latest-only debounce or serialize/discard superseded color requests before calling `updateWorkspaceItemColor`.</comment>
<file context>
@@ -131,44 +125,20 @@ export function useUpdateWorkspaceItemColorMutation() {
- updateWorkspaceItemColorInPageCache(queryClient, input);
- commitColor(input);
+ return useMutation({
+ mutationFn: (input: UpdateWorkspaceItemColorInput) => updateWorkspaceItemColor({ data: input }),
+ onSuccess: (command, input) => {
+ applyWorkspacePageDeltaToCache(queryClient, {
</file context>
There was a problem hiding this comment.
0 issues found across 4 files (changes from recent commits).
Requires human review: Auto-approval blocked by 3 unresolved issues from previous reviews.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 1 file (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| // A transaction owns one pg.Client, so its queries execute serially. Both | ||
| // callers use repeatable-read transactions to keep these statements coherent. | ||
| const revision = await getWorkspaceRevision(db, workspaceId); | ||
| const items = await getActiveWorkspaceItems(db, workspaceId); |
There was a problem hiding this comment.
React Doctor · react-doctor/server-sequential-independent-await (warning)
This await doesn't use the previous result, so your users wait twice as long for nothing.
Fix → These two awaits don't depend on each other. Wrap them in Promise.all([...]) so they run at the same time.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/integrations/posthog/ai-observability.worker.test.ts (1)
41-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover the PostHog capture path directly.
This test verifies
getGatewayServedRoute, but it does not exercisecapturePostHogAiGeneration. Add or point to a test that assertsservice_tier: "priority"reaches captured properties and that a downgraded request omits the property.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/integrations/posthog/ai-observability.worker.test.ts` around lines 41 - 51, The test currently covers only getGatewayServedRoute; extend the tests around capturePostHogAiGeneration to verify that a priority service tier is included as service_tier: "priority" in captured properties, while a downgraded request omits that property. Reuse the existing routing setup and capture assertions where possible.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/features/workspaces/realtime/use-workspace-presence.ts`:
- Around line 66-72: Update the realtime error handler in useWorkspacePresence
to invoke onDesyncRef.current?.() from handleError, ensuring workspace page data
is refetched immediately after a connection error rather than waiting for
handleOpen.
In `@src/features/workspaces/use-workspace-kernel-items.ts`:
- Around line 47-53: Reconcile workspace pages after ambiguous mutation failures
by invalidating the workspace-page query in the create mutation’s onError after
optimistic cleanup at
src/features/workspaces/use-workspace-kernel-items.ts:47-53, in the rename
onError handler at :86-91, and after reporting the color-update error at
:128-140. Use the existing query client and workspace query key utilities.
---
Nitpick comments:
In `@src/integrations/posthog/ai-observability.worker.test.ts`:
- Around line 41-51: The test currently covers only getGatewayServedRoute;
extend the tests around capturePostHogAiGeneration to verify that a priority
service tier is included as service_tier: "priority" in captured properties,
while a downgraded request omits that property. Reuse the existing routing setup
and capture assertions where possible.
🪄 Autofix
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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: dd3980a6-2faf-470f-b25d-7b0dcc918308
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (20)
package.jsonsrc/features/workspaces/ai/ai-thread-runtime.tssrc/features/workspaces/cache-page.test.tssrc/features/workspaces/cache-page.tssrc/features/workspaces/components/WorkspaceFileUploadProvider.tsxsrc/features/workspaces/components/WorkspaceItemActionsMenu.tsxsrc/features/workspaces/components/WorkspaceLayout.tsxsrc/features/workspaces/components/WorkspacePageRoute.tsxsrc/features/workspaces/kernel/workspace-kernel-access.tssrc/features/workspaces/model/workspace-page.test.tssrc/features/workspaces/model/workspace-page.tssrc/features/workspaces/persistence/workspace-postgres-files.tssrc/features/workspaces/persistence/workspace-postgres-persistence.tssrc/features/workspaces/persistence/workspace-postgres-support.tssrc/features/workspaces/query-options.tssrc/features/workspaces/realtime/use-workspace-presence.tssrc/features/workspaces/use-workspace-kernel-items.tssrc/integrations/posthog/ai-observability.tssrc/integrations/posthog/ai-observability.worker.test.tssrc/lib/keyed-debounced-latest.ts
💤 Files with no reviewable changes (5)
- package.json
- src/lib/keyed-debounced-latest.ts
- src/features/workspaces/components/WorkspaceLayout.tsx
- src/features/workspaces/model/workspace-page.ts
- src/features/workspaces/components/WorkspacePageRoute.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
- src/features/workspaces/query-options.ts
- src/features/workspaces/components/WorkspaceFileUploadProvider.tsx
- src/features/workspaces/persistence/workspace-postgres-persistence.ts
- src/features/workspaces/persistence/workspace-postgres-files.ts
| const handleOpen = useCallback(() => { | ||
| setPresence((current) => ({ | ||
| ...current, | ||
| status: "connected", | ||
| workspaceId, | ||
| })); | ||
| onWorkspaceChangedRef.current?.(); | ||
| onDesyncRef.current?.(); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify whether useAgent always reconnects and calls onOpen after onError.
rg -n -C 5 '\buseAgent\b|onError:|onOpen:' srcRepository: ThinkEx-OSS/thinkex
Length of output: 42926
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- presence hook ---'
cat -n src/features/workspaces/realtime/use-workspace-presence.ts | sed -n '1,145p'
printf '%s\n' '--- dependency declarations ---'
rg -n -C 3 '"agents"|"`@cloudflare/agents`"' package.json package-lock.json pnpm-lock.yaml yarn.lock bun.lockb 2>/dev/null || true
printf '%s\n' '--- agent lifecycle references ---'
rg -n -C 8 'shouldReconnect|reconnect|onError|onClose|onOpen|class Agent|function useAgent' \
. --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' \
--glob '*agent*' --glob '*.ts' --glob '*.tsx' --glob '*.md' | head -n 500Repository: ThinkEx-OSS/thinkex
Length of output: 44159
🌐 Web query:
agents/react useAgent onError onOpen shouldReconnect reconnect lifecycle
💡 Result:
The useAgent React hook, provided by the Cloudflare Agents SDK, is designed to manage WebSocket connections with integrated lifecycle handling, state synchronization, and automatic reconnection [1][2][3]. Connection Lifecycle and Reconnection The useAgent hook automatically handles connection loss and reconnection logic [2][4]. You do not need to manually implement reconnection behavior for standard connection drops [2][4]. If you need to force a reconnection manually, you can use the reconnect() method exposed by the hook [5][2][4]. Event Callbacks You can monitor and respond to connection lifecycle events by providing specific callbacks in the useAgent configuration options [5][2][3]: onOpen: Invoked when the WebSocket connection is successfully opened [5][2]. onClose: Invoked when the connection is closed. Note that the hook will automatically attempt to reconnect after closure [2][4]. onError: Invoked when a WebSocket connection error occurs [5][2]. Connection Status and Readiness The hook provides mechanisms to track connection status: - The hook automatically closes the connection when the component unmounts [2][4]. - You can check the connection status or wait for the agent to be ready using the ready promise [3][4]. - If you need to trigger UI updates based on connection status (e.g., changing a status indicator), you can implement state management within the provided onOpen, onClose, and onError handlers [6][3]. Note: The shouldReconnect property is not a standard configuration option for the useAgent hook as the reconnection logic is handled automatically by the client SDK [5][2][4]. Additionally, ensure that your agent implementation correctly handles WebSocket events on the server side (using onConnect, onClose, and onError in your Agent class) [7].
Citations:
- 1: https://github.com/cloudflare/agents/blob/main/docs/client-sdk.md
- 2: https://cloudflare-agents-18.mintlify.app/client/react-hooks
- 3: https://cloudflare-agents-18.mintlify.app/api/use-agent-hook
- 4: https://mintlify.wiki/cloudflare/agents/client/react-hooks
- 5: https://developers.cloudflare.com/agents/api-reference/client-sdk/
- 6: useAgent should trigger rerender when connected cloudflare/agents#73
- 7: https://developers.cloudflare.com/agents/api-reference/websockets/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- published agents 0.19.0 package metadata ---'
curl -fsSL https://registry.npmjs.org/agents/0.19.0 \
| jq -r '.dist.tarball, .repository.url'
printf '%s\n' '--- published useAgent implementation and types ---'
for url in \
https://unpkg.com/agents@0.19.0/dist/react/index.js \
https://unpkg.com/agents@0.19.0/dist/react.js \
https://unpkg.com/agents@0.19.0/dist/index.js \
https://unpkg.com/agents@0.19.0/dist/react/index.d.ts \
https://unpkg.com/agents@0.19.0/dist/react.d.ts
do
printf '\n### %s\n' "$url"
curl -fsSL "$url" 2>/dev/null | rg -n -C 12 'useAgent|onError|onOpen|onClose|reconnect|shouldReconnect' || true
done
printf '%s\n' '--- realtime hook call sites and desync handlers ---'
rg -n -C 12 'useWorkspaceRealtime|onDesync|refreshWorkspacePage|invalidateQueries' src/features/workspaces \
--glob '*.ts' --glob '*.tsx'Repository: ThinkEx-OSS/thinkex
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://registry.npmjs.org/agents/0.19.0 \
| jq -r '.dist.tarball' \
| xargs curl -fsSL \
| tar -xz -C "$tmpdir"
printf '%s\n' '--- all lifecycle and reconnect code in agents 0.19.0 ---'
rg -n -C 20 'useAgent|onError|onOpen|onClose|reconnect|WebSocket' "$tmpdir/package" \
--glob '*.js' --glob '*.mjs' --glob '*.cjs' --glob '*.ts' --glob '*.d.ts' \
| head -n 1200Repository: ThinkEx-OSS/thinkex
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://registry.npmjs.org/agents/0.19.0 \
| jq -r '.dist.tarball' \
| xargs curl -fsSL \
| tar -xz -C "$tmpdir"
printf '%s\n' '--- agents dependency versions ---'
jq '.dependencies, .peerDependencies' "$tmpdir/package/package.json"
printf '%s\n' '--- useAgent callback wiring ---'
sed -n '220,355p' "$tmpdir/package/dist/react.js"
printf '%s\n' '--- useAgent published tests/docs for lifecycle behavior ---'
rg -n -C 10 'onError|onOpen|onClose|shouldReconnectOnClose|error event|reconnect' \
"$tmpdir/package" --glob '*test*' --glob '*.md' --glob '*.d.ts' \
| head -n 500
printf '%s\n' '--- precise desync call sites ---'
rg -l 'useWorkspaceRealtime' src | while read -r file; do
printf '\n### %s\n' "$file"
rg -n -C 18 'useWorkspaceRealtime|onDesync|refreshWorkspacePage' "$file"
doneRepository: ThinkEx-OSS/thinkex
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
partysocket_version="$(
curl -fsSL https://registry.npmjs.org/agents/0.19.0 \
| jq -r '.dependencies.partysocket'
)"
printf 'partysocket range: %s\n' "$partysocket_version"
resolved="$(
curl -fsSL https://registry.npmjs.org/partysocket \
| jq -r --arg range "$partysocket_version" '
.versions
| to_entries[]
| select(.key == ($range | ltrimstr("^")))
| .key
' | head -n 1
)"
printf 'partysocket exact candidate: %s\n' "$resolved"
if [ -z "$resolved" ]; then
resolved="$(curl -fsSL https://registry.npmjs.org/partysocket | jq -r '.["dist-tags"].latest')"
fi
curl -fsSL "https://registry.npmjs.org/partysocket/$resolved" \
| jq -r '.dist.tarball' \
| xargs curl -fsSL \
| tar -xz -C "$tmpdir"
printf '%s\n' '--- PartySocket lifecycle implementation ---'
rg -n -C 18 'onerror|onError|onclose|onClose|shouldReconnect|reconnect' \
"$tmpdir/package" --glob '*.js' --glob '*.mjs' --glob '*.ts' --glob '*.d.ts' \
| head -n 1000Repository: ThinkEx-OSS/thinkex
Length of output: 50375
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://registry.npmjs.org/partysocket/1.3.0 \
| jq -r '.dist.tarball' \
| xargs curl -fsSL \
| tar -xz -C "$tmpdir"
printf '%s\n' '--- ReconnectingWebSocket event ordering ---'
sed -n '300,455p' "$tmpdir/package/dist/ws.js"
printf '%s\n' '--- PartySocket React handler forwarding ---'
sed -n '1,25p' "$tmpdir/package/dist/use-ws-CYFjFIcF.js"
sed -n '145,230p' "$tmpdir/package/dist/use-ws-CYFjFIcF.js"
printf '%s\n' '--- exact realtime hook call sites ---'
rg -l 'useWorkspaceRealtime' src --glob '*.ts' --glob '*.tsx' | while read -r file; do
printf '\n### %s\n' "$file"
rg -n -C 10 'useWorkspaceRealtime|onDesync|refreshWorkspacePage' "$file"
doneRepository: ThinkEx-OSS/thinkex
Length of output: 12090
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://registry.npmjs.org/partysocket/1.3.0 \
| jq -r '.dist.tarball' \
| xargs curl -fsSL \
| tar -xz -C "$tmpdir"
node - <<'JS' "$tmpdir/package/dist/ws.js"
const fs = require("fs");
const file = process.argv[2];
const source = fs.readFileSync(file, "utf8");
const names = ["onerror", "onclose", "_disconnect", "_connect"];
for (const name of names) {
const index = source.indexOf(name);
console.log(`\n--- ${name} at ${index} ---`);
if (index >= 0) console.log(source.slice(Math.max(0, index - 500), index + 1800));
}
JSRepository: ThinkEx-OSS/thinkex
Length of output: 9466
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://registry.npmjs.org/partysocket/1.3.0 \
| jq -r '.dist.tarball' \
| xargs curl -fsSL \
| tar -xz -C "$tmpdir"
python3 - <<'PY' "$tmpdir/package/dist/ws.js" "$tmpdir/package/dist/react.js"
import re
import sys
from pathlib import Path
ws = Path(sys.argv[1]).read_text()
react = Path(sys.argv[2]).read_text()
handle_error = re.search(
r"_handleError\s*=\s*\(event\)\s*=>\s*\{(?P<body>.*?)\n\s*\};",
ws,
re.S,
)
assert handle_error, "ReconnectingWebSocket._handleError was not found"
body = handle_error.group("body")
positions = {token: body.find(token) for token in ("_disconnect", "this.onerror", "dispatchEvent", "_connect")}
assert all(position >= 0 for position in positions.values()), positions
assert positions["_disconnect"] < positions["this.onerror"] < positions["_connect"], positions
assert "options.onOpen?.(event)" in react
assert "options.onClose?.(event)" in react
assert "options.onError" not in react.split("usePartySocket({", 1)[1].split("onOpen:", 1)[0], \
"unexpected useAgent-specific onError wrapper"
print("ReconnectingWebSocket._handleError order:", positions)
print("useAgent forwards onOpen/onClose wrappers and leaves onError as a PartySocket handler.")
print("An error triggers close/reconnect processing, but onOpen occurs only after a later successful connection.")
PYRepository: ThinkEx-OSS/thinkex
Length of output: 246
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://registry.npmjs.org/partysocket/1.3.0 \
| jq -r '.dist.tarball' \
| xargs curl -fsSL \
| tar -xz -C "$tmpdir"
python3 - <<'PY' "$tmpdir/package/dist/ws.js" "$tmpdir/package/dist/react.js"
import re
import sys
from pathlib import Path
ws = Path(sys.argv[1]).read_text()
react = Path(sys.argv[2]).read_text()
match = re.search(r"_handleError\s*=\s*\(event\)\s*=>\s*\{(?P<body>.*?)\n\s*\};", ws, re.S)
print("handleError found:", bool(match))
if match:
body = match.group("body")
positions = {token: body.find(token) for token in ("_disconnect", "this.onerror", "dispatchEvent", "_connect")}
print("handleError token positions:", positions)
print("handleError body:")
print(body)
for token in ("options.onOpen?.(event)", "options.onClose?.(event)", "options.onError"):
print(f"{token!r} present in react.js:", token in react)
print("useAgent forwards onOpen before onClose:",
react.find("options.onOpen?.(event)") < react.find("options.onClose?.(event)"))
print("PartySocket receives the caller's onError through restOptions:",
"onError" in react[react.find("const socketOptions"):react.find("const socketEnabled")])
PYRepository: ThinkEx-OSS/thinkex
Length of output: 852
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
mkdir -p "$tmpdir/agents" "$tmpdir/partysocket"
curl -fsSL https://registry.npmjs.org/agents/0.19.0 \
| jq -r '.dist.tarball' \
| xargs curl -fsSL \
| tar -xz -C "$tmpdir/agents"
curl -fsSL https://registry.npmjs.org/partysocket/1.3.0 \
| jq -r '.dist.tarball' \
| xargs curl -fsSL \
| tar -xz -C "$tmpdir/partysocket"
python3 - <<'PY' "$tmpdir/agents/package/dist/react.js" "$tmpdir/partysocket/package/dist/ws.js"
import re
import sys
from pathlib import Path
react = Path(sys.argv[1]).read_text()
ws = Path(sys.argv[2]).read_text()
match = re.search(r"_handleError\s*=\s*\(event\)\s*=>\s*\{(?P<body>.*?)\n\s*\};", ws, re.S)
assert match, "ReconnectingWebSocket._handleError was not found"
body = match.group("body")
positions = {token: body.find(token) for token in ("_disconnect", "this.onerror", "dispatchEvent", "_connect")}
assert all(value >= 0 for value in positions.values()), positions
assert positions["_disconnect"] < positions["this.onerror"] < positions["_connect"], positions
assert "options.onOpen?.(event)" in react
assert "options.onClose?.(event)" in react
assert "options.onError" not in react.split("usePartySocket({", 1)[1].split("onOpen:", 1)[0]
print("PartySocket _handleError order:", positions)
print("agents useAgent wraps onOpen and onClose, while onError remains a forwarded PartySocket handler.")
print("A later onOpen requires a successful reconnect; automatic retry does not make that event immediate or guaranteed.")
PYRepository: ThinkEx-OSS/thinkex
Length of output: 480
Refetch page data after a realtime error.
useAgent reconnects automatically, but onOpen runs only after a later successful connection. If reconnect is delayed or fails, missed deltas remain in the workspace page cache. Call onDesyncRef.current?.() from handleError.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/workspaces/realtime/use-workspace-presence.ts` around lines 66 -
72, Update the realtime error handler in useWorkspacePresence to invoke
onDesyncRef.current?.() from handleError, ensuring workspace page data is
refetched immediately after a connection error rather than waiting for
handleOpen.
| onSuccess: (command, input) => { | ||
| applyWorkspacePageDeltaToCache(queryClient, { | ||
| type: "workspace.items.upserted", | ||
| workspaceId: input.workspaceId, | ||
| items: [command.result], | ||
| revision: command.revision, | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reconcile the page after ambiguous mutation failures.
A mutation request can fail after the server commits. A realtime delta can then update the cache before onError runs. The create error handler can remove that confirmed item, and rename or color can retain stale item fields.
src/features/workspaces/use-workspace-kernel-items.ts#L47-L53: After optimistic-create cleanup, invalidate the workspace page inonError.src/features/workspaces/use-workspace-kernel-items.ts#L86-L91: Invalidate the workspace page in the renameonErrorhandler.src/features/workspaces/use-workspace-kernel-items.ts#L128-L140: Invalidate the workspace page after reporting a color-update error.
📍 Affects 1 file
src/features/workspaces/use-workspace-kernel-items.ts#L47-L53(this comment)src/features/workspaces/use-workspace-kernel-items.ts#L86-L91src/features/workspaces/use-workspace-kernel-items.ts#L128-L140
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/workspaces/use-workspace-kernel-items.ts` around lines 47 - 53,
Reconcile workspace pages after ambiguous mutation failures by invalidating the
workspace-page query in the create mutation’s onError after optimistic cleanup
at src/features/workspaces/use-workspace-kernel-items.ts:47-53, in the rename
onError handler at :86-91, and after reporting the color-update error at
:128-140. Use the existing query client and workspace query key utilities.
Summary
WorkspaceKernelroom@tanstack/pacerdependencyDesign
Postgres remains canonical. The Durable Object remains an ephemeral presence and delivery room; it does not store another workspace projection. Item mutations publish small typed deltas after commit. One TanStack Query cache function owns ordering: it ignores stale/duplicate deltas, applies only the next revision, and refetches the authoritative page on a gap. Every socket connection also refetches once, closing the initial fetch-before-subscribe race without adding a handshake, outbox, event log, or custom subscription engine.
This follows the useful parts of Convex's invalidation model and TanStack Query's mutation-response/optimistic-cache guidance without recreating Convex read sets or adopting another synchronization layer.
References:
Verification
pnpm checkpnpm test— 81 files, 335 testspnpm test:workers— 14 files, 47 testspnpm buildSummary by CodeRabbit
New Features
Bug Fixes
Changes