npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@canopytax/fetcher

v0.0.2

Published

Canopy's wrapper around the [`window.fetch` api](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) . It automatically provides:

Downloads

5

Readme

fetcher

Canopy's wrapper around the window.fetch api . It automatically provides:

  • JSON.stringifying the request body by default.
  • Prefixing urls with the api gateway url (canopyUrls.getAPIUrl) if a full url is not provided.
  • Defaulting the Content-Type header to be application/json for all requests that have a body.
  • Auth token refreshes. This will happen automatically whenever there is a 401 unless you provide a truthy passThrough401 property on the fetch configuration object. (For example, fetchAsObservable('/url', {passThrough401: true})). When the passThrough401 property is set, the auth token will not be refreshed and the request will be passed through like normal.
  • The X-CSRF-TOKEN http header
  • credentials
  • Removing angular properties that are prefixed with $$ (similar to what $http does).

Fancy Caching

Fetcher has an opt-in caching feature (fetchWithSharedCache) that allows redundant calls to the same URL without the performance/load penalty of making many ajax requests. It also gets even fancier by pushing changes to cached objects out to everyone who's interested in them. This is done by detecting if a PUT/PATCH is changing an object that other parts of the app have cached. If so, the response to the PUT/PATCH will be pushed out to all subscribers. See the fetchWithSharedCache and forceBustCache APIs

Methods

fetcher(url: string, options: {})

Returns Promise<any>

The default exported value, fetcher, is a function that simply wraps the fetch api. You should call fetcher just as if you were calling fetch. The only known limitation that we know of right now is that if you call fetcher with the first parameter being a Request object, fetcher will not be able to do its auth token refreshes for you if a 401 http status is returned for the server.

import fetcher from "fetcher!sofe";

fetcher("/my-url").then((res) => res);

fetchAsObservable(url: string, options: {})

Returns Observable<any>

Takes almost the exact same params as fetcher but returns an RxJS 6 observable rather than a promise. If response.ok, it will by default automatically try to return JSON with a fallback of text. To modify the default return behavior, you can including an optional responseType (ex. responseType: 'blob') in the second argument's object to explicitly specify the fetch response body operation (i.e. .json(), .text(), .blob(), etc.).

import { fetchAsObservable } from "fetcher!sofe";

fetchAsObservable("/my-url").subscribe((res) => res);

fetchAbort()

Returns (url: string, options: {}) => Promise<any>

This function will return a new fetch function, while also instantiating a new abort controller. That abort signal will be tied to all uses of this fetch.

