This project uses rbenv to manage its Ruby version.
bundle installto get the Ruby dependencies for this project
- If it's not already installed, you'll need Invoker:
gem install invoker
sudo invoker setup, which sets up a DNS resolver and firewall rule to allow Invoker to use custom domains and forward to ports 80 and 443.
invoker start config/invoker.inito start Rails and Webpack.
- Visit https://apmgo.dev and https://apmgojs.dev:9001/packs/app.js in your browser to verify that Rails and Webpack assets are being served. You will most likely need to add security exceptions for both domains to trust the SSL certificate.
- If you're having issues, restarting your machine and upgrading to a newer version of OS X are both possible solutions.
If you'd like to run a process outside of the Invoker stream (for instance, if you'd like to add a pry breakpoint in Rails and use the interactive REPL) you can comment out that application's entry in
config/invoker.ini. You would then start the app and allow Invoker to handle its network traffic using
add_http. For Rails, this would look like:
$ rails s -p 3001 $ invoker add_http rails 3001
npm run testto execute Jest tests
npm run test:watchto execute Jest tests in watch mode
npm run test:debugto execute Jest tests in debug mode (the process will wait for a debugger to attach before executing)
The API and WebSocket endpoints, along with a few other items, must be configured in
Rails configuration should also be modified to fit your needs, and copied from
Logo should be copied from
APMGo was designed to support OAuth2. A shim around your authentication solution that conforms to the following API implemented in
- Returns the user's display name
- Returns an OAuth2 token
- Returns true if the user is logged in, false otherwise
- Returns the URL that the user must visit to log in
- Returns the URL that the user must visit to log out
- Returns a promises that receives an OAuth2 token when fulfilled
Sample Client Implementation
In Rails development environment, you can visit
apmgo.dev/add_audio to see a sample implementation of what's needed to add audio to an APMGo playlist. The relevant source code for this implementation lives in
The template contains the schema.org data necessary for creating a playlist item. For more information, see
React components in this project will be written using the Redux approach of "Container" and "Presentational" components (also referred to as "Smart" and "Dumb"). Where possible, container and presentational components will be grouped together in the same file. Additionally, components will be grouped together into directories by domain.
Some syntax may appear unfamiliar. That's because this code is written using the static type checker Flow. Babel's flow preset is configured in
.babelrc-apm to strip out flow syntax during Webpack compilation.
Redux actions, reducers, and sagas will be grouped together in Ducks by use case. The app uses three ducks, located in the
The Audio Player duck lives in
audio-player.js. It manages state and provides action creators related to the audio player itself: which track is playing (just the id), how long the track is, the current playtime, volume, play/pause status, mute status etc.
The Data duck lives in
data.js. It handles things related to the api layer: when an item is deleted, or archived, or when the app first fetches a user's playlist. Because the Data duck handles all of the app's asynchronous actions, all of the app's Sagas are located here.
The Playlist duck lives in
playlist.js. It manages state and provides action creators related to the app's representation of the playlist and each item in it. For example, when an item plays for a second, the Playlist duck updates that specific item's
Ducks work together!
We take pains to separate concerns and avoid redundancy. But sometimes, ducks will have overlapping areas of interest. For example, when the
currentTime on a playing item changes (once per second, if it's playing), then the Playlist duck needs to update that playlist item, and the Audio Player duck needs to update its own
currentTime so it can properly track and display it. When an item is moved, the Playlist Duck reorders its own internal array, and the Data Duck sends out an API call to make sure the move is persisted.
Ducks get confused
It is sometimes difficult to determine which action belongs with which duck. When a user drags a
PlaylistItem, it needs to be moved within the redux state as it is dragged across each
ItemSlot in order to update the UI. The Playlist duck handles that. But when it is finally dropped in its new position, the Data duck fires an API call. Where should the
movePlaylistItem action live?
For now the operation is split into two actions:
movePlaylistItem, an imperative command to reorder the redux playlist, and
playlistItemMoved, an event signaling the action is complete and it's time to call the api. It does make sense, but it might make just as much sense the other way around, and it's not obvious which action does what just by reading the name. So, naming conventions could be improved for clarity.