dexiecable 2.0.0.alpha2 → 2.0.0.alpha4

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: 714562cd7c933eb48e81e9159acd326d77b6133a807cdd541000f350281c1c94
4
- data.tar.gz: d3bcacdc18d5f9c7a8be970f9ede859be19e282e8ca35aa4d27c81e581310eb3
3
+ metadata.gz: 502a99e32deef9551c55b8cb0bdae2e2d74af751e4f9889543f76ceea6d5a2ce
4
+ data.tar.gz: f42e9bca1c1d05ef287b3b72373e7c264af8ab78c350c092b8ccd9f887aa0270
5
5
  SHA512:
6
- metadata.gz: ed0dbe4fdfade70f3300d9c2a61882f6a00320c384fe74ad76b6ba11ef0733a26b86f6949606117b4847efdd5221709bf1a091088a510a0155d6ff9f48a87774
7
- data.tar.gz: d5cef46c7417c0e34ff4e08c08e0df846ff653f19ae0eeacbffe9de433a362c9ae9af60b32f669a6daa301b64a05d3844f227587cf20af4da23ecf149b39cfb9
6
+ metadata.gz: 37dc5d8afc9449875b3074c2684856ebb5d1f29e44a0c8d83558d061cf96baa270b8e5d1ad2a35a17bd01363b25956ec2946aead6929c3c465bee447a0c5d8d8
7
+ data.tar.gz: 7c370e6be94b70a65c63cf8bd47bb762b353df0ce0e964cd0f68b256703514c6a790124605a973008595358976319f9d6fdb3876fd5cfc97eeba65a8b5902642
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 never define your own channels or include anything. Every Dexie broadcast goes through it.
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
 
@@ -107,23 +114,45 @@ subscription.removeStream(userStream);
107
114
  subscription.removeAllStreams();
108
115
  ```
109
116
 
110
- #### Initial data on subscribe
117
+ #### Customizing DexieChannel
111
118
 
112
- To push a snapshot when a stream is added, set `DexieChannel.on_subscribe`. It receives the channel and the subscribed record (resolved from the signed GlobalID), so you can dispatch on the record type:
119
+ Add custom actions or push initial data directly on your channel:
113
120
 
114
121
  ```ruby
115
- # config/initializers/dexiecable.rb
116
- DexieChannel.on_subscribe = ->(channel, record) do
117
- case record
118
- when User
119
- channel.table("notifications").bulkAdd(record.notifications.map(&:as_json_for_dexie))
120
- when Conversation
121
- channel.table("messages").bulkAdd(record.messages.map(&:as_json_for_dexie))
122
+ # app/channels/dexie_channel.rb
123
+ class DexieChannel < ApplicationCable::Channel
124
+ include DexieCable
125
+
126
+ # Push a snapshot when a private stream is added.
127
+ def subscribed_to(record, params)
128
+ case record
129
+ when User
130
+ table("notifications").bulkAdd(record.notifications.map(&:as_json_for_dexie))
131
+ when Conversation
132
+ table("messages").bulkAdd(record.messages.where("seq_id > ?", params[:last_seq_id]).map(&:as_json_for_dexie))
133
+ end
134
+ end
135
+
136
+ # Any public method is a custom action the client can perform.
137
+ def mark_as_read(data)
138
+ Message.find(data["id"]).update!(read: true)
122
139
  end
123
140
  end
