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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b462191000153b424593099689d503a34152404f08cd33911f8db01a61d154cf
4
- data.tar.gz: 3fd86cd2c4b9cb4a73b43fc0fee7e686a91a9c4d1a1e390cc544d87f0dddbdc0
3
+ metadata.gz: 714562cd7c933eb48e81e9159acd326d77b6133a807cdd541000f350281c1c94
4
+ data.tar.gz: d3bcacdc18d5f9c7a8be970f9ede859be19e282e8ca35aa4d27c81e581310eb3
5
5
  SHA512:
6
- metadata.gz: 933c9999fa8d60d04fb3571a094568f1fe78ace8f3a13de6e48dd18d71471e647a1b33c6eb49e2ec6123321933967a8479a1fb6c10906412b5a518302b141f66
7
- data.tar.gz: 13751b561bb8f7d57d05ba7639b474c15d3077262a3ec488a8f217f1d5c20be4235757172eac5fa1b5e558af50b5c97d6d253d5a04544c60c80a768f44ab3049
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
- > The current version of DexieCable is NOT a local-first solution, because it lacks the capability to automatically sync updates back to the server. For now, think of it more as an alternative to turbo streams that works well with component frameworks.
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 2.0.
6
+ > Full synchronization utilizing event streams will arrive in DexieCable 3.0.
7
7
 
8
- DexieCable augments ActionCable channels 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 [`streams_via`](#streams_via--automatic-model-syncing) ActiveRecord macro for automatic change syncing.
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
- You can run any Dexie table update directly inside a channel:
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
- UserChannel[current_user].table("notifications").add(notification)
16
+ DexieChannel[current_user].table("notifications").add(notification)
31
17
  end
32
18
  end
33
19
  ```
34
20
 
35
- An even more convenient way is to use the `streams_via` macro (more info [below](#streams_via--automatic-model-syncing))
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
- streams_via UserChannel, to: :user
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 `streams_via`.
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, "UserChannel");
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
- ### `include DexieCable` in a channel
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
- class UserChannel < ApplicationCable::Channel
90
- include DexieCable
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
- def subscribed
93
- stream_for current_user
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
- This gives you:
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
- | Method | Description |
101
- |---|---|
102
- | `self.[](to)` | Returns a `ScopedChannel` bound to a recipient. `UserChannel[current_user]` |
103
- | `table(name)` | Starts a query chain. `table("messages")` |
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
- UserChannel[current_user].table("messages").add(id: 1, text: "hello")
162
+ DexieChannel[current_user].table("messages").add(id: 1, text: "hello")
112
163
 
113
164
  # Bulk insert
114
- UserChannel[current_user].table("messages").bulkAdd(messages)
165
+ DexieChannel[current_user].table("messages").bulkAdd(messages)
115
166
 
116
167
  # Update (using modify)
117
- UserChannel[current_user]
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
- UserChannel[current_user]
174
+ DexieChannel[current_user]
124
175
  .table("messages")
125
176
  .update(msg.id, text: "updated text")
126
177
 
127
178
  # Delete
128
- UserChannel[current_user]
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
- ### `streams_via` — automatic model streaming
187
+ ### `syncs_to_dexie` — automatic model streaming
137
188
 
138
- Add to any ActiveRecord model. Just provide the channel class and, optionally, the broadcast target.
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: UserChannel.broadcast_to(receiver, ...)
143
- streams_via UserChannel, to: :receiver
193
+ # Calls send(:receiver), then broadcasts: DexieChannel.broadcast_to(receiver, ...)
194
+ syncs_to_dexie via: :receiver
144
195
 
145
- # String used directly: RoomChannel.broadcast_to("public", ...)
146
- streams_via RoomChannel, to: "public"
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| UserChannel.broadcast_to(u, ...) }
150
- streams_via UserChannel, to: -> { conversation.users }
200
+ # conversation.users.each { |u| DexieChannel.broadcast_to(u, ...) }
201
+ syncs_to_dexie via: -> { conversation.users }
151
202
  end
152
203
  ```
153
204
 
154
- Internally, `streams_via` sets up the following ActiveRecord callbacks:
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
- | *(first argument)* | *(required)* | A DexieCable channel class |
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 `streams_via` declarations, each with different conditions:
231
+ You can combine multiple `syncs_to_dexie` declarations, each with different conditions:
175
232
 
176
233
  ```ruby
177
234
  class Message < ApplicationRecord
178
- streams_via UserChannel, to: -> { sender },
179
- if: :published?
235
+ syncs_to_dexie via: -> { sender },
236
+ if: :published?
180
237
 
181
- streams_via AdminChannel,
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
- streams_via UserChannel, to: :sender
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
- streams_via AdminChannel, to: :admin,
201
- with: :admin_payload
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
- streams_via PublicChannel,
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 a
9
- # DexieCable channel.
8
+ # Declares that this model syncs changes to Dexie (IndexedDB) via
9
+ # DexieChannel.
10
10
  #
11
11
  # class Message < ApplicationRecord
12
- # streams_via UserChannel, to: :sender
13
- # streams_via UserChannel, to: "global_feed"
14
- # streams_via UserChannel, to: -> { conversation.users }
15
- # streams_via PublicChannel
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 [Class] A DexieCable channel class, passed as the
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
- # used directly as the stream name for +broadcast_to+.
25
- # Must return a single recipient or collection of
26
- # recipients. Defaults to the record itself.
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 streams_via(via, to: nil, table: nil, only: nil, with: nil, **options)
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, to: to, table: table, only: events, with: serializer, **conditions }
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, to).each do |channel|
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, to).each do |channel|
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, to).each do |channel|
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, to = nil)
82
- recipients = to ? resolve(to) : self
83
- Array(recipients).map { |r| via[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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DexieCable
4
- VERSION = "1.0.0"
4
+ VERSION = "2.0.0.alpha2"
5
5
  end
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: 1.0.0
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 (streams_via) for automatic change
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/concern.rb
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
@@ -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