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 +7 -0
- data/README.md +203 -0
- data/lib/dexiecable/active_record_ext.rb +83 -0
- data/lib/dexiecable/concern.rb +26 -0
- data/lib/dexiecable/query.rb +37 -0
- data/lib/dexiecable/railtie.rb +11 -0
- data/lib/dexiecable/scoped_channel.rb +18 -0
- data/lib/dexiecable/version.rb +5 -0
- data/lib/dexiecable.rb +12 -0
- metadata +81 -0
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,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
|
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: []
|