124
141
  ```
125
142
 
126
- The callback runs after the stream is opened, and `channel.table(...)` transmits to just this subscriber — so the snapshot arrives before any live mutation.
143
+ The client subscribes to `DexieChannel` by default:
144
+
145
+ ```js
146
+ const subscription = subscribe(db);
147
+ ```
148
+
149
+ Pass params from the client when adding a stream:
150
+
151
+ ```js
152
+ subscription.addStream(userStream, { last_seq_id: 100 });
153
+ ```
154
+
155
+ `subscribed_to` runs after the stream is opened, and `table(...)` transmits to just this subscriber — so the snapshot arrives before any live mutation. Custom actions are triggered like any ActionCable action: `subscription.perform("mark_as_read", { id: 42 })`.
127
156
 
128
157
  #### Public streams
129
158
 
@@ -0,0 +1,118 @@
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
+
29
+ included do
30
+ public :transmit
31
+ end
32
+
33
+ class_methods do
34
+ # Open a scoped channel for broadcasting to a specific recipient.
35
+ #
36
+ # A String recipient is a public stream name (namespaced under
37
+ # +public:+); any other recipient targets that recipient's own stream.
38
+ def [](recipient)
39
+ target = recipient.is_a?(String) ? "#{PUBLIC_STREAM_PREFIX}#{recipient}" : recipient
40
+ ScopedChannel.new(self, target)
41
+ end
42
+
43
+ # Returns a signed token for +target+ (a record or other private
44
+ # target). Send it to the client, which passes it to
45
+ # +subscription.addStream+.
46
+ def stream_token_for(target)
47
+ verifier.generate(target.respond_to?(:to_gid_param) ? target.to_gid_param : target.to_s)
48
+ end
49
+
50
+ def verifier
51
+ @verifier ||= Rails.application.message_verifier("dexiecable:streams")
52
+ end
53
+ end
54
+
55
+ # Build a query against a Dexie table, transmitted to all subscribers
56
+ # of this channel.
57
+ def table(name)
58
+ Query.new(self, name)
59
+ end
60
+
61
+ def subscribed
62
+ # Streams are added dynamically via +add_stream+.
63
+ end
64
+
65
+ def add_stream(data)
66
+ target = verified_target(data["stream"])
67
+ return unless target
68
+
69
+ stream_for target
70
+ params = data.except("action", "stream").with_indifferent_access
71
+ subscribed_to(resolve_subscribe_target(target), params)
72
+ end
73
+
74
+ def remove_stream(data)
75
+ target = verified_target(data["stream"])
76
+ return unless target
77
+
78
+ stop_stream_from self.class.broadcasting_for(target)
79
+ end
80
+
81
+ def remove_all_streams(_data)
82
+ stop_all_streams
83
+ end
84
+
85
+ def add_public_stream(data)
86
+ name = data["stream"].to_s
87
+ stream_from public_stream_name(name) if name.present?
88
+ end
89
+
90
+ def remove_public_stream(data)
91
+ name = data["stream"].to_s
92
+ stop_stream_from public_stream_name(name) if name.present?
93
+ end
94
+
95
+ # Override to push initial data when a private stream is added. +record+
96
+ # is the resolved target and +params+ are any extra params sent from the
97
+ # client.
98
+ def subscribed_to(_record, _params)
99
+ end
100
+
101
+ private
102
+
103
+ def public_stream_name(name)
104
+ self.class.broadcasting_for("#{PUBLIC_STREAM_PREFIX}#{name}")
105
+ end
106
+
107
+ def verified_target(token)
108
+ return unless token.present?
109
+
110
+ self.class.verifier.verify(token)
111
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
112
+ nil
113
+ end
114
+
115
+ def resolve_subscribe_target(target)
116
+ target.start_with?("gid://") ? GlobalID::Locator.locate(target) : target
117
+ end
118
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DexieCable
4
- VERSION = "2.0.0.alpha2"
4
+ VERSION = "2.0.0.alpha4"
5
5
  end
data/lib/dexiecable.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "dexiecable/version"
4
+ require_relative "dexiecable/concern"
4
5
  require_relative "dexiecable/scoped_channel"
5
6
  require_relative "dexiecable/query"
6
7
  require_relative "dexiecable/active_record_ext"
7
- require_relative "dexiecable/dexie_channel" if defined?(ActionCable::Channel::Base)
8
8
  require_relative "dexiecable/railtie" if defined?(Rails::Railtie)
9
9
 
10
10
  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.alpha2
4
+ version: 2.0.0.alpha4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Buhrmester
@@ -50,7 +50,7 @@ files:
50
50
  - README.md
51
51
  - lib/dexiecable.rb
52
52
  - lib/dexiecable/active_record_ext.rb
53
- - lib/dexiecable/dexie_channel.rb
53
+ - lib/dexiecable/concern.rb
54
54
  - lib/dexiecable/query.rb
55
55
  - lib/dexiecable/railtie.rb
56
56
  - lib/dexiecable/scoped_channel.rb
@@ -1,119 +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
- # When a private stream is added, +on_subscribe+ is invoked with the
17
- # channel and the subscribed record, so initial data can be pushed:
18
- #
19
- # DexieChannel.on_subscribe = ->(channel, record) do
20
- # case record
21
- # when User
22
- # channel.table("notifications").bulkAdd(record.notifications.map(&:as_json_for_dexie))
23
- # when Conversation
24
- # channel.table("messages").bulkAdd(record.messages.map(&:as_json_for_dexie))
25
- # end
26
- # end
27
- #
28
- class DexieChannel < ActionCable::Channel::Base
29
- PUBLIC_STREAM_PREFIX = "public:"
30
-
31
- class_attribute :on_subscribe, instance_accessor: false, default: nil
32
-
33
- public :transmit
34
-
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
- #
40
- # DexieChannel[current_user].table("notifications").add(notification)
41
- # DexieChannel["feed"].table("announcements").add(announcement)
42
- #
43
- def self.[](recipient)
44
- target = recipient.is_a?(String) ? "#{PUBLIC_STREAM_PREFIX}#{recipient}" : recipient
45
- ScopedChannel.new(self, target)
46
- end
47
-
48
- # Build a query against a Dexie table, transmitted to all subscribers
49
- # of this channel.
50
- #
51
- # table("messages").where(:room_id).equals(room.id).add(message)
52
- #
53
- def table(name)
54
- Query.new(self, name)
55
- end
56
-
57
- # Returns a signed token for +target+ (a record or other private
58
- # target). Send it to the client, which passes it to
59
- # +subscription.addStream+.
60
- def self.stream_token_for(target)
61
- verifier.generate(target.respond_to?(:to_gid_param) ? target.to_gid_param : target.to_s)
62
- end
63
-
64
- def self.verifier
65
- @verifier ||= Rails.application.message_verifier("dexiecable:streams")
66
- end
67
-
68
- def subscribed
69
- # Streams are added dynamically via +add_stream+.
70
- end
71
-
72
- def add_stream(data)
73
- target = verified_target(data["stream"])
74
- return unless target
75
-
76
- stream_for target
77
- self.class.on_subscribe&.call(self, resolve_subscribe_target(target))
78
- end
79
-
80
- def remove_stream(data)
81
- target = verified_target(data["stream"])
82
- return unless target
83
-
84
- stop_stream_from self.class.broadcasting_for(target)
85
- end
86
-
87
- def remove_all_streams(_data)
88
- stop_all_streams
89
- end
90
-
91
- def add_public_stream(data)
92
- name = data["stream"].to_s
93
- stream_from public_stream_name(name) if name.present?
94
- end
95
-
96
- def remove_public_stream(data)
97
- name = data["stream"].to_s
98
- stop_stream_from public_stream_name(name) if name.present?
99
- end
100
-
101
- private
102
-
103
- def public_stream_name(name)
104
- self.class.broadcasting_for("#{PUBLIC_STREAM_PREFIX}#{name}")
105
- end
106
-
107
- def verified_target(token)
108
- return unless token.present?
109
-
110
- self.class.verifier.verify(token)
111
- rescue ActiveSupport::MessageVerifier::InvalidSignature
112
- nil
113
- end
114
-
115
- def resolve_subscribe_target(target)
116
- target.start_with?("gid://") ? GlobalID::Locator.locate(target) : target
117
- end
118
- end
119
- end