Live-refresh list windows on content changes (posts, CPTs, comments, WooCommerce orders) - #388
Merged
Conversation
Saving a post, page, CPT, comment, or WooCommerce order in one window now refreshes every other window listing that type. Three delivery paths feed the existing broadcast bus (desktop-mode.<type>.changed): - Chromeless footer (instant): includes/content-changes.php records mutations into a per-request changelog (wp_after_insert_post, comment hooks, woocommerce_* order hooks for HPOS + legacy) and a 60s per-user transient carries it across the POST -> 302 -> GET redirect to the next chromeless admin_footer render. - Block editor (instant): the bridge save-watcher now also posts the broadcast upstream on save success (REST saves render no footer). - Heartbeat catch-all (<= 1 tick): pruned changelog option + shell module re-broadcasting fresh entries; covers Quick Edit, AJAX status flips, other tabs/users, REST/WP-CLI. First tick is a clock-skew-proof handshake. Consumers: the iframe soft-reload matcher is now generic (any edit.php?post_type=X reacts to its own type) plus declarative extras via the new desktop_mode_soft_reload_rules filter, shipping the HPOS admin.php?page=wc-orders rule (queryAbsent action protects the order editor). The recycle bin delegates its changelog into the generic recorder (single emitter, no duplicate broadcasts) and the native Comments window auto-refreshes on comment.changed. New public surface: desktop_mode_content_changes_record(), filters desktop_mode_content_changes_should_record / _change_topic / _changes_broadcasts / desktop_mode_soft_reload_rules, actions desktop_mode_content_change_recorded / _changes_emitted, heartbeat fields desktop_mode_content_changes[_seen_ts]. Documented in hooks-reference, javascript-reference, bridge-protocol, api-index, and a new examples/content-changes.md recipe. Tests: 22 PHPUnit + 6 vitest cases covering recorder dedupe, hook wiring, the redirect buffer, footer emitter, heartbeat filter, and the client handshake. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #372
Problem
Adding or updating a WooCommerce order in one desktop window left an open Orders-list window stale. The gap generalizes: the shell already had all the consumer machinery — the cross-window broadcast bus (
desktop-mode.<type>.changed), the iframe soft-reload, native list windows that subscribe — but nothing published those topics on a normal save. Only the recycle bin (trash verbs) and the native posts window's own mutations emitted.Solution — a generic content-change realtime layer
Any create / update / trash of a post, page,
show_uiCPT, comment, or WooCommerce order (HPOS and legacy, oneshop_ordertopic for both) now reaches every window through three delivery paths:includes/content-changes.phprecords mutations into a per-request changelog (wp_after_insert_postwith revision/autosave/auto-draft/trash-status skips, comment hooks,woocommerce_*order hooks behind aclass_existsguard). Because the dominant save flow is form-POST → 302 → GET, the changelog survives the redirect in a 60 s per-user transient flushed by the next chromelessadmin_footerrender (~500 ms after the click).core/editorsave-watcher now also posts the broadcast upstream on save success (source: 'editor',createdvsupdatedcaptured on the save-start tick). Gutenberg saves over REST, so no footer ever renders there._desktop_mode_content_changes_logoption (autoload=false, 5-min window / 100 entries); opted-in shells re-broadcast fresh entries (source: 'heartbeat'). Covers Quick Edit, AJAX status flips, other browser tabs/users, REST/WP-CLI. The first tick is a pure handshake that adopts the server clock, so client/server skew can never drop changes. Tabs that never opt in pay zero per tick.Consumers
edit.php?post_type=Xreacts todesktop-mode.X.changedfor any post type with zero per-type code (also covers legacyedit.php?post_type=shop_order).desktop_mode_soft_reload_rulesfilter, shipping one rule for the HPOS orders list (admin.php?page=wc-orders).queryAbsent: [ 'action' ]is load-bearing — it keeps the single-order editor under the single-edit exclusion so a background refresh can never destroy unsaved order state.desktop-mode.comment.changedand refreshes in place (the 30 s count-poll + reload pill stays as fallback).Recycle-bin delegation
The bin's per-domain changelog now forwards into the generic recorder (
desktop_mode_recycle_bin_record_change()kept as a thin wrapper), so a trash and a save flow through one footer emitter — each type/action pair broadcasts exactly once per render. The bin-specificdesktop-mode-recycle-bin-changedts signal, option, and heartbeat handler are unchanged. First-writer-wins dedupe pertype:idkeeps the more specific trash verb over the follow-up status-writeupdated.New public surface (all documented)
desktop_mode_content_changes_record( $type, $id, $action )— public recorder for plugins with custom storagedesktop_mode_content_changes_should_record,desktop_mode_content_change_topic,desktop_mode_content_changes_broadcasts,desktop_mode_soft_reload_rulesdesktop_mode_content_change_recorded,desktop_mode_content_changes_emitteddesktop_mode_content_changes_seen_ts(client → server),desktop_mode_content_changes(server → client)desktop-mode.<type>.changedpayload contract:sourcegains'editor'/'heartbeat',actiongains'created'/'updated'Docs updated in the same change:
hooks-reference.md(new section + rewritten cross-window broadcast contract),javascript-reference.md,bridge-protocol.md,api-index.md, newexamples/content-changes.mdrecipe.Tests
tests/phpunit/tests/contentChanges.php, 22 cases): recorder validation + first-writer-wins dedupe,should_recordveto, created-vs-updated (auto-draft first save), revision/show_uiskips, trash/untrash integration proving the recycle-bin delegation end-to-end, comment transitions, WC guard, redirect buffer, footer emitter (emit + buffer consumption + no-rebuffer + chromeless gate + suppress filter), heartbeat filter.tests/vitest/content-changes-heartbeat.test.ts, 6 cases): handshake semantics, re-broadcast fan-out, high-water-mark advance, malformed-entry hygiene.Manual QA notes
Verified flows: HPOS order status change + new order (instant list refresh, no spinner), legacy WC, Gutenberg save → posts list + native Posts window, CPT saves, comment moderation, AJAX status flip via heartbeat, cross-tab catch-up, and the regression checks (single soft-reload fetch per change; dirty editors never background-reloaded).
Known accepted redundancy (documented): a change delivered instantly by the footer/editor path is re-broadcast once by the next heartbeat tick — consumers are idempotent, cost is one extra refresh fetch.
🤖 Generated with Claude Code