Last active
July 27, 2023 10:35
-
-
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!)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'))); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Live demo on blocks.roadtolarissa.com