dexiecable 0.2.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 +96 -111
- data/lib/dexiecable/active_record_ext.rb +25 -42
- 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,40 +1,28 @@
|
|
|
1
1
|
# DexieCable
|
|
2
2
|
|
|
3
3
|
> [!NOTE]
|
|
4
|
-
> DexieCable is NOT a local-first solution
|
|
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
|
+
>
|
|
6
|
+
> Full synchronization utilizing event streams will arrive in DexieCable 3.0.
|
|
5
7
|
|
|
6
|
-
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.
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
```ruby
|
|
11
|
-
class UserChannel < ApplicationCable::Channel
|
|
12
|
-
include DexieCable
|
|
13
|
-
|
|
14
|
-
def subscribed
|
|
15
|
-
stream_for current_user
|
|
16
|
-
recent_notifications = current_user.notifications.last(10)
|
|
17
|
-
table("notifications").bulkAdd(recent_notifications)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Or from inside a controller:
|
|
10
|
+
Push Dexie table updates to a client from anywhere on the server:
|
|
23
11
|
|
|
24
12
|
```ruby
|
|
25
13
|
class NotificationsController < ApplicationController
|
|
26
14
|
def create
|
|
27
15
|
notification = current_user.notifications.create!(notification_params)
|
|
28
|
-
|
|
16
|
+
DexieChannel[current_user].table("notifications").add(notification)
|
|
29
17
|
end
|
|
30
18
|
end
|
|
31
19
|
```
|
|
32
20
|
|
|
33
|
-
|
|
21
|
+
Or sync model changes automatically with the `syncs_to_dexie` macro (more info [below](#syncs_to_dexie--automatic-model-syncing))
|
|
34
22
|
|
|
35
23
|
```ruby
|
|
36
24
|
class Notification < ApplicationRecord
|
|
37
|
-
|
|
25
|
+
syncs_to_dexie via: :user
|
|
38
26
|
end
|
|
39
27
|
```
|
|
40
28
|
|
|
@@ -48,7 +36,7 @@ Add to your `Gemfile`:
|
|
|
48
36
|
gem "dexiecable"
|
|
49
37
|
```
|
|
50
38
|
|
|
51
|
-
Then `bundle install`. The Railtie automatically extends `ActiveRecord::Base` with `
|
|
39
|
+
Then `bundle install`. The Railtie automatically extends `ActiveRecord::Base` with `syncs_to_dexie`.
|
|
52
40
|
|
|
53
41
|
### npm package
|
|
54
42
|
|
|
@@ -64,7 +52,9 @@ Pass your Dexie database as the first argument to `subscribe()`:
|
|
|
64
52
|
import { subscribe } from "dexiecable";
|
|
65
53
|
import { db } from "./db";
|
|
66
54
|
|
|
67
|
-
subscribe(db
|
|
55
|
+
const subscription = subscribe(db);
|
|
56
|
+
// Stream tokens come from the server: DexieChannel.stream_token_for(target)
|
|
57
|
+
subscription.addStream(streamToken);
|
|
68
58
|
```
|
|
69
59
|
|
|
70
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()`:
|
|
@@ -81,24 +71,69 @@ setConsumer(createConsumer("wss://example.com/cable"));
|
|
|
81
71
|
|
|
82
72
|
## Usage
|
|
83
73
|
|
|
84
|
-
###
|
|
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:
|
|
85
79
|
|
|
86
80
|
```ruby
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
DexieChannel.stream_token_for(target)
|
|
82
|
+
```
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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"
|
|
93
120
|
end
|
|
94
121
|
```
|
|
95
122
|
|
|
96
|
-
|
|
123
|
+
Then subscribe by name — no token required:
|
|
97
124
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
+
```
|
|
102
137
|
|
|
103
138
|
### Chaining Dexie operations
|
|
104
139
|
|
|
@@ -106,24 +141,24 @@ Any Dexie.js write operation triggers an immediate broadcast:
|
|
|
106
141
|
|
|
107
142
|
```ruby
|
|
108
143
|
# Single insert
|
|
109
|
-
|
|
144
|
+
DexieChannel[current_user].table("messages").add(id: 1, text: "hello")
|
|
110
145
|
|
|
111
146
|
# Bulk insert
|
|
112
|
-
|
|
147
|
+
DexieChannel[current_user].table("messages").bulkAdd(messages)
|
|
113
148
|
|
|
114
149
|
# Update (using modify)
|
|
115
|
-
|
|
150
|
+
DexieChannel[current_user]
|
|
116
151
|
.table("messages")
|
|
117
152
|
.where(:id).equals(msg.id)
|
|
118
153
|
.modify(read: true)
|
|
119
154
|
|
|
120
155
|
# Update (using update)
|
|
121
|
-
|
|
156
|
+
DexieChannel[current_user]
|
|
122
157
|
.table("messages")
|
|
123
158
|
.update(msg.id, text: "updated text")
|
|
124
159
|
|
|
125
160
|
# Delete
|
|
126
|
-
|
|
161
|
+
DexieChannel[current_user]
|
|
127
162
|
.table("messages")
|
|
128
163
|
.where(:room_id).equals(room.id)
|
|
129
164
|
.delete()
|
|
@@ -131,25 +166,32 @@ UserChannel[current_user]
|
|
|
131
166
|
|
|
132
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.
|
|
133
168
|
|
|
134
|
-
### `
|
|
169
|
+
### `syncs_to_dexie` — automatic model streaming
|
|
135
170
|
|
|
136
|
-
Add to any ActiveRecord model.
|
|
171
|
+
Add to any ActiveRecord model. Optionally provide the broadcast target.
|
|
137
172
|
|
|
138
173
|
```ruby
|
|
139
174
|
class Message < ApplicationRecord
|
|
140
|
-
# Calls send(:receiver), then broadcasts:
|
|
141
|
-
|
|
175
|
+
# Calls send(:receiver), then broadcasts: DexieChannel.broadcast_to(receiver, ...)
|
|
176
|
+
syncs_to_dexie via: :receiver
|
|
142
177
|
|
|
143
|
-
# String
|
|
144
|
-
|
|
178
|
+
# String = public stream (subscribe via addPublicStream("public"))
|
|
179
|
+
syncs_to_dexie via: "public"
|
|
145
180
|
|
|
146
181
|
# Procs are also supported. If an array is returned, multiple broadcasts are made
|
|
147
|
-
# conversation.users.each { |u|
|
|
148
|
-
|
|
182
|
+
# conversation.users.each { |u| DexieChannel.broadcast_to(u, ...) }
|
|
183
|
+
syncs_to_dexie via: -> { conversation.users }
|
|
149
184
|
end
|
|
150
185
|
```
|
|
151
186
|
|
|
152
|
-
|
|
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:
|
|
153
195
|
|
|
154
196
|
| Event | Action |
|
|
155
197
|
|---|---|
|
|
@@ -161,23 +203,21 @@ Internally, `streams_to_dexie` sets up the following ActiveRecord callbacks:
|
|
|
161
203
|
|
|
162
204
|
| Option | Default | Description |
|
|
163
205
|
|---|---|---|
|
|
164
|
-
| `via:` |
|
|
165
|
-
| `to:` | *(none)* | 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. |
|
|
166
207
|
| `table:` | model's `table_name` | Override the Dexie table name. A Proc is evaluated in the record's context. |
|
|
167
208
|
| `only:` | `[:create, :update, :destroy]` | Limit which events trigger a sync |
|
|
168
209
|
| `with:` | `:as_json_for_dexie` | Method name (Symbol) or Proc for serializing records |
|
|
169
210
|
| `if:` | *(none)* | Symbol (method name) or Proc — only sync when it returns truthy |
|
|
170
211
|
| `unless:` | *(none)* | Symbol (method name) or Proc — skip sync when it returns truthy |
|
|
171
212
|
|
|
172
|
-
You can combine multiple `
|
|
213
|
+
You can combine multiple `syncs_to_dexie` declarations, each with different conditions:
|
|
173
214
|
|
|
174
215
|
```ruby
|
|
175
216
|
class Message < ApplicationRecord
|
|
176
|
-
|
|
217
|
+
syncs_to_dexie via: -> { sender },
|
|
177
218
|
if: :published?
|
|
178
219
|
|
|
179
|
-
|
|
180
|
-
unless: -> { draft? }
|
|
220
|
+
syncs_to_dexie unless: -> { draft? }
|
|
181
221
|
end
|
|
182
222
|
```
|
|
183
223
|
|
|
@@ -188,14 +228,14 @@ Override `as_json_for_dexie` in your model, or use the `with` option to specify
|
|
|
188
228
|
```ruby
|
|
189
229
|
class Message < ApplicationRecord
|
|
190
230
|
# Using the default as_json_for_dexie override:
|
|
191
|
-
|
|
231
|
+
syncs_to_dexie via: :sender
|
|
192
232
|
|
|
193
233
|
def as_json_for_dexie
|
|
194
234
|
super.merge(room_name: room.name)
|
|
195
235
|
end
|
|
196
236
|
|
|
197
237
|
# Or use a custom serializer method:
|
|
198
|
-
|
|
238
|
+
syncs_to_dexie via: :admin,
|
|
199
239
|
with: :admin_payload
|
|
200
240
|
|
|
201
241
|
def admin_payload
|
|
@@ -203,8 +243,7 @@ class Message < ApplicationRecord
|
|
|
203
243
|
end
|
|
204
244
|
|
|
205
245
|
# Or a Proc:
|
|
206
|
-
|
|
207
|
-
with: -> { { id: id, summary: body.truncate(100) } }
|
|
246
|
+
syncs_to_dexie with: -> { { id: id, summary: body.truncate(100) } }
|
|
208
247
|
end
|
|
209
248
|
```
|
|
210
249
|
|
|
@@ -245,60 +284,6 @@ The JS side replays it as:
|
|
|
245
284
|
dexie.messages.where("room_id").equals(5).add({ id: 1, text: "hello" })
|
|
246
285
|
```
|
|
247
286
|
|
|
248
|
-
## Recipies
|
|
249
|
-
|
|
250
|
-
### Use sequence IDs to avoid data loss
|
|
251
|
-
|
|
252
|
-
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.
|
|
253
|
-
|
|
254
|
-
To enable this, DexieCable ships its own version of the ActionCable client with one key
|
|
255
|
-
extension: **channel params can be functions**. When a param value is a
|
|
256
|
-
function, it is called and awaited at subscribe time — use this to submit the latest known sequence ID on connection:
|
|
257
|
-
|
|
258
|
-
```js
|
|
259
|
-
import { subscribe } from "dexiecable";
|
|
260
|
-
import { db, getLastSeqId } from './database';
|
|
261
|
-
|
|
262
|
-
const roomId = 123;
|
|
263
|
-
|
|
264
|
-
subscribe(db, {
|
|
265
|
-
channel: "RoomChannel",
|
|
266
|
-
room_id: roomId,
|
|
267
|
-
seq_id: () => getLastSeqId(roomId) // evaluated fresh on each reconnect
|
|
268
|
-
});
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
Send missed messages on reconnection:
|
|
272
|
-
|
|
273
|
-
```ruby
|
|
274
|
-
class RoomChannel < ApplicationChannel:Base
|
|
275
|
-
def subscribed
|
|
276
|
-
stream_from "room:#{params[:room_id]}"
|
|
277
|
-
missed_messages = room.messages.where("seq_id > ?", params[:seq_id])
|
|
278
|
-
table("messages").bulkAdd(missed_messages)
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
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.
|
|
284
|
-
|
|
285
|
-
### Multi-user environments
|
|
286
|
-
|
|
287
|
-
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.
|
|
288
|
-
|
|
289
|
-
```js
|
|
290
|
-
|
|
291
|
-
import Dexie from 'dexie'
|
|
292
|
-
import { subscribe } from 'dexiecable'
|
|
293
|
-
|
|
294
|
-
const userDB = new Dexie("user_"+userId)
|
|
295
|
-
const sharedDB = new Dexie("shared")
|
|
296
|
-
|
|
297
|
-
subscribe(userDB, 'UserChannel')
|
|
298
|
-
subscribe(sharedDB, 'PublicChannel')
|
|
299
|
-
|
|
300
|
-
```
|
|
301
|
-
|
|
302
287
|
## License
|
|
303
288
|
|
|
304
289
|
MIT
|
|
@@ -5,26 +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
|
-
# is given, each recipient is mapped through
|
|
20
|
-
# +via[to]+ to produce scoped channels.
|
|
21
|
-
# Without +to+, +via+ is used directly as an
|
|
22
|
-
# unscoped channel.
|
|
23
|
-
# @param to [Proc, Symbol, String] A Proc evaluated in record
|
|
18
|
+
# @param via [Proc, Symbol, String] A Proc evaluated in record
|
|
24
19
|
# context, a Symbol to call via +send+, or a String
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
20
|
+
# naming a public stream. Must return a single
|
|
21
|
+
# recipient or collection of recipients. Defaults to
|
|
22
|
+
# the record itself.
|
|
28
23
|
# @param table [String, Symbol, Proc] Override the Dexie table name
|
|
29
24
|
# (defaults to the model's table_name). A Proc is
|
|
30
25
|
# evaluated in the record's context.
|
|
@@ -36,19 +31,19 @@ module DexieCable
|
|
|
36
31
|
# returns truthy (evaluated in the record's context).
|
|
37
32
|
# @param unless [Symbol, Proc] Skip sync if the given method or proc
|
|
38
33
|
# returns truthy (evaluated in the record's context).
|
|
39
|
-
def
|
|
34
|
+
def syncs_to_dexie(via: nil, table: nil, only: nil, with: nil, **options)
|
|
40
35
|
events = Array(only || %i[create update destroy])
|
|
41
36
|
conditions = options.slice(:if, :unless)
|
|
42
37
|
serializer = with || :as_json_for_dexie
|
|
43
38
|
|
|
44
39
|
@dexie_sync_configs ||= []
|
|
45
|
-
@dexie_sync_configs << { via: via,
|
|
40
|
+
@dexie_sync_configs << { via: via, table: table, only: events, with: serializer, **conditions }
|
|
46
41
|
|
|
47
42
|
if events.include?(:destroy)
|
|
48
43
|
before_destroy :dexie_sync_before_destroy
|
|
49
44
|
|
|
50
45
|
after_commit on: :destroy, **conditions do
|
|
51
|
-
|
|
46
|
+
resolve_channels(via).each do |channel|
|
|
52
47
|
next unless channel
|
|
53
48
|
channel.table(resolve_table(table)).delete(dexie_destroy_id)
|
|
54
49
|
end
|
|
@@ -57,19 +52,19 @@ module DexieCable
|
|
|
57
52
|
|
|
58
53
|
if events.include?(:create)
|
|
59
54
|
after_commit on: :create, **conditions do
|
|
60
|
-
|
|
55
|
+
resolve_channels(via).each do |channel|
|
|
61
56
|
next unless channel
|
|
62
|
-
channel.table(resolve_table(table)).add(
|
|
57
|
+
channel.table(resolve_table(table)).add(resolve(serializer))
|
|
63
58
|
end
|
|
64
59
|
end
|
|
65
60
|
end
|
|
66
61
|
|
|
67
62
|
if events.include?(:update)
|
|
68
63
|
after_commit on: :update, **conditions do
|
|
69
|
-
|
|
64
|
+
resolve_channels(via).each do |channel|
|
|
70
65
|
next unless channel
|
|
71
66
|
|
|
72
|
-
changes =
|
|
67
|
+
changes = resolve(serializer).slice(*saved_changes.keys)
|
|
73
68
|
channel.table(resolve_table(table)).update(id, changes)
|
|
74
69
|
end
|
|
75
70
|
end
|
|
@@ -79,20 +74,16 @@ module DexieCable
|
|
|
79
74
|
|
|
80
75
|
private
|
|
81
76
|
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Array(recipients).map { |r| via[r] }
|
|
86
|
-
else
|
|
87
|
-
via
|
|
88
|
-
end
|
|
77
|
+
def resolve_channels(via = nil)
|
|
78
|
+
recipients = via ? resolve(via) : self
|
|
79
|
+
Array(recipients).map { |r| DexieChannel[r] }
|
|
89
80
|
end
|
|
90
81
|
|
|
91
|
-
def
|
|
92
|
-
case
|
|
93
|
-
when Proc then instance_exec(&
|
|
94
|
-
when Symbol then send(
|
|
95
|
-
else
|
|
82
|
+
def resolve(val)
|
|
83
|
+
case val
|
|
84
|
+
when Proc then instance_exec(&val)
|
|
85
|
+
when Symbol then send(val)
|
|
86
|
+
else val
|
|
96
87
|
end
|
|
97
88
|
end
|
|
98
89
|
|
|
@@ -104,14 +95,6 @@ module DexieCable
|
|
|
104
95
|
end
|
|
105
96
|
end
|
|
106
97
|
|
|
107
|
-
def serialize_record(serializer)
|
|
108
|
-
case serializer
|
|
109
|
-
when Proc then instance_exec(&serializer)
|
|
110
|
-
when Symbol then send(serializer)
|
|
111
|
-
else serializer
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
98
|
# Override in your model to customise the payload synced to Dexie.
|
|
116
99
|
def as_json_for_dexie
|
|
117
100
|
as_json
|
|
@@ -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: 0.
|
|
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
|