dexiecable 2.0.2 → 3.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5284c177e5517bff845edf2e5317d1ab9c676deafd605a662b3dea1b7363b6a
4
- data.tar.gz: 94f3e811e2bca16d05967c0c0dbf242bcae00b880212ad0dba7d1ee44f8263c9
3
+ metadata.gz: ef549275f108c037de78cad7dcabb5c3b3a0dfae666d6a56e413abd4bc1079f1
4
+ data.tar.gz: cb55611cd29a96e7fd79885b5e7575534f5f19e4ea417c745c1e8b13da9df59d
5
5
  SHA512:
6
- metadata.gz: 4999890a88da548b86e19843b40a0221654eb2a144ec52f954c7723cb652c287d7884fd34e0703d5895131e3b1213ee840a427f31defa91ff21738a38133514a
7
- data.tar.gz: 67bb97e60d5d9fc7416e239e0695b69ff00a4930df8d97d8380da29bd51686af9f2d70f04bd69d92322e61ea042a71704ce4e98bbc35d14ee5f0a33d13343def
6
+ metadata.gz: 2be1d8dcf416c79e83c6227762877c339e9bdb227956108f5f8ce80327b189c25a1c7ac9f974b330e1bf1f50dd058b3aa09ad2399f1e2642d58ed43622243b85
7
+ data.tar.gz: 8c2f011b0381a44ce0c5bc55ca0eeecd0beda02a359f16022770862d54b2710f6076a7388437f11d3ec673a115c8b4595fb9862332e2f3cd055fd8aaede8110b
data/README.md CHANGED
@@ -5,7 +5,13 @@
5
5
  >
6
6
  > An addon providing full synchronization based on event streams is currently in development. But for now, if you need full synchronization, you'll have to roll your own.
