yrb-lite-actioncable 0.1.0.beta3 → 0.1.0.beta5

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: 2a29ee03d2c3eb1dd68e3b6be4ffc8d57f7c244c6ace2b4edcc715467f525d60
4
- data.tar.gz: c80b92042a593af7bc9cf22233032af3ca783c507a7a2df2cce052501aadfcd2
3
+ metadata.gz: 8d48432476f4a644c91c193f72d6857aa5ce6bb0f2ded537b3277d31e8c49cfb
4
+ data.tar.gz: e28d77164e95bbba949133989f11555015160f39786a0c17bab9acacaaf05570
5
5
  SHA512:
6
- metadata.gz: 2687a09e9f83d54240f6b9fc6c40858a30864b8912e2dc8b146a70bc95f5069933efe3ed5643fada439ecdafeb0c392c8edfaff147b38ab67dd206378db9f373
7
- data.tar.gz: 39f410248b09f295d83a8621e7a7cdada6661fa9aa2b81fc967b75838a878a84583f1e3ec7e6dcf9f300ed840f32eca62c8b4d1cc13ab874efb2fc753cb47523
6
+ metadata.gz: d69b40149fccf8d84d8fc4cbfffe7f9ce4309a47e9971bbb0c65befc9bed6a5827d2251caa807da6da5b2b5b7f3ef13c4c8659d2856868848df13748deed77d2
7
+ data.tar.gz: a8babf32c1f72b70ff9f3e551c826b62306dd446ee86d44d09b71b5f5164b4c0817328a5ad14e16bcbc0134f1fc4a20624d59abc13a75766292d1a567933e432
@@ -5,50 +5,3 @@ format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
5
5
  this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
-
9
- ## [0.1.0.beta3] - 2026-06-22
10
-
11
- ### Changed
12
-
13
- - Document streams and awareness streams are separate under AnyCable. Document
14
- streams are normal server-relayed streams; awareness streams are subscribed
15
- with `whisper: true` and carry only ephemeral presence frames.
16
- - The channel accepts and emits the canonical document envelope,
17
- `{ "update" => "<base64 frame>" }`. Accepted document updates carrying an
18
- `"id"` are acknowledged with `{ "ack" => id }`.
19
- - Removed the backend switch. `YrbLite::ActionCable::Sync` is always
20
- store-backed and fails closed unless both `on_load` and `on_change` are
21
- configured, so acknowledgements always mean the update is durably recorded.
22
- - Requires `yrb-lite >= 0.1.0.beta6` (uses the new `update_advances?` and
23
- wire client-id frame validation).
24
-
25
- ## [0.1.0.beta2] - 2026-06-20
26
-
27
- ### Added
28
-
29
- - Automatic AnyCable whispering for awareness/presence. When running under
30
- AnyCable, the channel now enables client-to-client whispering on its stream
31
- (`stream_from key, whisper: true`), so a client that whispers awareness has its
32
- presence frames broadcast directly to other subscribers with no server
33
- round-trip. It's automatic -- no configuration -- and a no-op on plain
34
- ActionCable (no whisper support), where presence stays server-relayed. Document
35
- updates are never whispered.
36
-
37
- ## [0.1.0.beta1] - 2026-06-18
38
-
39
- ### Added
40
-
41
- - Initial release, extracted from `yrb-lite` (which shipped this as
42
- `YrbLite::Sync` through 0.1.0.beta4). Provides `YrbLite::ActionCable::Sync`, an
43
- ActionCable channel concern implementing the y-websocket sync protocol and
44
- awareness/presence over ActionCable and AnyCable: record-before-distribute
45
- auditing (`on_change`), persistence hooks, presence reaping, idle-document
46
- eviction, and multi-process replica sync. Depends on `yrb-lite`
47
- (>= 0.1.0.beta5) for the CRDT documents, awareness, and protocol primitives.
48
- - `on_change` recorders run in the channel instance's context (carried over from
49
- `yrb-lite` 0.1.0.beta4), so a recorder can call the channel's own methods
50
- directly.
51
-
52
- [Unreleased]: https://github.com/jpcamara/yrb-lite/commits/main
53
- [0.1.0.beta2]: https://github.com/jpcamara/yrb-lite/commits/main
54
- [0.1.0.beta1]: https://github.com/jpcamara/yrb-lite/releases/tag/v0.1.0.beta5
data/README.md CHANGED
@@ -19,12 +19,12 @@ end
19
19
 
20
20
  On the browser, use the `yrb-lite-client` `ActionCableProvider`. Tiptap,
21
21
  ProseMirror, and BlockNote all sync through the `Y.Doc` you pass in and the
22
- provider's Awareness instance, unless you supply your own.
22
+ provider's Awareness instance.
23
23
 
24
24
  ## What you get
25
25
 
26
- - Thread-safe Ruby wrappers for `Doc` and `Awareness`. You can share them
27
- across Puma threads; native CRDT work runs with the GVL released.
26
+ - A thread-safe Ruby `Doc` you can share across Puma threads; native CRDT work
27
+ runs with the GVL released.
28
28
  - The y-websocket protocol (document sync plus awareness/presence) as a
29
29
  one-include ActionCable concern.
30
30
  - Store-backed ActionCable/AnyCable delivery for multi-process deployments.
@@ -86,11 +86,7 @@ require "yrb_lite"
86
86
 
87
87
  # Create docs
88
88
  doc = YrbLite::Doc.new # random client ID
89
- doc = YrbLite::Doc.new(12345) # specific client ID
90
-
91
- # Get document info
92
- doc.client_id # => unique client identifier
93
- doc.guid # => document GUID
89
+ doc = YrbLite::Doc.new(12345) # specific client ID (used for CRDT identity)
94
90
 
95
91
  # Encoding
96
92
  doc.encode_state_vector # => current state vector
@@ -100,36 +96,23 @@ doc.encode_state_as_update(sv) # => update diff against state vector
100
96
  # Applying updates
101
97
  doc.apply_update(update_bytes) # apply raw V1 update
102
98
 
103
- # Sync protocol messages
104
- doc.sync_step1 # => SyncStep1 message (contains state vector)
105
- doc.sync_step2(state_vector) # => SyncStep2 message (contains update)
106
- doc.handle_sync_message(data) # => [msg_type, sync_type, response]
107
- doc.encode_update_message(update) # => wrap update as sync Update message
99
+ # Sync protocol
100
+ doc.sync_step1 # => SyncStep1 message (this doc's state vector)
101
+ doc.handle_sync_message(data) # => [msg_type, sync_type, response]; answers a
102
+ # peer's SyncStep1 with a SyncStep2
108
103
  ```
109
104
 
110
- ### Awareness (Document + Presence)
111
-
112
- ```ruby
113
- # Create awareness instances (each contains a Doc)
114
- awareness = YrbLite::Awareness.new # random client ID
115
- awareness = YrbLite::Awareness.new(12345) # specific client ID
116
-
117
- # Get document info
118
- awareness.client_id # => unique client identifier
119
- awareness.guid # => document GUID
120
- ```
105
+ ### Protocol codec (module functions)
121
106
 
122
- ### Handling Sync Messages
107
+ Classifying and unwrapping wire frames is stateless, so it's exposed as
108
+ `YrbLite` module functions rather than a class. The server never holds presence
109
+ or document state to route a frame — presence lives in the browser clients, and
110
+ the server only relays awareness frames opaquely.
123
111
 
124
112
  ```ruby
125
- # When connection opens, send initial sync messages
126
- initial_message = awareness.start
127
- # Send initial_message to peer via WebSocket
128
-
129
- # When receiving messages from peer
130
- response = awareness.handle(incoming_data)
131
- # Send response back to peer if not empty
132
- send_to_peer(response) unless response.empty?
113
+ YrbLite.message_kind(frame) # => 0 drop / 1 step1 / 2 update / 3 awareness / 4 query
114
+ YrbLite.update_from_message(frame) # => the document delta carried by a frame, or nil
115
+ YrbLite.wrap_update(update_bytes) # => wrap a raw doc update as a sync Update frame
133
116
  ```
134
117
 
135
118
  ### ActionCable Integration
@@ -178,6 +161,34 @@ oversized, or unknown frames are dropped. A bad frame can't crash the process: a
178
161
  Rust panic is caught at the FFI boundary and re-raised as a Ruby exception. And
179
162
  no single client can relay garbage that breaks the others in a room.
180
163
 
164
+ #### Delivery guarantees
165
+
166
+ The contract is the same at every scale — one process, or hundreds across many
167
+ servers:
168
+
169
+ - **The document always converges.** CRDT updates are commutative and
170
+ idempotent, so out-of-order, duplicate, or concurrent delivery all converge to
171
+ the same correct document. This needs no coordination and holds everywhere.
172
+ - **The durable log never goes gappy.** An update is recorded only once its
173
+ causal dependencies are already in the store (checked against `on_load`); a
174
+ causally-incomplete update triggers a resync instead, so the log always
175
+ rebuilds cleanly.
176
+ - **`on_change` is at-least-once, and the durable guarantee is that replaying the
177
+ log reconstructs the document.** Every change is recorded before it's acked or
178
+ broadcast (record-before-distribute). Entry count is not 1:1 with edits: a
179
+ best-effort check skips most lost-ack retries but isn't cross-process exact (a
180
+ retry on another process can record the same update twice), and a resync can
181
+ coalesce a client's un-acked tail into a single record. So **make `on_change`
182
+ idempotent** if duplicate side effects would matter (a webhook, a counter) — a
183
+ raw append-only delta log is naturally fine, since it replays to the same
184
+ document either way.
185
+
186
+ There is deliberately no in-gem cross-process lock. One that only spanned a
187
+ single process would give exactly-once at small scale and silently degrade as
188
+ you scale out, so the guarantee is uniform instead. If you need exactly-once
189
+ *side effects*, enforce it in your store (a unique key on the update) or with
190
+ your own distributed lock — the gem stays storage-agnostic and assumes neither.
191
+
181
192
  #### Multi-process deployments
182
193
 
183
194
  Most Rails apps run several processes (Puma workers, multiple dynos), and any of
@@ -295,43 +306,15 @@ one `{ ack: id }` cumulatively confirms everything up to it. Because CRDT apply
295
306
  is idempotent, a resend that already landed is a harmless no-op that just
296
307
  re-acks. Awareness stays ephemeral and is not acked.
297
308
 
298
- ### User Awareness/Presence
299
-
300
- ```ruby
301
- # Set local user state (cursor position, name, etc.)
302
- awareness.set_local_state('{"user": {"name": "Alice", "color": "#ff0000"}}')
303
-
304
- # Get local state
305
- awareness.local_state # => '{"user": {"name": "Alice", "color": "#ff0000"}}'
306
-
307
- # Clear local state (e.g., when disconnecting)
308
- awareness.clear_local_state
309
-
310
- # Encode awareness update for broadcasting
311
- update = awareness.encode_awareness_update
312
- ```
313
-
314
- ### Low-Level Access
315
-
316
- ```ruby
317
- # Get state vector for manual sync
318
- sv = awareness.encode_state_vector
319
-
320
- # Get update diffed against a state vector
321
- update = awareness.encode_state_as_update(remote_state_vector)
322
-
323
- # Apply raw update to the document
324
- awareness.apply_update(update_bytes)
325
-
326
- # Wrap raw update data in a sync message
327
- message = awareness.encode_update(update_bytes)
328
- ```
309
+ Presence (cursors, selections) is owned by the browser clients — the server
310
+ never sets or holds presence state, it only relays awareness frames opaquely.
311
+ See `yrb-lite-client` for the client-side awareness API.
329
312
 
330
313
  ## Thread Safety
331
314
 
332
- `Doc` and `Awareness` are safe to share across Ruby threads. A `Doc` or
333
- `Awareness` can be used concurrently from Puma workers, ActionCable connection
334
- threads, or background jobs without external locking.
315
+ A `Doc` is safe to share across Ruby threads used concurrently from Puma
316
+ workers, ActionCable connection threads, or background jobs without external
317
+ locking.
335
318
 
336
319
  That comes from how the underlying types work, not from locking on top:
337
320
 
@@ -41,15 +41,12 @@ module YrbLite::ActionCable # rubocop:disable Style/ClassAndModuleChildren
41
41
  # validated against `on_load`, recorded through `on_change`, then broadcast.
42
42
  # No authoritative document state is kept in ActionCable process memory.
43
43
  module Sync
44
- # Validated frame kinds from Awareness#message_kind. A frame only gets a
45
- # non-DROP kind if it is exactly one well-formed message; anything
46
- # malformed, truncated, multi-message, or unknown is dropped before it can
47
- # be processed or relayed.
48
- MSG_KIND_DROP = 0
44
+ # Frame kinds we act on, from Awareness#message_kind. The other codes it can
45
+ # return -- 0 (drop: malformed/truncated/multi-message/unknown) and 4
46
+ # (awareness query) -- fall through to a no-op in the dispatch below.
49
47
  MSG_KIND_SYNC_STEP1 = 1
50
48
  MSG_KIND_UPDATE = 2
51
49
  MSG_KIND_AWARENESS = 3
52
- MSG_KIND_AWARENESS_QUERY = 4
53
50
 
54
51
  # Default incoming-frame size cap (decoded bytes). Generous enough for a
55
52
  # large initial SyncStep2, small enough to bound a single message's
@@ -61,10 +58,11 @@ module YrbLite::ActionCable # rubocop:disable Style/ClassAndModuleChildren
61
58
  end
62
59
 
63
60
  module ClassMethods
64
- # Load persisted document state. Called once per key with (key);
65
- # return a binary Y.js update (or nil for a fresh document).
66
- def on_load(callable = nil, &block)
67
- @on_load = callable || block if callable || block
61
+ # Load persisted document state. Called once per key with (key); return a
62
+ # binary Y.js update (or nil for a fresh document). The block runs in the
63
+ # channel instance's context, the same as on_change (see below).
64
+ def on_load(&block)
65
+ @on_load = block if block
68
66
  return @on_load if defined?(@on_load) && @on_load
69
67
 
70
68
  superclass.respond_to?(:on_load) ? superclass.on_load : nil
@@ -75,13 +73,12 @@ module YrbLite::ActionCable # rubocop:disable Style/ClassAndModuleChildren
75
73
  # the exact CRDT delta. If the block raises, the change is rejected:
76
74
  # neither acknowledged nor broadcast to other subscribers.
77
75
  #
78
- # A block recorder runs in the *channel instance's* context, so it can
79
- # call the channel's own methods (current_user, params, a per-connection
80
- # Current.* accessor) directly, with no thread-local plumbing. (A non-Proc
81
- # callable is invoked with #call instead, since it carries its own
82
- # context.) on_change always fires from within sync_receive.
83
- def on_change(callable = nil, &block)
84
- @on_change = callable || block if callable || block
76
+ # The block runs in the *channel instance's* context (via instance_exec),
77
+ # so it can call the channel's own methods (current_user, params, a
78
+ # per-connection Current.* accessor) directly, with no thread-local
79
+ # plumbing. on_change always fires from within sync_receive.
80
+ def on_change(&block)
81
+ @on_change = block if block
85
82
  return @on_change if defined?(@on_change) && @on_change
86
83
 
87
84
  superclass.respond_to?(:on_change) ? superclass.on_change : nil
@@ -106,8 +103,11 @@ module YrbLite::ActionCable # rubocop:disable Style/ClassAndModuleChildren
106
103
  @sync_origin = SecureRandom.hex(8)
107
104
  sync_require_store_recorder!
108
105
 
109
- sync_stream sync_stream_name
110
- sync_stream sync_awareness_stream_name, whisper: true if respond_to?(:whispers_to)
106
+ # The document stream is never whisper-enabled; under AnyCable we also
107
+ # subscribe an awareness stream with `whisper: true`, scoping the client-to-
108
+ # client path to ephemeral presence rather than the durable document stream.
109
+ stream_from sync_stream_name
110
+ stream_from sync_awareness_stream_name, whisper: true if respond_to?(:whispers_to)
111
111
  sync_transmit(sync_load_doc.sync_step1)
112
112
  end
113
113
 
@@ -145,18 +145,11 @@ module YrbLite::ActionCable # rubocop:disable Style/ClassAndModuleChildren
145
145
 
146
146
  return if cap && bytes.bytesize > cap
147
147
 
148
- sync_send_ack(id, sync_dispatch(encoded, bytes))
148
+ sync_send_ack(id, sync_handle_frame(encoded, bytes))
149
149
  end
150
150
 
151
- # Route a decoded frame to the backend/path that handles it and return the
152
- # outcome symbol (:recorded/:applied/:gap/:noop) used by the reliable-
153
- # delivery ack. A dropped frame returns nil (never acked).
154
- def sync_dispatch(encoded, bytes)
155
- sync_receive_store_backed(encoded, bytes)
156
- end
157
-
158
- # Kept as the ActionCable lifecycle hook target. There is no cached document
159
- # or server-owned presence state to clean up in the store-backed design.
151
+ # The `unsubscribed` hook target. Nothing to clean up: the server keeps no
152
+ # per-connection document or presence state.
160
153
  def sync_unsubscribed(key = nil)
161
154
  @sync_key = key.to_s if key
162
155
  end
@@ -201,7 +194,6 @@ module YrbLite::ActionCable # rubocop:disable Style/ClassAndModuleChildren
201
194
  transmit(sync_envelope(Base64.strict_encode64(bytes)))
202
195
  end
203
196
 
204
- # Build an outgoing envelope.
205
197
  def sync_envelope(encoded, extra = {})
206
198
  { "update" => encoded }.merge(extra)
207
199
  end
@@ -223,53 +215,44 @@ module YrbLite::ActionCable # rubocop:disable Style/ClassAndModuleChildren
223
215
  "that never happened, and a cold load would lose the edit."
224
216
  end
225
217
 
226
- # Subscribe to a broadcast stream. The document stream is never whisper-
227
- # enabled; when AnyCable is present we separately subscribe an awareness
228
- # stream with `whisper: true`, so the client-to-client path is scoped to
229
- # ephemeral presence instead of the durable document stream.
230
- def sync_stream(name, **, &)
231
- stream_from(name, **, &)
232
- end
233
-
234
- # Stateless per message: no warm replica, no assumptions about which process
235
- # owns a document. A client's SyncStep1 is answered from the store, document
236
- # changes are recorded durably before relay and then broadcast, and
237
- # awareness is relayed best-effort. Echoing back to the sender is harmless,
238
- # since the CRDT apply is idempotent.
218
+ # Stateless per message: any process can handle any document. A client's
219
+ # SyncStep1 is answered from the store, document changes are recorded durably
220
+ # before relay and then broadcast, and awareness is relayed best-effort.
221
+ # Echoing back to the sender is harmless, since the CRDT apply is idempotent.
239
222
  #
240
223
  # Returns an outcome symbol for the reliable-delivery ack: :recorded when a
241
224
  # document update was durably recorded and relayed, :gap when it was
242
225
  # rejected for a resync, :noop for everything else.
243
- def sync_receive_store_backed(encoded, bytes)
226
+ def sync_handle_frame(encoded, bytes)
244
227
  sync_require_store_recorder!
245
228
 
246
- case Sync.codec.message_kind(bytes)
229
+ case YrbLite.message_kind(bytes)
247
230
  when MSG_KIND_SYNC_STEP1
248
231
  result = sync_load_doc.handle_sync_message(bytes)
249
232
  sync_transmit(result[2]) if result
250
233
  :noop
251
234
  when MSG_KIND_UPDATE
252
- update = Sync.codec.update_from_message(bytes)
235
+ update = YrbLite.update_from_message(bytes)
253
236
  return :noop unless update
254
237
 
255
- Sync.lock_for(@sync_key).synchronize do
256
- # To tell whether this update is causally ready we rebuild the doc
257
- # from the store and check against it. That's an O(history) load per
258
- # update, mitigated by snapshotting in the app's load path if needed.
259
- doc = sync_load_doc
260
- unless doc.update_ready?(update)
261
- sync_request_resync(doc)
262
- next :gap
263
- end
264
-
265
- # Lost-ack retry: the durable store already contains this update.
266
- # Ack it without recording or relaying again.
267
- next :applied unless doc.update_advances?(update)
268
-
269
- sync_record_change(self.class.on_change, update) # record before relay
270
- sync_distribute(encoded)
271
- :recorded
238
+ # Rebuild from the store (O(history) per update; snapshot in on_load if
239
+ # that cost bites).
240
+ doc = sync_load_doc
241
+
242
+ # Don't record a causally-incomplete update; resync instead so the gap
243
+ # heals as one complete delta.
244
+ unless doc.update_ready?(update)
245
+ sync_request_resync(doc)
246
+ return :gap
272
247
  end
248
+
249
+ # Skip a lost-ack retry the store already has. Best-effort, not
250
+ # cross-process exactly-once (see "Delivery guarantees" in the README).
251
+ return :applied unless doc.update_advances?(update)
252
+
253
+ sync_record_change(self.class.on_change, update) # record before relay
254
+ sync_distribute(encoded)
255
+ :recorded
273
256
  when MSG_KIND_AWARENESS
274
257
  sync_distribute(encoded)
275
258
  :noop
@@ -281,7 +264,8 @@ module YrbLite::ActionCable # rubocop:disable Style/ClassAndModuleChildren
281
264
  # Build a fresh document from the durable store (on_load).
282
265
  def sync_load_doc
283
266
  doc = YrbLite::Doc.new
284
- state = self.class.on_load&.call(@sync_key)
267
+ loader = self.class.on_load
268
+ state = instance_exec(@sync_key, &loader) if loader
285
269
  doc.apply_update(state) if state
286
270
  doc
287
271
  end
@@ -294,19 +278,14 @@ module YrbLite::ActionCable # rubocop:disable Style/ClassAndModuleChildren
294
278
  "#{sync_stream_name}:awareness"
295
279
  end
296
280
 
297
- # Invoke the on_change recorder. A block/proc runs in this channel instance's
298
- # context (instance_exec) so it can reach the channel's own methods; a
299
- # non-Proc callable is invoked with #call, since it carries its own context.
281
+ # Invoke the on_change recorder in this channel instance's context
282
+ # (instance_exec) so it can reach the channel's own methods.
300
283
  def sync_record_change(recorder, update)
301
- args = [@sync_key, update]
302
- recorder.is_a?(Proc) ? instance_exec(*args, &recorder) : recorder.call(*args)
284
+ instance_exec(@sync_key, update, &recorder)
303
285
  end
304
286
 
305
287
  # -- Shared process state ----------------------------------------------
306
288
 
307
- @locks = {}
308
- @registry_mutex = Mutex.new
309
-
310
289
  class << self
311
290
  # A stable id for this server process, stamped on every broadcast so
312
291
  # other processes know to apply it to their replica and this process
@@ -314,29 +293,6 @@ module YrbLite::ActionCable # rubocop:disable Style/ClassAndModuleChildren
314
293
  def process_id
315
294
  @process_id ||= SecureRandom.hex(8)
316
295
  end
317
-
318
- # A shared, stateless decoder for the store-backed path. message_kind and
319
- # update_from_message only read their argument (they don't touch the
320
- # instance's document), so one shared instance is safe across threads.
321
- def codec
322
- @codec ||= YrbLite::Awareness.new
323
- end
324
-
325
- # Per-document mutex serializing load -> record -> broadcast within this
326
- # process. The durable store remains the cross-process source of truth.
327
- # Only briefly holds the registry mutex to fetch/create the lock; the
328
- # durable write itself runs while holding only this per-key lock.
329
- def lock_for(key)
330
- @registry_mutex.synchronize { @locks[key] ||= Mutex.new }
331
- end
332
-
333
- # Clear process-local locks and codec (useful for testing).
334
- def reset!
335
- @registry_mutex.synchronize do
336
- @locks = {}
337
- @codec = nil
338
- end
339
- end
340
296
  end
341
297
  end
342
298
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module YrbLite
4
4
  module ActionCable
5
- VERSION = "0.1.0.beta3"
5
+ VERSION = "0.1.0.beta5"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yrb-lite-actioncable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.beta3
4
+ version: 0.1.0.beta5
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.1.0.beta6
32
+ version: 0.1.0.beta9
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.1.0.beta6
39
+ version: 0.1.0.beta9
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: actioncable
42
42
  requirement: !ruby/object:Gem::Requirement