yrby-actioncable 0.2.3 → 0.3.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-actioncable.md +54 -0
- data/README.md +15 -0
- data/lib/y/action_cable/sync.rb +181 -24
- data/lib/y/action_cable/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c93259f7cd51c197d4203903e5f8b522e526e164a858f17992c538e0a0b1f6f4
|
|
4
|
+
data.tar.gz: 46289b0f4544899a64a23c40f44a998667b399f1a9f0b6cbc1c9cbd63e7d613d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab99efb5b5244556492419d8d646bd394f5e4f672729c1f6355571b0ab65c927122aa1833154150041fd550b0dc21a681a60f6bb5a952ac160fb05060e5609d5
|
|
7
|
+
data.tar.gz: 7d66edea6a44ba686d825da4d370d3e38be94a28c9df7aafea20ee9e506c31300d5011b94cb6d0fa2b62f0e3bfea2599984ec185503b96912fc037d58b8ae9cd
|
data/CHANGELOG-actioncable.md
CHANGED
|
@@ -6,6 +6,60 @@ this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] - 2026-07-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Unhealable-gap defense — on plain ActionCable AND AnyCable.** When the same
|
|
13
|
+
update is rejected as a causal gap repeatedly on one connection, the channel
|
|
14
|
+
now stops resyncing it forever and instead settles it and drops it. A gap that
|
|
15
|
+
survives repeated resyncs is unhealable — its missing dependency is gone for
|
|
16
|
+
good (a permanently-orphaned pending struct) — and resyncing it endlessly just
|
|
17
|
+
feeds the client's retransmit loop (the "id-less frames several times a
|
|
18
|
+
second" failure mode). A *healable* gap heals within a resync or two, well
|
|
19
|
+
under the limit, so this only trips on genuinely-dead updates.
|
|
20
|
+
|
|
21
|
+
- The settle ack carries `"dropped" => true` so an aware client can tell
|
|
22
|
+
"durably recorded" from "abandoned" and surface the loss instead of
|
|
23
|
+
silently reporting synced (`yrby-client` raises it via
|
|
24
|
+
`onError(..., "ack-dropped")`; older clients ignore the extra key).
|
|
25
|
+
- Configurable via `gap_strike_limit` (default `3`; `nil` disables — always
|
|
26
|
+
resync). Limits below `2` raise `ArgumentError`: strike 1 must send a
|
|
27
|
+
resync before any drop can be justified.
|
|
28
|
+
- Strikes are tracked per update (SHA-256) per connection, guarded by a
|
|
29
|
+
mutex (ActionCable dispatches a connection's messages to a worker pool).
|
|
30
|
+
The table is bounded; at capacity a single lowest-count entry is evicted,
|
|
31
|
+
and only when inserting a new key — a client cycling distinct gaps can't
|
|
32
|
+
reset a tracked key's count. A gap that finally records frees its slot.
|
|
33
|
+
- Under AnyCable — where every RPC command gets a fresh channel instance —
|
|
34
|
+
the table is persisted via anycable-rails' `state_attr_accessor` (istate,
|
|
35
|
+
round-tripped through anycable-go), declared automatically when
|
|
36
|
+
anycable-rails is loaded. Verified end-to-end on both stacks
|
|
37
|
+
(`frontend/gap_strike.mjs` in the demo).
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
Fixes from a full source review (a 0.2.4 was prepared but never published; its
|
|
42
|
+
changes ship here):
|
|
43
|
+
|
|
44
|
+
- **A lost-ack retry now re-broadcasts.** If the original attempt recorded the
|
|
45
|
+
update and then crashed (or the pub/sub broadcast failed) before
|
|
46
|
+
distributing, the retry was previously settled as `:applied` without
|
|
47
|
+
re-broadcasting — live subscribers stayed stale until their next full resync,
|
|
48
|
+
and nothing else could reach them. The retry now re-broadcasts before acking;
|
|
49
|
+
idempotent CRDT apply makes the duplicate free for every receiver.
|
|
50
|
+
- **A missing document key now fails closed.** Under a transport that doesn't
|
|
51
|
+
keep the channel instance alive across actions (AnyCable), an app that forgot
|
|
52
|
+
to pass `key` to `sync_receive` silently recorded updates under a nil key,
|
|
53
|
+
broadcast them to a stream no one subscribes to, and still acked them. The
|
|
54
|
+
frame now raises `Y::Error` instead.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- Raised the `yrby` floor to `>= 0.3.1`, whose `update_ready?` is exact
|
|
59
|
+
(trial-integration, not just per-client clocks). With an older core, a
|
|
60
|
+
cross-client-origin gap passed the ready check and the `update_advances?`
|
|
61
|
+
probe then acked-and-dropped real content.
|
|
62
|
+
|
|
9
63
|
## [0.2.3] - 2026-07-01
|
|
10
64
|
|
|
11
65
|
### Changed
|
data/README.md
CHANGED
|
@@ -242,6 +242,21 @@ servers:
|
|
|
242
242
|
causal dependencies are already in the store (checked against `on_load`); a
|
|
243
243
|
causally-incomplete update triggers a resync instead, so the log always
|
|
244
244
|
rebuilds cleanly.
|
|
245
|
+
- **An unhealable gap is dropped, not resynced forever.** A resync heals a gap
|
|
246
|
+
whose missing dependency is still in flight. But a *permanently*-orphaned update
|
|
247
|
+
(its dependency is gone for good) stays gappy through every resync, and a client
|
|
248
|
+
retransmitting it would loop endlessly (server resyncs → client resends →
|
|
249
|
+
repeat). After `gap_strike_limit` rejections of the same update on one
|
|
250
|
+
connection (default 3, minimum 2), the channel settles it with
|
|
251
|
+
`{ "ack" => id, "dropped" => true }` and drops it instead of resyncing again —
|
|
252
|
+
breaking the loop while never dropping a *healable* gap (those heal within a
|
|
253
|
+
resync or two, and healing frees the strike). The `dropped` flag lets the
|
|
254
|
+
client surface the loss (`yrby-client` reports it via `onError`) instead of
|
|
255
|
+
silently showing synced. Set `gap_strike_limit nil` to disable. Works on both
|
|
256
|
+
transports: plain ActionCable keeps strikes on the channel instance; under
|
|
257
|
+
AnyCable (fresh instance per RPC command) they persist through
|
|
258
|
+
anycable-rails' `state_attr_accessor` (istate), declared automatically when
|
|
259
|
+
anycable-rails is loaded.
|
|
245
260
|
- **`on_change` is at-least-once, and the durable guarantee is that replaying the
|
|
246
261
|
log reconstructs the document.** Every update triggers `on_change` before it's acked or
|
|
247
262
|
broadcast (record-before-distribute). If exactly-once updates matter for you, **you
|
data/lib/y/action_cable/sync.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "y"
|
|
4
4
|
require "base64"
|
|
5
|
+
require "digest"
|
|
5
6
|
|
|
6
7
|
module Y::ActionCable # rubocop:disable Style/ClassAndModuleChildren
|
|
7
8
|
# y-websocket protocol over ActionCable.
|
|
@@ -51,8 +52,31 @@ module Y::ActionCable # rubocop:disable Style/ClassAndModuleChildren
|
|
|
51
52
|
# allocation/parse cost. Override per channel with `max_frame_bytes`.
|
|
52
53
|
DEFAULT_MAX_FRAME_BYTES = 8 * 1024 * 1024
|
|
53
54
|
|
|
55
|
+
# After this many times the *same* update is rejected as a causal gap on one
|
|
56
|
+
# connection, stop resyncing and instead settle it (ack) + drop it. A gap
|
|
57
|
+
# that survives repeated resyncs is unhealable — its missing dependency is
|
|
58
|
+
# gone for good (a permanently-orphaned pending struct) — and resyncing it
|
|
59
|
+
# forever just amplifies the client's retransmit loop. A healable gap heals
|
|
60
|
+
# within a resync or two, well under this. Override with `gap_strike_limit`;
|
|
61
|
+
# set to nil to disable (always resync). See `sync_gap_strike`.
|
|
62
|
+
DEFAULT_GAP_STRIKE_LIMIT = 3
|
|
63
|
+
|
|
64
|
+
# Cap on distinct gappy updates tracked per connection, so a client can't
|
|
65
|
+
# grow the strike table without bound by sending endless distinct gaps.
|
|
66
|
+
GAP_STRIKE_MAX_KEYS = 64
|
|
67
|
+
|
|
54
68
|
def self.included(base)
|
|
55
69
|
base.extend(ClassMethods)
|
|
70
|
+
# Durable strike state under AnyCable. Each AnyCable RPC command gets a
|
|
71
|
+
# fresh channel instance, so a plain ivar resets every message and the
|
|
72
|
+
# unhealable-gap drop would never trip. anycable-rails' state_attr_accessor
|
|
73
|
+
# persists the value into the subscription's istate, which anycable-go
|
|
74
|
+
# round-trips on every command — so strikes accumulate there too. On plain
|
|
75
|
+
# ActionCable (anycable-rails loaded but not serving) the accessor behaves
|
|
76
|
+
# like attr_accessor; without anycable-rails we fall back to an ivar.
|
|
77
|
+
# NOTE: anycable-rails must be loaded before the channel class is defined
|
|
78
|
+
# for this declaration to take effect (standard Bundler.require order).
|
|
79
|
+
base.state_attr_accessor :yrby_gap_strikes if base.respond_to?(:state_attr_accessor)
|
|
56
80
|
end
|
|
57
81
|
|
|
58
82
|
module ClassMethods
|
|
@@ -91,6 +115,24 @@ module Y::ActionCable # rubocop:disable Style/ClassAndModuleChildren
|
|
|
91
115
|
|
|
92
116
|
superclass.respond_to?(:max_frame_bytes) ? superclass.max_frame_bytes : DEFAULT_MAX_FRAME_BYTES
|
|
93
117
|
end
|
|
118
|
+
|
|
119
|
+
# Number of times the same update may be rejected as a gap on one
|
|
120
|
+
# connection before it is settled + dropped instead of resynced again.
|
|
121
|
+
# Defaults to DEFAULT_GAP_STRIKE_LIMIT; set to nil to always resync.
|
|
122
|
+
# A limit below 2 is rejected: strike 1 must send a resync (the heal
|
|
123
|
+
# attempt) before any drop, or a first-sight healable gap would be
|
|
124
|
+
# settled with no recovery ever attempted.
|
|
125
|
+
def gap_strike_limit(limit = :__unset__)
|
|
126
|
+
# Combined reader/writer; the sentinel keeps nil a real value (disables the drop).
|
|
127
|
+
unless limit == :__unset__
|
|
128
|
+
raise ArgumentError, "gap_strike_limit must be nil or >= 2 (got #{limit.inspect})" if limit && limit < 2
|
|
129
|
+
|
|
130
|
+
@gap_strike_limit = limit
|
|
131
|
+
end
|
|
132
|
+
return @gap_strike_limit if defined?(@gap_strike_limit)
|
|
133
|
+
|
|
134
|
+
superclass.respond_to?(:gap_strike_limit) ? superclass.gap_strike_limit : DEFAULT_GAP_STRIKE_LIMIT
|
|
135
|
+
end
|
|
94
136
|
end
|
|
95
137
|
|
|
96
138
|
# Call from `subscribed`. Streams broadcasts for this document and
|
|
@@ -172,11 +214,87 @@ module Y::ActionCable # rubocop:disable Style/ClassAndModuleChildren
|
|
|
172
214
|
# is already present in the durable store.
|
|
173
215
|
def sync_send_ack(id, outcome)
|
|
174
216
|
return if id.nil?
|
|
175
|
-
|
|
217
|
+
# :dropped_unhealable settles a permanently-orphaned retry so the client
|
|
218
|
+
# stops retransmitting it (it was never going to integrate anywhere). The
|
|
219
|
+
# envelope carries "dropped" so the client can tell "durably recorded"
|
|
220
|
+
# from "abandoned" and surface it, instead of silently reporting synced
|
|
221
|
+
# over lost data. Clients that don't know the key ignore it.
|
|
222
|
+
return unless %i[recorded applied dropped_unhealable].include?(outcome)
|
|
176
223
|
|
|
177
224
|
# The braces are required: a bare hash would bind to transmit's `via:`
|
|
178
225
|
# keyword instead of its positional data argument.
|
|
179
|
-
|
|
226
|
+
if outcome == :dropped_unhealable
|
|
227
|
+
transmit({ "ack" => id, "dropped" => true })
|
|
228
|
+
else
|
|
229
|
+
transmit({ "ack" => id })
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Count consecutive gap rejections of `update` on this connection, returning
|
|
234
|
+
# the new count. Keyed by update content (SHA-256) so independent gaps track
|
|
235
|
+
# separately — a slow-to-heal legit gap must not push an unrelated one toward
|
|
236
|
+
# the drop.
|
|
237
|
+
#
|
|
238
|
+
# Where the state lives:
|
|
239
|
+
# - Plain ActionCable reuses the channel instance across a connection's
|
|
240
|
+
# messages, so an ivar accumulates (guarded by a mutex: ActionCable
|
|
241
|
+
# dispatches messages to a worker pool, so two receives on one instance
|
|
242
|
+
# can run concurrently).
|
|
243
|
+
# - Under AnyCable each RPC command gets a fresh instance, so the table is
|
|
244
|
+
# persisted via anycable-rails' `state_attr_accessor` (istate), which
|
|
245
|
+
# anycable-go round-trips on every command. See `self.included`.
|
|
246
|
+
def sync_gap_strike(update)
|
|
247
|
+
key = Digest::SHA256.hexdigest(update)
|
|
248
|
+
sync_gap_strike_mutex.synchronize do
|
|
249
|
+
strikes = sync_read_gap_strikes
|
|
250
|
+
# Bound the table so endless *distinct* gaps can't grow it without
|
|
251
|
+
# limit. Evict a single lowest-count entry, and only when inserting a
|
|
252
|
+
# genuinely new key: clearing wholesale would let a client cycling 64
|
|
253
|
+
# distinct gaps reset every tracked strike (defense bypass) and would
|
|
254
|
+
# starve a legitimate hot key.
|
|
255
|
+
if !strikes.key?(key) && strikes.size >= GAP_STRIKE_MAX_KEYS
|
|
256
|
+
strikes.delete(strikes.min_by { |_, count| count }.first)
|
|
257
|
+
end
|
|
258
|
+
strikes[key] = strikes.fetch(key, 0) + 1
|
|
259
|
+
sync_write_gap_strikes(strikes)
|
|
260
|
+
strikes[key]
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# A gap that finally records has healed: free its strike slot so it can't
|
|
265
|
+
# bias a future eviction and the table reflects only live gaps.
|
|
266
|
+
def sync_clear_gap_strike(update)
|
|
267
|
+
sync_gap_strike_mutex.synchronize do
|
|
268
|
+
strikes = sync_read_gap_strikes
|
|
269
|
+
next_key = Digest::SHA256.hexdigest(update)
|
|
270
|
+
if strikes.key?(next_key)
|
|
271
|
+
strikes.delete(next_key)
|
|
272
|
+
sync_write_gap_strikes(strikes)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def sync_gap_strike_mutex
|
|
278
|
+
@sync_gap_strike_mutex ||= Mutex.new
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Strike-table storage: istate-backed under AnyCable (survives the
|
|
282
|
+
# per-command fresh instance), plain ivar otherwise. Values round-trip JSON
|
|
283
|
+
# under AnyCable, so the table is a plain Hash of hex-digest => Integer.
|
|
284
|
+
def sync_read_gap_strikes
|
|
285
|
+
if respond_to?(:yrby_gap_strikes)
|
|
286
|
+
(yrby_gap_strikes || {}).to_h { |k, v| [k.to_s, v.to_i] }
|
|
287
|
+
else
|
|
288
|
+
@gap_strikes || {}
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def sync_write_gap_strikes(strikes)
|
|
293
|
+
if respond_to?(:yrby_gap_strikes=)
|
|
294
|
+
self.yrby_gap_strikes = strikes
|
|
295
|
+
else
|
|
296
|
+
@gap_strikes = strikes
|
|
297
|
+
end
|
|
180
298
|
end
|
|
181
299
|
|
|
182
300
|
# Single broadcast point so relay semantics live in one place and tests can
|
|
@@ -240,6 +358,20 @@ module Y::ActionCable # rubocop:disable Style/ClassAndModuleChildren
|
|
|
240
358
|
"that never happened, and a cold load would lose the edit."
|
|
241
359
|
end
|
|
242
360
|
|
|
361
|
+
# Fail closed when no document key is set (typically: AnyCable rebuilt the
|
|
362
|
+
# channel instance and the app forgot to pass `key` to sync_receive).
|
|
363
|
+
# Proceeding would record under nil, broadcast to a stream nobody
|
|
364
|
+
# subscribes to, and still ack — the client believes the edit was
|
|
365
|
+
# delivered when it reached no one.
|
|
366
|
+
def sync_validate_key!
|
|
367
|
+
return unless @sync_key.nil? || @sync_key.empty?
|
|
368
|
+
|
|
369
|
+
raise Y::Error,
|
|
370
|
+
"Y::ActionCable::Sync has no document key. Call sync_subscribed(key) in " \
|
|
371
|
+
"subscribed, and pass the key to sync_receive(data, key) when the transport " \
|
|
372
|
+
"doesn't keep the channel instance alive across actions (e.g. AnyCable)."
|
|
373
|
+
end
|
|
374
|
+
|
|
243
375
|
# Stateless per message: any process can handle any document. A client's
|
|
244
376
|
# SyncStep1 is answered from the store, document changes are recorded durably
|
|
245
377
|
# before relay and then broadcast, and awareness is relayed best-effort.
|
|
@@ -247,9 +379,11 @@ module Y::ActionCable # rubocop:disable Style/ClassAndModuleChildren
|
|
|
247
379
|
#
|
|
248
380
|
# Returns an outcome symbol for the reliable-delivery ack: :recorded when a
|
|
249
381
|
# document update was durably recorded and relayed, :gap when it was
|
|
250
|
-
# rejected for a resync, :
|
|
382
|
+
# rejected for a resync, :dropped_unhealable when a repeatedly-gapped update
|
|
383
|
+
# is settled + dropped, :noop for everything else.
|
|
251
384
|
def sync_handle_frame(encoded, bytes)
|
|
252
385
|
sync_validate_required_hooks!
|
|
386
|
+
sync_validate_key!
|
|
253
387
|
|
|
254
388
|
case Y.message_kind(bytes)
|
|
255
389
|
when MSG_KIND_SYNC_STEP1
|
|
@@ -257,27 +391,7 @@ module Y::ActionCable # rubocop:disable Style/ClassAndModuleChildren
|
|
|
257
391
|
sync_transmit(result[2])
|
|
258
392
|
:noop
|
|
259
393
|
when MSG_KIND_UPDATE
|
|
260
|
-
|
|
261
|
-
return :noop unless update
|
|
262
|
-
|
|
263
|
-
# Rebuild from the store (O(history) per update; snapshot in on_load if
|
|
264
|
-
# that cost bites).
|
|
265
|
-
doc = sync_load_doc
|
|
266
|
-
|
|
267
|
-
# Don't record a causally-incomplete update; resync instead so the gap
|
|
268
|
-
# heals as one complete delta.
|
|
269
|
-
unless doc.update_ready?(update)
|
|
270
|
-
sync_request_resync(doc)
|
|
271
|
-
return :gap
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
# Skip a lost-ack retry the store already has. Best-effort, not
|
|
275
|
-
# cross-process exactly-once (see "Delivery guarantees" in the README).
|
|
276
|
-
return :applied unless doc.update_advances?(update)
|
|
277
|
-
|
|
278
|
-
sync_record_change(update) # record before relay
|
|
279
|
-
sync_distribute(encoded)
|
|
280
|
-
:recorded
|
|
394
|
+
sync_handle_update(encoded, bytes)
|
|
281
395
|
when MSG_KIND_AWARENESS
|
|
282
396
|
sync_distribute(encoded)
|
|
283
397
|
:noop
|
|
@@ -286,6 +400,49 @@ module Y::ActionCable # rubocop:disable Style/ClassAndModuleChildren
|
|
|
286
400
|
end
|
|
287
401
|
end
|
|
288
402
|
|
|
403
|
+
# The document-update arm of sync_handle_frame: gate on causal readiness
|
|
404
|
+
# (with the unhealable-gap strike defense), skip-but-rebroadcast retries,
|
|
405
|
+
# and record-before-distribute for genuinely new content.
|
|
406
|
+
def sync_handle_update(encoded, bytes)
|
|
407
|
+
update = Y.update_from_message(bytes)
|
|
408
|
+
return :noop unless update
|
|
409
|
+
|
|
410
|
+
# Rebuild from the store (O(history) per update; snapshot in on_load if
|
|
411
|
+
# that cost bites).
|
|
412
|
+
doc = sync_load_doc
|
|
413
|
+
|
|
414
|
+
# Don't record a causally-incomplete update; resync so the gap heals as
|
|
415
|
+
# one complete delta. But a gap that survives repeated resyncs is
|
|
416
|
+
# unhealable (its missing dependency is gone for good) — resyncing it
|
|
417
|
+
# forever just feeds the client's retransmit loop. After
|
|
418
|
+
# `gap_strike_limit` rejections of the same update, settle it (ack via
|
|
419
|
+
# the :dropped_unhealable outcome) and drop it instead.
|
|
420
|
+
unless doc.update_ready?(update)
|
|
421
|
+
limit = self.class.gap_strike_limit
|
|
422
|
+
if limit && sync_gap_strike(update) >= limit
|
|
423
|
+
sync_log_drop(:info, "dropping unhealable gappy update after #{limit} strikes")
|
|
424
|
+
return :dropped_unhealable
|
|
425
|
+
end
|
|
426
|
+
sync_request_resync(doc)
|
|
427
|
+
return :gap
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# A lost-ack retry: already recorded, so skip on_change — but DO
|
|
431
|
+
# re-broadcast. If the first attempt died between record and broadcast,
|
|
432
|
+
# this retry is the only path left to the live subscribers. Duplicate
|
|
433
|
+
# broadcasts are free (CRDT apply is idempotent).
|
|
434
|
+
unless doc.update_advances?(update)
|
|
435
|
+
sync_clear_gap_strike(update) # a formerly-gappy update that healed
|
|
436
|
+
sync_distribute(encoded)
|
|
437
|
+
return :applied
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
sync_record_change(update) # record before relay
|
|
441
|
+
sync_clear_gap_strike(update) # a gap that finally recorded has healed
|
|
442
|
+
sync_distribute(encoded)
|
|
443
|
+
:recorded
|
|
444
|
+
end
|
|
445
|
+
|
|
289
446
|
# Build a fresh document from the durable store (on_load). Callers validate
|
|
290
447
|
# the hooks first, so on_load is present; a nil state means a fresh document.
|
|
291
448
|
def sync_load_doc
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yrby-actioncable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- JP Camara
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.3.
|
|
32
|
+
version: 0.3.1
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.3.
|
|
39
|
+
version: 0.3.1
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: actioncable
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|