项目作者: cable-cr

项目描述 :
It's like ActionCable (100% compatible with JS Client), but you know, for Crystal
高级语言: Crystal
项目地址: git://github.com/cable-cr/cable.git
创建时间: 2019-07-11T20:42:01Z
项目社区:https://github.com/cable-cr/cable

开源协议:MIT License

下载


Cable

ci workflow

It’s like ActionCable (100% compatible with JS Client), but you know, for Crystal.

Installation

  1. Add the dependency to your shard.yml:
  1. dependencies:
  2. cable:
  3. github: cable-cr/cable
  4. branch: master # or use the latest version
  5. # Specify which backend you want to use
  6. cable-redis:
  7. github: cable-cr/cable-redis
  8. branch: main

Cable supports multiple backends. The most common one is Redis, but there’s a few to choose from with more being added:

Since there are multiple different versions of Redis for Crystal, you can choose which one you want to use.

Or if you don’t want to use Redis, you can try one of these alternatives

  1. Run shards install

Usage

Application code

  1. require "cable"
  2. # Or whichever backend you chose
  3. require "cable-redis"

Lucky example

To help better illustrate how the entire setup looks, we’ll use Lucky, but this will work in any Crystal web framework.

Load the shard

  1. # src/shards.cr
  2. require "cable"
  3. require "cable-redis"

Mount the middleware

Add the Cable::Handler before Lucky::RouteHandler

  1. # src/app_server.cr
  2. class AppServer < Lucky::BaseAppServer
  3. def middleware
  4. [
  5. Cable::Handler(ApplicationCable::Connection).new, # place before the middleware below
  6. Honeybadger::Handler.new,
  7. Lucky::ErrorHandler.new(action: Errors::Show),
  8. Lucky::RouteHandler.new,
  9. ]
  10. end
  11. end

Configure cable settings

After that, you can configure your Cable server. The defaults are:

  1. # config/cable.cr
  2. Cable.configure do |settings|
  3. settings.route = "/cable" # the URL your JS Client will connect
  4. settings.token = "token" # The query string parameter used to get the token
  5. settings.url = ENV.fetch("CABLE_BACKEND_URL", "redis://localhost:6379")
  6. settings.backend_class = Cable::RedisBackend
  7. settings.backend_ping_interval = 15.seconds
  8. settings.restart_error_allowance = 20
  9. settings.on_error = ->(error : Exception, message : String) do
  10. # or whichever error reportings you're using
  11. Bugsnag.report(error) do |event|
  12. event.app.app_type = "lucky"
  13. event.meta_data = {
  14. "error_class" => JSON::Any.new(error.class.name),
  15. "message" => JSON::Any.new(message),
  16. }
  17. end
  18. end
  19. end

Configure logging level

You may want to tune how to report logging.

  1. # config/log.cr
  2. log_levels = {
  3. "debug" => Log::Severity::Debug,
  4. "info" => Log::Severity::Info,
  5. "error" => Log::Severity::Error,
  6. }
  7. # use the `CABLE_DEBUG_LEVEL` env var to choose any of the 3 log levels above
  8. Cable::Logger.level = log_levels[ENV.fetch("CABLE_DEBUG_LEVEL", "info")]

Alternatively, use a global log level which matches you application log code also.

See Crystal API docs for more details..

  1. # config/log.cr
  2. # use the `LOG_LEVEL` env var
  3. Cable::Logger.setup_from_env(default_level: :warn)

NOTE: The volume of logs produced are high… If log costs are a concern, use warn level to only receive critical logs

Setup the main application connection and channel classes

Then you need to implement a few classes.

The connection class is how you are going to handle connections. It’s referenced in the src/app_server.cr file when creating the handler.

  1. # src/channels/application_cable/connection.cr
  2. module ApplicationCable
  3. class Connection < Cable::Connection
  4. # You need to specify how you identify the class, using something like:
  5. # Remembering that it must be a String
  6. # Tip: Use your `User#id` converted to String
  7. identified_by :identifier
  8. # If you'd like to keep a `User` instance together with the Connection, so
  9. # there's no need to fetch from the database all the time, you can use the
  10. # `owned_by` instruction
  11. owned_by current_user : User
  12. def connect
  13. UserToken.decode_user_id(token.to_s).try do |user_id|
  14. self.identifier = user_id.to_s
  15. self.current_user = UserQuery.find(user_id)
  16. end
  17. end
  18. end
  19. end

Then you need you a base channel to make it easy to inherit your app’s Cable logic.

  1. # src/channels/application_cable/channel.cr
  2. module ApplicationCable
  3. class Channel < Cable::Channel
  4. # some potential shared logic or helpers
  5. end
  6. end

Create your app channels

Kitchen sink example

