<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Barstool Engineering]]></title><description><![CDATA[Redefining the Barstool Difference.]]></description><link>https://barstool.engineering/</link><image><url>https://barstool.engineering/favicon.png</url><title>Barstool Engineering</title><link>https://barstool.engineering/</link></image><generator>Ghost 4.21</generator><lastBuildDate>Thu, 16 Apr 2026 00:50:00 GMT</lastBuildDate><atom:link href="https://barstool.engineering/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Rust Wasm on Fastly Compute@Edge]]></title><description><![CDATA[<p>Recently I&apos;ve been playing with a lot of different versions of WebAssmbly using Rust. From <a href="https://github.com/leptos-rs/leptos">Leptos</a>, <a href="https://dioxuslabs.com">Dioxus</a>, and most recently by using <a href="https://developer.fastly.com/learning/compute/rust">Compute Edge</a> on Fastly. In this blog, I&apos;m going to build a simple api, and show how we can deploy it to Fastly using</p>]]></description><link>https://barstool.engineering/rust-wasm-on-fastly/</link><guid isPermaLink="false">63cd80324b8e43170fa8a2b6</guid><dc:creator><![CDATA[Jude Giordano]]></dc:creator><pubDate>Tue, 24 Jan 2023 00:12:24 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2023/01/fastly-rust.png" medium="image"/><content:encoded><![CDATA[<img src="https://barstool.engineering/content/images/2023/01/fastly-rust.png" alt="Rust Wasm on Fastly Compute@Edge"><p>Recently I&apos;ve been playing with a lot of different versions of WebAssmbly using Rust. From <a href="https://github.com/leptos-rs/leptos">Leptos</a>, <a href="https://dioxuslabs.com">Dioxus</a>, and most recently by using <a href="https://developer.fastly.com/learning/compute/rust">Compute Edge</a> on Fastly. In this blog, I&apos;m going to build a simple api, and show how we can deploy it to Fastly using their Compute cli.</p><p>To get started, just install the Fastly Compute SDK using the following <a href="https://developer.fastly.com/learning/compute">documentation</a>. After creating an account and api token, you&apos;ll have to run something to effect of:</p><pre><code>fastly profile create &lt;NAME&gt; --token=&lt;FASTLY_API_TOKEN&gt;</code></pre><p>Then, create a new Rust Compute service using:</p><pre><code>fastly compute init</code></pre><p>When prompted, simply choose <code>rust</code> as the language of choice. This will create a typical Rust binary project, with the Fastly crate included as a dependency, as well as a <code>Fastly.toml</code> file in the root for configuring the Compute settings.</p><p>Before we continue, let&apos;s do some simple optimizations. If you run:</p><pre><code>fastly compute build</code></pre><p>you will see Fastly creates a <code>bin/main.wasm</code>. If you check the file size using <code>ls -lh bin/main.wasm</code>, you will see that this file is around 2.5 mb. Let&apos;s improve that at the Rust release level. I&apos;m just going to update the <code>Cargo.toml</code> to the following:</p><pre><code class="language-toml">[profile.release]
opt-level = 3     # 0-3
strip = true      # strip symbols from binary
lto = true        # enable link time optimization
codegen-units = 1 # maximize size reduction optimizations</code></pre><p>NOTE: codegen-units can have some tradeoffs, most notably of which is increased compile time; it&apos;s up to you if you want to include it. Now, if we re-run <code>fastly compute build</code> and then <code>ls -lh bin/main.wasm</code>, we can see it has decreased to around 400 kb (this will vary by machine specs, of course). Awesome!</p><p>Debugging the application is as simple as running:</p><pre><code>fastly compute serve</code></pre><p>and hitting the url printed in your terminal, for me it&apos;s at <code>127.0.0.1:7676</code> . As you would expect, Fastly simply creates some bindings to our <code>src/main.rs</code> file using their crate (which I believe just has some convenience wrappings around the <a href="https://crates.io/crates/http">http</a> crate).</p><p>Easy! To publish our service to Fastly, all we have to do is run</p><pre><code>fastly compute deploy</code></pre><p>For a more involved example where I use Fastly&apos;s backends to serve a proxy of <a href="https://jsonplaceholder.typicode.com">jsonplaceholder</a>, check out this <a href="https://github.com/BarstoolSports/compute-rs-blog">repository</a> where I added the tokio async runtime, as well as whipped up some custom routing, tracing, and serialization.</p><p>Keep on rusting!</p>]]></content:encoded></item><item><title><![CDATA[Fast and Efficient AWS Lambdas Built With Rust]]></title><description><![CDATA[<hr><p>As Barstool&apos;s resident <a href="https://www.rust-lang.org">Rustacean</a>, it is my obligation to push for the use of Rust, and to lament about its elegance and power. When running services at scale, my favorite solution is just &apos;throw it in a lambda&apos;, and let AWS worry about handling request load.</p>]]></description><link>https://barstool.engineering/fast-and-efficient-aws-lambdas-built-with-rust/</link><guid isPermaLink="false">63ab6e384b8e43170fa8a0ea</guid><dc:creator><![CDATA[Jude Giordano]]></dc:creator><pubDate>Wed, 28 Dec 2022 18:22:19 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2022/12/ferris.png" medium="image"/><content:encoded><![CDATA[<hr><img src="https://barstool.engineering/content/images/2022/12/ferris.png" alt="Fast and Efficient AWS Lambdas Built With Rust"><p>As Barstool&apos;s resident <a href="https://www.rust-lang.org">Rustacean</a>, it is my obligation to push for the use of Rust, and to lament about its elegance and power. When running services at scale, my favorite solution is just &apos;throw it in a lambda&apos;, and let AWS worry about handling request load.</p><p>As for other ways to trim cost, milliseconds per request is a decent angle for Rust to come in and do some heavy lifting. Now AWS does in fact support Rust as a <a href="https://docs.aws.amazon.com/sdk-for-rust/latest/dg/lambda.html">lambda runtime</a>, for this example I&apos;ll be using the fantastic IAC framework: <a href="https://www.serverless.com">Serverless</a></p><p>More specifically, the <a href="https://github.com/softprops/serverless-rust">Serverless-Rust</a> plugin. To get started, simply run the <a href="https://github.com/rust-lang/cargo">cargo </a>command <code>cargo init aws-example</code> to create a new Rust project.</p><figure class="kg-card kg-image-card"><img src="https://barstool.engineering/content/images/2022/12/image.png" class="kg-image" alt="Fast and Efficient AWS Lambdas Built With Rust" loading="lazy" width="707" height="295" srcset="https://barstool.engineering/content/images/size/w600/2022/12/image.png 600w, https://barstool.engineering/content/images/2022/12/image.png 707w"></figure><p>In the <code>Cargo.toml</code>, we are going to add the following new dependencies: </p><!--kg-card-begin: markdown--><pre><code class="language-toml">[dependencies]
lambda_http = &quot;0.7.2&quot;
tokio = { version = &quot;1.22.0&quot;, features = [&quot;macros&quot;, &quot;rt-multi-thread&quot;] }
serde = { version = &quot;1.0.151&quot;, features = [&quot;derive&quot;] }
serde_json = &quot;1.0.91&quot;
</code></pre>
<!--kg-card-end: markdown--><p>If you use Rust, you are likely familiar with most of these dependencies, save for<br><a href="https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/lambda-http">Lambda Http</a>, which adds types and helpers to tranform AWS request events. We will also need to run <code>npm init -y</code> in the root of the project, and use the following <code>package.json</code></p><!--kg-card-begin: markdown--><pre><code class="language-json">{
  &quot;name&quot;: &quot;aws-example&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;dependencies&quot;: {
    &quot;serverless&quot;: &quot;^3.24.0&quot;,
    &quot;serverless-rust&quot;: &quot;^0.3.8&quot;
  }
}
</code></pre>
<!--kg-card-end: markdown--><p>As you can see, we only need the <a href="https://github.com/serverless/serverless">Serverless </a> and <a href="https://github.com/softprops/serverless-rust">Serverless Rust</a> packages as dev-dependencies. </p><p>We&apos;ll also add a <code>serverless.yml</code> file in the root of the project, and paste the following in:</p><!--kg-card-begin: markdown--><pre><code class="language-yml">service: aws-example

frameworkVersion: &quot;3&quot;

provider:
  name: aws
  runtime: rust
  region: us-east-1
  versionFunctions: false
  memorySize: 2048
  timeout: 30

plugins:
  - serverless-rust

package:
  individually: true

custom:
  rust:
    target: x86_64-unknown-linux-musl
    linker: clang
    dockerless: true

</code></pre>
<!--kg-card-end: markdown--><p>I won&apos;t go over every line of the <code>custom</code> section, but you can read all about the plugin <a href="https://github.com/softprops/serverless-rust#-experimental-local-builds">here</a>.</p><p>In our project <code>src</code>, create a single rust binary called <code>bin/ping.rs</code> (or whatever you want). Serverless can point to individual rust binaries and deploy them as lambdas that all scale independently. (you can also delete <code>src/main.rs</code>) Our structure now looks like this:</p><figure class="kg-card kg-image-card"><img src="https://barstool.engineering/content/images/2022/12/image-1.png" class="kg-image" alt="Fast and Efficient AWS Lambdas Built With Rust" loading="lazy" width="604" height="383" srcset="https://barstool.engineering/content/images/size/w600/2022/12/image-1.png 600w, https://barstool.engineering/content/images/2022/12/image-1.png 604w"></figure><p>The actual code is very straightforward: we simply make each binary entry-point async using the Tokio macro, and return a response using the Lambda-Http Rust package. For example:</p><!--kg-card-begin: markdown--><pre><code class="language-typescript">use lambda_http::{
    aws_lambda_events::serde_json::json, http::StatusCode, run, service_fn, Error, IntoResponse,
    Request, Response,
};

pub async fn ping(_event: Request) -&gt; Result&lt;impl IntoResponse, Error&gt; {
    let body = json!({ &quot;message&quot;: &quot;sup&quot; }).to_string();
    let response = Response::builder()
        .status(StatusCode::OK)
        .header(&quot;Content-Type&quot;, &quot;application/json&quot;)
        .body(body)
        .map_err(Box::new)?;
    Ok(response)
}

#[tokio::main]
async fn main() -&gt; Result&lt;(), Error&gt; {
    run(service_fn(ping)).await
}

</code></pre>
<!--kg-card-end: markdown--><p>You could, of course, make your own structs that have a trait implementation &#xA0;of stringifying <code>&amp;self</code>. </p><p>Finally, all we have to do to invoke our binary is add it to our Serverless definition with whatever event we like. The syntax is <code>application-name.binary-name</code></p><!--kg-card-begin: markdown--><pre><code class="language-yml">functions:
  ping:
    handler: aws-example.ping
    # function url
    url: true
    events:
    # v1 REST Api
    - http:
        method: GET
        path: /api/v1/ping
    # v2 HTTP Api
    - httpApi:
        method: GET
        path: /api/v2/ping
</code></pre>
<!--kg-card-end: markdown--><p>I&apos;ve outlined different ways for our lambda to be invoked: <a href="https://www.serverless.com/framework/docs/providers/aws/events/apigateway">v1 Rest Api</a>, <a href="https://www.serverless.com/framework/docs/providers/aws/events/http-api">v2 HTTP Api</a>, and as a <a href="https://www.serverless.com/framework/docs/providers/aws/guide/functions#lambda-function-urls">Function Url</a>. The preference is simply up to you!</p><p>Finally, all that&apos;s left is to deploy and invoke our lambda. We can do this through the Serverless cli using <code>npx serverless deploy --stage prod</code> and then hitting one of our three endpoints.</p><figure class="kg-card kg-image-card"><img src="https://barstool.engineering/content/images/2022/12/image-3.png" class="kg-image" alt="Fast and Efficient AWS Lambdas Built With Rust" loading="lazy" width="1018" height="547" srcset="https://barstool.engineering/content/images/size/w600/2022/12/image-3.png 600w, https://barstool.engineering/content/images/size/w1000/2022/12/image-3.png 1000w, https://barstool.engineering/content/images/2022/12/image-3.png 1018w" sizes="(min-width: 720px) 720px"></figure><p>Ta-da! A super fast, scalable, and very resource-efficient service using rust on AWS : )</p>]]></content:encoded></item><item><title><![CDATA[A Real World Comparison between Cloudflare Workers and Fastly Compute@Edge]]></title><description><![CDATA[An end-to-end comparison of the two hottest edge computing platforms.]]></description><link>https://barstool.engineering/a-real-world-comparison-between-cloudflare-workers-and-fastly-compute-edge/</link><guid isPermaLink="false">61bb85f34b8e43170fa89b1f</guid><category><![CDATA[serverless]]></category><category><![CDATA[fastly]]></category><category><![CDATA[cloudflare]]></category><category><![CDATA[aws]]></category><category><![CDATA[lambda]]></category><category><![CDATA[streams]]></category><dc:creator><![CDATA[Andrew Barba]]></dc:creator><pubDate>Fri, 17 Dec 2021 19:02:21 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2021/12/70abb87e63d4.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://barstool.engineering/content/images/2021/12/70abb87e63d4.jpeg" alt="A Real World Comparison between Cloudflare Workers and Fastly Compute@Edge"><p>Over the past couple months the race to build the best edge compute platform has really heated up. We&apos;re seeing companies like Cloudflare, Fastly, AWS and Fly building compelling platforms to run code as close to your users as possible. Gone are the days of single compute instances handling many requests, we&apos;re entering a new era of compute where each request gets its own isolated container and the ability to scale to thousands, even millions, of requests per second. </p><p>Although there are many comparisons still to be done between all platforms, I want to take this opportunity to focus on Cloudflare and Fastly as the two companies have been going <a href="https://blog.cloudflare.com/network-performance-update-full-stack-week/">back</a> and <a href="https://www.fastly.com/blog/debunking-cloudflares-recent-performance-tests">forth</a> in what I would consider a largely meaningless feud. The saga began with Cloudflare testing their JavaScript runtime against Fastly&apos;s JavaScript runtime (still in beta) in a basic hello world test. The test was simple: how fast can each runtime return a hello world response. In this case &quot;hello world&quot; simply meant replying with a JSON response of the current request headers. If you&apos;re just dying to know how fast each platform could return such a response, let me spoil it for you: <em>really fucking fast</em>!</p><p>Both platforms were returning this response in under 100ms up to the 90th percentile. I don&apos;t know about you, but we dont have many hello world endpoints in production so this wasn&apos;t exactly going to sell us in either direction. What we wanted was a more robust example of moving a traditional server workload to the edge, and thats what I plan to show you today. Importantly, I think a comparison between any of these platforms needs to go well beyond just time-to-first-byte. We care a lot more about developer experience, framework support, CI/CD, and the other things that make development teams happy and more efficient in their everyday work. My goal is to give a comprehensive overview of the following:</p><ol><li>TypeScript Language Support</li><li>JavaScript Platform APIs</li><li>Deploying with GitHub Actions</li><li>Performance Comparison</li><li>Platform Limitations</li></ol><p>Before we get into our comparisons, it&apos;s important to understand the production workload that I&apos;ve re-written for each platform. Internally we call this product Pipe-Stream: it&apos;s goal is to take a list of MP3 files and stream them together, in order, as a single MP3 file. We use this technology for quite a few different things - but one of the obvious benefits is to swap out ad reads dynamically as we get new ad partners throughout the year. Our Podcast API pre-chunks our mp3 files into segments so they are 100% ready to be combined into a single stream. Pipe-Stream does not know anything about the mp3 spec, it&apos;s job is to simply concatenate the segments into one streaming response. And the &quot;streaming&quot; aspect of this service is really important. We have some MP3 files over 1GB in size and thus we do not want to pull all that data into memory. Streaming should be completely pass-through so we can optimize time-to-first-byte as well as runtime memory.</p><h2 id="typescript-language-support">TypeScript Language Support</h2><p>I&apos;m happy to report that both platforms have excellent support for developing your application in TypeScript. Each platform provides first-class types for their platform API&apos;s, making it dead simple to ensure your code is always using their API&apos;s correctly. Each platform compiles your TypeScript using webpack, and the webpack config files are nearly identical between the platforms. As of writing, Cloudflare gets the slight edge in getting started as they provide a <a href="https://github.com/cloudflare/worker-typescript-template">one-line-command</a> to create a new TypeScript workers project. Fastly provides a similar <a href="https://developer.fastly.com/solutions/starters/compute-starter-kit-javascript-default/">one-line-command</a> for a JavaScript project, but it&apos;s up to you to figure out how to add webpack and get it to build. Hint: copy the Cloudflare webpack file and dependencies.</p><h2 id="javascript-platform-apis">JavaScript Platform APIs</h2><p>Okay so the TypeScript support is nearly identical between the platforms, but what can we actually do in TypeScript (compiled to WASM) on each platform? There are some critical components coming from our production code running on Node.js that need to be available in each runtime:</p><ol><li>Readable Streams</li><li>Writable / Transform Streams</li><li>HTTP Requests with Streaming Bodies</li></ol><h3 id="readable-streams">Readable Streams</h3><p>A readable stream is perhaps the most important aspect of this entire project. Our goal is to stream each MP3 segment from the source (in our case Amazon S3) to the client. We <em>must</em> avoid reading the entire file into memory and instead stream the response directly to the client. This can only be achieved if the platform supports the concept of a readable stream. In Node.js this looks like:</p><pre><code class="language-typescript">import { Readable as ReadableStream } from &apos;stream&apos;</code></pre><p>In both Fastly Compute@Edge and Cloudflare Workers <code>ReadableStream</code> is a global class, so no need to import it to use it. Their implementations of <code>ReadableStream</code> follow the same spec as the <a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream">Web API</a>. This conformance to the Web API is a common theme that you will see throughout this post. Each platform makes a strong effort to conform to the Web API as much as possible, but each has their own differences and trade-offs which we will cover in another section.</p><h3 id="writable-transform-streams">Writable / Transform Streams</h3><p>In order to successfully combine multiple readable streams into a single destination stream, we must pipe the streams through what&apos;s known as a <code>TransformStream</code>. A <code>TransformStream</code> is once again part of the Web API and it provides both a writable and readable stream. You can write data to the writable stream and that data is made available on the readable stream. In Node.js this looks like:</p><pre><code class="language-typescript">import { Transform, Readable, Writable } from &apos;stream&apos;

