@svgrid/grid 3.0.8 → 3.0.9

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 (80) hide show
  1. package/dist/GridMenus.svelte +3 -2
  2. package/dist/SvGrid.controller.svelte.d.ts +60 -8
  3. package/dist/SvGrid.controller.svelte.js +475 -110
  4. package/dist/SvGrid.css +186 -3
  5. package/dist/SvGrid.svelte +281 -163
  6. package/dist/SvGrid.types.d.ts +24 -7
  7. package/dist/cdn/{GridMenus-D15bhpX5.js → GridMenus-B1sipj6y.js} +165 -165
  8. package/dist/cdn/{GridMenus-B8-Gbfq2.js → GridMenus-DlAzfHLk.js} +201 -201
  9. package/dist/cdn/server-block-cache-Dj-KPqEQ.js +1289 -0
  10. package/dist/cdn/{src-Dz625Fhx.js → src-BYsa66I0.js} +9738 -10174
  11. package/dist/cdn/{src-CleI0g9l.js → src-pA4RroLV.js} +10702 -11138
  12. package/dist/cdn/svgrid.js +27 -27
  13. package/dist/cdn/svgrid.svelte-external.js +27 -27
  14. package/dist/cdn/{validate-GEs4ZPSW.js → validate-XuMe4_KR.js} +2 -2
  15. package/dist/cell-render.js +25 -3
  16. package/dist/columns.js +16 -1
  17. package/dist/conditional-formatting.js +9 -2
  18. package/dist/core.d.ts +93 -0
  19. package/dist/core.js +313 -52
  20. package/dist/filtering/excel-filters.js +41 -6
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/selection.d.ts +2 -2
  24. package/dist/selection.js +32 -8
  25. package/dist/server-block-cache.d.ts +7 -32
  26. package/dist/server-block-cache.js +21 -12
  27. package/dist/server-data-source.js +3 -1
  28. package/dist/server.d.ts +1 -0
  29. package/dist/server.js +1 -0
  30. package/dist/spreadsheet.js +7 -0
  31. package/dist/validate.js +3 -11
  32. package/dist/virtualization/column-virtualizer.d.ts +4 -0
  33. package/dist/virtualization/column-virtualizer.js +2 -0
  34. package/dist/virtualization/types.d.ts +20 -0
  35. package/dist/virtualization/virtualizer.js +90 -24
  36. package/dist/windowed-brand.d.ts +22 -0
  37. package/dist/windowed-brand.js +60 -0
  38. package/dist/windowed-data.d.ts +6 -0
  39. package/dist/windowed-data.js +34 -0
  40. package/dist/windowed-row-model.d.ts +1 -0
  41. package/dist/windowed-row-model.js +97 -0
  42. package/package.json +1 -1
  43. package/src/GridMenus.svelte +3 -2
  44. package/src/SvGrid.controller.svelte.ts +469 -114
  45. package/src/SvGrid.css +186 -3
  46. package/src/SvGrid.svelte +281 -163
  47. package/src/SvGrid.types.ts +24 -7
  48. package/src/cell-render.ts +24 -2
  49. package/src/columns.ts +16 -1
  50. package/src/conditional-formatting.ts +6 -4
  51. package/src/core.sort.test.ts +61 -0
  52. package/src/core.ts +309 -52
  53. package/src/counting-cell.test.svelte +11 -0
  54. package/src/filtering/excel-filters.ts +39 -6
  55. package/src/headless.reactivity.svelte.test.ts +1 -1
  56. package/src/index.ts +6 -0
  57. package/src/new-features.test.ts +24 -0
  58. package/src/selection.test.ts +14 -0
  59. package/src/selection.ts +31 -8
  60. package/src/server-block-cache.ts +27 -10
  61. package/src/server-data-source.ts +3 -1
  62. package/src/server.ts +6 -0
  63. package/src/spreadsheet.ts +6 -0
  64. package/src/svgrid.behavior.test.ts +29 -0
  65. package/src/svgrid.column-cell-recycling.test.ts +120 -0
  66. package/src/svgrid.interaction.test.ts +4 -1
  67. package/src/svgrid.new-features.wrapper.test.ts +5 -4
  68. package/src/svgrid.pinned-column-virtualization.test.ts +162 -0
  69. package/src/svgrid.row-model-prop.svelte.test.ts +15 -3
  70. package/src/validate.test.ts +4 -4
  71. package/src/validate.ts +3 -14
  72. package/src/virtualization/column-virtualizer.ts +6 -0
  73. package/src/virtualization/types.ts +20 -0
  74. package/src/virtualization/virtualizer.test.ts +152 -0
  75. package/src/virtualization/virtualizer.ts +93 -27
  76. package/src/windowed-brand.ts +67 -0
  77. package/src/windowed-data.test.ts +105 -0
  78. package/src/windowed-data.ts +36 -0
  79. package/src/windowed-row-model.ts +113 -0
  80. package/dist/cdn/server-block-cache-DIqb3VZn.js +0 -241
