12033: Log out and back in to a site without forgetting it.
[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 window.models = window.models || {}
6 window.models.Pager = function(loadFunc) {
7     // loadFunc(filters) must return a promise for a page of results.
8     var pager = this
9     Object.assign(pager, {
10         done: false,
11         items: m.stream(),
12         thresholdItem: null,
13         loadMore: function() {
14             // Get the next page, if there are any more items to get.
15             if (pager.done)
16                 return
17             var filters = pager.thresholdItem ? [
18                 ["modified_at", "<=", pager.thresholdItem.modified_at],
19                 ["uuid", "!=", pager.thresholdItem.uuid],
20             ] : []
21             loadFunc(filters).then(function(resp) {
22                 var items = pager.items() || []
23                 Array.prototype.push.apply(items, resp.items)
24                 if (resp.items.length == 0)
25                     pager.done = true
26                 else
27                     pager.thresholdItem = resp.items[resp.items.length-1]
28                 pager.items(items)
29             })
30         },
31     })
32 }
33
34 // MultisiteLoader loads pages of results from multiple API sessions
35 // and merges them into a single result set.
36 //
37 // The constructor implicitly starts an initial page load for each
38 // session.
39 //
40 // new MultisiteLoader({loadFunc: function(session, filters){...},
41 // sessionDB: new window.models.SessionDB()}
42 //
43 // At any given time, ml.loadMore will be either false (meaning a page
44 // load is in progress or there are no more results to fetch) or a
45 // function that starts loading more results.
46 //
47 // loadFunc() must retrieve results in "modified_at desc" order.
48 window.models = window.models || {}
49 window.models.MultisiteLoader = function(config) {
50     var loader = this
51     if (!(config.loadFunc && config.sessionDB))
52         throw new Error("MultisiteLoader constructor requires loadFunc and sessionDB")
53     Object.assign(loader, config, {
54         // Sorted items ready to display, merged from all pagers.
55         displayable: [],
56         done: false,
57         pagers: {},
58         loadMore: false,
59         // Number of undisplayed items to keep on hand for each result
60         // set. When hitting "load more", if a result set already has
61         // this many additional results available, we don't bother
62         // fetching a new page. This is the _minimum_ number of rows
63         // that will be added to loader.displayable in each "load
64         // more" event (except for the case where all items are
65         // displayed).
66         lowWaterMark: 23,
67     })
68     var sessions = loader.sessionDB.loadActive()
69     m.stream.merge(Object.keys(sessions).map(function(key) {
70         var pager = new window.models.Pager(loader.loadFunc.bind(null, sessions[key]))
71         loader.pagers[key] = pager
72         pager.loadMore()
73         // Resolve the stream with the session key when the results
74         // arrive.
75         return pager.items.map(function() { return key })
76     })).map(function(keys) {
77         // Top (most recent) of {bottom (oldest) entry of any pager
78         // that still has more pages left to fetch}
79         var cutoff
80         keys.forEach(function(key) {
81             var pager = loader.pagers[key]
82             var items = pager.items()
83             if (items.length == 0 || pager.done)
84                 return
85             var last = items[items.length-1].modified_at
86             if (!cutoff || cutoff < last)
87                 cutoff = last
88         })
89         var combined = []
90         keys.forEach(function(key) {
91             var pager = loader.pagers[key]
92             pager.itemsDisplayed = 0
93             pager.items().every(function(item) {
94                 if (cutoff && item.modified_at < cutoff)
95                     // Some other pagers haven't caught up to this
96                     // point, so don't display this item or anything
97                     // after it.
98                     return false
99                 item.session = sessions[key]
100                 combined.push(item)
101                 pager.itemsDisplayed++
102                 return true // continue
103             })
104         })
105         loader.displayable = combined.sort(function(a, b) {
106             return a.modified_at < b.modified_at ? 1 : -1
107         })
108         // Make a new loadMore function that hits the pagers (if
109         // necessary according to lowWaterMark)... or set
110         // loader.loadMore to false if there is nothing left to fetch.
111         var loadable = []
112         Object.keys(loader.pagers).map(function(key) {
113             if (!loader.pagers[key].done)
114                 loadable.push(loader.pagers[key])
115         })
116         if (loadable.length == 0) {
117             loader.done = true
118             loader.loadMore = false
119         } else
120             loader.loadMore = function() {
121                 loader.loadMore = false
122                 loadable.map(function(pager) {
123                     if (pager.items().length - pager.itemsDisplayed < loader.lowWaterMark)
124                         pager.loadMore()
125                 })
126             }
127     })
128 }