export function combineStreams(streams: Readable[]): Readable {
  const stream = new Transform()
  _combineStreams(streams, stream).catch((err) =&gt; stream.destroy(err))
  return stream
}

async function _combineStreams(sources: Readable[], destination: Writable) {
  for (const stream of sources) {
    await new Promise((resolve, reject) =&gt; {
      stream.pipe(destination, { end: false })
      stream.on(&apos;end&apos;, resolve)
      stream.on(&apos;error&apos;, reject)
    })
  }
  destination.end()
}</code></pre><p>This block of code is doing the following:</p><ol><li>A function called <code>combineStreams</code> accepts an array of readable streams to stitch together</li><li>It creates a new <code>TransformStream</code> </li><li>Loops through each readable stream and pipes it to the transform stream</li><li>Prevents closing the transform stream during each pipe call</li><li>Closes the transform stream once all streams have been combined</li><li>Returns the transform stream synchronously so it can be used by the caller</li></ol><p>Node&apos;s <code>TransformStream</code> differs from the Web API in one key way - it is both a Readable and Writable stream and thus can be returned as either without the caller knowing it is one or the other. The Web API provides a slightly different spec where a <code>TransformStream</code> is actually not a stream at all, but a class the exposes both <code>readable</code> and <code>writable</code> streams as properties of the class.</p><p>Here is the exact Cloudflare implementation of the previous Node.js code:</p><pre><code class="language-typescript">export function combineStreams(streams: ReadableStream[]): ReadableStream {
  const stream = new TransformStream()
  _combineStreams(streams, stream.writable)
  return stream.readable
}

async function _combineStreams(sources: ReadableStream[], destination: WritableStream) {
  for (const stream of sources) {
    await stream.pipeTo(destination, {
      preventClose: true
    })
  }
  destination.close()
}</code></pre><p>Amazingly: this is less lines of codes than the Node.js implementation and even comes with an async version of <code>Stream.pipe</code>, greatly cleaning up our code. So how does Fastly compare? Well the Fastly implementation is actually identical to the Cloudflare implementation. This is a huge win for developers looking to experiment on both platforms.</p><h3 id="http-requests-with-streaming-bodies">HTTP Requests with Streaming Bodies</h3><p>At this point we have the ability to successfully combine readable streams into a single readable destination stream. Now we need a way to actually download content from Amazon S3 and return the data as a readable stream. Lucky for us, both Cloudflare and Fastly implement <code>fetch</code> from the Web API, but with one major difference.</p><p>Fetching data with Cloudflare is as easy as:</p><pre><code class="language-typescript">const urls = [...]
const requests = urls.map(url =&gt; fetch(url.href))
const responses = await Promise.all(requests)
const streams = responses.map(res =&gt; res.body)</code></pre><p>Although fetching data with Fastly is similar, there is one major difference in that the hostname of all <code>fetch</code>ed resources must be defined as <a href="https://developer.fastly.com/learning/compute/javascript/#communicating-with-backend-servers-and-the-fastly-cache">Fastly Backends</a>. This wont come as a surprise to anyone familiar with Fastly&apos;s VCL platform, but I bet this will be a major hangup for new customers coming from a more traditional web background. Assuming our hostnames are defined as backends, fetching data with Fastly is nearly as easy as Cloudflare:</p><pre><code class="language-typescript">const urls = [...]
const requests = urls.map(url =&gt; fetch(url.href, {
  backend: url.hostname
}))
const responses = await Promise.all(requests)
const streams = responses.map(res =&gt; res.body)</code></pre><h2 id="deploying-with-github-actions">Deploying with GitHub Actions</h2><p>We&apos;re finally ready to deploy our code to each platform, and at Barstool this involves setting up a GitHub actions workflow. I&apos;m happy to report that both platforms provide a GitHub Actions steps that makes it dead simple to deploy your code. Here&apos;s our workflow for Cloudflare:</p><pre><code class="language-yaml">name: Deploy Application

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Use Node.js
        uses: actions/setup-node@v2
        with:
          node-version: 16
          cache: yarn

      - run: yarn install --frozen-lockfile

      - name: Deploy to Cloudflare
        uses: cloudflare/wrangler-action@1.3.0
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
</code></pre><p>And here&apos;s our workflow for Fastly:</p><pre><code class="language-yaml">name: Deploy Application

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Use Node.js
        uses: actions/setup-node@v2
        with:
          node-version: 16
          cache: yarn

      - run: yarn install --frozen-lockfile

      - name: Deploy to Compute@Edge
        uses: fastly/compute-actions@beta
        env:
          FASTLY_API_TOKEN: ${{ secrets.FASTLY_API_TOKEN }}
</code></pre><p>Each step correctly uses the build command in your package.json which should compile your code with Webpack and then bundle it for deployment. There&apos;s technically a bit more to it than simply defining this workflow, as each platform has their own configuration file for setting up domains, environments, etc. That&apos;s slightly out of scope for this article, but I can assure you both are pretty complete and easy to define your infrastructure as code.</p><h2 id="performance-comparison">Performance Comparison</h2><p>Finally. Let&apos;s see how these platforms perform with a real world use-case. In order to provide a fair comparison I&apos;m going to run the tests without any CDN in front of our production workload on EC2, however, I am going to enable caching on the origin requests as each platform supports caching <code>fetch</code> requests and this is a critical part of the performance profile for our use case. If we don&apos;t cache the origin requests our Amazon S3 bill would be astronomical. </p><p>The way pipe-stream works is it provides a single route <code>GET /stream</code> and requires a JWT to be passed as <code>?token=</code>. Inside the JWT is the list of urls that need to be combined into a single stream. We use a JWT to ensure a bad actor cannot use our service to stream whatever files they want. The JWT is signed by a Barstool private key so we can validate it and ensure the JWT came from one of our services.</p><p>The first file we are going to test is a 3MB file defined by the following JWT payload:</p><pre><code class="language-json">{
  &quot;iat&quot;: 1624309200000,
  &quot;exp&quot;: 1624395600000,
  &quot;data&quot;: {
    &quot;u&quot;: [
      &quot;https://cms-media-library.s3.us-east-1.amazonaws.com/barba/splitFile-segment-0000.mp3&quot;,
      &quot;https://cms-media-library.s3.us-east-1.amazonaws.com/barba/splitFile-segment-0001.mp3&quot;,
      &quot;https://cms-media-library.s3.us-east-1.amazonaws.com/barba/splitFile-segment-0002.mp3&quot;
    ],
    &quot;c&quot;: &quot;audio/mpeg&quot;
  }
}</code></pre><p>At runtime, the platforms will fetch each file in the array and stream them back as a single combined MP3 file. Here are the test urls for each platform:</p><ol><li><a href="https://pipe-stream-origin.barstoolsports.com/stream.mp3?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MjQzMDkyMDAwMDAsImV4cCI6MTYyNDM5NTYwMDAwMCwiZGF0YSI6eyJ1IjpbImh0dHBzOi8vY21zLW1lZGlhLWxpYnJhcnkuczMudXMtZWFzdC0xLmFtYXpvbmF3cy5jb20vYmFyYmEvc3BsaXRGaWxlLXNlZ21lbnQtMDAwMC5tcDMiLCJodHRwczovL2Ntcy1tZWRpYS1saWJyYXJ5LnMzLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tL2JhcmJhL3NwbGl0RmlsZS1zZWdtZW50LTAwMDEubXAzIiwiaHR0cHM6Ly9jbXMtbWVkaWEtbGlicmFyeS5zMy51cy1lYXN0LTEuYW1hem9uYXdzLmNvbS9iYXJiYS9zcGxpdEZpbGUtc2VnbWVudC0wMDAyLm1wMyJdLCJjIjoiYXVkaW8vbXBlZyJ9fQ.DNVLvhaI-7K7v5nFiFlikoH5JlpPpU6O1wXuiAS9S54">EC2</a></li><li><a href="https://pipe-stream-workers.barstool.dev/stream.mp3?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MjQzMDkyMDAwMDAsImV4cCI6MTYyNDM5NTYwMDAwMCwiZGF0YSI6eyJ1IjpbImh0dHBzOi8vY21zLW1lZGlhLWxpYnJhcnkuczMudXMtZWFzdC0xLmFtYXpvbmF3cy5jb20vYmFyYmEvc3BsaXRGaWxlLXNlZ21lbnQtMDAwMC5tcDMiLCJodHRwczovL2Ntcy1tZWRpYS1saWJyYXJ5LnMzLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tL2JhcmJhL3NwbGl0RmlsZS1zZWdtZW50LTAwMDEubXAzIiwiaHR0cHM6Ly9jbXMtbWVkaWEtbGlicmFyeS5zMy51cy1lYXN0LTEuYW1hem9uYXdzLmNvbS9iYXJiYS9zcGxpdEZpbGUtc2VnbWVudC0wMDAyLm1wMyJdLCJjIjoiYXVkaW8vbXBlZyJ9fQ.hwtTWYf2aecgmveSVhPdhaEsz8A8LQax4aeXBEvtD50">Cloudflare</a></li><li><a href="https://pipe-stream-compute.barstool.dev/stream.mp3?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MjQzMDkyMDAwMDAsImV4cCI6MTYyNDM5NTYwMDAwMCwiZGF0YSI6eyJ1IjpbImh0dHBzOi8vY21zLW1lZGlhLWxpYnJhcnkuczMudXMtZWFzdC0xLmFtYXpvbmF3cy5jb20vYmFyYmEvc3BsaXRGaWxlLXNlZ21lbnQtMDAwMC5tcDMiLCJodHRwczovL2Ntcy1tZWRpYS1saWJyYXJ5LnMzLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tL2JhcmJhL3NwbGl0RmlsZS1zZWdtZW50LTAwMDEubXAzIiwiaHR0cHM6Ly9jbXMtbWVkaWEtbGlicmFyeS5zMy51cy1lYXN0LTEuYW1hem9uYXdzLmNvbS9iYXJiYS9zcGxpdEZpbGUtc2VnbWVudC0wMDAyLm1wMyJdLCJjIjoiYXVkaW8vbXBlZyJ9fQ.hwtTWYf2aecgmveSVhPdhaEsz8A8LQax4aeXBEvtD50">Fastly</a></li></ol><p>I performed all tests from my apartment in Brooklyn, NY on a 1GB Verizon FIOS connection. At the time of testing, I was consistently getting 580mbps according to <a href="https://fast.com">fast.com</a>. The benchmark was performed using a <a href="https://github.com/BarstoolSports/pipe-stream-benchmark">custom Deno script</a> which fetches each url 100 times and then computes the average time-to-first-byte (TTFB) and average time-to-download (TTD). Here are my results:</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>PLATFORM</th>
<th>TTFB (MS)</th>
<th>TTD (MS)</th>
<th>SIZE (MB)</th>
</tr>
</thead>
<tbody>
<tr>
<td>ec2</td>
<td>77.02</td>
<td>151.54</td>
<td>3.33</td>
</tr>
<tr>
<td>cloudflare</td>
<td>61.98</td>
<td>130.88</td>
<td>3.33</td>
</tr>
<tr>
<td>fastly</td>
<td>38.07</td>
<td>86.19</td>
<td>3.33</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><p>Next I wanted to test streaming a much larger file. Below are 3 more urls for a 70MB file:</p><ol><li><a href="https://pipe-stream-origin.barstoolsports.com/stream.mp3?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjEwMDAwMDAwMDAsImV4cCI6MTAwMDAwMDAwMDAwLCJkYXRhIjp7InUiOlsiaHR0cHM6Ly9jbXMtbWVkaWEtbGlicmFyeS5zMy5hbWF6b25hd3MuY29tL3VuaW9uLzIwMjEvMDYvMjkvc2lsZW50c3RlcmVvLmZjOWZkZDJmLjk2cy5tcDMiLCJodHRwczovL2JhcnN0b29sLXBvZGNhc3RzLnMzLmFtYXpvbmF3cy5jb20vYmFyc3Rvb2wtc3BvcnRzL21pY2tzdGFwZS9kYW5hcmF0cy4yNTdhNjBkNjkyMzYuOTYuOTZzLm1wMyJdLCJjIjoiYXVkaW8vbXBlZyJ9fQ.m7eAJ6EhE1kA6gWlwVDaSjk7Klch-KT7mZTD-MF4AlQ">EC2</a></li><li><a href="https://pipe-stream-workers.barstool.dev/stream.mp3?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjEwMDAwMDAwMDAsImV4cCI6MTAwMDAwMDAwMDAwLCJkYXRhIjp7InUiOlsiaHR0cHM6Ly9jbXMtbWVkaWEtbGlicmFyeS5zMy5hbWF6b25hd3MuY29tL3VuaW9uLzIwMjEvMDYvMjkvc2lsZW50c3RlcmVvLmZjOWZkZDJmLjk2cy5tcDMiLCJodHRwczovL2JhcnN0b29sLXBvZGNhc3RzLnMzLmFtYXpvbmF3cy5jb20vYmFyc3Rvb2wtc3BvcnRzL21pY2tzdGFwZS9kYW5hcmF0cy4yNTdhNjBkNjkyMzYuOTYuOTZzLm1wMyJdLCJjIjoiYXVkaW8vbXBlZyJ9fQ.m7eAJ6EhE1kA6gWlwVDaSjk7Klch-KT7mZTD-MF4AlQ">Cloudflare</a></li><li><a href="https://pipe-stream-compute.barstool.dev/stream.mp3?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjEwMDAwMDAwMDAsImV4cCI6MTAwMDAwMDAwMDAwLCJkYXRhIjp7InUiOlsiaHR0cHM6Ly9jbXMtbWVkaWEtbGlicmFyeS5zMy5hbWF6b25hd3MuY29tL3VuaW9uLzIwMjEvMDYvMjkvc2lsZW50c3RlcmVvLmZjOWZkZDJmLjk2cy5tcDMiLCJodHRwczovL2JhcnN0b29sLXBvZGNhc3RzLnMzLmFtYXpvbmF3cy5jb20vYmFyc3Rvb2wtc3BvcnRzL21pY2tzdGFwZS9kYW5hcmF0cy4yNTdhNjBkNjkyMzYuOTYuOTZzLm1wMyJdLCJjIjoiYXVkaW8vbXBlZyJ9fQ.m7eAJ6EhE1kA6gWlwVDaSjk7Klch-KT7mZTD-MF4AlQ">Fastly</a></li></ol><p>And here are the &#xA0;results from fetching these urls 10 times on each platform:</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>PLATFORM</th>
<th>TTFB (MS)</th>
<th>TTD (MS)</th>
<th>SIZE (MB)</th>
</tr>
</thead>
<tbody>
<tr>
<td>ec2</td>
<td>87.8</td>
<td>1750.9</td>
<td>69.79</td>
</tr>
<tr>
<td>cloudflare</td>
<td>66.4</td>
<td>1832.2</td>
<td>69.79</td>
</tr>
<tr>
<td>fastly</td>
<td>32.4</td>
<td>1369.1</td>
<td>69.79</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><p>The first thing to notice is TTFB is considerly better on the Edge platforms. This shouldn&apos;t be much of a surprise, as this is precisely what the original blog posts were showcasing from both Fastly and Cloudflare. Those blog posts did a much more robust analysis, testing TTFB from a variety of locations around the world. For the sake of time I did not perform my tests anywhere other than Brooklyn, NY. Keeping this in mind, it&apos;s still hard to ignore Fastly&apos;s results when it comes to TTFB. The project we&apos;re testing is doing considerably more work in the WASM runtime than the original blog posts, yet Fastly&apos;s runtime is optimizedto such a degree that you would barely notice.</p><p>Things become more interesting when it comes to downloading the entire stream. Keep in mind we&apos;re not downloading a single static file, the runtimes are stitching multiple static files together. Both Cloudflare and EC2 had similar performance characteristics, but Fastly managed to stream the entire smaller file 33% faster and the larger file 25% faster. Given this project is very IO heavy, and we enabled caching on the origin requests, this is also testing the pure CDN performance of each platform rather than just the WASM runtimes.</p><p><strong>[2022-02-04] Update:</strong></p><p>A previous version of this blog post tested an earlier version of Fastly&apos;s SDK which led to much slower performance than both Cloudflare and EC2. Since the initial post, the Fastly team has released the latest version their JS SDK with a native <code>TransformStream</code> and fixes to <code>pipeTo</code>. In all of our tests using the latest version of the SDK, Fastly has outperformed both EC2 and Cloudflare.</p><h2 id="platform-limitations">Platform Limitations</h2><p>Aside from the discussed limitations of Fastly&apos;s <code>fetch</code> API, there&apos;s another limitation that&apos;s present on both platforms: Content-Length headers are not returned on streaming responses, even if we pre-compute the content length and set it ourselfs. This is actually turning out to be a major blocker in terms of officially migrating away from EC2 and onto one of the edge platforms. </p><p>We&apos;ve spoken to both Cloudflare and Fastly about this limitation, and their teams are aware and looking for ways to fix it. Although we&apos;ve received minimal details about what it will take to implement a fix, it&apos;s clear there are issues in the Web API fetch specification that prevent setting a content-length when using chunked encoding. In our case, we&apos;re not really using chunked encoding as we know the total amount of bytes between the MP3 files, but the runtime doesn&apos;t. I found some additional details regarding the issue <a href="https://community.cloudflare.com/t/cant-set-content-length-for-get-requests/174487/6">here</a>.</p><p><strong>[2021-12-18] Update:</strong></p><p>The tech lead for Cloudflare Workers responded to us on HackerNews and showed us how to correctly stream with a Content-Length header. Cloudflare implemented a non-standard <code>FixedLengthStream</code> class that allows passing the known content length into the constructor. Updating our implementation was as simple as:</p><pre><code class="language-typescript">const responseStream = new FixedLengthStream(contentLength)