package/dist/core.d.ts CHANGED
@@ -614,6 +614,99 @@ export declare const filterFns: {
614
614
  includesString: (value: unknown, query: string) => boolean;
615
615
  equals: (value: unknown, query: unknown) => boolean;
616
616
  };
617
+ /**
618
+ * Everything a base row needs that is the same for every row in the table.
619
+ *
620
+ * One object per table, referenced by every row, instead of one closure scope
621
+ * per row. See {@link BASE_ROW_METHODS}.
622
+ */
623
+ export type BaseRowCtx<TData extends RowData> = {
624
+ grid: SvGrid<TData>;
625
+ store: {
626
+ state: Record<string, any>;
627
+ };
628
+ columns: Array<Column<TData>>;
629
+ columnCount: number;
630
+ columnIndexById: Map<string, number>;
631
+ };
632
+ /**
633
+ * Keys for a base row's private fields.
634
+ *
635
+ * Symbols, not string keys, and that is load-bearing. A row's shared methods
636
+ * need a pointer back to the table, but `_ctx` as a normal property made every
637
+ * row serialise the entire grid: `JSON.stringify(oneRow)` grew with the dataset
638
+ * (981 chars at 3 rows, 67,719 at 3,000) because `options.data` is reachable
639
+ * through it, so stringifying a row model was quadratic. Rows used to serialise
640
+ * to a small constant and must again.
641
+ *
642
+ * A symbol key is invisible to `JSON.stringify`, `Object.keys` and `for...in`,
643
+ * yet IS copied by object spread - which matters because several row models
644
+ * legitimately do `{ ...row, depth }` and the clone needs these to work.
645
+ * Non-enumerable string keys would have hidden them from JSON but also from the
646
+ * spread, silently breaking every cloned row.
647
+ */
648
+ export declare const ROW_CTX: unique symbol;
649
+ export declare const ROW_VALUES: unique symbol;
650
+ export declare const ROW_CELLS: unique symbol;
651
+ export declare const ROW_FILTER_GEN: unique symbol;
652
+ export declare const ROW_FILTER_POS: unique symbol;
653
+ export declare const ROW_SORT_DROPPED: unique symbol;
654
+ export declare const ROW_SORT_KEY: unique symbol;
655
+ /**
656
+ * The rows as a plain dense array. For a lazyRows() list, makes the rows not
657
+ * made yet and returns its backing array - the same row objects, without the
658
+ * Proxy in front. Anything that reads every row (a sort, a filter) should go
659
+ * through this first: a trap per read, over several passes of a million rows,
660
+ * cost more than building the rows did.
661
+ */
662
+ export declare function denseRows<T>(rows: T[]): T[];
663
+ /**
664
+ * The row model for windowed data (windowed-data.ts), or null when `data` is
665
+ * an ordinary array. It lives in windowed-row-model.ts and is installed by the
666
+ * first createWindowedData() call, so a grid that is never handed windowed
667
+ * data does not bundle it: this file imports nothing from either module.
668
+ */
669
+ export type WindowedRowModel<TData extends RowData> = (data: unknown, columns: Array<Column<TData>>) => RowModel<TData> | null;
670
+ export type WindowedRowModelInstall = <TData extends RowData>(ctx: {
671
+ rowCtxFor: (columns: Array<Column<TData>>) => BaseRowCtx<TData>;
672
+ getRowId: () => ((row: TData, index: number) => string) | undefined;
673
+ }) => WindowedRowModel<TData>;
674
+ /** Internal: windowed-row-model.ts installs itself here. */
675
+ export declare function registerWindowedRowModel(install: WindowedRowModelInstall): void;
676
+ /** A base row's private fields, on top of the public {@link Row} surface. */
677
+ export type BaseRowState<TData extends RowData> = Row<TData> & {
678
+ [ROW_CTX]: BaseRowCtx<TData>;
679
+ [ROW_VALUES]: Array<unknown> | null;
680
+ [ROW_CELLS]: Array<Cell<TData>> | null;
681
+ [ROW_FILTER_GEN]: number;
682
+ [ROW_FILTER_POS]: number;
683
+ [ROW_SORT_DROPPED]: number;
684
+ [ROW_SORT_KEY]: unknown;
685
+ };
686
+ /**
687
+ * The methods every base row carries, defined ONCE and assigned by reference.
688
+ *
689
+ * Rows used to be built as object literals whose methods were closures, which
690
+ * meant a 100k-row grid allocated 700k closures and a closure scope per row
691
+ * before painting anything. Measured at 100k x 9: 13.8 ms and 56.5 MB to build,
692
+ * against 2.0 ms and 14.5 MB for this shape - the single largest cost in
693
+ * mounting a large grid.
694
+ *
695
+ * They read their row through `this` rather than a captured variable, which is
696
+ * why they can be shared. Note they are assigned as OWN properties rather than
697
+ * put on a prototype: `Row` is public, several row models legitimately do
698
+ * `{ ...row, depth }`, and a spread copies own properties but not a prototype.
699
+ * A class here would silently strip every method off a cloned row.
700
+ */
701
+ export declare const BASE_ROW_METHODS: {
702
+ getCanExpand(this: BaseRowState<RowData>): boolean;
703
+ getIsExpanded(this: BaseRowState<RowData>): boolean;
704
+ toggleExpanded(this: BaseRowState<RowData>): void;
705
+ getIsSelected(this: BaseRowState<RowData>): boolean;
706
+ toggleSelected(this: BaseRowState<RowData>): void;
707
+ getAllCells(this: BaseRowState<RowData>): Cell<RowData>[];
708
+ getCellValueByColumnId(this: BaseRowState<RowData>, columnId: string): unknown;
709
+ };
617
710
  /**
618
711
  * One stage of the row pipeline: takes the rows produced so far and returns the
619
712
  * next set. Stages compose in the order given to `_rowModels`, so filtering
package/dist/core.js CHANGED
@@ -202,22 +202,102 @@ export const filterFns = {
202
202
  * Non-enumerable string keys would have hidden them from JSON but also from the
203
203
  * spread, silently breaking every cloned row.
204
204
  */
