11const pendingBatches = new Map ( )
22const activeBatches = new Map ( )
3+ const appliedRevisions = new Map ( )
4+ let requestSequence = 0
35
46class SolidObjectsBatchRefreshElement extends HTMLElement {
57 connectedCallback ( ) {
@@ -34,18 +36,26 @@ class SolidObjectsBatchRefreshElement extends HTMLElement {
3436 pendingBatches . set ( key , merged )
3537 queueMicrotask ( ( ) => {
3638 pendingBatches . delete ( key )
37- requestBatch ( group , batch , merged . sources )
39+ requestBatch ( group , batch , revision , merged . sources )
3840 } )
3941 this . remove ( )
4042 }
4143}
4244
43- async function requestBatch ( group , batch , sources ) {
44- const previous = activeBatches . get ( group )
45- previous ?. abort ( )
46-
45+ // Invalidations for one revision arrive in separate WebSocket messages, so the
46+ // microtask merge cannot see them all. Requests are tracked per revision and a
47+ // request is only cancelled by a strictly newer one; same-revision requests run
48+ // alongside each other and every frame is applied.
49+ async function requestBatch ( group , batch , revision , sources ) {
50+ const parsed = parseRevision ( revision )
51+ supersedeOlderRequests ( group , parsed )
52+
53+ // Same-revision requests run concurrently, so each needs its own entry.
54+ // Sharing one key per revision would leave all but the last untracked and
55+ // therefore impossible to supersede.
56+ const key = `${ group } :${ revision } :${ ( requestSequence += 1 ) } `
4757 const controller = new AbortController ( )
48- activeBatches . set ( group , controller )
58+ activeBatches . set ( key , { controller, group , revision : parsed } )
4959
5060 try {
5161 const url = mergedUrl ( sources )
@@ -68,10 +78,29 @@ async function requestBatch(group, batch, sources) {
6878 } catch ( error ) {
6979 if ( error . name !== "AbortError" ) dispatchBatchError ( batch , "request_failed" )
7080 } finally {
71- if ( activeBatches . get ( group ) === controller ) activeBatches . delete ( group )
81+ if ( activeBatches . get ( key ) ?. controller === controller ) activeBatches . delete ( key )
7282 }
7383}
7484
85+ function supersedeOlderRequests ( group , revision ) {
86+ if ( ! revision ) return
87+
88+ activeBatches . forEach ( ( entry , key ) => {
89+ if ( entry . group !== group ) return
90+ if ( ! olderRevision ( entry . revision , revision ) ) return
91+
92+ entry . controller . abort ( )
93+ activeBatches . delete ( key )
94+ } )
95+ }
96+
97+ function olderRevision ( candidate , current ) {
98+ if ( ! candidate || ! current ) return false
99+
100+ return candidate [ 0 ] < current [ 0 ] ||
101+ ( candidate [ 0 ] === current [ 0 ] && candidate [ 1 ] < current [ 1 ] )
102+ }
103+
75104// Every notification for one batch and revision carries the same endpoint and
76105// differs only by which components changed, so the union of their tokens is the
77106// complete set to render.
@@ -93,6 +122,13 @@ function applyFrame(frame) {
93122 const target = document . getElementById ( frame ?. target )
94123 if ( ! target || ! frame . html ) return
95124 if ( ! newerRevision ( frame . revision , target . dataset . solidObjectsRevision ) ) return
125+ // Concurrent same-revision responses can carry the same frame. The target's
126+ // own revision only advances once Turbo applies the stream, so what has
127+ // already been applied is tracked here as well.
128+ const applied = appliedRevisions . get ( frame . target )
129+ if ( applied && ! newerRevision ( frame . revision , applied ) ) return
130+
131+ appliedRevisions . set ( frame . target , frame . revision )
96132
97133 const parsed = new DOMParser ( ) . parseFromString ( frame . html , "text/html" )
98134 const replacement = parsed . getElementById ( frame . target )
0 commit comments