Skip to main content

real-time-sync

Decide whether a page needs external changes before refresh, then use the opt-in shared SSE and polling transport safely.

Source facts

Repository
BuilderIO/agent-native
Last source activity
October 3, 2026 at 03:54
Detected SKILL.md language
English
Stars
7,065
Forks
640

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
real-time-sync
description
Decide whether a page needs external changes before refresh, then use the opt-in shared SSE and polling transport safely.
scope
dev
metadata
{"internal":true}
# Real-Time Sync ## Decision rule Opt in only when both are true: 1. The data can change without the current user acting. 2. The user needs to see that change before refreshing or navigating. A local agent edit does not meet this rule. The chat run stream already invalidates action queries and source counters after each side-effecting tool and again at run end, without opening a background poll. ### Opt in - **Slides deck editor:** another collaborator can edit the open deck. - **Mail inbox:** new mail can arrive while the inbox is open. - **A watched job:** a background job changes progress while the user watches it. - **Cross-tab state:** state must converge across this user's open tabs without a new action. ### Keep it off - Public, anonymous, marketing, docs, and SSR pages. - Settings, forms, and read-mostly lists or dashboards. Refresh them on focus or navigation instead. - Single-user agent edits. The chat run stream handles those. - Pages where an update is useful eventually but not before the next refresh. ## Cost and behavior `useDbSync()` has no background transport unless the page opts in with a non-empty reason. One transport is shared per tab. A visible opted-in tab polls at 1 minute, then 2, then 5 minutes while idle; user activity or a local mutation resets that sequence. Hidden tabs pause by default. During an active agent run, opted-in sync can poll at its configured `interval`; local tool completion and run-end invalidation do not depend on that poll. On a long-lived host, same-process changes stream over `/_agent-native/events`; polling is the cross-process fallback. On a production serverless host, the local events endpoint refuses the long-lived stream and `/_agent-native/poll` carries remote changes. The paid Hosted Realtime Sync Gateway is not required for this pattern. While a collab doc shows another person present, the transport polls every 2.5 s instead (`acquireCollabPollBoost()`, held by the collab client, not by pages). It is a no-op whenever a stream is connected, lapses after 3 minutes without input or remote events, and never applies to lone tabs. ## Use the hook Declare the reason beside the route gate so reviewers can see why the page pays for remote sync: ```tsx useDbSync({ queryClient, realtime: isPrivateDeckEditorPath(location.pathname) ? { reason: "other collaborators can edit this deck while it is open" } : undefined, pauseWhenHidden: true, }); ``` The reason is required by the TypeScript API. `guard:realtime-opt-in` also requires a named private or authenticated pathname predicate, and rejects public/docs/SSR files and known anonymous routes. A reviewed exception must put this pragma on the opt-in or the line immediately above it: ```ts // guard:allow-realtime-opt-in — short reason ``` An opted-in page only hears about an action when its change event reaches the current user. By default an `action` event reaches the actor alone, so a collaborator's comment, save, or agent edit never arrives. Declare `changeResource: (input, result) => ({ resourceType, resourceId })` on the mutating action and the event also reaches everyone who can read that resource. Name it from `input` when the call carries the resource id (Content's `documentChangeResource`, Slides' comment actions) and from `result` when the call is keyed by a child id (Design's `designChangeResource` for `delete-file`); return `null` for a call that changed nothing. Do not publish a parallel per-template event for the same purpose. Do not start `subscribeSyncEvents()` or an `EventSource` in a feature to bypass the decision. `subscribeSyncEvents()` is a lower-level transport subscription used by the existing Yjs collaboration client and narrow framework plumbing. Keep Yjs collaborative editing on its existing channel. ## Query freshness Prefer `useActionQuery()` for action-backed data. Mutating actions refresh local action observers; the chat run stream also invalidates them for agent tool side effects. Raw queries should include the relevant source counters: ```tsx const versions = useChangeVersions(["dashboards", "action"]); useQuery({ queryKey: ["dashboard", id, versions], queryFn: () => fetchDashboard(id), placeholderData: (previous) => previous, }); ``` That covers local chat-run edits. Remote edits reach the counter through `useDbSync()` only on an opted-in page. Use `useReconciledState` when a form or inline editor copies a query value into local state so incoming data does not replace active typing. URL commands (`__set_url__`, `set-url`, and `set-search-params`) and `refresh-screen` also flow through local chat events. The sidebar listens for screen refresh only while its panel is open or a chat run is active; public docs can disable that boundary with `screenRefreshEnabled={false}`. ## Source counters On local tool completion, `useDbSync()` advances the action counter and any other raw-query source counters currently observed by the page. Generic tool completion events do not identify their data domain, so keep raw-query source lists narrow. Remote sync events advance their specific source counters. | Source | Changed by | | --- | --- | | `action` | A successful mutating action or local chat-run side-effect completion | | `app-state` | Writes to `application_state`, including URL commands | | `settings` | Writes to `settings` | | `dashboards`, `analyses`, `extensions` | Domain-specific mutations that emit those sources | | `collab` | Yjs collaborative document updates | | `screen-refresh` | The explicit `refresh-screen` agent tool | Use `useChangeVersions()` when one query depends on more than one source. ## Avoid - Do not create manual polling loops or a second `EventSource`. - Do not enable background sync for a whole app root when only one private route needs remote updates. - Do not assume a successful local action is a reason for a background subscriber; use local mutation invalidation and the chat run stream. - Do not blanket-invalidate template queries when a source-versioned query or action-backed query can target the refreshed data.
View on GitHub