bootstrap-table-pipeline.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. /**
  2. * @author doug-the-guy
  3. * @version v1.0.0
  4. *
  5. * Bootstrap Table Pipeline
  6. * -----------------------
  7. *
  8. * This plugin enables client side data caching for server side requests which will
  9. * eliminate the need to issue a new request every page change. This will allow
  10. * for a performance balance for a large data set between returning all data at once
  11. * (client side paging) and a new server side request (server side paging).
  12. *
  13. * There are two new options:
  14. * - usePipeline: enables this feature
  15. * - pipelineSize: the size of each cache window
  16. *
  17. * The size of the pipeline must be evenly divisible by the current page size. This is
  18. * assured by rounding up to the nearest evenly divisible value. For example, if
  19. * the pipeline size is 4990 and the current page size is 25, then pipeline size will
  20. * be dynamically set to 5000.
  21. *
  22. * The cache windows are computed based on the pipeline size and the total number of rows
  23. * returned by the server side query. For example, with pipeline size 500 and total rows
  24. * 1300, the cache windows will be:
  25. *
  26. * [{'lower': 0, 'upper': 499}, {'lower': 500, 'upper': 999}, {'lower': 1000, 'upper': 1499}]
  27. *
  28. * Using the limit (i.e. the pipelineSize) and offset parameters, the server side request
  29. * **MUST** return only the data in the requested cache window **AND** the total number of rows.
  30. * To wit, the server side code must use the offset and limit parameters to prepare the response
  31. * data.
  32. *
  33. * On a page change, the new offset is checked if it is within the current cache window. If so,
  34. * the requested page data is returned from the cached data set. Otherwise, a new server side
  35. * request will be issued for the new cache window.
  36. *
  37. * The current cached data is only invalidated on these events:
  38. * * sorting
  39. * * searching
  40. * * page size change
  41. * * page change moves into a new cache window
  42. *
  43. * There are two new events:
  44. * - cached-data-hit.bs.table: issued when cached data is used on a page change
  45. * - cached-data-reset.bs.table: issued when the cached data is invalidated and a
  46. * new server side request is issued
  47. *
  48. **/
  49. const Utils = $.fn.bootstrapTable.utils
  50. $.extend($.fn.bootstrapTable.defaults, {
  51. usePipeline: false,
  52. pipelineSize: 1000,
  53. // eslint-disable-next-line no-unused-vars
  54. onCachedDataHit (data) {
  55. return false
  56. },
  57. // eslint-disable-next-line no-unused-vars
  58. onCachedDataReset (data) {
  59. return false
  60. }
  61. })
  62. $.extend($.fn.bootstrapTable.Constructor.EVENTS, {
  63. 'cached-data-hit.bs.table': 'onCachedDataHit',
  64. 'cached-data-reset.bs.table': 'onCachedDataReset'
  65. })
  66. const BootstrapTable = $.fn.bootstrapTable.Constructor
  67. const _init = BootstrapTable.prototype.init
  68. const _onSearch = BootstrapTable.prototype.onSearch
  69. const _onSort = BootstrapTable.prototype.onSort
  70. const _onPageListChange = BootstrapTable.prototype.onPageListChange
  71. BootstrapTable.prototype.init = function (...args) {
  72. // needs to be called before initServer()
  73. this.initPipeline()
  74. _init.apply(this, Array.prototype.slice.apply(args))
  75. }
  76. BootstrapTable.prototype.initPipeline = function () {
  77. this.cacheRequestJSON = {}
  78. this.cacheWindows = []
  79. this.currWindow = 0
  80. this.resetCache = true
  81. }
  82. BootstrapTable.prototype.onSearch = function () {
  83. /* force a cache reset on search */
  84. if (this.options.usePipeline) {
  85. this.resetCache = true
  86. }
  87. _onSearch.apply(this, Array.prototype.slice.apply(arguments))
  88. }
  89. BootstrapTable.prototype.onSort = function () {
  90. /* force a cache reset on sort */
  91. if (this.options.usePipeline) {
  92. this.resetCache = true
  93. }
  94. _onSort.apply(this, Array.prototype.slice.apply(arguments))
  95. }
  96. BootstrapTable.prototype.onPageListChange = function (event) {
  97. /* rebuild cache window on page size change */
  98. const target = $(event.currentTarget)
  99. const newPageSize = parseInt(target.text(), 10)
  100. this.options.pipelineSize = this.calculatePipelineSize(this.options.pipelineSize, newPageSize)
  101. this.resetCache = true
  102. _onPageListChange.apply(this, Array.prototype.slice.apply(arguments))
  103. }
  104. BootstrapTable.prototype.calculatePipelineSize = (pipelineSize, pageSize) => {
  105. /* calculate pipeline size by rounding up to the nearest value evenly divisible
  106. * by the pageSize */
  107. if (pageSize === 0) return 0
  108. return Math.ceil(pipelineSize / pageSize) * pageSize
  109. }
  110. BootstrapTable.prototype.setCacheWindows = function () {
  111. /* set cache windows based on the total number of rows returned by server side
  112. * request and the pipelineSize */
  113. this.cacheWindows = []
  114. const numWindows = this.options.totalRows / this.options.pipelineSize
  115. for (let i = 0; i <= numWindows; i++) {
  116. const b = i * this.options.pipelineSize
  117. this.cacheWindows[i] = { lower: b, upper: b + this.options.pipelineSize - 1 }
  118. }
  119. }
  120. BootstrapTable.prototype.setCurrWindow = function (offset) {
  121. /* set the current cache window index, based on where the current offset falls */
  122. this.currWindow = 0
  123. for (let i = 0; i < this.cacheWindows.length; i++) {
  124. if (this.cacheWindows[i].lower <= offset && offset <= this.cacheWindows[i].upper) {
  125. this.currWindow = i
  126. break
  127. }
  128. }
  129. }
  130. BootstrapTable.prototype.drawFromCache = function (offset, limit) {
  131. /* draw rows from the cache using offset and limit */
  132. const res = $.extend(true, {}, this.cacheRequestJSON)
  133. const drawStart = offset - this.cacheWindows[this.currWindow].lower
  134. const drawEnd = drawStart + limit
  135. res.rows = res.rows.slice(drawStart, drawEnd)
  136. return res
  137. }
  138. BootstrapTable.prototype.initServer = function (silent, query, url) {
  139. /* determine if requested data is in cache (on paging) or if
  140. * a new ajax request needs to be issued (sorting, searching, paging
  141. * moving outside of cached data, page size change)
  142. * initial version of this extension will entirely override base initServer
  143. **/
  144. let data = {}
  145. const index = this.header.fields.indexOf(this.options.sortName)
  146. let params = {
  147. searchText: this.searchText,
  148. sortName: this.options.sortName,
  149. sortOrder: this.options.sortOrder
  150. }
  151. let request = null
  152. if (this.header.sortNames[index]) {
  153. params.sortName = this.header.sortNames[index]
  154. }
  155. if (this.options.pagination && this.options.sidePagination === 'server') {
  156. params.pageSize = this.options.pageSize === this.options.formatAllRows() ?
  157. this.options.totalRows : this.options.pageSize
  158. params.pageNumber = this.options.pageNumber
  159. }
  160. if (!(url || this.options.url) && !this.options.ajax) {
  161. return
  162. }
  163. let useAjax = true
  164. if (this.options.queryParamsType === 'limit') {
  165. params = {
  166. searchText: params.searchText,
  167. sortName: params.sortName,
  168. sortOrder: params.sortOrder
  169. }
  170. if (this.options.pagination && this.options.sidePagination === 'server') {
  171. params.limit = this.options.pageSize === this.options.formatAllRows() ? this.options.totalRows : this.options.pageSize
  172. params.offset = (this.options.pageSize === this.options.formatAllRows() ? this.options.totalRows : this.options.pageSize) * (this.options.pageNumber - 1)
  173. if (this.options.usePipeline) {
  174. // if cacheWindows is empty, this is the initial request
  175. if (!this.cacheWindows.length) {
  176. useAjax = true
  177. params.drawOffset = params.offset
  178. // cache exists: determine if the page request is entirely within the current cached window
  179. } else {
  180. const w = this.cacheWindows[this.currWindow]
  181. // case 1: reset cache but stay within current window (e.g. column sort)
  182. // case 2: move outside of the current window (e.g. search or paging)
  183. // since each cache window is aligned with the current page size
  184. // checking if params.offset is outside the current window is sufficient.
  185. // need to requery for preceding or succeeding cache window
  186. // also handle case
  187. if (this.resetCache || (params.offset < w.lower || params.offset > w.upper)) {
  188. useAjax = true
  189. this.setCurrWindow(params.offset)
  190. // store the relative offset for drawing the page data afterwards
  191. params.drawOffset = params.offset
  192. // now set params.offset to the lower bound of the new cache window
  193. // the server will return that whole cache window
  194. params.offset = this.cacheWindows[this.currWindow].lower
  195. // within current cache window
  196. } else {
  197. useAjax = false
  198. }
  199. }
  200. } else if (params.limit === 0) {
  201. delete params.limit
  202. }
  203. }
  204. }
  205. // force an ajax call - this is on search, sort or page size change
  206. if (this.resetCache) {
  207. useAjax = true
  208. this.resetCache = false
  209. }
  210. if (this.options.usePipeline && useAjax) {
  211. /* in this scenario limit is used on the server to get the cache window
  212. * and drawLimit is used to get the page data afterwards */
  213. params.drawLimit = params.limit
  214. params.limit = this.options.pipelineSize
  215. }
  216. // cached results can be used
  217. if (!useAjax) {
  218. const res = this.drawFromCache(params.offset, params.limit)
  219. this.load(res)
  220. this.trigger('load-success', res)
  221. this.trigger('cached-data-hit', res)
  222. return
  223. }
  224. // cached results can't be used
  225. // continue base initServer code
  226. if (!($.isEmptyObject(this.filterColumnsPartial))) {
  227. params.filter = JSON.stringify(this.filterColumnsPartial, null)
  228. }
  229. data = Utils.calculateObjectValue(this.options, this.options.queryParams, [params], data)
  230. $.extend(data, query || {})
  231. // false to stop request
  232. if (data === false) {
  233. return
  234. }
  235. if (!silent) {
  236. this.$tableLoading.show()
  237. }
  238. const self = this
  239. request = $.extend({}, Utils.calculateObjectValue(null, this.options.ajaxOptions), {
  240. type: this.options.method,
  241. url: url || this.options.url,
  242. data: this.options.contentType === 'application/json' && this.options.method === 'post' ?
  243. JSON.stringify(data) : data,
  244. cache: this.options.cache,
  245. contentType: this.options.contentType,
  246. dataType: this.options.dataType,
  247. success (res) {
  248. res = Utils.calculateObjectValue(self.options, self.options.responseHandler, [res], res)
  249. // cache results if using pipelining
  250. if (self.options.usePipeline) {
  251. // store entire request in cache
  252. self.cacheRequestJSON = $.extend(true, {}, res)
  253. // this gets set in load() also but needs to be set before
  254. // setting cacheWindows
  255. self.options.totalRows = res[self.options.totalField]
  256. // if this is a search, potentially less results will be returned
  257. // so cache windows need to be rebuilt. Otherwise it
  258. // will come out the same
  259. self.setCacheWindows()
  260. self.setCurrWindow(params.drawOffset)
  261. // just load data for the page
  262. res = self.drawFromCache(params.drawOffset, params.drawLimit)
  263. self.trigger('cached-data-reset', res)
  264. }
  265. self.load(res)
  266. self.trigger('load-success', res)
  267. if (!silent) {
  268. self.hideLoading()
  269. }
  270. },
  271. error (res) {
  272. let data = []
  273. if (self.options.sidePagination === 'server') {
  274. data = {}
  275. data[self.options.totalField] = 0
  276. data[self.options.dataField] = []
  277. }
  278. self.load(data)
  279. self.trigger('load-error', res.status, res)
  280. if (!silent) {
  281. self.hideLoading()
  282. }
  283. }
  284. })
  285. if (this.options.ajax) {
  286. Utils.calculateObjectValue(this, this.options.ajax, [request], null)
  287. } else {
  288. if (this._xhr && this._xhr.readyState !== 4) {
  289. this._xhr.abort()
  290. }
  291. this._xhr = $.ajax(request)
  292. }
  293. }