Skip to content

Commit

Permalink
🏭 Add middlewares helper. (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
elbywan committed Nov 17, 2017
1 parent b43cb1b commit b9c6a65
Show file tree
Hide file tree
Showing 17 changed files with 372 additions and 285 deletions.
251 changes: 119 additions & 132 deletions README.md
Expand Up @@ -10,7 +10,7 @@
<a href="https://github.com/elbywan/wretch/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license-badge" height="20"></a>
</h1>
<h4 align="center">
A tiny (&lt; 2.2Kb g-zipped) wrapper built around fetch with an intuitive syntax.
A tiny (&lt; 2.4Kb g-zipped) wrapper built around fetch with an intuitive syntax.
</h4>
<h5 align="center">
<i>f[ETCH] [WR]apper</i>
Expand Down Expand Up @@ -473,135 +473,6 @@ wretch().polyfills({
})
```
Polyfills can be used to replicate the [middleware](http://expressjs.com/en/guide/using-middleware.html) style.
```javascript
/*
How to use a polyfill as a chain of middlewares
-----------------------------------------------
# Polyfill signature
(url: the url, options: standard fetch options) =>
a fetch response promise (see https://developer.mozilla.org/fr/docs/Web/API/Response)
# Middleware signature
(...options: a bunch of middleware options) =>
(next: the next middleware to call or fetch to end the chain) =>
Polyfill
*/

///////////////
// Samples //
///////////////

/* A simple delay middleware. */
const delayFetch = (delay) => (next = fetch) => (url, opts) => {
return new Promise(res => setTimeout(() => res(next(url, opts)), delay))
}

/* Returns the url and method without performing an actual request. */
const shortCircuit = () => (next = fetch) => (url, opts) => {
// We create a new Response object to comply because wretch expects that from fetch.
const response = new Response()
response.text = () => Promise.resolve(opts.method + "@" + url)
response.json = () => Promise.resolve({ url, method: opts.method })
// Instead of calling next(), returning a Response Promise bypasses the rest of the chain.
return Promise.resolve(response)
}

/* Logs all requests passing through. */
const logFetch = () => (next = fetch) => (url, opts) => {
console.log(opts.method + "@" + url)
return next(url, opts)
}

/* A throttling cache. */
const cacheFetch = (throttle = 0) => (next = fetch) => {

const cache = new Map()
const inflight = new Map()
const throttling = new Set()

return (url, opts) => {
const key = opts.method + "@" + url

if(!opts.noCache && throttling.has(key)) {
// If the cache contains a previous response and we are throttling, serve it and bypass the chain.
if(cache.has(key))
return Promise.resolve(cache.get(key).clone())
// If the request in already in-flight, wait until it is resolved
else if(inflight.has(key)) {
return new Promise(resolve => {
inflight.get(key).push(resolve)
})
}
}

// Init. the pending promises Map
if(!inflight.has(key))
inflight.set(key, [])

// If we are not throttling, activate the throttle for X milliseconds
if(throttle && !throttling.has(key)) {
throttling.add(key)
setTimeout(() => { throttling.delete(key) }, throttle)
}

// We call the next middleware in the chain.
return next(url, opts).then(_ => {
// Add a cloned response to the cache
cache.set(key, _.clone())
// Resolve pending promises
inflight.get(key).forEach(resolve => resolve(_.clone()))
// Remove the inflight pending promises
inflight.delete(key)
// Return the original response
return _
})
}
}

// A helper to ease middleware chaining with an elegant syntax :
const middlewares = (...middlewares) => (fetchFunction) => {
return (
middlewares.length === 0 ?
fetchFunction :
middlewares.length === 1 ?
middlewares[0](fetchFunction) :
middlewares.reduceRight((acc, curr, idx) =>
(idx === middlewares.length - 2) ? curr(acc(fetchFunction)) : curr(acc)
)
)
}

// Just use the "middleware" as a polyfill.
wretch().polyfills({
fetch: cacheFetch(1000)()
})

// You can also chain multiple "middlewares".
wretch().polyfills({
fetch: logFetch()(delayFetch(1000)(shortCircuit()()))
})

// And finally using the middlewares helper :

wretch().polyfills({
fetch: middlewares(cacheFetch(1000))(fetch)
})

wretch().polyfills({
fetch: middlewares(
logFetch(),
delayFetch(1000),
shortCircuit()
)(fetch)
})
```
## Body Types
*A body type is only needed when performing put/patch/post requests with a body.*
Expand Down Expand Up @@ -831,8 +702,8 @@ wretch("...").get().text(txt => console.log(txt))
*A set of extra features.*
| [Abortable requests](#abortable-requests) | [Performance API](#performance-api-experimental) |
|-----|-----|
| [Abortable requests](#abortable-requests) | [Performance API](#performance-api-experimental) | [Middlewares](#middlewares) |
|-----|-----|-----|
### Abortable requests
Expand Down Expand Up @@ -964,6 +835,122 @@ wretch().polyfills({
})
```
### Middlewares
Middlewares are functions that can intercept requests before being processed by Fetch.
Wretch includes a helper to help replicate the [middleware](http://expressjs.com/en/guide/using-middleware.html) style.
#### Signature
Basically a Middleware is a function having the following signature :
```ts
// A middleware accepts options and returns a configured version
type Middleware = (options?: {[key: string]: any}) => ConfiguredMiddleware
// A configured middleware (with options curried)
type ConfiguredMiddleware = (next: FetchLike) => FetchLike
// A "fetch like" function, accepting an url and fetch options and returning a response promise
type FetchLike = (url: string, opts: RequestInit) => Promise<Response>
```
#### middlewares(middlewares: ConfiguredMiddleware[], clear = false)
Add middlewares to intercept a request before being sent.
```javascript
/* A simple delay middleware. */
const delayMiddleware = delay => next => (url, opts) => {
return new Promise(res => setTimeout(() => res(next(url, opts)), delay))
}

// The request will be delayed by 1 second.
wretch("...").middlewares([
delayMiddleware(1000)
]).get().res(_ => /* ... */)
```
#### Middleware examples
```javascript
/* A simple delay middleware. */
const delayMiddleware = delay => next => (url, opts) => {
return new Promise(res => setTimeout(() => res(next(url, opts)), delay))
}

/* Returns the url and method without performing an actual request. */
const shortCircuitMiddleware = () => next => (url, opts) => {
// We create a new Response object to comply because wretch expects that from fetch.
const response = new Response()
response.text = () => Promise.resolve(opts.method + "@" + url)
response.json = () => Promise.resolve({ url, method: opts.method })
// Instead of calling next(), returning a Response Promise bypasses the rest of the chain.
return Promise.resolve(response)
}

/* Logs all requests passing through. */
const logMiddleware = () => next => (url, opts) => {
console.log(opts.method + "@" + url)
return next(url, opts)
}

/* A throttling cache. */
const cacheMiddleware = (throttle = 0) => {

const cache = new Map()
const inflight = new Map()
const throttling = new Set()

return next => (url, opts) => {
const key = opts.method + "@" + url

if(!opts.noCache && throttling.has(key)) {
// If the cache contains a previous response and we are throttling, serve it and bypass the chain.
if(cache.has(key))
return Promise.resolve(cache.get(key).clone())
// If the request in already in-flight, wait until it is resolved
else if(inflight.has(key)) {
return new Promise(resolve => {
inflight.get(key).push(resolve)
})
}
}

// Init. the pending promises Map
if(!inflight.has(key))
inflight.set(key, [])

// If we are not throttling, activate the throttle for X milliseconds
if(throttle && !throttling.has(key)) {
throttling.add(key)
setTimeout(() => { throttling.delete(key) }, throttle)
}

// We call the next middleware in the chain.
return next(url, opts).then(_ => {
// Add a cloned response to the cache
cache.set(key, _.clone())
// Resolve pending promises
inflight.get(key).forEach(resolve => resolve(_.clone()))
// Remove the inflight pending promises
inflight.delete(key)
// Return the original response
return _
})
}
}

// To call a single middleware
const cache = cacheMiddleware(1000)
wretch("...").middlewares([cache]).get()

// To chain middlewares
wretch("...").middlewares([
logMiddleware(),
delayMiddleware(1000),
shortCircuitMiddleware()
}).get().text(_ => console.log(text))
```
# License
MIT

0 comments on commit b9c6a65

Please sign in to comment.