yrby-rails 0.7.0 → 0.8.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-rails.md +41 -0
- data/README.md +31 -9
- data/app/channels/y/document_channel.rb +1 -1
- data/lib/y/action_cable/sync.rb +12 -2
- data/lib/y/action_cable.rb +11 -0
- data/lib/y/collaborative/attribute.rb +36 -0
- data/lib/y/collaborative/helper.rb +13 -10
- data/lib/y/collaborative.rb +14 -11
- data/lib/yrby/engine.rb +4 -0
- data/lib/yrby/rails/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: 83a5440fa6532dbedcf992660959297611b9b7cf958f7c7c6c1522dd0ac162b0
|
|
4
|
+
data.tar.gz: 23509b37d1bb0ce706c648a779993236addb244cb603031415de4084a58e4636
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c51b2e0b9acefc89d6c69e3b935206d195ccaba7b302ec13316c39f7fab0115d09fd7a5d108f537294b8c069a54ef2e1a53fc19b39ec21873e7b61be866e7a5d
|
|
7
|
+
data.tar.gz: 5ffd94a472cd16ada77f62e57c7bd571f7df461a9552756212ef6fd12c9c1a2cb92bdb3ac72fe0320a1992d27f6b22ce8964240db81057ffaaa4cc2be1872124
|
data/CHANGELOG-rails.md
CHANGED
|
@@ -7,6 +7,47 @@ this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-11-03
|
|
11
|
+
|
|
12
|
+
Requires yrby 0.9.0 or later.
|
|
13
|
+
|
|
14
|
+
### Experimental
|
|
15
|
+
|
|
16
|
+
These are new, and they may change in a minor release. See
|
|
17
|
+
[docs/experimental.md](docs/experimental.md).
|
|
18
|
+
|
|
19
|
+
- `collaborative_document(:body).edit { |doc| ... }` edits a record's
|
|
20
|
+
document from Ruby. It records the change in the document's rows, encrypted
|
|
21
|
+
ones included, and broadcasts it to open editors.
|
|
22
|
+
`Y::ActionCable.broadcast(key, update)` sends an already recorded update.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `collaborative_document(name)` answers `grant(expires_in:)` and `channel`:
|
|
27
|
+
the grant a page or client subscribes with, and the channel that accepts
|
|
28
|
+
it. A gem with its own grants and channel, such as lexxy-realtime, returns
|
|
29
|
+
its own document with both.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `collaborative_document_tag` renders the document's grant and channel, so
|
|
34
|
+
it works for a gem that returns its own document. `channel:` still
|
|
35
|
+
overrides the channel.
|
|
36
|
+
|
|
37
|
+
### Deprecated
|
|
38
|
+
|
|
39
|
+
- `collaborative_sgid(name, expires_in:)`. Use
|
|
40
|
+
`collaborative_document(name).grant(expires_in:)`. It logs through
|
|
41
|
+
`Y.deprecator`, which the engine adds to `Rails.application.deprecators`,
|
|
42
|
+
and goes away in yrby-rails 0.9.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- The channel relays a client's SyncStep2 to peers as a plain update.
|
|
47
|
+
Relayed as is, a peer that was still joining could take it for the answer
|
|
48
|
+
to its own handshake and report itself loaded before it had the server's
|
|
49
|
+
state.
|
|
50
|
+
|
|
10
51
|
## [0.7.0] - 2026-10-01
|
|
11
52
|
|
|
12
53
|
### Added
|
data/README.md
CHANGED
|
@@ -101,6 +101,7 @@ bin/rails generate yrby:install && bin/rails db:migrate
|
|
|
101
101
|
- [Rendering to HTML](#rendering-to-html)
|
|
102
102
|
- [Protocol codec (module functions)](#protocol-codec-module-functions)
|
|
103
103
|
- [ActionCable Integration](#actioncable-integration)
|
|
104
|
+
- [Experimental: writing from Ruby](#experimental-writing-from-ruby)
|
|
104
105
|
- [Thread Safety](#thread-safety)
|
|
105
106
|
- [Parallelism (GVL release)](#parallelism-gvl-release)
|
|
106
107
|
- [Message Type Constants](#message-type-constants)
|
|
@@ -117,7 +118,9 @@ primitives. By default the Ruby side treats a document as opaque CRDT state. It
|
|
|
117
118
|
applies updates, answers sync handshakes, and records deltas without reading
|
|
118
119
|
the contents, and the browser editor decides what shape the document has. When
|
|
119
120
|
you do need to look inside, `Doc#read_text` and `Doc#read_map` rebuild it in
|
|
120
|
-
Ruby
|
|
121
|
+
Ruby, and live handles (`Doc#get_map`, `get_array`, `get_text`,
|
|
122
|
+
`get_xml_text`) write to it. The handles are
|
|
123
|
+
[experimental](docs/experimental.md).
|
|
121
124
|
|
|
122
125
|
## Durability and delivery
|
|
123
126
|
|
|
@@ -338,12 +341,12 @@ every editor configures its own. `Y::Lexical` is the core Lexical base
|
|
|
338
341
|
(paragraphs, headings, quotes, code, lists, tables, links, and the full
|
|
339
342
|
text-format model), and other Lexical editors extend it with rules.
|
|
340
343
|
|
|
341
|
-
It handles every node in the
|
|
342
|
-
format and their combinations, links, the
|
|
343
|
-
blockquotes, code blocks, tabs and soft breaks,
|
|
344
|
-
header cells, image galleries, and ActionText
|
|
345
|
-
both render as `<action-text-attachment>`
|
|
346
|
-
re-render.
|
|
344
|
+
It handles every node in Lexxy 1.0, which has the same set as 0.9.x:
|
|
345
|
+
paragraphs, headings, every text format and their combinations, links, the
|
|
346
|
+
four list types with nesting, blockquotes, code blocks, tabs and soft breaks,
|
|
347
|
+
horizontal rules, tables with header cells, image galleries, and ActionText
|
|
348
|
+
attachments. Uploads and mentions both render as `<action-text-attachment>`
|
|
349
|
+
elements, which ActionText can re-render.
|
|
347
350
|
|
|
348
351
|
The Lexical support has the same two layers. `Y::Lexical` handles core Lexical
|
|
349
352
|
structure natively, and everything Lexxy adds is in `Y::Lexxy`'s rule set,
|
|
@@ -401,6 +404,13 @@ attribute names your templates and blocks will read, and `children` and `text`
|
|
|
401
404
|
tell you which `contains:` to pick. Child block types mean `:blocks`, and text
|
|
402
405
|
means `:inline`.
|
|
403
406
|
|
|
407
|
+
`unknown_types` lists the types whose `handled` is nil. `to_html` still
|
|
408
|
+
renders them as well as it can. An unknown container or inline wrapper renders
|
|
409
|
+
its children without its own markup, so its text stays. A node that keeps its
|
|
410
|
+
content only in its attributes renders nothing. Either way, the node's markup is
|
|
411
|
+
missing from the HTML, and nothing raises. When you add editor nodes, add a test
|
|
412
|
+
that renders a real document and asserts `unknown_types` is empty.
|
|
413
|
+
|
|
404
414
|
When a declarative rule can't express the markup, give the node a block:
|
|
405
415
|
|
|
406
416
|
```ruby
|
|
@@ -673,7 +683,7 @@ gets blocked at the next reconnect. To avoid that, pair `expires_in:` with
|
|
|
673
683
|
# app/controllers/posts_controller.rb
|
|
674
684
|
def grant
|
|
675
685
|
@post = current_user.posts.find(params[:id]) # your own authorization, again
|
|
676
|
-
render json: { grant: @post.
|
|
686
|
+
render json: { grant: @post.collaborative_document(:body).grant(expires_in: 10.minutes) }
|
|
677
687
|
end
|
|
678
688
|
```
|
|
679
689
|
|
|
@@ -737,7 +747,7 @@ up the record from it, so the page decides which document the client gets.
|
|
|
737
747
|
|
|
738
748
|
```erb
|
|
739
749
|
<%# the view picks the document and signs it %>
|
|
740
|
-
<%= tag.div data: { grant: post.
|
|
750
|
+
<%= tag.div data: { grant: post.collaborative_document(:body).grant } %>
|
|
741
751
|
```
|
|
742
752
|
|
|
743
753
|
```ruby
|
|
@@ -1067,6 +1077,18 @@ The browser clients manage presence (cursors, selections). The server doesn't
|
|
|
1067
1077
|
set or hold presence state, and it relays awareness frames without reading
|
|
1068
1078
|
them. See `yrby-client` for the client-side awareness API.
|
|
1069
1079
|
|
|
1080
|
+
## Experimental: writing from Ruby
|
|
1081
|
+
|
|
1082
|
+
These APIs are new. They may change in any minor release, and they aren't
|
|
1083
|
+
covered by yrby's usual compatibility promise.
|
|
1084
|
+
[docs/experimental.md](docs/experimental.md) documents them:
|
|
1085
|
+
|
|
1086
|
+
- Live `Y::Map`, `Y::Array`, and `Y::Text` handles that write to a document
|
|
1087
|
+
- Writing Lexical rich text from Ruby, with `Y::Lexical` and `Y::Lexxy`
|
|
1088
|
+
- Anchors: a block's identity as a value
|
|
1089
|
+
- Streaming into a document with `Doc#edit`
|
|
1090
|
+
- Editing a record's document from Ruby
|
|
1091
|
+
|
|
1070
1092
|
## Thread Safety
|
|
1071
1093
|
|
|
1072
1094
|
You can share a `Doc` across Ruby threads. Puma workers, ActionCable connection
|
|
@@ -6,7 +6,7 @@ module Y
|
|
|
6
6
|
# doesn't write a channel of its own.
|
|
7
7
|
#
|
|
8
8
|
# The client never picks its own document. It sends the signed grant the
|
|
9
|
-
# page rendered (record.
|
|
9
|
+
# page rendered (record.collaborative_document(name).grant), and the channel opens
|
|
10
10
|
# whatever document that grant verifies to. Your controller authorized the
|
|
11
11
|
# request when it rendered the tag, and the grant is how that decision gets
|
|
12
12
|
# to the socket. You can also register an authorize_document block to check
|
data/lib/y/action_cable/sync.rb
CHANGED
|
@@ -398,7 +398,7 @@ module Y::ActionCable # rubocop:disable Style/ClassAndModuleChildren
|
|
|
398
398
|
update = Y.update_from_message(bytes)
|
|
399
399
|
return :noop unless update
|
|
400
400
|
|
|
401
|
-
sync_handle_document_update(update, encoded)
|
|
401
|
+
sync_handle_document_update(update, sync_relay_frame(update, encoded, bytes))
|
|
402
402
|
when MSG_KIND_AWARENESS
|
|
403
403
|
sync_distribute(encoded)
|
|
404
404
|
:noop
|
|
@@ -422,6 +422,16 @@ module Y::ActionCable # rubocop:disable Style/ClassAndModuleChildren
|
|
|
422
422
|
:recorded
|
|
423
423
|
end
|
|
424
424
|
|
|
425
|
+
# A client's SyncStep2 answers the server's Step1 and is meant for the
|
|
426
|
+
# server. Peers get it as a plain update, so the only Step2 a client ever
|
|
427
|
+
# receives is the server's answer to its own handshake, which is how a
|
|
428
|
+
# client knows it has loaded.
|
|
429
|
+
def sync_relay_frame(update, encoded, bytes)
|
|
430
|
+
return encoded unless bytes.getbyte(1) == Y::MSG_SYNC_STEP2
|
|
431
|
+
|
|
432
|
+
Base64.strict_encode64(Y.wrap_update(update))
|
|
433
|
+
end
|
|
434
|
+
|
|
425
435
|
# Build a fresh document from the durable store (on_load). Callers validate
|
|
426
436
|
# the hooks first, so on_load is present; a nil state means a fresh document.
|
|
427
437
|
def sync_load_doc
|
|
@@ -432,7 +442,7 @@ module Y::ActionCable # rubocop:disable Style/ClassAndModuleChildren
|
|
|
432
442
|
end
|
|
433
443
|
|
|
434
444
|
def sync_stream_name
|
|
435
|
-
|
|
445
|
+
Y::ActionCable.stream_name(@sync_key)
|
|
436
446
|
end
|
|
437
447
|
|
|
438
448
|
def sync_awareness_stream_name
|
data/lib/y/action_cable.rb
CHANGED
|
@@ -17,6 +17,17 @@ module Y
|
|
|
17
17
|
def self.included(base)
|
|
18
18
|
base.include(Sync)
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
# The stream a document's subscribers listen on.
|
|
22
|
+
def self.stream_name(key) = "yrby:#{key}"
|
|
23
|
+
|
|
24
|
+
# Sends an update to everyone subscribed to a document, from outside a
|
|
25
|
+
# channel: a job, a console, or Y::Collaborative::Attribute#edit. Record
|
|
26
|
+
# the update first. This only distributes it.
|
|
27
|
+
def self.broadcast(key, update)
|
|
28
|
+
encoded = Base64.strict_encode64(Y.wrap_update(update))
|
|
29
|
+
::ActionCable.server.broadcast(stream_name(key), { "update" => encoded })
|
|
30
|
+
end
|
|
20
31
|
end
|
|
21
32
|
end
|
|
22
33
|
|
|
@@ -22,6 +22,42 @@ module Y
|
|
|
22
22
|
# this to return before it acknowledges or broadcasts the change.
|
|
23
23
|
def append(update) = document_row.append(update)
|
|
24
24
|
|
|
25
|
+
# A signed grant for this document, what a page or a Ruby client
|
|
26
|
+
# subscribes with. It's a signed GlobalID scoped to this attribute, and
|
|
27
|
+
# Y::Collaborative.locate turns it back into the record. Pass
|
|
28
|
+
# expires_in: to limit how long it lasts. Without it GlobalID's own
|
|
29
|
+
# default applies, which is one month under Rails. The option goes
|
|
30
|
+
# through only when it's given, because an explicit nil would mean
|
|
31
|
+
# "never expire".
|
|
32
|
+
def grant(expires_in: nil)
|
|
33
|
+
options = { for: Y::Collaborative.sgid_purpose(name) }
|
|
34
|
+
options[:expires_in] = expires_in if expires_in
|
|
35
|
+
record.to_sgid(**options).to_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The channel that accepts this document's grant. A gem with its own
|
|
39
|
+
# grants and channel, such as lexxy-realtime, returns its own document
|
|
40
|
+
# with both.
|
|
41
|
+
def channel = "Y::DocumentChannel"
|
|
42
|
+
|
|
43
|
+
# Edits the document from Ruby the way a browser does: yields the
|
|
44
|
+
# current document, records what the block changed, and broadcasts it so
|
|
45
|
+
# open editors apply it. Returns the update, or nil when the block
|
|
46
|
+
# changed nothing.
|
|
47
|
+
#
|
|
48
|
+
# post.collaborative_document(:body).edit do |doc|
|
|
49
|
+
# Y::Lexxy.append_paragraph(doc, "Reviewed by ops.")
|
|
50
|
+
# end
|
|
51
|
+
def edit
|
|
52
|
+
doc = y_doc
|
|
53
|
+
update = doc.edit { yield doc }
|
|
54
|
+
return unless update
|
|
55
|
+
|
|
56
|
+
append(update)
|
|
57
|
+
Y::ActionCable.broadcast(key, update)
|
|
58
|
+
update
|
|
59
|
+
end
|
|
60
|
+
|
|
25
61
|
# Builds a new Y::Doc from storage on every call.
|
|
26
62
|
def y_doc
|
|
27
63
|
Y::Doc.new.tap do |doc|
|
|
@@ -9,15 +9,18 @@ module Y
|
|
|
9
9
|
#
|
|
10
10
|
# renders the auto-connecting element:
|
|
11
11
|
#
|
|
12
|
-
# <yrby-document grant="<signed
|
|
12
|
+
# <yrby-document channel="Y::DocumentChannel" grant="<signed grant>" name="body"></yrby-document>
|
|
13
13
|
#
|
|
14
14
|
# Importing "yrby-client/element" registers the element. It subscribes to
|
|
15
|
-
#
|
|
15
|
+
# the document's channel by itself and exposes the synced Y.Doc through its
|
|
16
16
|
# `doc` property and `yrby:synced` event, much like the element behind
|
|
17
17
|
# turbo_stream_from.
|
|
18
18
|
#
|
|
19
|
-
# The grant
|
|
20
|
-
#
|
|
19
|
+
# The grant and channel come from record.collaborative_document(name), so
|
|
20
|
+
# the tag works for any gem that returns its own document, such as
|
|
21
|
+
# lexxy-realtime. Pass channel: for an app's own Y::DocumentChannel
|
|
22
|
+
# subclass. The grant is scoped to this record and attribute. Only render
|
|
23
|
+
# the tag on pages where
|
|
21
24
|
# the user may edit the record, because by default anyone holding the
|
|
22
25
|
# grant can open the document. To also check the user's current
|
|
23
26
|
# permissions when they subscribe, use Y::DocumentChannel.authorize_document.
|
|
@@ -30,18 +33,18 @@ module Y
|
|
|
30
33
|
# expires_in: limits how long the grant lasts. refresh: is a URL the
|
|
31
34
|
# element fetches a new grant from when a subscription is rejected, so it
|
|
32
35
|
# can resubscribe without a page load. The app's action there re-runs its
|
|
33
|
-
# own authorization and renders { grant: record.
|
|
36
|
+
# own authorization and renders { grant: record.collaborative_document(name).grant }:
|
|
34
37
|
#
|
|
35
38
|
# <%= collaborative_document_tag @post, :body, expires_in: 10.minutes,
|
|
36
39
|
# refresh: grant_post_path(@post) %>
|
|
37
40
|
module Helper
|
|
38
41
|
def collaborative_document_tag(record, name, expires_in: nil, refresh: nil, **, &)
|
|
39
|
-
|
|
40
|
-
attributes = { grant: grant, name: name }
|
|
42
|
+
document = record.collaborative_document(name)
|
|
43
|
+
attributes = { grant: document.grant(expires_in:), name: name }
|
|
41
44
|
attributes[:refresh] = refresh if refresh
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
tag.yrby_document(**, **attributes, &)
|
|
45
|
+
# A later key wins in a keyword splat. A template can pass its own
|
|
46
|
+
# channel, but it can't override the grant or the name.
|
|
47
|
+
tag.yrby_document(channel: document.channel, **, **attributes, &)
|
|
45
48
|
end
|
|
46
49
|
end
|
|
47
50
|
end
|
data/lib/y/collaborative.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "active_support/concern"
|
|
4
|
+
require "active_support/deprecation"
|
|
4
5
|
require "global_id"
|
|
6
|
+
require "y/action_cable"
|
|
5
7
|
|
|
6
8
|
module Y
|
|
7
9
|
# The signed token that connects a page to a channel for record-backed
|
|
@@ -12,7 +14,7 @@ module Y
|
|
|
12
14
|
# record up from it. The token is a signed GlobalID scoped to one attribute.
|
|
13
15
|
#
|
|
14
16
|
# # the view
|
|
15
|
-
# tag.div data: { grant: post.
|
|
17
|
+
# tag.div data: { grant: post.collaborative_document(:body).grant }
|
|
16
18
|
#
|
|
17
19
|
# # the channel
|
|
18
20
|
# def authorized?(_key)
|
|
@@ -28,6 +30,10 @@ module Y
|
|
|
28
30
|
#
|
|
29
31
|
# The engine includes this into ActiveRecord::Base so any channel's
|
|
30
32
|
# authorized? can use it. lexxy-realtime uses the same token flow.
|
|
33
|
+
# Warns about yrby-rails methods that are going away. The engine adds it to
|
|
34
|
+
# Rails.application.deprecators, so the app's deprecation settings apply.
|
|
35
|
+
def self.deprecator = @deprecator ||= ActiveSupport::Deprecation.new("0.9", "yrby-rails")
|
|
36
|
+
|
|
31
37
|
module Collaborative
|
|
32
38
|
extend ActiveSupport::Concern
|
|
33
39
|
|
|
@@ -39,7 +45,7 @@ module Y
|
|
|
39
45
|
# shipped one can both resolve the same tokens for an attribute.
|
|
40
46
|
def sgid_purpose(name) = "yrby/#{name}"
|
|
41
47
|
|
|
42
|
-
# Looks up the record for a
|
|
48
|
+
# Looks up the record for a grant from `collaborative_document(name).grant`.
|
|
43
49
|
# Returns nil for an invalid, tampered, expired, or wrong-attribute
|
|
44
50
|
# token, and for a record that no longer exists.
|
|
45
51
|
def locate(sgid, name)
|
|
@@ -76,20 +82,17 @@ module Y
|
|
|
76
82
|
end
|
|
77
83
|
|
|
78
84
|
# Returns the document for one attribute, stored the way the model
|
|
79
|
-
# declared. It responds to load_state, append, y_doc,
|
|
85
|
+
# declared. It responds to load_state, append, edit, y_doc, key, grant,
|
|
86
|
+
# and channel.
|
|
80
87
|
def collaborative_document(name)
|
|
81
88
|
Attribute.new(self, name)
|
|
82
89
|
end
|
|
83
90
|
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
# long the grant lasts. Without it GlobalID's own default applies, which is
|
|
87
|
-
# one month under Rails. We pass the option through only when it's given,
|
|
88
|
-
# because an explicit nil would mean "never expire".
|
|
91
|
+
# Deprecated. Use collaborative_document(name).grant. This one always
|
|
92
|
+
# signs yrby's own grant, as it did before.
|
|
89
93
|
def collaborative_sgid(name, expires_in: nil)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
to_sgid(**options).to_s
|
|
94
|
+
Y.deprecator.warn("collaborative_sgid(name) is deprecated. Use collaborative_document(name).grant instead.")
|
|
95
|
+
Attribute.new(self, name).grant(expires_in:)
|
|
93
96
|
end
|
|
94
97
|
end
|
|
95
98
|
end
|
data/lib/yrby/engine.rb
CHANGED
|
@@ -12,5 +12,9 @@ module Yrby
|
|
|
12
12
|
ActiveSupport.on_load(:active_record) { include Y::Collaborative }
|
|
13
13
|
ActiveSupport.on_load(:action_view) { include Y::Collaborative::Helper }
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
initializer "yrby.deprecator" do |app|
|
|
17
|
+
app.deprecators[:yrby] = Y.deprecator
|
|
18
|
+
end
|
|
15
19
|
end
|
|
16
20
|
end
|
data/lib/yrby/rails/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yrby-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- JP Camara
|
|
@@ -57,14 +57,14 @@ dependencies:
|
|
|
57
57
|
requirements:
|
|
58
58
|
- - ">="
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: 0.
|
|
60
|
+
version: 0.9.0
|
|
61
61
|
type: :runtime
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
65
|
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: 0.
|
|
67
|
+
version: 0.9.0
|
|
68
68
|
- !ruby/object:Gem::Dependency
|
|
69
69
|
name: actioncable
|
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|