dexiecable 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 86f0f439d4d0a6320b38ce77c0e188fdd590b592107c8a6c4b41f6d63d72c900
4
+ data.tar.gz: da0d7f54b972206adf4e07bbcec2660a597f4ac89c28865fc2a8974bc21ac38f
5
+ SHA512:
6
+ metadata.gz: 22d044c313789238aaa192415a96d8795111078727745ed9ed1b26ce28d14016be02415ef9903eebcfc75fa6d1a4b0856c8eb4cebbdd7693c3b67984ba6a6dd1
7
+ data.tar.gz: 7fc66fe6550e35e92fc426ae01131bda13b9a83394c763d24b3c19a7a84670c974843ee47385b9c0298cdfa0e3e8c5507115d4565370fdcbc3eb589632898143
data/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # DexieCable
2
+
3
+ Run Dexie.js IndexedDB operations from your Rails ActionCable channels.
4
+
5
+ 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` ActiveRecord macro for automatic change syncing.
6
+
7
+ You can run any Dexie table update directly inside a channel:
8
+
9
+ ```ruby
10
+ class UserChannel < ApplicationCable::Channel
11
+ include DexieCable
12
+
13
+ def subscribed
14
+ stream_for current_user
15
+ recent_notifications = current_user.notifications.last(10)
16
+ table("notifications").bulkAdd(recent_notifications)
17
+ end
18
+ end
19
+ ```
20
+
21
+ Or from inside a controller:
22
+
23
+ ```ruby
24
+ class NotificationsController < ApplicationController
25
+ def create
26
+ notification = current_user.notifications.create!(notification_params)
27
+ UserChannel[current_user].table("notifications").add(notification)
28
+ end
29
+ end
30
+ ```
31
+
32
+ An even more convenient way is to use the `syncs_to_dexie` macro:
33
+
34
+ ```ruby
35
+ class Notification < ApplicationRecord
36
+ syncs_to_dexie via: -> { UserChannel[user] }
37
+ end
38
+ ```
39
+
40
+ ## Installation
41
+
42
+ ### Ruby gem
43
+
44
+ Add to your `Gemfile`:
45
+
46
+ ```ruby
47
+ gem "dexiecable"
48
+ ```
49
+
50
+ Then `bundle install`. The Railtie automatically extends `ActiveRecord::Base` with `syncs_to_dexie`.
51
+
52
+ ### npm package
53
+
54
+ ```bash
55
+ npm install dexiecable
56
+ # or
57
+ yarn add dexiecable
58
+ ```
59
+
60
+ Configure it with your Dexie database and ActionCable consumer before subscribing:
61
+
62
+ ```js
63
+ import { configure, subscribe } from "dexiecable";
64
+ import { db } from "./db";
65
+ import { createConsumer } from "@rails/actioncable";
66
+
67
+ configure({ db, consumer: createConsumer() });
68
+
69
+ const sub = subscribe("UserChannel", { last_update: Date.now() });
70
+ ```
71
+
72
+ If you omit `consumer`, one is created for you automatically.
73
+
74
+ ## Usage
75
+
76
+ ### `include DexieCable` in a channel
77
+
78
+ ```ruby
79
+ class UserChannel < ApplicationCable::Channel
80
+ include DexieCable
81
+
82
+ def subscribed
83
+ stream_for current_user
84
+ end
85
+ end
86
+ ```
87
+
88
+ This gives you:
89
+
90
+ | Method | Description |
91
+ |---|---|
92
+ | `self.[](subject)` | Returns a `ScopedChannel` bound to a subject. `UserChannel[current_user]` |
93
+ | `table(name)` | Starts a query chain. `table("messages")` |
94
+
95
+ ### Chaining Dexie operations
96
+
97
+ Any Dexie.js write operation triggers an immediate broadcast:
98
+
99
+ ```ruby
100
+ # Single insert
101
+ UserChannel[current_user].table("messages").add(id: 1, text: "hello")
102
+
103
+ # Filtered bulk insert
104
+ UserChannel[current_user]
105
+ .table("messages")
106
+ .where(:room_id).equals(room.id)
107
+ .bulkAdd(messages)
108
+
109
+ # Update
110
+ UserChannel[current_user]
111
+ .table("messages")
112
+ .where(:id).equals(msg.id)
113
+ .modify(read: true)
114
+
115
+ # Delete
116
+ UserChannel[current_user]
117
+ .table("messages")
118
+ .where(:room_id).equals(room.id)
119
+ .delete()
120
+ ```
121
+
122
+ 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.
123
+
124
+ ### `syncs_to_dexie` — automatic model syncing
125
+
126
+ Add to any ActiveRecord model:
127
+
128
+ ```ruby
129
+ class Message < ApplicationRecord
130
+ syncs_to_dexie via: -> { UserChannel[sender] }
131
+ syncs_to_dexie via: -> { UserChannel[receiver] }
132
+ syncs_to_dexie via: PublicChannel # broadcast to all connected clients
133
+ end
134
+ ```
135
+
136
+ | Event | Action |
137
+ |---|---|
138
+ | `after_commit on: :create` | `channel.table("messages").add(record.as_json_for_dexie)` |
139
+ | `after_commit on: :update` | `channel.table("messages").put(record.as_json_for_dexie)` |
140
+ | `after_commit on: :destroy` | `channel.table("messages").delete(record.id)` |
141
+
142
+ #### Options
143
+
144
+ | Option | Default | Description |
145
+ |---|---|---|
146
+ | `via:` | *(required)* | Proc (evaluated in record context), channel class, or channel instance |
147
+ | `table:` | model's `table_name` | Override the Dexie table name |
148
+ | `only:` | `[:create, :update, :destroy]` | Limit which events trigger a sync |
149
+
150
+ #### Customizing the synced payload
151
+
152
+ Override `as_json_for_dexie` in your model:
153
+
154
+ ```ruby
155
+ class Message < ApplicationRecord
156
+ syncs_to_dexie via: -> { UserChannel[sender] }
157
+
158
+ def as_json_for_dexie
159
+ super.merge(room_name: room.name)
160
+ end
161
+ end
162
+ ```
163
+
164
+ ## How it works
165
+
166
+ ```mermaid
167
+ sequenceDiagram
168
+ participant Model as ActiveRecord Model
169
+ participant Channel as DexieCable Channel
170
+ participant WS as ActionCable WebSocket
171
+ participant JS as dexiecable.js
172
+ participant DB as Dexie.js (IndexedDB)
173
+
174
+ Model->>Channel: after_commit
175
+ Channel->>Channel: build Query DSL
176
+ Channel->>WS: broadcast JSON { table, ops }
177
+ WS->>JS: received(data)
178
+ JS->>DB: replay ops chain
179
+ DB-->>JS: result
180
+ ```
181
+
182
+ The Ruby side builds a JSON payload like:
183
+
184
+ ```json
185
+ {
186
+ "table": "messages",
187
+ "ops": [
188
+ { "method": "where", "params": ["room_id"] },
189
+ { "method": "equals", "params": [5] },
190
+ { "method": "add", "params": [{ "id": 1, "text": "hello" }] }
191
+ ]
192
+ }
193
+ ```
194
+
195
+ The JS side replays it as:
196
+
197
+ ```js
198
+ dexie.messages.where("room_id").equals(5).add({ id: 1, text: "hello" })
199
+ ```
200
+
201
+ ## License
202
+
203
+ MIT
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DexieCable
4
+ module ActiveRecordExt
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ # Declares that this model syncs changes to Dexie (IndexedDB) via a
9
+ # DexieCable channel.
10
+ #
11
+ # class Message < ApplicationRecord
12
+ # syncs_to_dexie via: -> { UserChannel[sender] }
13
+ # syncs_to_dexie via: -> { UserChannel[receiver] }
14
+ # syncs_to_dexie via: PublicChannel
15
+ # end
16
+ #
17
+ # @param via [Proc, DexieCable] Proc evaluated in record context,
18
+ # must return a channel (responds to +table+). A channel
19
+ # class/instance can also be passed directly. Skipped if nil.
20
+ # @param table [String, Symbol] Override the Dexie table name
21
+ # (defaults to the model's table_name).
22
+ # @param only [Array<Symbol>] Limit which lifecycle events sync.
23
+ # Default: [:create, :update, :destroy].
24
+ def syncs_to_dexie(via:, table: nil, only: nil)
25
+ table_name = (table || self.table_name).to_s
26
+ events = Array(only || %i[create update destroy])
27
+
28
+ @dexie_sync_configs ||= []
29
+ @dexie_sync_configs << { via: via, table: table_name, only: events }
30
+
31
+ if events.include?(:destroy)
32
+ before_destroy :dexie_sync_before_destroy
33
+
34
+ after_commit on: :destroy do
35
+ channel = resolve_channel(via)
36
+ next unless channel
37
+ channel.table(table_name).delete(dexie_destroy_id)
38
+ end
39
+ end
40
+
41
+ if events.include?(:create)
42
+ after_commit on: :create do
43
+ channel = resolve_channel(via)
44
+ next unless channel
45
+ channel.table(table_name).add(as_json_for_dexie)
46
+ end
47
+ end
48
+
49
+ if events.include?(:update)
50
+ after_commit on: :update do
51
+ channel = resolve_channel(via)
52
+ next unless channel
53
+
54
+ changes = as_json_for_dexie.slice(*saved_changes.keys)
55
+ channel.table(table_name).update(id, changes)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def resolve_channel(via)
64
+ case via
65
+ when Proc then instance_exec(&via)
66
+ else via
67
+ end
68
+ end
69
+
70
+ # Override in your model to customise the payload synced to Dexie.
71
+ def as_json_for_dexie
72
+ as_json
73
+ end
74
+
75
+ def dexie_sync_before_destroy
76
+ @dexie_destroy_id = id
77
+ end
78
+
79
+ def dexie_destroy_id
80
+ @dexie_destroy_id
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,26 @@
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 subject.
10
+ #
11
+ # UserChannel[current_user].table("notifications").add(notification)
12
+ #
13
+ def self.[](subject)
14
+ ScopedChannel.new(self, subject)
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
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DexieCable
4
+ # Builds a chain of Dexie.js operations and transmits them as a single
5
+ # payload once a write operation is reached.
6
+ #
7
+ # DexieCable::Query.new(channel, "messages")
8
+ # .where(:room_id).equals(5)
9
+ # .add(text: "hello")
10
+ #
11
+ class Query
12
+ WRITE_OPS = %i[
13
+ add bulkAdd put bulkPut bulkDelete
14
+ upsert update delete modify clear
15
+ ].freeze
16
+
17
+ attr_accessor :query
18
+
19
+ def initialize(transmitter, name)
20
+ @transmitter = transmitter
21
+ @query = { table: name, ops: [] }
22
+ end
23
+
24
+ def method_missing(method, *params)
25
+ @query[:ops] << { method: method, params: params }
26
+ if WRITE_OPS.include?(method)
27
+ @transmitter.transmit(query)
28
+ else
29
+ self
30
+ end
31
+ end
32
+
33
+ def respond_to_missing?(method, *)
34
+ true
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DexieCable
4
+ class Railtie < Rails::Railtie
5
+ initializer "dexiecable.active_record" do
6
+ ActiveSupport.on_load(:active_record) do
7
+ include DexieCable::ActiveRecordExt
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DexieCable
4
+ class ScopedChannel
5
+ def initialize(klass, subject)
6
+ @klass = klass
7
+ @subject = subject
8
+ end
9
+
10
+ def table(name)
11
+ Query.new(self, name)
12
+ end
13
+
14
+ def transmit(data)
15
+ @klass.broadcast_to @subject, data
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DexieCable
4
+ VERSION = "0.1.0"
5
+ end
data/lib/dexiecable.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dexiecable/version"
4
+ require_relative "dexiecable/concern"
5
+ require_relative "dexiecable/scoped_channel"
6
+ require_relative "dexiecable/query"
7
+ require_relative "dexiecable/active_record_ext"
8
+ require_relative "dexiecable/railtie" if defined?(Rails::Railtie)
9
+
10
+ module DexieCable
11
+ class Error < StandardError; end
12
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dexiecable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stefan Buhrmester
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actioncable
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activerecord
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
40
+ description: DexieCable augments ActionCable channels with a query DSL that mirrors
41
+ the Dexie.js API, letting you push database mutations from the server to the client
42
+ in real time. Includes an ActiveRecord macro (syncs_to_dexie) for automatic change
43
+ syncing.
44
+ email:
45
+ - stefan@buhrmi.de
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - README.md
51
+ - lib/dexiecable.rb
52
+ - lib/dexiecable/active_record_ext.rb
53
+ - lib/dexiecable/concern.rb
54
+ - lib/dexiecable/query.rb
55
+ - lib/dexiecable/railtie.rb
56
+ - lib/dexiecable/scoped_channel.rb
57
+ - lib/dexiecable/version.rb
58
+ homepage: https://github.com/buhrmi/dexiecable
59
+ licenses:
60
+ - MIT
61
+ metadata:
62
+ homepage_uri: https://github.com/buhrmi/dexiecable
63
+ source_code_uri: https://github.com/buhrmi/dexiecable
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '3.1'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 4.0.8
79
+ specification_version: 4
80
+ summary: Run Dexie.js IndexedDB operations from your Rails ActionCable channels.
81
+ test_files: []