1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 // MultipageLoader retrieves a multi-page result set from the
6 // server. The constructor initiates the first page load.
8 // config.loadFunc is a function that accepts an array of
9 // paging-related filters, and returns a promise for the API
10 // response. loadFunc() must retrieve results in "modified_at desc"
13 // done is true if there are no more pages to load.
15 // loading is true if a network request is in progress.
17 // items is a stream that resolves to an array of all items retrieved so far.
19 // loadMore() loads the next page, if any.
20 window.models = window.models || {}
21 window.models.MultipageLoader = function(config) {
23 Object.assign(loader, config, {
28 loadMore: function() {
29 if (loader.done || loader.loading)
31 var filters = loader.thresholdItem ? [
32 ["modified_at", "<=", loader.thresholdItem.modified_at],
33 ["uuid", "!=", loader.thresholdItem.uuid],
36 loader.loadFunc(filters).then(function(resp) {
37 var items = loader.items() || []
38 Array.prototype.push.apply(items, resp.items)
39 if (resp.items.length == 0)
42 loader.thresholdItem = resp.items[resp.items.length-1]
43 loader.loading = false
45 }).catch(function(err) {
47 loader.loading = false
54 // MultisiteLoader loads pages of results from multiple API sessions
55 // and merges them into a single result set.
57 // The constructor implicitly starts an initial page load for each
60 // new MultisiteLoader({loadFunc: function(session, filters){...},
61 // sessionDB: new window.models.SessionDB()}
63 // loadFunc() must retrieve results in "modified_at desc" order.
65 // (TODO? This could split into two parts: "make a loader for each
66 // session, attaching session to each returned item", and "merge items
68 window.models = window.models || {}
69 window.models.MultisiteLoader = function(config) {
71 if (!(config.loadFunc && config.sessionDB))
72 throw new Error("MultisiteLoader constructor requires loadFunc and sessionDB")
73 Object.assign(loader, config, {
74 sessions: config.sessionDB.loadActive(),
75 // Sorted items ready to display, merged from all children.
80 loadable: function() {
81 // Return an array of children that we could call
82 // loadMore() on. Update loader.done and loader.loading.
84 loader.loading = false
85 return Object.keys(loader.children)
86 .map(function(key) { return loader.children[key] })
87 .filter(function(child) {
97 loadMore: function() {
98 // Call loadMore() on children that have reached
100 loader.loadable().map(function(child) {
101 if (child.items().length - child.itemsDisplayed < loader.lowWaterMark) {
102 loader.loading = true
107 mergeItems: function() {
108 var keys = Object.keys(loader.sessions)
109 // cutoff is the topmost (recent) of {bottom (oldest) entry of
110 // any child that still has more pages left to fetch}
112 keys.forEach(function(key) {
113 var child = loader.children[key]
114 var items = child.items()
115 if (items.length == 0 || child.done)
117 var last = items[items.length-1].modified_at
118 if (!cutoff || cutoff < last)
122 keys.forEach(function(key) {
123 var child = loader.children[key]
124 child.itemsDisplayed = 0
125 child.items().every(function(item) {
126 if (cutoff && item.modified_at < cutoff)
127 // Some other children haven't caught up to this
128 // point, so don't display this item or anything
131 item.session = loader.sessions[key]
133 child.itemsDisplayed++
134 return true // continue
137 loader.items(combined.sort(function(a, b) {
138 return a.modified_at < b.modified_at ? 1 : -1
141 // Number of undisplayed items to keep on hand for each result
142 // set. When hitting "load more", if a result set already has
143 // this many additional results available, we don't bother
144 // fetching a new page. This is the _minimum_ number of rows
145 // that will be added to loader.items in each "load more"
146 // event (except for the case where all items are displayed).
149 var childrenItems = Object.keys(loader.sessions).map(function(key) {
150 var child = new window.models.MultipageLoader({
151 loadFunc: loader.loadFunc.bind(null, loader.sessions[key]),
153 loader.children[key] = child
154 // Resolve with the session key whenever results arrive for
158 var childrenReady = m.stream.merge(childrenItems)
159 childrenReady.map(loader.loadable)
160 childrenReady.map(loader.mergeItems)