205
- const ROW_CTX = Symbol('svgrid.row.ctx');
206
- const ROW_VALUES = Symbol('svgrid.row.values');
207
- const ROW_CELLS = Symbol('svgrid.row.cells');
205
+ // Exported for windowed-row-model.ts only; index.ts does not re-export them.
206
+ export const ROW_CTX = Symbol('svgrid.row.ctx');
207
+ export const ROW_VALUES = Symbol('svgrid.row.values');
208
+ export const ROW_CELLS = Symbol('svgrid.row.cells');
208
209
  // Where the filtered stage last put this row: the generation of that full
209
210
  // filter and the row's index in its output. Lets a tick swap a replaced row
210
211
  // into place without re-filtering everything (see createFilteredRowModel).
211
- const ROW_FILTER_GEN = Symbol('svgrid.row.filterGen');
212
- const ROW_FILTER_POS = Symbol('svgrid.row.filterPos');
212
+ export const ROW_FILTER_GEN = Symbol('svgrid.row.filterGen');
213
+ export const ROW_FILTER_POS = Symbol('svgrid.row.filterPos');
213
214
  // Stamped on the rows a sort repair drops, so the merge skips them with one
214
215
  // property read per row instead of a Set lookup (3 ms per 100k rows).
215
- const ROW_SORT_DROPPED = Symbol('svgrid.row.sortDropped');
216
+ export const ROW_SORT_DROPPED = Symbol('svgrid.row.sortDropped');
216
217
  // The key(s) the last full sort computed for a row: the key itself for a
217
218
  // one-clause sort, an array for several. Read by the repair path so a tick
218
219
  // never recomputes a kept row's key.
219
- const ROW_SORT_KEY = Symbol('svgrid.row.sortKey');
220
+ export const ROW_SORT_KEY = Symbol('svgrid.row.sortKey');
220
221
  let sortDropGeneration = 0;
