durable_streams-rails 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: ee437ac73b405cba8114f0a9b38a69f20a70fceb86aa80998b82396af577b133
4
+ data.tar.gz: ce7306811c5b50cab5490102353399a2e14ca1bf23564c93931c0d5344f656b5
5
+ SHA512:
6
+ metadata.gz: 7dba804eeefe0a329b0cc314cfcd595982a0d3eaf5b7e359fd5816998f14668ff28453017c826e7f3cc693c5065e20a81ab68b4cb995c7424da27cbc3956b50f
7
+ data.tar.gz: 71f865a9adec22871977e3e97b7cf2ce4ac7a3d5902f6e0583737c3388c93b257b98ee96240613ca2a9e1829aadb8bcb22ed27796e90508a5f9a9b93415e7d25
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) tokimonki
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/setup"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.pattern = "test/**/*_test.rb"
7
+ t.verbose = false
8
+ end
9
+
10
+ task default: :test
@@ -0,0 +1,9 @@
1
+ # The job that powers all the <tt>stream_*_later_to</tt> broadcasts available in <tt>DurableStreams::Broadcastable</tt>.
2
+ class DurableStreams::BroadcastJob < ActiveJob::Base
3
+ discard_on ActiveJob::DeserializationError
4
+ retry_on DurableStreams::ConnectionError, wait: :polynomially_longer, attempts: 10
5
+
6
+ def perform(stream_name, type:, key:, value:, operation:)
7
+ DurableStreams.broadcast_event_to stream_name, type: type, key: key, value: value, operation: operation
8
+ end
9
+ end
@@ -0,0 +1,202 @@
1
+ # Durable Streams can be broadcast directly from models that include this module (automatically
2
+ # done for Active Records via the engine). This makes it convenient to execute both synchronous
3
+ # and asynchronous State Protocol broadcasts from callbacks or controllers. Here's an example:
4
+ #
5
+ # class Comment < ApplicationRecord
6
+ # belongs_to :post
7
+ #
8
+ # after_create_commit :broadcast_later
9
+ #
10
+ # private
11
+ # def broadcast_later
12
+ # stream_insert_later_to post
13
+ # end
14
+ # end
15
+ #
16
+ # This broadcasts a State Protocol insert event to the stream derived from the post association.
17
+ # All clients subscribed to that stream will receive the event as an SSE message.
18
+ #
19
+ # There are four basic operations you can broadcast: <tt>insert</tt>, <tt>update</tt>, <tt>upsert</tt>,
20
+ # and <tt>delete</tt>. As a rule, you should use the <tt>_later</tt> versions when broadcasting within
21
+ # a real-time path, like a controller or model callback, since those go through a background job.
22
+ #
23
+ # == Declarative streaming
24
+ #
25
+ # For the common case of broadcasting creates, updates, and destroys, you can use the class-level
26
+ # <tt>streams_to</tt> declaration:
27
+ #
28
+ # class Comment < ApplicationRecord
29
+ # belongs_to :post
30
+ # streams_to :post
31
+ # end
32
+ #
33
+ # This is equivalent to registering +after_create_commit+, +after_update_commit+, and
34
+ # +after_destroy_commit+ callbacks that broadcast the appropriate State Protocol events.
35
+ #
36
+ # When the stream target is the model itself, use the self-targeting <tt>streams</tt> declaration:
37
+ #
38
+ # class Board < ApplicationRecord
39
+ # streams
40
+ # end
41
+ #
42
+ # == Suppressing broadcasts
43
+ #
44
+ # Sometimes, you need to disable broadcasts in certain scenarios. You can use <tt>.suppressing_streams</tt>
45
+ # to create execution contexts where broadcasts are disabled:
46
+ #
47
+ # Comment.suppressing_streams do
48
+ # Comment.create!(post: post) # This won't broadcast the insert event
49
+ # end
50
+ module DurableStreams::Broadcastable
51
+ extend ActiveSupport::Concern
52
+
53
+ included do
54
+ thread_mattr_accessor :suppressed_streams, instance_accessor: false
55
+ delegate :suppressed_streams?, to: "self.class"
56
+ end
57
+
58
+ module ClassMethods
59
+ # Configures the model to broadcast creates, updates, and destroys to a stream name derived at runtime by the
60
+ # <tt>stream</tt> symbol invocation. Examples:
61
+ #
62
+ # class Comment < ApplicationRecord
63
+ # belongs_to :post
64
+ # streams_to :post
65
+ # end
66
+ #
67
+ # class Comment < ApplicationRecord
68
+ # belongs_to :post
69
+ # streams_to ->(comment) { [ comment.post, :comments ] }
70
+ # end
71
+ def streams_to(stream)
72
+ after_create_commit -> { stream_insert_later_to(stream.try(:call, self) || send(stream)) }
73
+ after_update_commit -> { stream_update_later_to(stream.try(:call, self) || send(stream)) }
74
+ after_destroy_commit -> { stream_delete_later_to(stream.try(:call, self) || send(stream)) }
75
+ end
76
+
77
+ # Same as <tt>#streams_to</tt>, but the designated stream is automatically set to the current model,
78
+ # which can be overridden by passing <tt>stream</tt>. Examples:
79
+ #
80
+ # class Board < ApplicationRecord
81
+ # streams
82
+ # end
83
+ #
84
+ # class Board < ApplicationRecord
85
+ # streams "boards"
86
+ # end
87
+ def streams(stream = model_name.plural)
88
+ after_create_commit -> { stream_insert_later_to(stream) }
89
+ after_update_commit -> { stream_update_later }
90
+ after_destroy_commit -> { stream_delete_later }
91
+ end
92
+
93
+ # Executes +block+ preventing both synchronous and asynchronous broadcasts from this model.
94
+ def suppressing_streams(&block)
95
+ original, self.suppressed_streams = self.suppressed_streams, true
96
+ yield
97
+ ensure
98
+ self.suppressed_streams = original
99
+ end
100
+
101
+ def suppressed_streams?
102
+ suppressed_streams
103
+ end
104
+ end
105
+
106
+ # Broadcast a State Protocol insert event to the stream identified by the passed <tt>streamables</tt>.
107
+ # Returns the +txid+ that can be used for optimistic update confirmation. Example:
108
+ #
109
+ # # Broadcasts {"type":"message","key":"5","value":{...},"headers":{"operation":"insert"}}
110
+ # txid = message.stream_insert_to post, :messages
111
+ # render json: { txid: txid }
112
+ def stream_insert_to(*streamables)
113
+ DurableStreams.broadcast_event_to(*streamables, **stream_event_attributes(operation: :insert)) unless suppressed_streams?
114
+ end
115
+
116
+ # Same as <tt>#stream_insert_to</tt>, but the designated stream is automatically set to the current model.
117
+ def stream_insert
118
+ stream_insert_to self
119
+ end
120
+
121
+ # Broadcast a State Protocol update event to the stream identified by the passed <tt>streamables</tt>. Example:
122
+ #
123
+ # message.stream_update_to post
124
+ def stream_update_to(*streamables)
125
+ DurableStreams.broadcast_event_to(*streamables, **stream_event_attributes(operation: :update)) unless suppressed_streams?
126
+ end
127
+
128
+ # Same as <tt>#stream_update_to</tt>, but the designated stream is automatically set to the current model.
129
+ def stream_update
130
+ stream_update_to self
131
+ end
132
+
133
+ # Broadcast a State Protocol upsert event to the stream identified by the passed <tt>streamables</tt>. Example:
134
+ #
135
+ # message.stream_upsert_to post
136
+ def stream_upsert_to(*streamables)
137
+ DurableStreams.broadcast_event_to(*streamables, **stream_event_attributes(operation: :upsert)) unless suppressed_streams?
138
+ end
139
+
140
+ # Same as <tt>#stream_upsert_to</tt>, but the designated stream is automatically set to the current model.
141
+ def stream_upsert
142
+ stream_upsert_to self
143
+ end
144
+
145
+ # Broadcast a State Protocol delete event to the stream identified by the passed <tt>streamables</tt>.
146
+ # The value is nil — only the key is needed for deletes. Example:
147
+ #
148
+ # message.stream_delete_to post
149
+ def stream_delete_to(*streamables)
150
+ DurableStreams.broadcast_event_to(*streamables, **stream_event_attributes(operation: :delete, value: nil)) unless suppressed_streams?
151
+ end
152
+
153
+ # Same as <tt>#stream_delete_to</tt>, but the designated stream is automatically set to the current model.
154
+ def stream_delete
155
+ stream_delete_to self
156
+ end
157
+
158
+ # Same as <tt>#stream_insert_to</tt> but run asynchronously via a <tt>DurableStreams::BroadcastJob</tt>.
159
+ def stream_insert_later_to(*streamables)
160
+ DurableStreams.broadcast_event_later_to(*streamables, **stream_event_attributes(operation: :insert)) unless suppressed_streams?
161
+ end
162
+
163
+ # Same as <tt>#stream_insert_later_to</tt>, but the designated stream is automatically set to the current model.
164
+ def stream_insert_later
165
+ stream_insert_later_to self
166
+ end
167
+
168
+ # Same as <tt>#stream_update_to</tt> but run asynchronously via a <tt>DurableStreams::BroadcastJob</tt>.
169
+ def stream_update_later_to(*streamables)
170
+ DurableStreams.broadcast_event_later_to(*streamables, **stream_event_attributes(operation: :update)) unless suppressed_streams?
171
+ end
172
+
173
+ # Same as <tt>#stream_update_later_to</tt>, but the designated stream is automatically set to the current model.
174
+ def stream_update_later
175
+ stream_update_later_to self
176
+ end
177
+
178
+ # Same as <tt>#stream_upsert_to</tt> but run asynchronously via a <tt>DurableStreams::BroadcastJob</tt>.
179
+ def stream_upsert_later_to(*streamables)
180
+ DurableStreams.broadcast_event_later_to(*streamables, **stream_event_attributes(operation: :upsert)) unless suppressed_streams?
181
+ end
182
+
183
+ # Same as <tt>#stream_upsert_later_to</tt>, but the designated stream is automatically set to the current model.
184
+ def stream_upsert_later
185
+ stream_upsert_later_to self
186
+ end
187
+
188
+ # Same as <tt>#stream_delete_to</tt> but run asynchronously via a <tt>DurableStreams::BroadcastJob</tt>.
189
+ def stream_delete_later_to(*streamables)
190
+ DurableStreams.broadcast_event_later_to(*streamables, **stream_event_attributes(operation: :delete, value: nil)) unless suppressed_streams?
191
+ end
192
+
193
+ # Same as <tt>#stream_delete_later_to</tt>, but the designated stream is automatically set to the current model.
194
+ def stream_delete_later
195
+ stream_delete_later_to self
196
+ end
197
+
198
+ private
199
+ def stream_event_attributes(operation:, value: as_json)
200
+ { type: model_name.singular, key: id.to_s, value: value, operation: operation }
201
+ end
202
+ end
@@ -0,0 +1,157 @@
1
+ module DurableStreams
2
+ module Broadcastable
3
+ module TestHelper
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include DurableStreams::StreamName
8
+
9
+ setup { DurableStreams::Testing.install! }
10
+ teardown { DurableStreams::Testing.reset! }
11
+ end
12
+
13
+ # Asserts that State Protocol events were broadcast to a stream
14
+ #
15
+ # ==== Arguments
16
+ #
17
+ # * <tt>stream_name_or_object</tt> the objects used to generate the
18
+ # stream name, or the name itself
19
+ # * <tt>&block</tt> optional block executed before the
20
+ # assertion
21
+ #
22
+ # ==== Options
23
+ #
24
+ # * <tt>count:</tt> the number of events that are expected to be broadcast
25
+ #
26
+ # Asserts events were broadcast:
27
+ #
28
+ # comment = Comment.find(1)
29
+ # comment.stream_insert_to comment.post
30
+ #
31
+ # assert_stream_broadcasts comment.post
32
+ #
33
+ # Asserts that two events were broadcast:
34
+ #
35
+ # comment = Comment.find(1)
36
+ # comment.stream_insert_to comment.post
37
+ # comment.stream_update_to comment.post
38
+ #
39
+ # assert_stream_broadcasts comment.post, count: 2
40
+ #
41
+ # You can pass a block to run before the assertion:
42
+ #
43
+ # comment = Comment.find(1)
44
+ #
45
+ # assert_stream_broadcasts comment.post do
46
+ # comment.stream_insert_to comment.post
47
+ # end
48
+ #
49
+ # In addition to a String, the helper also accepts an Object or Array to
50
+ # determine the stream name:
51
+ #
52
+ # post = Post.find(1)
53
+ #
54
+ # assert_stream_broadcasts post do
55
+ # post.comments.create!(body: "Hello")
56
+ # end
57
+ #
58
+ def assert_stream_broadcasts(stream_name_or_object, count: nil, &block)
59
+ payloads = capture_stream_broadcasts(stream_name_or_object, &block)
60
+ stream_name = stream_name_from(stream_name_or_object)
61
+
62
+ if count.nil?
63
+ assert_not_empty payloads, "Expected at least one broadcast on #{stream_name.inspect}, but there were none"
64
+ else
65
+ broadcasts = "Durable Stream broadcast".pluralize(count)
66
+
67
+ assert count == payloads.count, "Expected #{count} #{broadcasts} on #{stream_name.inspect}, but there were #{payloads.count}"
68
+ end
69
+ end
70
+
71
+ # Asserts that no State Protocol events were broadcast to a stream
72
+ #
73
+ # ==== Arguments
74
+ #
75
+ # * <tt>stream_name_or_object</tt> the objects used to generate the
76
+ # stream name, or the name itself
77
+ # * <tt>&block</tt> optional block executed before the
78
+ # assertion
79
+ #
80
+ # Asserts that no events were broadcast:
81
+ #
82
+ # assert_no_stream_broadcasts "messages" do
83
+ # # do something other than broadcast to "messages"
84
+ # end
85
+ #
86
+ # In addition to a String, the helper also accepts an Object or Array to
87
+ # determine the stream name:
88
+ #
89
+ # post = Post.find(1)
90
+ #
91
+ # assert_no_stream_broadcasts post do
92
+ # # do something other than broadcast to post's stream
93
+ # end
94
+ #
95
+ def assert_no_stream_broadcasts(stream_name_or_object, &block)
96
+ block&.call
97
+
98
+ stream_name = stream_name_from(stream_name_or_object)
99
+ payloads = stream_broadcasts_for(stream_name)
100
+
101
+ assert payloads.empty?, "Expected no broadcasts on #{stream_name.inspect}, but there were #{payloads.count}"
102
+ end
103
+
104
+ # Captures any State Protocol events that were broadcast to a stream
105
+ #
106
+ # ==== Arguments
107
+ #
108
+ # * <tt>stream_name_or_object</tt> the objects used to generate the
109
+ # stream name, or the name itself
110
+ # * <tt>&block</tt> optional block to capture broadcasts during execution
111
+ #
112
+ # Returns any events that have been broadcast as an Array of parsed JSON hashes
113
+ #
114
+ # comment = Comment.find(1)
115
+ # comment.stream_insert_to comment.post
116
+ # comment.stream_update_to comment.post
117
+ #
118
+ # events = capture_stream_broadcasts comment.post
119
+ #
120
+ # assert_equal "insert", events.first["headers"]["operation"]
121
+ # assert_equal "update", events.second["headers"]["operation"]
122
+ #
123
+ # You can pass a block to limit the scope of the broadcasts being captured:
124
+ #
125
+ # comment = Comment.find(1)
126
+ #
127
+ # events = capture_stream_broadcasts comment.post do
128
+ # comment.stream_insert_to comment.post
129
+ # end
130
+ #
131
+ # assert_equal "insert", events.first["headers"]["operation"]
132
+ #
133
+ def capture_stream_broadcasts(stream_name_or_object, &block)
134
+ stream_name = stream_name_from(stream_name_or_object)
135
+
136
+ if block_given?
137
+ before = stream_broadcasts_for(stream_name).size
138
+ block.call
139
+ stream_broadcasts_for(stream_name)[before..]
140
+ else
141
+ stream_broadcasts_for(stream_name)
142
+ end
143
+ end
144
+
145
+ private
146
+ def stream_broadcasts_for(stream_name)
147
+ DurableStreams::Testing.messages_for(stream_name).map do |message|
148
+ if message.is_a?(String)
149
+ JSON.parse(message)
150
+ else
151
+ message
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,55 @@
1
+ # Provides the broadcast actions in synchronous and asynchronous form for the <tt>DurableStreams</tt> module.
2
+ # See <tt>DurableStreams::Broadcastable</tt> for the user-facing API that invokes these methods with most of the
3
+ # paperwork filled out already.
4
+ #
5
+ # Can be used directly using something like <tt>DurableStreams.broadcast_event_to :room, type: "message", key: "5",
6
+ # value: { content: "Hello" }, operation: :insert</tt>.
7
+ module DurableStreams::Broadcasts
8
+ def broadcast_event_to(*streamables, type:, key:, value:, operation:)
9
+ streamables.flatten!
10
+ streamables.compact_blank!
11
+
12
+ if streamables.present?
13
+ txid = SecureRandom.uuid
14
+
15
+ append_to_stream(
16
+ stream_name_from(streamables),
17
+ JSON.generate({
18
+ type: type, key: key, value: value,
19
+ headers: { operation: operation, txid: txid }
20
+ })
21
+ )
22
+
23
+ txid
24
+ end
25
+ end
26
+
27
+ def broadcast_event_later_to(*streamables, type:, key:, value:, operation:)
28
+ streamables.flatten!
29
+ streamables.compact_blank!
30
+
31
+ if streamables.present?
32
+ DurableStreams::BroadcastJob.perform_later \
33
+ stream_name_from(streamables),
34
+ type: type, key: key, value: value, operation: operation.to_s
35
+ end
36
+ end
37
+
38
+ def broadcast_to(*streamables, **payload)
39
+ streamables.flatten!
40
+ streamables.compact_blank!
41
+
42
+ if streamables.present?
43
+ append_to_stream(stream_name_from(streamables), JSON.generate(payload))
44
+ end
45
+ end
46
+
47
+ private
48
+ def append_to_stream(stream_name, message)
49
+ if DurableStreams::Testing.recording?
50
+ DurableStreams::Testing.record(stream_name, message)
51
+ else
52
+ stream(stream_name).append(message)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,45 @@
1
+ require "rails/engine"
2
+
3
+ module DurableStreams
4
+ class Engine < Rails::Engine
5
+ isolate_namespace DurableStreams
6
+ config.eager_load_namespaces << DurableStreams
7
+ config.durable_streams = ActiveSupport::OrderedOptions.new
8
+ config.autoload_once_paths = %W(
9
+ #{root}/app/models
10
+ #{root}/app/models/concerns
11
+ #{root}/app/jobs
12
+ )
13
+
14
+ initializer "durable_streams.no_active_job", before: :set_eager_load_paths do
15
+ unless defined?(ActiveJob)
16
+ Rails.autoloaders.once.do_not_eager_load("#{root}/app/jobs")
17
+ end
18
+ end
19
+
20
+ initializer "durable_streams.broadcastable" do
21
+ ActiveSupport.on_load(:active_record) do
22
+ if defined?(ActiveJob)
23
+ include DurableStreams::Broadcastable
24
+ end
25
+ end
26
+ end
27
+
28
+ initializer "durable_streams.signed_stream_verifier_key" do
29
+ config.after_initialize do
30
+ DurableStreams.signed_stream_verifier_key =
31
+ config.durable_streams&.signed_stream_verifier_key ||
32
+ Rails.application.key_generator.generate_key("durable_streams/signed_stream_verifier_key")
33
+ end
34
+ end
35
+
36
+ initializer "durable_streams.test_assertions" do
37
+ ActiveSupport.on_load(:active_support_test_case) do
38
+ if defined?(ActiveJob)
39
+ require "durable_streams/broadcastable/test_helper"
40
+ include DurableStreams::Broadcastable::TestHelper
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ # Stream names are how we identify which updates should go to which subscribers. Since stream names
2
+ # are exposed directly to the client via signed URL tokens, we need to ensure that the name isn't
3
+ # tampered with, so the names are signed upon generation and verified upon receipt. All verification
4
+ # happens through the <tt>DurableStreams.signed_stream_verifier</tt>.
5
+ module DurableStreams::StreamName
6
+ # Used by the stream auth endpoint to verify a signed stream name.
7
+ def verified_stream_name(signed_stream_name)
8
+ DurableStreams.signed_stream_verifier.verified signed_stream_name
9
+ end
10
+
11
+ # Used by <tt>DurableStreams.signed_stream_url</tt> to generate a signed stream name.
12
+ def signed_stream_name(streamables)
13
+ DurableStreams.signed_stream_verifier.generate stream_name_from(streamables)
14
+ end
15
+
16
+ private
17
+ def stream_name_from(streamables)
18
+ if streamables.is_a?(Array)
19
+ streamables.map { |streamable| stream_name_from(streamable) }.join("/")
20
+ else
21
+ streamables.then { |streamable| streamable.try(:to_gid_param) || streamable.to_param }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ require "active_support/core_ext/module/attribute_accessors_per_thread"
2
+
3
+ # Intercepts stream appends during tests, similar to how <tt>ActionCable::TestHelper</tt> captures broadcasts.
4
+ # Activated by <tt>DurableStreams::Broadcastable::TestHelper</tt> via +setup+/+teardown+ hooks.
5
+ #
6
+ # When installed, all calls to <tt>DurableStreams.broadcast_event_to</tt> and <tt>DurableStreams.broadcast_to</tt>
7
+ # are recorded in memory instead of being sent to the stream server. Captured messages can be inspected via
8
+ # <tt>messages_for</tt>.
9
+ module DurableStreams::Testing
10
+ thread_mattr_accessor :messages
11
+
12
+ class << self
13
+ def install!
14
+ self.messages = Hash.new { |h, k| h[k] = [] }
15
+ end
16
+
17
+ def reset!
18
+ self.messages = nil
19
+ end
20
+
21
+ def recording?
22
+ messages.present?
23
+ end
24
+
25
+ def record(stream_name, message)
26
+ messages[stream_name] << message if recording?
27
+ end
28
+
29
+ def messages_for(stream_name)
30
+ (messages || {})[stream_name] || []
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module DurableStreams
2
+ RAILS_VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,30 @@
1
+ require "durable_streams/stream_name"
2
+ require "durable_streams/broadcasts"
3
+ require "durable_streams/testing"
4
+ require "durable_streams/engine"
5
+ require "active_support/core_ext/module/attribute_accessors_per_thread"
6
+
7
+ module DurableStreams
8
+ extend DurableStreams::StreamName
9
+ extend DurableStreams::Broadcasts
10
+
11
+ mattr_accessor :base_url
12
+
13
+ class << self
14
+ attr_writer :signed_stream_verifier_key
15
+
16
+ def signed_stream_verifier
17
+ @signed_stream_verifier ||= ActiveSupport::MessageVerifier.new(signed_stream_verifier_key, digest: "SHA256", serializer: JSON)
18
+ end
19
+
20
+ def signed_stream_verifier_key
21
+ @signed_stream_verifier_key or raise ArgumentError, "DurableStreams requires a signed_stream_verifier_key"
22
+ end
23
+
24
+ def signed_stream_url(*streamables, expires_in: 24.hours)
25
+ path = stream_name_from(streamables)
26
+ token = signed_stream_verifier.generate(path, expires_in: expires_in)
27
+ "#{base_url}/#{path}?token=#{token}"
28
+ end
29
+ end
30
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: durable_streams-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - tokimonki
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: durable_streams
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: railties
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '7.1'
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: 7.1.0
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '7.1'
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: 7.1.0
46
+ email: opensource@tokimonki.com
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - MIT-LICENSE
52
+ - Rakefile
53
+ - app/jobs/durable_streams/broadcast_job.rb
54
+ - app/models/concerns/durable_streams/broadcastable.rb
55
+ - lib/durable_streams-rails.rb
56
+ - lib/durable_streams/broadcastable/test_helper.rb
57
+ - lib/durable_streams/broadcasts.rb
58
+ - lib/durable_streams/engine.rb
59
+ - lib/durable_streams/stream_name.rb
60
+ - lib/durable_streams/testing.rb
61
+ - lib/durable_streams/version.rb
62
+ homepage: https://github.com/tokimonki/durable_streams-rails
63
+ licenses:
64
+ - MIT
65
+ metadata: {}
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '3.1'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.6.9
81
+ specification_version: 4
82
+ summary: Durable Streams integration for Rails
83
+ test_files: []