Categories
Learning

React Native with Rails Turbo Streams

In preparation for a talk I would like to give at Desert Code Camp I am adding a React Native front end to my simple counting app built in rails. I would like to provide the same real-time capabilities in my React Native version as I have in my rails front-end. In order to do this, I need to find a way to open a web socket connection between my React Native app and my rails back end. I think the best way to approach this problem is to understand how the web socket connection in my rails front-end works and then see how I can port it over to React Native.

Let’s start by taking a look at the network tab when I load the page.

I see a websocket connection to ws://localhost:3000/cable

Now if I search (cmd-f) for “/cable” I see that this connection is setup by the javascript with the name starting with turbo.min

This javascript does not look like the easiest to understand just by looking at it (somewhat minified). So I will ask Leo for a simple set of code to open a websocket connection in javascript. Here’s what Leo gave me:

const webSocket = new WebSocket("ws://example.com/ws"); 
webSocket.connect(); 
webSocket.onmessage = (event) => { console.log("Received message:", event.data); };

Now my guess is that this will not work directly without some headers that authenticate the request, but I will give it a try. I create an html page with the above script, replacing example.com/ws with localhost:3000/cable and here’s what I get:

I’m not seeing the data I expect, so I need to dig further into what kind of websocket connection is being created by Turbo. After scanning through the turbo javascript a bit more, I notice that the websocket is opened with something called a “protocol.” This can also be seen on the working cable connection in the network tab looking at the headers:

The protocol in question is called actioncable-v1-json so I place that in my test html page and try again. Now my first line of script looks like this:

const webSocket = new WebSocket("ws://localhost:3000/cable", { protocol: 'actioncable-v1-json' });

Now when I open my test page, I get nothing. No websocket called cable at all. I look at my console and see this:

Uncaught DOMException: Failed to construct 'WebSocket': The subprotocol '[object Object]' is invalid.

I decide to search for the WebSocket documentation and learn that Leo has steered me wrong. According to MDN I need to provide the protocols as a string or array of strings, not an object with a property of protocol. So I change the line to this:

const webSocket = new WebSocket("ws://localhost:3000/cable", "actioncable-v1-json");

Now I’m seeing the websocket called cable again and I see the Sec-Websocket-Protocol header under Request headers in my network tab. But I’m still not seeing any data, and I notice in my console that I have two errors:

That first error makes me think AI steered me wrong again, so I look at the MDN documentation and indeed, there is no “connect” method. It looks like the WebSocket should automatically connect when I create it. So I remove that line. Ok, after refresh I just have the “WebSocket connection to … failed” error. I think I will need to provide some auth headers somehow. When I compare the working one to the broken one, the main header that stands out to me is “Cookie.” I’m going to try providing that header and see if it will work.

How do I add a header to my WebSocket? I no longer trust Leo on web sockets, so I just Google it with Bing on Brave like the good ol’ days. The second answer to this stackoverflow question gives me a way to make it happen. I try adding document.cookie = followed by the _counting_session cookie from my working page.

At first, it didn’t work. I did a few more searches and then decided to try something. I had been opening my test html file directly from my file system. What if the browser doesn’t send cookies that way? I don’t think it does. When I moved my file into the public folder of my rails app and then opened it from http://localhost:3000/testsocket.html the cookies were set properly and I was able to see ping messages.

What I didn’t see, however, was the data from my rails app. Comparing the working version to the broken one, I see that the working channel has a message sent that “subscribes” to the turbo stream.

So I replicate this message using this javascript:

webSocket.onopen = (event) => {
        console.log("Connected to server");

        webSocket.send(
          JSON.stringify({
            command: "subscribe",
            identifier: JSON.stringify({
              channel: "Turbo::StreamsChannel",
              signed_stream_name:
                "ImNvdW50ZXJzIg==--0ea83a6891780eeb16ca1638c48c642b78ef6b6a4ea3d1534902108f05af4006",
            }),
          })
        );
      };

Just in case you didn’t read that carefully, that script contains an identifier as stringified JSON wrapped inside stringified JSON sent to the websocket when the websocket opens.

The signed stream name is an element of complexity I expected as I had just read this very helpful tutorial on Turbo Streams and security.

Now that I get the information flowing, I notice one more thing. The information sent via this websocket is the HTML that turbo is sending to the page based on what changed.

While this works fine in my rails app, it won’t really be ideal for my React Native as it won’t have HTML at all.