Then create your cables, as much as your want!! Let’s set up a ChatChannel as an example:

  1. # src/channels/chat_channel.cr
  2. class ChatChannel < ApplicationCable::Channel
  3. def subscribed
  4. # We don't support stream_for, needs to generate your own unique string
  5. stream_from "chat_#{params["room"]}"
  6. end
  7. def receive(data)
  8. broadcast_message = {} of String => String
  9. broadcast_message["message"] = data["message"].to_s
  10. broadcast_message["current_user_id"] = connection.identifier
  11. ChatChannel.broadcast_to("chat_#{params["room"]}", broadcast_message)
  12. end
  13. def perform(action, action_params)
  14. user = UserQuery.new.find(connection.identifier)
  15. # Perform actions on a user object. For example, you could manage
  16. # its status by adding some .away and .status methods on it like below
  17. # user.away if action == "away"
  18. # user.status(action_params["status"]) if action == "status"
  19. ChatChannel.broadcast_to("chat_#{params["room"]}", {
  20. "user" => user.email,
  21. "performed" => action.to_s,
  22. })
  23. end
  24. def unsubscribed
  25. # Perform any action after the client closes the connection.
  26. user = UserQuery.new.find(connection.identifier)
  27. # You could, for example, call any method on your user
  28. # user.logout
  29. end
  30. end

Rejection example

Reject channel subscription if the request is invalid:

  1. # src/channels/chat_channel.cr
  2. class ChatChannel < ApplicationCable::Channel
  3. def subscribed
  4. reject if user_not_allowed_to_join_chat_room?
  5. stream_from "chat_#{params["room"]}"
  6. end
  7. end

Callbacks example

Use callbacks to perform actions or transmit messages once the connection/channel has been subscribed.

  1. # src/channels/chat_channel.cr
  2. class ChatChannel < ApplicationCable::Channel
  3. # you can name these callbacks anything you want...
  4. # `after_subscribed` can accept 1 or more callbacks to be run in order
  5. after_subscribed :broadcast_welcome_pack_to_single_subscribed_user,
  6. :announce_user_joining_to_everyone_else_in_the_channel,
  7. :process_some_stuff
  8. def subscribed
  9. stream_from "chat_#{params["room"]}"
  10. end
  11. # If you ONLY need to send the current_user a message
  12. # and none of the other subscribers
  13. #
  14. # use -> transmit(message), which accepts Hash(String, String) or String
  15. def broadcast_welcome_pack_to_single_subscribed_user
  16. transmit({ "welcome_pack" => "some cool stuff for this single user" })
  17. end
  18. # On the other hand,
  19. # if you want to broadcast a message
  20. # to all subscribers connected to this channel
  21. #
  22. # use -> broadcast(message), which accepts Hash(String, String) or String
  23. def announce_user_joining_to_everyone_else_in_the_channel
  24. broadcast("username xyz just joined")
  25. end
  26. # you don't need to use the transmit functionality
  27. def process_some_stuff
  28. send_welcome_email_to_user
  29. update_their_profile
  30. end
  31. end

Error handling

You can setup a hook to report errors to any 3rd party service you choose.

  1. # config/cable.cr
  2. Cable.configure do |settings|
  3. settings.on_error = ->(exception : Exception, message : String) do
  4. # new 3rd part service handler
  5. ExceptionService.notify(exception, message: message)
  6. # default logic
  7. Cable::Logger.error(exception: exception) { message }
  8. end
  9. end

Default Handler

  1. Habitat.create do
  2. setting on_error : Proc(Exception, String, Nil) = ->(exception : Exception, message : String) do
  3. Cable::Logger.error(exception: exception) { message }
  4. end
  5. end

NOTE: The message field will contain details regarding which class/method raised the error

Client-Side

Check below on the JavaScript section how to communicate with the Cable backend.

JavaScript

It works with ActionCable JS Client out-of-the-box!! Yeah, that’s really cool no? If you need to adapt, make a hack, or something like that?!

No, you don’t need it! Just read the few lines below and start playing with Cable in 5 minutes!

ActionCable JS Example

examples/action-cable-js-client.md

Vanilla JS Examples

If you want to use this shard with iOS clients or vanilla JS using react etc., there is an example in the examples folder.

Note - If you are using a vanilla - non-action-cable JS client, you may want to disable the action cable response headers as they cause issues for clients who don’t know how to handle them. Set a Habitat disable_sec_websocket_protocol_header like so to disable those headers;

  1. # config/cable.cr
  2. Cable.configure do |settings|
  3. settings.disable_sec_websocket_protocol_header = true
  4. end

Debugging

You can create a JSON endpoint to ping the server and check how things are going.

  1. # src/actions/debug/index.cr
  2. class Debug::Index < ApiAction
  3. include RequireAuthToken
  4. get "/debug" do
  5. json(Cable.server.debug_json) # Cable.server.debug_json is provided by this shard
  6. end
  7. end

Alternatively, you can ping Redis directly using the redis-cli as follows;

  1. PUBLISH _internal debug

This will dump a debug status into the logs.

Contributing

  1. Fork it (https://github.com/cable-cr/cable/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request