/**
* This class manages a sparse collection of `Page` objects keyed by their page number.
* Pages are lazily created on request by the `getPage` method.
*
* When pages are locked, they are scheduled to be loaded. The loading is prioritized by
* the type of lock held on the page. Pages with "active" locks are loaded first while
* those with "prefetch" locks are loaded only when no "active" locked pages are in the
* queue.
*
* The value of the `concurrentLoading` config controls the maximum number of simultaneously
* pending, page load requests.
*
* @private
* @since 6.5.0
*/
Ext.define('Ext.data.virtual.PageMap', {
requires: [
'Ext.data.virtual.Page'
],
isVirtualPageMap: true,
config: {
/**
* @cfg {Number} cacheSize
* The number of pages to retain in the `cache`.
*/
cacheSize: 10,
/**
* @cfg {Number} concurrentLoading
* The maximum number of simultaneous load requests that should be made to the
* server for pages.
*/
concurrentLoading: 1,
/**
* The number of pages in the data set.
*/
pageCount: null
},
generation: 0,
store: null,
constructor: function(config) {
var me = this;
me.prefetchSortFn = me.prefetchSortFn.bind(me);
me.initConfig(config);
me.clear();
},
destroy: function() {
this.clear(true);
this.callParent();
},
canSatisfy: function(range) {
var end = this.getPageIndex(range.end),
pageCount = this.getPageCount();
return pageCount === null || end < pageCount;
},
clear: function(destroy) {
var me = this,
alive = !destroy || null,
pages = me.pages,
pg;
++me.generation;
/**
* @property {Object} byId
* A map of records by their `idProperty`.
*/
me.byId = alive && {};
/**
* @property {Object} byInternalId
* A map of records by their `internalId`.
*/
me.byInternalId = alive && {};
/**
* @property {Ext.data.virtual.Page[]} cache
* The array of unlocked pages with the oldest at the front and the newest (most
* recently unlocked) page at the end.
* @readonly
*/
me.cache = alive && [];
/**
* @property {Object} indexMap
* A map of record indices by their `internalId`.
*/
me.indexMap = alive && {};
/**
* @property {Object} pages
* The sparse collection of `Page` objects keyed by their page number.
* @readonly
*/
me.pages = alive && {};
/**
* @property {Ext.data.virtual.Page[]} loading
* The array of currently loading pages.
*/
me.loading = alive && [];
/**
* @property {Object} loadQueues
* A collection of loading queues keyed by the lock state.
* @property {Ext.data.virtual.Page[]} loadQueues.active The queue of pages to
* load that have an "active" lock state.
* @property {Ext.data.virtual.Page[]} loadQueues.prefetch The queue of pages to
* load that have a "prefetch" lock state.
*/
me.loadQueues = alive && {
active: [],
prefetch: []
};
if (pages) {
for (pg in pages) {
me.destroyPage(pages[pg]);
}
}
},
getPage: function(number, autoCreate) {
var me = this,
pageCount = me.getPageCount(),
pages = me.pages,
page;
if (pageCount === null || number < pageCount) {
page = pages[number];
if (!page && autoCreate !== false) {
pages[number] = page = new Ext.data.virtual.Page({
pageMap: me,
number: number
});
}
}
//<debug>
else {
Ext.raise('Invalid page number ' + number + ' when limit is ' + pageCount);
}
//</debug>
return page || null;
},
getPageIndex: function(index) {
if (index.isEntity) {
index = this.indexOf(index);
}
return Math.floor(index / this.store.getPageSize());
},
getPageOf: function(index, autoCreate) {
var pageSize = this.store.getPageSize(),
n = Math.floor(index / pageSize);
return this.getPage(n, autoCreate);
},
getPages: function(begin, end) {
var pageSize = this.store.getPageSize(),
// Convert record indices into page numbers:
first = Math.floor(begin / pageSize),
last = Math.ceil(end / pageSize),
ret = {},
n;
for (n = first; n < last; ++n) {
ret[n] = this.getPage(n);
}
return ret;
},
flushNextLoad: function() {
var me = this,
queueTimer = me.queueTimer;
if (queueTimer) {
Ext.unasap(queueTimer);
}
me.loadNext();
},
indexOf: function(record) {
var ret;
// return indexMap if record is not null/undefined
if (record) {
ret = this.indexMap[record.internalId];
}
return (ret || ret === 0) ? ret : -1;
},
getByInternalId: function(internalId) {
var index = this.indexMap[internalId],
page;
if (index || index === 0) {
page = this.pages[Math.floor(index / this.store.getPageSize())];
if (page) {
return page.records[index - page.begin];
}
}
},
updatePageCount: function(pageCount, oldPageCount) {
var pages = this.pages,
pageNumber, page;
if (oldPageCount === null || pageCount < oldPageCount) {
// Safe to delete during a for in
for (pageNumber in pages) {
page = pages[pageNumber];
if (page.number >= pageCount) {
this.clearPage(page);
this.destroyPage(page);
}
}
}
},
privates: {
queueTimer: null,
clearPage: function(page, fromCache) {
var me = this,
A = Ext.Array,
loadQueues = me.loadQueues;
delete me.pages[page.number];
page.clearRecords(me.byId, 'id');
page.clearRecords(me.byInternalId, 'internalId');
page.clearRecords(me.indexMap, 'internalId');
A.remove(loadQueues.active, page);
A.remove(loadQueues.prefetch, page);
if (!fromCache) {
Ext.Array.remove(me.cache, page);
}
},
destroyPage: function(page) {
this.store.onPageDestroy(page);
page.destroy();
},
loadNext: function() {
var me = this,
loading = me.loading,
loadQueues = me.loadQueues,
concurrency, page;
if (me.destroyed) {
return;
}
concurrency = me.getConcurrentLoading();
me.queueTimer = null;
// Keep pulling from the queue(s) as long as we have more concurrency
// allowed...
while (loading.length < concurrency) {
if (!(page = loadQueues.active.shift() || loadQueues.prefetch.shift())) {
break;
}
loading.push(page);
page.load();
}
},
onPageLoad: function(page) {
var me = this,
store = me.store,
activeRanges = store.activeRanges,
n = activeRanges.length,
i;
Ext.Array.remove(me.loading, page);
if (!page.error) {
page.fillRecords(me.byId, 'id');
page.fillRecords(me.byInternalId, 'internalId');
page.fillRecords(me.indexMap, 'internalId', true);
store.onPageDataAcquired(page);
for (i = 0; i < n; ++i) {
activeRanges[i].onPageLoad(page);
}
}
me.flushNextLoad();
},
onPageLockChange: function(page, state, oldState) {
var me = this,
cache = me.cache,
loadQueues = me.loadQueues,
store = me.store,
cacheSize, concurrency;
// When a page that has never been loaded becomes locked, we want to put
// it in the appropriate loadQueue. It is also possible for the lock state
// to change while waiting in a loadQueue, so we may need to move it around
// while it waits...
if (page.isInitial()) {
if (oldState) {
Ext.Array.remove(loadQueues[oldState], page);
}
if (state) {
loadQueues[state].push(page);
concurrency = me.getConcurrentLoading();
// Initiating loads immediately can easily cause problems, so wait
// for a tick before firing off the loads.
if (!me.queueTimer && me.loading.length < concurrency) {
me.queueTimer = Ext.asap(me.loadNext, me);
}
}
}
if (state) {
if (!oldState) {
// Make sure the page is not in the LRU queue for recycling. If it
// was previously not locked (!oldState) then the page is in line
// for removal...
Ext.Array.remove(cache, page);
}
}
else {
cache.push(page); // put MRU item at the end
for (cacheSize = me.getCacheSize(); cache.length > cacheSize;) {
page = cache.shift();
me.clearPage(page, true); // remove LRU item
store.onPageEvicted(page);
me.destroyPage(page);
}
}
},
prefetchSortFn: function(a, b) {
a = a.number;
b = b.number;
/* eslint-disable-next-line vars-on-top */
var M = Math,
firstPage = this.sortFirstPage,
lastPage = this.sortLastPage,
direction = this.sortDirection,
aDir = a < firstPage,
bDir = b < firstPage,
ret;
a = aDir ? M.abs(firstPage - a) : M.abs(lastPage - a);
b = bDir ? M.abs(firstPage - b) : M.abs(lastPage - b);
if (a === b) {
ret = aDir ? direction : -direction;
}
else {
ret = a - b;
}
return ret;
},
prioritizePrefetch: function(direction, firstPage, lastPage) {
var me = this;
me.sortDirection = direction;
me.sortFirstPage = firstPage;
me.sortLastPage = lastPage;
me.loadQueues.prefetch.sort(me.prefetchSortFn);
}
}
});