Skip to content

Instantly share code, notes, and snippets.

@whiteinge
Last active July 27, 2023 10:35
Show Gist options
  • Save whiteinge/7721a637afd4c001313514062bd1bdbb to your computer and use it in GitHub Desktop.
Save whiteinge/7721a637afd4c001313514062bd1bdbb to your computer and use it in GitHub Desktop.
Use an ADT with RxJS to model a complete ajax request/response life cycle (in batches!)
<!doctype html>
<html>
<div id="content"></div>
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
}
blockquote { font-size: 21px; line-height: 30px; }
pre {
font-family: "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace;
font-size: 13px;
line-height: 18.5714px;
}
h1, h2, h3, h4 {
font-family: Garamond, Baskerville, "Baskerville Old Face", "Hoefler Text", "Times New Roman", serif;
font-weight: 500;
}
h1 { font-size: 24px; line-height: 26.4px; }
h3 { font-size: 14px; line-height: 15.4px; }
.success { color: #2ECC40; }
.success a { color: #2ECC40; }
.failure { color: #FF4136; }
.spinner {
margin: 0;
display: inline-block;
font-size: 2em;
animation-name: spin;
animation-duration: 1000ms;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
@keyframes spin {
from {transform:rotate(360deg);}
to {transform:rotate(0deg);}
}
</style>
<script src="https://unpkg.com/rx@4.1.0/dist/rx.all.min.js"></script>
<script src="https://unpkg.com/rx-dom@7.0.3/dist/rx.dom.min.js"></script>
<script src="https://unpkg.com/lodash@4.17.4/lodash.min.js"></script>
<script src="wrapXhr.js"></script>
</html>
class XhrResult {
constructor(val, type) { this.val = val; this.type = type }
inspect() { return `${this.type}: ${JSON.stringify(this.val)}` }
map(f) { return this.type === 'Right'
? XhrResult.Right(f(this.val)) : this }
chain(f) { return this.type === 'Right' ? f(this.val) : this }
fold(f, g, h, i) {
switch(this.type) {
case 'Left': return f(this.val);
case 'Right': return g(this.val);
case 'Loading': return h(this.val);
case 'Initial': return i(this.val);
}
}
static Left(x) { return new XhrResult(x, 'Left') }
static Right(x) { return new XhrResult(x, 'Right') }
static Loading(x) { return new XhrResult(x, 'Loading') }
static Initial(x) { return new XhrResult(x, 'Initial') }
static of(x) { return XhrResult.Right(x) }
}
function wrapResponse(resp) {
return (resp.status >= 200 && resp.status < 300)
? XhrResult.Right(resp)
: XhrResult.Left(resp);
}
function wrapXhr(ox) {
const cacheLookupTimeout = 10;
return ox.publish(oy => oy
.map(resp => Rx.Observable.just(resp).map(wrapResponse))
.takeUntilWithTime(cacheLookupTimeout)
.defaultIfEmpty(Rx.Observable.just(XhrResult.Loading())
.concat(oy.map(wrapResponse)))
.mergeAll());
}
// ----------------------------------------------------------------------------
// Fake a list of user names.
// A map of all possible users to use for the initial render.
// NOTE: switch these comments to hit the real GitHub API.
const userList = _.range(30).map(x => `user-${x}`);
// const userList = ["mojombo", "defunkt", "pjhyett", "wycats", "ezmobius",
// "ivey", "evanphx", "vanpelt", "wayneeseguin", "brynary", "kevinclark",
// "technoweenie", "macournoyer", "takeo", "Caged", "topfunky", "anotherjesse",
// "roland", "lukas", "fanvsfan", "tomtt", "railsjitsu", "nitay", "kevwil",
// "KirinDave", "jamesgolick", "atmos", "errfree", "mojodna", "bmizerany"];
const defaultUserMap = userList.reduce(function(acc, user) {
acc[user] = XhrResult.Initial();
return acc;
}, {});
function makeXhr(login) {
// NOTE: switch these comments to hit the real GitHub API.
// return Rx.DOM.get({url: `https://api.github.com/users/${login}`, responseType: 'json'});
return Rx.Observable.just(login)
.delay(_.random(100, 3000))
.map(login => ({
status: _.sample([
200, 200, 200, 200, 200, 200, 200, 200, 200, 200,
404, 500, 302,
]),
response: {
login,
html_url: `https://github.com/${login}`,
},
}));
}
// ---
const Dispatcher = new Rx.Subject();
const send = (tag, arg) => ev => Dispatcher.onNext({
tag,
data: typeof arg === 'function' ? arg(ev) : arg,
})
const maxParallelCalls = 5;
// NOTE: Switch these comments to toggle between all-at-once and manually
// one-by-one.
const UsersStore = Rx.Observable.from(userList)
// const UsersStore = Dispatcher
// .filter(x => x.tag === 'DETAIL').pluck('data')
.flatMapWithMaxConcurrent(maxParallelCalls, user => makeXhr(user)
.let(wrapXhr)
.map(ret => ({user, ret})))
.scan(function(acc, {user, ret}) {
acc[user] = ret;
return acc;
}, defaultUserMap)
.startWith(defaultUserMap);
const showError = err => `
<span class="failure">
Failed to load user details: ${err.status}
</span>
`;
const showDetail = ret => `
<span class="success">
Got <a href="${ret.response.html_url}">${ret.response.html_url}</a>
</span>
`;
const showSpinner = () => `
<span class="spinner">${String.fromCodePoint(0x1F300)}</span>
`;
const showPlaceholder = user => `
<button onclick="send('DETAIL', '${user}')(event)">Get</button>
`;
const UsersView = UsersStore.map(users => [].concat(
'<div>',
'<h1>Load All the Users!</h1>',
'<ul>',
Object.entries(users).map(([user, ret]) => [].concat(
'<li>',
`${user}: `,
ret.fold(
showError,
showDetail,
showSpinner,
() => showPlaceholder(user)),
'</li>').join('\n')).join('\n'),
'</ul>',
'</div>',
).join(''));
// ----------------------------------------------------------------------------
// Demo!
const log = Dispatcher.subscribe(x => console.log('Dispatching', x));
const render = el => content => el.innerHTML = content;
const sub = UsersView.subscribe(render(document.querySelector('#content')));
@whiteinge
Copy link
Author

whiteinge commented Jun 3, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment