hibiki_rails 0.17.1 → 0.17.2

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: 4919aaf1d8c96e1436138dfc5c0050602e0590447ccf9daebb5598072bf01e2d
4
- data.tar.gz: 214048d87eec87fa482744750f736eb3d3004381864e69c12b1664a10ffaf9a5
3
+ metadata.gz: 83e9656d09f3910405a4db7167b526c173158eb1035d5cb5129e4f916c88c278
4
+ data.tar.gz: e22b25605c6522e98cfdc86dcd685e62b3c60ed31ca865eb2daa53f74aaaf946
5
5
  SHA512:
6
- metadata.gz: f1da64047eb007f11f45f55007519808ad717f9b53968273455586c3977486f1e03e2478f9041c05a7f9d889191d6591a7e116e21d918dfb7719b337e0eeed3b
7
- data.tar.gz: 1ed922620989a1997ef4231a7c2e737cbca070ff3c3917966c74b1ed48c28e087e08fd7763152eeb96b713e8675b5126d831b07e5e4dc10e22c31d20cb908fdb
6
+ metadata.gz: 8c459eb9453de6af88c6e7859849a41dcbf7c0daa5d2e79da12e5ac8f729f4b85093e9d10c176d97cb8cd5141ef47ffeb0bcb2482aa03b223e42b2af4b13d9a6
7
+ data.tar.gz: 7ccd090ef5fdd72735d7bb45d356a1f7085fb6c323d42692c91b7226a5fd6ac058f0ea477efaa6d7693dfbfe66254d85c358e8ea5ca7d3fcd49f6fde56366040
data/CHANGELOG.md CHANGED
@@ -4,6 +4,44 @@ The gem and the npm package are released in lockstep and share these version
4
4
  numbers — `app/assets/javascripts/hibiki.js` is a single copy served both ways,
5
5
  so importmap and bundler apps always resolve identical client code.
6
6
 
7
+ ## 0.17.2 — 2026-10-11
8
+
9
+ ### Fixed
10
+
11
+ **A save the model refused could leave a `reactive_association` form unable
12
+ to save.** The form keeps one record between attempts. When a `commit` was
13
+ refused, the join rows were rolled back, but the record still remembered the
14
+ selection that attempt had written, and the next `commit` was compared with
15
+ that memory instead of the table. If the refused attempt had removed an id
16
+ that the next one still held, the retry inserted a join row that was already
17
+ there. With a unique index on the pair, which the `multiselect` generator
18
+ writes, that raised `ActiveRecord::RecordNotUnique` and the visitor saw a
19
+ Save that did nothing, every time, until the form was opened again. Without
20
+ the index it wrote a duplicate row.
21
+
22
+ - Any refused save could set it up, not only one about the association: a
23
+ blank name saved together with a removed tag was enough.
24
+ - A refused `commit` or `commit!` now unloads the associations named in
25
+ `reactive_association`, and the join association behind a
26
+ `has_many :through` or a `has_and_belongs_to_many`. The form keeps the
27
+ visitor's selection; nothing changes in the channel or the view.
28
+ - An app that calls `record.reload` before `commit` to get round this can
29
+ remove the call.
30
+ - `hibiki_associations` on a form class lists the names it declared.
31
+
32
+ **A form that had saved once could save its `reactive_association` wrongly
33
+ after another session changed the same record.** The cause is the same
34
+ memory. If the other session had added an id this form then added too, the
35
+ commit raised `ActiveRecord::RecordNotUnique`. If it had removed an id this
36
+ form still showed, the commit returned `true` and did not write it.
37
+
38
+ - `commit` now reads the association from the table each time, at the cost
39
+ of one query per association when it was already loaded.
40
+ - The later save still wins: the form's selection replaces the other
41
+ session's, as its other fields already did.
42
+ - Nested forms (`reactive_nested`) are not changed.
43
+ - The npm package is unchanged apart from its version.
44
+
7
45
  ## 0.17.1 — 2026-10-09
8
46
 
9
47
  ### Fixed
@@ -77,6 +77,7 @@ module Hibiki
77
77
  def reactive_association(*names)
78
78
  raise "#{self}: declare reactive_attributes before reactive_association" unless __hibiki_model_declared?
79
79
 