The fetch function will also have a new property method on it called abort, with each use subsequently instantiating a new abort controller (basically this means you can reuse the fetch as much as you'd like).

Abort all fetches:

import { fetchAbort } from "fetcher!sofe";

// only one fetch is needed if you're going to abort them together
const runFetch = fetchAbort();

runFetch("/my-url-1").then((res) => res);
runFetch("/my-url-2").then((res) => res);

// abort all
runFetch.abort();

Abort a single fetch:

import { fetchAbort } from "fetcher!sofe";

// controlling each abort differently requires multiple fetches
const runFetch1 = fetchAbort();
const runFetch2 = fetchAbort();

runFetch1("/my-url-1").then((res) => res);
runFetch2("/my-url-2").then((res) => res);

// abort only "runFetch1"
runFetch1.abort();

useFetchAbort(opts: { abortOnUnmount: boolean })

React hook version of fetchAbort. Hook returns an array of two variables:

  • (url: string, options: {}) => Promise<any> - fetcher method
  • () => void - method to abort fetch

When fetching on component mount, fetching/aborting is as simple as throwing them in a useEffect:

import React, { useEffect } from 'react';
import { useFetchAbort } from 'fetcher!sofe';

function MyComponent() {
  const [runFetch, runAbort] = useFetchAbort();

  useEffect(() => {
    runFetch('/my-url').then(res => res);
    () => runAbort();
  });

  return (...);
}

(Keep in mind React 18's quirk of running useEffect twice on initial render...)

If you need more finite control of aborting, this works the same way as fetchAbort:

import React, { useEffect } from 'react';
import { useFetchAbort } from 'fetcher!sofe';

function MyComponent() {
  const [runFetch1, runAbort1] = useFetchAbort();
  const [runFetch2, runAbort2] = useFetchAbort();

  async function getOne() {
    const res = await runFetch1('/my-url-1');
  }
  async function getTwo() {
    const res = await runFetch2('/my-url-2');
  }

  useEffect(() => {
    // only abort "runFetch1"
    () => runAbort1();
  });

  return (...);
}

By default, the opts.abortOnUnmount argument is set to true, so you don't need to manually abort - the hook will do that for you 😉.

import React, { useEffect } from 'react';
import { useFetchAbort } from 'fetcher!sofe';

function MyComponent() {
  const [runFetch] = useFetchAbort();

  async function getTheThing() {
    const res = await runFetch('/my-url');
  }
  return (...);
}

You can set opts.abortOnUnmount to false if you need to manually control how the fetch aborts.

Older docs that need to get updated:

React Hook options

  • useFetcher
  • useObservable

useFetcher

A helper that allows you to run fetcher on component mount. useFetcher takes two arguments the url and optionalProps:

  • api url: ( eg api/clients/${contactId}?include=users,contacts,tags,contact_for,contact_sources) ,
  • optionalProps: { pluck: 'clients', // string that we use to pluck the value out of the response (via rxjs pluck operator) options: fetcherOptions, initialResults: [] // initial results value manualFire: true // defaults to false. If true you'll also get a fire method that allows you to "fire when ready" and not immediately. }

Example

const { loading, results, error, refetch } = useFetcher(
  `api/clients/${contactId}?include=users,contacts,tags,contact_for,contact_sources`,
  {
    pluck: "clients",
  }
);

useObservable

A more powerful version of useFetcher but requires more configuration. useObservable takes two arguments an rxjs observable and the initialResults

  • rxjs observable can do anything/everything
  • options: {initialResults, manualFire}
    • initialResults: represents the initial state returned by the hook
    • manualFire: defaults to false. If true you'll also get a fire method that allows you to "fire when ready" and not immediately.

NOTE you must memoize your observable if dynamically created

Example

const observable = useMemo(
  () => {
    return fromEvent(...).pipe(
      tap(...),
      debounceTime(...),
      switchMap(...),
      pluck(...)
    )
  },
  []
)

const { loading, results, error, resubscribe } = useObservable(observable, [])

Usage

import fetcher, {
  onPusher,
  fetchWithSharedCache,
  forceBustCache,
  fetchAsObservable,
  fetchWithProgress,
} from "fetcher";
import canopyUrls from "canopy-urls!sofe";
import { skipLast, last } from "rxjs/operators";

fetcher(`${canopyUrls.getWorkflowUrl()}/api/users/0`)
  .then((resp) => {
    if (resp.ok) {
      resp
        .json() // or .text()
        .then((json) => {
          console.log(json.users);
        })
        .catch((ex) => {
          throw ex;
        });
    } else {
      throw new Error(
        `Couldn't get user -- server responded with ${resp.status}`
      );
    }
  })
  .catch((ex) => {
    throw ex;
  });

fetchAsObservable(`${canopyUrls.getWorkflowUrl()}/api/users/0`).subscribe(
  (user) => {
    /* user json object */
  },
  (error) => {
    throw error;
  }
);

const formData = new FormData();
formData.append("file", document.getElementById("file-input").files[0]);
const withProgress = fetchWithProgress(
  `${canopyUrls.getAPIUrl()}/contacts/1/files`,
  {
    method: "post",
    body: formData,
  }
);
withProgress.pipe(skipLast(1)).subscribe((progressEvent) => {
  console.log(
    `File upload progress is now at ${progressEvent.overallProgress}`
  );
});
withProgress.pipe(last()).subscribe((data) => {
  console.log(`Response from server`, data);
});

withProgress.connect();

const subscription = fetchWithSharedCache(
  `${canopyUrls.getWorkflowUrl()}/api/clients/1/engagements/1`
).subscribe(
  (engagement) => {
    // this is the json from the response, not the response itself
    console.log(engagement.id);
  },

  (err) => {
    throw err;
  }
);

setTimeout(subscription.dispose, 10000);

forceBustCache(`${canopyUrls.getWorkflowUrl()}/api/clients/1`)
  .then((resp) => {
    console.log("cache was busted!");
    resp
      .json()
      .then((json) => console.log(json))
      .catch((ex) => {
        throw ex;
      });
  })
  .catch((ex) => {
    throw ex;
  });

const disposable = onPusher("message-type").subscribe((data) =>
  console.log(data)
);

disposable.dispose();

API

  • fetcher(params): The default exported value, fetcher, is a function that simply wraps the fetch api. You should call fetcher just as if you were calling fetch. The only known limitation that we know of right now is that if you call fetcher with the first parameter being a Request object, fetcher will not be able to do its auth token refreshes for you if a 401 http status is returned for the server.

  • fetchAsObservable(params): Returns an RxJS 6 Observable. Takes almost the exact same params as fetcher but returns an RxJS 6 observable rather than a promise. If response.ok, it will by default automatically try to return JSON with a fallback of text. To modify the default return behavior, you can including an optional responseType (ex. responseType: 'blob') in the second argument's object to explicitly specify the fetch response body operation (i.e. .json(), .text(), .blob(), etc.).

  • onPusher(message-type): Subscribe to server push events. Unsubscribe by disposing the subscriptions. The returned observable is deferred.

  • fetchWithSharedCache(url, subscriptionDuration, forceBust): This function takes in the string url, a string or function subscriptionDuration, and an optional boolean forceBust. It will either make a network request or return the cached object, depending on if the cache already has the object for that URL. Here are some things to note:

    • This API returns an Observable. Please make sure to dispose() your subscription to the observable when you are done with it
    • The value given is not a Response object, but a json object. This is different than when you do fetcher(url).then(response => ...), which indeed gives you a Response object.
    • The subscriptionDuration parameter is either a string (recommended) or a function. Whenever a hashchange event fires, fetcher will check to see if the subscriptionDuration has finished. It will check this for all subscriptions to the same url and if any of them says that the duration has ended, then it will bust the cache and call onCompleted on the observable which will end all of the subscriptions for that url. The reasoning for this is that the supported use case is subscribing to a data source as long as a certain SPA route is active, and then not caring about updates to the data once that SPA route is no longer active.
      • Usage as a string: as long as window.location.hash includes this string, your subscription will stay alive.
      • Usage as a function: your function will be called with no arguments. Your subscription will stay alive until the function returns something truthy.
      • Usually the best time to bust the cache is when the user navigates away from the part of the single-page-app that cares about the specific url/restful-object. This is just the best practice for SPAs, since the page is never refreshed and we want the object to be re-fetched if users go away from part of the app and then come back later.
    • If anyone performs a PUT or PATCH to a url that has been cached, it is assumed that the response to that PUT or PATCH is the full object (the same as if a GET had been performed). That assumption allows us to not make another GET request immediately after a PUT or PATCH. Because of that assumption, however, you have to be aware of if the RESTful url you are hitting actually meets that assumption or not.
    • This API tries to be smart about query params in urls and should work just fine no matter what you do with query params.
    • If anyone performs a plain old fetcher GET on a url that is cached, when the response comes back it will automatically update the cache and its subscribers.
  • forceBustCache(url): This function will forcibly bust the cache for a URL, and also trigger a re-fetch of the URL so that all subscribers to the cache are notified of the new change. It returns a promise just the same as if you had done fetch(url).

  • bustCacheForSubscriptionDuration(subscriptionDuration, [refetchCachedUrls = false]): Will bust the cache for all subscriptionDuration key/url hash that ===what you pass in. By default, it will notify all observers that the observable returned from fetchWithSharedCache is completed., but optionally you can make refetchCachedUrls = true to have all subjects refetch their data and update the cache instead of being removed.

    Note: will only work for subscriptionDurations that are typeof string. Also see fetchWithSharedCache's subscriptionDuration for more info

  • fetchWithProgress(url, options): This function uses XMLHttpRequest to call an API so that the progress of the API request can be tracked. It returns an ConnectableObservable that will produce a stream of ProgressEvent objects, except the last value is the response data the server responds with. The ProgressEvent objects are also modified to include an overallProgress property, which is a number between 0 and 1 which approximates what percentage of the overall user wait time has occurred. Note that since it is a ConnectableObservable, you have to call connect() on it after you subscribe. The fetchWithProgress function takes two arguments: a string url and an object options. The following properties are supported in options:

    • method (required): a string for the HTTP method of the request
    • headers (optional): an object where the keys are header names and the values are header values.
    • body (optional): Something that you can call xhr.send(body). This is usually a string or FormData object. The FormData object is useful when you want to upload files.
    • baseServerLatency (optional): A number of milliseconds to use when approximating the overallProgress of the api request. This number represents how long it takes on average for the server to handle a small request. For example, file uploads incur a set amount of latency on the server even when the file size is very small. baseServerLatency defaults to 1500 ms, which seems about right for file uploads.

Examples

fetchWithSharedCache(
  `https://workflow.canopytax.com/api/clients/1`,
  `clients/1`
).subscribe((json) => {
  console.log(`Client name is ${json.clients.name}`);
});

fetchWithSharedCache(`https://workflow.canopytax.com/api/clients/1`, () => {
  const pleaseEndMySubscription = true;
  return pleaseEndMySubscription;
}).subscribe((json) => {
  console.log(`Client name is ${json.clients.name}`);
});

redirectOrCatch()

redirectOrCatch() is a shortcut for asyncStacktrace -> check for 403/404 and maybe redirect -> else catchSyncStacktrace. It checks the request error for 404/403, logs to console the failure, and redirect the user to relevant 404/403 page

import {redirectOrCatch} from 'fetcher!sofe';

useEffect(() => {
  const sub = getContact().subscribe(
    successFunction(),
    asyncStacktrace(err => {
      if (err.status === 404 || err.status === 403) {
        console.info(`api/contact returned ${err.status}, redirecting`);
        window.history.replaceState(null, "", `/#/${err.status}`);
      } else catchSyncStacktrace(err);
    }
  )
  return () => sub.unsubscribe()
}, [])

// becomes

useEffect(() => {
  const sub = getContact().subscribe(
    successFunction(),
    redirectOrCatch()
  )
  return () => sub.unsubscribe()
}, [])