7
7
 
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-streaming) ActiveRecord macro for automatic change syncing.
8
+ ## Who's this for?
9
+
10
+ DexieCable is made for Ruby on Rails apps that manage their client-side state in [Dexie](https://dexie.org) and use Dexie live queries for reactive UI updates. It's an alternative to Turbo Streams for apps that opt to use component frameworks (React, Vue, Svelte, etc.) instead of Turbo.
11
+
12
+ ## How does it work?
13
+
14
+ DexieCable gives your ActionCable channel a query DSL that mirrors the [Dexie.js API](https://dexie.org/docs), 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-streaming) ActiveRecord macro for automatic change syncing.
9
15
 
10
16
  Push Dexie table updates to a client from anywhere on the server:
11
17
 
@@ -28,6 +34,8 @@ end
28
34
 
29
35
  ## What's new in 2.0
30
36
 
37
+ ### Custom Channels
38
+
31
39
  DexieCable 2.0 is a mixin. Create your own `DexieChannel` and `include DexieCable`:
32
40
 
33
41
  ```ruby
@@ -36,6 +44,8 @@ class DexieChannel < ApplicationCable::Channel
36
44
  end
37
45
  ```
38
46
 
47
+ ### Reuse the same subscription for multiple streams
48
+
39
49
  On the client, subscribe to that channel and add streams as needed. `addStream` accepts a signed token or a plain string, and returns a function that removes the stream:
40
50
 
41
51
  ```js
@@ -43,19 +53,28 @@ const subscription = subscribe(db);
43
53
  const unsubscribe = subscription.addStream(userStreamToken);
44
54
  ```
45
55
 
46
- You can then use the new `subscribed_to` hook to push initial data before any live mutation arrives. The first argument is the record the token was issued for, or the plain stream name for a public stream:
56
+ ## What's new in 3.0
57
+
58
+ ### `subscribed_to` moved to the model
59
+
60
+ The `subscribed_to` hook now lives on the record rather than on the channel. Private streams declare their initial snapshot on the record the token was issued for, and receive the channel plus any params sent from the client:
61
+
62
+ ```ruby
63
+ class User < ApplicationRecord
64
+ subscribed_to do |channel, params|
65
+ channel.table("notifications").bulkAdd(notifications.map(&:as_json_for_dexie))
66
+ end
67
+ end
68
+ ```
69
+
70
+ Public streams register a handler by name in your channel. The block runs in the channel's context, so `table` and `transmit` are available:
47
71
 
48
72
  ```ruby
49
73
  class DexieChannel < ApplicationCable::Channel
50
74
  include DexieCable
51
75
 
52
- def subscribed_to(record, params)
53
- case record
54
- when "feed"
55
- table(record).bulkAdd(Announcement.for_stream(record).map(&:as_json_for_dexie))
56
- when User
57
- table("notifications").bulkAdd(record.notifications.map(&:as_json_for_dexie))
58
- end
76
+ subscribed_to "feed" do |params|
77
+ table("announcements").bulkAdd(Announcement.for_stream("feed").map(&:as_json_for_dexie))
59
78
  end
60
79
  end
61
80
  ```
@@ -165,22 +184,46 @@ Add custom actions or push initial data directly on your channel:
165
184
  class DexieChannel < ApplicationCable::Channel
166
185
  include DexieCable
167
186
 
168
- # Push a snapshot when a stream is added. `record` is a record for private
169
- # streams, or the stream name (String) for public streams.
170
- def subscribed_to(record, params)
171
- case record
172
- when User
173
- table("notifications").bulkAdd(record.notifications.map(&:as_json_for_dexie))
174
- when Conversation
175
- table("messages").bulkAdd(record.messages.where("seq_id > ?", params[:last_seq_id]).map(&:as_json_for_dexie))
176
- when String
177
- table(record).bulkAdd(Announcement.for_stream(record).map(&:as_json_for_dexie))
178
- end
187
+ # Push a snapshot when a public stream is added. The block receives the
188
+ # params sent from the client and runs in the channel's context, so
189
+ # `table(...)` and `transmit` are available.
190
+ subscribed_to "feed" do |params|
191
+ table("feed").bulkAdd(Announcement.for_stream("feed").map(&:as_json_for_dexie))
179
192
  end
180
193
 
181
- # Any public method is a custom action the client can perform.
194
+ # Any public method is a custom action the client can perform. Always
195
+ # resolve records through the connection's actor, never from the payload:
196
+ # `Message.find(data["id"])` would let any client mark anyone's message
197
+ # read.
182
198
  def mark_as_read(data)
183
- Message.find(data["id"]).update!(read: true)
199
+ current_user.messages.find(data["id"]).update!(read: true)
200
+ end
201
+ end
202
+ ```
203
+
204
+ Private streams declare their snapshot on the record itself, which is where the stream was opened for. The block runs in the record's context and receives the channel — so `channel.table(...)` transmits to just this subscriber — plus the params sent from the client:
205
+
206
+ ```ruby
207
+ class User < ApplicationRecord
208
+ subscribed_to do |channel, params|
209
+ channel.table("notifications").bulkAdd(notifications.map(&:as_json_for_dexie))
210
+ end
211
+ end
212
+
213
+ class Conversation < ApplicationRecord
214
+ subscribed_to do |channel, params|
215
+ channel.table("messages").bulkAdd(messages.where("seq_id > ?", params[:last_seq_id]).map(&:as_json_for_dexie))
216
+ end
217
+ end
218
+ ```
219
+
220
+ Declare `subscribed_to` more than once to push several snapshots — the blocks run in declaration order. To share a snapshot across models, or take full control, override the instance method instead and call `super` to keep any registered blocks running:
221
+
222
+ ```ruby
223
+ module HasNotifications
224
+ def subscribed_to(channel, params)
225
+ channel.table("notifications").bulkAdd(notifications.map(&:as_json_for_dexie))
226
+ super
184
227
  end
185
228
  end
186
229
  ```
@@ -197,7 +240,7 @@ Pass params from the client when adding a stream:
197
240
  subscription.addStream(userStream, { last_seq_id: 100 });
198
241
  ```
199
242
 
200
- `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 })`.
243
+ `subscribed_to` runs after the stream is opened, and the snapshot it pushes (via `table(...)` on the channel, or `channel.table(...)` on the record) transmits to just this subscriber, so it arrives before any live mutation. Custom actions are triggered like any ActionCable action: `subscription.perform("mark_as_read", { id: 42 })`. The payload is client-controlled, so pass it through the authenticated actor — `current_user.messages.find(data["id"])`, not `Message.find(data["id"])`.
201
244
 
202
245
  #### Public streams
203
246
 
@@ -70,6 +70,51 @@ module DexieCable
70
70
  end
71
71
  end
72
72
  end
73
+
74
+ # Declares what to push to a single subscriber when they subscribe to
75
+ # this record's stream, before any live mutation is delivered. The block
76
+ # runs in the record's context and receives the channel and the params
77
+ # sent by the client:
78
+ #
79
+ # class User < ApplicationRecord
80
+ # subscribed_to do |channel, params|
81
+ # channel.table("notifications").bulkAdd(notifications.map(&:as_json_for_dexie))
82
+ # end
83
+ # end
84
+ #
85
+ # Declare it more than once to push several snapshots; blocks run in
86
+ # declaration order.
87
+ def subscribed_to(&block)
88
+ raise ArgumentError, "subscribed_to requires a block" if block.nil?
89
+
90
+ dexie_subscribed_to_blocks << block
91
+ end
92
+
93
+ # The blocks registered with +subscribed_to+, inherited from
94
+ # superclasses.
95
+ def dexie_subscribed_to_blocks
96
+ @dexie_subscribed_to_blocks ||=
97
+ if superclass.respond_to?(:dexie_subscribed_to_blocks)
98
+ superclass.dexie_subscribed_to_blocks.dup
99
+ else
100
+ []
101
+ end
102
+ end
103
+ end
104
+
105
+ # Called when a client subscribes to this record's private stream. Runs
106
+ # every block registered with the +subscribed_to+ macro in the record's
107
+ # context. Override in your model for full control, calling +super+ if you
108
+ # still want the registered blocks to run:
109
+ #
110
+ # def subscribed_to(channel, params)
111
+ # channel.table("notifications").bulkAdd(notifications.map(&:as_json_for_dexie))
112
+ # super
113
+ # end
114
+ def subscribed_to(channel, params)
115
+ self.class.dexie_subscribed_to_blocks.each do |block|
116
+ instance_exec(channel, params, &block)
117
+ end
73
118
  end
74
119
 
75
120
  private
@@ -7,12 +7,18 @@ module DexieCable
7
7
  # class DexieChannel < ApplicationCable::Channel
8
8
  # include DexieCable
9
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
10
+ # # Push initial data when a public stream is added. The block runs in
11
+ # # the channel's context, so +table+ and +transmit+ are available.
12
+ # subscribed_to "feed" do |params|
13
+ # table("announcements").bulkAdd(Announcement.for_stream("feed").map(&:as_json_for_dexie))
14
+ # end
15
+ # end
16
+ #
17
+ # Private streams push their initial data from the record itself:
18
+ #
19
+ # class User < ApplicationRecord
20
+ # def subscribed_to(channel, params)
21
+ # channel.table("notifications").bulkAdd(notifications.map(&:as_json_for_dexie))
16
22
  # end
17
23
  # end
18
24
  #
@@ -41,6 +47,28 @@ module DexieCable
41
47
  ScopedChannel.new(self, target)
42
48
  end
43
49
 
50
+ # Register a handler for a public stream. When a client subscribes to
51
+ # +name+ (for example +subscription.addStream("feed")+), the block is
52
+ # evaluated in the channel's context with the params sent by the client:
53
+ #
54
+ # subscribed_to "feed" do |params|
55
+ # table("announcements").bulkAdd(Announcement.for_stream("feed"))
56
+ # end
57
+ def subscribed_to(name, &block)
58
+ dexie_subscribed_to_handlers[name.to_s] = block
59
+ end
60
+
61
+ # The registered public stream handlers. Subclasses inherit the handlers
62
+ # registered on their superclass.
63
+ def dexie_subscribed_to_handlers
64
+ @dexie_subscribed_to_handlers ||=
65
+ if superclass.respond_to?(:dexie_subscribed_to_handlers)
66
+ superclass.dexie_subscribed_to_handlers.dup
67
+ else
68
+ {}
69
+ end
70
+ end
71
+
44
72
  # Returns a signed token for +target+ (a record). Send it to the client,
45
73
  # which passes it to +subscription.addStream+.
46
74
  #
@@ -82,12 +110,12 @@ module DexieCable
82
110
  return unless record
83
111
 
84
112
  stream_for record
85
- subscribed_to(record, params)
113
+ record.subscribed_to(self, params) if record.respond_to?(:subscribed_to)
86
114
  else
87
115
  return if token.blank?
88
116
 
89
117
  stream_from public_stream_name(token)
90
- subscribed_to(token, params)
118
+ run_subscribed_to_handler(token, params)
91
119
  end
92
120
  end
93
121
 
@@ -108,14 +136,15 @@ module DexieCable
108
136
  stop_all_streams
109
137
  end
110
138
 
111
- # Override to push initial data when a stream is added. +record+ is the
112
- # resolved target — a record for private streams, or the stream name for
113
- # public streams — and +params+ are any extra params sent from the client.
114
- def subscribed_to(_record, _params)
115
- end
116
-
117
139
  private
118
140
 
141
+ # Runs the handler registered with +subscribed_to+ for a public stream, in
142
+ # the channel's context.
143
+ def run_subscribed_to_handler(name, params)
144
+ handler = self.class.dexie_subscribed_to_handlers[name]
145
+ instance_exec(params, &handler) if handler
146
+ end
147
+
119
148
  # Distinguishes a signed stream token from a public stream name. A signed
120
149
  # token keeps a valid signature even after it expires, which is what lets us
121
150
  # reject expired tokens instead of falling back to a public stream.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DexieCable
4
- VERSION = "2.0.2"
4
+ VERSION = "3.0.0"
5
5
  end
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.2
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Buhrmester