80
+ @hibiki_associations = [*hibiki_associations, *names.map(&:to_sym)]
80
81
  names.each { |name| __hibiki_ids_signal(name) }
81
82
  end
82
83
 
@@ -108,6 +109,10 @@ module Hibiki
108
109
  @hibiki_nested || __hibiki_inherited(:hibiki_nested) || {}
109
110
  end
110
111
 
112
+ def hibiki_associations
113
+ @hibiki_associations || __hibiki_inherited(:hibiki_associations) || []
114
+ end
115
+
111
116
  # The child form class, resolved on every use like hibiki_model so
112
117
  # a String declaration never pins a constant across a reload.
113
118
  def hibiki_nested_form(name)
@@ -259,7 +264,7 @@ module Hibiki
259
264
  # have moved values, and #persisted? flips after an INSERT.
260
265
  # rubocop:disable-next Naming/PredicateMethod -- boolean without a ?, exactly like AR's #save
261
266
  def commit
262
- record = __hibiki_record!
267
+ record = __hibiki_unloaded_record
263
268
  if record.update(**to_h)
264
269
  # Unload before re-hydrating, so the reload honors the
265
270
  # association's own scope and drops destroyed rows.
@@ -272,13 +277,13 @@ module Hibiki
272
277
  # update APPENDS its built new children to the in-memory
273
278
  # association — a later success would insert them all again.
274
279
  # Unloading makes the next attempt start clean.
275
- __hibiki_reset_nested(record)
280
+ __hibiki_unload(record)
276
281
  false
277
282
  end
278
283
  end
279
284
 
280
285
  # The raising half. Re-assigns and raises ActiveRecord::RecordInvalid
281
- # (no rescue, and no AR constant named here); the nested associations
286
+ # (no rescue, and no AR constant named here); the record's associations
282
287
  # are unloaded even on the raise, so a rescued retry starts clean.
283
288
  def commit!
284
289
  return true if commit
@@ -287,7 +292,7 @@ module Hibiki
287
292
  begin
288
293
  record.update!(**to_h)
289
294
  ensure
290
- __hibiki_reset_nested(record)
295
+ __hibiki_unload(record)
291
296
  end
292
297
  end
293
298
 
@@ -359,6 +364,34 @@ module Hibiki
359
364
  self.class.hibiki_nested.each_key { |name| record.public_send(name).reset }
360
365
  end
361
366
 
367
+ # What a refused attempt leaves behind on the record.
368
+ def __hibiki_unload(record)
369
+ __hibiki_reset_nested(record)
370
+ __hibiki_reset_associations(record)
371
+ end
372
+
373
+ # A refused update rolls the join rows back, but the ids writer has
374
+ # already left the association holding the refused set, and the join
375
+ # association behind a :through (or a has_and_belongs_to_many) holding
376
+ # the join records it built. The next ids write would be diffed
377
+ # against that memory instead of the table, so both are unloaded.
378
+ def __hibiki_reset_associations(record)
379
+ self.class.hibiki_associations.each do |name|
380
+ association = record.association(name)
381
+ association.reset
382
+ reflection = association.reflection
383
+ record.association(reflection.through_reflection.name).reset if reflection.through_reflection?
384
+ end
385
+ end
386
+
387
+ # The record to write to. Its ids associations are unloaded first, so
388
+ # the ids writer reads the table and not what this record remembers
389
+ # from an earlier commit or a preload: rows another session wrote
390
+ # since would otherwise be inserted twice, or silently left out.
391
+ def __hibiki_unloaded_record
392
+ __hibiki_record!.tap { __hibiki_reset_associations(it) }
393
+ end
394
+
362
395
  def __hibiki_record!
363
396
  record || raise("#{self.class} has no record — build it with .from(record)")
364
397
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Hibiki
4
4
  module Rails
5
- VERSION = "0.17.1"
5
+ VERSION = "0.17.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hibiki_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.1
4
+ version: 0.17.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - planetaska
@@ -216,7 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
216
216
  - !ruby/object:Gem::Version
217
217
  version: '0'
218
218
  requirements: []
219
- rubygems_version: 4.0.16
219
+ rubygems_version: 4.0.20
220
220
  specification_version: 4
221
221
  summary: 'Rails glue for hibiki: connection-scoped signal graphs over ActionCable
222
222
  + Turbo Streams.'