Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stream_from #104

Merged
merged 19 commits into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/channels/cable_ready/stream.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module CableReady
class Stream < ActionCable::Channel::Base
include CableReady::StreamIdentifier

def subscribed
locator = verified_stream_identifier(params[:identifier])
locator.present? ? stream_from(locator) : reject
end
end
end
11 changes: 11 additions & 0 deletions app/helpers/cable_ready_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module CableReadyHelper
include CableReady::Compoundable
include CableReady::StreamIdentifier

def stream_from(*keys)
keys.select!(&:itself)
tag.stream_from(identifier: signed_stream_identifier(compound(keys)))
end
end
7 changes: 7 additions & 0 deletions javascript/action_cable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export let consumer

export default {
setConsumer (value) {
consumer = value
}
}
10 changes: 9 additions & 1 deletion javascript/cable_ready.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { verifyNotMutable, verifyNotPermanent } from './morph_callbacks'
import { xpathToElement } from './utils'
import activeElement from './active_element'
import DOMOperations from './operations'
import actionCable from './action_cable'
import './stream_from_element'

export const shouldMorphCallbacks = [verifyNotMutable, verifyNotPermanent]
export const didMorphCallbacks = []
Expand Down Expand Up @@ -59,6 +61,11 @@ const performAsync = (
})
}

const initialize = (initializeOptions = {}) => {
const { consumer } = initializeOptions
actionCable.setConsumer(consumer)
}

document.addEventListener('DOMContentLoaded', function () {
if (!document.audio && document.body.hasAttribute('data-unlock-audio')) {
document.audio = new Audio(
Expand All @@ -82,5 +89,6 @@ export default {
performAsync,
DOMOperations,
shouldMorphCallbacks,
didMorphCallbacks
didMorphCallbacks,
initialize
}
38 changes: 38 additions & 0 deletions javascript/stream_from_element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import CableReady from 'cable_ready'
import { consumer } from './action_cable'

class StreamFromElement extends HTMLElement {
connectedCallback () {
if (this.preview) return
if (consumer) {
this.channel = consumer.subscriptions.create(
{
channel: 'CableReady::Stream',
identifier: this.getAttribute('identifier')
},
{
received (data) {
if (data.cableReady) CableReady.perform(data.operations)
}
}
)
} else {
console.error(
`The stream_from helper cannot connect without an ActionCable consumer.\nPlease set 'CableReady.initialize({ consumer })' in your index.js.`
)
}
}

disconnectedCallback () {
if (this.channel) this.channel.unsubscribe()
}

get preview () {
return (
document.documentElement.hasAttribute('data-turbolinks-preview') ||
document.documentElement.hasAttribute('data-turbo-preview')
)
}
}

customElements.define('stream-from', StreamFromElement)
18 changes: 13 additions & 5 deletions lib/cable_ready.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
require "cable_ready/operation_builder"
require "cable_ready/config"
require "cable_ready/broadcaster"
require "cable_ready/compoundable"
require "cable_ready/channel"
require "cable_ready/channels"
require "cable_ready/cable_car"
require "cable_ready/stream_identifier"

module CableReady
class Engine < Rails::Engine
Expand All @@ -28,11 +30,17 @@ class Engine < Rails::Engine
end
end

def self.config
CableReady::Config.instance
end
class << self
def config
CableReady::Config.instance
end

def self.configure
yield config
def configure
yield config
end

def signed_stream_verifier
@signed_stream_verifier ||= ActiveSupport::MessageVerifier.new(config.verifier_key, digest: "SHA256", serializer: JSON)
end
end
end
5 changes: 4 additions & 1 deletion lib/cable_ready/channels.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ module CableReady
# This class is a thread local singleton: CableReady::Channels.instance
# SEE: https://github.com/socketry/thread-local/tree/master/guides/getting-started
class Channels
include Compoundable
extend Thread::Local

def initialize
@channels = {}
end

def [](identifier)
def [](*keys)
keys.select!(&:itself)
identifier = keys.many? || (keys.one? && keys.first.is_a?(ActiveRecord::Base)) ? compound(keys) : keys.pop
leastbad marked this conversation as resolved.
Show resolved Hide resolved
@channels[identifier] ||= CableReady::Channel.new(identifier)
end

Expand Down
11 changes: 11 additions & 0 deletions lib/cable_ready/compoundable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module CableReady
module Compoundable
def compound(keys)
keys.map { |key|
key.class < ActiveRecord::Base ? key.to_global_id.to_s : key.to_s
}.join(":")
end
end
end
6 changes: 6 additions & 0 deletions lib/cable_ready/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class Config
include Observable
include Singleton

attr_writer :verifier_key

def initialize
super
@operation_names = Set.new(default_operation_names)
Expand All @@ -16,6 +18,10 @@ def observers
@observer_peers&.keys || []
end

def verifier_key
@verifier_key || Rails.application.key_generator.generate_key("cable_ready/verifier_key")
end

def operation_names
@operation_names.to_a
end
Expand Down
13 changes: 13 additions & 0 deletions lib/cable_ready/stream_identifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module CableReady
module StreamIdentifier
def verified_stream_identifier(signed_stream_identifier)
CableReady.signed_stream_verifier.verified signed_stream_identifier
end

def signed_stream_identifier(compoundable)
CableReady.signed_stream_verifier.generate compoundable
end
end
end
43 changes: 43 additions & 0 deletions lib/generators/cable_ready/stream_from_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require "rails/generators"
require "fileutils"

module CableReady
class StreamFromGenerator < Rails::Generators::Base
desc "Initializes CableReady with a reference to the shared ActionCable consumer"
source_root File.expand_path("templates", __dir__)

def copy_controller_file
main_folder = defined?(Webpacker) ? Webpacker.config.source_path.to_s.gsub("#{Rails.root}/", "") : "app/javascript"

filepath = [
"#{main_folder}/controllers/index.js",
"#{main_folder}/controllers/index.ts",
"#{main_folder}/packs/application.js",
"#{main_folder}/packs/application.ts"
]
.select { |path| File.exist?(path) }
.map { |path| Rails.root.join(path) }
.first

lines = File.open(filepath, "r") { |f| f.readlines }

unless lines.find { |line| line.start_with?("import CableReady") }
matches = lines.select { |line| line =~ /\A(require|import)/ }
lines.insert lines.index(matches.last).to_i + 1, "import CableReady from 'cable_ready'\n"
File.open(filepath, "w") { |f| f.write lines.join }
end

unless lines.find { |line| line.start_with?("import consumer") }
matches = lines.select { |line| line =~ /\A(require|import)/ }
lines.insert lines.index(matches.last).to_i + 1, "import consumer from '../channels/consumer'\n"
File.open(filepath, "w") { |f| f.write lines.join }
end

unless lines.find { |line| line.include?("CableReady.initialize({ consumer })") }
append_to_file filepath, "CableReady.initialize({ consumer })"
end
end
end
end