MessageBus.callbackInterval does not work in production - ruby-on-rails

I've been using the message_bus gem for a few months as a more simplified alternative to a websocket solution. The default MessageBus.callbackIntervalis too wide, so I want to narrow it. I've done this successfully in my development env. However, when deploying to production it seems that the second line below is ignored:
MessageBus.start();
MessageBus.callbackInterval = 1800; # this should change the interval frequency, but it doesn't
MessageBus.subscribe("/some_id", function(status) {
// work
});
Any idea how I could debug this?

I haven't personally encountered this problem before, but like with many JavaScript libraries/files in Rails it is probably something in the assets pipeline.
Are you certain that you included JavaScript required for message bus to work in your view with:
<script src="message-bus.js" type="text/javascript"></script>
and in assets
//= require message-bus
If you're using a Ruby version of this gem, you can always try to set this using Ruby instead of JavaScript.
client = MessageBus::Client.new('http://127.0.0.1/8000') #port is optional
#you should be able to do
client.callbackInterval = 1800
#after that you should be able to do
client.subscribe('/message') do |payload|
# Do stuff
end
For more info refer to https://github.com/lowjoel/message_bus-client

Related

How to set up ActionCable with Importmaps in Rails 6? (MRI and Jruby)

Actioncable - an overview.
I am using jruby 9.2.16 (hence ruby 2.5.7) with rails 6.1.6.1.
I am not sure if only in development or only without ssl (wss) Actioncable can be used with the simple client side:
var ws = new WebSocket('ws://0.0.0.0:3000/channels');
ws.onmessage = function(e){ console.log(e.data); }
But at least I didn't get it running to do "Streaming from Channel" using wss in production, as it works locally (visible starting 'redis-cli' in terminal, and then 'monitor').
So I tried to implement the actioncable client scripts and hence 8 days got lost.
First I struggled with the fact that there is no description which is somehow complete. Many people publish particular solutions, but this is like gambling: maybe you are lucky.
Second, files are named in a way that seems to be general even though they are only about actioncable (the folder 'javascript', or 'application.js')
It is misleading to not call them 'actioncable_files' and 'actioncable_app.js' and voilà there are problems because of several files with the same name.
The next problem is, that lots has to be changed only, because the orderly structure of files is ignored. Javascripts no longer are in assets, but why?
They could be in assets/javascripts/actioncable/ ?
So manifest.js has to be changed, and even attributes have to be added in application.rb (attr_accessor :importmap). Something you find after some days in a forum.
but even more confusing: importmap is a gem, which requires some directories and which somehow has to be installed (rake app:update:bin, rails importmap:install) and someone wrote, the order of the gems were relevant but you cannot only deinstall actioncable gem, because rails depends on that. Dependencies could be organized using a permutation of priority.
importmaps is not working in firefox, so you need shims additionally
But in the end, everything what importmap does, looks like reinventing the wheel to me: it loads javascript files. Something, that can be done easily manually.
Also importing modules is possible in simple javascript. And what else then a javascript file shall it be, that the browser finally will work with?
Now importmap creates from a string '#rails/actioncable' another string 'actioncable.esm.js', which in my case (after 8 days of working 15 h to get it to work, still is not found automatically.
I cannot find that file, or any description where it is generated, or if it is only a link, or has to be compiled somehwere, but it looks to me, as if importmap is completely redundant and only makes things very complicated. I don't understand the benefit from writing a string 'xyz' which is translated in a laborious way to set up into another string 'xyz_2', which also might not be found. And if there are variables, they can be loaded directly using the same idea like action_cable_meta_tag.
Technically Actioncable is only doing what Faye did before. So why do we need all that so called "modern" way, which I think only is reinventing the wheel?
So, I would like to create a description on how to install actioncable in an easy way - without unnecessary tools and clearly.
But first I need to get it to work on my own. And therefore the question is: what shall I do due to:
GET http://0.0.0.0:3000/actioncable.esm.js net::ERR_ABORTED 404 (Not Found)
Thanks everyone for any idea!
I've never set up ActionCable before and never used it (except second hand through Turbo::Broadcastable). Seems like you had quite the journey. I used rails guides and rails github to set it up.
First of all, from importmap-rails:
Note: In order to use JavaScript from Rails frameworks like Action Cable, Action Text, and Active Storage, you must be running Rails 7.0+. This was the first version that shipped with ESM compatible builds of these libraries.
https://github.com/rails/importmap-rails#installation
Challenge accepted. I'll use mri for now (I'll try with your versions later, to see if anything weird comes up).
ActionCable
$ rails _6.1.6.1_ new cable --skip-javascript
$ cd cable
# https://github.com/rails/importmap-rails#installation
$ bin/bundle add importmap-rails
$ bin/rails importmap:install
# ActionCable guide seems rather crusty. Until this section:
# https://guides.rubyonrails.org/v6.1/action_cable_overview.html#connect-consumer
# A generator for the client side js is mentioned in the code comment.
$ bin/rails g channel chat
# Oops, generated server side rb as well.
# This should really be at the start of the guide.
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
# NOTE: just keep it simple
stream_from "some_channel"
end
end
The directory app/javascript seems generic, because it is. This is for all Javascript stuff, used by shakapacker, jsbundling-rails, importmap-rails and others. I've described it a bit here: https://stackoverflow.com/a/73174481/207090
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("ChatChannel", {
connected() {
// NOTE: We have to check if our set up is online first,
// before chatting and authenticating or anything else.
console.log("ChatChannel connected")
},
disconnected() {},
received(data) {}
});
To broadcast a message, call this somewhere in your app [sic]:
ActionCable.server.broadcast("some_channel", "some message")
Ok, we have to make a controller anyway:
$ bin/rails g scaffold Message content
$ bin/rails db:migrate
$ open http://localhost:3000/messages
$ bin/rails s
Also, channels have to be imported somewhere, to load on the page. javascript_importmap_tags in the layout only imports application:
https://github.com/rails/importmap-rails#usage
<script type="module">import "application"</script>
<!-- ^ -->
<!-- this imports the pinned `application` -->
Makes sense to import channels in application.js. Can't import ./channels/index because it has require. We'd have to use node for it to work or do something else to import all the channels. Manual way is the simplest:
// app/javascript/channels/index.js
// NOTE: it works a little differently with importmaps that I haven't mentioned yet.
// skip this index file for now, and import channels in application.js
// app/javascript/application.js
import "./channels/chat_channel"
Browser console shows missing #rails/actioncable. Nobody told me to install it yet. Use pin command to add it:
https://github.com/rails/importmap-rails#using-npm-packages-via-javascript-cdns
$ bin/importmap pin #rails/actioncable
Refresh the browser:
ChatChannel connected chat_channel:5
We got our javascript on the page. Let's make it broadcast:
# app/controllers/messages_controller.rb
# POST /messages
def create
#message = Message.create(message_params)
ActionCable.server.broadcast("some_channel", #message.content)
end
<!-- app/views/messages/index.html.erb -->
<div id="chat"></div> <!-- output for broadcasted messages -->
<!-- since I have no rails ujs, for my purposes: bushcraft { remote: true } -->
<!-- v -->
<%= form_with model: Message.new, html: { onsubmit: "remote(event, this)" } do |f| %>
<%= f.text_field :content %>
<%= f.submit %>
<% end %>
<script type="text/javascript">
// you can skip this, I assume you have `rails_ujs` installed or `turbo`.
// { remote: true } or { local: false } is all you need on the form.
function remote(e, form) {
e.preventDefault();
fetch(form.action, {method: form.method, body: new FormData(form)})
form["message[content]"].value = ""
}
</script>
We know we're connected. The form is submitting to MessagesController#create without refreshing where we're broadcasting to "some_channel". All that's left is to do is output data on the page:
https://guides.rubyonrails.org/v6.1/action_cable_overview.html#client-server-interactions-subscriptions
// app/javascript/channels/chat_channel.js
// update received() function
received(data) {
document.querySelector("#chat")
.insertAdjacentHTML("beforeend", `<p>${data}</p>`)
}
ActionCable done. Now let's fix importmaps.
Importmaps
Something I didn't mention before and it is super important to understand.
Everything works, but, only in development, I explained why here:
https://stackoverflow.com/a/73136675/207090
URLs and relative or absolute paths will not be mapped and more importantly will bypass the asset pipeline sprockets. To actually use importmaps, all the files in app/javascript/channels have to be mapped, aka pinned, and then referred to only by the pinned name when importing.
# config/importmap.rb
# NOTE: luckily there is a command to help with bulk pins
pin_all_from "app/javascript/channels", under: "channels"
pin "application", preload: true
pin "#rails/actioncable", to: "https://ga.jspm.io/npm:#rails/actioncable#7.0.3-1/app/assets/javascripts/actioncable.esm.js"
# NOTE: the big reveal -> follow me ->------------------------------------------------------------------^^^^^^^^^^^^^^^^^^
# NOTE: this only works in rails 7+
# pin "#rails/actioncable", to: "actioncable.esm.js"
# `actioncable.esm.js` is in the asset pipeline so to speak and can be found here:
# https://github.com/rails/rails/tree/v7.0.3.1/actioncable/app/assets/javascripts
For some info on pin and pin_all_from:
https://stackoverflow.com/a/72855705/207090
You can see the importmaps this creates in the browser or in the terminal:
$ bin/importmap json
{
"imports": {
"application": "/assets/application-3ac17ae8a9bbfcdc9571d7ffac88746f5a76b18c149fdaf02fa7ed721b3e7c49.js",
"#rails/actioncable": "https://ga.jspm.io/npm:#rails/actioncable#7.0.3-1/app/assets/javascripts/actioncable.esm.js",
"channels": "/assets/channels/index-78e712d4a980790be34a2e859a2bd9a1121f9f3b508bd3f7de89889ff75828a0.js",
"channels/chat_channel": "/assets/channels/chat_channel-0a2f983da2629a4d7edef5b7f05a494670df3f99ec6a22a2e2fee91a5d1c1d05.js",
"channels/consumer": "/assets/channels/consumer-b0ce945e7ae055dba9cceb062a47080dd9c7794a600762c19d38dbde3ba8ff0d.js"
}# ^ ^
} # | |
# names you use urls browser uses
# | to import ^ to actually get it
# | |
# `---> importmaped to ----'
For importmaps info (not the importmap-rails gem):
https://github.com/WICG/import-maps
Importmaps do not import anything, they map name to url. If you make name look like url with /name, ./name, ../name, http://js.cdn/name there is nothing to map.
import "channels/chat_channel"
// stays unchanged and is now the same as
import "/assets/channels/chat_channel-0a2f983da2629a4d7edef5b7f05a494670df3f99ec6a22a2e2fee91a5d1c1d05.js"
// because we have an importmap for "channels/chat_channel"
You don't want to use the second form with an absolute path in you js files, because the digest hash changes on file updates to invalidate the browser cache (this is handled by sprockets).
Convert all the imports:
import consumer from "./consumer"
import "./channels/chat_channel"
to match the pinned names:
import consumer from "channels/consumer"
import "channels/chat_channel"
// import "channels" // is mapped to `channels/index`
// TODO: want to auto import channels in index file?
// just get all the pins named *_channel and import them,
// like stumulus-loading does for controllers:
// https://github.com/hotwired/stimulus-rails/blob/v1.1.0/app/assets/javascripts/stimulus-loading.js#L8
Jruby
Same setup on jruby. I just installed it and updated my Gemfile:
# Gemfile
ruby "2.5.7", engine: "jruby", engine_version: "9.2.16.0"
gem "activerecord-jdbcsqlite3-adapter"
gem "importmap-rails", "< 0.8" # after version 0.8.0 ruby >= 2.7 is required
First error, when starting the server:
NoMethodError: private method `importmap=' called for #<Cable::Application:0x496a31da>
importmap= method is defined here:
https://github.com/rails/importmap-rails/blob/v0.7.6/lib/importmap/engine.rb#L4
Rails::Application.send(:attr_accessor, :importmap)
In jruby it defines private methods when used this way:
>> A = Class.new
>> A.attr_accessor(:m)
>> A.new.m
NoMethodError (private method 'm' called for #<A:0x5f2f577>)
The fix is to override the definition in you app, or make those methods public:
# config/application.rb
module Cable
class Application < Rails::Application
# make them public
public :importmap, :importmap=
config.load_defaults 6.1
end
end
That's it. No other issues. You should expect some set backs anyway, because you're using jruby which is quite behind the mri. Ruby 2.5 EOL'd on Apr 05, 2021. You can't expect latest gems to play nice with old ruby versions.

Vue.js app works in development but not mounting template in production with Rails 5.2.0 / Webpacker - blank screen with no errors in console

I'm using Rails 5.2.0 and Webpacker gem to deploy a Vue application.
The show.html.erb file is very simple:
<div data-behavior="vue-app"><MyComponent></MyComponent></div>
And then in my entry pack, packs/my_vue_app.js:
import TurbolinksAdapter from 'vue-turbolinks';
import Vue from 'vue/dist/vue.esm'
Vue.use(TurbolinksAdapter);
import MyComponent from '../components/my_app/index.vue'
document.addEventListener('turbolinks:load', () => {
var element = $('[data-behavior="vue-app"]');
if(!element) { return };
console.log('Initializing Vue');
const app = new Vue({
el: element[0],
data: {
},
components: { MyComponent }
})
})
In development, everything works absolutely fine. The app is mounted and functional.
But in production, after the page load and JS runs, <div data-behavior="vue-app"> is removed from the paging, leaving only <!-- --> in it's place.
In the console, there are absolutely no errors. I can confirm using DevTools that the pack js file is loaded, and it was parsed, since the console.log is printed in the console.
Heck, the proof that Vue is working is that the entire <div> where it was mounted was removed from DOM after JS parsing.
The weirdest thing of all is that I could get the app to mount ONCE, by attaching a debugger on the console.log line and turning it off while the debugger paused execution. Even tough I saw the app mounting that time, I could not get it to mount later on, even fiddling with the debugger again ... it's really, really weird.
These are the versions of package.json:
"vue": "^2.5.16",
"vue-loader": "14.2.2",
"vue-template-compiler": "^2.5.16",
The Rails app is brand new, with no config other than the default.
Webpacker gem is 3.5.3 and Rails is 5.2.0.
After spending a really long time on this, I only found this github issue: https://github.com/rails/webpacker/issues/1520
EDIT: I'm providing a link to the real, production app where this bug is happening: https://planilha.tramitacaointeligente.com.br/planilhas/ED2sUXz32-R9CJKdkmtf8Q
You'll see it's not mounting. Here's the same page in development:
I eventually managed to solve it by changing how the Vue app loading was defined.
Try import Vue from 'vue' (instead of from 'vue/dist/vue.esm') and then:
const app = new Vue({
el: domElement,
render: h => h(RootComponent)
})
The comments that appear in the hello_vue.js scaffold from the Webpacker gem tell you that you can choose between using the DOM as your template OR load the component with a render function; they both do work in development, but only the latter (loading the component with a render function, using vue instead of vue/dist/vue.esm and render: h => h(RootComponent) worked for me in production.
This has been, by far, the longest, most frustrating debugging session of my life, since there are absolutely no errors in console, you just stare into a blank screen, and Vue is running since it removes the DOM element it was mounted to from the DOM.
Source of solution: https://stackoverflow.com/a/48651338/1290457 and here's the github issue (currently open) on Webpacker gem https://github.com/rails/webpacker/issues/1520
I still don't know how to use DOM as template with Vue in production tough.
I had similar problem in Rails 5.2 + webpack Vue. All was good in development, but not working in production. After hours of investigating I found the reason. It was in this recommendation from webpaker gem docs.
Adding this
Rails.application.config.content_security_policy do |policy|
if Rails.env.development?
policy.script_src :self, :https, :unsafe_eval
else
policy.script_src :self, :https
end
end
brokes production. Removing else part - fixing the situation.
Chrome silently ignoring this. Firefox shows warnings.

JavaScript won't work in Heroku deployment but works locally

I wrote this function in app/assets/javascripts/expand.js
$( document ).ready(function() {
var open = $('.toggle-expand'),
a = $('ul').find('a');
open.click(function(e){
e.preventDefault();
var $this = $(this),
speed = 500;
if($this.hasClass('active') === true) {
$this.removeClass('active').next('.expandable').slideUp(speed);
} else if(a.hasClass('active') === false) {
$this.addClass('active').next('.expandable').slideDown(speed);
} else {
a.removeClass('active').next('.expandable').slideUp(speed);
$this.addClass('active').next('.expandable').delay(speed).slideDown(speed);
}
});
});
The script works fine locally and behaves as expected, but when I deploy to Heroku it stops working entirely.
for reference, here is the HTML that the script is acting on:
<ul>
<li>
<h2>text here</h2>
<div class="expandable">
...more stuff here
</div>
</li>
...more li tags here
</ul>
Debugging I have already done:
I've been able to replicate the problem in my development environment by changing a line in development.rb to config.assets.debug = false. I don't know enough about rails to gain any insight from that.
I precompiled all the assets with RAILS_ENV=production bundle exec rake assets:precompile. and pushed that up.
after that didn't work I tried precompiling again after a git rm -r public/assets/
When that did not work I then used the developer tools on chrome or firefox to look through all the uglified js being loaded in my production environment. I saw that my functions appear to have been loaded, but then I'm not seeing the functionality I expect so I don't know what's going on there either.
To see what I saw you can go to https://www.shopperbot.com/about and look at the resource https://www.shopperbot.com/assets/application-db55113ff25f4decb133ccb74aa98298822de2381cddc0ae3fa8c03b65180dd0.js It's a huge file so you will want to search for the word expandable. that word is unique to the function in question.
I am still very new to rails so I am not sure where to go from here. I could try changing the location of my js but I don't think that would be ideal. Does anyone have any suggestions?
Because this all works fine in my local environment... I suspect I'm making some beginner's mistake that can be fixed with some magic 1 liner out there that I have not yet found.
As Jan Klimo pointed out in a comment I had removed the google maps API and still had functions expecting the API to exist. Those errors caused my script to not function.
The solution was to remove all references to the removed API.

Karma + Rails: File structure?

When using the karma javascript test library (née Testacular) together with Rails, where should test files and mocked data go be placed?
It seems weird to have them in /assets/ because we don’t actually want to serve them to users. (But I guess if they are simply never precompiled, then that’s not an actual problem, right?)
Via this post: https://groups.google.com/forum/#!topic/angular/Mg8YjKWbEJ8
I'm experimenting with something that looks like this:
// list of files / patterns to load in the browser
files: [
'http://localhost:3000/assets/application.js',
'spec/javascripts/*_spec.coffee',
{
pattern: 'app/assets/javascripts/*.{js,coffee}',
watched: true,
included: false,
served: false
}
],
It watches app js files, but doesn't include them or serve them, instead including the application.js served by rails and sprockets.
I've also been fiddling with https://github.com/lucaong/sprockets-chain , but haven't found a way to use requirejs to include js files from within gems (such as jquery-rails or angularjs-rails).
We ended up putting tests and mocked data under the Rails app’s spec folder and configuring Karma to import them as well as our tested code from app/assets.
Works for us. Other thoughts are welcome.
Our config/karma.conf.js file:
basePath = '../';
files = [
JASMINE,
JASMINE_ADAPTER,
//libs
'vendor/assets/javascripts/angular/angular.js',
'vendor/assets/javascripts/angular/angular-*.js',
'vendor/assets/javascripts/jquery-1.9.1.min.js',
'vendor/assets/javascripts/underscore-min.js',
'vendor/assets/javascripts/angular-strap/angular-strap.min.js',
'vendor/assets/javascripts/angular-ui/angular-ui.js',
'vendor/assets/javascripts/angular-bootstrap/ui-bootstrap-0.2.0.min.js',
//our app!
'app/assets/javascripts/<our-mini-app>/**',
// and our tests
'spec/javascripts/<our-mini-app>/lib/angular/angular-mocks.js',
'spec/javascripts/<our-mini-app>/unit/*.coffee',
// mocked data
'spec/javascripts/<our-mini-app>/mocked-data/<data-file>.js.coffee',
];
autoWatch = true;
browsers = 'PhantomJS'.split(' ')
preprocessors = {
'**/*.coffee': 'coffee'
}
I found this project helpful as a starting point. https://github.com/monterail/rails-angular-karma-example. It is explained by the authors on their blog.
It's an example rails app with angular.js and karma test runner.

Conditional javascript require in the asset pipeline

I'm struggling with the asset pipeline. I'm loading dojo from Google CDN putting this in my template:
= javascript_include_tag 'http://ajax.googleapis.com/ajax/libs/dojo/1.6.1/dojo/dojo.xd.js', :'data-dojo-config' => %Q(dojoBlankHtmlUrl:'/blank.html', baseUrl: 'assets/', modulePaths: {custom: 'javascripts/modules'})
I just want a fallback to a local version if running locally or if the CDN is down. I thought of doing this:
script typeof(dojo) === "undefined" && document.write(unescape('%3Cscript src="js/libs/dojo-1.6.1.min.js"%3E%3C/script%3E'));
But I don't like it as it works out of the asset pipeline. I want to keep dojo in vendors/assets/javascripts/dojo. How can I get the fallback to be served by the asset pipeline.
Is there a way do declare conditional require in the asset pipeline. What I want is to run some javascript tests, and depending on the result serve a file.
Thanks
I suggest you use yepnope, a lightweight library for loading libraries like this in parallel (for speed) and it gives you the option to run some other code to test if the library is loaded. For example:
yepnope([{
load: 'http://ajax.googleapis.com/ajax/libs/dojo/1.6.1/dojo/dojo.xd.js',
complete: function () {
if (!window.jQuery) {
yepnope('asset_path('you_local_copy_of_dojo') ');
}
}
}])
(Note: You will need erb tags around the asset_path helper)
The local dojo file would be in the assets/javascript folder, but not included in the application manifest. You need to add the dojo file to the precompile array:
config.assets.precompile += 'your_local_file.js'
And this will make it available to the asset_path helper.
Thanks Richard!
I don't want to have yepnope to load one library. It would be overkill imo. Here is the solution I came up with, based on your help (written in slim):
1/ In vendors/assets/javascripts/, I have my dojo.js.
2/ In config/application.rb:
# Precompile these assets files
config.assets.precompile += ['dojo.js']
3/ In the template:
= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/dojo/#{Settings.dojoVersion}/dojo/dojo.xd.js", :'data-dojo-config' => %Q(dojoBlankHtmlUrl:'/blank.html', baseUrl: 'assets/', modulePaths: {custom: 'javascripts/modules'})
script = "typeof(dojo) === \"undefined\" && document.write(unescape('%3Cscript src=\"#{asset_path('dojo')}\"%3E%3C/script%3E'));".html_safe
I also posted on the Rails Google Group to request the addition of two options to the javascript_include_tag, :test and :local that would take care of all the work. We'll see.

Resources