222
+ const LAZY_ROWS = Symbol('svgrid.lazyRows');
223
+ const LAZY_FILL = Symbol('svgrid.lazyRows.fill');
224
+ /**
225
+ * A real array of `length` rows whose entries are made on first read.
226
+ *
227
+ * A Proxy over a holey array: `get` fills a hole by calling `make`, and
228
+ * `has` / `getOwnPropertyDescriptor` report every index as present, so
229
+ * map, filter, slice, sort and iteration see a dense array (without them
230
+ * they would skip the holes). Writes go to the backing array.
231
+ */
232
+ function lazyRows(length, make) {
233
+ const store = new Array(length);
234
+ const at = (key) => {
235
+ if (typeof key !== 'string')
236
+ return -1;
237
+ const c = key.charCodeAt(0);
238
+ if (c < 48 || c > 57)
239
+ return -1;
240
+ const i = +key;
241
+ return i < length && (i | 0) === i ? i : -1;
242
+ };
243
+ // Make every row not made yet, in one plain loop, and return the backing
244
+ // array: dense from then on, and read without the trap.
245
+ const fill = () => {
246
+ for (let i = 0; i < length; i++)
247
+ if (store[i] === undefined)
248
+ store[i] = make(i);
249
+ return store;
250
+ };
251
+ return new Proxy(store, {
252
+ get(t, key, receiver) {
253
+ if (key === LAZY_ROWS)
254
+ return t;
255
+ if (key === LAZY_FILL)
256
+ return fill;
257
+ const i = at(key);
258
+ if (i < 0)
259
+ return Reflect.get(t, key, receiver);
260
+ let row = t[i];
261
+ if (row === undefined)
262
+ t[i] = row = make(i);
263
+ return row;
264
+ },
265
+ has(t, key) {
266
+ if (key === LAZY_ROWS)
267
+ return true;
268
+ return at(key) >= 0 || Reflect.has(t, key);
269
+ },
270
+ getOwnPropertyDescriptor(t, key) {
271
+ const i = at(key);
272
+ if (i < 0)
273
+ return Reflect.getOwnPropertyDescriptor(t, key);
274
+ let row = t[i];
275
+ if (row === undefined)
276
+ t[i] = row = make(i);
277
+ return { value: row, writable: true, enumerable: true, configurable: true };
278
+ },
279
+ });
280
+ }
281
+ /**
282
+ * The rows as a plain dense array. For a lazyRows() list, makes the rows not
283
+ * made yet and returns its backing array - the same row objects, without the
284
+ * Proxy in front. Anything that reads every row (a sort, a filter) should go
285
+ * through this first: a trap per read, over several passes of a million rows,
286
+ * cost more than building the rows did.
287
+ */
288
+ export function denseRows(rows) {
289
+ const fill = rows[LAZY_FILL];
290
+ return fill ? fill() : rows;
291
+ }
292
+ /** The backing array of a lazyRows() list (rows made so far, holes elsewhere), or null. */
293
+ function lazyRowsBacking(rows) {
294
+ return rows ? (rows[LAZY_ROWS] ?? null) : null;
295
+ }
296
+ let installWindowedRowModel = null;
297
+ /** Internal: windowed-row-model.ts installs itself here. */
298
+ export function registerWindowedRowModel(install) {
299
+ installWindowedRowModel = install;
300
+ }
221
301
  /**
222
302
  * The methods every base row carries, defined ONCE and assigned by reference.
223
303
  *
@@ -233,7 +313,7 @@ let sortDropGeneration = 0;
233
313
  * `{ ...row, depth }`, and a spread copies own properties but not a prototype.
234
314
  * A class here would silently strip every method off a cloned row.
235
315
  */