combineStreams(streams).pipeTo(responseStream.writable)

return new Response(responseStream.readable)</code></pre><p>If you view any of the Cloudflare URL&apos;s above you will now see the correct Content-Length returned in all cases.</p><h2 id="final-thoughts">Final Thoughts</h2><p>I think the future is incredibly bright for this new age of edge computing, and I&apos;m excited for both Fastly and Cloudflare to continue to improving their platforms and taking away market share from the 3 big major cloud vendors. Clear competition across the industry can only mean a better product for us, the developers. If you&apos;re interested in working on projects like this, check out <a href="https://www.barstoolsports.com/jobs">barstoolsports.com/jobs</a> or shoot me an email at <a href="mailto:barba@barstoolsports.com">barba@barstoolsports.com</a></p>]]></content:encoded></item><item><title><![CDATA[Reporting on Data in High Volume Mongo Collections]]></title><description><![CDATA[<p>There are several services used here at Barstool that generate millions of mongo documents every day. In order to facilitate efficient reporting tools for the data and analytics team, we implemented a pattern that would yield near real-time results while not creating any long-running queries on millions of records. Our</p>]]></description><link>https://barstool.engineering/reporting-on-data-in-high-volume-mongo-collections/</link><guid isPermaLink="false">61a803b84b8e43170fa89868</guid><category><![CDATA[mongodb]]></category><dc:creator><![CDATA[Markham F Rollins IV]]></dc:creator><pubDate>Mon, 06 Dec 2021 22:34:48 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2021/12/e2170e60ea04-2.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://barstool.engineering/content/images/2021/12/e2170e60ea04-2.jpeg" alt="Reporting on Data in High Volume Mongo Collections"><p>There are several services used here at Barstool that generate millions of mongo documents every day. In order to facilitate efficient reporting tools for the data and analytics team, we implemented a pattern that would yield near real-time results while not creating any long-running queries on millions of records. Our solution entailed using TTL on Mongo collections, Stitch to Snowflake for backup, and Mongo aggregations.</p><p>Mongo has built-in support to set <a href="https://docs.mongodb.com/manual/core/index-ttl/#timing-of-the-delete-operation" rel="noopener noreferrer">TTLs</a> on collection to act as a sort of cleanup after a set period of time. Even with proper indexing running aggregates on a high volume of documents is non-performant. We did an assessment of our collections and determined what the most useful timeframe would be and then created the indexes. When adding a TTL to an existing collection, if you have a large number of documents that will qualify for deletion you&apos;ll need to consider the strain this will place on the DB. In order to launch, we began by setting the <code>expireAfterSeconds</code> to a date far in the past and periodically updated it until we reached our desired TTL (<a href="https://docs.mongodb.com/manual/reference/command/collMod/" rel="noopener noreferrer">collMod to update index</a>). This minimized the number of records it was deleting and distributed them over time.</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">// Create the initial index
db.reportCollection.index(
  {
    created_at: 1
  },
  {
    expireAfterSeconds: 60 * 60 * 24 * [STARTING_NUM_DAYS]
  }
)

// Update the TTL
db.runCommand({
  collMod: reportCollection,
  index: {
    keyPattern: created_at_1,
    expireAfterSeconds: 60 * 60 * 24 * [NUM_DAYS]
  }
})</code></pre><figcaption>TTL Index</figcaption></figure><p>After <code>NUM_DAYS</code>, based on <code>created_at</code>, the document will be deleted. To ensure proper backup we sync records via Stitch into Snowflake for long-term storage. The collection now has a manageable number of documents that we can report on. To further streamline the process and ensure Mongo doesn&apos;t get overloaded, we generate aggregates on a cron and store the results in another collection.</p><p>Working with the data and analytics team we determined what the smallest granularity of reporting would be and wrote a Mongo aggregation based on their parameters. On our end, we store the results and create records for each level of granularity to cut down on the amount of processing needed when generating reports.</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">// Aggregation
db.collection.aggregate({
  $match: {
    field: &apos;foo&apos;
  }
},
{
  $group: {
    _id: {
      topLevelGranularity: &apos;$topLevel&apos;,
      midLevelGranularity: &apos;$midLevel&apos;,
      baseLevelGranularity: &apos;$baseLevel&apos;
    }
  },
  sum: {
    $sum: 1
  }
}
                        
// Records Examples
{
  aggregation: &apos;baseLevel&apos;,
  baseLevel: &apos;blog&apos;,
  midLevel: &apos;sports&apos;,
  topLevel: &apos;barstool&apos;,
  sum: 1
}
{
  aggregation: &apos;baseLevel&apos;,
  baseLevel: &apos;video&apos;,
  midLevel: &apos;sports&apos;,
  topLevel: &apos;barstool&apos;,
  sum: 1
}
{
  aggregation: &apos;midLevel&apos;,
  midLevel: &apos;sports&apos;,
  topLevel: &apos;barstool&apos;,
  sum: 2
}
{
  aggregation: &apos;topLevel&apos;,
  topLevel: &apos;barstool&apos;,
  sum: 2
}</code></pre><figcaption>Aggregate Query</figcaption></figure><p>The above is a very basic aggregation example but can be expanded to have as many pipelines as required. Additionally, add indexes on the aggregated data to make querying data for reports performant. The front-end team uses our API to collect and present this data in various charts and graphs. We have a roadmap to further offload these reports using a few services and will post about the outcome.</p>]]></content:encoded></item><item><title><![CDATA[Better Sold Out Variant Styling On Shopify Dawn Theme]]></title><description><![CDATA[This blog will guide you through building a better UI/UX for showing sold out variants without disabling them on Shopify's Dawn theme.]]></description><link>https://barstool.engineering/shopify-dawn-soldout-variant-styling/</link><guid isPermaLink="false">61a8dee74b8e43170fa898a5</guid><dc:creator><![CDATA[Joseph Bona]]></dc:creator><pubDate>Fri, 03 Dec 2021 16:20:01 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2021/12/Screen-Shot-2021-12-03-at-9.11.58-AM.png" medium="image"/><content:encoded><![CDATA[<img src="https://barstool.engineering/content/images/2021/12/Screen-Shot-2021-12-03-at-9.11.58-AM.png" alt="Better Sold Out Variant Styling On Shopify Dawn Theme"><p>Here at Barstool we recently launched a new Shopify store for the podcast brand Call Her Daddy. We chose the Shopify built Dawn theme as a starting point for this new project and it has performed very well for us through our busy holiday season. </p><p>When building out the theme for our use case we have made some customizations like <a href="https://barstool.engineering/product-image-slider-for-shopify-dawn-theme/">creating a more traditional product image gallery for the Dawn theme</a>, and building a better way to display sold out variants for a product without disabling selection of these variants. This blog will guide you through completing the latter.</p><p>Our inventory changes quite frequently, especially during the holidays so we use a CTA to sign up for a back in stock email when someone is viewing an out of stock variant. Out of the box the Dawn theme disables the add to cart button and adds a subtle &apos;Sold out&apos; label. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://barstool.engineering/content/images/2021/12/Screen-Shot-2021-12-03-at-10.12.43-AM.png" class="kg-image" alt="Better Sold Out Variant Styling On Shopify Dawn Theme" loading="lazy" width="1106" height="956" srcset="https://barstool.engineering/content/images/size/w600/2021/12/Screen-Shot-2021-12-03-at-10.12.43-AM.png 600w, https://barstool.engineering/content/images/size/w1000/2021/12/Screen-Shot-2021-12-03-at-10.12.43-AM.png 1000w, https://barstool.engineering/content/images/2021/12/Screen-Shot-2021-12-03-at-10.12.43-AM.png 1106w" sizes="(min-width: 720px) 720px"><figcaption>Default sold out variant UI on the Dawn Theme (https://dawn-theme-default.myshopify.com/products/thelma-sandal)</figcaption></figure><p>What we prefer is to show the user what variants are out of stock and keep them clickable so they can open the back in stock email signup form.</p><h3 id="edit-the-js-for-the-%60variantradios%60-class">Edit the JS for the `VariantRadios` class:</h3><figure class="kg-card kg-code-card"><pre><code class="language-javascript">class VariantRadios extends VariantSelects {
  constructor() {
    super();
    // Trigger change when loaded
    this.onVariantChange()
  }

  // Overwrite updateOptions method to check for unavailable variants
  updateOptions() {
    const fieldsets = Array.from(this.querySelectorAll(&apos;fieldset&apos;));
    this.options = fieldsets.map((fieldset) =&gt; {
      return Array.from(fieldset.querySelectorAll(&apos;input&apos;)).find((radio) =&gt; radio.checked).value;
    });
    const possibleVariants = this.getVariantData().filter(variant =&gt; variant.option1 === this.options[0])
    for (let index = 0; index &lt; possibleVariants.length; index++) {
      const variant = possibleVariants[index]
      const input = document.querySelector(`[value=&quot;${variant.option2}&quot;]`)
      if (!variant.available) {
        input.classList.add(&apos;unavailable&apos;)
      } else {
        input.classList.remove(&apos;unavailable&apos;)
      }
    }
  }
}

customElements.define(&apos;variant-radios&apos;, VariantRadios);
</code></pre><figcaption>Editing VariantRadios class in /assets/global.js</figcaption></figure><p>Here we are editing the <code>updateOptions</code> class method to check for unavailable variants. For example if we have a product with size and color options we want to check all colors for size: small and add a classname to any that are unavailable. We get all variants with <code>this.getVariantData()</code> and filter those results to find variants with the first selected option, in this example the selected size option. </p><p>Once we have those variants we simply check if <code>variant.available</code> is falsy and add/remove the <code>.unavailable</code> class accordingly. One thing to note is that we invoke this method in the <code>constructor</code> so this check is done when the page loads and then on each subsequent selection.</p><h3 id="adding-the-css-to-style-the-radio-buttons">Adding the CSS to style the radio buttons:</h3><figure class="kg-card kg-code-card"><pre><code class="language-css">.product-form__input input[type=&apos;radio&apos;]:disabled + label,
.product-form__input input[type=&apos;radio&apos;].uanvailable + label {
  border-color: rgba(var(--color-foreground), 0.3);
  color: rgba(var(--color-foreground), 0.4);
  text-decoration: line-through;
}
.product-form__input input[type=&apos;radio&apos;].unavailable:checked + label {
  color: rgb(var(--color-background));
}</code></pre><figcaption>Add styles to assets/section-main-product.css</figcaption></figure><p>We add some CSS to designate unavailable variants with gray strikethrough text.</p><h3 id="the-finished-experience">The finished experience:</h3><figure class="kg-card kg-image-card"><img src="https://barstool.engineering/content/images/2021/12/recording.gif" class="kg-image" alt="Better Sold Out Variant Styling On Shopify Dawn Theme" loading="lazy" width="3718" height="2082"></figure><p>We are hiring a full-time Shopify Engineer to join our team. If you have experience creating custom Shopify experiences like this one please apply.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://jobs.lever.co/barstoolsports/fe2112ea-3509-420b-89d6-03ba660b7ebc"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Barstool Sports - Shopify Engineer</div><div class="kg-bookmark-description">Barstool Sports is hiring a Shopify Engineer for Barstool Sports located in New York City. You will be expected to create and manage Shopify frontend features/improvements across our multiple storefronts. We are looking for someone with a UX background who is accustomed to working on Shopify Plus st&#x2026;</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://jobs.lever.co/favicon.ico" alt="Better Sold Out Variant Styling On Shopify Dawn Theme"><span class="kg-bookmark-author">Barstool Sports logo</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://lever-client-logos.s3-us-west-2.amazonaws.com/207e3c94-114a-4305-bf0b-4e491b29cbd9-1596042915597.png" alt="Better Sold Out Variant Styling On Shopify Dawn Theme"></div></a></figure>]]></content:encoded></item><item><title><![CDATA[Creating an SNS Fanout in Serverless]]></title><description><![CDATA[Build out a complete SNS fanout in serverless.yml]]></description><link>https://barstool.engineering/creating-an-sns-fanout-in-serverless/</link><guid isPermaLink="false">6181530b4b8e43170fa8967b</guid><category><![CDATA[serverless]]></category><category><![CDATA[aws]]></category><dc:creator><![CDATA[Markham F Rollins IV]]></dc:creator><pubDate>Tue, 16 Nov 2021 15:51:06 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2021/11/22549ca46444.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://barstool.engineering/content/images/2021/11/22549ca46444.jpeg" alt="Creating an SNS Fanout in Serverless"><p>Previously we&apos;ve covered what the Barstool Queue Engine is and how we use it for long-running services. Our next problem was how to handle alerting our various services that those jobs had finished or errored. As we rely heavily on the Serverless framework, we implemented this principle using their tools. The idea of an SNS fanout is to send out messages to a single SNS topic that any SQS handler could listen to and interpret. To begin, we need a new topic, <code>MyOutputTopic</code>, created as follows that utilizes a multi-stage deployment:</p><figure class="kg-card kg-code-card"><pre><code class="language-yml">resources:
    Resources:
        MyOutputTopic:
            Type: &apos;AWS::SNS::Topic&apos;
            Properties:
                TopicName: MyOutput-${opt:stage}</code></pre><figcaption>service1/serverless.yml</figcaption></figure><p>Since our services are in separate repositories, we also need to make sure the ARN of that topic is accessible outside of this single serverless deployment. Serverless provides an <code>Output</code> implementation to direct our stack to export values:</p><figure class="kg-card kg-code-card"><pre><code class="language-yml">resources:
    Outputs:
        MyOutputTopic:
            Value: !Ref MyOutputTopic
            Export:
                Name: MyOutputTopic-${opt:stage}</code></pre><figcaption>service1/serverless.yml</figcaption></figure><p>In our other repositories, we built out SQS queues in Serverless that subscribe to a singular topic. We had to create access policies so that we had permission to read from the topic:</p><figure class="kg-card kg-code-card"><pre><code class="language-yml">resources:
    # Create a basic queue
    Service2Queue:
        Type: &apos;AWS::SQS::Queue&apos;
        Properties:
            QueueName: Service2-${opt:stage}
            VisibilityTimeout: 60

    # Create an SNS subscription for the queue above
    Service2Subscription:
        Type: &apos;AWS::SNS::Subscription&apos;
        Properties:
            TopicArn: MyOutputTopic-${opt:stage}
            Endpoint:
                Fn::GetAtt: [Service2Queue, Arn]
            Protocol: sqs
            RawMessageDelivery: &apos;true&apos;
        
    # Provide the proper permissions for Service2Queue to recieve messages from MyOutputTopic-{$opt:stage}
    Service2QueuePolicy:
        Type: AWS::SQS::QueuePolicy
        Properties:
            PolicyDocument:
            Version: &apos;2012-10-17&apos;
            Statement:
                - Effect: Allow
                  Principal: &apos;*&apos;
              	  Action: SQS:SendMessage
              	  Resource:
                      - Fn::GetAtt: [Service2Queue, Arn]
                  Condition:
                      ArnEquals:
                      AWS:SourceArn: MyOutputTopic-${opt:stage}
            Queues:
                - !Ref Service2Queue</code></pre><figcaption>service2/serverless.ymlv</figcaption></figure><p>Now we have an SQS queue <code>Service2Queue</code> that is listening to all messages sent to the <code>MyOutputTopic-${opt:stage}</code> topic. This would have been enough if we truly cared about receiving everything but we don&apos;t, so as an additional step we employed SQS filters to limit what each queue would receive. When we send messages to BQE we include the following snippet as part of the message:</p><figure class="kg-card kg-code-card"><pre><code class="language-json">MessageAttributes: {
    filterKey1: {
        DataType: &apos;String&apos;,
        StringValue: `${output.filterKey1}`
    },
    filterKey2: {
        DataType: &apos;String.Array&apos;,
        StringValue: JSON.stringify(output.filterKey2)
    }
}</code></pre><figcaption>SNS Message</figcaption></figure><p>Once the above exists on a message the Serverless definition for the subscription can be updated to include a <code>FilterPolicy</code> to limit the invocations to only the messages that the queue needs to handle:</p><figure class="kg-card kg-code-card"><pre><code class="language-yml">resources:
    Service2Subscription:
        Type: &apos;AWS::SNS::Subscription&apos;
        Properties:
            TopicArn: MyOutputTopic-${opt:stage}
            Endpoint:
                Fn::GetAtt: [Service2Queue, Arn]
            Protocol: sqs
            RawMessageDelivery: &apos;true&apos;
            FilterPolicy:
                filterKey1: [&apos;foo&apos;]
                filterKey2: [&apos;bar&apos;]
</code></pre><figcaption>service2/serverless.yml</figcaption></figure><p>The last piece of code to drive this home is to hook the subscription into a lambda function to process the messages:</p><pre><code class="language-yml">functions:
  MyOutputTopicQueue:
    handler: handlers/sqs.myOutputTopic
    events:
      - sqs:
          arn:
            Fn::GetAtt: [Service2Queue, Arn]
          batchSize: 10</code></pre><p>SNS fanouts have proven incredibly helpful for our needs. I&apos;ve put all this <code>yml</code> together in a <a href="https://gist.github.com/mrollinsiv/dfa22ccbf415afe3e70911f895f0ea61">Gist</a> for easy reference. To learn more about SNS, Amazon has additional information <a href="https://docs.aws.amazon.com/sns/latest/dg/sns-common-scenarios.html">here</a>.</p>]]></content:encoded></item><item><title><![CDATA[Getting Started: Convert a React Project to TypeScript]]></title><description><![CDATA[<p>As a superset of JavaScript, Typescript can work in conjunction with JavaScript, importing Typescript code into a JavaScript file and vice versa. This means that migrating to TypeScript can be done incrementally, far different from converting a codebase from one programming language to something unrelated.</p><p>However, it can be daunting</p>]]></description><link>https://barstool.engineering/converting-a-react-project-to-typescript/</link><guid isPermaLink="false">616714d57e644a15ce08b5bd</guid><dc:creator><![CDATA[Gabriel Zarate]]></dc:creator><pubDate>Fri, 22 Oct 2021 16:50:17 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2021/10/ezgif.com-gif-maker--1-.gif" medium="image"/><content:encoded><![CDATA[<img src="https://barstool.engineering/content/images/2021/10/ezgif.com-gif-maker--1-.gif" alt="Getting Started: Convert a React Project to TypeScript"><p>As a superset of JavaScript, Typescript can work in conjunction with JavaScript, importing Typescript code into a JavaScript file and vice versa. This means that migrating to TypeScript can be done incrementally, far different from converting a codebase from one programming language to something unrelated.</p><p>However, it can be daunting to convert a large codebase to TypeScript without creating days or sometimes weeks of work.</p><p><br>The best way to embark on this transition is to make small, incremental changes to your codebase and quickly get those changes into <code>main</code>. You don&apos;t want to have some long-running TypeScript branch while other members of your team are making changes upstream; that is setting yourself up for more work and a merge conflicts nightmare. Making focused efforts to push forward basic conversions to TypeScript will save a lot of time. Get your codebase converted to TypeScript before you type everything perfectly right away.</p><h2 id="get-the-compiler-running">Get the Compiler Running</h2><p>In this first step, set up the Typescript compiler with the most permissive settings. This is not the time to enable <code>strict</code> mode. In this first phase, disable <code>noImplicitAny</code> and rename all your files from <code>.js</code> to <code>.ts</code> (use this bash <a href="https://gist.github.com/afternoon/9022899">script</a>). </p><p></p><pre><code class="language-js">{ 
   &quot;compilerOptions&quot;:{
      &quot;baseUrl&quot;: &quot;src&quot;,
      &quot;target&quot;:&quot;es5&quot;,
      &quot;allowJs&quot;: true,
      &quot;skipLibCheck&quot;: true,
      &quot;noImplicitAny&quot;: false,
      &quot;moduleResolution&quot;: &quot;node&quot;,
      &quot;module&quot;: &quot;esnext&quot;,
      &quot;jsx&quot;: &quot;preserve&quot;,
      &quot;strict&quot;: false,
   },
   &quot;include&quot;:[ 
      &quot;src/**/*&quot;
   ],
   &quot;exclude&quot;:[ 
      &quot;node_modules&quot;
   ],
}</code></pre><p>At this stage, only fix errors causing Typescript compiler errors, being careful to avoid functionality changes to the codebase. </p><p>Depending on your application, many of the errors you will find at this point involve defining what function parameters are required or optional, typing event onChange handlers, typing React Component props, etc.</p><pre><code>Property &apos;children&apos; is missing in type &apos;{ title: string; items: any[]; secondary: any; small: any; }&apos; but required in type &apos;Pick&lt;Pick&lt;{ items?: any[]; title?: string; showTitle?: boolean; children: any; minWidth?: string; placement?: string; ignoreBoundary?: boolean; primary: any; secondary?: boolean; small?: boolean; style: any; }, &quot;items&quot; | ... 6 more ... | &quot;small&quot;&gt; &amp; Pick&lt;...&gt; &amp; Pick&lt;...&gt;, &quot;items&quot; | ... 3 more ... | &quot;tertiary&quot;&gt;&apos;.ts(2741)</code></pre><p>Don&apos;t shy away from using the explicit <code>any</code> type at this point. You can add more meaningful types later.</p><pre><code>const App = ({ props }: any) =&gt; &lt;div&gt;{props.message}&lt;/div&gt;;</code></pre><h2 id="disable-noimplicitany">Disable noImplicitAny</h2><p>Next, set <code>noImplicitAny</code> to true. Your goal for this step will be to provide more meaning types where you can or add explicit <code>any</code>.</p><p>The compiler will no longer infer types in your components / functions. </p><p></p><pre><code class="language-js">function fetchData(arg) {
	return fetch(arg)
}
// Error: arg has an implicit &apos;any&apos; type

