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 +4 -4
- data/README.md +66 -23
- data/lib/dexiecable/active_record_ext.rb +45 -0
- data/lib/dexiecable/concern.rb +43 -14
- data/lib/dexiecable/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef549275f108c037de78cad7dcabb5c3b3a0dfae666d6a56e413abd4bc1079f1
|
|
4
|
+
data.tar.gz: cb55611cd29a96e7fd79885b5e7575534f5f19e4ea417c745c1e8b13da9df59d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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.
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/dexiecable/concern.rb
CHANGED
|
@@ -7,12 +7,18 @@ module DexieCable
|
|
|
7
7
|
# class DexieChannel < ApplicationCable::Channel
|
|
8
8
|
# include DexieCable
|
|
9
9
|
#
|
|
10
|
-
# # Push initial data when a
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
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(
|
|
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
|
-
|
|
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.
|
data/lib/dexiecable/version.rb
CHANGED