236
- const BASE_ROW_METHODS = {
316
+ export const BASE_ROW_METHODS = {
237
317
  getCanExpand() {
238
318
  return false;
239
319
  },
@@ -777,6 +857,7 @@ export function createSortedRowModel(localSortFns = sortFns) {
777
857
  let compare;
778
858
  let keyOf;
779
859
  let autoMode;
860
+ let rankCount;
780
861
  if (comparator === sortFns.number) {
781
862
  keyOf = (row) => numberSortKey(row.getCellValueByColumnId(columnId));
782
863
  for (let i = 0; i < n; i++)
@@ -827,9 +908,20 @@ export function createSortedRowModel(localSortFns = sortFns) {
827
908
  // column. Reading every k-th row is no more expensive and does not care
828
909
  // how the rows are arranged.
829
910
  const rankLimit = n >> 1;
830
- let distinct = null;
911
+ // Each distinct value's id (its insertion order), when ranking.
912
+ let idOf = null;
831
913
  if (n > 0) {
832
- const sampleTarget = Math.min(n, 256);
914
+ // Ranking pays when the column has at most n/2 distinct values. A
915
+ // small sample cannot see that by asking whether half of it repeats:
916
+ // 1M rows of 80k values, each one 12 times over, gave 256 samples
917
+ // with almost no repeats, so ranking was skipped and the sort made
918
+ // 20M collator calls (1.75 s; ranked, it is a fraction of that).
919
+ // Count the repeats in a larger sample instead. S samples from u
920
+ // equally common values repeat about S^2 / 2u times, so u <= n/2
921
+ // shows as at least S^2 / n repeats. Up to SAMPLE rows the whole
922
+ // column is the sample and the test is exact.
923
+ const SAMPLE = 2048;
924
+ const sampleTarget = Math.min(n, SAMPLE);
833
925
  const stride = Math.max(1, Math.floor(n / sampleTarget));
834
926
  const sample = new Set();
835
927
  let sampled = 0;
@@ -837,17 +929,30 @@ export function createSortedRowModel(localSortFns = sortFns) {
837
929
  sample.add(autoTextKey(rows[i].getCellValueByColumnId(columnId)));
838
930
  sampled++;
839
931
  }
840
- // Only attempt ranking when the sample suggests real repetition.
841
- if (sample.size * 2 <= sampled)
842
- distinct = new Set();
932
+ const repeats = sampled - sample.size;
933
+ const worthRanking = stride === 1 ? sample.size <= rankLimit : repeats * n >= sampled * sampled;
934
+ if (worthRanking)
935
+ idOf = new Map();
843
936
  }
937
+ // One hash per row: the value's id is looked up (or assigned) as the
938
+ // row is read, and the rank lookup further down is an array read by
939
+ // id. It used to add every value to a Set and then look every value up
940
+ // again in a rank Map, two hashes per row.
941
+ const ids = idOf ? new Int32Array(n) : null;
844
942
  for (let i = 0; i < n; i++) {
845
943
  const s = autoTextKey(rows[i].getCellValueByColumnId(columnId));
846
944
  strings[i] = s;
847
- if (distinct) {
848
- distinct.add(s);
849
- if (distinct.size > rankLimit)
850
- distinct = null;
945
+ if (idOf) {
946
+ let id = idOf.get(s);
947
+ if (id === undefined) {
948
+ id = idOf.size;
949
+ idOf.set(s, id);
950
+ if (idOf.size > rankLimit) {
951
+ idOf = null;
952
+ continue;
953
+ }
954
+ }
955
+ ids[i] = id;
851
956
  }
852
957
  }
853
958
  // Collation is by far the most expensive comparison we do - a CPU
@@ -863,13 +968,17 @@ export function createSortedRowModel(localSortFns = sortFns) {
863
968
  // Guarded on the uniqueness ratio: when nearly every value is distinct
864
969
  // the ranking pass cannot save any collator calls and would just add an
865
970
  // O(n) Map build, so that case keeps comparing directly.
866
- if (distinct) {
867
- const ordered = Array.from(distinct).sort(compareCollatedKeys);
868
- const rankOf = new Map();
869
- for (let i = 0; i < ordered.length; i++)
870
- rankOf.set(ordered[i], i);
971
+ if (idOf) {
972
+ // Map keys come back in insertion order, which is id order.
973
+ const values = Array.from(idOf.keys());
974
+ const byRank = values.map((_, id) => id).sort((a, b) => compareCollatedKeys(values[a], values[b]));
975
+ const rankById = new Int32Array(values.length);
976
+ for (let r = 0; r < byRank.length; r++)
977
+ rankById[byRank[r]] = r;
871
978
  for (let i = 0; i < n; i++)
872
- keys[i] = rankOf.get(strings[i]);
979
+ keys[i] = rankById[ids[i]];
980
+ const rankIds = idOf;
981
+ rankCount = values.length;
873
982
  compare = compareNumericKeys;
874
983
  // A replacement whose text is not among the ranked values has no
875
984
  // rank; the repair path treats that as "sort everything again".
@@ -878,7 +987,8 @@ export function createSortedRowModel(localSortFns = sortFns) {
878
987
  // A number arriving in a tick makes the column mixed; resort fully.
879
988
  if (typeof value === 'number')
880
989
  return MISSING_KEY;
881
- return rankOf.get(autoTextKey(value)) ?? MISSING_KEY;
990
+ const id = rankIds.get(autoTextKey(value));
991
+ return id === undefined ? MISSING_KEY : rankById[id];
882
992
  };
883
993
  }
884
994
  else {
@@ -896,14 +1006,16 @@ export function createSortedRowModel(localSortFns = sortFns) {
896
1006
  keys[i] = keyOf(rows[i]);
897
1007
  compare = comparator;
898
1008
  }
899
- clauses.push({ keys, desc: clause.desc, compare, keyOf });
1009
+ clauses.push({ keys, desc: clause.desc, compare, keyOf, rankCount });
900
1010
  }
901
1011
  if (!clauses.length)
902
1012
  return null;
903
1013
  // Sort an index array, then materialise. `Array.prototype.sort` is stable,
904
1014
  // so equal keys keep their original relative order exactly as the previous
905
1015
  // `[...rows].sort(...)` did.
906
- const order = new Array(rows.length);
1016
+ // An index array the comparator sorts in place, or a typed array from
1017
+ // countingOrder / radixOrder for a large one-column numeric sort.
1018
+ let order = new Array(rows.length);
907
1019
  for (let i = 0; i < order.length; i++)
908
1020
  order[i] = i;
909
1021
  // Single-clause sorts get a specialised comparator.
@@ -920,19 +1032,28 @@ export function createSortedRowModel(localSortFns = sortFns) {
920
1032
  // which the spec coerces to 0, and they differ only in producing 0 versus
921
1033
  // -0 for equal keys, which sorts identically.
922
1034
  if (clauses.length === 1) {
923
- const { keys, compare, desc } = clauses[0];
924
- if (compare === compareNumericKeys) {
1035
+ const { keys, compare, desc, rankCount } = clauses[0];
1036
+ if (rankCount !== undefined && rows.length >= STABLE_NUMERIC_SORT_MIN) {
1037
+ order = countingOrder(keys, rankCount, desc);
1038
+ }
1039
+ else if (compare === compareNumericKeys && rows.length >= STABLE_NUMERIC_SORT_MIN) {
1040
+ order = radixOrder(keys, desc);
1041
+ }
1042
+ else if (compare === compareNumericKeys) {
1043
+ ;
925
1044
  order.sort(desc
926
1045
  ? function compareOneNumericDesc(ia, ib) { return keys[ib] - keys[ia]; }
927
1046
  : function compareOneNumericAsc(ia, ib) { return keys[ia] - keys[ib]; });
928
1047
  }
929
1048
  else {
1049
+ ;
930
1050
  order.sort(desc
931
1051
  ? function compareOneDesc(ia, ib) { return -compare(keys[ia], keys[ib]); }
932
1052
  : function compareOneAsc(ia, ib) { return compare(keys[ia], keys[ib]); });
933
1053
  }
934
1054
  }
935
1055
  else {
1056
+ ;
936
1057
  order.sort(function compareRowsByClauses(ia, ib) {
937
1058
  for (let k = 0; k < clauses.length; k++) {
938
1059
  const clause = clauses[k];
@@ -1159,6 +1280,94 @@ function autoSortMode(rows, columnId) {
1159
1280
  * Left as its own function so the sort path has one place to change if that
1160
1281
  * ever stops being true. Re-measure before "optimising" this again.
1161
1282
  */
1283
+ /**
1284
+ * From this many rows, a one-column numeric sort orders its row indices
1285
+ * without a comparator. `Array.prototype.sort` with a JS comparator made
1286
+ * ~20M comparator calls for 1M rows, most of a numeric sort's time; both
1287
+ * orders below are stable, as that sort is, so equal keys keep input order.
1288
+ * Under it the comparator sort is cheaper than the bucket arrays.
1289
+ */
1290
+ const STABLE_NUMERIC_SORT_MIN = 20000;
1291
+ /** Stable order of `keys` that are integer ranks in [0, rankCount): one counting pass. */
1292
+ function countingOrder(keys, rankCount, desc) {
1293
+ const n = keys.length;
1294
+ const starts = new Uint32Array(rankCount + 1);
1295
+ for (let i = 0; i < n; i++)
1296
+ starts[(desc ? rankCount - 1 - keys[i] : keys[i]) + 1]++;
1297
+ for (let r = 0; r < rankCount; r++)
1298
+ starts[r + 1] += starts[r];
1299
+ const out = new Uint32Array(n);
1300
+ for (let i = 0; i < n; i++)
1301
+ out[starts[desc ? rankCount - 1 - keys[i] : keys[i]]++] = i;
1302
+ return out;
1303
+ }
1304
+ const LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
1305
+ /**
1306
+ * Stable order of float keys: an LSD radix sort over the IEEE bits, four
1307
+ * 16-bit passes, a pass skipped when every key shares its digit. The bits are
1308
+ * mapped so unsigned order is numeric order (sign bit flipped for positives,
1309
+ * every bit for negatives), and inverted for a descending sort, which keeps
1310
+ * equal keys in input order exactly as a descending comparator does. -0 is
1311
+ * folded into 0 first, since the comparator treated them as equal. Keys are
1312
+ * never NaN here: numberSortKey and dateSortKey map those to -Infinity.
1313
+ */
1314
+ function radixOrder(keys, desc) {
1315
+ const n = keys.length;
1316
+ const f64 = new Float64Array(n);
1317
+ for (let i = 0; i < n; i++)
1318
+ f64[i] = keys[i] + 0;
1319
+ const words = new Uint32Array(f64.buffer);
1320
+ const lo = new Uint32Array(n);
1321
+ const hi = new Uint32Array(n);
1322
+ const loAt = LITTLE_ENDIAN ? 0 : 1;
1323
+ const hiAt = 1 - loAt;
1324
+ for (let i = 0; i < n; i++) {
1325
+ let h = words[2 * i + hiAt];
1326
+ let l = words[2 * i + loAt];
1327
+ if (h & 0x80000000) {
1328
+ h = ~h >>> 0;
1329
+ l = ~l >>> 0;
1330
+ }
1331
+ else {
1332
+ h = (h | 0x80000000) >>> 0;
1333
+ }
1334
+ if (desc) {
1335
+ h = ~h >>> 0;
1336
+ l = ~l >>> 0;
1337
+ }
1338
+ hi[i] = h;
1339
+ lo[i] = l;
1340
+ }
1341
+ let src = new Uint32Array(n);
1342
+ for (let i = 0; i < n; i++)
1343
+ src[i] = i;
1344
+ let dst = new Uint32Array(n);
1345
+ const counts = new Uint32Array(65536);
1346
+ for (let pass = 0; pass < 4; pass++) {
1347
+ const word = pass < 2 ? lo : hi;
1348
+ const shift = pass % 2 === 0 ? 0 : 16;
1349
+ counts.fill(0);
1350
+ for (let i = 0; i < n; i++)
1351
+ counts[(word[i] >>> shift) & 0xffff]++;
1352
+ // Every key in one bucket: this digit cannot change the order.
1353
+ if (counts[(word[src[0]] >>> shift) & 0xffff] === n)
1354
+ continue;
1355
+ let sum = 0;
1356
+ for (let d = 0; d < 65536; d++) {
1357
+ const c = counts[d];
1358
+ counts[d] = sum;
1359
+ sum += c;
1360
+ }
1361
+ for (let i = 0; i < n; i++) {
1362
+ const idx = src[i];
1363
+ dst[counts[(word[idx] >>> shift) & 0xffff]++] = idx;
1364
+ }
1365
+ const t = src;
1366
+ src = dst;
1367
+ dst = t;
1368
+ }
1369
+ return src;
1370
+ }
1162
1371
  function compareCollatedKeys(a, b) {
1163
1372
  return a.localeCompare(b);
1164
1373
  }
@@ -1192,6 +1401,24 @@ export function createSvGridCore(options) {
1192
1401
  /** Old row -> new row for the rebuild the next pipeline run follows; see `replacedRowsOf`. */
1193
1402
  let replacedRows = null;
1194
1403
  let cachedRowCtx = null;
1404
+ /** This grid's windowed-data row model, made on first use from the installed hook. */
1405
+ let windowedRowModel = null;
1406
+ /** The shared row context for `columns`, reused while they are the same. */
1407
+ function rowCtxFor(columns) {
1408
+ if (cachedRowCtx && cachedRowCtx.columns === columns)
1409
+ return cachedRowCtx;
1410
+ const columnIndexById = new Map();
1411
+ for (let i = 0; i < columns.length; i++)
1412
+ columnIndexById.set(columns[i].id, i);
1413
+ cachedRowCtx = {
1414
+ grid: grid,
1415
+ store,
1416
+ columns,
1417
+ columnCount: columns.length,
1418
+ columnIndexById,
1419
+ };
1420
+ return cachedRowCtx;
1421
+ }
1195
1422
  let cachedRowModel = null;
1196
1423
  let cachedRowModelBaseRows = null;
1197
1424
  let cachedPipeline = options._rowModels;
@@ -1375,6 +1602,12 @@ export function createSvGridCore(options) {
1375
1602
  // (a $state.raw signal), and the rebuild below touched it twice per row:
1376
1603
  // 200k tracked reads per 100k-row tick, 11 ms of a 65 ms tick.
1377
1604
  const data = options.data;
1605
+ if (installWindowedRowModel) {
1606
+ windowedRowModel ?? (windowedRowModel = installWindowedRowModel({ rowCtxFor, getRowId: () => options.getRowId }));
1607
+ const model = windowedRowModel(data, columns);
1608
+ if (model)
1609
+ return model;
1610
+ }
1378
1611
  if (cachedBaseRowsInput !== data || cachedBaseRowsColumns !== columns) {
1379
1612
  // Same columns as last time: the shared context still describes them,
1380
1613
  // and a row whose data object sits at the same index can keep its
@@ -1384,7 +1617,10 @@ export function createSvGridCore(options) {
1384
1617
  // landing. The reused row drops its memoised values and cells, since
1385
1618
  // an app may have changed the object in place before passing a new
1386
1619
  // array - that is the case the old rebuild covered by accident.
1387
- const previous = cachedBaseRowsColumns === columns && cachedRowCtx ? cachedBaseRows : null;
1620
+ // A lazy row list (lazyRows below) is read through its backing array,
1621
+ // so reusing rows never makes the ones nobody looked at.
1622
+ const previous = cachedBaseRowsColumns === columns && cachedRowCtx ? (lazyRowsBacking(cachedBaseRows) ?? cachedBaseRows) : null;
1623
+ const previousInput = cachedBaseRowsInput;
1388
1624
  // Same length as last time: a replacement, not an add or remove, so
1389
1625
  // each new row stands in for the old row at its index and the stages
1390
1626
  // can repair rather than recompute. Anything else is structural.
@@ -1412,31 +1648,14 @@ export function createSvGridCore(options) {
1412
1648
  };
1413
1649
  cachedRowCtx = rowCtx;
1414
1650
  }
1415
- cachedBaseRows = new Array(data.length);
1416
1651
  const getRowId = options.getRowId;
1417
1652
  const m = BASE_ROW_METHODS;
1418
- for (let index = 0; index < data.length; index++) {
1653
+ // `_values` and `_cells` stay null until something reads them - a
1654
+ // 100k-row grid showing twenty rows must not materialise every row's
1655
+ // values or cell objects to paint.
1656
+ const makeRow = (index) => {
1419
1657
  const original = data[index];
1420
- const kept = previous?.[index];
1421
- if (kept && kept.original === original) {
1422
- // Read before writing: a row nobody rendered (a million off-screen
1423
- // placeholders under a row model) has nothing memoised, and a
1424
- // store per row was a measurable share of each block landing.
1425
- if (kept[ROW_VALUES] !== null)
1426
- kept[ROW_VALUES] = null;
1427
- if (kept[ROW_CELLS] !== null)
1428
- kept[ROW_CELLS] = null;
1429
- cachedBaseRows[index] = kept;
1430
- continue;
1431
- }
1432
- // Past a quarter of the rows the merge stops being cheaper than a
1433
- // sort with the ranking pass; let the stages do the full work.
1434
- if (replacedRows && replacedRows.size * 4 >= data.length)
1435
- replacedRows = null;
1436
- // `_values` and `_cells` stay null until something reads them - a
1437
- // 100k-row grid showing twenty rows must not materialise every row's
1438
- // values or cell objects to paint.
1439
- const row = {
1658
+ return {
1440
1659
  id: getRowId ? getRowId(original, index) : String(index),
1441
1660
  index,
1442
1661
  original,
@@ -1456,6 +1675,42 @@ export function createSvGridCore(options) {
1456
1675
  getAllCells: m.getAllCells,
1457
1676
  getCellValueByColumnId: m.getCellValueByColumnId,
1458
1677
  };
1678
+ };
1679
+ if (!previous) {
1680
+ // A first build has nothing to reuse or repair: make each row the
1681
+ // first time something reads it. A grid that only shows the first
1682
+ // screen makes ~40 row objects, not 100k; a sort or a filter reads
1683
+ // every row and makes them then. Building them all up front was
1684
+ // most of a 100k-row mount (~24 ms, plus the garbage).
1685
+ cachedBaseRows = lazyRows(data.length, makeRow);
1686
+ }
1687
+ else
1688
+ cachedBaseRows = new Array(data.length);
1689
+ for (let index = 0; previous && index < data.length; index++) {
1690
+ const original = data[index];
1691
+ const kept = previous[index];
1692
+ if (kept && kept.original === original) {
1693
+ // Read before writing: a row nobody rendered (a million off-screen
1694
+ // placeholders under a row model) has nothing memoised, and a
1695
+ // store per row was a measurable share of each block landing.
1696
+ if (kept[ROW_VALUES] !== null)
1697
+ kept[ROW_VALUES] = null;
1698
+ if (kept[ROW_CELLS] !== null)
1699
+ kept[ROW_CELLS] = null;
1700
+ cachedBaseRows[index] = kept;
1701
+ continue;
1702
+ }
1703
+ // Past a quarter of the rows the merge stops being cheaper than a
1704
+ // sort with the ranking pass; let the stages do the full work.
1705
+ if (replacedRows && replacedRows.size * 4 >= data.length)
1706
+ replacedRows = null;
1707
+ const row = makeRow(index);
1708
+ // The previous list was lazy and never made this row, but its data
1709
+ // object is the same one: hand the old list this row too, so a
1710
+ // caller still holding it gets the same object for the same data,
1711
+ // as when every row was built up front.
1712
+ if (kept === undefined && previousInput?.[index] === original)
1713
+ previous[index] = row;
1459
1714
  cachedBaseRows[index] = row;
1460
1715
  if (replacedRows && kept)
1461
1716
  replacedRows.set(kept, row);
@@ -1499,7 +1754,13 @@ export function createSvGridCore(options) {
1499
1754
  cachedSlices?.expanded === currentSlices.expanded) {
1500
1755
  return cachedRowModel;
1501
1756
  }
1502
- let rows = cachedBaseRows;
1757
+ // A stage with work to do reads every row; give it the dense array.
1758
+ const anyStageActive = (currentSlices.sorting?.length ?? 0) > 0 ||
1759
+ (currentSlices.columnFilters?.length ?? 0) > 0 ||
1760
+ (currentSlices.grouping?.length ?? 0) > 0 ||
1761
+ currentSlices.expanded === true ||
1762
+ Object.keys(currentSlices.expanded ?? {}).length > 0;
1763
+ let rows = anyStageActive ? denseRows(cachedBaseRows) : cachedBaseRows;
1503
1764
  const pipeline = options._rowModels ?? {};
1504
1765
  const ordered = [
1505
1766
  pipeline.coreRowModel,