dexiecable 2.0.0 → 2.0.2
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 +53 -14
- data/lib/dexiecable/concern.rb +35 -23
- 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: c5284c177e5517bff845edf2e5317d1ab9c676deafd605a662b3dea1b7363b6a
|
|
4
|
+
data.tar.gz: 94f3e811e2bca16d05967c0c0dbf242bcae00b880212ad0dba7d1ee44f8263c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4999890a88da548b86e19843b40a0221654eb2a144ec52f954c7723cb652c287d7884fd34e0703d5895131e3b1213ee840a427f31defa91ff21738a38133514a
|
|
7
|
+
data.tar.gz: 67bb97e60d5d9fc7416e239e0695b69ff00a4930df8d97d8380da29bd51686af9f2d70f04bd69d92322e61ea042a71704ce4e98bbc35d14ee5f0a33d13343def
|
data/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# DexieCable
|
|
2
2
|
|
|
3
3
|
> [!NOTE]
|
|
4
|
-
> DexieCable is NOT
|
|
4
|
+
> By itself, DexieCable is NOT a local-first solution. It has no automatic capability to push client-side changes back to the server.
|
|
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
|
|
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.
|
|
9
9
|
|
|
10
10
|
Push Dexie table updates to a client from anywhere on the server:
|
|
11
11
|
|
|
@@ -18,7 +18,7 @@ class NotificationsController < ApplicationController
|
|
|
18
18
|
end
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Or sync model changes automatically with the `syncs_to_dexie` macro (more info [below](#syncs_to_dexie
|
|
21
|
+
Or sync model changes automatically with the `syncs_to_dexie` macro (more info [below](#syncs_to_dexie-automatic-model-streaming))
|
|
22
22
|
|
|
23
23
|
```ruby
|
|
24
24
|
class Notification < ApplicationRecord
|
|
@@ -26,6 +26,40 @@ class Notification < ApplicationRecord
|
|
|
26
26
|
end
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
## What's new in 2.0
|
|
30
|
+
|
|
31
|
+
DexieCable 2.0 is a mixin. Create your own `DexieChannel` and `include DexieCable`:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
class DexieChannel < ApplicationCable::Channel
|
|
35
|
+
include DexieCable
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
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
|
+
|
|
41
|
+
```js
|
|
42
|
+
const subscription = subscribe(db);
|
|
43
|
+
const unsubscribe = subscription.addStream(userStreamToken);
|
|
44
|
+
```
|
|
45
|
+
|
|
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:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
class DexieChannel < ApplicationCable::Channel
|
|
50
|
+
include DexieCable
|
|
51
|
+
|
|
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
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
29
63
|
## Installation
|
|
30
64
|
|
|
31
65
|
### Ruby gem
|
|
@@ -101,6 +135,8 @@ Tokens never expire by default. Pass `expires_in:` or `expires_at:` to limit a t
|
|
|
101
135
|
DexieChannel.stream_token_for(current_user, expires_in: 1.day)
|
|
102
136
|
```
|
|
103
137
|
|
|
138
|
+
An expired token, or one whose record no longer exists, is rejected. Only plain strings are treated as public stream names.
|
|
139
|
+
|
|
104
140
|
Send that token to the client (render it in a view, return it from an endpoint, etc.) and add it to the subscription:
|
|
105
141
|
|
|
106
142
|
```js
|
|
@@ -114,7 +150,7 @@ const stopStreaming = subscription.addStream(userStream);
|
|
|
114
150
|
stopStreaming(); // equivalent to subscription.removeStream(userStream)
|
|
115
151
|
```
|
|
116
152
|
|
|
117
|
-
`addStream`/`removeStream` perform `add_stream`/`remove_stream` on `DexieChannel`, which verifies the token and then `stream_from`/`stop_stream_from` the decoded identifier. `removeAllStreams()` performs `remove_all_streams`, stopping every current stream
|
|
153
|
+
`addStream`/`removeStream` perform `add_stream`/`remove_stream` on `DexieChannel`, which verifies the token and then `stream_from`/`stop_stream_from` the decoded identifier. `removeAllStreams()` performs `remove_all_streams`, stopping every current stream. Handy on logout:
|
|
118
154
|
|
|
119
155
|
```js
|
|
120
156
|
subscription.removeAllStreams();
|
|
@@ -161,11 +197,11 @@ Pass params from the client when adding a stream:
|
|
|
161
197
|
subscription.addStream(userStream, { last_seq_id: 100 });
|
|
162
198
|
```
|
|
163
199
|
|
|
164
|
-
`subscribed_to` runs after the stream is opened, and `table(...)` transmits to just this subscriber
|
|
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 })`.
|
|
165
201
|
|
|
166
202
|
#### Public streams
|
|
167
203
|
|
|
168
|
-
For data that's public (a global feed, announcements, etc.), skip the signature. Use a string target
|
|
204
|
+
For data that's public (a global feed, announcements, etc.), skip the signature. Use a string target. It's namespaced under `public:` automatically:
|
|
169
205
|
|
|
170
206
|
```ruby
|
|
171
207
|
DexieChannel["feed"].table("announcements").add(announcement)
|
|
@@ -176,11 +212,11 @@ class Announcement < ApplicationRecord
|
|
|
176
212
|
end
|
|
177
213
|
```
|
|
178
214
|
|
|
179
|
-
Then subscribe by name
|
|
215
|
+
Then subscribe by name. No token required:
|
|
180
216
|
|
|
181
217
|
```js
|
|
182
|
-
const stopPublicStream = subscription.
|
|
183
|
-
stopPublicStream(); // equivalent to subscription.
|
|
218
|
+
const stopPublicStream = subscription.addStream("feed");
|
|
219
|
+
stopPublicStream(); // equivalent to subscription.removeStream("feed")
|
|
184
220
|
```
|
|
185
221
|
|
|
186
222
|
Public streams are namespaced under `public:`, so this path can never reach a signed (private) stream.
|
|
@@ -222,16 +258,19 @@ DexieChannel[current_user]
|
|
|
222
258
|
|
|
223
259
|
The full query chain is serialized as JSON and sent over ActionCable. The JS client replays every method call against the local Dexie database in order.
|
|
224
260
|
|
|
225
|
-
### `syncs_to_dexie
|
|
261
|
+
### `syncs_to_dexie`: automatic model streaming
|
|
226
262
|
|
|
227
263
|
Add to any ActiveRecord model. Optionally provide the broadcast target.
|
|
228
264
|
|
|
229
265
|
```ruby
|
|
230
266
|
class Message < ApplicationRecord
|
|
267
|
+
belongs_to :conversation
|
|
268
|
+
belongs_to :receiver
|
|
269
|
+
|
|
231
270
|
# Calls send(:receiver), then broadcasts: DexieChannel.broadcast_to(receiver, ...)
|
|
232
271
|
syncs_to_dexie via: :receiver
|
|
233
272
|
|
|
234
|
-
# String = public stream (subscribe via
|
|
273
|
+
# String = public stream (subscribe via addStream("public"))
|
|
235
274
|
syncs_to_dexie via: "public"
|
|
236
275
|
|
|
237
276
|
# Procs are also supported. If an array is returned, multiple broadcasts are made
|
|
@@ -263,8 +302,8 @@ Internally, `syncs_to_dexie` sets up the following ActiveRecord callbacks:
|
|
|
263
302
|
| `table:` | model's `table_name` | Override the Dexie table name. A Proc is evaluated in the record's context. |
|
|
264
303
|
| `only:` | `[:create, :update, :destroy]` | Limit which events trigger a sync |
|
|
265
304
|
| `with:` | `:as_json_for_dexie` | Method name (Symbol) or Proc for serializing records |
|
|
266
|
-
| `if:` | *(none)* | Symbol (method name) or Proc
|
|
267
|
-
| `unless:` | *(none)* | Symbol (method name) or Proc
|
|
305
|
+
| `if:` | *(none)* | Symbol (method name) or Proc. Only sync when it returns truthy |
|
|
306
|
+
| `unless:` | *(none)* | Symbol (method name) or Proc. Skip sync when it returns truthy |
|
|
268
307
|
|
|
269
308
|
You can combine multiple `syncs_to_dexie` declarations, each with different conditions:
|
|
270
309
|
|
data/lib/dexiecable/concern.rb
CHANGED
|
@@ -20,7 +20,7 @@ module DexieCable
|
|
|
20
20
|
#
|
|
21
21
|
# subscribe(db) # subscribes to "DexieChannel"
|
|
22
22
|
# subscription.addStream(DexieChannel.stream_token_for(current_user))
|
|
23
|
-
# subscription.
|
|
23
|
+
# subscription.addStream("feed") # plain strings are public streams
|
|
24
24
|
#
|
|
25
25
|
extend ActiveSupport::Concern
|
|
26
26
|
|
|
@@ -72,37 +72,40 @@ module DexieCable
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def add_stream(data)
|
|
75
|
-
|
|
76
|
-
return unless record
|
|
77
|
-
|
|
78
|
-
stream_for record
|
|
75
|
+
token = data["stream"].to_s
|
|
79
76
|
params = data.except("action", "stream").with_indifferent_access
|
|
80
|
-
subscribed_to(record, params)
|
|
81
|
-
end
|
|
82
77
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
if signed_token?(token)
|
|
79
|
+
# A token that no longer resolves (expired, revoked, deleted record) is
|
|
80
|
+
# rejected instead of being treated as a public stream name.
|
|
81
|
+
record = resolve_subscribe_target(token)
|
|
82
|
+
return unless record
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
stream_for record
|
|
85
|
+
subscribed_to(record, params)
|
|
86
|
+
else
|
|
87
|
+
return if token.blank?
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
stream_from public_stream_name(token)
|
|
90
|
+
subscribed_to(token, params)
|
|
91
|
+
end
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
def
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
def remove_stream(data)
|
|
95
|
+
token = data["stream"].to_s
|
|
96
|
+
|
|
97
|
+
if signed_token?(token)
|
|
98
|
+
record = resolve_subscribe_target(token)
|
|
99
|
+
return unless record
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
stop_stream_from self.class.broadcasting_for(record)
|
|
102
|
+
else
|
|
103
|
+
stop_stream_from public_stream_name(token) if token.present?
|
|
104
|
+
end
|
|
101
105
|
end
|
|
102
106
|
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
stop_stream_from public_stream_name(name) if name.present?
|
|
107
|
+
def remove_all_streams(_data)
|
|
108
|
+
stop_all_streams
|
|
106
109
|
end
|
|
107
110
|
|
|
108
111
|
# Override to push initial data when a stream is added. +record+ is the
|
|
@@ -113,6 +116,15 @@ module DexieCable
|
|
|
113
116
|
|
|
114
117
|
private
|
|
115
118
|
|
|
119
|
+
# Distinguishes a signed stream token from a public stream name. A signed
|
|
120
|
+
# token keeps a valid signature even after it expires, which is what lets us
|
|
121
|
+
# reject expired tokens instead of falling back to a public stream.
|
|
122
|
+
def signed_token?(token)
|
|
123
|
+
return false if token.blank? || SignedGlobalID.verifier.nil?
|
|
124
|
+
|
|
125
|
+
SignedGlobalID.verifier.valid_message?(token)
|
|
126
|
+
end
|
|
127
|
+
|
|
116
128
|
def public_stream_name(name)
|
|
117
129
|
self.class.broadcasting_for("#{PUBLIC_STREAM_PREFIX}#{name}")
|
|
118
130
|
end
|
data/lib/dexiecable/version.rb
CHANGED