function fetchData(arg: string) {
    return fetch(arg)
}</code></pre><p>Depending on if you setting your <code>skipLibCheck</code> you may need to import types for your dependencies at this stage as well.</p><h2 id="enable-strict-mode">Enable Strict Mode</h2><p>This last phase will most likely need to happen incrementally. Each team and application will have different needs as far as what how <a href="https://www.typescriptlang.org/tsconfig">strict</a> you set your compiler.</p><pre><code>{ 
   &quot;compilerOptions&quot;:{
     &quot;strict&quot;: true,
     &quot;noUnusedLocals&quot;: true,
     &quot;noUnusedParameters&quot;: true,
     &quot;noImplicitReturns&quot;: true,
     &quot;forceConsistentCasingInFileNames&quot;: true
   },
   
}</code></pre><p>You can also extend <code>@typescript-eslint/parser</code> for type specific linting.</p><p>In conclusion, the TypeScript compiler is your friend. It has not always felt that way when I am in the middle of converting large amounts of code to TypeScript in the past, but I certainly miss it when I am working in a vanilla JS codebase. Also, the TypeScript documentation is a great resource. You can find helpful <a href="https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html">guides</a> to assist you in migrating to TypeScript or to learn the language for the first time.</p>]]></content:encoded></item><item><title><![CDATA[Using ACRCloud to Identify Copyrighted Audio Content]]></title><description><![CDATA[<p>Consumer applications like Shazam have been around for years, but only recently has using Automated Content Recognition (ACR) tools in enterprise software been easy and affordable. &#xA0;As a major podcast producer, the team at Barstool has to verify that any new podcast episodes we upload don&#x2019;t contain</p>]]></description><link>https://barstool.engineering/using-acr-cloud-to-identify-copyrighted-content-in-audio-files/</link><guid isPermaLink="false">6103175c7e644a15ce08b304</guid><dc:creator><![CDATA[Nick Booth]]></dc:creator><pubDate>Fri, 06 Aug 2021 21:32:00 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2021/07/11d51244.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://barstool.engineering/content/images/2021/07/11d51244.jpeg" alt="Using ACRCloud to Identify Copyrighted Audio Content"><p>Consumer applications like Shazam have been around for years, but only recently has using Automated Content Recognition (ACR) tools in enterprise software been easy and affordable. &#xA0;As a major podcast producer, the team at Barstool has to verify that any new podcast episodes we upload don&#x2019;t contain copyrighted content that we aren&#x2019;t licensed to use prior to publishing. &#xA0;Up until recently, this process was manual and laborious. The engineering team set out to fix that. </p><p>The goal was to create an automated system identify any copyrighted audio at upload-time. &#xA0;We could then compare the identified content with a whitelist of labels that we license for use in podcasts, and display a simple UI so producers can verify at-a-glance that we have rights to use all of the content prior to publishing. &#xA0;If a section of audio comes back as containing unlicensed content, the producer would then recut the episode with alternate content in its place. </p><p>After evaluating several vendors, we chose to go with <a href="https://www.acrcloud.com/">ACRCloud</a>. &#xA0;They&#x2019;re a relatively new startup, but have made a name for themselves in their short time in the industry. Their music identification service allows you to upload an audio or <a href="https://en.wikipedia.org/wiki/Acoustic_fingerprint">fingerprint</a> file for identification. &#xA0;While uploading a full podcast audio file was tempting, in practice the transit and processing time was too long for our purposes.</p><p>ACRCloud provides a number of tools and SDKs on their GitHub to interface with their APIs. &#xA0;To prove out the concept we used the <a href="https://github.com/acrcloud/acrcloud_scan_files_python3">Python Scan Tool</a>. &#xA0;This generates fingerprint files for every 10 second segment of the input file, then uploads them to the ACRCloud API. &#xA0;The output is parsed as either JSON or CSV locally. &#xA0;While we wouldn&apos;t use this tool directly in our infrastructure, it helped us to understand &amp; prove out the process. &#xA0;After vetting the data, we were ready to replace the python script with a direct integration in our CMS.</p><p>Under the hood, the Python script uses the ACRCloud <a href="https://github.com/acrcloud/acrcloud_extr_tools">Extractor Tool</a> to generate fingerprint files. The fingerprint files are generated for a specific span of time based on the CLI input. &#xA0;When each fingerprint file was ready, a request was made to the ACRCloud&apos;s identification API. &#xA0;These endpoints require each upload be signed to verify the payload integrity, so each request required an SHA1 hash to accompany the request. &#xA0;Below is the NodeJS to send the request</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">
async function _identifyFingerprintFile({ file, offset, options }) {
//ACRCloud requires a header signing the payload 
  const data = Buffer.from(await fs.readFile(file))
  let current_data = new Date()
  let timestamp = current_data.getTime() / 1000
  let stringToSign = _buildStringToSign(
    &apos;POST&apos;,
    options.endpoint,
    options.access_key,
    options.data_type,
    options.signature_version,
    timestamp
  )
  //Creating signature for the request
  let signature = crypto.createHmac(&apos;sha1&apos;, options.access_secret)
      .update(Buffer.from(stringToSign, &apos;utf-8&apos;))
      .digest().toString(&apos;base64&apos;)
  let form = new FormData()
  form.append(&apos;sample&apos;, data)
  form.append(&apos;sample_bytes&apos;, data.length)
  form.append(&apos;access_key&apos;, options.access_key)
  form.append(&apos;data_type&apos;, options.data_type)
  form.append(&apos;signature_version&apos;, options.signature_version)
  form.append(&apos;signature&apos;, signature)
  form.append(&apos;timestamp&apos;, timestamp)
  const body = await http
    .post(&apos;https://&apos; + options.host + options.endpoint, {
      method: &apos;POST&apos;,
      body: form
    })
    .json()
  return { file, offset, result: body }
}
function _sign(signString, accessSecret) {
  return 
}
</code></pre><figcaption>Uploading audio fingerprint to ACRCloud</figcaption></figure><p>Our first performance improvement was to asynchronously upload the fingerprints for identification in parallel rather than one at a time. This reduced the total processing time greatly, while adding comparatively little complexity. This would all live in <a href="https://barstool.engineering/long-jobs-with-the-barstool-queue-engine/">BQE</a> so we aren&apos;t resource constrained per-process and don&apos;t have an upper run-time limit to worry about. &#xA0; </p><p>While the actual identification was performant, we noticed the tool took exponentially longer to generate fingerprints the further into the audio file they were, meaning generating a fingerprint from 0-10 was almost instant, while each subsequent fingerprint would &#xA0;longer and longer to generate. &#xA0;This is common behavior with applications that require seeking to a specific location in a media file, as the underlying mp3 library has to decode every frame to make sure your seek command is frame-accurate. &#xA0;</p><p>While FFMPEG can have this same issue, there are several ways to make <a href="https://trac.ffmpeg.org/wiki/Seeking">frame-accurate seeking faster</a>. &#xA0;We&#x2019;ve used some of these methods when clipping live video so we were familiar with the problem. &#xA0;To speed up ACRCloud, our solution was to generate mp3 clips using FFMPEG map function with the flag -segment_time flag set to 10 seconds. &#xA0;</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">async createMp3Chunks({ input, eventId }) {
    return new Promise((resolve, reject) =&gt; {
    //_createAudioFfmepgProcess is a helper that creates an ffmpeg 
    //process with a standard bitrate, refresh rate &amp; audio codec
      const ffmpegProcess = _createAudioFfmpegProcess(input, eventId)
      ffmpegProcess
        .addOutputOptions([&apos;-c copy&apos;, &apos;-map 0&apos;, &apos;-segment_time 00:00:10&apos;, &apos;-f segment&apos;, &apos;-reset_timestamps 1&apos;])
        .save(`temp/${eventId}-segment-%04d.mp3`)
        .on(&apos;start&apos;, function (commandLine) {
          console.log(`Segment process spawned Ffmpeg with command: ${commandLine}`)
        })
        .on(&apos;end&apos;, async (event) =&gt; {
          console.log(&apos;event&apos;, event)
          console.log(`${eventId} -  File segmented successfully`)
          //have to read the temp directory for matching files, since
          //ffmpeg wont return output files as part of stdout
          const tempFiles = await fs.readdir(&apos;temp/&apos;)
          resolve(tempFiles.filter((file) =&gt; file.indexOf(`${eventId}-segment-`) &gt; -1))
        })
        .on(&apos;error&apos;, reject)
    })
  }</code></pre><figcaption>Generating Segmented MP3s</figcaption></figure><p>We then could take all of the generated mp3s segments and process them using the ACR extractor tool in parallel. &#xA0;Once we have an array of fingerprint files we had to tie everything together and generate a response with the specific time ranges where copyrighted content was found. &#xA0;This reduced our processing time to ~1 minute for a 1 hour long audio file.</p><p>Once we had a JSON response with all of the licensed content used in the audio file, all that was left to do was return the value back to our Podcast API. &#xA0;Below is a view of that our producers see if an audio file is uploaded with unlicensed content:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://barstool.engineering/content/images/2021/07/Screen-Shot-2021-07-29-at-5.06.47-PM.png" class="kg-image" alt="Using ACRCloud to Identify Copyrighted Audio Content" loading="lazy" width="1934" height="1260" srcset="https://barstool.engineering/content/images/size/w600/2021/07/Screen-Shot-2021-07-29-at-5.06.47-PM.png 600w, https://barstool.engineering/content/images/size/w1000/2021/07/Screen-Shot-2021-07-29-at-5.06.47-PM.png 1000w, https://barstool.engineering/content/images/size/w1600/2021/07/Screen-Shot-2021-07-29-at-5.06.47-PM.png 1600w, https://barstool.engineering/content/images/2021/07/Screen-Shot-2021-07-29-at-5.06.47-PM.png 1934w" sizes="(min-width: 1200px) 1200px"><figcaption>Example of a Podcast Producer View&#xA0;</figcaption></figure><p></p>]]></content:encoded></item><item><title><![CDATA[Product Image Slider for Shopify Dawn Theme Using Web Components]]></title><description><![CDATA[This blog will guide you through creating a more traditional product image gallery for the Dawn theme.]]></description><link>https://barstool.engineering/product-image-slider-for-shopify-dawn-theme/</link><guid isPermaLink="false">61043a477e644a15ce08b358</guid><dc:creator><![CDATA[Joseph Bona]]></dc:creator><pubDate>Fri, 30 Jul 2021 20:46:37 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2021/07/chd-1.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://barstool.engineering/content/images/2021/07/chd-1.jpg" alt="Product Image Slider for Shopify Dawn Theme Using Web Components"><p>Among the multitude of updates announced at Shopify Unite 2021 was the introduction of a new starter theme built by Shopify called Dawn. The theme is a great resource for learning and using Shopify&apos;s Online Store 2.0 and it&apos;s new features.</p><p>The team at Shopify put a lot of time and effort into the UX and you can read about their approach <a href="https://ux.shopify.com/next-generation-theme-design-5aae94f6d44c">here</a>. While I like a lot of the choices they made when designing the theme there is one component in particular that may not be best for all stores. On the product detail page product images are laid out in a grid rather than using a more common image slider gallery.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://barstool.engineering/content/images/2021/07/dawn.jpg" class="kg-image" alt="Product Image Slider for Shopify Dawn Theme Using Web Components" loading="lazy" width="1156" height="1817" srcset="https://barstool.engineering/content/images/size/w600/2021/07/dawn.jpg 600w, https://barstool.engineering/content/images/size/w1000/2021/07/dawn.jpg 1000w, https://barstool.engineering/content/images/2021/07/dawn.jpg 1156w" sizes="(min-width: 720px) 720px"><figcaption>Default product image gallery on Dawn</figcaption></figure><p>This blog will guide you through creating a more traditional product image gallery for the Dawn theme.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://barstool.engineering/content/images/2021/07/chd.jpg" class="kg-image" alt="Product Image Slider for Shopify Dawn Theme Using Web Components" loading="lazy" width="2000" height="1421" srcset="https://barstool.engineering/content/images/size/w600/2021/07/chd.jpg 600w, https://barstool.engineering/content/images/size/w1000/2021/07/chd.jpg 1000w, https://barstool.engineering/content/images/size/w1600/2021/07/chd.jpg 1600w, https://barstool.engineering/content/images/size/w2400/2021/07/chd.jpg 2400w" sizes="(min-width: 720px) 720px"><figcaption>Product image gallery slideshow</figcaption></figure><p>One thing you will notice when browsing the code for the Dawn theme is the decision to use Javascript sparingly and leverage broswer APIs for progressive enhancement. One of these APIs is the <a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements">Web Components API</a> to create custom elements. We will leverage Web Components to create our product image gallery.</p><h3 id="lets-start-by-writing-our-liquid-mockup-for-the-image-slider">Let&apos;s start by writing our liquid mockup for the image slider:</h3><pre><code class="language-html">&lt;product-gallery class=&quot;product-gallery&quot;&gt;
  {%- if product.media.size &gt; 1 -%}
  &lt;ul class=&quot;product-gallery__nav&quot;&gt;
    {%- for media in product.media -%}
      &lt;li class=&quot;product-gallery__nav-item {% if media.id == product.selected_or_first_available_variant.featured_media.id %}product-gallery__nav-item--active{% endif %}&quot; data-media-id=&quot;{{ media.id }}&quot;&gt;
        {% render &apos;product-thumbnail&apos;, media: media %}
      &lt;/li&gt;
    {%- endfor -%}
  &lt;/ul&gt;
  {%- endif -%}
  &lt;div class=&quot;product-gallery__images&quot;&gt;
    {%- for media in product.media -%}
      &lt;div class=&quot;product-gallery__image {% if media.id == product.selected_or_first_available_variant.featured_media.id or product.media.size == 1 %}product-gallery__image--active{% endif %}&quot; data-media-id=&quot;{{ media.id }}&quot;&gt;
        {% render &apos;product-thumbnail&apos;, media: media %}
      &lt;/div&gt;
    {%- endfor -%}
    &lt;button type=&quot;button&quot; class=&quot;slider-button slider-button--prev&quot; name=&quot;previous&quot; aria-label=&quot;{{ &apos;accessibility.previous_slide&apos; | t }}&quot;&gt;{% render &apos;icon-caret&apos; %}&lt;/button&gt;
    &lt;button type=&quot;button&quot; class=&quot;slider-button slider-button--next&quot; name=&quot;next&quot; aria-label=&quot;{{ &apos;accessibility.next_slide&apos; | t }}&quot;&gt;{% render &apos;icon-caret&apos; %}&lt;/button&gt;
  &lt;/div&gt;
