dexiecable 0.1.7 → 0.1.9

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: 85944f789c9d0a2a10d718fbab783e4c9b89e00bb2ad0fe2cb5804648660447c
4
- data.tar.gz: a17bc5ccd07702c90ea508b9261391980fbfa68e4bf1a5d1b9dc4f85f6a8c417
3
+ metadata.gz: 390eddcb71f97e5e8755696c49a8ce2417e7f8636e0e43be8742acd4f31f4b59
4
+ data.tar.gz: 2058ae8d76759483bc047b4e74622154335eb55fa2124052f518f51d14539a0d
5
5
  SHA512:
6
- metadata.gz: 62b8fbad4ae8392ec0da800697247833a62cdd1dd8165f95ed05c6b81e4b97a4984e75eb3179481139e9bf8f2378641eecb50e2e7ba5503c94f445a9e30e7128
7
- data.tar.gz: 17330cb1f871afb2b9599596e2d8f1cd882fe323c686f9464490b779e1f2fdfbb4c9b6a9bacca0bd2eb5751ff4001a93cea4f0cbb9dbacae4ab1a1d6489ac2a0
6
+ metadata.gz: 9057f8f5bdbb694a2673c47773db75d2d99a0f3383043908b51152f3f6279a4aabae9f4436dffeebd31e6882d684ca9f505f7a30e5bf1942de89787199a07589
7
+ data.tar.gz: 39b6503f4ac13bf8abbaae9352ea6b2355bf34f3210dfd2cd99d87413a39a606f389f39236cafca0143aa94621f8bb0be5835fb34c08027cca0d0c2a7ac6feac
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # DexieCable
2
2
 
