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

Web... wait for it... sockets! #88

Merged
merged 20 commits into from
Aug 5, 2024
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
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ group :test do
gem "connection_pool", "~> 2.0"
gem "rbnacl"
gem "domain_name"
gem "websocket-client-simple"
end
38 changes: 23 additions & 15 deletions lib/rage-rb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,17 @@

module Rage
def self.application
app = Application.new(__router)

config.middleware.middlewares.reverse.inject(app) do |next_in_chain, (middleware, args, block)|
# in Rails compatibility mode we first check if the middleware is a part of the Rails middleware stack;
# if it is - it is expected to be built using `ActionDispatch::MiddlewareStack::Middleware#build`
if Rage.config.internal.rails_mode
rails_middleware = Rails.application.config.middleware.middlewares.find { |m| m.name == middleware.name }
end

if rails_middleware
rails_middleware.build(next_in_chain)
else
middleware.new(next_in_chain, *args, &block)
end
end
with_middlewares(Application.new(__router), config.middleware.middlewares)
end

def self.multi_application
Rage::Router::Util::Cascade.new(application, Rails.application)
end

def self.cable
Rage::Cable
end

def self.routes
Rage::Router::DSL.new(__router)
end
Expand Down Expand Up @@ -90,6 +80,23 @@ def self.patch_active_record_connection_pool
end
end

# @private
def self.with_middlewares(app, middlewares)
middlewares.reverse.inject(app) do |next_in_chain, (middleware, args, block)|
# in Rails compatibility mode we first check if the middleware is a part of the Rails middleware stack;
# if it is - it is expected to be built using `ActionDispatch::MiddlewareStack::Middleware#build`
if Rage.config.internal.rails_mode
rails_middleware = Rails.application.config.middleware.middlewares.find { |m| m.name == middleware.name }
end

if rails_middleware
rails_middleware.build(next_in_chain)
else
middleware.new(next_in_chain, *args, &block)
end
end
end

module Router
module Strategies
end
Expand All @@ -106,6 +113,7 @@ module ActiveRecord

autoload :Cookies, "rage/cookies"
autoload :Session, "rage/session"
autoload :Cable, "rage/cable/cable"
end

module RageController
Expand Down
1 change: 1 addition & 0 deletions lib/rage/all.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
require_relative "logger/json_formatter"
require_relative "logger/logger"

require_relative "middleware/origin_validator"
require_relative "middleware/fiber_wrapper"
require_relative "middleware/cors"
require_relative "middleware/reloader"
Expand Down
130 changes: 130 additions & 0 deletions lib/rage/cable/cable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# frozen_string_literal: true

module Rage::Cable
# Create a new Cable application.
#
# @example
# map "/cable" do
# run Rage.cable.application
# end
def self.application
protocol = Rage.config.cable.protocol
protocol.init(__router)

handler = __build_handler(protocol)
accept_response = [0, protocol.protocol_definition, []]

application = ->(env) do
if env["rack.upgrade?"] == :websocket
env["rack.upgrade"] = handler
accept_response
else
[426, { "Connection" => "Upgrade", "Upgrade" => "websocket" }, []]
end
end

Rage.with_middlewares(application, Rage.config.cable.middlewares)
end

# @private
def self.__router
@__router ||= Router.new
end

# @private
def self.__build_handler(protocol)
klass = Class.new do
def initialize(protocol)
Iodine.on_state(:on_start) do
unless Fiber.scheduler
Fiber.set_scheduler(Rage::FiberScheduler.new)
end
end

@protocol = protocol
end

def on_open(connection)
Fiber.schedule do
@protocol.on_open(connection)
rescue => e
log_error(e)
end
end

def on_message(connection, data)
Fiber.schedule do
@protocol.on_message(connection, data)
rescue => e
log_error(e)
end
end

if protocol.respond_to?(:on_close)
def on_close(connection)
return unless ::Iodine.running?

Fiber.schedule do
@protocol.on_close(connection)
rescue => e
log_error(e)
end
end
end

if protocol.respond_to?(:on_shutdown)
def on_shutdown(connection)
@protocol.on_shutdown(connection)
rescue => e
log_error(e)
end
end

private

def log_error(e)
Rage.logger.error("Unhandled exception has occured - #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
end
end

klass.new(protocol)
end

# Broadcast data directly to a named stream.
#
# @param stream [String] the name of the stream
# @param data [Object] the object to send to the clients. This will later be encoded according to the protocol used.
# @example
# Rage.cable.broadcast("chat", { message: "A new member has joined!" })
def self.broadcast(stream, data)
Rage.config.cable.protocol.broadcast(stream, data)
end

# @!parse [ruby]
# # @abstract
# class WebSocketConnection
# # Write data to the connection.
# #
# # @param data [String] the data to write
# def write(data)
# end
#
# # Subscribe to a channel.
# #
# # @param name [String] the channel name
# def subscribe(name)
# end
#
# # Close the connection.
# def close
# end
# end

module Protocol
end
end

require_relative "protocol/actioncable_v1_json"
require_relative "channel"
require_relative "connection"
require_relative "router"
Loading
Loading