&lt;/product-gallery&gt;
</code></pre><p>First we set up our <code>product-gallery</code> custom element. If the product has multiple images we will render the thumbnail navigation element: <code>ul.product-gallery__nav</code>. We then create a <code>div.product-gallery__images</code> to hold the current image being displayed. By default these images will be hidden unless the item is the active image, which is designated with a classname <code>.product-gallery__image--active</code>. We also add navigational buttons for previous and next slide. The <code>product-thumbnail</code> snippet we use for our images is the one that comes with the theme with some minor changes to remove the modal that displays a larger image.</p><h3 id="next-lets-add-some-css">Next let&apos;s add some CSS:</h3><pre><code class="language-css">.product-gallery {
  display: flex;
}
// Slider buttons are positioned absolutely over the active image
.product-gallery .slider-button {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}
.product-gallery .slider-button:not([disabled]):hover {
  border-color: rgba(var(--color-foreground), 0.3);
}
.product-gallery .slider-button:disabled {
  display: none;
}
.product-gallery .slider-button--prev {
  left: 0;
  border-left-width: 0;
}
.product-gallery .slider-button--next {
  right: 0;
  border-right-width: 0;
}
// Thumbnail navigation will not exceed the height of the active image and will scroll overflowing elements
.product-gallery__nav {
  width: 140px;
  list-style: none;
  margin: 0 .5rem 0 0;
  padding: 0;
  height: 100%;
  overflow-y: auto;
  display: none;
}
.product-gallery__nav::-webkit-scrollbar { 
  display: none; 
}
.product-gallery__nav-item {
  display: block;
  cursor: pointer;
}
.product-gallery__nav-item + .product-gallery__nav-item {
  margin-top: .5rem;
}
.product-gallery__nav-item img {
  width: 100%;
  display: block;
}
.product-gallery__images {
  flex-grow: 1;
  height: fit-content;
  position: relative;
}
// Hide images unless they are the active image
.product-gallery__image {
  display: none;
}
.product-gallery__image--active {
  display: block;
}
@media screen and (min-width: 750px) {
  .product-gallery__nav {
    display: block;
  }
}
</code></pre><p>Here we are setting up the basic layout for our gallery. Things to note are that the <code>.product-gallery__nav</code> will fill 100% of the height of it&apos;s parent. The parent <code>.product-gallery</code> will have it&apos;s height set programattically to be the height of the active image. This allows the nav to not exceed the height of the image and scroll if it does. I think this is a better use of vertical space than the default image gallery, especially if you care about the viewability of recommendations, reviews or user generated content below the main product. One other note is that the thumbnail navigation is hidden on mobile. I don&apos;t think it adds anything to the mobile experience and adds more images for the user to download. Our navigational elements do a good job of letting the user know there are more images without cluttering the UI.</p><h3 id="finally-we-create-our-web-component-for-the-slider">Finally we create our Web Component for the slider:</h3><pre><code class="language-javascript">class ProductGallery extends HTMLElement {
  constructor() {
    super();
    this.init()

    // Add resize observer to update container height
    const resizeObserver = new ResizeObserver(entries =&gt; this.update());
    resizeObserver.observe(this);

    // Bind event listeners
    this.navItems.forEach(item =&gt; item.addEventListener(&apos;click&apos;, this.onNavItemClick.bind(this)))
    this.prevButton.addEventListener(&apos;click&apos;, this.onButtonClick.bind(this));
    this.nextButton.addEventListener(&apos;click&apos;, this.onButtonClick.bind(this));
    // Listen for variant selection change to make current variant image active
    window.addEventListener(&apos;message&apos;, this.onVariantChange.bind(this))
  }

  init() {
    // Set up our DOM element variables
    this.imagesContainer = this.querySelector(&apos;.product-gallery__images&apos;);
    this.navItems = this.querySelectorAll(&apos;.product-gallery__nav-item&apos;);
    this.images = this.querySelectorAll(&apos;.product-gallery__image&apos;);
    this.prevButton = this.querySelector(&apos;button[name=&quot;previous&quot;]&apos;);
    this.nextButton = this.querySelector(&apos;button[name=&quot;next&quot;]&apos;);
    // If there is no active images set the first image to active
    if (this.findCurrentIndex() === -1) {
      this.setCurrentImage(this.images[0])
    }
  }

  onVariantChange(event) {
    if (!event.data || event.data.type !== &apos;variant_changed&apos;) return 
    const currentImage = Array.from(this.images).find(item =&gt; item.dataset.mediaId == event.data.variant.featured_media.id)
    if (currentImage) {
      this.setCurrentImage(currentImage)
    }
  }

  onNavItemClick(event) {
    const mediaId = event.target.closest(&apos;li&apos;).dataset.mediaId
    this.images.forEach(item =&gt; item.classList.remove(&apos;product-gallery__image--active&apos;))
    this.setCurrentImage(Array.from(this.images).find(item =&gt; item.dataset.mediaId === mediaId))
  }

  update() {
    this.style.height = `${this.imagesContainer.offsetHeight}px`
    this.prevButton.removeAttribute(&apos;disabled&apos;)
    this.nextButton.removeAttribute(&apos;disabled&apos;)
    if (this.findCurrentIndex() === 0) this.prevButton.setAttribute(&apos;disabled&apos;, true)
    if (this.findCurrentIndex() === this.images.length - 1) this.nextButton.setAttribute(&apos;disabled&apos;, true)
  }

  setCurrentImage(elem) {
    this.images.forEach(item =&gt; item.classList.remove(&apos;product-gallery__image--active&apos;))
    elem.classList.add(&apos;product-gallery__image--active&apos;)
    this.update()
  }

  findCurrentIndex() {
    return Array.from(this.images).findIndex(item =&gt; item.classList.contains(&apos;product-gallery__image--active&apos;))
  }

  onButtonClick(event) {
    event.preventDefault();
    let index = this.findCurrentIndex()
    if (event.currentTarget.name === &apos;next&apos;) {
      index++
    } else {
      index--
    }
    this.setCurrentImage(this.images[index])
  }
}

customElements.define(&apos;product-gallery&apos;, ProductGallery);
</code></pre><p>This is the bulk of the functionality for our gallery. We create our web component by extending the HTMLElement class. In our constructor we set up variables for our DOM elements and bind event listeners to the component. We rely on the data attributes set in our liquid to reference which thumbnails belong to which images to help with the <code>onNavItemClick</code> method as well as our <code>onVariantChange</code> callback.</p><p>Another caveat is using <code>Array.from(this.images)</code>. The images are stored in a variable using <code>querySelectorAll</code>. This function returns a <code>NodeList</code> which is array-like but not an array. Our component uses array methods to do some of the heavy lifting so it&apos;s important to create an array from the <code>NodeList</code> and not use the <code>NodeList</code> directly.</p><h3 id="emit-an-event-when-a-variant-selection-is-made">Emit an event when a variant selection is made:</h3><p>We want to update our slider so when a variant is chosen the active image is that variant&apos;s image. To do this we will add some code to the <code>VariantSelects</code> class in <code>assets/global.js</code>.</p><pre><code class="language-javascript">onVariantChange() {
  this.updateOptions();
  this.updateMasterId();
  this.toggleAddButton(true, &apos;&apos;, false);
  this.updatePickupAvailability();

  if (!this.currentVariant) {
    this.toggleAddButton(true, &apos;&apos;, true);
    this.setUnavailable();
  } else {
    this.updateMedia();
    this.updateURL();
    this.updateVariantInput();
    this.renderProductInfo();
  }
  // When variant is changed post a message with the variant&apos;s data
  window.postMessage({
    type: &apos;variant_changed&apos;,
    variant: this.currentVariant
  }, &apos;*&apos;)
}
</code></pre><p>Using Shopify&apos;s new Dawn theme is a great way to see how Shopify thinks about theme development in 2021. When making changes for your store you should follow the patterns and conventions they are using but that doesn&apos;t mean you can&apos;t add your own features. Hopefully this blog helps get you started on that path by showing how to use a product image slider over their product gallery grid on the Dawn theme.</p><p>We are hiring a full-time Shopify Engineer to join our team. If you have experience creating custom Shopify experiences like this one please apply.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://jobs.lever.co/barstoolsports/fe2112ea-3509-420b-89d6-03ba660b7ebc"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Barstool Sports - Shopify Engineer</div><div class="kg-bookmark-description">Barstool Sports is hiring a Shopify Engineer for Barstool Sports located in New York City. You will be expected to create and manage Shopify frontend features/improvements across our multiple storefronts. We are looking for someone with a UX background who is accustomed to working on Shopify Plus st&#x2026;</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://jobs.lever.co/favicon.ico" alt="Product Image Slider for Shopify Dawn Theme Using Web Components"><span class="kg-bookmark-author">Barstool Sports logo</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://lever-client-logos.s3-us-west-2.amazonaws.com/207e3c94-114a-4305-bf0b-4e491b29cbd9-1596042915597.png" alt="Product Image Slider for Shopify Dawn Theme Using Web Components"></div></a></figure>]]></content:encoded></item><item><title><![CDATA[Combining Sequential Streams with Node.js and Express]]></title><description><![CDATA[Asynchronously streaming bytes of data was perhaps the single most important feature of Node.js when it launched in 2009.]]></description><link>https://barstool.engineering/combining-sequential-streams-with-node-js/</link><guid isPermaLink="false">60dde5c67e644a15ce08b1bb</guid><category><![CDATA[nodejs]]></category><category><![CDATA[streams]]></category><category><![CDATA[express]]></category><dc:creator><![CDATA[Andrew Barba]]></dc:creator><pubDate>Tue, 13 Jul 2021 16:05:00 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2021/07/54f5b474-2.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://barstool.engineering/content/images/2021/07/54f5b474-2.jpeg" alt="Combining Sequential Streams with Node.js and Express"><p>Asynchronously streaming bytes of data was perhaps the single most important feature of Node.js when it launched in 2009. Node provided a brilliant API that allowed developers to stream bytes through a pipeline of operations and never block the main thread. Streams back everything from http requests and responses, to child processes and a lot more.</p><p>The primary benefit of streams is they remove the need to buffer large amounts of data in memory and then perform operations once that data is fully available. With streams you can effeciently operate on data in small chunks and push it through a pipeline where you&apos;re only ever consuming a small fraction of the total data in your currently running process. For example, let&apos;s say you wanted to build a basic proxy server in Node.js for large video files. When a request comes in for a particular file, you do not want the process to download the entire video into memory and then begin returning the video file to the client. Instead, its much more effecient to download the file in small chunks and send those chunks to the client as soon as they are received by the Node.js process. Once the chunks are written to the client Node can evict them from memory allowing us to handle many concurrent streams of data at once.</p><p>In order to implement this http proxy server lets start by building a basic async http request function that returns a stream:</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">const https = require(&apos;https&apos;)

function fetch(src, options = {}) {
  return new Promise((resolve, reject) =&gt; {
    const url = new URL(src)
    const options = {
      hostname: url.hostname,
      port: 443,
      path: url.pathname,
      method: &apos;GET&apos;,
      ...options
    }
    const req = https.request(options, (res) =&gt; {
      if (res.statusCode &gt;= 300) {
        const error = new Error(res.statusMessage || &apos;Invalid file&apos;)
        reject(error)
        return
      }
      resolve({
        res,
        url: url.href,
        headers: res.headers
      })
    })
    req.end()
  })
}</code></pre><figcaption>It might not be immediately obvious, but the resolved `res` is a Node.js Readable Stream</figcaption></figure><p>Next lets create a small express app that accepts all traffic and proxies to a pre-defined origin:</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">const express = require(&apos;express&apos;)
const app = express()
const origin = &apos;https://example.com&apos;