In order to make this work on React Native, I will need to figure out the following:

  1. How to get the session cookie to authenticate the initial websocket call.
  2. How to get the signed stream name to subscribe to the Turbo Stream.
  3. How to use the information sent by rails to update my React Native app.

Here’s what I’m thinking I will end up with for each of these:

  1. Use a custom endpoint authenticated with a signed JWT issued at login to provide the session cookie.
  2. Use a custom endpoint authenticated with a signed JWT issued at login to provide the signed stream name.
  3. Create a custom ActionCable stream for the React Native front end that provides the information in JSON form.

Part 2 – Authenticating React Native to Rails

I did a little more testing on what I did yesterday, and it looks like the cookie is actually not necessary for opening the websocket to rails. The only reason it wasn’t working initially appears to be because I was running from an html file on my file system rather than running from a domain. It does not even have to be run from the same domain. I was able to host my html test file from localhost and open a websocket to a deployed instance of my app and it worked just fine without a cookie. This eliminates #1 of what I needed at the end of Part 1.

While I intended to support Google sign-in with my react native app (like I have with my rails app) after looking over the steps required I decided it was not worth my time just for a demonstration app. I will just use basic username/password auth.

I still need the signed stream name. In order to return this securely, I need to be able to login to my rails app from my React Native app. So I set up an authentication mechanism in this commit.

Part 3 – Setting up a stream of JSON

Next, I need my rails app to send JSON when the data relevant to my user changes. I will start by looking to see if I can use the built in turbo broadcast and just customize the format. I googled “customize turbo broadcast stream” and looked at the first thing the Brave search engine referenced, which was the Turbo Handbook. It contains a link to the Broadcastable concern in Turbo. Looking at that file, I see the example below:

    #   class Message < ApplicationRecord
    #     belongs_to :board
    #     broadcasts_to ->(message) { [ message.board, :messages ] }, partial: "messages/custom_message"
    #   end

I’m wondering if I can use a custom partial to return JSON instead of HTML.

I fire up my Visual Studio Code and start my react native app to reorient myself since it has been a few weeks. In order to avoid using my phone and the Expo app, I navigate to my counting-rn folder and use this command to install react-native-web so I can run the react app in my browser:

npx expo install react-native-web react-dom @expo/metro-runtime

Then I run npx expo start and hit W. My browser opens up to http://localhost:8081/ and I am able to see my login interface I created. I enter my credentials and hit Login. Then I realize that I have not started my rails app, so I open another terminal and run bin/dev from my root folder. I try the Login button again and realize that I need to use localhost as my base URL. So I change this in client.js. Then I try again. I get a CORS error because I’m now trying to use my browser. So I install the gem rack-cors in my bundle and add this block to my rails app in the config/environments/development.rb file:

  config.middleware.insert_before 0, Rack::Cors do
    allow do
      origins 'localhost:8081'
      resource(
        '*',
        headers: :any,
        methods: [:get, :patch, :put, :delete, :post, :options]
        )
    end
  end

Then I realize that the response from my rails app, hosted on localhost:3000 will not include cookies when the request is made from a browser that is on localhost:8081 anyway, so I give up on this attempt to use react-native-web and go back to opening Expo on my phone. I have to go back and restart my rails app using the command rails s -b 0.0.0.0 instead of bin/dev so that the port is open on my machine’s IP address.

I add the following to my react app to open the websocket:

let stream = '';
let webSocket;
export const openSocket = (streamName) => {
    if (!streamName) {
        streamName = stream;
    }
    else {
        stream = streamName;
    }
    if (webSocket) {
        webSocket.close();
    }
    webSocket = new WebSocket(`ws://${host}/cable`, [
        "actioncable-v1-json"
      ]);

      webSocket.onmessage = (event) => {
        console.log("Received message:", event.data);
      };

      webSocket.onopen = (event) => {
        console.log("Connected to server");

        webSocket.send(
          JSON.stringify({
            command: "subscribe",
            identifier: JSON.stringify({
              channel: "Turbo::StreamsChannel",
              signed_stream_name: streamName,
            }),
          })
        );
      };

      webSocket.onclose = (event) => {
        console.log("Disconnected from server");
      };

      webSocket.onerror = (event) => {
        console.error("Error:", event);
      };
}

The next step is to figure out how to get the signed stream name when I login. Turns out this is pretty simple. The following line of ruby gets the stream name:

signed_stream_name = Turbo::StreamsChannel.signed_stream_name(:counters)

Now, this is not currently secured to my user’s counters, but I will handle that later. The stream is now successfully showing messages in my react console whenever the data changes in my counters on my rails app. But it is still in HTML form. So I go back to that Broadcastable idea and try adding this to my model:

broadcasts_to ->(counter) { :counters }, partial: "counters/counter_data"

After experimenting for a while in my rails app, I realize that what I need is not to broadcast to a partial, but rather to broadcast with a template. So I add this to my model:


    after_update_commit :update_clients

    def update_clients
        broadcast_replace_to "counters_json", template: "counters/_counter_data"
    end

And I create a partial called _counter_data.erb with this:

<%= counter.to_json %>

Then I change my line that returns the stream name to say this:

signed_stream_name = Turbo::StreamsChannel.signed_stream_name("counters_json")

Now my react app shows this when I make a change to a counter:

 LOG  Received message: {"identifier":"{\"channel\":\"Turbo::StreamsChannel\",\"signed_stream_name\":\"ImNvdW50ZXJzX2pzb24i--ee00a02ef3249b8885a9aed19ab0d0a9e0efe5575503a20eded238025ddc2192\"}","message":"\u003cturbo-stream action=\"replace\" target=\"counter_6\"\u003e\u003ctemplate\u003e{\u0026quot;number\u0026quot;:7,\u0026quot;id\u0026quot;:6,\u0026quot;name\u0026quot;:\u0026quot;Test counter \u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2023-11-20T03:57:36.354Z\u0026quot;,\u0026quot;updated_at\u0026quot;:\u0026quot;2024-02-25T21:12:35.673Z\u0026quot;,\u0026quot;user_id\u0026quot;:1}\n\u003c/template\u003e\u003c/turbo-stream\u003e"}

It doesn’t look like it, but there is JSON in there (encoded and escaped). So I need to add some code in my react app to decode/unescape it.

let json = data.message.substring(data.message.indexOf("<template>")+10);
json = json.substring(0, json.indexOf("</template>"));
json = json.replace(/&quot;/g, '"');
json = JSON.parse(json);

Finally, I need to update my react component with the latest data from the JSON. So I add this snippet:

const newCounters = counters.map((counter) => {
  if (counter.id === json.id) {
    counter.number = json.number;
  }
  return counter;
});
setCounters(newCounters);

It works! Now whenever I change the counter on the rails app, the react app shows the change in real time. Here is the commit.

Categories
Learning

Bitwarden subdomains

Context

I use Bitwarden as my password manager. It is open source, battle tested, and secure. But I realized recently that it doesn’t recognize a subdomain as a separate website to which I can login. One of the applications I use daily has many instances that are hosted on subdomains and each instance has its own username and password. When I need to access an instance, I have to navigate to the URL and then manually select the right option from a long list in my Bitwarden plugin for Brave to autofill my username and password. This got me thinking – I wonder if Bitwarden has an option to handle this situation.

Search terms

bitwarden subdomain

Helpful link

https://bitwarden.com/help/article/uri-match-detection/

Outcome

Turns out the Bitwarden team has solved this with a feature. By changing my default URI match detection option to “Host” I get the behavior I was hoping for. The plugin matches the right instance by subdomain and provides me a quick way to fill in my username and password.

Categories
Learning

Working folder for npm scripts

Context

I am setting up some scripts for using terraform to deploy a node project to AWS and when I execute the commands from the root folder, they do not work. They must be executed from the folder that contains the .tf files. But I am not sure how to do this in my package.json file. The scripts property looks like this:

"scripts": {
        "start": "node server.js",
        ...
        "deploy": "terraform apply" <-- must be run from subfolder
}

Search terms

npm scripts working directory

Helpful link

https://stackoverflow.com/questions/30286498/change-working-directory-for-npm-scripts

Outcome

Thanks to the answer provided by eljefedelrodeodeljefe, I learned that you simply have to include “cd FOLDER_NAME &&” before your script and this works across most platforms. So in my case, this looks like the following:

"scripts": {
        "start": "node server.js",
        ...
        "deploy": "cd infrastructure && terraform apply" <-- must be run from subfolder
}

One nice thing is that this approach is consistent with the syntax used in a Makefile for a Python project I have recently worked with.

Categories
Learning

Async Python Handlers in AWS Lambda

Context

I am creating my first AWS Lambda function in Python from scratch. My function contains async functions, so I declared my handler with the async keyword. But when I deployed and smoke tested in AWS, I received the following error:

{
"errorMessage": "Unable to marshal response: Object of type coroutine is not JSON serializable",
"errorType": "Runtime.MarshalError"
}

Search terms

aws lambda python async handler

Helpful link

https://stackoverflow.com/questions/60455830/can-you-have-an-async-handler-in-lambda-python-3-6

Outcome

Apparently AWS Lambda does not support async handler functions in Python. So you are required to call your asynchronous code from a synchronous handler function using this line of code:

asyncio.get_event_loop().run_until_complete(your_async_handler())

Categories
Learning

Google Calendar Drag and Drop

Context

My dad has a new phone since we recently switched back to Total Wireless from Sprint. It is another Android phone so most things are the same but there is one feature missing. He cannot drag and drop appointments on his Google calendar to different days or times.

Search terms

Google calendar app drag items

Helpful link

https://support.google.com/calendar/thread/850605?hl=en

Outcome

It turns out that disabling an accessibility feature called “Select to speak” resolved this issue.

Categories
Learning

Cholesterol Blood Testing

Context

My doctor informed me at my last annual physical that my good cholesterol is too low and my bad and total cholesterol are too high. I knew this from past tests but he urged me to take action to improve my levels. I also know that this is somewhat genetic because my vegetarian marathon running Dad also has the same problem.

My doctor recommended exercise and dietary changes. Though I have not increased my physical activity as much as I should, I did improve my diet. Based on a suggestion from a friend at church and with my doctor’s support, I also agreed to try a supplement called red yeast rice that has similar effects as prescription statins to see if that would help. This was about six months ago and I’m wondering how often it makes sense to test cholesterol levels in my blood.

Search terms

frequency of cholesterol blood tests

how long does it take for cholesterol to change

Helpful link

https://www.healthline.com/health/high-cholesterol/how-long-does-it-take-to-lower

Outcome

My first search only returned articles recommending testing every five years or in certain cases annually, or as prescribed by a doctor. But I wanted to know how often I can test to see improvement based on dietary and supplement changes. So I was more specific in my second search. The answer was in line with my doctor’s suggestion.

I learned that taking this test as often as three to six months makes sense to see if changes have had the desired effect, so I have a blood test scheduled for this week.

I also learned that you can order this kind of routine blood test directly from companies like Sonora Quest without having to go through your physician or insurance and it is very affordable ($28).

Categories
Learning

Constructing Javascript Objects

Context

I am building a simple model class in Typescript/Javascript and was wondering if there was a shorthand way to avoid having to explicitly set each property name based on the constructor arguments.

Search terms

javascript constructor properties shorthand

Helpful link

https://maksimivanov.com/posts/typescript-constructor-shorthand/

Outcome

I found exactly what I was looking for. This is a Typescript feature only, so it wouldn’t work in pure Javascript, but it is much easier to type:

class Something {
  constructor(public prop1: string, public prop2: string) {}
}

than to type:

class Something {
  public prop1: string;
  public prop2: string;
  constructor(prop1: string, prop2: string) {
    this.prop1 = prop1;
    this.prop2 = prop2;
  }
}

Both produce the same Javascript in the end.

Categories
Learning

Creating Subfolders

Context

I need to provide instructions in a project on which folders and subfolders need to exist before the code will run locally. The required folder is three levels deep, and I want a quick way to create the full directory tree.

Search terms

mkdir subfolders

Helpful link

https://stackoverflow.com/questions/9242163/bash-mkdir-and-subfolders

Outcome

I learned that the mkdir command in a bash terminal includes option “-p” to create all necessary parent folders for a full path in one command.

Categories
Learning

Table metadata in Oracle

Context

Normally I work in SQL Server when I am dealing with relational data but on my current project one of our primary systems uses Oracle as its back end. In order to quickly generate a list of column names for use in an access request, I need to interrogate the table metadata and I cannot recall the way to do this on Oracle.

Search terms

oracle column list query

Helpful link

https://dataedo.com/kb/query/oracle/list-columns-names-in-specific-table

Outcome

I was reminded to use sys.all_tab_columns to get this information.

Categories
Learning

Multidomain SSL Certificates

Context

As I relaunch my website and blog, I want to make it secure. But I host two different domains on this same site (with a host-based redirect), so I wanted to make sure I could secure both of them. My host’s support team told me I would need to use their “Certificate Signing Request” (CSR) to install an SSL cert but this requires a single common name for the domain name. My question was – how do I generate a CSR for multiple domains?

Search terms

csr for multidomain certificate

Helpful link

https://www.namecheap.com/support/knowledgebase/article.aspx/9840/67/how-do-i-activate-a-multidomain-ssl-certificate

Outcome

According to this article, I can use my primary domain in the CSR and add additional domains (SANs) when activating the certificate.