// response. loadFunc() must retrieve results in "modified_at desc"
// order.
//
-// done is true if there are no more pages to load.
-//
-// loading is true if a network request is in progress.
+// state is:
+// * 'loading' if a network request is in progress;
+// * 'done' if there are no more items to load;
+// * 'ready' otherwise.
//
// items is a stream that resolves to an array of all items retrieved so far.
//
// loadMore() loads the next page, if any.
-window.models = window.models || {}
-window.models.MultipageLoader = function(config) {
+window.MultipageLoader = function(config) {
var loader = this
Object.assign(loader, config, {
- done: false,
- loading: false,
- items: m.stream(),
+ state: 'ready',
+ DONE: 'done',
+ LOADING: 'loading',
+ READY: 'ready',
+
+ items: m.stream([]),
thresholdItem: null,
loadMore: function() {
- if (loader.done || loader.loading)
+ if (loader.state == loader.DONE || loader.state == loader.LOADING)
return
var filters = loader.thresholdItem ? [
["modified_at", "<=", loader.thresholdItem.modified_at],
["uuid", "!=", loader.thresholdItem.uuid],
] : []
- loader.loading = true
+ loader.state = loader.LOADING
loader.loadFunc(filters).then(function(resp) {
- var items = loader.items() || []
+ var items = loader.items()
Array.prototype.push.apply(items, resp.items)
- if (resp.items.length == 0)
- loader.done = true
- else
+ if (resp.items.length == 0) {
+ loader.state = loader.DONE
+ } else {
loader.thresholdItem = resp.items[resp.items.length-1]
- loader.loading = false
+ loader.state = loader.READY
+ }
loader.items(items)
}).catch(function(err) {
loader.err = err
- loader.loading = false
+ loader.state = loader.READY
})
},
})
loader.loadMore()
}
-// MultisiteLoader loads pages of results from multiple API sessions
-// and merges them into a single result set.
-//
-// The constructor implicitly starts an initial page load for each
-// session.
+// MergingLoader merges results from multiple loaders (given in the
+// config.children array) into a single result set.
//
-// new MultisiteLoader({loadFunc: function(session, filters){...},
-// sessionDB: new window.models.SessionDB()}
+// new MergingLoader({children: [loader, loader, ...]})
//
-// loadFunc() must retrieve results in "modified_at desc" order.
-//
-// (TODO? This could split into two parts: "make a loader for each
-// session, attaching session to each returned item", and "merge items
-// from N loaders".)
-window.models = window.models || {}
-window.models.MultisiteLoader = function(config) {
+// The children must retrieve results in "modified_at desc" order.
+window.MergingLoader = function(config) {
var loader = this
- if (!(config.loadFunc && config.sessionDB))
- throw new Error("MultisiteLoader constructor requires loadFunc and sessionDB")
Object.assign(loader, config, {
- sessions: config.sessionDB.loadActive(),
// Sorted items ready to display, merged from all children.
- items: m.stream(),
- done: false,
- children: {},
- loading: false,
+ items: m.stream([]),
+ state: 'ready',
+ DONE: 'done',
+ LOADING: 'loading',
+ READY: 'ready',
loadable: function() {
// Return an array of children that we could call
- // loadMore() on. Update loader.done and loader.loading.
- loader.done = true
- loader.loading = false
- return Object.keys(loader.children)
- .map(function(key) { return loader.children[key] })
- .filter(function(child) {
- if (child.done)
- return false
- loader.done = false
- if (!child.loading)
- return true
- loader.loading = true
+ // loadMore() on. Update loader.state.
+ loader.state = loader.DONE
+ return loader.children.filter(function(child) {
+ if (child.state == child.DONE)
return false
- })
+ if (child.state == child.LOADING) {
+ loader.state = loader.LOADING
+ return false
+ }
+ if (loader.state == loader.DONE)
+ loader.state = loader.READY
+ return true
+ })
},
loadMore: function() {
// Call loadMore() on children that have reached
// lowWaterMark.
loader.loadable().map(function(child) {
if (child.items().length - child.itemsDisplayed < loader.lowWaterMark) {
- loader.loading = true
+ loader.state = loader.LOADING
child.loadMore()
}
})
},
mergeItems: function() {
- var keys = Object.keys(loader.sessions)
- // cutoff is the topmost (recent) of {bottom (oldest) entry of
- // any child that still has more pages left to fetch}
+ // We want to avoid moving items around on the screen once
+ // they're displayed.
+ //
+ // To this end, here we find the last safely displayable
+ // item ("cutoff") by getting the last item from each
+ // unfinished child, and taking the topmost (most recent)
+ // one of those.
+ //
+ // (If we were to display an item below that cutoff, the
+ // next page of results from an unfinished child could
+ // include items that get inserted above the cutoff,
+ // causing the cutoff item to move down.)
var cutoff
- keys.forEach(function(key) {
- var child = loader.children[key]
+ var cutoffUnknown = false
+ loader.children.forEach(function(child) {
+ if (child.state == child.DONE)
+ return
var items = child.items()
- if (items.length == 0 || child.done)
+ if (items.length == 0) {
+ // No idea what's coming in the next page.
+ cutoffUnknown = true
return
+ }
var last = items[items.length-1].modified_at
if (!cutoff || cutoff < last)
cutoff = last
})
+ if (cutoffUnknown)
+ return
var combined = []
- keys.forEach(function(key) {
- var child = loader.children[key]
+ loader.children.forEach(function(child) {
child.itemsDisplayed = 0
child.items().every(function(item) {
if (cutoff && item.modified_at < cutoff)
- // Some other children haven't caught up to this
- // point, so don't display this item or anything
- // after it.
+ // Don't display this item or anything after
+ // it (see "cutoff" comment above).
return false
- item.session = loader.sessions[key]
combined.push(item)
child.itemsDisplayed++
return true // continue
// event (except for the case where all items are displayed).
lowWaterMark: 23,
})
- var childrenItems = Object.keys(loader.sessions).map(function(key) {
- var child = new window.models.MultipageLoader({
- loadFunc: loader.loadFunc.bind(null, loader.sessions[key]),
- })
- loader.children[key] = child
- // Resolve with the session key whenever results arrive for
- // that session.
+ var childrenReady = m.stream.merge(loader.children.map(function(child) {
return child.items
- })
- var childrenReady = m.stream.merge(childrenItems)
+ }))
childrenReady.map(loader.loadable)
childrenReady.map(loader.mergeItems)
}