@svgrid/grid 3.0.4 → 3.0.6

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.
Files changed (176) hide show
  1. package/README.md +32 -11
  2. package/dist/GridMenus.svelte +34 -4
  3. package/dist/SvGrid.controller.svelte.d.ts +33 -5
  4. package/dist/SvGrid.controller.svelte.js +397 -58
  5. package/dist/SvGrid.css +200 -2
  6. package/dist/SvGrid.svelte +350 -40
  7. package/dist/SvGrid.types.d.ts +433 -7
  8. package/dist/SvGridDropdown.svelte +20 -0
  9. package/dist/SvGridDropdown.svelte.d.ts +12 -0
  10. package/dist/SvModal.svelte +22 -0
  11. package/dist/SvModal.svelte.d.ts +7 -0
  12. package/dist/a11y/dismissable.d.ts +10 -0
  13. package/dist/a11y/dismissable.js +24 -2
  14. package/dist/build-api.js +92 -37
  15. package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-C-NtjlrJ.js} +155 -146
  16. package/dist/cdn/GridMenus-TssHej3q.js +644 -0
  17. package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
  18. package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
  19. package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
  20. package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
  21. package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-BlyCSp0q.js} +1 -1
  22. package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-ClRRA9lW.js} +1 -1
  23. package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-B4EV8U6t.js} +4 -4
  24. package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DsmwTyZF.js} +4 -4
  25. package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-cHloHobN.js} +4 -4
  26. package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-vOl7C0Za.js} +4 -4
  27. package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-AcqF-Ll5.js} +5 -5
  28. package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-biMtSBPA.js} +5 -5
  29. package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-Dx_GmZuL.js} +2 -2
  30. package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-I3rDHXAq.js} +2 -2
  31. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
  32. package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
  33. package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
  34. package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
  35. package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-DVVKOSI9.js} +1 -1
  36. package/dist/cdn/{chart-Dz7SqMXH.js → chart-DEZxwC0i.js} +1 -1
  37. package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-D12XcTZJ.js} +1 -1
  38. package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DguS7gC5.js} +1 -1
  39. package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-CMAXBGY-.js} +1 -1
  40. package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-BkEp61Gj.js} +1 -1
  41. package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
  42. package/dist/cdn/{export-format-CUDLy2yn.js → export-format-YzdwBww2.js} +1 -1
  43. package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
  44. package/dist/cdn/server-block-cache-DIqb3VZn.js +241 -0
  45. package/dist/cdn/{src-V1uu8iE9.js → src-CgoeSM8k.js} +5339 -4753
  46. package/dist/cdn/{src-BKhZ6eXd.js → src-DfwxB1TH.js} +9817 -9231
  47. package/dist/cdn/svgrid.js +28 -27
  48. package/dist/cdn/svgrid.svelte-external.js +28 -27
  49. package/dist/cdn/validate-AZoD8BoR.js +80 -0
  50. package/dist/cell-formatting.js +18 -1
  51. package/dist/cell-render.js +25 -6
  52. package/dist/clipboard.d.ts +2 -2
  53. package/dist/clipboard.js +72 -23
  54. package/dist/columns.js +7 -2
  55. package/dist/conditional-formatting.d.ts +2 -0
  56. package/dist/conditional-formatting.js +2 -0
  57. package/dist/core.d.ts +22 -6
  58. package/dist/core.js +401 -38
  59. package/dist/editing.js +34 -0
  60. package/dist/gantt-view.svelte.d.ts +24 -0
  61. package/dist/gantt-view.svelte.js +13 -0
  62. package/dist/grid-icons.d.ts +2 -2
  63. package/dist/grid-icons.js +2 -2
  64. package/dist/grid-messages.d.ts +10 -0
  65. package/dist/grid-messages.js +9 -0
  66. package/dist/headless.d.ts +5 -0
  67. package/dist/headless.js +12 -0
  68. package/dist/history.d.ts +21 -0
  69. package/dist/history.js +34 -4
  70. package/dist/index.d.ts +5 -4
  71. package/dist/index.js +3 -3
  72. package/dist/keyboard-handlers.js +24 -1
  73. package/dist/keyboard.js +4 -0
  74. package/dist/row-drag-touch.d.ts +1 -1
  75. package/dist/row-drag.js +35 -1
  76. package/dist/row-model.d.ts +139 -0
  77. package/dist/row-model.js +16 -0
  78. package/dist/row-resize.js +22 -1
  79. package/dist/selection.d.ts +2 -1
  80. package/dist/selection.js +31 -3
  81. package/dist/server-block-cache.d.ts +219 -0
  82. package/dist/server-block-cache.js +539 -0
  83. package/dist/server-data-source.d.ts +274 -8
  84. package/dist/server-data-source.js +229 -20
  85. package/dist/server.d.ts +13 -0
  86. package/dist/server.js +13 -0
  87. package/dist/sparkline.js +7 -5
  88. package/dist/svgrid-wrapper.types.d.ts +33 -0
  89. package/dist/validate.d.ts +4 -0
  90. package/dist/validate.js +29 -3
  91. package/package.json +11 -1
  92. package/src/GridMenus.svelte +34 -4
  93. package/src/SvGrid.controller.svelte.ts +397 -61
  94. package/src/SvGrid.css +200 -2
  95. package/src/SvGrid.svelte +350 -40
  96. package/src/SvGrid.types.ts +453 -7
  97. package/src/SvGridDropdown.svelte +20 -0
  98. package/src/SvModal.svelte +22 -0
  99. package/src/SvModal.test.ts +30 -1
  100. package/src/a11y/dismissable.test.ts +10 -1
  101. package/src/a11y/dismissable.ts +22 -2
  102. package/src/build-api.ts +96 -31
  103. package/src/cell-formatting.test.ts +17 -0
  104. package/src/cell-formatting.ts +18 -1
  105. package/src/cell-render.test.ts +24 -2
  106. package/src/cell-render.ts +24 -8
  107. package/src/clipboard.test.ts +107 -0
  108. package/src/clipboard.ts +62 -23
  109. package/src/columns.test.ts +4 -4
  110. package/src/columns.ts +7 -2
  111. package/src/conditional-formatting.test.ts +8 -0
  112. package/src/conditional-formatting.ts +3 -0
  113. package/src/core.rowmodel-cache.test.ts +51 -0
  114. package/src/core.tick-repair.test.ts +287 -0
  115. package/src/core.ts +407 -38
  116. package/src/editing.test.ts +52 -0
  117. package/src/editing.ts +33 -0
  118. package/src/gantt-stub.test.svelte +38 -0
  119. package/src/gantt-view.svelte.ts +35 -0
  120. package/src/grid-icons.ts +2 -2
  121. package/src/grid-messages.test.ts +7 -0
  122. package/src/grid-messages.ts +22 -0
  123. package/src/headless.ts +24 -0
  124. package/src/history.test.ts +49 -1
  125. package/src/history.ts +48 -2
  126. package/src/icon-seam.test.ts +2 -4
  127. package/src/index.ts +38 -10
  128. package/src/keyboard-handlers.coverage.test.ts +65 -0
  129. package/src/keyboard-handlers.ts +26 -1
  130. package/src/keyboard.ts +3 -0
  131. package/src/row-drag-touch.ts +2 -2
  132. package/src/row-drag.test.ts +51 -0
  133. package/src/row-drag.ts +35 -2
  134. package/src/row-model.ts +152 -0
  135. package/src/row-resize.ts +24 -1
  136. package/src/selection.test.ts +3 -0
  137. package/src/selection.ts +33 -3
  138. package/src/server-block-cache.test.ts +672 -0
  139. package/src/server-block-cache.ts +690 -0
  140. package/src/server-data-source.infinite.test.ts +405 -0
  141. package/src/server-data-source.test.ts +19 -0
  142. package/src/server-data-source.ts +496 -29
  143. package/src/server.ts +49 -0
  144. package/src/sparkline.test.ts +7 -0
  145. package/src/sparkline.ts +6 -4
  146. package/src/svgrid-wrapper.types.ts +32 -0
  147. package/src/svgrid.api.test.ts +21 -0
  148. package/src/svgrid.charting.test.ts +12 -4
  149. package/src/svgrid.context-menu.test.ts +12 -0
  150. package/src/svgrid.detail-rows.svelte.test.ts +187 -0
  151. package/src/svgrid.filter-menu-scroll.test.ts +9 -0
  152. package/src/svgrid.gantt-seam.test.ts +199 -0
  153. package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
  154. package/src/svgrid.menu-scroll-close.test.ts +10 -0
  155. package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
  156. package/src/svgrid.row-model-seam.svelte.test.ts +305 -0
  157. package/src/svgrid.row-pinning.test.ts +15 -1
  158. package/src/svgrid.selection-bar-seam.test.ts +1 -1
  159. package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
  160. package/src/svgrid.upsell-license.test.ts +8 -5
  161. package/src/test-setup.ts +12 -0
  162. package/src/transaction.test.ts +58 -0
  163. package/src/validate.test.ts +29 -0
  164. package/src/validate.ts +34 -3
  165. package/dist/SvGroupCell.svelte +0 -141
  166. package/dist/SvGroupCell.svelte.d.ts +0 -49
  167. package/dist/SvRowGroupPanel.svelte +0 -186
  168. package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
  169. package/dist/cdn/GridMenus-7kbpnnBW.js +0 -635
  170. package/dist/cdn/validate-_CDJzgIo.js +0 -75
  171. package/dist/server-group-model.d.ts +0 -98
  172. package/dist/server-group-model.js +0 -263
  173. package/src/SvGroupCell.svelte +0 -141
  174. package/src/SvRowGroupPanel.svelte +0 -186
  175. package/src/server-group-model.test.ts +0 -294
  176. package/src/server-group-model.ts +0 -370
