Git Product home page Git Product logo

fetch-engine's Introduction

fetch-engine

A smart request-making library that makes sure your Javascript client is a good citizen of your distributed system.

Current status: Design

Rationale

The idea for this project comes from TweetDeck, a very 'chatty' native web app. The client-side is an oft-forgetten but important component of a distributed system, capable of bringing down servers with large amounts of traffic.

To mitigate the risk that TweetDeck contributes to a system failure we have developed a fairly complex networking layer. In various places, often ad-hoc, we do some of the following:

  • use a OAuth1-signing server to authenticate to our requests
  • de-duplicate in-flight requests
  • poll rate-limited API endpoints as fast as the limits allow
  • retry requests if they fail due to lack of network connection
  • retry, with exponential back-off, requests that return an error from the server
  • add authentication data in different ways based on app-configuration
  • track network request performance
  • timeout requests
  • fall-back from a stream connection to polling

However, these behaviours can require some manual work. Not all are used for all network requests, even if they should be. In particular, retry + exponential backoff with timeouts are a general good practice that we don't always apply. They can prevent cascading failures, or compounding of issues as they arise.

In many places on the server-side, this is assumed behaviour and done automatically. For example, Twitter's Finagle comes with timeouts, retries and other features out-of-the-box.

This project aims to make it easy to build good-citizen JavaScript network clients.

Goals

  • Highly configurable
  • Do nothing by default
  • Support plug-ins to add functionality
  • Provide useful plugins to solve common problems:
    • respect common HTTP error codes (eg. 503 retry with exponential backoff to a limit)
    • throttle requests to a rate-limited API endpoint
    • deduplicate identical in-flight requests
    • retry on network-failure
  • Instrumentable for performance and behaviour monitoring
  • Work in IE9+ (stretch-goal, IE6+)
  • Node-compatible
  • Drop-in replacement for fetch

API Design

Taken from #1.

There'd be two main constructors that come with fetch-engine: FetchEngine and FetchPreset. What they'd do is documented below:

FetchEngine

const fetch = new FetchEngine({
  plugins: [ ... ]
});
  • on creation, compose each plugin method into one method
  • on fetch(...), run methods in order:
    • shouldFetch
      • if false, bail with CancelledError
    • getRequest
      • produces a (Promise for a) Request
    • willFetch
      • side-effects, ignore return value
    • (internal-only) fetch
      • make the Request with an inner fetch implementation
    • fetch
      • allow plugins to cancel
    • getResponse
      • produce a (Promise for a) Response
    • didFetch
      • side-effects only, ignore return value

FetchPreset

const preset = new FetchPreset({
  filters: [ ... ],
  plugins: [ ... ]
});
  • compose filter + plugin methods into functions, then act like a plugin
  • gates each method based on filter responses

Plugins

The plugin API would be as below. They're just simple objects with methods. All are optional.

shouldFetch

Passed the current Request object.

Allows a plugin to prevent a request from being made.

Return a (Promise for a) Boolean. If false, the request is Cancelled.

getRequest

Passed the current Request object.

Allows plugins to add data to Request, or produce an entirely new Request.

Should return a (Promise for a) Request.

willFetch

Passed the current Request object.

Allows a plugin to react to a request being made, but not affect it.

Return value would be ignored.

fetch

Passed an object of the form { promise, cancel }.

Allows plugins to react to the completion of the fetch but not affect it, or cancel the request.

Return value is ignored.

getResponse

Passed the current Response object.

Allows plugins to add data to Response, or produce an entirely new Response.

Should return a (Promise for a) Response.

didFetch

Passed the current Response object.

Allows a plugin to react to a response arriving being made, but not affect it.

Return value would be ignored.

Filters

The filter API would be as below. Filters are used to gate the application of plugins within a FetchPreset. They're just simple objects with methods. All are optional.

testRequest

Passed the current Request.

Run before shouldFetch, getRequest, willFetch and fetch, which will not be applied if testRequest resolves to false.

Should return a (Promise for a) Boolean.

testResponse

Passed the current Response.

Run before getResponse and didFetch, which will not be applied if testResponse resolves to false.

Should return a (Promise for a) Boolean.

Example

Here's an example without an implementation, just in case this is super unclear...

import { FetchEngine, FetchPreset } from 'fetch-engine';

class PathPrefixFilter {
  constructor(prefix) {
    this.prefix = prefix;
  }
  testRequest(request) {
    const url = new URL(request.url);
    return url.pathname.startsWith(prefix);
  }
}

class CORSAuthPlugin {
  getRequest(request) {
    return new Request(request, {
      mode: 'cors',
      credentials: 'include',
      headers: Object.assign(request.headers, {
        'X-Csrf-Token': getCsrfToken()
      })
    });
  }
}

class RateLimitPlugin {
  shouldFetch(request) {
    return !isRateLimited(request);
  }
}

class TimeoutPlugin {
  constructor(time=1000) {
    this.time = time;
  }
  fetch({ cancel }) {
    setTimeout(cancel, this.time);
  }
}

class MetricsPlugin {
  willFetch(request) {
    trackRequest(request);
  }
  didFetch(response) {
    trackResponse(response);
  }
}

let fetch = new FetchEngine({
  plugins: [
    new TimeoutPlugin(5000),
    new CORSAuthPlugin(),
    new FetchPreset({
      filters: [ new PathPrefixFilter('/1.1/') ],
      plugins: [ new RateLimitPlugin() ]
    }),
    new MetricsPlugin()
  ]
});

Notes

  • To support some of these use cases, there needs to be a place to add metadata to the request (for example, to record when the request was initiated).

fetch-engine's People

Contributors

tgvashworth avatar

Watchers

James Cloos avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.