3
- Run [Dexie.js](https://dexie.org) IndexedDB operations from your Rails ActionCable channels.
3
+ > [!NOTE]
4
+ > DexieCable is NOT a local-first solution, because it lacks the capability to automatically sync updates back to the server (this might be added later). For now, think of it as a real-time local-cache solution. Also, check out this [blog post introducing DexieCable](https://dev.to/buhrmi/real-time-rails-without-turbo-modern-reactive-uis-with-inertia-and-dexiecable-4lge).
4
5
 
5
6
  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 [`syncs_to_dexie`](#syncs_to_dexie--automatic-model-syncing) ActiveRecord macro for automatic change syncing.
6
7
 
@@ -33,7 +34,7 @@ An even more convenient way is to use the `syncs_to_dexie` macro (more info [bel
33
34
 
34
35
  ```ruby
35
36
  class Notification < ApplicationRecord
36
- syncs_to_dexie via: UserChannel, to: :user
37
+ syncs_to_dexie via: -> { UserChannel[user] }
37
38
  end
38
39
  ```
39
40
 
@@ -57,19 +58,26 @@ npm install dexiecable
57
58
  yarn add dexiecable
58
59
  ```
59
60
 
60
- Configure it with your Dexie database and ActionCable consumer before subscribing:
61
+ Pass your Dexie database as the first argument to `subscribe()`:
61
62
 
62
63
  ```js
63
- import { configure, subscribe } from "dexiecable";
64
+ import { subscribe } from "dexiecable";
64
65
  import { db } from "./db";
65
- import { createConsumer } from "@rails/actioncable";
66
66
 
67
- configure({ db, consumer: createConsumer() });
68
-
69
- const sub = subscribe("UserChannel", { last_update: Date.now() });
67
+ subscribe(db, "UserChannel");
70
68
  ```
71
69
 
72
- If you omit `consumer`, one is created for you automatically.
70
+ A consumer is lazily created on the first `subscribe()` call. If you need to access or set the consumer explicitly, use `getConsumer()` and `setConsumer()`:
71
+
72
+ ```js
73
+ import { getConsumer, setConsumer, createConsumer } from "dexiecable";
74
+
75
+ // Get the consumer (creates one lazily if needed)
76
+ const consumer = getConsumer();
77
+
78
+ // Or set a custom one
79
+ setConsumer(createConsumer("wss://example.com/cable"));
80
+ ```
73
81
 
74
82
  ## Usage
75
83
 
@@ -125,7 +133,15 @@ The full query chain is serialized as JSON and sent over ActionCable. The JS cli
125
133
 
126
134
  ### `syncs_to_dexie` — automatic model syncing
127
135
 
128
- Add to any ActiveRecord model. Just provide the channel class and the broadcast target.
136
+ Add to any ActiveRecord model. The simplest approach: pass a Proc to `via` that returns a scoped channel.
137
+
138
+ ```ruby
139
+ class Message < ApplicationRecord
140
+ syncs_to_dexie via: -> { UserChannel[current_user] }
141
+ end
142
+ ```
143
+
144
+ You can also pass a channel class with `to:` to have DexieCable handle the scoping:
129
145
 
130
146
  ```ruby
131
147
  class Message < ApplicationRecord
@@ -145,18 +161,19 @@ Internally, `syncs_to_dexie` sets up the following ActiveRecord callbacks:
145
161
 
146
162
  | Event | Action |
147
163
  |---|---|
148
- | `after_commit on: :create` | `channel.table(table).add(record.as_json_for_dexie)` |
149
- | `after_commit on: :update` | `channel.table(table).put(record.as_json_for_dexie)` |
150
- | `after_commit on: :destroy` | `channel.table(table).delete(record.id)` |
164
+ | `after_commit on: :create` | `channel.table(table).add(as_json_for_dexie)` |
165
+ | `after_commit on: :update` | `channel.table(table).update(id, as_json_for_dexie.slice(*saved_changes.keys))` |
166
+ | `after_commit on: :destroy` | `channel.table(table).delete(id)` |
151
167
 
152
168
  #### Options
153
169
 
154
170
  | Option | Default | Description |
155
171
  |---|---|---|
156
- | `via:` | *(required)* | A DexieCable channel class |
157
- | `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. |
172
+ | `via:` | *(required)* | A DexieCable channel class, or a Proc that returns a channel or scoped channel (e.g. `-> { UserChannel[current_user] }`) |
173
+ | `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. Only used when `via` is a channel class. |
158
174
  | `table:` | model's `table_name` | Override the Dexie table name. A Proc is evaluated in the record's context. |
159
175
  | `only:` | `[:create, :update, :destroy]` | Limit which events trigger a sync |
176
+ | `with:` | `:as_json_for_dexie` | Method name (Symbol) or Proc for serializing records |
160
177
  | `if:` | *(none)* | Symbol (method name) or Proc — only sync when it returns truthy |
161
178
  | `unless:` | *(none)* | Symbol (method name) or Proc — skip sync when it returns truthy |
162
179
 
@@ -164,25 +181,36 @@ You can combine multiple `syncs_to_dexie` declarations, each with different cond
164
181
 
165
182
  ```ruby
166
183
  class Message < ApplicationRecord
167
- syncs_to_dexie via: UserChannel, to: -> { sender },
184
+ syncs_to_dexie via: -> { UserChannel[current_user] }
185
+ syncs_to_dexie via: -> { BoardChannel[board] },
168
186
  if: :published?
169
-
170
- syncs_to_dexie via: AdminChannel,
171
- unless: -> { draft? }
172
187
  end
173
188
  ```
174
189
 
175
190
  #### Customizing the synced payload
176
191
 
177
- Override `as_json_for_dexie` in your model:
192
+ Override `as_json_for_dexie` in your model, or use the `with` option to specify a different method or Proc:
178
193
 
179
194
  ```ruby
180
195
  class Message < ApplicationRecord
181
- syncs_to_dexie via: UserChannel, to: :sender
196
+ # Using the default as_json_for_dexie override:
197
+ syncs_to_dexie via: -> { UserChannel[current_user] }
182
198
 
183
199
  def as_json_for_dexie
184
200
  super.merge(room_name: room.name)
185
201
  end
202
+
203
+ # Or use a custom serializer method:
204
+ syncs_to_dexie via: AdminChannel,
205
+ with: :admin_payload
206
+
207
+ def admin_payload
208
+ attributes.slice("id", "body", "flagged")
209
+ end
210
+
211
+ # Or a Proc:
212
+ syncs_to_dexie via: PublicChannel,
213
+ with: -> { { id: id, summary: body.truncate(100) } }
186
214
  end
187
215
  ```
188
216
 
@@ -223,6 +251,60 @@ The JS side replays it as:
223
251
  dexie.messages.where("room_id").equals(5).add({ id: 1, text: "hello" })
224
252
  ```
225
253
 
254
+ ## Recipies
255
+
256
+ ### Use sequence IDs to avoid data loss
257
+
258
+ A common pattern to avoid data loss during transient disconnections is using sequence IDs to bridge the offline gap. 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.
259
+
260
+ To enable this, DexieCable ships its own version of the ActionCable client with one key
261
+ extension: **channel params can be functions**. When a param value is a
262
+ function, it is called and awaited at subscribe time — use this to submit the latest known sequence ID on connection:
263
+
264
+ ```js
265
+ import { subscribe } from "dexiecable";
266
+ import { db, getLastSeqId } from './database';
267
+
268
+ const roomId = 123;
269
+
270
+ subscribe(db, {
271
+ channel: "RoomChannel",
272
+ room_id: roomId,
273
+ seq_id: () => getLastSeqId(roomId) // evaluated fresh on each reconnect
274
+ });
275
+ ```
276
+
277
+ Send missed messages on reconnection:
278
+
279
+ ```ruby
280
+ class RoomChannel < ApplicationChannel:Base
281
+ def subscribed
282
+ stream_from "room:#{params[:room_id]}"
283
+ missed_messages = room.messages.where("seq_id > ?", params[:seq_id])
284
+ table("messages").bulkAdd(missed_messages)
285
+ end
286
+ end
287
+ ```
288
+
289
+ You might want to have look at the [Sequenced](https://github.com/derrickreimer/sequenced) gem to automatically add sequence IDs to your records.
290
+
291
+ ### Multi-user environments
292
+
293
+ 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.
294
+
295
+ ```js
296
+
297
+ import Dexie from 'dexie'
298
+ import { subscribe } from 'dexiecable'
299
+
300
+ const userDB = new Dexie("user_"+userId)
301
+ const sharedDB = new Dexie("shared")
302
+
303
+ subscribe(userDB, 'UserChannel')
304
+ subscribe(sharedDB, 'PublicChannel')
305
+
306
+ ```
307
+
226
308
  ## License
227
309
 
228
310
  MIT
@@ -9,17 +9,19 @@ module DexieCable
9
9
  # DexieCable channel.
10
10
  #
11
11
  # class Message < ApplicationRecord
12
- # syncs_to_dexie via: UserChannel, to: :sender
13
- # syncs_to_dexie via: UserChannel, to: "global_feed"
14
- # syncs_to_dexie via: UserChannel, to: -> { conversation.users }
12
+ # syncs_to_dexie via: -> { UserChannel[current_user] }
15
13
  # syncs_to_dexie via: PublicChannel
14
+ # syncs_to_dexie via: UserChannel, to: :user
15
+ # syncs_to_dexie via: UserChannel, to: -> { conversation.users }
16
+ # syncs_to_dexie via: -> { tenant_channel }
16
17
  # end
17
18
  #
18
- # @param via [Class] A DexieCable channel class. When +to+
19
+ # @param via [Class, Proc] A DexieCable channel class, or a Proc
20
+ # evaluated in record context that returns a channel
21
+ # class or a scoped channel (e.g.
22
+ # +-> { UserChannel[current_user] }+). When +to+
19
23
  # is given, each recipient is mapped through
20
24
  # +via[to]+ to produce scoped channels.
21
- # Without +to+, +via+ is used directly as an
22
- # unscoped channel.
23
25
  # @param to [Proc, Symbol, String] A Proc evaluated in record
24
26
  # context, a Symbol to call via +send+, or a String
25
27
  # used directly as the stream name for +broadcast_to+.
@@ -30,16 +32,19 @@ module DexieCable
30
32
  # evaluated in the record's context.
31
33
  # @param only [Array<Symbol>] Limit which lifecycle events sync.
32
34
  # Default: [:create, :update, :destroy].
35
+ # @param with [Symbol, Proc] Method name or proc to use for
36
+ # serializing records (defaults to :as_json_for_dexie).
33
37
  # @param if [Symbol, Proc] Only sync if the given method or proc
34
38
  # returns truthy (evaluated in the record's context).
35
39
  # @param unless [Symbol, Proc] Skip sync if the given method or proc
36
40
  # returns truthy (evaluated in the record's context).
37
- def syncs_to_dexie(via:, to: nil, table: nil, only: nil, **options)
41
+ def syncs_to_dexie(via:, to: nil, table: nil, only: nil, with: nil, **options)
38
42
  events = Array(only || %i[create update destroy])
39
43
  conditions = options.slice(:if, :unless)
44
+ serializer = with || :as_json_for_dexie
40
45
 
41
46
  @dexie_sync_configs ||= []
42
- @dexie_sync_configs << { via: via, to: to, table: table, only: events, **conditions }
47
+ @dexie_sync_configs << { via: via, to: to, table: table, only: events, with: serializer, **conditions }
43
48
 
44
49
  if events.include?(:destroy)
45
50
  before_destroy :dexie_sync_before_destroy
@@ -56,7 +61,7 @@ module DexieCable
56
61
  after_commit on: :create, **conditions do
57
62
  Array(resolve_channel(via, to)).each do |channel|
58
63
  next unless channel
59
- channel.table(resolve_table(table)).add(as_json_for_dexie)
64
+ channel.table(resolve_table(table)).add(serialize_record(serializer))
60
65
  end
61
66
  end
62
67
  end
@@ -66,7 +71,7 @@ module DexieCable
66
71
  Array(resolve_channel(via, to)).each do |channel|
67
72
  next unless channel
68
73
 
69
- changes = as_json_for_dexie.slice(*saved_changes.keys)
74
+ changes = serialize_record(serializer).slice(*saved_changes.keys)
70
75
  channel.table(resolve_table(table)).update(id, changes)
71
76
  end
72
77
  end
@@ -77,11 +82,12 @@ module DexieCable
77
82
  private
78
83
 
79
84
  def resolve_channel(via, to = nil)
85
+ channel = via.is_a?(Proc) ? instance_exec(&via) : via
80
86
  if to
81
87
  recipients = resolve_recipient(to)
82
- Array(recipients).map { |r| via[r] }
88
+ Array(recipients).map { |r| channel[r] }
83
89
  else
84
- via
90
+ channel
85
91
  end
86
92
  end
87
93
 
@@ -101,6 +107,14 @@ module DexieCable
101
107
  end
102
108
  end
103
109
 
110
+ def serialize_record(serializer)
111
+ case serializer
112
+ when Proc then instance_exec(&serializer)
113
+ when Symbol then send(serializer)
114
+ else serializer
115
+ end
116
+ end
117
+
104
118
  # Override in your model to customise the payload synced to Dexie.
105
119
  def as_json_for_dexie
106
120
  as_json
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DexieCable
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.9"
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: 0.1.7
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Buhrmester