@@ -0,0 +1,405 @@
1
+ /**
2
+ * `createServerDataSource({ mode: 'infinite' })` - the free infinite row model.
3
+ *
4
+ * Page mode hands the grid one page and a pager. Infinite mode hands it the
5
+ * whole result as one scrollable list, with placeholders where the data has
6
+ * not arrived. These tests are about that difference: what `state.rows` holds,
7
+ * when requests happen, and what the same controller methods mean once there
8
+ * are no pages to move between.
9
+ *
10
+ * The block mechanics themselves (eviction, concurrency, unknown counts) are
11
+ * covered in `server-block-cache.test.ts`; this file checks the wiring.
12
+ */
13
+ import { describe, expect, it, vi } from 'vitest'
14
+ import { createServerDataSource, type ServerDataSource, type ServerState } from './server-data-source'
15
+ import { rowPlaceholderState } from './server-block-cache'
16
+
17
+ type Row = { id: number; name: string }
18
+
19
+ /** A backend of `total` rows that honours startRow/endRow, sort and filter. */
20
+ function tableOf(total = 1000) {
21
+ const requests: Array<{ startRow: number; endRow: number; sort: string; global: string }> = []
22
+ const source: ServerDataSource<Row> = {
23
+ async getRows(req) {
24
+ requests.push({
25
+ startRow: req.startRow,
26
+ endRow: req.endRow,
27
+ sort: req.sortModel.map((s) => `${s.id}:${s.desc ? 'desc' : 'asc'}`).join(','),
28
+ global: req.filterModel.global ?? '',
29
+ })
30
+ const all: Row[] = []
31
+ for (let i = 0; i < total; i += 1) all.push({ id: i, name: `Row ${i}` })
32
+ const filtered = req.filterModel.global
33
+ ? all.filter((r) => r.name.includes(req.filterModel.global!))
34
+ : all
35
+ const sorted = req.sortModel[0]?.desc ? [...filtered].reverse() : filtered
36
+ return { rows: sorted.slice(req.startRow, req.endRow), rowCount: sorted.length }
37
+ },
38
+ }
39
+ return { source, requests, starts: () => requests.map((r) => r.startRow) }
40
+ }
41
+
42
+ const settle = async () => {
43
+ for (let i = 0; i < 20; i += 1) await Promise.resolve()
44
+ }
45
+
46
+ describe('createServerDataSource in infinite mode', () => {
47
+ it('spans the whole result, with placeholders where nothing is loaded', async () => {
48
+ const { source } = tableOf(1000)
49
+ let view!: ServerState<Row>
50
+ const ctl = createServerDataSource<Row>(source, {
51
+ mode: 'infinite',
52
+ blockSize: 100,
53
+ onChange: (s) => (view = s),
54
+ })
55
+
56
+ ctl.setViewport(0, 20)
57
+ await settle()
58
+
59
+ expect(view.rows).toHaveLength(1000)
60
+ expect(view.rows[0]).toEqual({ id: 0, name: 'Row 0' })
61
+ expect(rowPlaceholderState(view.rows[900])).toBe('loading')
62
+ expect(view.rowCount).toBe(1000)
63
+ expect(view.lastRowKnown).toBe(true)
64
+ ctl.dispose()
65
+ })
66
+
67
+ it('fetches a block only when the viewport reaches it', async () => {
68
+ const { source, starts } = tableOf(1000)
69
+ const ctl = createServerDataSource<Row>(source, {
70
+ mode: 'infinite',
71
+ blockSize: 100,
72
+ onChange: () => {},
73
+ })
74
+
75
+ ctl.setViewport(0, 20)
76
+ await settle()
77
+ expect(starts()).toEqual([0])
78
+
79
+ ctl.setViewport(640, 660)
80
+ await settle()
81
+ expect(starts()).toEqual([0, 600])
82
+
83
+ // Scrolling back to a block it still holds asks for nothing.
84
+ ctl.setViewport(0, 20)
85
+ await settle()
86
+ expect(starts()).toEqual([0, 600])
87
+ ctl.dispose()
88
+ })
89
+
90
+ it('reloads from the top when the sort or the filter changes', async () => {
91
+ const { source, requests, starts } = tableOf(1000)
92
+ let view!: ServerState<Row>
93
+ const ctl = createServerDataSource<Row>(source, {
94
+ mode: 'infinite',
95
+ blockSize: 100,
96
+ onChange: (s) => (view = s),
97
+ })
98
+
99
+ ctl.setViewport(0, 20)
100
+ await settle()
101
+ ctl.setViewport(300, 320)
102
+ await settle()
103
+ expect(starts()).toEqual([0, 300])
104
+
105
+ ctl.setSort([{ id: 'name', desc: true }])
106
+ await settle()
107
+ // The user is still looking at row 300, so that is what is re-requested -
108
+ // not row 0, which would be a wasted round trip nobody can see.
109
+ expect(starts()).toEqual([0, 300, 300])
110
+ expect(requests.at(-1)!.sort).toBe('name:desc')
111
+ expect(view.rows[300]).toEqual({ id: 699, name: 'Row 699' })
112
+
113
+ ctl.setFilter({ global: 'Row 1' })
114
+ await settle()
115
+ expect(requests.at(-1)!.global).toBe('Row 1')
116
+ ctl.dispose()
117
+ })
118
+
119
+ it('has no pages to move between', async () => {
120
+ const { source, starts } = tableOf(1000)
121
+ const ctl = createServerDataSource<Row>(source, {
122
+ mode: 'infinite',
123
+ blockSize: 100,
124
+ onChange: () => {},
125
+ })
126
+
127
+ ctl.setViewport(0, 20)
128
+ await settle()
129
+ const before = starts().length
130
+
131
+ ctl.setPage(4)
132
+ ctl.setPageSize(25)
133
+ await settle()
134
+ expect(starts()).toHaveLength(before)
135
+ ctl.dispose()
136
+ })
137
+
138
+ it('surfaces a failed block and recovers on retryLoads', async () => {
139
+ let failing = true
140
+ const source: ServerDataSource<Row> = {
141
+ async getRows(req) {
142
+ if (failing && req.startRow >= 100) throw new Error('backend down')
143
+ const rows: Row[] = []
144
+ for (let i = req.startRow; i < Math.min(req.endRow, 1000); i += 1) {
145
+ rows.push({ id: i, name: `Row ${i}` })
146
+ }
147
+ return { rows, rowCount: 1000 }
148
+ },
149
+ }
150
+ let view!: ServerState<Row>
151
+ const ctl = createServerDataSource<Row>(source, {
152
+ mode: 'infinite',
153
+ blockSize: 100,
154
+ onChange: (s) => (view = s),
155
+ })
156
+
157
+ ctl.setViewport(100, 150)
158
+ await settle()
159
+ expect(view.failedBlocks).toEqual([1])
160
+ expect(rowPlaceholderState(view.rows[120])).toBe('failed')
161
+
162
+ failing = false
163
+ ctl.retryLoads()
164
+ await settle()
165
+ expect(view.failedBlocks).toEqual([])
166
+ expect(view.rows[120]).toEqual({ id: 120, name: 'Row 120' })
167
+ ctl.dispose()
168
+ })
169
+
170
+ it('retries only the block under the row the grid asks about', async () => {
171
+ let failing = true
172
+ const starts: number[] = []
173
+ const source: ServerDataSource<Row> = {
174
+ async getRows(req) {
175
+ starts.push(req.startRow)
176
+ if (failing && req.startRow >= 100) throw new Error('backend down')
177
+ const rows: Row[] = []
178
+ for (let i = req.startRow; i < Math.min(req.endRow, 1000); i += 1) {
179
+ rows.push({ id: i, name: `Row ${i}` })
180
+ }
181
+ return { rows, rowCount: 1000 }
182
+ },
183
+ }
184
+ let view!: ServerState<Row>
185
+ const ctl = createServerDataSource<Row>(source, {
186
+ mode: 'infinite',
187
+ blockSize: 100,
188
+ onChange: (s) => (view = s),
189
+ })
190
+ ctl.setViewport(100, 299)
191
+ await settle()
192
+ expect(view.failedBlocks).toEqual([1, 2])
193
+
194
+ failing = false
195
+ const before = starts.length
196
+ // The GridRowModel seam the failed row's Retry button goes through.
197
+ ctl.retryRow!(view.rows[250]!, 250)
198
+ await settle()
199
+ expect(starts.slice(before)).toEqual([200])
200
+ expect(view.failedBlocks).toEqual([1])
201
+ ctl.dispose()
202
+ })
203
+
204
+ it('hands each block a signal that purge, a sort change and dispose abort', async () => {
205
+ const signals: AbortSignal[] = []
206
+ const source: ServerDataSource<Row> = {
207
+ async getRows(req) {
208
+ signals.push(req.signal!)
209
+ // A server that never answers: only the abort can end these.
210
+ return new Promise(() => {})
211
+ },
212
+ }
213
+ const ctl = createServerDataSource<Row>(source, { mode: 'infinite', blockSize: 100, onChange: () => {} })
214
+ ctl.setViewport(0, 20)
215
+ await settle()
216
+ expect(signals).toHaveLength(1)
217
+ expect(signals[0]!.aborted).toBe(false)
218
+
219
+ ctl.purge()
220
+ await settle()
221
+ expect(signals[0]!.aborted).toBe(true)
222
+ expect(signals).toHaveLength(2)
223
+
224
+ ctl.setSort([{ id: 'id', desc: true }])
225
+ await settle()
226
+ expect(signals[1]!.aborted).toBe(true)
227
+
228
+ ctl.dispose()
229
+ expect(signals.at(-1)!.aborted).toBe(true)
230
+ })
231
+
232
+ it('refresh keeps the count and the scroll position, purge does not', async () => {
233
+ const { source, starts } = tableOf(1000)
234
+ let view!: ServerState<Row>
235
+ const ctl = createServerDataSource<Row>(source, {
236
+ mode: 'infinite',
237
+ blockSize: 100,
238
+ onChange: (s) => (view = s),
239
+ })
240
+
241
+ ctl.setViewport(500, 520)
242
+ await settle()
243
+ expect(starts()).toEqual([500])
244
+ expect(view.rowCount).toBe(1000)
245
+
246
+ ctl.refresh()
247
+ await settle()
248
+ expect(starts()).toEqual([500, 500])
249
+ expect(view.rowCount).toBe(1000)
250
+
251
+ ctl.purge()
252
+ await settle()
253
+ expect(starts()).toEqual([500, 500, 500])
254
+ ctl.dispose()
255
+ })
256
+
257
+ it('keeps the whole list after a write, re-reading the held blocks in place', async () => {
258
+ const { source, starts } = tableOf(1000)
259
+ const writable: ServerDataSource<Row> = {
260
+ ...source,
261
+ async updateRow(id, patch) {
262
+ return { id: Number(id), name: `Row ${id}`, ...patch }
263
+ },
264
+ }
265
+ let view!: ServerState<Row>
266
+ const ctl = createServerDataSource<Row>(writable, { mode: 'infinite', blockSize: 100, onChange: (s) => (view = s) })
267
+ ctl.setViewport(500, 520)
268
+ await settle()
269
+ expect(view.rows).toHaveLength(1000)
270
+
271
+ await ctl.updateRow('510', { name: 'Edited' })
272
+ await settle()
273
+ // Not one page of a hundred: the list is still the whole result, and the
274
+ // block the row sits in was asked for again rather than page 0.
275
+ expect(view.rows).toHaveLength(1000)
276
+ expect(starts()).toEqual([500, 500])
277
+ expect(view.rowCount).toBe(1000)
278
+ ctl.dispose()
279
+ })
280
+
281
+ it('patches the cached row on an optimistic update and removes it on delete', async () => {
282
+ const { source } = tableOf(1000)
283
+ let fail = false
284
+ const writable: ServerDataSource<Row> = {
285
+ ...source,
286
+ async updateRow(id, patch) {
287
+ if (fail) throw new Error('nope')
288
+ return { id: Number(id), name: `Row ${id}`, ...patch }
289
+ },
290
+ async deleteRow() {
291
+ if (fail) throw new Error('nope')
292
+ },
293
+ }
294
+ let view!: ServerState<Row>
295
+ const ctl = createServerDataSource<Row>(writable, {
296
+ mode: 'infinite',
297
+ blockSize: 100,
298
+ optimistic: true,
299
+ getRowId: (r) => String(r.id),
300
+ onChange: (s) => (view = s),
301
+ })
302
+ ctl.setViewport(0, 20)
303
+ await settle()
304
+ const at = (i: number) => view.rows[i] as Row
305
+
306
+ await ctl.updateRow('5', { name: 'Edited' })
307
+ expect(at(5).name).toBe('Edited')
308
+ // A later block landing does not undo it: the cache itself was patched.
309
+ ctl.setViewport(200, 220)
310
+ await settle()
311
+ expect(at(5).name).toBe('Edited')
312
+
313
+ fail = true
314
+ await expect(ctl.updateRow('6', { name: 'Nope' })).rejects.toThrow('nope')
315
+ expect(at(6).name).toBe('Row 6')
316
+
317
+ fail = false
318
+ await ctl.deleteRow('7')
319
+ expect(at(7).id).toBe(8)
320
+ expect(view.rowCount).toBe(999)
321
+
322
+ fail = true
323
+ await expect(ctl.deleteRow('8')).rejects.toThrow('nope')
324
+ expect(at(7).id).toBe(8)
325
+ expect(view.rowCount).toBe(999)
326
+ ctl.dispose()
327
+ })
328
+
329
+ it('reports block state for debugging', async () => {
330
+ const { source } = tableOf(1000)
331
+ const ctl = createServerDataSource<Row>(source, {
332
+ mode: 'infinite',
333
+ blockSize: 100,
334
+ onChange: () => {},
335
+ })
336
+
337
+ ctl.setViewport(0, 120)
338
+ await settle()
339
+ expect(ctl.getCacheState()).toEqual([
340
+ { blockIndex: 0, startRow: 0, endRow: 100, status: 'loaded', lastTouched: expect.any(Number) },
341
+ { blockIndex: 1, startRow: 100, endRow: 200, status: 'loaded', lastTouched: expect.any(Number) },
342
+ ])
343
+ ctl.dispose()
344
+ })
345
+
346
+ it('leaves page mode exactly as it was', async () => {
347
+ const { source, starts } = tableOf(1000)
348
+ let view!: ServerState<Row>
349
+ const ctl = createServerDataSource<Row>(source, {
350
+ pageSize: 25,
351
+ onChange: (s) => (view = s),
352
+ })
353
+
354
+ ctl.refresh()
355
+ await settle()
356
+ expect(view.rows).toHaveLength(25)
357
+ expect(view.total).toBe(1000)
358
+ expect(view.pageCount).toBe(40)
359
+
360
+ ctl.setPage(2)
361
+ await settle()
362
+ expect(starts()).toEqual([0, 50])
363
+ expect(view.rows[0]).toEqual({ id: 50, name: 'Row 50' })
364
+
365
+ // The infinite-only methods are inert rather than throwing, so one piece of
366
+ // calling code can drive either mode.
367
+ ctl.setViewport(0, 10)
368
+ ctl.retryLoads()
369
+ ctl.purge()
370
+ expect(ctl.getCacheState()).toEqual([])
371
+ await settle()
372
+ expect(starts()).toEqual([0, 50])
373
+ ctl.dispose()
374
+ })
375
+
376
+ it('warns once when an advanced filter is sent but not acknowledged', async () => {
377
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
378
+ const source: ServerDataSource<Row> = {
379
+ async getRows(req) {
380
+ const rows: Row[] = []
381
+ for (let i = req.startRow; i < Math.min(req.endRow, 500); i += 1) {
382
+ rows.push({ id: i, name: `Row ${i}` })
383
+ }
384
+ return { rows, rowCount: 500 } // never sets appliedExpression
385
+ },
386
+ }
387
+ let view!: ServerState<Row>
388
+ const ctl = createServerDataSource<Row>(source, {
389
+ mode: 'infinite',
390
+ blockSize: 100,
391
+ onChange: (s) => (view = s),
392
+ })
393
+
394
+ ctl.setFilter({ expression: { kind: 'cmp', column: 'name', op: 'contains', value: 'x' } })
395
+ ctl.setViewport(0, 20)
396
+ await settle()
397
+ ctl.setViewport(200, 220)
398
+ await settle()
399
+
400
+ expect(view.expressionUnapplied).toBe(true)
401
+ expect(warn).toHaveBeenCalledTimes(1) // once per controller, not per block
402
+ warn.mockRestore()
403
+ ctl.dispose()
404
+ })
405
+ })
@@ -86,6 +86,25 @@ describe('createServerDataSource', () => {
86
86
  expect(ctl.getState().rows[0]).toEqual({ id: 30 })
87
87
  })
88
88
 
89
+ it('aborts the page fetch a newer one supersedes, and the last one on dispose', async () => {
90
+ const signals: AbortSignal[] = []
91
+ const src: ServerDataSource<Row> = {
92
+ getRows: async (req) => {
93
+ signals.push(req.signal!)
94
+ return new Promise(() => {})
95
+ },
96
+ }
97
+ const ctl = createServerDataSource(src, { pageSize: 10, onChange: () => {} })
98
+ ctl.refresh()
99
+ ctl.setPage(3)
100
+ await new Promise((r) => setTimeout(r, 5))
101
+ expect(signals).toHaveLength(2)
102
+ expect(signals[0]!.aborted).toBe(true)
103
+ expect(signals[1]!.aborted).toBe(false)
104
+ ctl.dispose()
105
+ expect(signals[1]!.aborted).toBe(true)
106
+ })
107
+
89
108
  it('dispose stops further updates', async () => {
90
109
  const src = makeSource(50, 20)
91
110
  const onChange = vi.fn()