Skip to content

Instantly share code, notes, and snippets.

@lucaswinningham
Last active November 14, 2020 16:44
Show Gist options
  • Save lucaswinningham/a4a842fa48e2f65789c439f63641022b to your computer and use it in GitHub Desktop.
Save lucaswinningham/a4a842fa48e2f65789c439f63641022b to your computer and use it in GitHub Desktop.
Rails Notes
module AppCallbacks
  extend ActiveSupport::Concern

  included do
    include ActiveSupport::Callbacks
  end

  class_methods do
    def define_app_callbacks(*callback_names)
      callback_names.each do |callback_name|
        symbolized_callback_name = callback_name.to_sym
        define_callbacks symbolized_callback_name

        instance_eval do
          ActiveSupport::Callbacks::CALLBACK_FILTER_TYPES.each do |filter_type|
            define_method "#{filter_type}_#{callback_name}" do |*methods|
              methods.each do |method|
                set_callback symbolized_callback_name, filter_type, method
              end
            end
          end
        end
      end
    end
  end
end
class Klass
  include AppCallbacks
  define_app_callbacks :foo, :bar
  define_app_callbacks :baz
  
  def foo
    run_callbacks :foo do
      # do foo stuff
    end
  end

  def bar
    run_callbacks :bar do
      # do bar stuff
    end
  end

  def baz
    run_callbacks :baz do
      # do baz stuff
    end
  end
end
const noop = () => {};
const SOCKET_URL = 'ws://localhost:3000/cable';
const composeLeft = (...fns) => x => fns.reduce((y, f) => f(y), x);

const pinged = 'ping';
const connected = 'welcome';
const subscribed = 'confirm_subscription';

export { pinged, connected, subscribed };

const defaultConnectionEvents = {
  [pinged]: noop,
  [connected]: ({ appWebSocket }) => {
    appWebSocket.send({ command: 'subscribe' });
  },
  [subscribed]: noop
};

export default class AppWebSocket {
  constructor({ channel, actionEvents = {}, connectionEvents = {}, middlewares = [] }) {
    this.channel = channel;
    this.actionEvents = actionEvents;
    this.connectionEvents = { ...defaultConnectionEvents, ...connectionEvents };
    this.middleware = composeLeft(...middlewares.map((middleware) => (obj) => {
      return middleware(obj) || obj;
    }));

    this._webSocket = new WebSocket(SOCKET_URL);

    this._handleMessaging();
  }

  send = ({ command, data }) => {
    const args = { command, identifier: this._identifier };

    if (data) {
      args.data = JSON.stringify(data);
    }

    this._webSocket.send(JSON.stringify(args));
  }

  get channel() {
    return this._channel;
  }

  set channel(channel) {
    this._channel = channel;
    this._identifier = JSON.stringify({ channel });
  }

  _handleMessaging = () => {
    const determineEventFunction = ({ message, type }) => {
      if (type && this.connectionEvents[type]) {
        return this.connectionEvents[type];
      }

      if (message && message.action && this.actionEvents[message.action]) {
        return this.actionEvents[message.action];
      }

      return noop;
    };

    this._webSocket.onmessage = ({ data }) => {
      const parsedData = JSON.parse(data);
      const { message, type } = this.middleware({ ...parsedData, appWebSocket: this });
      const eventFunction = determineEventFunction({ message, type });

      eventFunction({ message, appWebSocket: this });
    };
  }
}

Use

const appWebSocket = new AppWebSocket({
  channel: 'Namespace::Channel',
  connectionEvents: {
    [subscribed]: ({ appWebSocket }) => {
      const data = { action: 'join', number: Math.floor(Math.random() * 100) };

      appWebSocket.send({ command: 'message', data });
    }
  },
  actionEvents: {
    joined: ({ message }) => console.log('joined', { ...message })
  },
  middlewares: [
    ({ message, type }) => {
      if (type !== 'ping') console.log({ ...message, type })
    }
  ]
});

Define a channel in rails

app/channels/namespace/channel.rb

module Namespace
  class Channel < ApplicationCable::Channel
    def subscribed
      stream_from 'namespace_channel'
    end

    def join(data)
      ActionCable.server.broadcast('namespace_channel', action: 'joined', data: data)
    end
  end
end

Get it to work

config/initializers/cors.rb

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
             headers: :any,
             methods: %i[get post put patch delete options head]
  end
end

config/environments/development.rb

Rails.application.configure do
  ...

  config.action_cable.url = 'ws://localhost:3000/cable'
  config.action_cable.disable_request_forgery_protection = true
end

config/routes.rb

Rails.application.routes.draw do
  mount ActionCable.server => '/cable'
end

Taken mostly from this article but this article has a little more to it.

$ createuser myapp --superuser
$ rails g migration enable_pgcrypto_extension

db/migrate/timestamp_enable_pgcrypto_extension.rb

class EnablePgcryptoExtension < ActiveRecord::Migration[6.0]
  def change
    enable_extension 'pgcrypto'
  end
end
$ rails db:migrate
# if error try
$ brew postgresql-upgrade-database

config/application.rb

...

module Api
  class Application < Rails::Application
    ...

    config.generators do |g|
      g.orm :active_record, primary_key_type: :uuid
    end
  end
end

app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  ...
  self.implicit_order_column = :created_at
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment