A StimulusReflex Sandbox on Glitch
Tags: stimulusreflex, sandbox, rails
Published: 2021-05-25 18:53:54

Earlier this month, @hopsoft published a ~75LOC [single-file Rails app](https://github.com/hopsoft/sr_mini) that demonstrates a working [StimulusReflex](https://docs.stimulusreflex.com)-powered mini-app. It's an impressive feat of software engineering... but without a real-world use case, it's just a curiosity.

I was able to adapt [sr_mini](https://github.com/hopsoft/sr_mini) to work on Glitch, with the express goal of providing a way to embed a live StimulusReflex or CableReady app into a blog post. This gadget invites people to interact with the SR/CR stack and see the code behind working examples. Best of all, they won't have to worry about cloning a repo or security concerns.

{% glitch stimulus-reflex-sandbox app %}

This post first introduces [stimulus-reflex-sandbox](https://glitch.com/~stimulus-reflex-sandbox), then goes into specific detail on how it came together.

# Why make a StimulusReflex sandbox?

We've had a [StimulusReflex Expo](https://expo.stimulusreflex.com/) site for a few years, but it requires that people go to a different site. Often, it's not clear if people fully understand that they are using a fully server-rendered Rails app that is delivering updates via WebSockets. Many people who visit are not yet able to take the code snippets presented and immediately understand how it works or how they could use it themselves. Sandbox examples are great because they are minimalist and direct. It's easy to look at two examples and see what's different.

However, the main reason we're excited is that Glitch apps are "remixable". You can clone your own copy and immediately start experimenting with a feature or technique, live in the browser, without needing to even have Rails configured on your device. Moreover, you can do that without any chance of us exposing ourselves to security risks associated with the execution of arbitrary Ruby code. Everyone wins.

## What does the default sandbox show?

Out of the box, `stimulus-reflex-sandbox` implements a simple counter using the [declared Reflex syntax](https://docs.stimulusreflex.com/rtfm/reflexes#declaring-a-reflex-in-html-with-data-attributes) (eg. setting a data attribute on the button) and a [Page Morph](https://docs.stimulusreflex.com/rtfm/reflexes#there-are-three-kinds-of-reflex), which is the default operation mode in StimulusReflex. Every time you click, it runs the Reflex action method, re-runs the page's controller action, re-renders the page, broadcasts it to the client using CableReady and then the client updates only the parts of the DOM that have changed - in this case, the number you are incrementing.

You can learn more about all three Morph Modes in the documentation, or check out this handy [flowchart](https://lucid.app/lucidchart/e83d2cac-d2b1-4a05-8a2f-d55ea5e40bc9/view?page=0_0).

## How do I develop in the sandbox?

The app is configured in such a way that every change to a Ruby or Javascript file will trigger a restart, and every time you added a Ruby gem or Javascript package will trigger a re-install (see [below](#stitching-it-together)).

## What limitations are there?

Glitch is a JavaScript-first environment, so working with Ruby is not as smooth as it could be. In fact, we install Ruby 3 so that we're not stuck with Ruby 2.3!

There is no database enabled, but you do have Redis available.

# How `stimulus-reflex-sandbox` was made

Fitting the SR/CR + Rails + Redis stack into Glitch's 200MB space constraint was a major challenge. I had to do quite an elaborate limbo dance of installing, zipping, downloading, compiling, unzipping to stay below that limit.


### Install Ruby 3.0.1 with Rbenv

Check out Rbenv from Github and install ruby-build
```bash
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
$ mkdir -p "$(rbenv root)"/plugins
$ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
```

Install Ruby 3.0.1 and symlink it to `/app/bin`
```bash
$ rbenv install 3.0.1
$ ln -s /app/.rbenv/shims/* /app/bin
```

Remove `ri` completely since it takes up space and we don't need it. Then, create a tarball from `.rbenv` because we will need the disk space for the following steps.
```bash
$ rm -rf .rbenv/versions/3.0.1/share/ri/*
$ tar cvfz rbenv.tar.gz .rbenv
```

Configure rubygems not to load documentation to save space:
```bash
$ echo "gem: --no-document" > .gemrc
```


### Install Redis from Source
Follow [this guide](https://redis.io/download#from-source-code) to download and compile redis:

```bash
$ wget https://download.redis.io/releases/redis-6.2.2.tar.gz
$ tar xvfz redis-6.2.2.tar.gz
$ rm redis-6.2.2.tar.gz
$ cd redis-6.2.2 && make
```

Now move the redis server binary to the app's bin folder, then remove the source folder:
```bash
$ mv src/redis-server /app/bin
$ cd .. && rm -rf redis-6.2.2
```

Restore Ruby:
```bash
$ tar xvfz rbenv.tar.gz
```

That's about it. I did have to remove the `.cache`, `.bundle` and `.local` folders a few times when space ran out, though.

### Redis Configuration
For this very limited setup, we don't want Redis to dump its data when it's shut down. Also, we need to enfore an eviction policy, otherwise it's going to fill up our RAM quickly:

```
# .redis.conf
maxmemory 2mb
maxmemory-policy volatile-lru
save ""
appendonly no
```

### Stitching It Together
Being Javascript-first, we need to use a `package.json` to run our app. This, however, entails that `npm install` is run automatically and `node_modules` don't count towards the disk size limit. We use the [concurrently](https://www.npmjs.com/package/concurrently/) package to start both redis and the Rails app:

```json
{
  ...,
  "scripts": {
    "build": "snowpack build",
    "redis": "redis-server .redis.conf",
    "install": "rm -rf tmp .local .bundle && bundle install --no-cache && rm -rf .bundle",
    "start": "concurrently npm:redis \"npm:build && ruby application.rb $PORT\""
  },
  "engines": {
    "node": "14.x"
  },
  ...
}

```

The `watch.json` declares what when to trigger a re-install or restart:

```json
{
  "install": {
    "include": ["^Gemfile", "^package.json", "^\\.env$"],
    "exclude": []
  },
  "restart": {
    "include": ["\\.rb$", "\\.js$"],
    "exclude": ["^public/"]
  },
  "throttle": 10000
}
```