Skip to content

feat(listview): realtime list updates baked into useListViewResource - #43436

Merged
villebro merged 2 commits into
gaq-to-gtffrom
villebro/gtf-realtime-listviews
Aug 23, 2026
Merged

feat(listview): realtime list updates baked into useListViewResource#43436
villebro merged 2 commits into
gaq-to-gtffrom
villebro/gtf-realtime-listviews

Conversation

@villebro

Copy link
Copy Markdown
Member

SUMMARY

Step 7 of the GAQ→GTF epic (targets gaq-to-gtf) — realtime list views, the headline capability of the new websocket transport. A list built on useListViewResource now live-patches its on-screen rows as the underlying entities change, with no manual refresh. The Task List is the first surface; any other list opts in with two args.

Shared realtime client (src/middleware/realtime.ts) — owns the single browser socket and fans the generic {channel, payload} envelope out to any number of subscribers, so features share one connection. Socket ownership is extracted out of asyncEvent.ts, which now simply subscribes for its tier-2 chart-data handler. The client is payload-agnostic (routes on channel) and best-effort (reconnects on close); connection auth is the superset-ws-token JWT cookie riding the handshake.

useListViewResource realtime (opt-in via enableRealtime, realtimeIdField) — subscribes to entity-changes:<resource>, ignores nudges for rows not currently displayed, debounce-collects the rest (~500ms) and issues one batched fetch of just those rows through the normal authorized list endpoint (col:<idField> op:in), merging them in place by id. Update-only (no new-row insertion, no full refetch/redraw), so:

  • authz/RLS are unchanged — the socket only carries opaque ids; real data comes from the authorized REST endpoint;
  • a burst of changes can't hammer the backend (one coalesced fetch per window);
  • untouched rows keep their reference, so React only re-renders what changed.

Task List = first surface — realtime enabled with realtimeIdField: 'uuid' (tasks are UUID-facing). Tasks already emit entity-changes:task nudges at completion (shipped in 6a, #43431), so this works end-to-end with no new backend nudge. Added uuid to the task API search_columns so the batched refetch can filter by it.

Known limitation / follow-up: tasks currently nudge only on terminal completion (per 6a), so intermediate transitions (pending→in_progress, progress) don't yet live-update; emitting nudges on those transitions, and adding nudges at other entities' DAO/command commit points (dashboards/charts/datasets/…), are incremental follow-ups — each new list inherits realtime for free once its backend nudges land.

TESTING INSTRUCTIONS

Automated (all green locally):

  • jest src/middleware/realtime.test.ts — shared client: dispatch/subscribe/unsubscribe, malformed/handler-error isolation, enabled/disabled connect, reconnect-on-close, disconnect.
  • jest src/middleware/asyncEvent.test.ts — refactored onto the shared client; tier-2 acceleration still settles/rejects/ignores correctly.
  • jest src/views/CRUD/hooks.test.tsx — realtime nudge live-patches a displayed row in place (batched col:id op:in fetch + merge); nudges for off-screen rows are ignored.
  • pytest tests/unit_tests/tasks/ — 359 passed (search_columns addition).
  • tsc / eslint / ruff / pylint clean.

Manual (with WEBSOCKET_ENABLED, DISTRIBUTED_COORDINATION_CONFIG, superset-websocket running same-host):

  1. Open the Task List, trigger async work → completed task rows update their status/duration in place without a manual refresh, while the rest of the page stays put.
  2. Kill the websocket server → the list still reflects state on its next normal load/refresh (best-effort degradation).

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags: WEBSOCKET_ENABLED (realtime transport)
  • Changes UI
  • Includes DB Migration
  • Introduces new feature or API
  • Removes existing feature or API

Step 7 of the GAQ→GTF epic — realtime list views, the headline of the new
websocket transport. A list backed by useListViewResource now live-patches its
on-screen rows as the underlying entities change, with no manual refresh.

- Shared realtime client (src/middleware/realtime.ts): owns the single browser
  socket and fans the generic {channel, payload} envelope out to any number of
  subscribers, so features share one connection. Extracted the socket ownership
  out of asyncEvent.ts, which now just subscribes for its tier-2 chart-data
  handler. Payload-agnostic and best-effort (reconnect on close).

- useListViewResource gains opt-in realtime (enableRealtime, realtimeIdField):
  it subscribes to entity-changes:<resource>, ignores nudges for rows not
  currently displayed, debounce-collects the rest (~500ms) and issues ONE
  batched fetch of just those rows through the normal authorized list endpoint
  (col:<idField> op:in), merging them in place by id. Update-only (no new-row
  insertion), so authz/RLS are unchanged and a burst can't hammer the backend.

- Task List is the first surface: realtime enabled with idField 'uuid' (tasks
  are UUID-facing); tasks already emit entity-change nudges at completion (6a).
  Added 'uuid' to the task API search_columns so the batched refetch can filter.

Tests: shared client (dispatch/subscribe/reconnect), asyncEvent refactored onto
it, useListViewResource live-patch + ignore-offscreen, all green; tsc/eslint/
ruff/pylint clean.
@dosubot dosubot Bot added change:frontend Requires changing the frontend listview Namespace | Anything related to lists, such as Dashboards, Charts, Datasets, etc. labels Aug 23, 2026
@bito-code-review

bito-code-review Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Bito Automatic Review Skipped - Branch Excluded

Bito didn't auto-review because the source or target branch is excluded from automatic reviews.
No action is needed if you didn't intend for the agent to review it. Otherwise, to manually trigger a review, type /review in a comment and save.
You can change the branch exclusion settings here, or contact your Bito workspace admin at evan@preset.io.

@github-actions github-actions Bot added the api Related to the REST API label Aug 23, 2026
@netlify

netlify Bot commented Aug 23, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 0f132ec
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a8b3ad1843d0500073c5d0e
😎 Deploy Preview https://deploy-preview-43436--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@villebro

Copy link
Copy Markdown
Member Author

/review

@bito-code-review

bito-code-review Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #dba1fd

Actionable Suggestions - 0
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • superset-frontend/src/middleware/realtime.test.ts - 1
Review Details
  • Files reviewed - 8 · Commit Range: 0f132ec..0f132ec
    • superset-frontend/src/middleware/asyncEvent.test.ts
    • superset-frontend/src/middleware/asyncEvent.ts
    • superset-frontend/src/middleware/realtime.test.ts
    • superset-frontend/src/middleware/realtime.ts
    • superset-frontend/src/pages/TaskList/index.tsx
    • superset-frontend/src/views/CRUD/hooks.test.tsx
    • superset-frontend/src/views/CRUD/hooks.ts
    • superset/tasks/api.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

// enabled — independent of GLOBAL_ASYNC_QUERIES, since realtime list views
// (tier-1 entity-change nudges) ride the same socket. Idempotent: a no-op when
// WEBSOCKET_ENABLED is false, and supersedes any prior socket otherwise.
connectRealtime(config);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: When GlobalAsyncQueries is disabled, waitersByTaskId is never initialized because the function returns after this connection call. However, the newly connected socket still delivers realtime:* messages to handleRealtimeMessage, which calls applyStatus and dereferences waitersByTaskId with .get, causing a TypeError for every terminal task message. Initialize the waiter map before the feature-flag return or guard realtime status handling when async queries are disabled. [null pointer]

Severity Level: Minor 🧹
- ⚠️ Realtime chart-task messages generate repeated handler errors.
- ⚠️ Async completion handling cannot process messages in this state.
- ⚠️ Errors are swallowed by the shared realtime dispatcher.

Use CodeAnt Skill

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/middleware/asyncEvent.ts
**Line:** 267:267
**Comment:**
	*Null Pointer: When `GlobalAsyncQueries` is disabled, `waitersByTaskId` is never initialized because the function returns after this connection call. However, the newly connected socket still delivers `realtime:*` messages to `handleRealtimeMessage`, which calls `applyStatus` and dereferences `waitersByTaskId` with `.get`, causing a `TypeError` for every terminal task message. Initialize the waiter map before the feature-flag return or guard realtime status handling when async queries are disabled.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — fixed in the latest push. waitersByTaskId is now initialized eagerly at declaration (new Map()), so the shared-socket handler is safe when a realtime:* message arrives with GLOBAL_ASYNC_QUERIES off (the socket connects on WEBSOCKET_ENABLED alone): applyStatus finds an empty map and no-ops instead of dereferencing undefined. Added a regression test.

Note this PR also grew a related feature per maintainer request: task rows now live-update on intermediate transitions (IN_PROGRESS/progress/abort), not just completion — nudges are emitted post-commit from the internal status/update/cancel commands, and the nudge now carries the integer id (FAB can't filter a UUIDType column with in, so the batched refetch keys on id).

@bito-code-review

Copy link
Copy Markdown
Contributor

The flagged issue is correct. When GlobalAsyncQueries is disabled, waitersByTaskId is not initialized, but handleRealtimeMessage is still subscribed to the shared realtime client and will attempt to access waitersByTaskId when a message arrives, causing a TypeError.

To resolve this, you should initialize waitersByTaskId regardless of the feature flag, or guard the applyStatus call within handleRealtimeMessage to ensure it only proceeds if the map is initialized.

Here is a concise fix for superset-frontend/src/middleware/asyncEvent.ts:

export const handleRealtimeMessage = (message: RealtimeMessage) => {
  if (!waitersByTaskId) return;
  const { channel, payload } = message;
  if (!channel.startsWith(REALTIME_CHANNEL_PREFIX)) return;
  const taskId = payload?.task_id;
  const status = payload?.status;
  if (typeof taskId === 'string' && typeof status === 'string') {
    applyStatus(taskId, status);
  }
};

Would you like me to fetch and validate the rest of the comments on this PR to see if there are other issues to address?

superset-frontend/src/middleware/asyncEvent.ts

export const handleRealtimeMessage = (message: RealtimeMessage) => {
  if (!waitersByTaskId) return;
  const { channel, payload } = message;
  if (!channel.startsWith(REALTIME_CHANNEL_PREFIX)) return;
  const taskId = payload?.task_id;
  const status = payload?.status;
  if (typeof taskId === 'string' && typeof status === 'string') {
    applyStatus(taskId, status);
  }
};

@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.83333% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 78.84%. Comparing base (8489b0b) to head (0991301).

Files with missing lines Patch % Lines
superset/daos/tasks.py 66.66% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff               @@
##           gaq-to-gtf   #43436      +/-   ##
==============================================
- Coverage       78.85%   78.84%   -0.01%     
==============================================
  Files            2880     2880              
  Lines          164476   164476              
  Branches        38004    38005       +1     
==============================================
- Hits           129691   129682       -9     
- Misses          32340    32346       +6     
- Partials         2445     2448       +3     
Flag Coverage Δ
hive 38.13% <29.16%> (-0.01%) ⬇️
mysql ?
postgres 57.73% <83.33%> (+<0.01%) ⬆️
presto 40.06% <29.16%> (-0.01%) ⬇️
python 83.52% <95.83%> (-0.02%) ⬇️
sqlite 57.42% <83.33%> (+<0.01%) ⬆️
unit 73.61% <62.50%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

… fix

Follow-up on the realtime-listview work: task rows now live-update on every
status transition and progress write, not just terminal completion, so the
Task List reflects IN_PROGRESS / progress / abort states as they happen.

- Emit publish_entity_change post-commit from InternalStatusTransitionCommand
  (all status transitions incl. PENDING->IN_PROGRESS), InternalUpdateTaskCommand
  (progress/payload -- already throttled via TASK_PROGRESS_UPDATE_THROTTLE_INTERVAL,
  so nudge volume is bounded), and CancelTaskCommand (abort). Removed the now
  redundant nudge from publish_completion (the terminal transition nudges via the
  command); publish_completion keeps the guaranteed completion signal + tier-2.

- The entity-change nudge now carries the integer primary id (via TaskDAO.get_id)
  instead of the uuid: FAB's default filter converter can't build an "in" filter
  on a UUIDType column, so the list view's batched refetch must key on the int
  id. Added "id" (not "uuid") to the task API search_columns; the Task List uses
  the hook's default "id" idField.

- Bump the list-view refetch debounce 500ms->1000ms to coalesce the higher nudge
  volume into at most one batched fetch per second per list.

- Review fix (CodeAnt on #43436): initialize waitersByTaskId eagerly. The shared
  socket connects whenever WEBSOCKET_ENABLED regardless of GLOBAL_ASYNC_QUERIES,
  so the subscribed handler can run applyStatus with the flag off and must find a
  map, not undefined. Added a regression test.

Tests: 360 backend task tests, asyncEvent/hooks/realtime/TaskList frontend suites
green; mypy/ruff/pylint/tsc clean.
@villebro

Copy link
Copy Markdown
Member Author

Thanks — both bots flagged the same waitersByTaskId null-pointer (a realtime:* message arriving while GLOBAL_ASYNC_QUERIES is off, since the shared socket connects on WEBSOCKET_ENABLED alone). Fixed in 0991301 by initializing waitersByTaskId = new Map() eagerly at declaration rather than only inside init() after the flag gate — so applyStatus always finds a map and no-ops when there are no waiters, with no per-call guard needed. Regression test added ("a tier-2 message is a no-op when async queries are disabled").

Heads up that this PR also grew a maintainer-requested feature since the initial review: task rows now live-update on intermediate transitions (IN_PROGRESS / progress / abort), not just completion. Nudges are emitted post-commit from the internal status/update/cancel commands, and the nudge now carries the integer id (FAB's default converter can't build an in filter on a UUIDType column, so the batched refetch keys on id); the refetch debounce was bumped to 1s to coalesce the higher volume.

@villebro
villebro merged commit 5ab7d5f into gaq-to-gtf Aug 23, 2026
82 of 84 checks passed
@villebro
villebro deleted the villebro/gtf-realtime-listviews branch August 23, 2026 22:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Related to the REST API change:frontend Requires changing the frontend listview Namespace | Anything related to lists, such as Dashboards, Charts, Datasets, etc. size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant