dexiecable 2.0.0.alpha3 → 2.0.0.alpha5

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: 2c2de6d52cf60491a659aebc8452a88ca8b8878142d08e842fadc8fb4bb1050b
4
- data.tar.gz: 6be72c303e815a4809aff6b88a718b3e9ec1df8d5ebd341af03768225ee86bc3
3
+ metadata.gz: 16d440593b555940ff8039e04091edf54026d8678183acfb5d56b6189b08560c
4
+ data.tar.gz: 78c9c8e4b7e8e9cd3ae3d8cb7367b040962ef7ea0149d167a20396c58093a938
5
5
  SHA512:
6
- metadata.gz: ac607ef7627a7a0562459042bcc8b23244362553624cb2f7543a7997430842c5185d5c889b8557c3ad28f10265d286f7a557a1ce76c397473adc97ead82b13d5
7
- data.tar.gz: 1386543cc03df9409d17f184a8e1b4b80e85f993663009a32324da9b8e79561000c907c42e25c84c683bef4d4f11e525a5c1cd6dda58a0817595d53417880786
6
+ metadata.gz: d1d15766bb5dde84e8aaac57998da45e3dc88665740c6beb333d12dafddb3115b148b4e3b49ec8badf706d898eaba1c6fd7c8ad33e77b2aa60083b6b6e3b3a88
7
+ data.tar.gz: c716de1ee2d50979513c911b168879da8fc640d49f85ddd288ad70715197691c35ffb9020872a2250ae00a62fb35909214a501f91cdeb458323d31cb201bf511
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  >
6
6
  > Full synchronization utilizing event streams will arrive in DexieCable 3.0.
7
7
 
8
- DexieCable ships a single `DexieChannel` with a query DSL that mirrors the Dexie.js API, letting you push database mutations from the server to the client in real time. It also gives you a [`syncs_to_dexie`](#syncs_to_dexie--automatic-model-syncing) ActiveRecord macro for automatic change syncing.
8
+ DexieCable gives your ActionCable channel a query DSL that mirrors the Dexie.js API, letting you push database mutations from the server to the client in real time. It also gives you a [`syncs_to_dexie`](#syncs_to_dexie--automatic-model-syncing) ActiveRecord macro for automatic change syncing.
9
9
 
10
10
  Push Dexie table updates to a client from anywhere on the server:
11
11
 
@@ -73,7 +73,14 @@ setConsumer(createConsumer("wss://example.com/cable"));
73
73
 
74
74
  ### DexieChannel
75
75
 
76
- DexieCable ships one channel — `DexieChannel` so you don't need to define channels yourself. Every Dexie broadcast goes through it, and you can reopen it to add custom actions or push initial data.
76
+ Create a `DexieChannel` in your app and `include DexieCable` in it. Every Dexie broadcast goes through this channel:
77
+
78
+ ```ruby
79
+ # app/channels/dexie_channel.rb
80
+ class DexieChannel < ApplicationCable::Channel
81
+ include DexieCable
82
+ end
83
+ ```
77
84
 
78
85
  Stream tokens are signed with the application secret, so a client can only subscribe to streams the server has issued for it:
79
86
 
@@ -81,11 +88,17 @@ Stream tokens are signed with the application secret, so a client can only subsc
81
88
  DexieChannel.stream_token_for(target)
82
89
  ```
83
90
 
84
- For an ActiveRecord model it signs the record's GlobalID:
91
+ For an ActiveRecord model it returns a signed GlobalID (Rails' `signed_id`):
85
92
 
86
93
  ```ruby
87
94
  DexieChannel.stream_token_for(current_user)
88
- # => "signed token for gid://app/User/1"
95
+ # => "signed global id"
96
+ ```
97
+
98
+ Tokens never expire by default. Pass `expires_in:` or `expires_at:` to limit a token's lifetime:
99
+
100
+ ```ruby
101
+ DexieChannel.stream_token_for(current_user, expires_in: 1.day)
89
102
  ```
90
103
 
91
104
  Send that token to the client (render it in a view, return it from an endpoint, etc.) and add it to the subscription:
@@ -107,20 +120,25 @@ subscription.removeStream(userStream);
107
120
  subscription.removeAllStreams();
108
121
  ```
109
122
 
110
- #### Extending DexieChannel
123
+ #### Customizing DexieChannel
111
124
 
112
- To add custom actions or push initial data, reopen `DexieChannel` in your app:
125
+ Add custom actions or push initial data directly on your channel:
113
126
 
114
127
  ```ruby
115
- # config/initializers/dexiecable.rb
116
- class DexieCable::DexieChannel
117
- # Push a snapshot when a private stream is added.
128
+ # app/channels/dexie_channel.rb
129
+ class DexieChannel < ApplicationCable::Channel
130
+ include DexieCable
131
+
132
+ # Push a snapshot when a stream is added. `record` is a record for private
133
+ # streams, or the stream name (String) for public streams.
118
134
  def subscribed_to(record, params)
119
135
  case record
120
136
  when User
121
137
  table("notifications").bulkAdd(record.notifications.map(&:as_json_for_dexie))
122
138
  when Conversation
123
139
  table("messages").bulkAdd(record.messages.where("seq_id > ?", params[:last_seq_id]).map(&:as_json_for_dexie))
140
+ when String
141
+ table(record).bulkAdd(Announcement.for_stream(record).map(&:as_json_for_dexie))
124
142
  end
125
143
  end
126
144
 
@@ -131,7 +149,7 @@ class DexieCable::DexieChannel
131
149
  end
132
150
  ```
133
151
 
134
- The client still subscribes to `DexieCable::DexieChannel`, so no extra client config is needed:
152
+ The client subscribes to `DexieChannel` by default:
135
153
 
136
154
  ```js
137
155
  const subscription = subscribe(db);
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DexieCable
4
+ # Include this in an ActionCable channel to add Dexie broadcasting and
5
+ # streaming:
6
+ #
7
+ # class DexieChannel < ApplicationCable::Channel
8
+ # include DexieCable
9
+ #
10
+ # # Push initial data when a private stream is added.
11
+ # def subscribed_to(record, params)
12
+ # case record
13
+ # when User
14
+ # table("notifications").bulkAdd(record.notifications.map(&:as_json_for_dexie))
15
+ # end
16
+ # end
17
+ # end
18
+ #
19
+ # The client subscribes to that channel and adds streams dynamically:
20
+ #
21
+ # subscribe(db) # subscribes to "DexieChannel"
22
+ # subscription.addStream(DexieChannel.stream_token_for(current_user))
23
+ # subscription.addPublicStream("feed")
24
+ #
25
+ extend ActiveSupport::Concern
26
+
27
+ PUBLIC_STREAM_PREFIX = "public:"
28
+ STREAM_TOKEN_PURPOSE = "dexiecable:streams"
29
+
30
+ included do
31
+ public :transmit
32
+ end
33
+
34
+ class_methods do
35
+ # Open a scoped channel for broadcasting to a specific recipient.
36
+ #
37
+ # A String recipient is a public stream name (namespaced under
38
+ # +public:+); any other recipient targets that recipient's own stream.
39
+ def [](recipient)
40
+ target = recipient.is_a?(String) ? "#{PUBLIC_STREAM_PREFIX}#{recipient}" : recipient
41
+ ScopedChannel.new(self, target)
42
+ end
43
+
44
+ # Returns a signed token for +target+ (a record). Send it to the client,
45
+ # which passes it to +subscription.addStream+.
46
+ #
47
+ # Tokens never expire by default. Pass +expires_in:+ (a duration) or
48
+ # +expires_at:+ (a time) to limit a token's lifetime.
49
+ def stream_token_for(target, expires_at: nil, expires_in: nil)
50
+ options = { for: STREAM_TOKEN_PURPOSE }
51
+
52
+ if expires_at
53
+ options[:expires_at] = expires_at
54
+ elsif expires_in
55
+ options[:expires_in] = expires_in
56
+ else
57
+ options[:expires_at] = nil # never expire
58
+ end
59
+
60
+ target.to_sgid_param(**options)
61
+ end
62
+ end
63
+
64
+ # Build a query against a Dexie table, transmitted to all subscribers
65
+ # of this channel.
66
+ def table(name)
67
+ Query.new(self, name)
68
+ end
69
+
70
+ def subscribed
71
+ # Streams are added dynamically via +add_stream+.
72
+ end
73
+
74
+ def add_stream(data)
75
+ record = resolve_subscribe_target(data["stream"])
76
+ return unless record
77
+
78
+ stream_for record
79
+ params = data.except("action", "stream").with_indifferent_access
80
+ subscribed_to(record, params)
81
+ end
82
+
83
+ def remove_stream(data)
84
+ record = resolve_subscribe_target(data["stream"])
85
+ return unless record
86
+
87
+ stop_stream_from self.class.broadcasting_for(record)
88
+ end
89
+
90
+ def remove_all_streams(_data)
91
+ stop_all_streams
92
+ end
93
+
94
+ def add_public_stream(data)
95
+ name = data["stream"].to_s
96
+ return if name.blank?
97
+
98
+ stream_from public_stream_name(name)
99
+ params = data.except("action", "stream").with_indifferent_access
100
+ subscribed_to(name, params)
101
+ end
102
+
103
+ def remove_public_stream(data)
104
+ name = data["stream"].to_s
105
+ stop_stream_from public_stream_name(name) if name.present?
106
+ end
107
+
108
+ # Override to push initial data when a stream is added. +record+ is the
109
+ # resolved target — a record for private streams, or the stream name for
110
+ # public streams — and +params+ are any extra params sent from the client.
111
+ def subscribed_to(_record, _params)
112
+ end
113
+
114
+ private
115
+
116
+ def public_stream_name(name)
117
+ self.class.broadcasting_for("#{PUBLIC_STREAM_PREFIX}#{name}")
118
+ end
119
+
120
+ def resolve_subscribe_target(token)
121
+ GlobalID::Locator.locate_signed(token, for: STREAM_TOKEN_PURPOSE)
122
+ rescue ActiveRecord::RecordNotFound, NameError
123
+ nil
124
+ end
125
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DexieCable
4
- VERSION = "2.0.0.alpha3"
4
+ VERSION = "2.0.0.alpha5"
5
5
  end
data/lib/dexiecable.rb CHANGED
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "dexiecable/version"
4
+ require "global_id/railtie" if defined?(Rails::Railtie)
5
+ require_relative "dexiecable/concern"
4
6
  require_relative "dexiecable/scoped_channel"
5
7
  require_relative "dexiecable/query"
6
8
  require_relative "dexiecable/active_record_ext"
7
- require_relative "dexiecable/dexie_channel" if defined?(ActionCable::Channel::Base)
8
9
  require_relative "dexiecable/railtie" if defined?(Rails::Railtie)
9
10
 
10
11
  module DexieCable
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dexiecable
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.alpha3
4
+ version: 2.0.0.alpha5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Buhrmester
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '7.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: globalid
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0.6'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0.6'
40
54
  description: DexieCable augments ActionCable channels with a query DSL that mirrors
41
55
  the Dexie.js API, letting you push database mutations from the server to the client
42
56
  in real time. Includes an ActiveRecord macro (syncs_to_dexie) for automatic change
@@ -50,7 +64,7 @@ files:
50
64
  - README.md
51
65
  - lib/dexiecable.rb
52
66
  - lib/dexiecable/active_record_ext.rb
53
- - lib/dexiecable/dexie_channel.rb
67
+ - lib/dexiecable/concern.rb
54
68
  - lib/dexiecable/query.rb
55
69
  - lib/dexiecable/railtie.rb
56
70
  - lib/dexiecable/scoped_channel.rb
@@ -1,128 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DexieCable
4
- # The single channel through which all Dexie transfer happens.
5
- #
6
- # Broadcasts are scoped to a recipient with +DexieChannel[recipient]+ or
7
- # +DexieChannel.broadcast_to+. String recipients are public stream names;
8
- # any other recipient (e.g. a record) targets a private, signed stream.
9
- #
10
- # # private (signed)
11
- # subscription.addStream(DexieChannel.stream_token_for(current_user))
12
- #
13
- # # public (no signature, namespaced under "public:")
14
- # subscription.addPublicStream("feed")
15
- #
16
- # Reopen this channel in your app to push initial data or add custom
17
- # actions:
18
- #
19
- # class DexieCable::DexieChannel
20
- # def subscribed_to(record, params)
21
- # case record
22
- # when User
23
- # table("notifications").bulkAdd(record.notifications.map(&:as_json_for_dexie))
24
- # end
25
- # end
26
- #
27
- # def my_custom_action(data)
28
- # # ...
29
- # end
30
- # end
31
- #
32
- class DexieChannel < ActionCable::Channel::Base
33
- PUBLIC_STREAM_PREFIX = "public:"
34
-
35
- public :transmit
36
-
37
- # Open a scoped channel for broadcasting to a specific recipient.
38
- #
39
- # A String recipient is a public stream name (namespaced under
40
- # +public:+); any other recipient targets that recipient's own stream.
41
- #
42
- # DexieChannel[current_user].table("notifications").add(notification)
43
- # DexieChannel["feed"].table("announcements").add(announcement)
44
- #
45
- def self.[](recipient)
46
- target = recipient.is_a?(String) ? "#{PUBLIC_STREAM_PREFIX}#{recipient}" : recipient
47
- ScopedChannel.new(self, target)
48
- end
49
-
50
- # Build a query against a Dexie table, transmitted to all subscribers
51
- # of this channel.
52
- #
53
- # table("messages").where(:room_id).equals(room.id).add(message)
54
- #
55
- def table(name)
56
- Query.new(self, name)
57
- end
58
-
59
- # Returns a signed token for +target+ (a record or other private
60
- # target). Send it to the client, which passes it to
61
- # +subscription.addStream+.
62
- def self.stream_token_for(target)
63
- verifier.generate(target.respond_to?(:to_gid_param) ? target.to_gid_param : target.to_s)
64
- end
65
-
66
- def self.verifier
67
- @verifier ||= Rails.application.message_verifier("dexiecable:streams")
68
- end
69
-
70
- def subscribed
71
- # Streams are added dynamically via +add_stream+.
72
- end
73
-
74
- def add_stream(data)
75
- target = verified_target(data["stream"])
76
- return unless target
77
-
78
- stream_for target
79
- params = data.except("action", "stream").with_indifferent_access
80
- subscribed_to(resolve_subscribe_target(target), params)
81
- end
82
-
83
- def remove_stream(data)
84
- target = verified_target(data["stream"])
85
- return unless target
86
-
87
- stop_stream_from self.class.broadcasting_for(target)
88
- end
89
-
90
- def remove_all_streams(_data)
91
- stop_all_streams
92
- end
93
-
94
- def add_public_stream(data)
95
- name = data["stream"].to_s
96
- stream_from public_stream_name(name) if name.present?
97
- end
98
-
99
- def remove_public_stream(data)
100
- name = data["stream"].to_s
101
- stop_stream_from public_stream_name(name) if name.present?
102
- end
103
-
104
- # Override by reopening the channel to push initial data when a private
105
- # stream is added. +record+ is the resolved target and +params+ are any
106
- # extra params sent from the client.
107
- def subscribed_to(_record, _params)
108
- end
109
-
110
- private
111
-
112
- def public_stream_name(name)
113
- self.class.broadcasting_for("#{PUBLIC_STREAM_PREFIX}#{name}")
114
- end
115
-
116
- def verified_target(token)
117
- return unless token.present?
118
-
119
- self.class.verifier.verify(token)
120
- rescue ActiveSupport::MessageVerifier::InvalidSignature
121
- nil
122
- end
123
-
124
- def resolve_subscribe_target(target)
125
- target.start_with?("gid://") ? GlobalID::Locator.locate(target) : target
126
- end
127
- end
128
- end