dexiecable 2.0.0.alpha3 → 2.0.0.alpha5
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 +4 -4
- data/README.md +28 -10
- data/lib/dexiecable/concern.rb +125 -0
- data/lib/dexiecable/version.rb +1 -1
- data/lib/dexiecable.rb +2 -1
- metadata +16 -2
- data/lib/dexiecable/dexie_channel.rb +0 -128
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 16d440593b555940ff8039e04091edf54026d8678183acfb5d56b6189b08560c
|
|
4
|
+
data.tar.gz: 78c9c8e4b7e8e9cd3ae3d8cb7367b040962ef7ea0149d167a20396c58093a938
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d1d15766bb5dde84e8aaac57998da45e3dc88665740c6beb333d12dafddb3115b148b4e3b49ec8badf706d898eaba1c6fd7c8ad33e77b2aa60083b6b6e3b3a88
|
|
7
|
+
data.tar.gz: c716de1ee2d50979513c911b168879da8fc640d49f85ddd288ad70715197691c35ffb9020872a2250ae00a62fb35909214a501f91cdeb458323d31cb201bf511
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
>
|
|
6
6
|
> Full synchronization utilizing event streams will arrive in DexieCable 3.0.
|
|
7
7
|
|
|
8
|
-
DexieCable
|
|
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.
|
|
9
9
|
|
|
10
10
|
Push Dexie table updates to a client from anywhere on the server:
|
|
11
11
|
|
|
@@ -73,7 +73,14 @@ setConsumer(createConsumer("wss://example.com/cable"));
|
|
|
73
73
|
|
|
74
74
|
### DexieChannel
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
Create a `DexieChannel` in your app and `include DexieCable` in it. Every Dexie broadcast goes through this channel:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# app/channels/dexie_channel.rb
|
|
80
|
+
class DexieChannel < ApplicationCable::Channel
|
|
81
|
+
include DexieCable
|
|
82
|
+
end
|
|
83
|
+
```
|
|
77
84
|
|
|
78
85
|
Stream tokens are signed with the application secret, so a client can only subscribe to streams the server has issued for it:
|
|
79
86
|
|
|
@@ -81,11 +88,17 @@ Stream tokens are signed with the application secret, so a client can only subsc
|
|
|
81
88
|
DexieChannel.stream_token_for(target)
|
|
82
89
|
```
|
|
83
90
|
|
|
84
|
-
For an ActiveRecord model it
|
|
91
|
+
For an ActiveRecord model it returns a signed GlobalID (Rails' `signed_id`):
|
|
85
92
|
|
|
86
93
|
```ruby
|
|
87
94
|
DexieChannel.stream_token_for(current_user)
|
|
88
|
-
# => "signed
|
|
95
|
+
# => "signed global id"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Tokens never expire by default. Pass `expires_in:` or `expires_at:` to limit a token's lifetime:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
DexieChannel.stream_token_for(current_user, expires_in: 1.day)
|
|
89
102
|
```
|
|
90
103
|
|
|
91
104
|
Send that token to the client (render it in a view, return it from an endpoint, etc.) and add it to the subscription:
|
|
@@ -107,20 +120,25 @@ subscription.removeStream(userStream);
|
|
|
107
120
|
subscription.removeAllStreams();
|
|
108
121
|
```
|
|
109
122
|
|
|
110
|
-
####
|
|
123
|
+
#### Customizing DexieChannel
|
|
111
124
|
|
|
112
|
-
|
|
125
|
+
Add custom actions or push initial data directly on your channel:
|
|
113
126
|
|
|
114
127
|
```ruby
|
|
115
|
-
#
|
|
116
|
-
class
|
|
117
|
-
|
|
128
|
+
# app/channels/dexie_channel.rb
|
|
129
|
+
class DexieChannel < ApplicationCable::Channel
|
|
130
|
+
include DexieCable
|
|
131
|
+
|
|
132
|
+
# Push a snapshot when a stream is added. `record` is a record for private
|
|
133
|
+
# streams, or the stream name (String) for public streams.
|
|
118
134
|
def subscribed_to(record, params)
|
|
119
135
|
case record
|
|
120
136
|
when User
|
|
121
137
|
table("notifications").bulkAdd(record.notifications.map(&:as_json_for_dexie))
|
|
122
138
|
when Conversation
|
|
123
139
|
table("messages").bulkAdd(record.messages.where("seq_id > ?", params[:last_seq_id]).map(&:as_json_for_dexie))
|
|
140
|
+
when String
|
|
141
|
+
table(record).bulkAdd(Announcement.for_stream(record).map(&:as_json_for_dexie))
|
|
124
142
|
end
|
|
125
143
|
end
|
|
126
144
|
|
|
@@ -131,7 +149,7 @@ class DexieCable::DexieChannel
|
|
|
131
149
|
end
|
|
132
150
|
```
|
|
133
151
|
|
|
134
|
-
The client
|
|
152
|
+
The client subscribes to `DexieChannel` by default:
|
|
135
153
|
|
|
136
154
|
```js
|
|
137
155
|
const subscription = subscribe(db);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DexieCable
|
|
4
|
+
# Include this in an ActionCable channel to add Dexie broadcasting and
|
|
5
|
+
# streaming:
|
|
6
|
+
#
|
|
7
|
+
# class DexieChannel < ApplicationCable::Channel
|
|
8
|
+
# include DexieCable
|
|
9
|
+
#
|
|
10
|
+
# # Push initial data when a private stream is added.
|
|
11
|
+
# def subscribed_to(record, params)
|
|
12
|
+
# case record
|
|
13
|
+
# when User
|
|
14
|
+
# table("notifications").bulkAdd(record.notifications.map(&:as_json_for_dexie))
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# The client subscribes to that channel and adds streams dynamically:
|
|
20
|
+
#
|
|
21
|
+
# subscribe(db) # subscribes to "DexieChannel"
|
|
22
|
+
# subscription.addStream(DexieChannel.stream_token_for(current_user))
|
|
23
|
+
# subscription.addPublicStream("feed")
|
|
24
|
+
#
|
|
25
|
+
extend ActiveSupport::Concern
|
|
26
|
+
|
|
27
|
+
PUBLIC_STREAM_PREFIX = "public:"
|
|
28
|
+
STREAM_TOKEN_PURPOSE = "dexiecable:streams"
|
|
29
|
+
|
|
30
|
+
included do
|
|
31
|
+
public :transmit
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class_methods do
|
|
35
|
+
# Open a scoped channel for broadcasting to a specific recipient.
|
|
36
|
+
#
|
|
37
|
+
# A String recipient is a public stream name (namespaced under
|
|
38
|
+
# +public:+); any other recipient targets that recipient's own stream.
|
|
39
|
+
def [](recipient)
|
|
40
|
+
target = recipient.is_a?(String) ? "#{PUBLIC_STREAM_PREFIX}#{recipient}" : recipient
|
|
41
|
+
ScopedChannel.new(self, target)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns a signed token for +target+ (a record). Send it to the client,
|
|
45
|
+
# which passes it to +subscription.addStream+.
|
|
46
|
+
#
|
|
47
|
+
# Tokens never expire by default. Pass +expires_in:+ (a duration) or
|
|
48
|
+
# +expires_at:+ (a time) to limit a token's lifetime.
|
|
49
|
+
def stream_token_for(target, expires_at: nil, expires_in: nil)
|
|
50
|
+
options = { for: STREAM_TOKEN_PURPOSE }
|
|
51
|
+
|
|
52
|
+
if expires_at
|
|
53
|
+
options[:expires_at] = expires_at
|
|
54
|
+
elsif expires_in
|
|
55
|
+
options[:expires_in] = expires_in
|
|
56
|
+
else
|
|
57
|
+
options[:expires_at] = nil # never expire
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
target.to_sgid_param(**options)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Build a query against a Dexie table, transmitted to all subscribers
|
|
65
|
+
# of this channel.
|
|
66
|
+
def table(name)
|
|
67
|
+
Query.new(self, name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def subscribed
|
|
71
|
+
# Streams are added dynamically via +add_stream+.
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def add_stream(data)
|
|
75
|
+
record = resolve_subscribe_target(data["stream"])
|
|
76
|
+
return unless record
|
|
77
|
+
|
|
78
|
+
stream_for record
|
|
79
|
+
params = data.except("action", "stream").with_indifferent_access
|
|
80
|
+
subscribed_to(record, params)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def remove_stream(data)
|
|
84
|
+
record = resolve_subscribe_target(data["stream"])
|
|
85
|
+
return unless record
|
|
86
|
+
|
|
87
|
+
stop_stream_from self.class.broadcasting_for(record)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def remove_all_streams(_data)
|
|
91
|
+
stop_all_streams
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def add_public_stream(data)
|
|
95
|
+
name = data["stream"].to_s
|
|
96
|
+
return if name.blank?
|
|
97
|
+
|
|
98
|
+
stream_from public_stream_name(name)
|
|
99
|
+
params = data.except("action", "stream").with_indifferent_access
|
|
100
|
+
subscribed_to(name, params)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def remove_public_stream(data)
|
|
104
|
+
name = data["stream"].to_s
|
|
105
|
+
stop_stream_from public_stream_name(name) if name.present?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Override to push initial data when a stream is added. +record+ is the
|
|
109
|
+
# resolved target — a record for private streams, or the stream name for
|
|
110
|
+
# public streams — and +params+ are any extra params sent from the client.
|
|
111
|
+
def subscribed_to(_record, _params)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def public_stream_name(name)
|
|
117
|
+
self.class.broadcasting_for("#{PUBLIC_STREAM_PREFIX}#{name}")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def resolve_subscribe_target(token)
|
|
121
|
+
GlobalID::Locator.locate_signed(token, for: STREAM_TOKEN_PURPOSE)
|
|
122
|
+
rescue ActiveRecord::RecordNotFound, NameError
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
end
|
data/lib/dexiecable/version.rb
CHANGED
data/lib/dexiecable.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "dexiecable/version"
|
|
4
|
+
require "global_id/railtie" if defined?(Rails::Railtie)
|
|
5
|
+
require_relative "dexiecable/concern"
|
|
4
6
|
require_relative "dexiecable/scoped_channel"
|
|
5
7
|
require_relative "dexiecable/query"
|
|
6
8
|
require_relative "dexiecable/active_record_ext"
|
|
7
|
-
require_relative "dexiecable/dexie_channel" if defined?(ActionCable::Channel::Base)
|
|
8
9
|
require_relative "dexiecable/railtie" if defined?(Rails::Railtie)
|
|
9
10
|
|
|
10
11
|
module DexieCable
|
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.0.alpha5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stefan Buhrmester
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '7.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: globalid
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.6'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.6'
|
|
40
54
|
description: DexieCable augments ActionCable channels with a query DSL that mirrors
|
|
41
55
|
the Dexie.js API, letting you push database mutations from the server to the client
|
|
42
56
|
in real time. Includes an ActiveRecord macro (syncs_to_dexie) for automatic change
|
|
@@ -50,7 +64,7 @@ files:
|
|
|
50
64
|
- README.md
|
|
51
65
|
- lib/dexiecable.rb
|
|
52
66
|
- lib/dexiecable/active_record_ext.rb
|
|
53
|
-
- lib/dexiecable/
|
|
67
|
+
- lib/dexiecable/concern.rb
|
|
54
68
|
- lib/dexiecable/query.rb
|
|
55
69
|
- lib/dexiecable/railtie.rb
|
|
56
70
|
- lib/dexiecable/scoped_channel.rb
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module DexieCable
|
|
4
|
-
# The single channel through which all Dexie transfer happens.
|
|
5
|
-
#
|
|
6
|
-
# Broadcasts are scoped to a recipient with +DexieChannel[recipient]+ or
|
|
7
|
-
# +DexieChannel.broadcast_to+. String recipients are public stream names;
|
|
8
|
-
# any other recipient (e.g. a record) targets a private, signed stream.
|
|
9
|
-
#
|
|
10
|
-
# # private (signed)
|
|
11
|
-
# subscription.addStream(DexieChannel.stream_token_for(current_user))
|
|
12
|
-
#
|
|
13
|
-
# # public (no signature, namespaced under "public:")
|
|
14
|
-
# subscription.addPublicStream("feed")
|
|
15
|
-
#
|
|
16
|
-
# Reopen this channel in your app to push initial data or add custom
|
|
17
|
-
# actions:
|
|
18
|
-
#
|
|
19
|
-
# class DexieCable::DexieChannel
|
|
20
|
-
# def subscribed_to(record, params)
|
|
21
|
-
# case record
|
|
22
|
-
# when User
|
|
23
|
-
# table("notifications").bulkAdd(record.notifications.map(&:as_json_for_dexie))
|
|
24
|
-
# end
|
|
25
|
-
# end
|
|
26
|
-
#
|
|
27
|
-
# def my_custom_action(data)
|
|
28
|
-
# # ...
|
|
29
|
-
# end
|
|
30
|
-
# end
|
|
31
|
-
#
|
|
32
|
-
class DexieChannel < ActionCable::Channel::Base
|
|
33
|
-
PUBLIC_STREAM_PREFIX = "public:"
|
|
34
|
-
|
|
35
|
-
public :transmit
|
|
36
|
-
|
|
37
|
-
# Open a scoped channel for broadcasting to a specific recipient.
|
|
38
|
-
#
|
|
39
|
-
# A String recipient is a public stream name (namespaced under
|
|
40
|
-
# +public:+); any other recipient targets that recipient's own stream.
|
|
41
|
-
#
|
|
42
|
-
# DexieChannel[current_user].table("notifications").add(notification)
|
|
43
|
-
# DexieChannel["feed"].table("announcements").add(announcement)
|
|
44
|
-
#
|
|
45
|
-
def self.[](recipient)
|
|
46
|
-
target = recipient.is_a?(String) ? "#{PUBLIC_STREAM_PREFIX}#{recipient}" : recipient
|
|
47
|
-
ScopedChannel.new(self, target)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Build a query against a Dexie table, transmitted to all subscribers
|
|
51
|
-
# of this channel.
|
|
52
|
-
#
|
|
53
|
-
# table("messages").where(:room_id).equals(room.id).add(message)
|
|
54
|
-
#
|
|
55
|
-
def table(name)
|
|
56
|
-
Query.new(self, name)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Returns a signed token for +target+ (a record or other private
|
|
60
|
-
# target). Send it to the client, which passes it to
|
|
61
|
-
# +subscription.addStream+.
|
|
62
|
-
def self.stream_token_for(target)
|
|
63
|
-
verifier.generate(target.respond_to?(:to_gid_param) ? target.to_gid_param : target.to_s)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def self.verifier
|
|
67
|
-
@verifier ||= Rails.application.message_verifier("dexiecable:streams")
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def subscribed
|
|
71
|
-
# Streams are added dynamically via +add_stream+.
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def add_stream(data)
|
|
75
|
-
target = verified_target(data["stream"])
|
|
76
|
-
return unless target
|
|
77
|
-
|
|
78
|
-
stream_for target
|
|
79
|
-
params = data.except("action", "stream").with_indifferent_access
|
|
80
|
-
subscribed_to(resolve_subscribe_target(target), params)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def remove_stream(data)
|
|
84
|
-
target = verified_target(data["stream"])
|
|
85
|
-
return unless target
|
|
86
|
-
|
|
87
|
-
stop_stream_from self.class.broadcasting_for(target)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def remove_all_streams(_data)
|
|
91
|
-
stop_all_streams
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def add_public_stream(data)
|
|
95
|
-
name = data["stream"].to_s
|
|
96
|
-
stream_from public_stream_name(name) if name.present?
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def remove_public_stream(data)
|
|
100
|
-
name = data["stream"].to_s
|
|
101
|
-
stop_stream_from public_stream_name(name) if name.present?
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Override by reopening the channel to push initial data when a private
|
|
105
|
-
# stream is added. +record+ is the resolved target and +params+ are any
|
|
106
|
-
# extra params sent from the client.
|
|
107
|
-
def subscribed_to(_record, _params)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
private
|
|
111
|
-
|
|
112
|
-
def public_stream_name(name)
|
|
113
|
-
self.class.broadcasting_for("#{PUBLIC_STREAM_PREFIX}#{name}")
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def verified_target(token)
|
|
117
|
-
return unless token.present?
|
|
118
|
-
|
|
119
|
-
self.class.verifier.verify(token)
|
|
120
|
-
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
121
|
-
nil
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def resolve_subscribe_target(target)
|
|
125
|
-
target.start_with?("gid://") ? GlobalID::Locator.locate(target) : target
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|