12033: Move classes to global namespace.
[arvados.git] / apps / workbench / app / assets / javascripts / models / loader.js
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 // MultipageLoader retrieves a multi-page result set from the
6 // server. The constructor initiates the first page load.
7 //
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"
11 // order.
12 //
13 // done is true if there are no more pages to load.
14 //
15 // loading is true if a network request is in progress.
16 //
17 // items is a stream that resolves to an array of all items retrieved so far.
18 //
19 // loadMore() loads the next page, if any.
20 window.MultipageLoader = function(config) {
21     var loader = this
22     Object.assign(loader, config, {
23         done: false,
24         loading: false,
25         items: m.stream(),
26         thresholdItem: null,
27         loadMore: function() {
28             if (loader.done || loader.loading)
29                 return
30             var filters = loader.thresholdItem ? [
31                 ["modified_at", "<=", loader.thresholdItem.modified_at],
32                 ["uuid", "!=", loader.thresholdItem.uuid],
33             ] : []
34             loader.loading = true
35             loader.loadFunc(filters).then(function(resp) {
36                 var items = loader.items() || []
37                 Array.prototype.push.apply(items, resp.items)
38                 if (resp.items.length == 0)
39                     loader.done = true
40                 else
41                     loader.thresholdItem = resp.items[resp.items.length-1]
42                 loader.loading = false
43                 loader.items(items)
44             }).catch(function(err) {
45                 loader.err = err
46                 loader.loading = false
47             })
48         },
49     })
50     loader.loadMore()
51 }
52
53 // MergingLoader merges results from multiple loaders (given in the
54 // config.children array) into a single result set.
55 //
56 // new MergingLoader({children: [loader, loader, ...]})
57 //
58 // The children must retrieve results in "modified_at desc" order.
59 window.MergingLoader = function(config) {
60     var loader = this
61     Object.assign(loader, config, {
62         // Sorted items ready to display, merged from all children.
63         items: m.stream(),
64         done: false,
65         loading: false,
66         loadable: function() {
67             // Return an array of children that we could call
68             // loadMore() on. Update loader.done and loader.loading.
69             loader.done = true
70             loader.loading = false
71             return loader.children.filter(function(child) {
72                 if (child.done)
73                     return false
74                 loader.done = false
75                 if (!child.loading)
76                     return true
77                 loader.loading = true
78                 return false
79             })
80         },
81         loadMore: function() {
82             // Call loadMore() on children that have reached
83             // lowWaterMark.
84             loader.loadable().map(function(child) {
85                 if (child.items().length - child.itemsDisplayed < loader.lowWaterMark) {
86                     loader.loading = true
87                     child.loadMore()
88                 }
89             })
90         },
91         mergeItems: function() {
92             // cutoff is the topmost (recent) of {bottom (oldest) entry of
93             // any child that still has more pages left to fetch}
94             var cutoff
95             loader.children.forEach(function(child) {
96                 var items = child.items()
97                 if (items.length == 0 || child.done)
98                     return
99                 var last = items[items.length-1].modified_at
100                 if (!cutoff || cutoff < last)
101                     cutoff = last
102             })
103             var combined = []
104             loader.children.forEach(function(child) {
105                 child.itemsDisplayed = 0
106                 child.items().every(function(item) {
107                     if (cutoff && item.modified_at < cutoff)
108                         // Some other children haven't caught up to this
109                         // point, so don't display this item or anything
110                         // after it.
111                         return false
112                     combined.push(item)
113                     child.itemsDisplayed++
114                     return true // continue
115                 })
116             })
117             loader.items(combined.sort(function(a, b) {
118                 return a.modified_at < b.modified_at ? 1 : -1
119             }))
120         },
121         // Number of undisplayed items to keep on hand for each result
122         // set. When hitting "load more", if a result set already has
123         // this many additional results available, we don't bother
124         // fetching a new page. This is the _minimum_ number of rows
125         // that will be added to loader.items in each "load more"
126         // event (except for the case where all items are displayed).
127         lowWaterMark: 23,
128     })
129     var childrenReady = m.stream.merge(loader.children.map(function(child) {
130         return child.items
131     }))
132     childrenReady.map(loader.loadable)
133     childrenReady.map(loader.mergeItems)
134 }