solid_objects 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c9697331316791269c98ec881f02a59e7556c2ab2abf98c3feb662e5f8cf742
4
- data.tar.gz: 12b0390bfcceca1362e7e1779bcb1562ea2e640259ca9ccefac9835b0b2d9a2e
3
+ metadata.gz: de78ca94f2c12e3c08d50c7b2512d6dbda67a47571540390c7494c6a828f1d35
4
+ data.tar.gz: b194c137ff3110cabb6cb2f7319819fdd64f314a9ae2113868438a89acce36b8
5
5
  SHA512:
6
- metadata.gz: 1acdc877187c455dee2843527e8826a719ab66c2f3917233eb1d565c19b6662a9fd6d319213f3340ada04e60f2d299dff8ad675de8762f65e336630a40b7ef79
7
- data.tar.gz: f69a34ac8c724164ec597ad3b78637d8537c1d76a527998aa50fa9f7bd7c3327244449be9555a2214c2a9ae7f019e119b5f35c58ad2d28528ea824aad0a0f538
6
+ metadata.gz: 78834ecb346469854b6d21d938fe3a6ed2689cd60cb037b22b3ce00025466c172ceb3dacdfcf94f88b0e0739e3dcac1c3e84896c6685bc4fe53d4db565ab8513
7
+ data.tar.gz: 31fbae44dce542e1fc07fe218bf504eb8627583ecc64895e8d86a5805340aeea47531b5f08a6e844809c31b513998c53c28b90993ae9cd40b864e868323eee27
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.3 - 2026-08-09
4
+
5
+ - Coordinate batched component refreshes by revision as well as scope and batch
6
+ name. Invalidations for one revision arrive as separate WebSocket messages, so
7
+ the microtask merge could not see them all, and each request aborted the one
8
+ before it. Only the last component updated. Same-revision requests now run
9
+ alongside each other and every frame is applied; only a strictly newer
10
+ revision supersedes an in-flight request. Frames already applied at a revision
11
+ are not applied twice.
12
+
3
13
  ## 0.7.2 - 2026-08-09
4
14
 
5
15
  - Render batched component partials as HTML regardless of the request format.
@@ -1,5 +1,7 @@
1
1
  const pendingBatches = new Map()
2
2
  const activeBatches = new Map()
3
+ const appliedRevisions = new Map()
4
+ let requestSequence = 0
3
5
 
4
6
  class SolidObjectsBatchRefreshElement extends HTMLElement {
5
7
  connectedCallback() {
@@ -34,18 +36,26 @@ class SolidObjectsBatchRefreshElement extends HTMLElement {
34
36
  pendingBatches.set(key, merged)
35
37
  queueMicrotask(() => {
36
38
  pendingBatches.delete(key)
37
- requestBatch(group, batch, merged.sources)
39
+ requestBatch(group, batch, revision, merged.sources)
38
40
  })
39
41
  this.remove()
40
42
  }
41
43
  }
42
44
 
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)}`
47
57
  const controller = new AbortController()
48
- activeBatches.set(group, controller)
58
+ activeBatches.set(key, { controller, group, revision: parsed })
49
59
 
50
60
  try {
51
61
  const url = mergedUrl(sources)
@@ -68,10 +78,29 @@ async function requestBatch(group, batch, sources) {
68
78
  } catch (error) {
69
79
  if (error.name !== "AbortError") dispatchBatchError(batch, "request_failed")
70
80
  } finally {
71
- if (activeBatches.get(group) === controller) activeBatches.delete(group)
81
+ if (activeBatches.get(key)?.controller === controller) activeBatches.delete(key)
72
82
  }
73
83
  }
74
84
 
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
+
75
104
  // Every notification for one batch and revision carries the same endpoint and
76
105
  // differs only by which components changed, so the union of their tokens is the
77
106
  // complete set to render.
@@ -93,6 +122,13 @@ function applyFrame(frame) {
93
122
  const target = document.getElementById(frame?.target)
94
123
  if (!target || !frame.html) return
95
124
  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)
96
132
 
97
133
  const parsed = new DOMParser().parseFromString(frame.html, "text/html")
98
134
  const replacement = parsed.getElementById(frame.target)
data/docs/realtime.md CHANGED
@@ -173,8 +173,11 @@ they are while giving the client a documented contract with per-frame revisions.
173
173
  ### What the protocol guarantees
174
174
 
175
175
  Only components whose dependencies changed are requested; the rest are never
176
- named in the batch. Duplicate notifications for the same batch and revision merge
177
- into one request, and a superseded request for the same batch is aborted. Each
176
+ named in the batch. Notifications for the same batch and revision that arrive in
177
+ one task merge into a single request. Notifications that arrive in separate
178
+ WebSocket messages issue their own requests and all of their frames are applied,
179
+ because cancelling a same-revision request would drop the components it carried.
180
+ Only a strictly newer revision supersedes an in-flight request. Each
178
181
  frame carries its own revision and cannot overwrite a target that already holds a
179
182
  newer one. Authorization is unchanged: every component in the batch passes the
180
183
  same `authorize_query` boundary an individual refresh uses, and the batch name is
@@ -1,5 +1,5 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  module SolidObjects
4
- VERSION = "0.7.2"
4
+ VERSION = "0.7.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_objects
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucas Carlson