dexiecable 0.1.7 → 0.1.8

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: 2bc64f029d73e3630a14e254852ea7e6b3cf2b00d369f144c0b40ebdd6d4de1f
4
+ data.tar.gz: ea3df61fd1319c5f8fcc8697e99a7954213b5c4f97748ae55e3e012e00f156ac
5
5
  SHA512:
6
- metadata.gz: 62b8fbad4ae8392ec0da800697247833a62cdd1dd8165f95ed05c6b81e4b97a4984e75eb3179481139e9bf8f2378641eecb50e2e7ba5503c94f445a9e30e7128
7
- data.tar.gz: 17330cb1f871afb2b9599596e2d8f1cd882fe323c686f9464490b779e1f2fdfbb4c9b6a9bacca0bd2eb5751ff4001a93cea4f0cbb9dbacae4ab1a1d6489ac2a0
6
+ metadata.gz: f44765e378f1860f5a239cc58169c724bc164c9eada457241a7e70f8cfcbe8ef898e2357d0dfa6981f1a6b3b170f9852a5afdb06e0dee935f7deea0489e568c2
7
+ data.tar.gz: f1227ada19e9a7ccb9825bb9665ec8f7c57be8afc4b05e9f8dd35c07d8da46f92cfb9cebdb74d5b7e5ef3fe87e3a549e14f039ba4412e4f373706e6e4535a318
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
 
@@ -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
 
@@ -145,9 +153,9 @@ Internally, `syncs_to_dexie` sets up the following ActiveRecord callbacks:
145
153
 
146
154
  | Event | Action |
147
155
  |---|---|
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)` |
156
+ | `after_commit on: :create` | `channel.table(table).add(as_json_for_dexie)` |
157
+ | `after_commit on: :update` | `channel.table(table).update(id, as_json_for_dexie.slice(*saved_changes.keys))` |
158
+ | `after_commit on: :destroy` | `channel.table(table).delete(id)` |
151
159
 
152
160
  #### Options
153
161
 
@@ -157,6 +165,7 @@ Internally, `syncs_to_dexie` sets up the following ActiveRecord callbacks:
157
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. |
158
166
  | `table:` | model's `table_name` | Override the Dexie table name. A Proc is evaluated in the record's context. |
159
167
  | `only:` | `[:create, :update, :destroy]` | Limit which events trigger a sync |
168
+ | `with:` | `:as_json_for_dexie` | Method name (Symbol) or Proc for serializing records |
160
169
  | `if:` | *(none)* | Symbol (method name) or Proc — only sync when it returns truthy |
161
170
  | `unless:` | *(none)* | Symbol (method name) or Proc — skip sync when it returns truthy |
162
171
 
@@ -174,15 +183,28 @@ end
174
183
 
175
184
  #### Customizing the synced payload
176
185
 
177
- Override `as_json_for_dexie` in your model:
186
+ Override `as_json_for_dexie` in your model, or use the `with` option to specify a different method or Proc:
178
187
 
179
188
  ```ruby
180
189
  class Message < ApplicationRecord
190
+ # Using the default as_json_for_dexie override:
181
191
  syncs_to_dexie via: UserChannel, to: :sender
182
192
 
183
193
  def as_json_for_dexie
184
194
  super.merge(room_name: room.name)
185
195
  end
196
+
197
+ # Or use a custom serializer method:
198
+ syncs_to_dexie via: AdminChannel, to: :admin,
199
+ with: :admin_payload
200
+
201
+ def admin_payload
202
+ attributes.slice("id", "body", "flagged")
203
+ end
204
+
205
+ # Or a Proc:
206
+ syncs_to_dexie via: PublicChannel,
207
+ with: -> { { id: id, summary: body.truncate(100) } }
186
208
  end
187
209
  ```
188
210
 
@@ -223,6 +245,60 @@ The JS side replays it as:
223
245
  dexie.messages.where("room_id").equals(5).add({ id: 1, text: "hello" })
224
246
  ```
225
247
 
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. 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
+ You might want to have look at the [Sequenced](https://github.com/derrickreimer/sequenced) gem to automatically add sequence IDs to your records.
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
+
226
302
  ## License
227
303
 
228
304
  MIT
@@ -30,16 +30,19 @@ module DexieCable
30
30
  # evaluated in the record's context.
31
31
  # @param only [Array<Symbol>] Limit which lifecycle events sync.
32
32
  # Default: [:create, :update, :destroy].
33
+ # @param with [Symbol, Proc] Method name or proc to use for
34
+ # serializing records (defaults to :as_json_for_dexie).
33
35
  # @param if [Symbol, Proc] Only sync if the given method or proc
34
36
  # returns truthy (evaluated in the record's context).
35
37
  # @param unless [Symbol, Proc] Skip sync if the given method or proc
36
38
  # returns truthy (evaluated in the record's context).
37
- def syncs_to_dexie(via:, to: nil, table: nil, only: nil, **options)
39
+ def syncs_to_dexie(via:, to: nil, table: nil, only: nil, with: nil, **options)
38
40
  events = Array(only || %i[create update destroy])
39
41
  conditions = options.slice(:if, :unless)
42
+ serializer = with || :as_json_for_dexie
40
43
 
41
44
  @dexie_sync_configs ||= []
42
- @dexie_sync_configs << { via: via, to: to, table: table, only: events, **conditions }
45
+ @dexie_sync_configs << { via: via, to: to, table: table, only: events, with: serializer, **conditions }
43
46
 
44
47
  if events.include?(:destroy)
45
48
  before_destroy :dexie_sync_before_destroy
@@ -56,7 +59,7 @@ module DexieCable
56
59
  after_commit on: :create, **conditions do
57
60
  Array(resolve_channel(via, to)).each do |channel|
58
61
  next unless channel
59
- channel.table(resolve_table(table)).add(as_json_for_dexie)
62
+ channel.table(resolve_table(table)).add(serialize_record(serializer))
60
63
  end
61
64
  end
62
65
  end
@@ -66,7 +69,7 @@ module DexieCable
66
69
  Array(resolve_channel(via, to)).each do |channel|
67
70
  next unless channel
68
71
 
69
- changes = as_json_for_dexie.slice(*saved_changes.keys)
72
+ changes = serialize_record(serializer).slice(*saved_changes.keys)
70
73
  channel.table(resolve_table(table)).update(id, changes)
71
74
  end
72
75
  end
@@ -101,6 +104,14 @@ module DexieCable
101
104
  end
102
105
  end
103
106
 
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
+
104
115
  # Override in your model to customise the payload synced to Dexie.
105
116
  def as_json_for_dexie
106
117
  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.8"
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.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Buhrmester