dexiecable 2.0.0 → 2.0.2

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