dexiecable 1.0.0 → 2.0.0.alpha1
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 +97 -114
- data/lib/dexiecable/active_record_ext.rb +18 -22
- data/lib/dexiecable/dexie_channel.rb +96 -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: 5262c05d144bc7c002152fd8da1918e87421f4ec81a060100b71c57237c152be
|
|
4
|
+
data.tar.gz: 339cf7b68cd9923b0b20b4a74c0caf563699be7ff8fbaf32ace71728a87fc787
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e315b48c4d222dd5c4fb04056dded3f61d9d76584dfc7ee02d1645b6d28521120130b945cda897ca6682b7d6483232a3f6b92a3220bc417dc9fb6d8718169416
|
|
7
|
+
data.tar.gz: a5c155688bd21d6970842b86f28d2f9e4d901561fb5e786a59ed3447ae7b96db083d5b63e1d724b7fc8c5b7c2428661d713b00a5dc41a5c5b0f2a0f62ca568cc
|
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,69 @@ 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
|
+
```
|
|
91
83
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
For an ActiveRecord model it signs the record's GlobalID:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
DexieChannel.stream_token_for(current_user)
|
|
88
|
+
# => "eyJkYXRhIjoiZGV4aWVfY2FibGU6ZGV4aWVfY2hhbm5lbDpnaWQ6Ly9hcHAvVXNlci8xIn0=--signature"
|
|
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:
|
|
99
|
+
|
|
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
|
+
#### Public streams
|
|
111
|
+
|
|
112
|
+
For data that's public (a global feed, announcements, etc.), skip the signature. Use a string target — it's namespaced under `public:` automatically:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
DexieChannel["feed"].table("announcements").add(announcement)
|
|
116
|
+
|
|
117
|
+
# or, on a model:
|
|
118
|
+
class Announcement < ApplicationRecord
|
|
119
|
+
syncs_to_dexie via: "feed"
|
|
95
120
|
end
|
|
96
121
|
```
|
|
97
122
|
|
|
98
|
-
|
|
123
|
+
Then subscribe by name — no token required:
|
|
99
124
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
125
|
+
```js
|
|
126
|
+
subscription.addPublicStream("feed");
|
|
127
|
+
subscription.removePublicStream("feed");
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Public streams are namespaced under `public:`, so this path can never reach a signed (private) stream.
|
|
131
|
+
|
|
132
|
+
`DexieChannel[target]` returns a scoped channel for broadcasting to one recipient:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
DexieChannel[current_user].table("notifications").add(notification)
|
|
136
|
+
```
|
|
104
137
|
|
|
105
138
|
### Chaining Dexie operations
|
|
106
139
|
|
|
@@ -108,24 +141,24 @@ Any Dexie.js write operation triggers an immediate broadcast:
|
|
|
108
141
|
|
|
109
142
|
```ruby
|
|
110
143
|
# Single insert
|
|
111
|
-
|
|
144
|
+
DexieChannel[current_user].table("messages").add(id: 1, text: "hello")
|
|
112
145
|
|
|
113
146
|
# Bulk insert
|
|
114
|
-
|
|
147
|
+
DexieChannel[current_user].table("messages").bulkAdd(messages)
|
|
115
148
|
|
|
116
149
|
# Update (using modify)
|
|
117
|
-
|
|
150
|
+
DexieChannel[current_user]
|
|
118
151
|
.table("messages")
|
|
119
152
|
.where(:id).equals(msg.id)
|
|
120
153
|
.modify(read: true)
|
|
121
154
|
|
|
122
155
|
# Update (using update)
|
|
123
|
-
|
|
156
|
+
DexieChannel[current_user]
|
|
124
157
|
.table("messages")
|
|
125
158
|
.update(msg.id, text: "updated text")
|
|
126
159
|
|
|
127
160
|
# Delete
|
|
128
|
-
|
|
161
|
+
DexieChannel[current_user]
|
|
129
162
|
.table("messages")
|
|
130
163
|
.where(:room_id).equals(room.id)
|
|
131
164
|
.delete()
|
|
@@ -133,25 +166,32 @@ UserChannel[current_user]
|
|
|
133
166
|
|
|
134
167
|
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
168
|
|
|
136
|
-
### `
|
|
169
|
+
### `syncs_to_dexie` — automatic model streaming
|
|
137
170
|
|
|
138
|
-
Add to any ActiveRecord model.
|
|
171
|
+
Add to any ActiveRecord model. Optionally provide the broadcast target.
|
|
139
172
|
|
|
140
173
|
```ruby
|
|
141
174
|
class Message < ApplicationRecord
|
|
142
|
-
# Calls send(:receiver), then broadcasts:
|
|
143
|
-
|
|
175
|
+
# Calls send(:receiver), then broadcasts: DexieChannel.broadcast_to(receiver, ...)
|
|
176
|
+
syncs_to_dexie via: :receiver
|
|
144
177
|
|
|
145
|
-
# String
|
|
146
|
-
|
|
178
|
+
# String = public stream (subscribe via addPublicStream("public"))
|
|
179
|
+
syncs_to_dexie via: "public"
|
|
147
180
|
|
|
148
181
|
# Procs are also supported. If an array is returned, multiple broadcasts are made
|
|
149
|
-
# conversation.users.each { |u|
|
|
150
|
-
|
|
182
|
+
# conversation.users.each { |u| DexieChannel.broadcast_to(u, ...) }
|
|
183
|
+
syncs_to_dexie via: -> { conversation.users }
|
|
151
184
|
end
|
|
152
185
|
```
|
|
153
186
|
|
|
154
|
-
|
|
187
|
+
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)`:
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
const subscription = subscribe(db);
|
|
191
|
+
subscription.addStream(streamIdentifier);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Internally, `syncs_to_dexie` sets up the following ActiveRecord callbacks:
|
|
155
195
|
|
|
156
196
|
| Event | Action |
|
|
157
197
|
|---|---|
|
|
@@ -163,23 +203,21 @@ Internally, `streams_via` sets up the following ActiveRecord callbacks:
|
|
|
163
203
|
|
|
164
204
|
| Option | Default | Description |
|
|
165
205
|
|---|---|---|
|
|
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. |
|
|
206
|
+
| `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
207
|
| `table:` | model's `table_name` | Override the Dexie table name. A Proc is evaluated in the record's context. |
|
|
169
208
|
| `only:` | `[:create, :update, :destroy]` | Limit which events trigger a sync |
|
|
170
209
|
| `with:` | `:as_json_for_dexie` | Method name (Symbol) or Proc for serializing records |
|
|
171
210
|
| `if:` | *(none)* | Symbol (method name) or Proc — only sync when it returns truthy |
|
|
172
211
|
| `unless:` | *(none)* | Symbol (method name) or Proc — skip sync when it returns truthy |
|
|
173
212
|
|
|
174
|
-
You can combine multiple `
|
|
213
|
+
You can combine multiple `syncs_to_dexie` declarations, each with different conditions:
|
|
175
214
|
|
|
176
215
|
```ruby
|
|
177
216
|
class Message < ApplicationRecord
|
|
178
|
-
|
|
179
|
-
|
|
217
|
+
syncs_to_dexie via: -> { sender },
|
|
218
|
+
if: :published?
|
|
180
219
|
|
|
181
|
-
|
|
182
|
-
unless: -> { draft? }
|
|
220
|
+
syncs_to_dexie unless: -> { draft? }
|
|
183
221
|
end
|
|
184
222
|
```
|
|
185
223
|
|
|
@@ -190,23 +228,22 @@ Override `as_json_for_dexie` in your model, or use the `with` option to specify
|
|
|
190
228
|
```ruby
|
|
191
229
|
class Message < ApplicationRecord
|
|
192
230
|
# Using the default as_json_for_dexie override:
|
|
193
|
-
|
|
231
|
+
syncs_to_dexie via: :sender
|
|
194
232
|
|
|
195
233
|
def as_json_for_dexie
|
|
196
234
|
super.merge(room_name: room.name)
|
|
197
235
|
end
|
|
198
236
|
|
|
199
237
|
# Or use a custom serializer method:
|
|
200
|
-
|
|
201
|
-
|
|
238
|
+
syncs_to_dexie via: :admin,
|
|
239
|
+
with: :admin_payload
|
|
202
240
|
|
|
203
241
|
def admin_payload
|
|
204
242
|
attributes.slice("id", "body", "flagged")
|
|
205
243
|
end
|
|
206
244
|
|
|
207
245
|
# Or a Proc:
|
|
208
|
-
|
|
209
|
-
with: -> { { id: id, summary: body.truncate(100) } }
|
|
246
|
+
syncs_to_dexie with: -> { { id: id, summary: body.truncate(100) } }
|
|
210
247
|
end
|
|
211
248
|
```
|
|
212
249
|
|
|
@@ -247,60 +284,6 @@ The JS side replays it as:
|
|
|
247
284
|
dexie.messages.where("room_id").equals(5).add({ id: 1, text: "hello" })
|
|
248
285
|
```
|
|
249
286
|
|
|
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
287
|
## License
|
|
305
288
|
|
|
306
289
|
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,96 @@
|
|
|
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
|
+
class DexieChannel < ActionCable::Channel::Base
|
|
17
|
+
PUBLIC_STREAM_PREFIX = "public:"
|
|
18
|
+
|
|
19
|
+
public :transmit
|
|
20
|
+
|
|
21
|
+
# Open a scoped channel for broadcasting to a specific recipient.
|
|
22
|
+
#
|
|
23
|
+
# A String recipient is a public stream name (namespaced under
|
|
24
|
+
# +public:+); any other recipient targets that recipient's own stream.
|
|
25
|
+
#
|
|
26
|
+
# DexieChannel[current_user].table("notifications").add(notification)
|
|
27
|
+
# DexieChannel["feed"].table("announcements").add(announcement)
|
|
28
|
+
#
|
|
29
|
+
def self.[](recipient)
|
|
30
|
+
target = recipient.is_a?(String) ? "#{PUBLIC_STREAM_PREFIX}#{recipient}" : recipient
|
|
31
|
+
ScopedChannel.new(self, target)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Build a query against a Dexie table, transmitted to all subscribers
|
|
35
|
+
# of this channel.
|
|
36
|
+
#
|
|
37
|
+
# table("messages").where(:room_id).equals(room.id).add(message)
|
|
38
|
+
#
|
|
39
|
+
def table(name)
|
|
40
|
+
Query.new(self, name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns a signed token for the stream identifier of +target+ (private
|
|
44
|
+
# record-based streams). Send it to the client, which passes it to
|
|
45
|
+
# +subscription.addStream+.
|
|
46
|
+
def self.stream_token_for(target)
|
|
47
|
+
verifier.generate(broadcasting_for(target))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.verifier
|
|
51
|
+
@verifier ||= Rails.application.message_verifier("dexiecable:streams")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def subscribed
|
|
55
|
+
# Streams are added dynamically via +add_stream+.
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def add_stream(data)
|
|
59
|
+
stream = verified_stream(data["stream"])
|
|
60
|
+
stream_from stream if stream
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def remove_stream(data)
|
|
64
|
+
stream = verified_stream(data["stream"])
|
|
65
|
+
stop_stream_from stream if stream
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def remove_all_streams(_data)
|
|
69
|
+
stop_all_streams
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def add_public_stream(data)
|
|
73
|
+
name = data["stream"].to_s
|
|
74
|
+
stream_from public_stream_name(name) if name.present?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def remove_public_stream(data)
|
|
78
|
+
name = data["stream"].to_s
|
|
79
|
+
stop_stream_from public_stream_name(name) if name.present?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def public_stream_name(name)
|
|
85
|
+
self.class.broadcasting_for("#{PUBLIC_STREAM_PREFIX}#{name}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def verified_stream(token)
|
|
89
|
+
return unless token.present?
|
|
90
|
+
|
|
91
|
+
self.class.verifier.verify(token)
|
|
92
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
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.alpha1
|
|
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
|