app.get(&apos;/*&apos;, async (req, res) =&gt; {
    const proxy = await fetch(`${origin}${req.path}`)
    res.writeHead(proxy.res.statusCode)
    proxy.res.pipe(res)
})

app.listen(process.env.PORT)</code></pre><figcaption>For a code-complete proxy we should also set the same response headers</figcaption></figure><p>Here&apos;s a breakdown of what this proxy route is doing:</p><ol><li>Issue a request to the defined origin + request path</li><li>Write the same status code to our response</li><li>Pipe the response from the fetch request to the current response stream</li></ol><p>At this point we can efficiently stream a single file from an origin, through our Node.js proxy, and back to the client. To make things more interesting, let&apos;s say we are a video provider that is required to insert ads into the middle of our video streams. In order to accommodate this, the video team saves our video files in chunks, split up depending on where we want to insert ads. For example, lets say we have a video file that is 1 hour long, and we want to insert an ad at 20 min, and 40 min into the video. Our video team would save 3 files:</p><ol><li>https://cdn.com/video_0_20.mp4</li><li>https://cdn.com/video_20_40.mp4</li><li>https://cdn.com/video_40_60.mp4</li></ol><p>In addition to this, our advertising team has provided us urls for video ads:</p><ol><li>https://cdn.com/ad_1.mp4</li><li>https://cdn.com/ad_2.mp4</li></ol><p>Our job is to combine these 5 files into a single video stream. Of course we could have our video team manually edit these files together and store them as one file, but lets use our knowledge of Node.js streams and combine these files on the fly.</p><p>First lets gather our video files into an array in the exact order we want to stich them together:</p><pre><code class="language-javascript">const urls = [
  &apos;https://cdn.com/video_0_20.mp4&apos;,
  &apos;https://cdn.com/ad_1.mp4&apos;,
  &apos;https://cdn.com/video_20_40.mp4&apos;,
  &apos;https://cdn.com/video_40_60.mp4&apos;,
  &apos;https://cdn.com/ad_2.mp4&apos;
]</code></pre><p>Next, lets issue a request for each video file:</p><pre><code class="language-javascript">const reqs = await Promise.all(urls.map(fetch))
const streams = reqs.map(req =&gt; req.res)</code></pre><p>Now we need to create a new function that can combine our array of streams into a single stream, maintaining the order of the bytes:</p><pre><code class="language-javascript">const { PassThrough } = require(&apos;stream&apos;)

function combineStreams(streams) {
  const stream = new PassThrough()
  _combineStreams(streams, stream).catch((err) =&gt; stream.destroy(err))
  return stream
}

async function _combineStreams(sources, destination) {
  for (const stream of sources) {
    await new Promise((resolve, reject) =&gt; {
      stream.pipe(destination, { end: false })
      stream.on(&apos;end&apos;, resolve)
      stream.on(&apos;error&apos;, reject)
    })
  }
  destination.emit(&apos;end&apos;)
}</code></pre><p>Notice we need two functions to implement this correctly. The first function creates a new <code>PassThrough</code> stream which acts as a single container for piping the other streams into. The second function is responsible for actually writing the bytes of the source streams into the destination stream. Aside from error handling, the most important thing to remember is that <code>pipe</code> will automatically end the destination stream when called, but lucky for us, Node provides an option to disable that behavior allowing us to continue writing bytes to the stream. Finally, once we loop through all stream we can manually call <code>end</code> on the destination stream.</p><p>Our complete Express route now can now be implemented like so:</p><pre><code class="language-javascript">app.get(&apos;/video-ssai.mp4&apos;, async (req, res) =&gt; {
    const reqs = await Promise.all(urls.map(fetch))
    const streams = reqs.map(req =&gt; req.res)
    const combined = combineStreams(streams)
    res.writeHead(200)
    combined.pipe(res)
})</code></pre><h2 id="wrapping-up">Wrapping Up</h2><p>Combining streams in Node.js is an extremely powerful concept that can be used to accomplish sophisticated data workflows, server-side ad insertion being just one of many. One important feature missing from our Express server is the ability to support Byte Range requests. Byte Range requests are when the client requests a certain range of the file instead of the whole thing. This is a critical feature to support for any streaming media, as you do not want clients consuming more resoruces than necessary. If you&apos;re interested in how we built this functionality at Barstool feel free to email me at barba@barstoolsports.com or apply to an open position here: <a href="https://www.barstoolsports.com/jobs">https://www.barstoolsports.com/jobs</a></p>]]></content:encoded></item><item><title><![CDATA[Gzip Compression with AWS Lambda and API Gateway HTTP API]]></title><description><![CDATA[In late 2017 we made the decision to go all in on Serverless. Our API's would move off of EC2 and traditional load-balancers and move to AWS Lambda and API Gateway. This led to some dramatic shifts in how we think about our architecture.]]></description><link>https://barstool.engineering/gzip-compression-with-aws-lambda-and-api-gateway-http-api/</link><guid isPermaLink="false">609e9ceb7e644a15ce0895fb</guid><dc:creator><![CDATA[Andrew Barba]]></dc:creator><pubDate>Fri, 18 Jun 2021 20:54:44 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2021/06/GettyImages-1252346549.daa387fe.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://barstool.engineering/content/images/2021/06/GettyImages-1252346549.daa387fe.jpg" alt="Gzip Compression with AWS Lambda and API Gateway HTTP API"><p>In late 2017 we made the decision to go all in on Serverless. Our API&apos;s would move off of EC2 and traditional load-balancers and move to AWS Lambda and API Gateway. This led to some dramatic shifts in how we think about our architecture, and also many nights of frustration dealing with the shortcomings of these cutting edge services.</p><p>AWS has since filled many of the issues and gaps around Serverless architecture. Some of our favorites include - much faster startup times for Lambdas in a VPC, 15 minute execution times, and a shiny new version of API Gateway with a focus on cost and performance for HTTP API&apos;s.</p><p>API Gateway HTTP API&apos;s are a new product that provide much lower latency than the traidtional API Gateway REST API&apos;s. They also propvide a massive cost improvement making Lambda HTTP API&apos;s perfectly viable for production workloads. However, like many new AWS products, it&apos;s missing some key features of the prior version. HTTP compression in the form of gzip and brotli have become the standard way to reduce the number of bytes clients need to download to display a website, render json data, etc. Most CDN&apos;s offer gzip and brotli compression out of the box, and ours does as well, but with one major caveat - only for cacheable requests. </p><p>We choose Fastly over CloudFront for its incredible performance and feature set, as well as excellent pricing across the board. But one major limitation with Fastly is its inability to compress non-cacheable content. Before moving to HTTP API&apos;s, we would compress our content with API Gateway. This allowed our Lambdas to simply return a response and let API Gateway handle the rest. However, now that we&apos;ve moved to HTTP API&apos;s, we needed a way to compress our responses in the Lambda runtime directly.</p><p>Lets start by creating a new file called <code>compression.js</code> and exporting a single function called <code>compress</code>:</p><pre><code class="language-javascript">const zlib = require(&apos;zlib&apos;)

exports.compress = (input, headers) =&gt; {
  ...
}</code></pre><p>Our function needs to take in two parameters: the input string to compress, and the headers object which will allow us to look at the <code>accept-encoding</code> header to figure out which compression algorithm to use:</p><pre><code class="language-javascript">// Parse the acceptable encoding, if any
const acceptEncodingHeader = headers[&apos;accept-encoding&apos;] || &apos;&apos;

// Build a set of acceptable encodings, there could be multiple
const acceptableEncodings = new Set(acceptEncodingHeader.toLowerCase().split(&apos;,&apos;).map(str =&gt; str.trim()))</code></pre><p>Next we need to check certain encodings in priority order, and use that encoding if its present in our set. We will check <code>brotli</code>, <code>gzip</code> then <code>deflate</code>, in that order:</p><pre><code class="language-javascript">// Handle Brotli compression (Only supported in Node v10 and later)
if (acceptableEncodings.has(&apos;br&apos;) &amp;&amp; typeof zlib.brotliCompressSync === &apos;function&apos;) {
  ...
}

// Handle Gzip compression
if (acceptableEncodings.has(&apos;gzip&apos;)) {
   ...
}

// Handle deflate compression
if (acceptableEncodings.has(&apos;deflate&apos;)) {
  ...
}</code></pre><p>Finally, we can call the correct compression method on the <code>zlib</code> framework and return the compressed data along with the algoritm we used:</p><pre><code class="language-javascript">// Brotli
return {
  data: zlib.brotliCompressSync(input),
  contentEncoding: &apos;br&apos;
}

// Gzip
return {
  data: zlib.gzipSync(input),
  contentEncoding: &apos;gzip&apos;
}

// Deflate
return {
  data: zlib.deflateSync(input),
  contentEncoding: &apos;deflate&apos;
}

// No Match
return {
  data: input,
  contentEncoding: null
}</code></pre><p>Now that we can successfully compress any input based on the request headers, we can call our compression function right before we return our content in the lambda handler:</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">exports.handler = async (event, context) =&gt; {
  const res = await handleRequest(event, context)
  
  const { data, contentEncoding } = compression.compress(res.body, event.headers)
  
  return {
    statusCode: res.statusCode,
    body: data.toString(&apos;base64&apos;),
    headers: {
      ...res.headers,
      &apos;content-encoding&apos;: contentEncoding
    },
    isBase64Encoded: true
  }
}</code></pre><figcaption>It&apos;s important to return the correct <code>content-encoding</code> so the client knows how to correctly parse the response.</figcaption></figure><h4 id="wrapping-up">Wrapping Up</h4><p>AWS has already vouched to make the new HTTP API&apos;s feature complete with the old REST API&apos;s, but they made that promise nearly 2 years ago now and they have yet to add support for compression out of the box. Hopefully this tuturial helps you fill the gap in the meantime.</p>]]></content:encoded></item><item><title><![CDATA[Intro to SWR: improving data-fetching & cache for fast user-interfaces]]></title><description><![CDATA[<p>SWR is a React Hooks library created by Vercel that simplifies data-fetching logic in your application and makes it possible to implement caching and dependent querying. SWR, or &apos;stale-while-revalidate,&apos; returns data from cache first, sends the fetch request, and finally renders up-to-date data. The package is simple, lightweight,</p>]]></description><link>https://barstool.engineering/data-fetching-swr/</link><guid isPermaLink="false">60c76e257e644a15ce08a971</guid><dc:creator><![CDATA[Gabriel Zarate]]></dc:creator><pubDate>Thu, 17 Jun 2021 18:27:59 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2021/06/175fc572-1.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://barstool.engineering/content/images/2021/06/175fc572-1.jpeg" alt="Intro to SWR: improving data-fetching &amp; cache for fast user-interfaces"><p>SWR is a React Hooks library created by Vercel that simplifies data-fetching logic in your application and makes it possible to implement caching and dependent querying. SWR, or &apos;stale-while-revalidate,&apos; returns data from cache first, sends the fetch request, and finally renders up-to-date data. The package is simple, lightweight, and includes features like SSR support, pagination, scroll-position recovery, and revalidation on focus.</p><h2 id="swr-vs-axiosfetch-api">SWR vs Axios/Fetch API</h2><p>Other popular data-fetching strategies such as Axios or Fetch API make the request and return the expected response, nothing more. SWR is a layer built on top of Fetch API that provides features like caching and pagination that would otherwise need to be managed elsewhere in the codebase. As a result, SWR provides a significant advantage for React applications to have fast, reactive user interfaces. A more appropriate library to compare with SWR would be something like <a href="https://react-query.tanstack.com">react-query</a> which supports caching and pre-fetching. </p><h2 id="refactoring-data-fetching-with-swr">Refactoring data-fetching with SWR</h2><p>Earlier this year, the engineering team at Barstool launched a feature for our internal CMS to process audio files via ACR to identify copyrighted content for podcast uploads.</p><p>The client-side of this feature involves multiple successive polling requests as the files are uploaded and processed. With polling coming as a feature out of the box in SWR, we took the opportunity to refactor the data-fetching for this feature to clean things up.</p><pre><code class="language-JavaScript">const pollMedia = async (mediaId) =&gt; {
   interval.current = setInterval(async () =&gt; {
     const response = await mediaApi.findById(mediaId)

     if (response.status === &apos;ready&apos;) {
       clearInterval(interval.current)
       pollCopyright(media.id, media.url)
     }
   }, pollInterval)
 }

const pollCopyright = async (mediaId, url) =&gt; {
	interval.current = setInterval(async () =&gt; {
    	const response = await podcastApi.processCopyrightClaims(mediaId, url)
        
        if (response?.status === &apos;ready&apos;) {
            clearInterval(interval.current)
        }
    }, 5000)
}</code></pre><p>These functions poll the <code>copyrights</code> and <code>media</code> endpoints until the API service returns the copyrighted content for the audio file. Let&apos;s refactor with SWR.</p><p>The <code>useSWR</code> hook accepts a <code>key</code> string and a <code>fetcher</code> function.</p><pre><code class="language-JavaScript">const ENDPOINT_KEY = &apos;/podcast-api/admin/media&apos;

const fetcher = url =&gt; fetch(url).then(r =&gt; r.json())

function useEpisodeCopyrightClaims() {
 	const { data, error } = useSWR(PODCAST_MEDIA_ID, fetcher)
    
    return { data, error }
}</code></pre><p>This is the most basic setup, but we need to implement polling and a custom fetcher that returns the data once the audio file has been processed.</p><pre><code class="language-JavaScript">const ENDPOINT_KEY = &apos;/podcast-api/admin/media&apos;

async function fetchEpisodeMedia(url, mediaId) {
	return await mediaApi.findById(mediaId)
}

function useEpisodeCopyrightClaims(mediaObj) {
	const { data, error } = useSWR(
    	[ENDPOINT_KEY, mediaObj.id],
    	fetchEpisodeMedia, 
        { refreshInterval: 5000 }
    )
    
    return { data, error }
}</code></pre><p>The parameters needed for our <code>fetcher</code> are passed within the array argument to <code>useSWR</code>. To manage when to stop polling, SWR includes an <code>onSuccess</code> handler:</p><pre><code class="language-JavaScript">const ENDPOINT_KEY = &apos;/podcast-api/admin/media&apos;
const interval = 5000

async function fetchEpisodeMedia(url, mediaId) {
	return await mediaApi.findById(mediaId)
}

function useEpisodeCopyrightClaims(mediaObj) {
  const [interval, setPollingInterval] = useState(interval)

  const { data, error } = useSWR(
    [ENDPOINT_KEY, mediaObj.id], 
    fetchEpisodeMedia, 
    {
      refreshInterval: interval,
      onSuccess: (data) =&gt; data?.status === &apos;ready&apos; &amp;&amp; setPollingInterval(0)
    }
  )

  return { data, error }
}</code></pre><p><code>pollingInterval</code> &#xA0;gets set to zero once the media is processed, canceling the polling for the request. The last portion of this refactor involves adding the second request for copyright claims processing. SWR supports dependent fetching which makes this process easy: </p><pre><code class="language-JavaScript">const ENDPOINT_KEY = &apos;/podcast-api/admin/media&apos;
const interval = 5000

async function fetchEpisodeMedia(url, mediaId) {
	return await mediaApi.findById(mediaId)
}

async function fetchEpisodeCopyrightClaims(url, mediaId, mediaUrl) {
	return await podcastApi.processCopyrightClaims(mediaId, mediaUrl)
}

function useEpisodeCopyrightClaims(mediaObj) {
	const [intervalMedia, setIntervalMedia] = useState(interval)
    const [intervalCopyright, setIntervalCopyright] = useState(interval)

    const { data: media, error: mediaError } = useSWR(
      [ENDPOINT_KEY, mediaObj.id], 
      fetchEpisodeMedia, 
      { 
        refreshInterval: intervalMedia, 
        onSuccess: (data) =&gt; data?.status === &apos;ready&apos; &amp;&amp; setIntervalMedia(0) 
      }
    )

  const { data: copyrightClaims, error: copyrightError } = useSWR(
    [ENDPOINT_KEY, media.id, media.url],
    fetchEpisodeCopyrightClaims,
    { 
      refreshInterval: intervalCopyright, 
      onSuccess: (data) =&gt; data?.status === &apos;ready&apos; &amp;&amp; setIntervalCopyright(0)
    }
  )

	return { copyrightClaims, error: mediaError || copyrightError }

}
</code></pre><p>The second request will not run until <code>media.id</code> is defined, which removes the complexity around handling these requests separately and allows us to list the copyright claims response as follows:</p><pre><code class="language-JavaScript">function CopyrightClaimsSummary({ mediaObj }) {
	const { copyrightClaims, error } = useEpisodeCopyrightClaims(mediaObj)
    
    if (error) return &lt;Error&gt;Something went wrong&lt;/Error&gt;
    if (!copyrightClaims) return &lt;Loading /&gt;
    
    return (
    	&lt;ul&gt;
        	{copyrightClaims.map((item) =&gt; (
            	&lt;li key={item.id}&gt;
                	{item.song.name}
                &lt;/li&gt;
            )}
        &lt;/ul&gt;
    )
}
</code></pre><p>We are excited about how SWR simplifies data-fetching in our codebase, with all of its useful <a href="https://swr.vercel.app/#features">features</a>, especially how well it fits with Next.js. Thank you, Vercel!</p>]]></content:encoded></item><item><title><![CDATA[Creating a slider component in React]]></title><description><![CDATA[<p>As a digital media company, Barstool Sports is home to a lot of podcasts. In an average week we publish 100+ episodes and while a majority of people listen to these on platforms like Apple Podcasts or Spotify, we&apos;d like to bring those listeners to our site. </p><p>In</p>]]></description><link>https://barstool.engineering/creating-a-slider-component-in-react/</link><guid isPermaLink="false">60cb11987e644a15ce08ad93</guid><dc:creator><![CDATA[Mike Nichols]]></dc:creator><pubDate>Thu, 17 Jun 2021 17:39:43 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2021/06/e79d58b7.gif" medium="image"/><content:encoded><![CDATA[<img src="https://barstool.engineering/content/images/2021/06/e79d58b7.gif" alt="Creating a slider component in React"><p>As a digital media company, Barstool Sports is home to a lot of podcasts. In an average week we publish 100+ episodes and while a majority of people listen to these on platforms like Apple Podcasts or Spotify, we&apos;d like to bring those listeners to our site. </p><p>In an effort to do this, we updated the audio player component all our sites use to have a more modern look and feel.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://barstool.engineering/content/images/2021/06/new_audio_player.png" class="kg-image" alt="Creating a slider component in React" loading="lazy" width="1664" height="408" srcset="https://barstool.engineering/content/images/size/w600/2021/06/new_audio_player.png 600w, https://barstool.engineering/content/images/size/w1000/2021/06/new_audio_player.png 1000w, https://barstool.engineering/content/images/size/w1600/2021/06/new_audio_player.png 1600w, https://barstool.engineering/content/images/2021/06/new_audio_player.png 1664w" sizes="(min-width: 720px) 720px"><figcaption>Our new audio player component</figcaption></figure><!--kg-card-begin: markdown--><p>At its core it&apos;s the standard <code>audio</code> element with a React UI built on top that uses <code>Refs</code> to control the audio file. Nothing new there. Something that&apos;s somewhat unique though is how we went about presenting the range <code>input</code> elements for progress and volume control.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://barstool.engineering/content/images/2021/06/Animated-GIF-downsized_large-1.gif" class="kg-image" alt="Creating a slider component in React" loading="lazy" width="480" height="58"><figcaption>Our Slider Component</figcaption></figure><!--kg-card-begin: markdown--><p>A good indication of progress or change needs a clear seperator for before and after, something that the default range <code>input</code> doesn&apos;t do too well. I&apos;ve personally always had a strong disdain for the range <code>input</code> as I feel the element is unusable without throwing at least some css at it. Even then, trying to style it in a way with two colors has been something that has always stumped me. I was able to find a solution though that simply handles that problem. A solution so simple that I didn&apos;t believe it would work.</p>
<!--kg-card-end: markdown--><p>Before I get into that though let me quicky go over the logic behind the component.</p><h3 id="logic">Logic</h3><!--kg-card-begin: markdown--><p>The component, named <code>Slider</code>, takes mulitple props:</p>
<ul>
<li><strong>colorAfter</strong>/<strong>colorBefore</strong>: the color of the bar before and after the current spot. The thumb of the slider will be the colorBefore.</li>
<li><strong>highlighted</strong>: the color of the thumb and the bar before it on hover</li>
<li><strong>size</strong>: height of the thumb (will grow <code>4px</code> on hover)</li>
<li><strong>value</strong>: the current value. Will be between <code>0</code> and <code>1</code></li>
</ul>
<p>also accepts additional <strong>props</strong> that will be directly passed to the <code>input</code> element</p>
<!--kg-card-end: markdown--><pre><code class="language-JavaScript">const Slider = ({
  colorAfter = &apos;#E1E1E6&apos;,
  colorBefore = &apos;#A5AAB2&apos;,
  highlighted = &apos;#EB3E3E&apos;,
  size = 10,
  value,
  ...props
}) =&gt; {
  const percent = value * 100
  const growTo = size + 4

  const [hover, setHover] = useState(false)

  return (
    &lt;StyledSlider
      type=&apos;range&apos;
      onMouseOver={() =&gt; setHover(true)}
      onMouseLeave={() =&gt; setHover(false)}
      value={value}
      size={size}
      colorAfter={colorAfter}
      colorBefore={colorBefore}
      highlighted={highlighted}
      percent={percent}
      growTo={growTo}
      seeking={hover}
      {...props}
    /&gt;
  )
}</code></pre><!--kg-card-begin: markdown--><p>The <code>input</code> element here, <code>StyledSlider</code>, is actually a <a href="https://styled-components.com">styled-components</a> <code>input</code> element. This doesn&apos;t affect the element other than how the css is applied to it. A cool feature of styled-components though is that you can essentially pass props right to the css. As you&apos;ll see in the next section, most of the props for <code>StyledSlider</code> are solely to affect the look of the Slider.</p>
<!--kg-card-end: markdown--><h3 id="styling">Styling</h3><!--kg-card-begin: markdown--><p>Background: Initially we tried using <a href="https://www.npmjs.com/package/styled-jsx">styled-jsx</a> to style the audio player but ran into issues upon testing integration into our sites. After digging through every issue in their repo and trying the provided solutions, we decided to switch to styled-components. It uses a very similar method as styled-jsx and was very easy to get the hang of.</p>
<p>The real magic of this component is in the styles because it seperates it from every other boring range <code>input</code> out there. This block of css might look a bit overwhelming, especially if you haven&apos;t used styled-components before, so let me highlight the key points and explain.</p>
<!--kg-card-end: markdown--><pre><code class="language-JavaScript">const transition = &apos;height 0.15s 0s ease, width 0.15s 0s ease&apos;

const StyledSlider = styled.input`
  cursor: pointer;
  background: linear-gradient(
    to right,
    ${(props) =&gt; (props.seeking ? props.highlighted : props.colorBefore)} 0%,
    ${(props) =&gt; (props.seeking ? props.highlighted : props.colorBefore)}
      ${(props) =&gt; props.percent}%,
    ${(props) =&gt; props.colorAfter} ${(props) =&gt; props.percent}%,
    ${(props) =&gt; props.colorAfter} 100%
  );
  border-radius: 8px;
  height: 4px;
  width: 100%;
  outline: none;
  padding: 0;
  margin: 5px 10px;
  -webkit-transition: ${transition};
  -moz-transition: ${transition};
  -o-transition: ${transition};
  transition: ${transition};
  -webkit-appearance: none;
  &amp;::-webkit-slider-thumb {
    border: none;
    -webkit-appearance: none;
    width: ${(props) =&gt; (props.seeking ? props.growTo : props.size)}px;
    height: ${(props) =&gt; (props.seeking ? props.growTo : props.size)}px;
    cursor: pointer;
    background: ${(props) =&gt; (props.seeking ? props.highlighted : props.colorBefore)};
    border-radius: 50%;
  }
  &amp;::-ms-thumb {
    border: none;
    height: ${(props) =&gt; (props.seeking ? props.growTo : props.size)}px;
    width: ${(props) =&gt; (props.seeking ? props.growTo : props.size)}px;
    border-radius: 50%;
    background: ${(props) =&gt; (props.seeking ? props.highlighted : props.colorBefore)};
    cursor: pointer;
  }
  &amp;::-moz-range-thumb {
    border: none;
    height: ${(props) =&gt; (props.seeking ? props.growTo : props.size)}px;
    width: ${(props) =&gt; (props.seeking ? props.growTo : props.size)}px;
    border-radius: 50%;
    background: ${(props) =&gt; (props.seeking ? props.highlighted : props.colorBefore)};
    cursor: pointer;
  }
`</code></pre><p>The two color slider comes from using a linear-gradient background with the props for the value and different colors passed in. It may look a bit confusing with all the JS sprinkled in but it&apos;s really just telling it what colors you want to use and where to stop the before bar and thumb at.</p><pre><code class="language-JavaScript">background: linear-gradient(
    to right,
    ${(props) =&gt; (props.seeking ? props.highlighted : props.colorBefore)} 0%,
    ${(props) =&gt; (props.seeking ? props.highlighted : props.colorBefore)}
    ${(props) =&gt; props.percent}%,
    ${(props) =&gt; props.colorAfter} ${(props) =&gt; props.percent}%,
    ${(props) =&gt; props.colorAfter} 100%
);</code></pre><p>The browser-specific styles target the slider thumb and basically do the same as mentioned above.</p><pre><code class="language-JavaScript">&amp;::-webkit-slider-thumb {
    border: none;
    -webkit-appearance: none;
    width: ${(props) =&gt; (props.seeking ? props.growTo : props.size)}px;
    height: ${(props) =&gt; (props.seeking ? props.growTo : props.size)}px;
    cursor: pointer;
    background: ${(props) =&gt; (props.seeking ? props.highlighted : props.colorBefore)};
    border-radius: 50%;
}
&amp;::-ms-thumb {
    border: none;
    height: ${(props) =&gt; (props.seeking ? props.growTo : props.size)}px;
    width: ${(props) =&gt; (props.seeking ? props.growTo : props.size)}px;
    border-radius: 50%;
    background: ${(props) =&gt; (props.seeking ? props.highlighted : props.colorBefore)};
    cursor: pointer;
}
&amp;::-moz-range-thumb {
    border: none;
    height: ${(props) =&gt; (props.seeking ? props.growTo : props.size)}px;
    width: ${(props) =&gt; (props.seeking ? props.growTo : props.size)}px;
    border-radius: 50%;
    background: ${(props) =&gt; (props.seeking ? props.highlighted : props.colorBefore)};
    cursor: pointer;
}</code></pre><h3 id="conclusion">Conclusion</h3><!--kg-card-begin: markdown--><p>While this focused on a method of creating a slider component that fit our needs at Barstool, hopefully this inspires you to improve upon any range <code>input</code>/sliders you may have on your site already. My main goal here was to present the ways of creating a two-color, unique slider as having separate colors and some animation really improves the usability of any element, but especially this one.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Errors from the BQE and beyond....using Slack]]></title><description><![CDATA[How we built a dynamic library to send formatted messages to Slack]]></description><link>https://barstool.engineering/monitoring-the-bqe-any-beyond-using-slack/</link><guid isPermaLink="false">60c3c7427e644a15ce08a88a</guid><dc:creator><![CDATA[Markham F Rollins IV]]></dc:creator><pubDate>Wed, 16 Jun 2021 17:31:41 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2021/06/8214bb0cc706.png" medium="image"/><content:encoded><![CDATA[<img src="https://barstool.engineering/content/images/2021/06/8214bb0cc706.png" alt="Errors from the BQE and beyond....using Slack"><p>Recently Nick Booth wrote a <a href="https://barstool.engineering/long-jobs-with-the-barstool-queue-engine/">blog</a> about our long-running service the Barstool Queue Engine. This being a core service we needed active error notifications to alert the team when it runs into trouble. While there are multiple options, levels, and redundant alerts to consider, I will focus on how I built a simple dynamic system to communicate with Slack.</p><p>My main goal was to build a reusable library that would accept a recognizable format that would allow for dynamic content. Slack&apos;s webhooks accept JSON to structure the message so it seemed a natural choice. Using the <a href="https://api.slack.com/block-kit">Block Kit</a> documentation I created a base set up to be used around our various applications:</p><pre><code class="language-json">{
  &quot;attachments&quot;: [
    {
      &quot;color&quot;: &quot;#ff3d41&quot;,
      &quot;blocks&quot;: [
        {
          &quot;type&quot;: &quot;header&quot;,
          &quot;text&quot;: {
            &quot;type&quot;: &quot;plain_text&quot;,
            &quot;text&quot;: &quot;&#x26A0;&#xFE0F; {{Application}}-{{Stage}}&quot;,
            &quot;emoji&quot;: true
          }
        },
        {
          &quot;type&quot;: &quot;section&quot;,
          &quot;fields&quot;: [
            {
              &quot;type&quot;: &quot;mrkdwn&quot;,
              &quot;text&quot;: &quot;*Field 1*\n{{FIELD_1}}&quot;
            },
            {
              &quot;type&quot;: &quot;mrkdwn&quot;,
              &quot;text&quot;: &quot;*Field 2*\n{{FIELD_2}}&quot;
            }
          ]
        },
        {
          &quot;type&quot;: &quot;divider&quot;
        },
        {
          &quot;type&quot;: &quot;section&quot;,
          &quot;text&quot;: {
            &quot;type&quot;: &quot;mrkdwn&quot;,
            &quot;text&quot;: &quot;*Error Message*\n{{ERROR_MESSAGE}}&quot;
          }
        }
      ]
    }
  ]
}</code></pre><p>This JSON will render the following in Slack:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://barstool.engineering/content/images/2021/06/Base_Block_Kit.png" class="kg-image" alt="Errors from the BQE and beyond....using Slack" loading="lazy" width="657" height="200" srcset="https://barstool.engineering/content/images/size/w600/2021/06/Base_Block_Kit.png 600w, https://barstool.engineering/content/images/2021/06/Base_Block_Kit.png 657w"><figcaption>Base Block Kit Format</figcaption></figure><p>Pulling from other templating languages I utilized <code>{{</code> <code>}}</code> to denote variable placement to support dynamic content in the messages. The core logic simply accepts a slack webhook, a path to the template and the data to be used when recursively replacing all of the placeholders in the template.</p><pre><code class="language-javascript">async function sendMessage({ endpoint, messageFilepath, data }) {
  const messageBody = JSON.parse(await fs.readFile(messageFilepath))
  const messageWithData = _replaceVariables({ message: messageBody, data })
  await got.post(endpoint, {
    json: {
      ...messageWithData
    }
  })
}

function _replaceVariables({ message, data }) {
  for (var key in message) {
    if (typeof message[key] == &apos;object&apos; &amp;&amp; message[key] !== null) {
      _replaceVariables({ message: message[key], data })
    } else {
      if ([&apos;text&apos;, &apos;color&apos;].includes(key)) {
        Object.keys(data).map((dataKey) =&gt; {
          message[key] = message[key].replace(`{{${dataKey}}}`, data[dataKey])
        })
      }
    }
  }
  return message
}</code></pre><p>The final product is a well-formatted error message in Slack that looks similar to the example below.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://barstool.engineering/content/images/2021/06/Example_Slack_Error_Message.png" class="kg-image" alt="Errors from the BQE and beyond....using Slack" loading="lazy" width="720" height="389" srcset="https://barstool.engineering/content/images/size/w600/2021/06/Example_Slack_Error_Message.png 600w, https://barstool.engineering/content/images/2021/06/Example_Slack_Error_Message.png 720w" sizes="(min-width: 720px) 720px"><figcaption>Error message as seen in Slack</figcaption></figure><p>We&apos;ve implemented this in our BQE but plan to roll it out to many of our services to stay ahead of our users when it comes to resolving issues they may run into. The flexibility allows us to send all pertinent information to enable speedy debugging.</p>]]></content:encoded></item><item><title><![CDATA[Resilient Multi-part file uploads to S3]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>Just about anyone who has spent time developing web apps has had the need to handle file uploads. We live in a (literal) web of profile pics, gifs, memes, live streams, vlogs, etc, etc. With the rise of services like AWS S3,  the task of handling uploads and storing file</p>]]></description><link>https://barstool.engineering/resilient-multi-part-file-uploads-to-s3/</link><guid isPermaLink="false">609efb977e644a15ce089638</guid><category><![CDATA[file uploads]]></category><category><![CDATA[media management]]></category><category><![CDATA[image management]]></category><category><![CDATA[react]]></category><category><![CDATA[aws]]></category><dc:creator><![CDATA[Zach Ward]]></dc:creator><pubDate>Sat, 05 Jun 2021 18:46:13 GMT</pubDate><media:content url="https://barstool.engineering/content/images/2021/06/1b719d6e7c20-1.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://barstool.engineering/content/images/2021/06/1b719d6e7c20-1.jpg" alt="Resilient Multi-part file uploads to S3"><p>Just about anyone who has spent time developing web apps has had the need to handle file uploads. We live in a (literal) web of profile pics, gifs, memes, live streams, vlogs, etc, etc. With the rise of services like AWS S3,  the task of handling uploads and storing file objects has, for the most part, become trivial. This is obviously a great thing and for most web apps, anything more than a basic integration with AWS S3 would be overkill. Like many other engineering teams, at Barstool we are big on keeping it simple and not over-engineering. In the (paraphrased) words of Knuth</p>
<blockquote>
<p>&quot;Premature optimization is the root of all evil&quot;</p>
</blockquote>
<p>In mid-2019, the engineering team at Barstool was in the early stages of the odyssey that has been replacing wordpress with our own in-house CMS - Barstool HQ. We were in the midst of pumping out new features almost weekly, in a race to gain internal buy-in to the new and unfamiliar platform. If we were going to enable our content team to be productive in HQ, a proper media management module was an absolutely crucial. Blog posts need thumbnails, bloggers need avatars, video posts require... videos. So we quickly built the internal tools for users to upload new files, browse existing files, and to attach those files wherever they might be needed.</p>
<h4 id="phase-1">Phase 1</h4>
<p>By late 2019, HQ had started to gain traction internally. Some bloggers were operating entirely within HQ and consequently usage of our file uploader skyrocketed. With increased usage, unaccounted-for edge cases and plain old bugs naturally followed. Bugs were squashed and some edge-cases covered, and work generally carried on as usual.</p>
<p>Things started to get interesting when a member of the content team made a request for bulk image uploads. It was a simple enough request and it would provide a big boost in productivity for end-users. The changes required to support multiple uploads didn&apos;t seem too difficult either. Our initial implementation had consisted of a drag-n-drop uploader component:</p>
<!--kg-card-end: markdown--><pre><code class="language-javascript">&lt;Uploader
  multiple={false}
  accept={this.props.fileType ? this.fileTypes[this.props.fileType] : &apos;*&apos;}
  errorMessage={this.state.errorMessage}
  onCancel={this.reset}
  onDrop={this.handleDrop}
  onRetry={this.reset}
  progressAmount={this.state.progressAmount}
/&gt;</code></pre><!--kg-card-begin: markdown--><p>While bulk of the work occurred in the <code>handleDrop</code> method:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-code-card"><pre><code class="language-javascript">handleDrop = async (acceptedFiles, rejectedFiles) =&gt; {
  if (acceptedFiles.length &gt; 0) {
    const extension = acceptedFiles[0].name.split(&apos;.&apos;).pop()
    const data = await mediaApi.getSignedUrl({ extension, content_type: acceptedFiles[0].type })
    await mediaApi.uploadToS3(data.upload, acceptedFiles[0], this.onUploadProgress)
    const { provider } = this.props
    const mediaObject = await mediaApi.create({ provider, key: data.key, title: filename })
    this.props.onCompleted(mediaObject)
    this.reset()
  } else if (rejectedFiles.length &gt; 0) {
    this.setState({
      errorMessage: &apos;Upload failed&apos;
    })
  }
}</code></pre><figcaption>Forgive the icky class component</figcaption></figure><!--kg-card-begin: markdown--><p>To add support for bulk uploads, we moved the logic responsible for communicating with the <code>mediaApi</code> service to a function <code>handleUploadFile</code> while <code>handleDrop</code> would now only be responsible for iterating through the files and passing them to as arguments to <code>handleUploadFile</code>:</p>
<!--kg-card-end: markdown--><pre><code class="language-javascript">async function handleUploadFile(file, index) {
  const { type, name, size } = file
  const key = `${name}-${index}`
  setUploadProgress(key, { loaded: 0, total: size })
  const extension = last(name.split(&apos;.&apos;))
  const data = await mediaApi.getSignedUrl({ extension, content_type: type })
  await mediaApi.uploadToS3(data.upload, file, progressData =&gt; onUploadProgress(key, progressData))
  const mediaObject = await mediaApi.create({ provider, key: data.key, title: name })
  incFilesUploadedCount()
  onFile(mediaObject)
  return mediaObject
}

async function handleDrop(acceptedFiles, rejectedFiles) {
  if (acceptedFiles.length &gt; 0) {
    setFilesToUploadCount(acceptedFiles.length)
    const mediaObjects = await Promise.all(acceptedFiles.map(handleUploadFile))
    if (onCompleted &amp;&amp; isFunction(onCompleted)) {
      onCompleted(mediaObjects)
    }
    reset()
  } else if (rejectedFiles.length &gt; 0) {
    setErrorMessage(&apos;Upload failed&apos;)
  }
}</code></pre><!--kg-card-begin: markdown--><p>During QA, this worked great. We were able to drop multiple files into the uploader and they&apos;d all get uploaded concurrently thanks to <code>Promise.all</code>. However, once we released it quickly became apparent that the simple solution above would not be adequate for a number of reasons:</p>
<ol>
<li>Due to the nature of <code>Promise.all</code>, one failed upload would cause the entire process to fail.</li>
<li>Our admin api&apos;s have maximum concurrency limits, which it turns out were occasionally being exceeded when a single user opens 40 additional connections while uploading 40 files</li>
<li>There is no way for users to &apos;retry&apos; failed uploads, they have to start from the beginning and select all their files again, this is obviously very frustrating for users.</li>
</ol>
<h4 id="phase-2">Phase 2</h4>
<p>So we went back to the drawing board and determined that with the following improvements, we would cooking with gas:</p>
<ol>
<li>Upload files in batches with a maximum concurrency set, preventing the maximum concurrency limit imposed by our api from being exceeded.</li>
<li>For each batch of uploads, add automated retry logic in order to mitigate non-fatal errors caused by network conditions, etc</li>
<li>In addition to automated retry logic, track failed uploads and allow user to manually retry the failed uploads</li>
</ol>
<!--kg-card-end: markdown--><p>Here is the helper function we came up with, <code>mapAsync</code> which powers the batched upload logic. It takes as arguments: an array of items, a concurrency limit, and a callback which will be invoked with each item, respectively. </p><pre><code class="language-javascript">async function mapAsync(items, concurrency = 1, handler) {
  let results = []
  let failures = []
  let index = 0
  while (index &lt; items.length) {
    const batch = items.slice(index, index + concurrency)
    try {
      const _results = await Promise.all(batch.map(handler))
      results = [...results, ..._results]
    } catch (err) {
      failures = [...failures, ...batch]
    }
    index += concurrency
  }
  return { results, failures }
}</code></pre><p>For the automated retry logic, we wrote a simple but powerful helper function, <code>withRetries</code> which takes as arguments: an async function, the number of retries to allow, and an error. Note that the <code>err</code> argument is meant to be passed when the function is invoked, but rather from within recursive calls after an error.</p><pre><code class="language-javascript">async function withRetries(fn, retries = 3, err = null) {
  if (!retries) {
    return Promise.reject(err)
  }
  return fn().catch(err =&gt; {
    return withRetries(fn, retries - 1, err)
  })
}</code></pre><p>It should be clear what is going on here, but basically the callback <code>fn</code> is invoked and if it rejects, then <code>withRetries</code> is recursively called with <code>retries - 1</code>, it will continue to do this either until <code>fn</code> succeeds or retries is exhausted, at which point an error is thrown back to the caller.</p><p>With these two helper functions we made the following modifications to the code in our <code>FileUploader</code> component:</p><pre><code class="language-javascript">async function handleRetry(files) {
  try {
    await handleDrop(files)
  } catch (err) {
    setErrorMessage(`Uploading failed again, please try again later`)
  }
}

async function handleDrop(acceptedFiles, rejectedFiles) {
  if (acceptedFiles.length &gt; 0) {
    // update progress state with all files to be uploaded prior to processing - because processing is done in batches this step is necessary beforehand in order to achieve realistic progress
    acceptedFiles.forEach(({ name, size }) =&gt; setUploadProgress(name, { loaded: 0, total: size }))
    setFilesToUploadCount(acceptedFiles.length)

    // upload files in asynchronous batches because concurrency limits can cause uploads to fail if surpassed resulting in entire upload hanging
    // because processing happens in batches, its possible that only certain batches fail to upload, so there can be successes and failures (rather than just one or the other like if this was all wrapped in a promise.all)
    const { results: allMediaObjects, failures } = await mapAsync(
      acceptedFiles,
      4,
      async (file, index) =&gt; await withRetries(async () =&gt; await handleUploadFile(file, index))
    )

    // if there are failures, we could allow retrying the upload with just those items, that way we wont re-upload any successful uploads
    if (failures.length) {
      setErrorMessage(`${failures.length} files failed to upload, would you like to retry uploading these files?`)
      setFilesToRetry(failures)
    } else {
      reset()
      if (onCompleted &amp;&amp; isFunction(onCompleted)) {
        onCompleted(allMediaObjects)
      }
    }
  } else if (rejectedFiles.length &gt; 0) {
    setErrorMessage(&apos;Upload failed&apos;)
  }
}</code></pre><p>And the end-result, when uploading four files and the fourth file fails to upload, the user can retry manually, and the uploader will pick back up where it left off, only attempting the fourth file again:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://barstool.engineering/content/images/2021/06/Uploader-Demo.gif" class="kg-image" alt="Resilient Multi-part file uploads to S3" loading="lazy" width="1220" height="578"><figcaption>Failure is simulated</figcaption></figure><p>The compounding effects of the batched uploads and automated retry logic actually made it rather tedious to test the manual retry logic - generally it&apos;s a pretty good sign when it&apos;s hard to break something.</p><p>After the changes were released, complaints dropped off almost entirely. We were very satisfied: without <em>too</em> much effort, we had iterated on our initial uploader, reusing most of our existing code to build a resilient uploader that could handle any number of files. I tested a bulk upload of 100 high-res images from Unsplash, and the uploader churned right through them, no failures.</p><!--kg-card-begin: markdown--><h4 id="phase-3">Phase 3</h4>
<p>For several months, no one touched the code of the uploader component. Our primary users early on were bloggers who primarily uploaded images or short video clips that they wanted to use in blog posts, and the uploader continued to work great for them.</p>
<p>During that time, however, the engineering team was working on a big project to bring all our video management functionality in-house. As part of bringing video management in-house, we&apos;d need to have the capability to handle large files, often up to and sometimes even over 10 GB.</p>
<p>We were ahead of the curve this time, before anything was released we were aware of the fact that we had a big problem: The maximum part size of an object being uploaded to S3 is 5 GB. Without a workaround, this was a show-stopper for moving video management in-house.</p>
<p>S3 supports multi-part uploads as documented here <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.htmland">Uploading and copying objects using multi-part upload</a> and so did some research to figure out what our exact approach would be. We browsed the GitHub, NPM, etc. for any existing solutions, surely we weren&apos;t the first engineering team to run into this issue.</p>
<p>Our head of engineering, Andrew, found this really great article <a href="https://www.altostra.com/blog/multipart-uploads-with-s3-presigned-url">Multipart uploads with S3 pre-signed URLs</a> which outlines the server-side changes needed to support multi-part uploads. Using the article as a reference, we were able to very quickly implement the necessary endpoints for supporting multi-part uploads. The endpoints we implemented were as follows:</p>
<ol>
<li><code>POST /upload-multipart { content_type: String, extension: String, filename: String, parts: Number }</code>: <code>parts</code> is the number of chunks we will break the file into when uploading, the response includes <code>{ bucket: String, key: String, location: String, upload_id: String, urls: [String] }</code> where <code>urls</code> is the array of endpoints that we will use to upload each corresponding chunk.</li>
<li><code>POST /upload-multipart/complete { key, bucket, upload_id, etags }</code> which is hit after all the chunks have been upload, this stage&#x2019;s job is to inform to S3 that all the parts were uploaded. By pass the etag of each part to this endpoint, S3 knows how to construct the object from the uploaded parts.</li>
</ol>
<p>Next, we came across <a href="https://github.com/muxinc/upchunk">UpChunk</a>, which just so happens to be built by our video provider, <a href="https://www.mux.com/">Mux</a>. According to the ReadMe, UpChunk is &quot;a JavaScript module for handling large file uploads via chunking and making a put request for each chunk with the correct range request headers.&quot; This is exactly the type of library we were looking for. Unfortunately, after digging deeper and even asking the mux team about it directly, it was apparent that it would not be compatible with multi-part uploads that S3. But not all was lost - the UpChunk source is very tiny and easy to comprehend, so using that as a starting point and reference, while modifying what we needed to make it compatible with our system, we were able to make our own chunk uploader that was compatible with S3 multi-part uploads.</p>
<p>I remember very clearly that all of this discovery was done during a Friday afternoon in September of 2020. I was heads down on the code all evening and had something nearly working by around 9pm. Our new <code>ChunkUploader</code> module worked as follows:</p>
<ol>
<li>Initialize uploader with <code>file</code> to upload and <code>getEndpoints</code> which is either an array of endpoints or an async function which returns the endpoints after being invoked. Additionally, an <code>onUploadProgress</code> callback can be passed, which will receive progress events as chunks are uploaded, this is useful for displaying a progress bar.</li>
<li>Call <code>uploader.upload()</code> which, as its name implies, kicks of the upload process. <code>upload()</code> first calculates the number of chunks to break the file into and subsequently calls the <code>getEndpoints</code> method with that number of chunks to get the array of endpoints that each chunk will be uploaded to.</li>
<li>Next, a private method <code>_sendChunks</code> which similar to the <code>FileUploader</code> component, uploads the chunks in batches set to a maximum concurrency, each of the chunks can be retried up to 5 times exponential backoff upon failures. To get each chunk, each endpoint return from <code>getEndpoints</code> is iterated, and each file is sliced from <code>index * chunk size in bytes</code> to <code>index + 1 * chunk size in bytes</code>, where <code>index</code> is the current index of each endpoint as they are iterated through.</li>
<li>Upon successful upload of each chunk, the <code>etag</code> returned is stored in an array corresponding to each chunk.</li>
<li><code>uploader.upload()</code> returns an object with the shape: <code>{ key: String, bucket: String, upload_id: String, etags: [String] }</code>, this will be used in to make the request to <code>POST /upload-multipart/complete</code> to finish the multipart upload process.</li>
</ol>
<p>And here&apos;s what that code looks like:</p>
<!--kg-card-end: markdown--><pre><code class="language-javascript">async function handleUploadFile(file) {
  if (cancelUploadRef.current) {
    throw new CanceledUploadError()
  }
  const { type, name } = file
  const key = `${name}`
  const extension = last(name.split(&apos;.&apos;))

  // initialize uploader with file object and getEndpoints callback for fetching pre-signed urls for each file part
  const uploader = new ChunkUploader({
    file,
    cancelRef: cancelUploadRef,
    getEndpoints: ({ parts }) =&gt; {
      return mediaApi.requestMultipartUpload({
        extension,
        content_type: type,
        filename: name,
        parts
      })
    },
    onUploadProgress: (progressData) =&gt; {
      onUploadProgressThrottled(key, progressData)
    }
  })

  // complete chunk upl
  const multipartData = await uploader.upload()
  const signedUrl = await () =&gt; mediaApi.completeMultipartUpload(multipartData)

  const mediaObject = await mediaApi.create({ provider, key: signedUrl.key, title: name, duration: audioFile?.duration })
  incFilesUploadedCount()
  return mediaObject
}</code></pre><!--kg-card-begin: markdown--><p>To keep our codebase as simple as possible, we use this as the upload logic for all of our files, not just big ones. Since files are uploaded in 8 MB chunks, so anything smaller than 8 MB is just a single chunk, nice and consistent!</p>
<p>Another nice benefit of uploading in chunks is that we can retry individual chunks in addition to retrying individual files, so we now have an extra layer to our retry logic to combat network issues. Additionally, we track upload progress per-chunk-url, which gives us progress data on a very granular level, allowing for some nice-to-haves like displaying &apos;Estimated time remaining&apos; for uploads. &apos;Estimated time remaining&apos; actually proved to be a very valuable tool for producers, who spend a lot of time waiting for files to upload: knowing the time remaining, they can continue working on other stuff and come back to the upload once the file is close to being fully uploaded.</p>
<h4 id="closing-thoughts">Closing Thoughts</h4>
<p>Since releasing the changes ~9 months ago, I can&apos;t think of a single complaint that we&apos;ve received related to failed uploads that could be attributed to our upload logic. A lack of complaints does not necessarily mean that there are no issues, but it&apos;s a promising sign considering we consistently upload several hundred videos and several thousand images per month.</p>
<h4 id="future-improvements">Future Improvements</h4>
<p>There are always improvements to be made. Given that we are a small engineering team with a ton of shit to do, we try to make those changes when we know that they are needed and that they will have a noticeable impact on the productivity of our users. Regardless, here&apos;s a list of some things that are on our radar that we&apos;d like to improve at some point:</p>
<ul>
<li>Support uploads in the background - a simple starting point would be to store a global reference to the upload process so that the user can leave the upload page and continue work while the uploader continues to work in the background. Upon successful upload, the user would receive a toast notification with a link to the new file</li>
<li>Better support for changes to network conditions like online/offline events, if the browser loses connection, we should immediately pause the upload process and allow the user to continue once their connection is restored.</li>
<li>Improvements to &apos;Estimated time remaining&apos; calculation. Our current implementation works very well for larger files, the algorithm being <code>((milliseconds elapsed / progress amount) - milliseconds elapsed)</code> where <code>progressAmount</code> is a decimal between 0 and 1. The current algorithm is nice and simple, it first calculates <code>millisecondsTotal</code> (estimated total time that the upload will take) by dividing <code>millisecondsElapsed</code> (time since start of the upload) by <code>progressAmount</code> (decimal between 0 and 1 indicating the current progress, 50% progress being 0.5), and then substracting <code>millisecondsElapsed</code> from <code>millisecondsTotal</code>. This works really nicely and gets more accurate with each second that upload is running, however it always takes a few seconds to &apos;calibrate&apos;, which for smaller files means that it is almost never accurate.</li>
<li>Last but not least, code improvements: a lot of love has gone into the <code>FileUploader</code> component and related <code>ChunkUploader</code>, but there&apos;s more we could do to make it reusable, or even potentially open-source it some day.</li>
</ul>
<p>Hopefully you&apos;ve learned a thing or two about handling file uploads in a resilient manner and I also hope that you&apos;ve learned about how the culture Barstool engineering team culture and how we tackle problems. If you&apos;re interested in working on problems like this (or not, if file uploads aren&apos;t for you, we do a lot of other stuff too) check out our openings <a href="https://www.barstoolsports.com/jobs">here</a>, we&apos;re actively hiring. Thanks for reading.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item></channel></rss>