Skip to content

Auto refresh with#2305

Open
vcombey wants to merge 6 commits intodigitallyinduced:masterfrom
vcombey:auto-refresh-with
Open

Auto refresh with#2305
vcombey wants to merge 6 commits intodigitallyinduced:masterfrom
vcombey:auto-refresh-with

Conversation

@vcombey
Copy link
Copy Markdown

@vcombey vcombey commented Feb 6, 2026

No description provided.

@vcombey vcombey force-pushed the auto-refresh-with branch 4 times, most recently from 22d76f0 to c1ed23c Compare February 13, 2026 23:35
@vcombey
Copy link
Copy Markdown
Author

vcombey commented Feb 13, 2026

what do you think @mpscholten ?
I refined it, tried to minimise the diff and make the interface clean.
Tested it with this project: https://github.com/vcombey/test-autorefresh-with. Seems to be working fine.

@mpscholten
Copy link
Copy Markdown
Member

Can you some more details why this feature is needed? I've never seen a project where auto refresh was the bottleneck

@vcombey
Copy link
Copy Markdown
Author

vcombey commented Feb 14, 2026

Well I didn't found the bottleneck but the current autorefresh seems not scalable.

I did a project with ihp-openai to generate CV. streming live the cv generation with autorefresh. ( the doc of ihp-openai advise using autorefresh + a job ).
It works well.
But the openai job is making maybe 10 sql write per second for 1 user.

So if i have 1000 users connected to this page (doing nothing), for each write of the openai job of the other user, it will refresh the page server side for each user connected which would do at least 10000 sql reqd per second (if the route contains only 1 query) ?

And If the 1000 users do each one openai request at the same time, it would even do 1000 * 1000 * 10 = 10 M sql request per second !

With the autorefreshWith, it supress all these autorefresh read as it refresh only when should refresh returns true. so we replace 10M sql request -> 1000 if in should refresh.

Do I missunderstood something ?

@vcombey
Copy link
Copy Markdown
Author

vcombey commented Feb 14, 2026

  • I really love autorefresh and i have another pr making it work with htmx
    So fragment of the page can autorefresh.
    It feels so magical and simple to code!. but will even increase the scalability issues without autorefreshWith

@mpscholten
Copy link
Copy Markdown
Member

makes sense. the new interface just feels a bit complex. we basically solved the same problems in data sync already. likely we could just copy some of the ideas and transfer them to auto refresh.

The ideal interface would just be like todays autoRefresh do ..., but it's itself smart enough to only watch for changes that are relevant to that action.

In datasync we do this by keeping track of all ids of all objects. Then for an UPDATE sql query we can detect whether that should trigger a re-render by looking it up in set of ids.

Here's some thoughts from claude:

