dexiecable 1.0.0 → 2.0.0.alpha2
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 +114 -113
- data/lib/dexiecable/active_record_ext.rb +18 -22
- data/lib/dexiecable/dexie_channel.rb +119 -0
- data/lib/dexiecable/version.rb +1 -1
- data/lib/dexiecable.rb +1 -1
- metadata +3 -3
- data/lib/dexiecable/concern.rb +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 714562cd7c933eb48e81e9159acd326d77b6133a807cdd541000f350281c1c94
|
|
4
|
+
data.tar.gz: d3bcacdc18d5f9c7a8be970f9ede859be19e282e8ca35aa4d27c81e581310eb3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed0dbe4fdfade70f3300d9c2a61882f6a00320c384fe74ad76b6ba11ef0733a26b86f6949606117b4847efdd5221709bf1a091088a510a0155d6ff9f48a87774
|
|
7
|
+
data.tar.gz: d5cef46c7417c0e34ff4e08c08e0df846ff653f19ae0eeacbffe9de433a362c9ae9af60b32f669a6daa301b64a05d3844f227587cf20af4da23ecf149b39cfb9
|
data/README.md
CHANGED
|
@@ -1,42 +1,28 @@
|
|
|
1
1
|
# DexieCable
|
|
2
2
|
|
|
3
3
|
> [!NOTE]
|
|
4
|
-
>
|
|
4
|
+
> DexieCable is NOT meant to be a local-first solution. It has no automatic capability to sync updates back to the server. For now, think of it as an alternative to Turbo Streams built with component frameworks (Vue, React, Svelte, etc) in mind.
|
|
5
5
|
>
|
|
6
|
-
> Full synchronization utilizing event streams will arrive in DexieCable
|
|
6
|
+
> Full synchronization utilizing event streams will arrive in DexieCable 3.0.
|
|
7
7
|
|
|
8
|
-
DexieCable
|
|
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.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
```ruby
|
|
13
|
-
class UserChannel < ApplicationCable::Channel
|
|
14
|
-
include DexieCable
|
|
15
|
-
|
|
16
|
-
def subscribed
|
|
17
|
-
stream_for current_user
|
|
18
|
-
recent_notifications = current_user.notifications.last(10)
|
|
19
|
-
table("notifications").bulkAdd(recent_notifications)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Or from inside a controller:
|
|
10
|
+
Push Dexie table updates to a client from anywhere on the server:
|
|
25
11
|
|
|
26
12
|
```ruby
|
|
27
13
|
class NotificationsController < ApplicationController
|
|
28
14
|
def create
|
|
29
15
|
notification = current_user.notifications.create!(notification_params)
|
|
30
|
-
|
|
16
|
+
DexieChannel[current_user].table("notifications").add(notification)
|
|
31
17
|
end
|
|
32
18
|
end
|
|
33
19
|
```
|
|
34
20
|
|
|
35
|
-
|
|
21
|
+
Or sync model changes automatically with the `syncs_to_dexie` macro (more info [below](#syncs_to_dexie--automatic-model-syncing))
|
|
36
22
|
|
|
37
23
|
```ruby
|
|
38
24
|
class Notification < ApplicationRecord
|
|
39
|
-
|
|
25
|
+
syncs_to_dexie via: :user
|
|
40
26
|
end
|
|
41
27
|
```
|
|
42
28
|
|
|
@@ -50,7 +36,7 @@ Add to your `Gemfile`:
|
|
|
50
36
|
gem "dexiecable"
|
|
51
37
|
```
|
|
52
38
|
|
|
53
|
-
Then `bundle install`. The Railtie automatically extends `ActiveRecord::Base` with `
|
|
39
|
+
Then `bundle install`. The Railtie automatically extends `ActiveRecord::Base` with `syncs_to_dexie`.
|
|
54
40
|
|
|
55
41
|
### npm package
|
|
56
42
|
|
|
@@ -66,7 +52,9 @@ Pass your Dexie database as the first argument to `subscribe()`:
|
|
|
66
52
|
import { subscribe } from "dexiecable";
|
|
67
53
|
import { db } from "./db";
|
|
68
54
|
|
|
69
|
-
subscribe(db
|
|
55
|
+
const subscription = subscribe(db);
|
|
56
|
+
// Stream tokens come from the server: DexieChannel.stream_token_for(target)
|
|
57
|
+
subscription.addStream(streamToken);
|
|
70
58
|
```
|
|
71
59
|
|
|
72
60
|
A consumer is lazily created on the first `subscribe()` call. If you need to access or set the consumer explicitly, use `getConsumer()` and `setConsumer()`:
|
|
@@ -83,24 +71,87 @@ setConsumer(createConsumer("wss://example.com/cable"));
|
|
|
83
71
|
|
|
84
72
|
## Usage
|
|
85
73
|
|
|
86
|
-
###
|
|
74
|
+
### DexieChannel
|
|
75
|
+
|
|
76
|
+
DexieCable ships one channel — `DexieChannel` — so you never define your own channels or include anything. Every Dexie broadcast goes through it.
|
|
77
|
+
|
|
78
|
+
Stream tokens are signed with the application secret, so a client can only subscribe to streams the server has issued for it:
|
|
87
79
|
|
|
88
80
|
```ruby
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
DexieChannel.stream_token_for(target)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For an ActiveRecord model it signs the record's GlobalID:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
DexieChannel.stream_token_for(current_user)
|
|
88
|
+
# => "signed token for gid://app/User/1"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Send that token to the client (render it in a view, return it from an endpoint, etc.) and add it to the subscription:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
const subscription = subscribe(db);
|
|
95
|
+
subscription.addStream(userStream);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The client now receives every mutation broadcast to `current_user`. To stop listening:
|
|
91
99
|
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
```js
|
|
101
|
+
subscription.removeStream(userStream);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`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:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
subscription.removeAllStreams();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Initial data on subscribe
|
|
111
|
+
|
|
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:
|
|
113
|
+
|
|
114
|
+
```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))
|
|
94
122
|
end
|
|
95
123
|
end
|
|
96
124
|
```
|
|
97
125
|
|
|
98
|
-
|
|
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.
|
|
99
127
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
#### Public streams
|
|
129
|
+
|
|
130
|
+
For data that's public (a global feed, announcements, etc.), skip the signature. Use a string target — it's namespaced under `public:` automatically:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
DexieChannel["feed"].table("announcements").add(announcement)
|
|
134
|
+
|
|
135
|
+
# or, on a model:
|
|
136
|
+
class Announcement < ApplicationRecord
|
|
137
|
+
syncs_to_dexie via: "feed"
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Then subscribe by name — no token required:
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
subscription.addPublicStream("feed");
|
|
145
|
+
subscription.removePublicStream("feed");
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Public streams are namespaced under `public:`, so this path can never reach a signed (private) stream.
|
|
149
|
+
|
|
150
|
+
`DexieChannel[target]` returns a scoped channel for broadcasting to one recipient:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
DexieChannel[current_user].table("notifications").add(notification)
|
|
154
|
+
```
|
|
104
155
|
|
|
105
156
|
### Chaining Dexie operations
|
|
106
157
|
|
|
@@ -108,24 +159,24 @@ Any Dexie.js write operation triggers an immediate broadcast:
|
|
|
108
159
|
|
|
109
160
|
```ruby
|
|
110
161
|
# Single insert
|
|
111
|
-
|
|
162
|
+
DexieChannel[current_user].table("messages").add(id: 1, text: "hello")
|
|
112
163
|
|
|
113
164
|
# Bulk insert
|
|
114
|
-
|
|
165
|
+
DexieChannel[current_user].table("messages").bulkAdd(messages)
|
|
115
166
|
|
|
116
167
|
# Update (using modify)
|
|
117
|
-
|
|
168
|
+
DexieChannel[current_user]
|
|
118
169
|
.table("messages")
|
|
119
170
|
.where(:id).equals(msg.id)
|
|
120
171
|
.modify(read: true)
|
|
121
172
|
|
|
122
173
|
# Update (using update)
|
|
123
|
-
|
|
174
|
+
DexieChannel[current_user]
|
|
124
175
|
.table("messages")
|
|
125
176
|
.update(msg.id, text: "updated text")
|
|
126
177
|
|
|
127
178
|
# Delete
|
|
128
|
-
|
|
179
|
+
DexieChannel[current_user]
|
|
129
180
|
.table("messages")
|
|
130
181
|
.where(:room_id).equals(room.id)
|
|
131
182
|
.delete()
|
|
@@ -133,25 +184,32 @@ UserChannel[current_user]
|
|
|
133
184
|
|
|
134
185
|
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.
|
|
135
186
|
|
|
136
|
-
### `
|
|
187
|
+
### `syncs_to_dexie` — automatic model streaming
|
|
137
188
|
|
|
138
|
-
Add to any ActiveRecord model.
|
|
189
|
+
Add to any ActiveRecord model. Optionally provide the broadcast target.
|
|
139
190
|
|
|
140
191
|
```ruby
|
|
141
192
|
class Message < ApplicationRecord
|
|
142
|
-
# Calls send(:receiver), then broadcasts:
|
|
143
|
-
|
|
193
|
+
# Calls send(:receiver), then broadcasts: DexieChannel.broadcast_to(receiver, ...)
|
|
194
|
+
syncs_to_dexie via: :receiver
|
|
144
195
|
|
|
145
|
-
# String
|
|
146
|
-
|
|
196
|
+
# String = public stream (subscribe via addPublicStream("public"))
|
|
197
|
+
syncs_to_dexie via: "public"
|
|
147
198
|
|
|
148
199
|
# Procs are also supported. If an array is returned, multiple broadcasts are made
|
|
149
|
-
# conversation.users.each { |u|
|
|
150
|
-
|
|
200
|
+
# conversation.users.each { |u| DexieChannel.broadcast_to(u, ...) }
|
|
201
|
+
syncs_to_dexie via: -> { conversation.users }
|
|
151
202
|
end
|
|
152
203
|
```
|
|
153
204
|
|
|
154
|
-
|
|
205
|
+
Broadcasts go out over `DexieChannel`, the channel DexieCable provides. On the client, subscribe to it and add the stream token returned by `DexieChannel.stream_token_for(target)`:
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
const subscription = subscribe(db);
|
|
209
|
+
subscription.addStream(streamIdentifier);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Internally, `syncs_to_dexie` sets up the following ActiveRecord callbacks:
|
|
155
213
|
|
|
156
214
|
| Event | Action |
|
|
157
215
|
|---|---|
|
|
@@ -163,23 +221,21 @@ Internally, `streams_via` sets up the following ActiveRecord callbacks:
|
|
|
163
221
|
|
|
164
222
|
| Option | Default | Description |
|
|
165
223
|
|---|---|---|
|
|
166
|
-
|
|
|
167
|
-
| `to:` | the record itself | The stream target passed to `broadcast_to`. Symbol → calls `send`. String → used as-is. Proc → evaluated in record context. Returns a single recipient or collection. |
|
|
224
|
+
| `via:` | the record itself | The stream target. Symbol → calls `send` (a record, signed). String → public stream name. Proc → evaluated in record context. Returns a single recipient or collection. |
|
|
168
225
|
| `table:` | model's `table_name` | Override the Dexie table name. A Proc is evaluated in the record's context. |
|
|
169
226
|
| `only:` | `[:create, :update, :destroy]` | Limit which events trigger a sync |
|
|
170
227
|
| `with:` | `:as_json_for_dexie` | Method name (Symbol) or Proc for serializing records |
|
|
171
228
|
| `if:` | *(none)* | Symbol (method name) or Proc — only sync when it returns truthy |
|
|
172
229
|
| `unless:` | *(none)* | Symbol (method name) or Proc — skip sync when it returns truthy |
|
|
173
230
|
|
|
174
|
-
You can combine multiple `
|
|
231
|
+
You can combine multiple `syncs_to_dexie` declarations, each with different conditions:
|
|
175
232
|
|
|
176
233
|
```ruby
|
|
177
234
|
class Message < ApplicationRecord
|
|
178
|
-
|
|
179
|
-
|
|
235
|
+
syncs_to_dexie via: -> { sender },
|
|
236
|
+
if: :published?
|
|
180
237
|
|
|
181
|
-
|
|
182
|
-
unless: -> { draft? }
|
|
238
|
+
syncs_to_dexie unless: -> { draft? }
|
|
183
239
|
end
|
|
184
240
|
```
|
|
185
241
|
|
|
@@ -190,23 +246,22 @@ Override `as_json_for_dexie` in your model, or use the `with` option to specify
|
|
|
190
246
|
```ruby
|
|
191
247
|
class Message < ApplicationRecord
|
|
192
248
|
# Using the default as_json_for_dexie override:
|
|
193
|
-
|
|
249
|
+
syncs_to_dexie via: :sender
|
|
194
250
|
|
|
195
251
|
def as_json_for_dexie
|
|
196
252
|
super.merge(room_name: room.name)
|
|
197
253
|
end
|
|
198
254
|
|
|
199
255
|
# Or use a custom serializer method:
|
|
200
|
-
|
|
201
|
-
|
|
256
|
+
syncs_to_dexie via: :admin,
|
|
257
|
+
with: :admin_payload
|
|
202
258
|
|
|
203
259
|
def admin_payload
|
|
204
260
|
attributes.slice("id", "body", "flagged")
|
|
205
261
|
end
|
|
206
262
|
|
|
207
263
|
# Or a Proc:
|
|
208
|
-
|
|
209
|
-
with: -> { { id: id, summary: body.truncate(100) } }
|
|
264
|
+
syncs_to_dexie with: -> { { id: id, summary: body.truncate(100) } }
|
|
210
265
|
end
|
|
211
266
|
```
|
|
212
267
|
|
|
@@ -247,60 +302,6 @@ The JS side replays it as:
|
|
|
247
302
|
dexie.messages.where("room_id").equals(5).add({ id: 1, text: "hello" })
|
|
248
303
|
```
|
|
249
304
|
|
|
250
|
-
## Recipies
|
|
251
|
-
|
|
252
|
-
### Use sequence IDs to avoid data loss
|
|
253
|
-
|
|
254
|
-
A common pattern to avoid data loss during transient disconnections is using sequence IDs to bridge the offline gap and detect gaps in transmitted records. When a connection drops, updates continue on the server. Sending the client’s latest known sequence ID upon reconnect allows the backend to query and stream only the records missed while offline.
|
|
255
|
-
|
|
256
|
-
To enable this, DexieCable ships its own version of the ActionCable client with one key
|
|
257
|
-
extension: **channel params can be functions**. When a param value is a
|
|
258
|
-
function, it is called and awaited at subscribe time — use this to submit the latest known sequence ID on connection:
|
|
259
|
-
|
|
260
|
-
```js
|
|
261
|
-
import { subscribe } from "dexiecable";
|
|
262
|
-
import { db, getLastSeqId } from './database';
|
|
263
|
-
|
|
264
|
-
const roomId = 123;
|
|
265
|
-
|
|
266
|
-
subscribe(db, {
|
|
267
|
-
channel: "RoomChannel",
|
|
268
|
-
room_id: roomId,
|
|
269
|
-
seq_id: () => getLastSeqId(roomId) // evaluated fresh on each reconnect
|
|
270
|
-
});
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
Send missed messages on reconnection:
|
|
274
|
-
|
|
275
|
-
```ruby
|
|
276
|
-
class RoomChannel < ApplicationChannel:Base
|
|
277
|
-
def subscribed
|
|
278
|
-
stream_from "room:#{params[:room_id]}"
|
|
279
|
-
missed_messages = room.messages.where("seq_id > ?", params[:seq_id])
|
|
280
|
-
table("messages").bulkAdd(missed_messages)
|
|
281
|
-
end
|
|
282
|
-
end
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
To add Sequence IDs, you might want to have look at the [Sequenced](https://github.com/derrickreimer/sequenced). Another option is to use [AnyCable](https://docs.anycable.io/rails/getting_started) since it guarantees deliveries of ActionCable messages.
|
|
286
|
-
|
|
287
|
-
### Multi-user environments
|
|
288
|
-
|
|
289
|
-
In multi-user or multi-tenant applications, you can isolate records by binding different subscription channels to separate Dexie database instances. This prevents local data leaks between user accounts and keeps private user data separate from public or shared feeds.
|
|
290
|
-
|
|
291
|
-
```js
|
|
292
|
-
|
|
293
|
-
import Dexie from 'dexie'
|
|
294
|
-
import { subscribe } from 'dexiecable'
|
|
295
|
-
|
|
296
|
-
const userDB = new Dexie("user_"+userId)
|
|
297
|
-
const sharedDB = new Dexie("shared")
|
|
298
|
-
|
|
299
|
-
subscribe(userDB, 'UserChannel')
|
|
300
|
-
subscribe(sharedDB, 'PublicChannel')
|
|
301
|
-
|
|
302
|
-
```
|
|
303
|
-
|
|
304
305
|
## License
|
|
305
306
|
|
|
306
307
|
MIT
|
|
@@ -5,25 +5,21 @@ module DexieCable
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
class_methods do
|
|
8
|
-
# Declares that this model syncs changes to Dexie (IndexedDB) via
|
|
9
|
-
#
|
|
8
|
+
# Declares that this model syncs changes to Dexie (IndexedDB) via
|
|
9
|
+
# DexieChannel.
|
|
10
10
|
#
|
|
11
11
|
# class Message < ApplicationRecord
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
12
|
+
# syncs_to_dexie via: :sender
|
|
13
|
+
# syncs_to_dexie via: "global_feed"
|
|
14
|
+
# syncs_to_dexie via: -> { conversation.users }
|
|
15
|
+
# syncs_to_dexie
|
|
16
16
|
# end
|
|
17
17
|
#
|
|
18
|
-
# @param via [
|
|
19
|
-
# first positional argument. Each recipient is
|
|
20
|
-
# mapped through +via[to]+ to produce scoped
|
|
21
|
-
# channels.
|
|
22
|
-
# @param to [Proc, Symbol, String] A Proc evaluated in record
|
|
18
|
+
# @param via [Proc, Symbol, String] A Proc evaluated in record
|
|
23
19
|
# context, a Symbol to call via +send+, or a String
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
20
|
+
# naming a public stream. Must return a single
|
|
21
|
+
# recipient or collection of recipients. Defaults to
|
|
22
|
+
# the record itself.
|
|
27
23
|
# @param table [String, Symbol, Proc] Override the Dexie table name
|
|
28
24
|
# (defaults to the model's table_name). A Proc is
|
|
29
25
|
# evaluated in the record's context.
|
|
@@ -35,19 +31,19 @@ module DexieCable
|
|
|
35
31
|
# returns truthy (evaluated in the record's context).
|
|
36
32
|
# @param unless [Symbol, Proc] Skip sync if the given method or proc
|
|
37
33
|
# returns truthy (evaluated in the record's context).
|
|
38
|
-
def
|
|
34
|
+
def syncs_to_dexie(via: nil, table: nil, only: nil, with: nil, **options)
|
|
39
35
|
events = Array(only || %i[create update destroy])
|
|
40
36
|
conditions = options.slice(:if, :unless)
|
|
41
37
|
serializer = with || :as_json_for_dexie
|
|
42
38
|
|
|
43
39
|
@dexie_sync_configs ||= []
|
|
44
|
-
@dexie_sync_configs << { via: via,
|
|
40
|
+
@dexie_sync_configs << { via: via, table: table, only: events, with: serializer, **conditions }
|
|
45
41
|
|
|
46
42
|
if events.include?(:destroy)
|
|
47
43
|
before_destroy :dexie_sync_before_destroy
|
|
48
44
|
|
|
49
45
|
after_commit on: :destroy, **conditions do
|
|
50
|
-
resolve_channels(via
|
|
46
|
+
resolve_channels(via).each do |channel|
|
|
51
47
|
next unless channel
|
|
52
48
|
channel.table(resolve_table(table)).delete(dexie_destroy_id)
|
|
53
49
|
end
|
|
@@ -56,7 +52,7 @@ module DexieCable
|
|
|
56
52
|
|
|
57
53
|
if events.include?(:create)
|
|
58
54
|
after_commit on: :create, **conditions do
|
|
59
|
-
resolve_channels(via
|
|
55
|
+
resolve_channels(via).each do |channel|
|
|
60
56
|
next unless channel
|
|
61
57
|
channel.table(resolve_table(table)).add(resolve(serializer))
|
|
62
58
|
end
|
|
@@ -65,7 +61,7 @@ module DexieCable
|
|
|
65
61
|
|
|
66
62
|
if events.include?(:update)
|
|
67
63
|
after_commit on: :update, **conditions do
|
|
68
|
-
resolve_channels(via
|
|
64
|
+
resolve_channels(via).each do |channel|
|
|
69
65
|
next unless channel
|
|
70
66
|
|
|
71
67
|
changes = resolve(serializer).slice(*saved_changes.keys)
|
|
@@ -78,9 +74,9 @@ module DexieCable
|
|
|
78
74
|
|
|
79
75
|
private
|
|
80
76
|
|
|
81
|
-
def resolve_channels(via
|
|
82
|
-
recipients =
|
|
83
|
-
Array(recipients).map { |r|
|
|
77
|
+
def resolve_channels(via = nil)
|
|
78
|
+
recipients = via ? resolve(via) : self
|
|
79
|
+
Array(recipients).map { |r| DexieChannel[r] }
|
|
84
80
|
end
|
|
85
81
|
|
|
86
82
|
def resolve(val)
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
data/lib/dexiecable/version.rb
CHANGED
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"
|
|
5
4
|
require_relative "dexiecable/scoped_channel"
|
|
6
5
|
require_relative "dexiecable/query"
|
|
7
6
|
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:
|
|
4
|
+
version: 2.0.0.alpha2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stefan Buhrmester
|
|
@@ -39,7 +39,7 @@ dependencies:
|
|
|
39
39
|
version: '7.0'
|
|
40
40
|
description: DexieCable augments ActionCable channels with a query DSL that mirrors
|
|
41
41
|
the Dexie.js API, letting you push database mutations from the server to the client
|
|
42
|
-
in real time. Includes an ActiveRecord macro (
|
|
42
|
+
in real time. Includes an ActiveRecord macro (syncs_to_dexie) for automatic change
|
|
43
43
|
syncing.
|
|
44
44
|
email:
|
|
45
45
|
- stefan@buhrmi.de
|
|
@@ -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/
|
|
53
|
+
- lib/dexiecable/dexie_channel.rb
|
|
54
54
|
- lib/dexiecable/query.rb
|
|
55
55
|
- lib/dexiecable/railtie.rb
|
|
56
56
|
- lib/dexiecable/scoped_channel.rb
|
data/lib/dexiecable/concern.rb
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module DexieCable
|
|
4
|
-
extend ActiveSupport::Concern
|
|
5
|
-
|
|
6
|
-
included do
|
|
7
|
-
public :transmit
|
|
8
|
-
|
|
9
|
-
# Open a scoped channel for broadcasting to a specific recipient.
|
|
10
|
-
#
|
|
11
|
-
# UserChannel[current_user].table("notifications").add(notification)
|
|
12
|
-
#
|
|
13
|
-
def self.[](recipient)
|
|
14
|
-
ScopedChannel.new(self, recipient)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Build a query against a Dexie table, transmitted to all subscribers
|
|
18
|
-
# of this channel.
|
|
19
|
-
#
|
|
20
|
-
# table("messages").where(:room_id).equals(room.id).add(message)
|
|
21
|
-
#
|
|
22
|
-
def table(name)
|
|
23
|
-
Query.new(self, name)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|