hibiki_rails 0.17.2 → 0.18.0
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 +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +46 -0
- data/app/assets/javascripts/hibiki.js +57 -5
- data/lib/hibiki/rails/broadcasts.rb +9 -4
- data/lib/hibiki/rails/channel.rb +12 -3
- data/lib/hibiki/rails/instrumentation.rb +71 -0
- data/lib/hibiki/rails/registry.rb +7 -0
- data/lib/hibiki/rails/version.rb +1 -1
- data/lib/hibiki/rails.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 915a98a11ca62685f60697d27383e8592978e0c2173b0e984c24eca7736cef55
|
|
4
|
+
data.tar.gz: 070cd14175c3027b164ec0b52378b59178499f3f3474fe3296722f6432c580ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d07fa8e0a897a21cce85e559e42a02dc78090f89986bc37c24202a36fb81322287a1ae86fb4c5166aa615a4ee8924732bb63020643a2e4db8e8ec72a5d870eb
|
|
7
|
+
data.tar.gz: 993b20b7af2d105e5c067823621eb8050ff426a57a9ac0b0aacccbd2a9fd9136a9a2a6f0f635d7932952f7c4c58bef2645492c67fe1b79acbb28d1c2e86f5a37
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ 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.18.0 — 2026-10-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
**Instrumentation events.** A channel now publishes
|
|
12
|
+
`ActiveSupport::Notifications` events, so an app can time what a graph does.
|
|
13
|
+
Action Cable's own `perform_action.action_cable` covers only the hand-off to
|
|
14
|
+
the graph's thread; the action, its effects and its renders run after it.
|
|
15
|
+
|
|
16
|
+
| Event | Duration covers | Payload |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `subscribe.hibiki_rails` | building the graph and its first renders | `channel` |
|
|
19
|
+
| `unsubscribe.hibiki_rails` | disposing the graph | `channel` |
|
|
20
|
+
| `action.hibiki_rails` | the action and every effect it flushed | `channel`, `action`, `wait`, `renders` |
|
|
21
|
+
| `render.hibiki_rails` | one `broadcast_replace`, `broadcast_morph` or `broadcast_refresh` | `channel`, `target`, `method` |
|
|
22
|
+
|
|
23
|
+
- The events are always on and need no configuration. The gem subscribes to
|
|
24
|
+
none of them. With no subscriber an action costs one to two microseconds
|
|
25
|
+
more than in 0.17.2.
|
|
26
|
+
- A subscriber runs on the graph's thread, so keep it to cheap work in
|
|
27
|
+
memory.
|
|
28
|
+
- An action the channel does not have publishes nothing. A raising action
|
|
29
|
+
publishes, with the exception in the payload.
|
|
30
|
+
- `transmit_value` and `transmit_url` have no event of their own; Action
|
|
31
|
+
Cable's `transmit.action_cable` covers them.
|
|
32
|
+
|
|
33
|
+
**`Hibiki::Rails.registry.tally`** returns the live subscriptions by channel
|
|
34
|
+
class name.
|
|
35
|
+
|
|
36
|
+
**Trips: how long a visitor waited.** With `HibikiController.reportTrips =
|
|
37
|
+
true`, each island times its round trips and sends them with its next action.
|
|
38
|
+
The channel publishes one `trip.hibiki_rails` event for each, with `channel`,
|
|
39
|
+
`action`, `ack` (milliseconds until the server answered) and `paint`
|
|
40
|
+
(milliseconds until the first render arrived, or `nil`).
|
|
41
|
+
|
|
42
|
+
- Off by default. Set it once, on the class, for every island.
|
|
43
|
+
- A dropped or stalled trip is not reported, and neither is the last trip on
|
|
44
|
+
a page, since no action follows it.
|
|
45
|
+
- The figures come from the browser. The channel takes at most five per
|
|
46
|
+
action, only for its own actions, and clamps each to 0 to 60 000 ms.
|
|
47
|
+
- `hbt` joins `hbk` as a reserved payload key. The channel removes it before
|
|
48
|
+
the action method sees its data.
|
|
49
|
+
|
|
7
50
|
## 0.17.2 — 2026-10-11
|
|
8
51
|
|
|
9
52
|
### Fixed
|
data/README.md
CHANGED
|
@@ -125,6 +125,52 @@ The generated components are just Rails partials (or Phlex components, if you us
|
|
|
125
125
|
|
|
126
126
|
Congratulations! Now you have your first reactive component!
|
|
127
127
|
|
|
128
|
+
## Instrumentation
|
|
129
|
+
|
|
130
|
+
A channel publishes `ActiveSupport::Notifications` events. They are always
|
|
131
|
+
on, and the gem subscribes to none of them.
|
|
132
|
+
|
|
133
|
+
| Event | Duration covers | Payload |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `subscribe.hibiki_rails` | building the graph and its first renders | `channel` |
|
|
136
|
+
| `unsubscribe.hibiki_rails` | disposing the graph | `channel` |
|
|
137
|
+
| `action.hibiki_rails` | the action and every effect it flushed | `channel`, `action`, `wait`, `renders` |
|
|
138
|
+
| `render.hibiki_rails` | rendering and handing the stream to Action Cable | `channel`, `target`, `method` |
|
|
139
|
+
| `trip.hibiki_rails` | nothing | `channel`, `action`, `ack`, `paint` |
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
ActiveSupport::Notifications.subscribe("action.hibiki_rails") do |event|
|
|
143
|
+
Rails.logger.info "#{event.payload[:channel]}##{event.payload[:action]} #{event.duration.round(1)}ms"
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- `channel` is the class name. `wait` is the milliseconds the action queued
|
|
148
|
+
for the graph's thread. `renders` is how many renders it caused.
|
|
149
|
+
- `method` is `:replace`, `:morph` or `:refresh`. A refresh has no `target`.
|
|
150
|
+
- An action the channel does not have publishes nothing. A raising action
|
|
151
|
+
publishes, with the exception in the payload.
|
|
152
|
+
- Action Cable's `broadcast.action_cable` fires inside the render event, on
|
|
153
|
+
the same thread, and carries the message if you want its size.
|
|
154
|
+
- `Hibiki::Rails.registry.tally` returns the live subscriptions by channel
|
|
155
|
+
class, and `registry.size` the total.
|
|
156
|
+
|
|
157
|
+
**A subscriber runs on the graph's thread.** Keep it to cheap work in
|
|
158
|
+
memory; anything slow holds up that visitor's actions.
|
|
159
|
+
|
|
160
|
+
A `trip` is the browser's own timing of one action: `ack` is the
|
|
161
|
+
milliseconds until the server answered and `paint` the milliseconds until
|
|
162
|
+
its first render arrived, or `nil` when nothing rendered. Reporting is off
|
|
163
|
+
until the app turns it on:
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
import HibikiController from "hibiki-rails"
|
|
167
|
+
HibikiController.reportTrips = true
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Each island then sends its last trips with its next action. The figures come
|
|
171
|
+
from visitors' browsers, so treat them as reported: the gem bounds them (five
|
|
172
|
+
per action, known action names, 0 to 60 000 ms) and cannot verify them.
|
|
173
|
+
|
|
128
174
|
## Documentation
|
|
129
175
|
|
|
130
176
|
Documentation site: <https://planetaska.github.io/hibiki/rails-introduction/>
|
|
@@ -284,6 +284,10 @@ export class ChannelController extends Controller {
|
|
|
284
284
|
static busyCeiling = 10000
|
|
285
285
|
static focusGrace = 500
|
|
286
286
|
|
|
287
|
+
// Whether to tell the server how long each trip took (see "Trips"
|
|
288
|
+
// below). Off unless an app sets it, once, for every island.
|
|
289
|
+
static reportTrips = false
|
|
290
|
+
|
|
287
291
|
async connect() {
|
|
288
292
|
this.prepareTransport()
|
|
289
293
|
await this.openSubscription()
|
|
@@ -302,6 +306,7 @@ export class ChannelController extends Controller {
|
|
|
302
306
|
this.seq = 0
|
|
303
307
|
this.renders = 0
|
|
304
308
|
this.busy = new Map()
|
|
309
|
+
this.trips = []
|
|
305
310
|
this.settledAt = -Infinity
|
|
306
311
|
this.lastControl = null
|
|
307
312
|
this.queued = []
|
|
@@ -345,7 +350,8 @@ export class ChannelController extends Controller {
|
|
|
345
350
|
// a sequence number under the reserved `hbk` key and opens a busy record
|
|
346
351
|
// that the matching ack closes; the seq is stamped LAST so a form field
|
|
347
352
|
// can never overwrite it. (`hbk` is the second reserved payload key —
|
|
348
|
-
// ActionCable's own Subscription#perform already writes `action`.
|
|
353
|
+
// ActionCable's own Subscription#perform already writes `action`. `hbt`
|
|
354
|
+
// is the third: the trips timed since the last perform.)
|
|
349
355
|
//
|
|
350
356
|
// Public API on the island controller (the header's "App JS reaching
|
|
351
357
|
// the graph"). Returns the seq when the action was accepted, so a
|
|
@@ -356,8 +362,10 @@ export class ChannelController extends Controller {
|
|
|
356
362
|
perform(action, payload = {}) {
|
|
357
363
|
const seq = ++this.seq
|
|
358
364
|
payload.hbk = seq
|
|
365
|
+
delete payload.hbt
|
|
359
366
|
if (this.subscribed) {
|
|
360
|
-
this.
|
|
367
|
+
this.sendTrips(payload)
|
|
368
|
+
this.beginBusy(seq, action)
|
|
361
369
|
// Subscription#perform returns false when the socket is not open —
|
|
362
370
|
// the gap between the socket dying and the connection monitor
|
|
363
371
|
// noticing, during which `subscribed` still says live. The frame
|
|
@@ -383,7 +391,8 @@ export class ChannelController extends Controller {
|
|
|
383
391
|
// whole gap — the signal the connect window cannot give, where the
|
|
384
392
|
// page is painted and looks live.
|
|
385
393
|
if (!this.connectedOnce) {
|
|
386
|
-
this.
|
|
394
|
+
this.sendTrips(payload)
|
|
395
|
+
this.beginBusy(seq, action)
|
|
387
396
|
this.queued.push([action, payload])
|
|
388
397
|
return seq
|
|
389
398
|
}
|
|
@@ -430,9 +439,11 @@ export class ChannelController extends Controller {
|
|
|
430
439
|
// means the server has not answered, and a hold is motion after the
|
|
431
440
|
// answer.
|
|
432
441
|
|
|
433
|
-
beginBusy(seq) {
|
|
442
|
+
beginBusy(seq, action) {
|
|
434
443
|
const record = {
|
|
435
444
|
control: null,
|
|
445
|
+
action,
|
|
446
|
+
at: performance.now(),
|
|
436
447
|
// Which render the island was on when this started, so the ack can
|
|
437
448
|
// ask "has anything painted since?" without consulting a clock.
|
|
438
449
|
renders: this.renders,
|
|
@@ -486,6 +497,7 @@ export class ChannelController extends Controller {
|
|
|
486
497
|
if (this.state === "stalled") this.setState("ready")
|
|
487
498
|
const record = this.busy.get(ack)
|
|
488
499
|
if (!record) return
|
|
500
|
+
if (!dropped) record.ack = Math.round(performance.now() - record.at)
|
|
489
501
|
if (dropped || this.renders > record.renders) return this.settle(ack)
|
|
490
502
|
record.grace = setTimeout(() => this.settle(ack), this.constructor.busyGrace)
|
|
491
503
|
}
|
|
@@ -499,6 +511,7 @@ export class ChannelController extends Controller {
|
|
|
499
511
|
this.settledAt = performance.now()
|
|
500
512
|
record.control?.removeAttribute("data-hibiki-busy")
|
|
501
513
|
if (this.busy.size === 0) this.hideBusy()
|
|
514
|
+
if (record.ack !== undefined && this.constructor.reportTrips) this.keepTrip(record)
|
|
502
515
|
}
|
|
503
516
|
|
|
504
517
|
settleAll() {
|
|
@@ -528,13 +541,49 @@ export class ChannelController extends Controller {
|
|
|
528
541
|
this.setState("stalled")
|
|
529
542
|
}
|
|
530
543
|
|
|
544
|
+
// ── Trips ─────────────────────────────────────────────────────────────
|
|
545
|
+
//
|
|
546
|
+
// Only the browser sees both halves of a trip land: the ack on the
|
|
547
|
+
// island's socket and the render after its pubsub hop. With reportTrips
|
|
548
|
+
// on, each acked trip is kept as its action, the ms to the ack and the ms
|
|
549
|
+
// to the first render of its own (null when nothing rendered), and the
|
|
550
|
+
// next perform carries the last five under `hbt`. A dropped or stalled
|
|
551
|
+
// trip is not reported, and the last trip on a page never is.
|
|
552
|
+
|
|
553
|
+
keepTrip({ action, at, ack, paint }) {
|
|
554
|
+
this.trips.push({ action, at, ack, paint, settled: this.settledAt })
|
|
555
|
+
if (this.trips.length > 5) this.trips.shift()
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
sendTrips(payload) {
|
|
559
|
+
if (!this.trips.length) return
|
|
560
|
+
payload.hbt = this.trips.map(({ action, ack, paint }) => [action, ack, paint ?? null])
|
|
561
|
+
this.trips = []
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// A render of this island's own has arrived. Timed now, not when it is
|
|
565
|
+
// applied: the order and motion holds come after the answer. A trip that
|
|
566
|
+
// settled a moment ago still takes it, by the rule own() uses.
|
|
567
|
+
arrived() {
|
|
568
|
+
const now = performance.now()
|
|
569
|
+
for (const record of this.busy.values()) record.paint ??= Math.round(now - record.at)
|
|
570
|
+
for (const trip of this.trips) {
|
|
571
|
+
if (trip.paint === undefined && now - trip.settled <= this.constructor.focusGrace) {
|
|
572
|
+
trip.paint = Math.round(now - trip.at)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
531
577
|
// Every inbound frame lands here first. Acks are transport bookkeeping,
|
|
532
578
|
// handled by the base and deliberately NOT routed through received() — so
|
|
533
579
|
// a subclass that overrides received without calling super still clears
|
|
534
580
|
// its pending state.
|
|
535
581
|
handleMessage(data) {
|
|
536
582
|
if (data && "ack" in data) return this.acknowledge(data)
|
|
537
|
-
if (data && data.html)
|
|
583
|
+
if (data && data.html) {
|
|
584
|
+
this.renders++
|
|
585
|
+
this.arrived()
|
|
586
|
+
}
|
|
538
587
|
this.received(data)
|
|
539
588
|
}
|
|
540
589
|
|
|
@@ -855,6 +904,9 @@ export default class HibikiController extends ChannelController {
|
|
|
855
904
|
this.streamRender = (event) => {
|
|
856
905
|
const render = event.detail.render
|
|
857
906
|
const stream = event.detail.newStream
|
|
907
|
+
if (stream?.getAttribute?.("data-hibiki-from") === `${this.channelValue}/${this.cidValue}`) {
|
|
908
|
+
this.arrived()
|
|
909
|
+
}
|
|
858
910
|
// Taken now, not when the render's turn comes: see arrival().
|
|
859
911
|
const arrival = this.arrival()
|
|
860
912
|
event.detail.render = (streamElement) =>
|
|
@@ -13,14 +13,19 @@ module Hibiki
|
|
|
13
13
|
# All private — a public helper would become a client-invocable action
|
|
14
14
|
# (see the NOTE on Hibiki::Rails::Channel).
|
|
15
15
|
module Broadcasts
|
|
16
|
+
include Instrumentation
|
|
17
|
+
|
|
16
18
|
private
|
|
17
19
|
|
|
18
20
|
# Replace the element with DOM id `target` (accepts partial:/locals:,
|
|
19
21
|
# html:, or anything Turbo's renderer takes).
|
|
20
22
|
def broadcast_replace(target:, attributes: {}, **rendering)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
method = attributes[:method].to_s == "morph" ? :morph : :replace
|
|
24
|
+
instrument_render(target:, method:) do
|
|
25
|
+
Turbo::StreamsChannel.broadcast_replace_to(
|
|
26
|
+
*stream_name, target:, attributes: attributes.merge(render_stamp), **rendering
|
|
27
|
+
)
|
|
28
|
+
end
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
# Replace via Turbo 8 morphing (<turbo-stream action="replace"
|
|
@@ -32,7 +37,7 @@ module Hibiki
|
|
|
32
37
|
# Tell the page to refresh itself (Turbo 8 morph-everything style).
|
|
33
38
|
# Pair with Debounce when many writes should mean one refresh.
|
|
34
39
|
def broadcast_refresh
|
|
35
|
-
Turbo::StreamsChannel.broadcast_refresh_to(*stream_name)
|
|
40
|
+
instrument_render(method: :refresh) { Turbo::StreamsChannel.broadcast_refresh_to(*stream_name) }
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
# The Morph-everything style as one call: an effect that tracks
|
data/lib/hibiki/rails/channel.rb
CHANGED
|
@@ -77,13 +77,20 @@ module Hibiki
|
|
|
77
77
|
# produces zero bytes, and "clear on the next render" would hang
|
|
78
78
|
# forever. `delete`, not `[]`: the key must not reach the action
|
|
79
79
|
# method's data. No `hbk`, no ack, so a 0.3.0 page keeps working.
|
|
80
|
+
#
|
|
81
|
+
# `hbt` is the third reserved key: the trips the browser timed since
|
|
82
|
+
# its last action (see Instrumentation#trips_from). Deleted whether or
|
|
83
|
+
# not anyone listens, for the same reason as `hbk`.
|
|
80
84
|
def perform_action(data)
|
|
81
85
|
seq = data.delete("hbk")
|
|
86
|
+
trips = trips_from(data.delete("hbt"))
|
|
87
|
+
asked = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
82
88
|
|
|
83
89
|
# A nil actor means the subscription was rejected or already torn
|
|
84
90
|
# down; drop the action rather than blow up the cable thread.
|
|
85
91
|
posted = @__hibiki_actor&.post do
|
|
86
|
-
|
|
92
|
+
trips.each { instrument("trip", it) }
|
|
93
|
+
instrument_action(data, asked) { Hibiki.batch { super(data) } }
|
|
87
94
|
ensure
|
|
88
95
|
# Hibiki.batch flushes in its own ensure, so effects queued
|
|
89
96
|
# before a raise have run by the time this does — the ack is
|
|
@@ -111,7 +118,9 @@ module Hibiki
|
|
|
111
118
|
# The block runs owned + untracked (Hibiki.root); the effects it
|
|
112
119
|
# creates do their dependency-collecting first run right here, on
|
|
113
120
|
# the graph thread.
|
|
114
|
-
@__hibiki_actor.post
|
|
121
|
+
@__hibiki_actor.post do
|
|
122
|
+
instrument("subscribe") { @__hibiki_root = Hibiki.root { build_graph } }
|
|
123
|
+
end
|
|
115
124
|
Hibiki::Rails.registry.register(self)
|
|
116
125
|
end
|
|
117
126
|
|
|
@@ -123,7 +132,7 @@ module Hibiki
|
|
|
123
132
|
return unless @__hibiki_actor
|
|
124
133
|
|
|
125
134
|
Hibiki::Rails.registry.unregister(self)
|
|
126
|
-
@__hibiki_actor.post { @__hibiki_root.dispose }
|
|
135
|
+
@__hibiki_actor.post { instrument("unsubscribe") { @__hibiki_root.dispose } }
|
|
127
136
|
@__hibiki_actor.stop
|
|
128
137
|
end
|
|
129
138
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hibiki
|
|
4
|
+
module Rails
|
|
5
|
+
# The channel's ActiveSupport::Notifications events (`*.hibiki_rails`).
|
|
6
|
+
# Always on: with no subscriber `instrument` only yields. The gem
|
|
7
|
+
# subscribes to none of them. A subscriber runs on the graph thread, so
|
|
8
|
+
# it holds up that graph for as long as it takes.
|
|
9
|
+
#
|
|
10
|
+
# All private — a public helper would become a client-invocable action
|
|
11
|
+
# (see the NOTE on Hibiki::Rails::Channel).
|
|
12
|
+
module Instrumentation
|
|
13
|
+
# The most trips one action may report, and the longest time one may
|
|
14
|
+
# claim, in ms. The figures come from a browser, so they are bounded
|
|
15
|
+
# rather than believed.
|
|
16
|
+
TRIP_LIMIT = 5
|
|
17
|
+
TRIP_RANGE = (0..60_000)
|
|
18
|
+
|
|
19
|
+
EVENTS = %w[subscribe unsubscribe action render trip].to_h { [it, "#{it}.hibiki_rails"] }.freeze
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# With nobody listening this is one lookup and a yield; no payload is
|
|
24
|
+
# built.
|
|
25
|
+
def instrument(event, payload = nil, &)
|
|
26
|
+
return (yield({}) if block_given?) unless listening?(event)
|
|
27
|
+
|
|
28
|
+
ActiveSupport::Notifications.instrument(EVENTS.fetch(event), { channel: self.class.name, **payload }, &)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# `asked` is the clock as the cable thread posted the action; `wait` is
|
|
32
|
+
# how long the graph thread took to reach it, in ms. An action the
|
|
33
|
+
# channel does not have gets no event: the name is the client's, and a
|
|
34
|
+
# subscriber that stores it must not be handed arbitrary strings.
|
|
35
|
+
def instrument_action(data, asked)
|
|
36
|
+
action = data["action"].to_s
|
|
37
|
+
return yield unless listening?("action") && self.class.action_methods.include?(action)
|
|
38
|
+
|
|
39
|
+
before = @__hibiki_render_count.to_i
|
|
40
|
+
instrument("action", { action:, wait: ms_since(asked) }) do |payload|
|
|
41
|
+
yield
|
|
42
|
+
ensure
|
|
43
|
+
payload[:renders] = @__hibiki_render_count.to_i - before
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def listening?(event) = ActiveSupport::Notifications.notifier.listening?(EVENTS.fetch(event))
|
|
48
|
+
|
|
49
|
+
def ms_since(clock) = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - clock) * 1000
|
|
50
|
+
|
|
51
|
+
def instrument_render(method:, target: nil, &)
|
|
52
|
+
@__hibiki_render_count = @__hibiki_render_count.to_i + 1
|
|
53
|
+
instrument("render", { target:, method: }.compact, &)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The trips a browser reported under `hbt`, as event payloads. Each is
|
|
57
|
+
# `[action, ack, paint]`: ms to the ack, and ms to the first render, or
|
|
58
|
+
# nil when nothing rendered.
|
|
59
|
+
def trips_from(reported)
|
|
60
|
+
reported.is_a?(Array) ? reported.first(TRIP_LIMIT).filter_map { trip(*it) } : []
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def trip(action = nil, ack = nil, paint = nil, *)
|
|
64
|
+
return unless action.is_a?(String) && self.class.action_methods.include?(action)
|
|
65
|
+
return unless ack.is_a?(Numeric) && (paint.nil? || paint.is_a?(Numeric))
|
|
66
|
+
|
|
67
|
+
{ action:, ack: ack.clamp(TRIP_RANGE), paint: paint&.clamp(TRIP_RANGE) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -28,6 +28,13 @@ module Hibiki
|
|
|
28
28
|
@lock.synchronize { @channels.size }
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# The live subscriptions by channel class name. Count from this rather
|
|
32
|
+
# than from subscribe and unsubscribe events: a development reload
|
|
33
|
+
# drops sockets without running `unsubscribed`.
|
|
34
|
+
def tally
|
|
35
|
+
@lock.synchronize { @channels.map { it.class.name }.tally }
|
|
36
|
+
end
|
|
37
|
+
|
|
31
38
|
# Close each distinct cable connection, which routes every one of
|
|
32
39
|
# its channels through the normal unsubscribed path — dispose root,
|
|
33
40
|
# stop actor, unregister — so teardown has exactly ONE code path and
|
data/lib/hibiki/rails/version.rb
CHANGED
data/lib/hibiki/rails.rb
CHANGED
|
@@ -68,6 +68,7 @@ require_relative "rails/version"
|
|
|
68
68
|
require_relative "rails/registry"
|
|
69
69
|
require_relative "rails/graph_actor"
|
|
70
70
|
require_relative "rails/debounce"
|
|
71
|
+
require_relative "rails/instrumentation"
|
|
71
72
|
require_relative "rails/broadcasts"
|
|
72
73
|
require_relative "rails/channel"
|
|
73
74
|
require_relative "rails/nested_actions"
|
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.
|
|
4
|
+
version: 0.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- planetaska
|
|
@@ -187,6 +187,7 @@ files:
|
|
|
187
187
|
- lib/hibiki/rails/engine.rb
|
|
188
188
|
- lib/hibiki/rails/graph_actor.rb
|
|
189
189
|
- lib/hibiki/rails/helpers.rb
|
|
190
|
+
- lib/hibiki/rails/instrumentation.rb
|
|
190
191
|
- lib/hibiki/rails/keys.rb
|
|
191
192
|
- lib/hibiki/rails/nested_actions.rb
|
|
192
193
|
- lib/hibiki/rails/reactive_form.rb
|