⏺ Here's what I see. The PR essentially re-invents three things DataSync already has:

  1. Row-level triggers with to_jsonb(OLD) / to_jsonb(NEW) — DataSync's createNotificationFunction in ChangeNotifications.hs already does this, including the DO $$ BEGIN ... EXCEPTION WHEN SQLSTATE 'XX000' THEN null; END $$ wrapper for race safety. The PR
  loses that protection.

  2. large_pg_notifications table — Same pattern, same 7800 byte threshold, same 30s cleanup. Duplicated.

  3. JSON payload parsing — Different types but same concept.

  The key difference in DataSync's approach is actually simpler and more powerful: instead of giving users a raw shouldRefresh :: ChangeSet -> IO Bool callback to write, DataSync tracks watched record IDs automatically. It knows which records were in the
  initial query result, so when a notification arrives it just checks Set.member id watchedRecordIdsRef. No user code needed.

  For Vincent's use case (1000 users on a CV page, each watching their own CV while OpenAI jobs write to the same table), the ideal fix would be closer to DataSync's approach:

  1. Use the same row-level triggers DataSync already has (the notification includes the row id)
  2. Track which record IDs each auto-refresh session fetched during the initial render (AutoRefresh already hooks into fetch via withTableReadTracker — extending it to also capture record IDs would be straightforward)
  3. Only fire the MVar when a watched ID changes — no user-facing shouldRefresh callback needed at all

  This way a user would just write:
  action ShowCvAction { cvId } = autoRefresh do  -- same API, no callback needed
      cv <- fetch cvId
      render ShowCvView { .. }

  And AutoRefresh would automatically know this session only cares about that specific cvId, because it saw it go through fetch. When another user's CV row gets updated, the notification includes the changed row's id, AutoRefresh sees it's not in this
  session's watched set, and skips the re-render. Zero user effort.

  The implementation could share the trigger infrastructure from ihp-datasync (moved to ihp core or duplicated minimally), and the filtering is much simpler than the full shouldRefresh callback + rowFieldNew @"userId" API the PR proposes.

  The main question is whether auto-refresh should also handle the case where the set of relevant records changes (e.g., new insert that matches a WHERE clause). DataSync handles this with a re-query, but for auto-refresh it might be fine to just always
  re-render on INSERTs and DELETEs (which are far less frequent than UPDATEs in Vincent's scenario) and only use ID filtering for UPDATEs.

@vcombey
Copy link
Copy Markdown
Author

vcombey commented Feb 14, 2026

would be nice if it is automatic indeed.
But i am afraid it would not be deductible in all cases.
For exemple:
If i have a query
select * from sections where cv_id = {cv_id}
And a job is inserting new sections record.

for Datasync, it does:

ChangeNotifications.DidInsert { id } -> do
    ...
    newRecord :: [[Field]] <- sqlQueryWithRLS hasqlPool
        (wrapDynamicQuery
            (Snippet.sql "SELECT * FROM (" <> theSnippet <> Snippet.sql ") AS records WHERE records.id = "
                <> Snippet.param id <> Snippet.sql " LIMIT 1"))
        dynamicRowDecoder

So it still has the scaling issue in case of inserts. performing number of connected users sql read for each on each insert

And in datasync we know exactly the requests performed. In autorefresh, it currently only track tables, is it possible to track the query ? and even impossible if pgquery is used in the route ?

whereas with autorefreshWith i can add a user_id on all records, create a helper and use that helper on all autorefresh for exemple.
shouldRefreshForCurrentUser changes = pure (anyChangeWithField @"userId" userId changes)

Maybe the doc exemples are too complex?
We could add a shouldRefreshForCurrentUser helper ?

It is true the notification table is duplicated though.

@vcombey vcombey force-pushed the auto-refresh-with branch 5 times, most recently from a2282e4 to f5e2d04 Compare February 18, 2026 19:22
@vcombey
Copy link
Copy Markdown
Author

vcombey commented Feb 18, 2026

what do you think @mpscholten ?

@mpscholten mpscholten closed this Feb 21, 2026
@mpscholten mpscholten reopened this Feb 21, 2026
@vcombey
Copy link
Copy Markdown
Author

vcombey commented Feb 21, 2026

Seems other reactive systems doesnt fully solve the scaling issue automatically either.

Maybe doing what datasync do is enough as the sql query to check is verry cheap ?
Otherwise maybe we can limit the number of checked per time interval.
Like only recheck if last check was 100ms second before. and schedule a check 100ms second after.

For exemple, for this senario:
So if i have 1000 users connected to this page (doing nothing), for each write of the openai job of the other user, it will refresh the page server side for each user connected which would do at least 10000 sql reqd per second (if the route contains only 1 query) ? And If the 1000 users do each one openai request at the same time, it would even do 1000 * 1000 * 10 = 10 M sql request per second !

it would only do ~ 1000 * 10 queries so still scaling linearly with the number of users. while having an acceptable 100ms latency.

@mpscholten
Copy link
Copy Markdown
Member

I think we ned to extend the trackTableRead function to capture the actual sql query as well. Then we could apply the optimizations from data sync

vincentcombey-design and others added 4 commits February 21, 2026 18:45
Make autoRefresh smart by tracking fetched row IDs and filtering
notifications in Haskell — no SQL at notification time, zero API changes.

On notification:
- UPDATE/DELETE: extract row ID from payload JSON, skip if not in tracked set
- INSERT: conservative refresh (new row, can't check without filter values)
- Tables without ID tracking (raw SQL, fetchCount): always refresh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
autoRefresh now always uses ID-based smart filtering, making the
separate autoRefreshWith API unnecessary. This also resolves the
subscription conflict where registerSmartNotificationTrigger and
registerRowNotificationTrigger shared subscribedRowTables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mpscholten mpscholten force-pushed the auto-refresh-with branch 3 times, most recently from a95265d to 0b67156 Compare February 21, 2026 18:10
@mpscholten mpscholten force-pushed the auto-refresh-with branch 3 times, most recently from a6c2453 to 535891b Compare February 21, 2026 18:18
Track WHERE conditions alongside row IDs during fetch. On INSERT
notifications, evaluate the new row against the query's conditions
using the hasql encoder printer. Rows that don't match the filters
(e.g. different projectId) skip the re-render.

- Add getParamPrinterText to extract text values from Encoders.Params
- Track conditions via ModelContext callback and Dynamic wrapper
- Evaluate ColumnCondition (EqOp, IsOp, InOp), And/Or trees
- Safe fallback to refresh for unsupported operators (LIKE, etc.)
- 16 new tests for matchesInsertPayload and shouldRefreshForPayload

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

3 participants