// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 // MultipageLoader retrieves a multi-page result set from the // server. The constructor initiates the first page load. // // config.loadFunc is a function that accepts an array of // paging-related filters, and returns a promise for the API // response. loadFunc() must retrieve results in "modified_at desc" // order. // // 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.MultipageLoader = function(config) { var loader = this Object.assign(loader, config, { state: 'ready', DONE: 'done', LOADING: 'loading', READY: 'ready', items: m.stream([]), thresholdItem: null, loadMore: function() { 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.state = loader.LOADING loader.loadFunc(filters).then(function(resp) { var items = loader.items() Array.prototype.push.apply(items, resp.items) if (resp.items.length == 0) { loader.state = loader.DONE } else { loader.thresholdItem = resp.items[resp.items.length-1] loader.state = loader.READY } loader.items(items) }).catch(function(err) { loader.err = err loader.state = loader.READY }) }, }) loader.loadMore() } // MergingLoader merges results from multiple loaders (given in the // config.children array) into a single result set. // // new MergingLoader({children: [loader, loader, ...]}) // // The children must retrieve results in "modified_at desc" order. window.MergingLoader = function(config) { var loader = this Object.assign(loader, config, { // Sorted items ready to display, merged from all children. 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.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.state = loader.LOADING child.loadMore() } }) }, mergeItems: function() { // 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 var cutoffUnknown = false loader.children.forEach(function(child) { if (child.state == child.DONE) return var items = child.items() 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 = [] loader.children.forEach(function(child) { child.itemsDisplayed = 0 child.items().every(function(item) { if (cutoff && item.modified_at < cutoff) // Don't display this item or anything after // it (see "cutoff" comment above). return false combined.push(item) child.itemsDisplayed++ return true // continue }) }) loader.items(combined.sort(function(a, b) { return a.modified_at < b.modified_at ? 1 : -1 })) }, // Number of undisplayed items to keep on hand for each result // set. When hitting "load more", if a result set already has // this many additional results available, we don't bother // fetching a new page. This is the _minimum_ number of rows // that will be added to loader.items in each "load more" // event (except for the case where all items are displayed). lowWaterMark: 23, }) var childrenReady = m.stream.merge(loader.children.map(function(child) { return child.items })) childrenReady.map(loader.loadable) childrenReady.map(loader.mergeItems) }