Skip to content

Perf: instant comment feedback, cached markdown rendering, quieter presence - #139

Merged
HamptonMakes merged 3 commits into
mainfrom
hampton/perf-snappy
Jul 16, 2026
Merged

Perf: instant comment feedback, cached markdown rendering, quieter presence#139
HamptonMakes merged 3 commits into
mainfrom
hampton/perf-snappy

Conversation

@HamptonMakes

Copy link
Copy Markdown
Collaborator

Why

The app feels laggy on exactly the interactions that should feel instant. Investigation found three architectural causes (this composes with #137, which fixes the template-digest cost on comment writes):

  1. Your own comment took a full cable round-trip to appear. Comment/thread actions responded with an empty turbo stream ("prevents scroll-to-top") and relied on the ActionCable broadcast to update the submitter's own page: SolidCable DB write → poll interval → websocket push → DOM apply. On a loaded system that's the entire perceived "lag when you comment."
  2. Markdown rendering ran from scratch everywhere. Commonmarker + two Nokogiri passes + sanitize on every plan show, on every content-mutation broadcast, and once per summary-less card on every /plans page (up to 20 full parses per index render).
  3. Presence chatter. Every viewer pings every 15s; every ping re-rendered and re-broadcast the full viewer list to every open tab even when nothing changed — with N viewers, N renders + N×N pushes per 15s of pure noise.

What

  • Inline turbo-stream responses for the actor on comment create/delete and thread create/resolve/accept/discard/reopen. Partial renders once; the same HTML goes inline to the actor and out via broadcast to other viewers. Turbo's append removes same-id children first, so the echoed broadcast can't duplicate the actor's copy; replace/remove are naturally idempotent. Empty-stream behavior (no navigation/scroll jump) is preserved.
  • Fragment caching per content SHA for the rendered plan body (_content_body) and index previews. skip_digest: true keeps template digesting off write paths (same approach as Skip comment template digest on writes #137); an explicit RENDER_CACHE_VERSION constant invalidates when the render pipeline changes. Falls back gracefully where no cache store is configured.
  • Presence broadcast dedupe via viewer-set fingerprint. Stale viewers still disappear promptly — their departure changes the fingerprint, so the next ping from any remaining viewer broadcasts.
  • Broadcaster.append_to/prepend_to accept pre-rendered html: like replace_to already did.

Note for coplan-square

Production there has no cache_store configured (the mem_cache_store line is commented out), so fragment caches fall back to the default file store — per-pod but functional. A follow-up in coplan-square to enable a real store (e.g. solid_cache like this repo's production config) would make the caching fully effective.

Testing

  • Full suite green: 942 examples, 0 failures, including all browser system specs (comment UX, anchoring, checkbox, history).
  • New request spec pins the inline-append response (comment HTML present in the turbo_stream response body, correct target).
  • Channel specs pass unchanged (test env has no cache store, so every ping broadcasts — old behavior preserved as the fallback).

🤖 Generated with Claude Code

Three sources of perceived lag, all behavior-preserving fixes:

1. Inline turbo-stream responses for the acting user. Comment create/
   delete and thread create/resolve/accept/discard/reopen previously
   responded with an EMPTY turbo stream and relied on the ActionCable
   broadcast to update the actor's own page — submit a comment and it
   only appeared after a SolidCable write, poll interval, websocket
   push, and DOM apply. The actor now gets the stream action inline in
   the HTTP response; broadcasts still fan out to other viewers. The
   partial is rendered once and shared by both paths. Turbo removes
   same-id children before append, so the echoed broadcast doesn't
   duplicate the inline copy.

2. Fragment-cache rendered plan bodies and index previews per content
   SHA. The full Commonmarker + Nokogiri + sanitize pipeline ran on
   every plan show, every content-mutation broadcast, and once per
   summary-less plan card on every index page. Cache keys use an
   explicit RENDER_CACHE_VERSION with skip_digest to keep template
   digesting off the write path.

3. Stop re-broadcasting the presence viewer list when nothing changed.
   Every viewer pings every 15s and each ping re-rendered and re-pushed
   the full list to every open tab. A fingerprint check skips the
   broadcast when the active-viewer set is unchanged; stale viewers
   still disappear because their departure changes the fingerprint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a8357e24b

ℹ️ 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".

Comment on lines +43 to +44
html = render_to_string(partial: "coplan/comment_threads/thread_popover", locals: { thread: thread, plan: @plan }, formats: [:html])
Broadcaster.append_to(@plan, target: "plan-threads", html: html)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't broadcast request-scoped form HTML

When an anchored thread is created, _thread_popover includes form_with/button_to forms for replies and thread actions. Rendering it in the actor's request and then broadcasting that exact HTML sends the actor's session-specific authenticity tokens to every subscribed collaborator; in clients that submit the form token without a valid per-viewer CSRF header this will 422, and it also leaks a CSRF secret. Keep the request-scoped render for the inline response, but render the broadcast payload in a requestless context (or omit/regenerate tokens per viewer).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

agreed

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 Reply from Hampton's AI agent (Claude Fable).

Fixed in 8273ce5. Confirmed the bug: render_to_string in the actor's request embedded that session's authenticity token in the popover's form_with/button_to hidden inputs, and the broadcast shipped it to every subscriber.

Broadcasts now go through Broadcaster's partial:/locals: path, which renders in a requestless context (same as the pre-existing broadcast pattern elsewhere in the app) — applied to comment create/destroy, thread create, and all four thread status actions. Only the actor's inline turbo_stream response keeps the request-scoped render, so the zero-round-trip feedback from this PR is unchanged; the cost is one extra render per write.

Also added a warning comment on Broadcaster (html: must never carry request-rendered form HTML) and request specs pinning that these broadcasts pass partial:, not html:.

HamptonMakes and others added 2 commits July 16, 2026 11:04
…m HTML

Review feedback (Codex, ack'd by Hampton): reusing the actor's
render_to_string output for broadcasts sent HTML whose form_with/
button_to hidden inputs embed the acting session's authenticity token
to every subscribed viewer — leaking a per-session CSRF secret and
producing forms that 422 for any client that submits the embedded
token instead of the meta-tag header.

Broadcasts now go through Broadcaster's partial:/locals: path (rendered
in a requestless context, the long-standing pattern for broadcast
partials); only the actor's inline turbo_stream response uses the
request-scoped render. Costs one extra render per write, keeps the
zero-round-trip inline feedback. Specs pin that comment/thread
broadcasts pass partial:, not html:.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* origin/main:
  Markdown extensions: footnotes, hover definitions, collapsible sections (#142)
  Fix checkbox toggle targeting via sourcepos-derived line verification (#138)
  Skip comment template digest on writes (#137)

# Conflicts:
#	spec/requests/comments_spec.rb
@HamptonMakes
HamptonMakes merged commit 5615cdb into main Jul 16, 2026
4 checks passed
HamptonMakes added a commit that referenced this pull request Jul 16, 2026
* origin/main:
  Perf: instant comment feedback, cached markdown rendering, quieter presence (#139)
  Markdown extensions: footnotes, hover definitions, collapsible sections (#142)
  Fix checkbox toggle targeting via sourcepos-derived line verification (#138)
  Skip comment template digest on writes (#137)
HamptonMakes added a commit that referenced this pull request Jul 16, 2026
…pace

* origin/main:
  Human editing UI: markdown editor, status dropdown, tag editing (items 1+2) (#141)
  Perf: instant comment feedback, cached markdown rendering, quieter presence (#139)
  Markdown extensions: footnotes, hover definitions, collapsible sections (#142)
  Fix checkbox toggle targeting via sourcepos-derived line verification (#138)
  Skip comment template digest on writes (#137)

# Conflicts:
#	engine/app/assets/stylesheets/coplan/application.css
#	engine/app/controllers/coplan/plans_controller.rb
HamptonMakes added a commit that referenced this pull request Jul 16, 2026
* origin/main:
  Folders + sidebar workspace index (COPLAN items 6+7) (#145)
  Human editing UI: markdown editor, status dropdown, tag editing (items 1+2) (#141)
  Perf: instant comment feedback, cached markdown rendering, quieter presence (#139)
  Markdown extensions: footnotes, hover definitions, collapsible sections (#142)
  Fix checkbox toggle targeting via sourcepos-derived line verification (#138)
  Skip comment template digest on writes (#137)

# Conflicts:
#	db/schema.rb
#	engine/app/controllers/coplan/application_controller.rb
#	engine/app/helpers/coplan/plan_events_helper.rb
#	engine/app/models/coplan/plan.rb
#	engine/app/views/coplan/plans/show.html.erb
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