@svgrid/grid 3.0.3 → 3.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/GridMenus.svelte +44 -2
- package/dist/SvChartMenu.svelte +69 -0
- package/dist/SvChartMenu.svelte.d.ts +11 -0
- package/dist/SvChartPanes.svelte +113 -0
- package/dist/SvChartPanes.svelte.d.ts +20 -0
- package/dist/SvGrid.controller.svelte.d.ts +122 -41
- package/dist/SvGrid.controller.svelte.js +846 -207
- package/dist/SvGrid.css +178 -2
- package/dist/SvGrid.svelte +717 -720
- package/dist/SvGrid.types.d.ts +566 -12
- package/dist/SvGridCellEditor.svelte +17 -0
- package/dist/SvGridChart.svelte +2257 -329
- package/dist/SvGridChart.svelte.d.ts +6 -106
- package/dist/SvGridChart.types.d.ts +338 -0
- package/dist/SvGridChart.types.js +1 -0
- package/dist/SvGridChartBuilder.svelte +323 -0
- package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
- package/dist/SvGridChartPanel.svelte +221 -275
- package/dist/SvGridChartPickers.svelte +417 -0
- package/dist/SvGridChartPickers.svelte.d.ts +70 -0
- package/dist/SvGridChartView.svelte +13 -14
- package/dist/SvModal.svelte +7 -1
- package/dist/SvModal.svelte.d.ts +5 -0
- package/dist/ai.d.ts +40 -8
- package/dist/ai.js +135 -24
- package/dist/aria.d.ts +11 -0
- package/dist/build-api.js +119 -28
- package/dist/cdn/GridMenus-CID62rlI.js +636 -0
- package/dist/cdn/GridMenus-DLL-qZeu.js +635 -0
- package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
- package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
- package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
- package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
- package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
- package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
- package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
- package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
- package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
- package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
- package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
- package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
- package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
- package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
- package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
- package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
- package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
- package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
- package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
- package/dist/cdn/SvModal-CIZWCcad.js +397 -0
- package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
- package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
- package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
- package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
- package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
- package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
- package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
- package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
- package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
- package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
- package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
- package/dist/cdn/dismissable-DAHetSNk.js +44 -0
- package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
- package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
- package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
- package/dist/cdn/server-block-cache-CGoWz-87.js +237 -0
- package/dist/cdn/{src-D6XvA1Ij.js → src-Bklofsad.js} +10615 -10155
- package/dist/cdn/{src-Bugf5XlI.js → src-jcFMkBnN.js} +7111 -6651
- package/dist/cdn/svgrid.js +27 -15
- package/dist/cdn/svgrid.svelte-external.js +27 -15
- package/dist/cdn/validate-C7Rn_Bns.js +76 -0
- package/dist/chart-axes.d.ts +53 -0
- package/dist/chart-axes.js +351 -0
- package/dist/chart-cartesian.d.ts +88 -0
- package/dist/chart-cartesian.js +1862 -0
- package/dist/chart-decimate.d.ts +51 -0
- package/dist/chart-decimate.js +199 -0
- package/dist/chart-export-pdf.d.ts +47 -0
- package/dist/chart-export-pdf.js +187 -0
- package/dist/chart-export.d.ts +2 -1
- package/dist/chart-export.js +18 -3
- package/dist/chart-financial.d.ts +91 -0
- package/dist/chart-financial.js +175 -0
- package/dist/chart-flow.d.ts +10 -0
- package/dist/chart-flow.js +298 -0
- package/dist/chart-format.d.ts +18 -0
- package/dist/chart-format.js +65 -0
- package/dist/chart-grid.d.ts +8 -0
- package/dist/chart-grid.js +190 -0
- package/dist/chart-hierarchy.d.ts +17 -0
- package/dist/chart-hierarchy.js +146 -0
- package/dist/chart-indicators.d.ts +115 -0
- package/dist/chart-indicators.js +389 -0
- package/dist/chart-messages.d.ts +95 -0
- package/dist/chart-messages.js +92 -0
- package/dist/chart-motion.d.ts +16 -0
- package/dist/chart-motion.js +141 -0
- package/dist/chart-panel-messages.d.ts +265 -0
- package/dist/chart-panel-messages.js +285 -0
- package/dist/chart-pivot.d.ts +87 -0
- package/dist/chart-pivot.js +156 -0
- package/dist/chart-polar.d.ts +25 -0
- package/dist/chart-polar.js +700 -0
- package/dist/chart-samples.d.ts +16 -0
- package/dist/chart-samples.js +153 -0
- package/dist/chart-scale.d.ts +113 -0
- package/dist/chart-scale.js +405 -0
- package/dist/chart-stats.d.ts +111 -0
- package/dist/chart-stats.js +419 -0
- package/dist/chart-stream.d.ts +45 -0
- package/dist/chart-stream.js +91 -0
- package/dist/chart-summary.d.ts +27 -0
- package/dist/chart-summary.js +205 -0
- package/dist/chart-sync.svelte.d.ts +32 -0
- package/dist/chart-sync.svelte.js +23 -0
- package/dist/chart-table.d.ts +35 -0
- package/dist/chart-table.js +165 -0
- package/dist/chart-trend.d.ts +19 -0
- package/dist/chart-trend.js +61 -0
- package/dist/chart-types.d.ts +1456 -0
- package/dist/chart-types.js +6 -0
- package/dist/chart-validate.d.ts +43 -0
- package/dist/chart-validate.js +272 -0
- package/dist/chart-zoom.d.ts +40 -0
- package/dist/chart-zoom.js +145 -0
- package/dist/chart.d.ts +186 -865
- package/dist/chart.js +460 -2204
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +137 -18
- package/dist/column-resize.d.ts +3 -0
- package/dist/column-resize.js +4 -1
- package/dist/columns.js +3 -0
- package/dist/command-context.d.ts +19 -0
- package/dist/command-context.js +116 -0
- package/dist/core.d.ts +7 -0
- package/dist/core.js +36 -13
- package/dist/createPopoverSelect.svelte.d.ts +2 -2
- package/dist/editing.d.ts +2 -8
- package/dist/editing.js +272 -33
- package/dist/fill-patterns.d.ts +0 -5
- package/dist/fill-patterns.js +56 -2
- package/dist/gantt-view.svelte.d.ts +24 -0
- package/dist/gantt-view.svelte.js +13 -0
- package/dist/grid-icons.d.ts +2 -2
- package/dist/grid-icons.js +2 -2
- package/dist/grid-messages.d.ts +16 -3
- package/dist/grid-messages.js +10 -0
- package/dist/history.d.ts +83 -0
- package/dist/history.js +133 -0
- package/dist/index.d.ts +29 -10
- package/dist/index.js +23 -5
- package/dist/keyboard-handlers.js +88 -31
- package/dist/keyboard.d.ts +55 -0
- package/dist/keyboard.js +132 -0
- package/dist/menus.d.ts +3 -1
- package/dist/menus.js +7 -2
- package/dist/merges.d.ts +97 -0
- package/dist/merges.js +147 -0
- package/dist/row-model.d.ts +133 -0
- package/dist/row-model.js +16 -0
- package/dist/row-resize.d.ts +3 -0
- package/dist/row-resize.js +14 -0
- package/dist/scroll-sync.d.ts +1 -0
- package/dist/scroll-sync.js +24 -0
- package/dist/selection.d.ts +18 -1
- package/dist/selection.js +138 -18
- package/dist/server-block-cache.d.ts +214 -0
- package/dist/server-block-cache.js +531 -0
- package/dist/server-data-source.d.ts +240 -6
- package/dist/server-data-source.js +220 -20
- package/dist/server.d.ts +13 -0
- package/dist/server.js +13 -0
- package/dist/shortcut-registry.d.ts +145 -0
- package/dist/shortcut-registry.js +44 -0
- package/dist/svgrid-wrapper.types.d.ts +213 -22
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +21 -3
- package/dist/virtualization/virtualizer.js +38 -17
- package/package.json +11 -1
- package/src/GridMenus.svelte +44 -2
- package/src/SvChartMenu.svelte +69 -0
- package/src/SvChartPanes.svelte +113 -0
- package/src/SvChartPanes.test.ts +66 -0
- package/src/SvGrid.controller.svelte.ts +819 -179
- package/src/SvGrid.css +178 -2
- package/src/SvGrid.svelte +717 -720
- package/src/SvGrid.types.ts +564 -11
- package/src/SvGridCellEditor.svelte +17 -0
- package/src/SvGridChart.svelte +2257 -329
- package/src/SvGridChart.test.ts +1419 -1
- package/src/SvGridChart.types.ts +336 -0
- package/src/SvGridChartBuilder.svelte +323 -0
- package/src/SvGridChartPanel.svelte +221 -275
- package/src/SvGridChartPickers.svelte +417 -0
- package/src/SvGridChartView.svelte +13 -14
- package/src/SvModal.svelte +7 -1
- package/src/SvModal.test.ts +9 -0
- package/src/ai.test.ts +57 -2
- package/src/ai.ts +166 -22
- package/src/aria.d.ts +11 -0
- package/src/build-api.coverage.test.ts +184 -0
- package/src/build-api.ts +118 -22
- package/src/chart-axes.test.ts +573 -0
- package/src/chart-axes.ts +342 -0
- package/src/chart-cartesian.ts +1863 -0
- package/src/chart-decimate.test.ts +249 -0
- package/src/chart-decimate.ts +186 -0
- package/src/chart-export-pdf.test.ts +91 -0
- package/src/chart-export-pdf.ts +207 -0
- package/src/chart-export.test.ts +14 -0
- package/src/chart-export.ts +13 -3
- package/src/chart-financial.ts +208 -0
- package/src/chart-flow.ts +286 -0
- package/src/chart-format.ts +67 -0
- package/src/chart-grid.ts +179 -0
- package/src/chart-hierarchy.ts +135 -0
- package/src/chart-indicators.test.ts +443 -0
- package/src/chart-indicators.ts +384 -0
- package/src/chart-messages.test.ts +21 -0
- package/src/chart-messages.ts +184 -0
- package/src/chart-motion.test.ts +71 -0
- package/src/chart-motion.ts +148 -0
- package/src/chart-panel-messages.test.ts +29 -0
- package/src/chart-panel-messages.ts +550 -0
- package/src/chart-pivot.test.ts +54 -0
- package/src/chart-pivot.ts +225 -0
- package/src/chart-polar.ts +674 -0
- package/src/chart-samples.ts +106 -0
- package/src/chart-scale.ts +394 -0
- package/src/chart-series-types.test.ts +714 -0
- package/src/chart-stats.ts +409 -0
- package/src/chart-stream.test.ts +75 -0
- package/src/chart-stream.ts +114 -0
- package/src/chart-summary.test.ts +90 -0
- package/src/chart-summary.ts +197 -0
- package/src/chart-sync.svelte.ts +41 -0
- package/src/chart-table.test.ts +76 -0
- package/src/chart-table.ts +178 -0
- package/src/chart-trend.ts +47 -0
- package/src/chart-types.ts +1309 -0
- package/src/chart-validate.test.ts +100 -0
- package/src/chart-validate.ts +247 -0
- package/src/chart-zoom.test.ts +137 -0
- package/src/chart-zoom.ts +124 -0
- package/src/chart.coverage.test.ts +81 -0
- package/src/chart.test.ts +56 -3
- package/src/chart.ts +557 -2784
- package/src/clipboard.test.ts +151 -0
- package/src/clipboard.ts +145 -16
- package/src/column-resize.ts +7 -1
- package/src/columns.ts +2 -0
- package/src/command-context.test.ts +200 -0
- package/src/command-context.ts +137 -0
- package/src/core.rowmodel-cache.test.ts +51 -0
- package/src/core.ts +40 -12
- package/src/editing.test.ts +281 -3
- package/src/editing.ts +264 -38
- package/src/fill-patterns.test.ts +35 -0
- package/src/fill-patterns.ts +56 -2
- package/src/gantt-stub.test.svelte +38 -0
- package/src/gantt-view.svelte.ts +35 -0
- package/src/grid-icons.ts +2 -2
- package/src/grid-messages.ts +28 -3
- package/src/history.test.ts +196 -0
- package/src/history.ts +162 -0
- package/src/icon-seam.test.ts +5 -6
- package/src/index.ts +186 -12
- package/src/keyboard-handlers.coverage.test.ts +126 -6
- package/src/keyboard-handlers.ts +91 -30
- package/src/keyboard-shortcuts.seam.test.ts +200 -0
- package/src/keyboard.test.ts +124 -1
- package/src/keyboard.ts +142 -0
- package/src/menus.test.ts +26 -0
- package/src/menus.ts +10 -3
- package/src/merges.test.ts +112 -0
- package/src/merges.ts +168 -0
- package/src/pivot.grid.test.ts +40 -5
- package/src/row-model.ts +146 -0
- package/src/row-resize.test.ts +44 -0
- package/src/row-resize.ts +17 -0
- package/src/scroll-sync.test.ts +42 -1
- package/src/scroll-sync.ts +25 -0
- package/src/selection.multi-range.test.ts +34 -0
- package/src/selection.test.ts +33 -2
- package/src/selection.ts +143 -21
- package/src/server-block-cache.test.ts +645 -0
- package/src/server-block-cache.ts +677 -0
- package/src/server-data-source.infinite.test.ts +343 -0
- package/src/server-data-source.ts +450 -27
- package/src/server.ts +47 -0
- package/src/shortcut-registry.test.ts +97 -0
- package/src/shortcut-registry.ts +181 -0
- package/src/svgrid-wrapper.types.ts +184 -13
- package/src/svgrid.behavior.test.ts +135 -0
- package/src/svgrid.charting.test.ts +513 -3
- package/src/svgrid.context-menu.test.ts +31 -0
- package/src/svgrid.gantt-seam.test.ts +199 -0
- package/src/svgrid.new-features.wrapper.test.ts +10 -3
- package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +289 -0
- package/src/svgrid.upsell-license.test.ts +7 -4
- package/src/validate.test.ts +29 -0
- package/src/validate.ts +28 -3
- package/src/virtualization/virtualizer.ts +39 -17
- package/dist/SvGroupCell.svelte +0 -141
- package/dist/SvGroupCell.svelte.d.ts +0 -49
- package/dist/SvRowGroupPanel.svelte +0 -186
- package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
- package/dist/cdn/GridMenus-CHOMnW-c.js +0 -602
- package/dist/cdn/GridMenus-DhdPF700.js +0 -603
- package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
- package/dist/cdn/SvGridChart-1PMSp2LZ.js +0 -1481
- package/dist/cdn/SvGridChart-D02avhyr.js +0 -1480
- package/dist/cdn/SvGridChartPanel-DJnViOv4.js +0 -834
- package/dist/cdn/SvGridChartPanel-vPTNerFZ.js +0 -810
- package/dist/cdn/SvGridChartView-C--HhdUo.js +0 -56
- package/dist/cdn/SvGridChartView-KyyFG-wY.js +0 -55
- package/dist/cdn/chart-DLg3_zTQ.js +0 -1652
- package/dist/cdn/validate-_CDJzgIo.js +0 -75
- package/dist/server-group-model.d.ts +0 -98
- package/dist/server-group-model.js +0 -263
- package/src/SvGroupCell.svelte +0 -141
- package/src/SvRowGroupPanel.svelte +0 -186
- package/src/server-group-model.test.ts +0 -294
- package/src/server-group-model.ts +0 -370
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block cache for server-backed rows - the engine behind infinite scrolling.
|
|
3
|
+
*
|
|
4
|
+
* Paging asks for "page 3". Infinite scrolling asks a different question: the
|
|
5
|
+
* user is looking at rows 4,000 to 4,020 of a table with a million rows, so
|
|
6
|
+
* fetch the block that covers them, keep a bounded number of recently-seen
|
|
7
|
+
* blocks, and render something sensible for every row nobody has fetched yet.
|
|
8
|
+
* That is all this module does, in plain TypeScript with no Svelte and no DOM.
|
|
9
|
+
*
|
|
10
|
+
* The shape of the problem (and most of the vocabulary) is the same one every
|
|
11
|
+
* grid with a server row model solves, so the options here are deliberately
|
|
12
|
+
* recognisable: `blockSize`, `maxBlocksInCache`, `maxConcurrentRequests`,
|
|
13
|
+
* `blockLoadDebounceMs`, `initialRowCount`, `overflowRows`.
|
|
14
|
+
*
|
|
15
|
+
* Three things it takes seriously:
|
|
16
|
+
*
|
|
17
|
+
* - **Unloaded rows still have to render.** `rows()` always returns a dense
|
|
18
|
+
* array, with a shared sentinel object in every slot that has no data. The
|
|
19
|
+
* grid recognises those (see {@link rowPlaceholderState}) and draws a
|
|
20
|
+
* skeleton or a retry affordance instead of an empty row, so the scrollbar
|
|
21
|
+
* never lies and scrolling never leaves a hole.
|
|
22
|
+
* - **The row count may be unknown.** A backend that cannot cheaply count
|
|
23
|
+
* says so (omit `rowCount`, or send `-1`) and the list grows by a block at
|
|
24
|
+
* a time until a short block proves where the end is.
|
|
25
|
+
* - **Requests are not free.** In-flight blocks are deduplicated, at most
|
|
26
|
+
* `maxConcurrentRequests` are open at once, a fast scroll debounces rather
|
|
27
|
+
* than firing a request per frame, and blocks abandoned by eviction or
|
|
28
|
+
* purge are aborted.
|
|
29
|
+
*
|
|
30
|
+
* Enterprise's server row model runs one of these per group level; the free
|
|
31
|
+
* flat controller runs exactly one.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* The mark that says "this row is not data yet".
|
|
35
|
+
*
|
|
36
|
+
* A symbol, for two reasons. It cannot collide with a field in anyone's row
|
|
37
|
+
* data, which a string key like `__svPlaceholder` eventually would. And
|
|
38
|
+
* `Symbol.for` puts it in the global registry, so a placeholder still reads as
|
|
39
|
+
* one after it has crossed a module boundary - two copies of this file loaded
|
|
40
|
+
* through different specifiers would otherwise mint different symbols.
|
|
41
|
+
*/
|
|
42
|
+
const PLACEHOLDER = Symbol.for('svgrid.rowPlaceholder');
|
|
43
|
+
/** Rows the cache has not loaded. One shared object per state, not per row. */
|
|
44
|
+
const LOADING_ROW = Object.freeze({ [PLACEHOLDER]: 'loading' });
|
|
45
|
+
const FAILED_ROW = Object.freeze({ [PLACEHOLDER]: 'failed' });
|
|
46
|
+
/**
|
|
47
|
+
* Why a row is not real data, or `null` when it is.
|
|
48
|
+
*
|
|
49
|
+
* Deliberately NOT an identity check. The sentinels are shared objects, so
|
|
50
|
+
* `row === LOADING_ROW` looks like the obvious test - but rows routinely
|
|
51
|
+
* arrive here through something that wrapped them. Svelte is the everyday
|
|
52
|
+
* case: assigning the controller state into `$state` makes the row array
|
|
53
|
+
* deeply reactive, and every row read back out is a proxy of the original.
|
|
54
|
+
* Identity fails, every placeholder reads as real data, and the grid renders
|
|
55
|
+
* a screen of blank rows instead of skeletons. Reading the mark works through
|
|
56
|
+
* any wrapper that forwards property access, which is all of them.
|
|
57
|
+
*/
|
|
58
|
+
export function rowPlaceholderState(row) {
|
|
59
|
+
if (row == null || typeof row !== 'object')
|
|
60
|
+
return null;
|
|
61
|
+
const mark = row[PLACEHOLDER];
|
|
62
|
+
return mark === 'loading' || mark === 'failed' ? mark : null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Mint a frozen placeholder the grid will recognise, carrying whatever
|
|
66
|
+
* other fields the caller wants on it. The row model uses this to give its
|
|
67
|
+
* placeholders a `kind`, so code reading display rows and code reading grid
|
|
68
|
+
* rows agree on what they are looking at.
|
|
69
|
+
*/
|
|
70
|
+
export function createRowPlaceholder(state, fields) {
|
|
71
|
+
return Object.freeze({ ...fields, [PLACEHOLDER]: state });
|
|
72
|
+
}
|
|
73
|
+
const defaultSchedule = (flush) => {
|
|
74
|
+
void Promise.resolve().then(flush);
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Build a block cache over a `fetch(startRow, endRow, signal)`. The free
|
|
78
|
+
* infinite row model and the Enterprise server-side row model both run on
|
|
79
|
+
* it, one per level.
|
|
80
|
+
*/
|
|
81
|
+
export function createBlockCache(options) {
|
|
82
|
+
const blockSize = Math.max(1, options.blockSize ?? 100);
|
|
83
|
+
const maxBlocks = options.maxBlocksInCache ?? Infinity;
|
|
84
|
+
const maxConcurrent = Math.max(1, options.maxConcurrentRequests ?? 2);
|
|
85
|
+
const debounceMs = Math.max(0, options.blockLoadDebounceMs ?? 0);
|
|
86
|
+
const initialRowCount = Math.max(0, options.initialRowCount ?? 1);
|
|
87
|
+
const overflowRows = Math.max(0, options.overflowRows ?? 1);
|
|
88
|
+
const schedule = options.schedule ?? defaultSchedule;
|
|
89
|
+
const blocks = new Map();
|
|
90
|
+
/** Block indices waiting for a slot, in the order the viewport asked for them. */
|
|
91
|
+
const queue = [];
|
|
92
|
+
let inFlight = 0;
|
|
93
|
+
let touchClock = 0;
|
|
94
|
+
let count = null;
|
|
95
|
+
let countKnown = false;
|
|
96
|
+
let disposed = false;
|
|
97
|
+
let viewStart = 0;
|
|
98
|
+
let viewEnd = 0;
|
|
99
|
+
let debounceTimer = null;
|
|
100
|
+
let emitScheduled = false;
|
|
101
|
+
let rowsCache = null;
|
|
102
|
+
const blockOf = (rowIndex) => Math.floor(rowIndex / blockSize);
|
|
103
|
+
const startOf = (blockIndex) => blockIndex * blockSize;
|
|
104
|
+
/**
|
|
105
|
+
* How many rows to claim we have.
|
|
106
|
+
*
|
|
107
|
+
* With a known count that is simply the count. Without one it is the last row
|
|
108
|
+
* we have touched plus `overflowRows`, so there is always somewhere to scroll
|
|
109
|
+
* that will ask for the next block - this is what makes the list grow as you
|
|
110
|
+
* go rather than ending at the first unloaded row.
|
|
111
|
+
*/
|
|
112
|
+
function currentLength() {
|
|
113
|
+
if (countKnown && count != null)
|
|
114
|
+
return count;
|
|
115
|
+
let highestEnd = 0;
|
|
116
|
+
let anySettled = false;
|
|
117
|
+
for (const b of blocks.values())
|
|
118
|
+
if (b.status !== 'loading')
|
|
119
|
+
anySettled = true;
|
|
120
|
+
for (const [index, block] of blocks) {
|
|
121
|
+
// A block in flight extends the list only once some block has settled.
|
|
122
|
+
// Before that the list is `initialRowCount` long: the few skeleton
|
|
123
|
+
// rows a level shows while it waits, not a hundred of them. The first
|
|
124
|
+
// block failing says nothing about how long the list is either, so it
|
|
125
|
+
// keeps those few rows (its Retry sits on them); a failed block further
|
|
126
|
+
// in was scrolled to, so its slots stay and the scrollbar holds still.
|
|
127
|
+
const end = block.status === 'loaded'
|
|
128
|
+
? startOf(index) + block.rows.length
|
|
129
|
+
: block.status === 'failed'
|
|
130
|
+
? index === 0 ? 0 : startOf(index) + blockSize
|
|
131
|
+
: anySettled
|
|
132
|
+
? startOf(index) + blockSize
|
|
133
|
+
: 0;
|
|
134
|
+
if (end > highestEnd)
|
|
135
|
+
highestEnd = end;
|
|
136
|
+
}
|
|
137
|
+
if (!anySettled)
|
|
138
|
+
return initialRowCount;
|
|
139
|
+
return Math.max(initialRowCount, highestEnd + overflowRows);
|
|
140
|
+
}
|
|
141
|
+
function state() {
|
|
142
|
+
const failed = [];
|
|
143
|
+
for (const [index, block] of blocks)
|
|
144
|
+
if (block.status === 'failed')
|
|
145
|
+
failed.push(index);
|
|
146
|
+
return {
|
|
147
|
+
rowCount: countKnown ? count : null,
|
|
148
|
+
lastRowKnown: countKnown,
|
|
149
|
+
loading: inFlight > 0,
|
|
150
|
+
failedBlocks: failed.sort((a, b) => a - b),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Something changed: forget the row array NOW, so a synchronous read after
|
|
155
|
+
* a mutation sees the new state, and notify on the next tick, so a burst of
|
|
156
|
+
* mutations notifies once. Every mutating path ends here.
|
|
157
|
+
*/
|
|
158
|
+
function emit() {
|
|
159
|
+
rowsCache = null; // rebuilt lazily, on the next read
|
|
160
|
+
if (disposed || emitScheduled)
|
|
161
|
+
return;
|
|
162
|
+
emitScheduled = true;
|
|
163
|
+
schedule(() => {
|
|
164
|
+
emitScheduled = false;
|
|
165
|
+
if (disposed)
|
|
166
|
+
return;
|
|
167
|
+
options.onChange(state());
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function ensureBlock(blockIndex) {
|
|
171
|
+
if (disposed)
|
|
172
|
+
return;
|
|
173
|
+
const existing = blocks.get(blockIndex);
|
|
174
|
+
// Loading or loaded blocks are left alone; a failed one only retries when
|
|
175
|
+
// asked, so a broken backend is not hammered once per scroll frame.
|
|
176
|
+
if (existing)
|
|
177
|
+
return;
|
|
178
|
+
if (queue.includes(blockIndex))
|
|
179
|
+
return;
|
|
180
|
+
queue.push(blockIndex);
|
|
181
|
+
pump();
|
|
182
|
+
}
|
|
183
|
+
function pump() {
|
|
184
|
+
while (!disposed && inFlight < maxConcurrent && queue.length > 0) {
|
|
185
|
+
const blockIndex = queue.shift();
|
|
186
|
+
if (blocks.has(blockIndex))
|
|
187
|
+
continue;
|
|
188
|
+
void load(blockIndex);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function load(blockIndex) {
|
|
192
|
+
const controller = new AbortController();
|
|
193
|
+
const block = {
|
|
194
|
+
status: 'loading',
|
|
195
|
+
rows: [],
|
|
196
|
+
lastTouched: (touchClock += 1),
|
|
197
|
+
controller,
|
|
198
|
+
};
|
|
199
|
+
blocks.set(blockIndex, block);
|
|
200
|
+
inFlight += 1;
|
|
201
|
+
emit();
|
|
202
|
+
const startRow = startOf(blockIndex);
|
|
203
|
+
const endRow = startRow + blockSize;
|
|
204
|
+
try {
|
|
205
|
+
const result = await options.fetch(startRow, endRow, controller.signal);
|
|
206
|
+
if (disposed || controller.signal.aborted || blocks.get(blockIndex) !== block)
|
|
207
|
+
return;
|
|
208
|
+
block.rows = [...result.rows];
|
|
209
|
+
block.status = 'loaded';
|
|
210
|
+
block.controller = null;
|
|
211
|
+
applyCountFrom(blockIndex, block.rows.length, result.rowCount);
|
|
212
|
+
evictIfNeeded();
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
if (disposed || controller.signal.aborted || blocks.get(blockIndex) !== block)
|
|
216
|
+
return;
|
|
217
|
+
block.status = 'failed';
|
|
218
|
+
block.rows = [];
|
|
219
|
+
block.controller = null;
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
if (!disposed && !controller.signal.aborted) {
|
|
223
|
+
inFlight -= 1;
|
|
224
|
+
emit();
|
|
225
|
+
pump();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Learn the total from a block that just landed.
|
|
231
|
+
*
|
|
232
|
+
* An explicit non-negative `rowCount` settles it. Otherwise a SHORT block is
|
|
233
|
+
* the proof that we hit the end: fewer rows came back than were asked for, so
|
|
234
|
+
* the data stops where they stop.
|
|
235
|
+
*/
|
|
236
|
+
function applyCountFrom(blockIndex, received, reported) {
|
|
237
|
+
if (typeof reported === 'number' && reported >= 0) {
|
|
238
|
+
count = reported;
|
|
239
|
+
countKnown = true;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (received < blockSize) {
|
|
243
|
+
count = startOf(blockIndex) + received;
|
|
244
|
+
countKnown = true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Evict loaded blocks over the limit, least-recently-seen first, never one
|
|
249
|
+
* the viewport is currently on - dropping a visible block would swap real
|
|
250
|
+
* rows for skeletons under the user's eyes and immediately re-fetch them.
|
|
251
|
+
*/
|
|
252
|
+
function evictIfNeeded() {
|
|
253
|
+
if (!Number.isFinite(maxBlocks))
|
|
254
|
+
return;
|
|
255
|
+
const firstVisible = blockOf(viewStart);
|
|
256
|
+
const lastVisible = blockOf(Math.max(viewStart, viewEnd));
|
|
257
|
+
const evictable = [...blocks.entries()]
|
|
258
|
+
.filter(([index, b]) => b.status === 'loaded' && (index < firstVisible || index > lastVisible))
|
|
259
|
+
.sort((a, b) => a[1].lastTouched - b[1].lastTouched);
|
|
260
|
+
let loaded = 0;
|
|
261
|
+
for (const b of blocks.values())
|
|
262
|
+
if (b.status === 'loaded')
|
|
263
|
+
loaded += 1;
|
|
264
|
+
for (const [index] of evictable) {
|
|
265
|
+
if (loaded <= maxBlocks)
|
|
266
|
+
break;
|
|
267
|
+
blocks.delete(index);
|
|
268
|
+
loaded -= 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function fetchViewport() {
|
|
272
|
+
if (disposed)
|
|
273
|
+
return;
|
|
274
|
+
const first = blockOf(viewStart);
|
|
275
|
+
const last = blockOf(Math.max(viewStart, viewEnd));
|
|
276
|
+
for (let i = first; i <= last; i += 1) {
|
|
277
|
+
const block = blocks.get(i);
|
|
278
|
+
if (block)
|
|
279
|
+
block.lastTouched = touchClock += 1;
|
|
280
|
+
else
|
|
281
|
+
ensureBlock(i);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function abortAll() {
|
|
285
|
+
for (const block of blocks.values())
|
|
286
|
+
block.controller?.abort();
|
|
287
|
+
inFlight = 0;
|
|
288
|
+
queue.length = 0;
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
setViewport(startIndex, endIndex) {
|
|
292
|
+
if (disposed)
|
|
293
|
+
return;
|
|
294
|
+
viewStart = Math.max(0, startIndex);
|
|
295
|
+
viewEnd = Math.max(viewStart, endIndex);
|
|
296
|
+
if (debounceMs === 0) {
|
|
297
|
+
fetchViewport();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (debounceTimer)
|
|
301
|
+
clearTimeout(debounceTimer);
|
|
302
|
+
debounceTimer = setTimeout(() => {
|
|
303
|
+
debounceTimer = null;
|
|
304
|
+
fetchViewport();
|
|
305
|
+
}, debounceMs);
|
|
306
|
+
},
|
|
307
|
+
rows() {
|
|
308
|
+
if (rowsCache)
|
|
309
|
+
return rowsCache;
|
|
310
|
+
const length = currentLength();
|
|
311
|
+
const out = new Array(length);
|
|
312
|
+
// Fill from the blocks we have, then pad the rest with placeholders. One
|
|
313
|
+
// pass per block beats one lookup per row: at a million rows the
|
|
314
|
+
// difference is the frame budget.
|
|
315
|
+
out.fill(LOADING_ROW);
|
|
316
|
+
for (const [index, block] of blocks) {
|
|
317
|
+
const start = startOf(index);
|
|
318
|
+
if (start >= length)
|
|
319
|
+
continue;
|
|
320
|
+
if (block.status === 'failed') {
|
|
321
|
+
const end = Math.min(start + blockSize, length);
|
|
322
|
+
for (let i = start; i < end; i += 1)
|
|
323
|
+
out[i] = FAILED_ROW;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
for (let i = 0; i < block.rows.length && start + i < length; i += 1) {
|
|
327
|
+
out[start + i] = block.rows[i];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
rowsCache = out;
|
|
331
|
+
return out;
|
|
332
|
+
},
|
|
333
|
+
getRow(index) {
|
|
334
|
+
const block = blocks.get(blockOf(index));
|
|
335
|
+
if (!block)
|
|
336
|
+
return LOADING_ROW;
|
|
337
|
+
if (block.status === 'failed')
|
|
338
|
+
return FAILED_ROW;
|
|
339
|
+
const row = block.rows[index - startOf(blockOf(index))];
|
|
340
|
+
return row ?? LOADING_ROW;
|
|
341
|
+
},
|
|
342
|
+
rowCount: () => (countKnown ? count : null),
|
|
343
|
+
lastRowKnown: () => countKnown,
|
|
344
|
+
retryFailed() {
|
|
345
|
+
let any = false;
|
|
346
|
+
for (const [index, block] of [...blocks]) {
|
|
347
|
+
if (block.status !== 'failed')
|
|
348
|
+
continue;
|
|
349
|
+
blocks.delete(index);
|
|
350
|
+
ensureBlock(index);
|
|
351
|
+
any = true;
|
|
352
|
+
}
|
|
353
|
+
if (any)
|
|
354
|
+
emit();
|
|
355
|
+
},
|
|
356
|
+
refresh() {
|
|
357
|
+
// Keep the count and the scroll position: drop the DATA, re-request the
|
|
358
|
+
// same blocks. The user sees skeletons where they were, not a jump home.
|
|
359
|
+
for (const [index, block] of [...blocks]) {
|
|
360
|
+
block.controller?.abort();
|
|
361
|
+
blocks.delete(index);
|
|
362
|
+
ensureBlock(index);
|
|
363
|
+
}
|
|
364
|
+
emit();
|
|
365
|
+
},
|
|
366
|
+
purge() {
|
|
367
|
+
abortAll();
|
|
368
|
+
blocks.clear();
|
|
369
|
+
count = null;
|
|
370
|
+
countKnown = false;
|
|
371
|
+
rowsCache = null;
|
|
372
|
+
fetchViewport();
|
|
373
|
+
emit();
|
|
374
|
+
},
|
|
375
|
+
setRowCount(next, known = next != null) {
|
|
376
|
+
count = next;
|
|
377
|
+
countKnown = known && next != null;
|
|
378
|
+
emit();
|
|
379
|
+
},
|
|
380
|
+
applyRows(startRow, rows, rowCount) {
|
|
381
|
+
if (startRow % blockSize !== 0) {
|
|
382
|
+
throw new Error(`createBlockCache.applyRows: startRow ${startRow} is not a multiple of blockSize ${blockSize}`);
|
|
383
|
+
}
|
|
384
|
+
const firstBlock = blockOf(startRow);
|
|
385
|
+
const blockCount = Math.ceil(rows.length / blockSize);
|
|
386
|
+
for (let i = 0; i < blockCount; i += 1) {
|
|
387
|
+
const index = firstBlock + i;
|
|
388
|
+
blocks.get(index)?.controller?.abort(); // ours now, whatever was in flight
|
|
389
|
+
blocks.set(index, {
|
|
390
|
+
status: 'loaded',
|
|
391
|
+
rows: rows.slice(i * blockSize, (i + 1) * blockSize),
|
|
392
|
+
lastTouched: (touchClock += 1),
|
|
393
|
+
controller: null,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
const lastBlock = blocks.get(firstBlock + blockCount - 1);
|
|
397
|
+
if (lastBlock)
|
|
398
|
+
applyCountFrom(firstBlock + blockCount - 1, lastBlock.rows.length, rowCount);
|
|
399
|
+
else if (typeof rowCount === 'number' && rowCount >= 0) {
|
|
400
|
+
count = rowCount;
|
|
401
|
+
countKnown = true;
|
|
402
|
+
}
|
|
403
|
+
emit();
|
|
404
|
+
},
|
|
405
|
+
patch(index, row) {
|
|
406
|
+
const block = blocks.get(blockOf(index));
|
|
407
|
+
if (!block || block.status !== 'loaded')
|
|
408
|
+
return false;
|
|
409
|
+
const offset = index - startOf(blockOf(index));
|
|
410
|
+
if (offset < 0 || offset >= block.rows.length)
|
|
411
|
+
return false;
|
|
412
|
+
block.rows[offset] = row;
|
|
413
|
+
emit();
|
|
414
|
+
return true;
|
|
415
|
+
},
|
|
416
|
+
findIndex(predicate) {
|
|
417
|
+
for (const [blockIndex, block] of blocks) {
|
|
418
|
+
if (block.status !== 'loaded')
|
|
419
|
+
continue;
|
|
420
|
+
const start = startOf(blockIndex);
|
|
421
|
+
for (let i = 0; i < block.rows.length; i += 1) {
|
|
422
|
+
if (predicate(block.rows[i], start + i))
|
|
423
|
+
return start + i;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return -1;
|
|
427
|
+
},
|
|
428
|
+
insert(index, rows) {
|
|
429
|
+
if (rows.length === 0)
|
|
430
|
+
return false;
|
|
431
|
+
const clamped = Math.max(0, Math.min(index, currentLength()));
|
|
432
|
+
const run = loadedRunAt(clamped);
|
|
433
|
+
if (run) {
|
|
434
|
+
// Rows shift across block boundaries, so re-slice the run from its
|
|
435
|
+
// first block; everything loaded past it is dropped (offsets moved).
|
|
436
|
+
run.flat.splice(clamped - startOf(run.startBlock), 0, ...rows);
|
|
437
|
+
reseat(run.startBlock, run.flat);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
dropLoadedFrom(blockOf(clamped));
|
|
441
|
+
}
|
|
442
|
+
if (countKnown && count != null)
|
|
443
|
+
count += rows.length;
|
|
444
|
+
emit();
|
|
445
|
+
return run != null;
|
|
446
|
+
},
|
|
447
|
+
remove(index, removeCount = 1) {
|
|
448
|
+
if (removeCount <= 0)
|
|
449
|
+
return 0;
|
|
450
|
+
const run = loadedRunAt(index);
|
|
451
|
+
if (!run)
|
|
452
|
+
return 0;
|
|
453
|
+
const removed = run.flat.splice(index - startOf(run.startBlock), removeCount).length;
|
|
454
|
+
reseat(run.startBlock, run.flat);
|
|
455
|
+
if (countKnown && count != null)
|
|
456
|
+
count = Math.max(0, count - removed);
|
|
457
|
+
emit();
|
|
458
|
+
return removed;
|
|
459
|
+
},
|
|
460
|
+
getCacheState() {
|
|
461
|
+
return [...blocks.entries()]
|
|
462
|
+
.map(([blockIndex, block]) => ({
|
|
463
|
+
blockIndex,
|
|
464
|
+
startRow: startOf(blockIndex),
|
|
465
|
+
endRow: startOf(blockIndex) + blockSize,
|
|
466
|
+
status: block.status,
|
|
467
|
+
lastTouched: block.lastTouched,
|
|
468
|
+
}))
|
|
469
|
+
.sort((a, b) => a.blockIndex - b.blockIndex);
|
|
470
|
+
},
|
|
471
|
+
dispose() {
|
|
472
|
+
disposed = true;
|
|
473
|
+
if (debounceTimer)
|
|
474
|
+
clearTimeout(debounceTimer);
|
|
475
|
+
debounceTimer = null;
|
|
476
|
+
abortAll();
|
|
477
|
+
blocks.clear();
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
/**
|
|
481
|
+
* The run of consecutive loaded blocks containing `index`, flattened, with
|
|
482
|
+
* the block it starts at. Null when the block at `index` is not loaded -
|
|
483
|
+
* an index-shifting edit there would have nothing to shift.
|
|
484
|
+
*/
|
|
485
|
+
function loadedRunAt(index) {
|
|
486
|
+
const at = blockOf(index);
|
|
487
|
+
if (blocks.get(at)?.status !== 'loaded')
|
|
488
|
+
return null;
|
|
489
|
+
let startBlock = at;
|
|
490
|
+
while (blocks.get(startBlock - 1)?.status === 'loaded')
|
|
491
|
+
startBlock -= 1;
|
|
492
|
+
const flat = [];
|
|
493
|
+
for (let i = startBlock;; i += 1) {
|
|
494
|
+
const block = blocks.get(i);
|
|
495
|
+
if (!block || block.status !== 'loaded')
|
|
496
|
+
break;
|
|
497
|
+
flat.push(...block.rows);
|
|
498
|
+
if (block.rows.length < blockSize)
|
|
499
|
+
break; // the last block, by definition
|
|
500
|
+
}
|
|
501
|
+
return { startBlock, flat };
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Lay a flat run back out into blocks from `startBlock`, and drop every
|
|
505
|
+
* block after the run: their rows have shifted, and re-fetching them at
|
|
506
|
+
* the new offsets is the honest outcome. Blocks before it are untouched.
|
|
507
|
+
*/
|
|
508
|
+
function reseat(startBlock, flat) {
|
|
509
|
+
const blockCount = Math.ceil(flat.length / blockSize);
|
|
510
|
+
dropLoadedFrom(startBlock);
|
|
511
|
+
for (let i = 0; i < blockCount; i += 1) {
|
|
512
|
+
blocks.set(startBlock + i, {
|
|
513
|
+
status: 'loaded',
|
|
514
|
+
rows: flat.slice(i * blockSize, (i + 1) * blockSize),
|
|
515
|
+
lastTouched: (touchClock += 1),
|
|
516
|
+
controller: null,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
rowsCache = null;
|
|
520
|
+
}
|
|
521
|
+
/** Forget every block from `fromBlock` on, aborting any in flight. */
|
|
522
|
+
function dropLoadedFrom(fromBlock) {
|
|
523
|
+
for (const [index, block] of [...blocks]) {
|
|
524
|
+
if (index < fromBlock)
|
|
525
|
+
continue;
|
|
526
|
+
block.controller?.abort();
|
|
527
|
+
blocks.delete(index);
|
|
528
|
+
}
|
|
529
|
+
rowsCache = null;
|
|
530
|
+
}
|
|
531
|
+
}
|