@datakable/core 2.0.3 → 2.1.1

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 (37) hide show
  1. package/dist/features/search/search-index.cjs +108 -81
  2. package/dist/features/search/search-index.cjs.map +1 -1
  3. package/dist/features/search/search-index.js +109 -82
  4. package/dist/features/search/search-index.js.map +1 -1
  5. package/dist/features/search/search.cjs +2 -1
  6. package/dist/features/search/search.cjs.map +1 -1
  7. package/dist/features/search/search.js +6 -2
  8. package/dist/features/search/search.js.map +1 -1
  9. package/dist/features/sorting/ranks.cjs +117 -50
  10. package/dist/features/sorting/ranks.cjs.map +1 -1
  11. package/dist/features/sorting/ranks.js +117 -50
  12. package/dist/features/sorting/ranks.js.map +1 -1
  13. package/dist/features/sorting/sorting.cjs +32 -8
  14. package/dist/features/sorting/sorting.cjs.map +1 -1
  15. package/dist/features/sorting/sorting.js +32 -8
  16. package/dist/features/sorting/sorting.js.map +1 -1
  17. package/dist/pipeline/column-cache.cjs +6 -2
  18. package/dist/pipeline/column-cache.cjs.map +1 -1
  19. package/dist/pipeline/column-cache.js +6 -2
  20. package/dist/pipeline/column-cache.js.map +1 -1
  21. package/dist/pipeline/release.cjs +34 -0
  22. package/dist/pipeline/release.cjs.map +1 -0
  23. package/dist/pipeline/release.js +8 -0
  24. package/dist/pipeline/release.js.map +1 -0
  25. package/dist/pipeline/runner.cjs +15 -1
  26. package/dist/pipeline/runner.cjs.map +1 -1
  27. package/dist/pipeline/runner.js +15 -1
  28. package/dist/pipeline/runner.js.map +1 -1
  29. package/dist/table/create-table.cjs +3 -0
  30. package/dist/table/create-table.cjs.map +1 -1
  31. package/dist/table/create-table.js +3 -0
  32. package/dist/table/create-table.js.map +1 -1
  33. package/dist/version.cjs +1 -1
  34. package/dist/version.cjs.map +1 -1
  35. package/dist/version.js +1 -1
  36. package/dist/version.js.map +1 -1
  37. package/package.json +3 -3
@@ -25,85 +25,152 @@ __export(ranks_exports, {
25
25
  module.exports = __toCommonJS(ranks_exports);
26
26
  var import_compare = require("./compare.cjs");
27
27
  var CHUNK = 4096;
28
- var RUN = 512;
29
- function* sortPositions(n, cmp) {
28
+ var RUN = 1024;
29
+ function* sortPositions(n, cmp, run) {
30
30
  let a = new Uint32Array(n);
31
+ let ea = new Uint8Array(n);
31
32
  for (let i = 0; i < n; i++) a[i] = i;
32
- for (let lo = 0; lo < n; lo += RUN) {
33
- a.subarray(lo, Math.min(lo + RUN, n)).sort(cmp);
33
+ for (let lo = 0; lo < n; lo += run) {
34
+ const hi = Math.min(lo + run, n);
35
+ let zero = false;
36
+ a.subarray(lo, hi).sort((x, y) => {
37
+ const c = cmp(x, y);
38
+ if (c === 0) zero = true;
39
+ return c;
40
+ });
41
+ if (zero)
42
+ for (let k = lo + 1; k < hi; k++) ea[k] = cmp(a[k - 1], a[k]) ? 0 : 1;
34
43
  yield;
35
44
  }
36
45
  let b = new Uint32Array(n);
46
+ let eb = new Uint8Array(n);
37
47
  let ops = 0;
38
- for (let width = RUN; width < n; width *= 2) {
48
+ for (let width = run; width < n; width *= 2) {
39
49
  for (let lo = 0; lo < n; lo += 2 * width) {
40
50
  const mid = Math.min(lo + width, n);
41
51
  const hi = Math.min(lo + 2 * width, n);
42
52
  let i = lo;
43
53
  let j = mid;
44
- let k = lo;
45
- while (i < mid && j < hi) {
46
- const x = a[i];
47
- const y = a[j];
48
- if (cmp(y, x) < 0) {
49
- b[k++] = y;
50
- j++;
51
- } else {
52
- b[k++] = x;
53
- i++;
54
- }
55
- if (++ops === CHUNK) {
56
- ops = 0;
57
- yield;
58
- }
54
+ let prev = -1;
55
+ let same = 0;
56
+ for (let k = lo; k < hi; k++) {
57
+ let c = 1;
58
+ let src;
59
+ if (i < mid && j < hi) {
60
+ c = cmp(a[j], a[i]);
61
+ src = c < 0 ? 1 : 0;
62
+ if (++ops === CHUNK) {
63
+ ops = 0;
64
+ yield;
65
+ }
66
+ } else src = i < mid ? 0 : 1;
67
+ const p = src ? j++ : i++;
68
+ b[k] = a[p];
69
+ eb[k] = prev === src ? ea[p] : prev < 0 ? 0 : same;
70
+ same = src === 0 && c === 0 ? 1 : 0;
71
+ prev = src;
59
72
  }
60
- while (i < mid) b[k++] = a[i++];
61
- while (j < hi) b[k++] = a[j++];
62
73
  }
63
74
  const t = a;
64
75
  a = b;
65
76
  b = t;
77
+ const e = ea;
78
+ ea = eb;
79
+ eb = e;
66
80
  }
67
- return a;
81
+ return { order: a, eq: ea };
68
82
  }
69
- function* collationRanks(values, collator) {
83
+ function* collationRanks(values, collator, prev = { values: [], ranks: [], reps: [] }) {
70
84
  const n = values.length;
71
- const ids = /* @__PURE__ */ new Map();
72
- const distinct = [];
73
- const slot = new Int32Array(n);
85
+ const { values: was, ranks: wasRank, reps } = prev;
86
+ const m = reps.length;
87
+ const out = new Float64Array(n);
88
+ const used = new Uint8Array(m);
89
+ const freshIds = /* @__PURE__ */ new Map();
90
+ const fresh = [];
91
+ const gap = [];
92
+ let rowOf;
93
+ let shift = 0;
94
+ let ops = 0;
74
95
  for (let r = 0; r < n; r++) {
75
96
  const v = values[r];
76
97
  if (typeof v === "string") {
77
- let id = ids.get(v);
78
- if (id === void 0) {
79
- id = distinct.length;
80
- ids.set(v, id);
81
- distinct.push(v);
98
+ let k;
99
+ const q = r + shift;
100
+ if (was[q] === v) k = wasRank[q];
101
+ else {
102
+ const id = freshIds.get(v);
103
+ if (id !== void 0) k = m + id;
104
+ else {
105
+ let lo = 0;
106
+ let hi = m;
107
+ while (lo < hi) {
108
+ const mid = lo + hi >>> 1;
109
+ const c = collator.compare(v, reps[mid]);
110
+ ops++;
111
+ if (c === 0) {
112
+ k = mid;
113
+ break;
114
+ }
115
+ if (c < 0) hi = mid;
116
+ else lo = mid + 1;
117
+ }
118
+ if (k === void 0) {
119
+ k = m + fresh.length;
120
+ freshIds.set(v, fresh.length);
121
+ fresh.push(v);
122
+ gap.push(lo - 0.5);
123
+ } else {
124
+ if (!rowOf) {
125
+ rowOf = new Int32Array(m);
126
+ for (let p = 0; p < was.length; p++) {
127
+ const x = wasRank[p];
128
+ if (!Number.isNaN(x)) rowOf[x] = p;
129
+ }
130
+ }
131
+ shift = rowOf[k] - r;
132
+ }
133
+ }
82
134
  }
83
- slot[r] = id;
84
- } else if ((0, import_compare.typeRank)(v) === import_compare.RANK.missing) slot[r] = -1;
135
+ if (k < m) used[k] = 1;
136
+ out[r] = k;
137
+ } else if ((0, import_compare.typeRank)(v) === import_compare.RANK.missing) out[r] = Number.NaN;
85
138
  else return null;
86
- if (r % CHUNK === CHUNK - 1) yield;
139
+ if (++ops >= CHUNK) {
140
+ ops = 0;
141
+ yield;
142
+ }
87
143
  }
88
- const order = yield* sortPositions(
89
- distinct.length,
90
- (a, b) => collator.compare(distinct[a], distinct[b])
144
+ if (!fresh.length && !used.includes(0)) return { ranks: out, state: { values, ranks: out, reps } };
145
+ const f = fresh.length;
146
+ const { order, eq } = yield* sortPositions(
147
+ f,
148
+ // By gap; two new strings in the same gap (cold: all of them) go to the collator.
149
+ (a, b) => gap[a] - gap[b] || collator.compare(fresh[a], fresh[b]),
150
+ // Comparing numbers is far cheaper: longer native runs, fewer merge passes.
151
+ m ? 8 * RUN : RUN
91
152
  );
92
- const rankOf = new Float64Array(distinct.length);
93
- let rank = 0;
94
- for (let k = 0; k < order.length; k++) {
95
- const id = order[k];
96
- const s = distinct[id];
97
- if (k > 0 && collator.compare(distinct[order[k - 1]], s) !== 0) rank++;
98
- rankOf[id] = rank;
99
- if (k % CHUNK === CHUNK - 1) yield;
153
+ const remap = new Float64Array(m + f);
154
+ const kept = [];
155
+ let rank = -1;
156
+ let j = 0;
157
+ for (let g = 0; g <= m; g++) {
158
+ while (j < f && gap[order[j]] < g) {
159
+ const id = order[j];
160
+ if (!eq[j]) kept[++rank] = fresh[id];
161
+ remap[m + id] = rank;
162
+ j++;
163
+ }
164
+ if (g < m && used[g]) {
165
+ kept[++rank] = reps[g];
166
+ remap[g] = rank;
167
+ }
100
168
  }
101
- const out = new Float64Array(n);
102
169
  for (let r = 0; r < n; r++) {
103
- const id = slot[r];
104
- out[r] = id < 0 ? Number.NaN : rankOf[id];
170
+ const k = out[r];
171
+ if (!Number.isNaN(k)) out[r] = remap[k];
105
172
  }
106
- return out;
173
+ return { ranks: out, state: { values, ranks: out, reps: kept } };
107
174
  }
108
175
  // Annotate the CommonJS export names for ESM import in node:
109
176
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/sorting/ranks.ts"],"sourcesContent":["import { RANK, typeRank } from './compare'\n\n/** Comparisons / rows between two `yield`s of the jobs below (§9.1). */\nconst CHUNK = 4096\n/** Run length sorted natively before the merge passes. */\nconst RUN = 512\n\n/**\n * Sorts `0..n-1` by `cmp` as a time-sliced job (§9.1): native sorts of short runs, then bottom-up\n * merge passes that `yield` every {@link CHUNK} comparisons. Stable.\n */\nfunction* sortPositions(\n n: number,\n cmp: (a: number, b: number) => number,\n): Generator<undefined, Uint32Array, undefined> {\n let a = new Uint32Array(n)\n for (let i = 0; i < n; i++) a[i] = i\n for (let lo = 0; lo < n; lo += RUN) {\n a.subarray(lo, Math.min(lo + RUN, n)).sort(cmp)\n yield\n }\n let b = new Uint32Array(n)\n let ops = 0\n for (let width = RUN; width < n; width *= 2) {\n for (let lo = 0; lo < n; lo += 2 * width) {\n const mid = Math.min(lo + width, n)\n const hi = Math.min(lo + 2 * width, n)\n let i = lo\n let j = mid\n let k = lo\n while (i < mid && j < hi) {\n const x = a[i] as number\n const y = a[j] as number\n if (cmp(y, x) < 0) {\n b[k++] = y\n j++\n } else {\n b[k++] = x\n i++\n }\n if (++ops === CHUNK) {\n ops = 0\n yield\n }\n }\n while (i < mid) b[k++] = a[i++] as number\n while (j < hi) b[k++] = a[j++] as number\n }\n const t = a\n a = b\n b = t\n }\n return a\n}\n\n/**\n * Collation ranks of a string column (§6.4, §9.1): per row, the dense rank of its string among\n * the column's distinct strings in `collator` order (strings the collator finds equal share a\n * rank), `NaN` for a missing value. Sorting by these numbers gives exactly the order of\n * `collator.compare`, with integer keys (counting / radix sort) instead of a collator call per\n * comparison. A time-sliced job; `null` when a present value is not a string.\n */\nexport function* collationRanks(\n values: readonly unknown[],\n collator: Intl.Collator,\n): Generator<undefined, Float64Array | null, undefined> {\n const n = values.length\n const ids = new Map<string, number>()\n const distinct: string[] = []\n /** Per row: its string's position in `distinct`, -1 when missing. */\n const slot = new Int32Array(n)\n for (let r = 0; r < n; r++) {\n const v = values[r]\n if (typeof v === 'string') {\n let id = ids.get(v)\n if (id === undefined) {\n id = distinct.length\n ids.set(v, id)\n distinct.push(v)\n }\n slot[r] = id\n } else if (typeRank(v) === RANK.missing) slot[r] = -1\n else return null\n if (r % CHUNK === CHUNK - 1) yield\n }\n const order = yield* sortPositions(distinct.length, (a, b) =>\n collator.compare(distinct[a] as string, distinct[b] as string),\n )\n const rankOf = new Float64Array(distinct.length)\n let rank = 0\n for (let k = 0; k < order.length; k++) {\n const id = order[k] as number\n const s = distinct[id] as string\n if (k > 0 && collator.compare(distinct[order[k - 1] as number] as string, s) !== 0) rank++\n rankOf[id] = rank\n if (k % CHUNK === CHUNK - 1) yield\n }\n const out = new Float64Array(n)\n for (let r = 0; r < n; r++) {\n const id = slot[r] as number\n out[r] = id < 0 ? Number.NaN : (rankOf[id] as number)\n }\n return out\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA+B;AAG/B,IAAM,QAAQ;AAEd,IAAM,MAAM;AAMZ,UAAU,cACR,GACA,KAC8C;AAC9C,MAAI,IAAI,IAAI,YAAY,CAAC;AACzB,WAAS,IAAI,GAAG,IAAI,GAAG,IAAK,GAAE,CAAC,IAAI;AACnC,WAAS,KAAK,GAAG,KAAK,GAAG,MAAM,KAAK;AAClC,MAAE,SAAS,IAAI,KAAK,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG;AAC9C;AAAA,EACF;AACA,MAAI,IAAI,IAAI,YAAY,CAAC;AACzB,MAAI,MAAM;AACV,WAAS,QAAQ,KAAK,QAAQ,GAAG,SAAS,GAAG;AAC3C,aAAS,KAAK,GAAG,KAAK,GAAG,MAAM,IAAI,OAAO;AACxC,YAAM,MAAM,KAAK,IAAI,KAAK,OAAO,CAAC;AAClC,YAAM,KAAK,KAAK,IAAI,KAAK,IAAI,OAAO,CAAC;AACrC,UAAI,IAAI;AACR,UAAI,IAAI;AACR,UAAI,IAAI;AACR,aAAO,IAAI,OAAO,IAAI,IAAI;AACxB,cAAM,IAAI,EAAE,CAAC;AACb,cAAM,IAAI,EAAE,CAAC;AACb,YAAI,IAAI,GAAG,CAAC,IAAI,GAAG;AACjB,YAAE,GAAG,IAAI;AACT;AAAA,QACF,OAAO;AACL,YAAE,GAAG,IAAI;AACT;AAAA,QACF;AACA,YAAI,EAAE,QAAQ,OAAO;AACnB,gBAAM;AACN;AAAA,QACF;AAAA,MACF;AACA,aAAO,IAAI,IAAK,GAAE,GAAG,IAAI,EAAE,GAAG;AAC9B,aAAO,IAAI,GAAI,GAAE,GAAG,IAAI,EAAE,GAAG;AAAA,IAC/B;AACA,UAAM,IAAI;AACV,QAAI;AACJ,QAAI;AAAA,EACN;AACA,SAAO;AACT;AASO,UAAU,eACf,QACA,UACsD;AACtD,QAAM,IAAI,OAAO;AACjB,QAAM,MAAM,oBAAI,IAAoB;AACpC,QAAM,WAAqB,CAAC;AAE5B,QAAM,OAAO,IAAI,WAAW,CAAC;AAC7B,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,UAAM,IAAI,OAAO,CAAC;AAClB,QAAI,OAAO,MAAM,UAAU;AACzB,UAAI,KAAK,IAAI,IAAI,CAAC;AAClB,UAAI,OAAO,QAAW;AACpB,aAAK,SAAS;AACd,YAAI,IAAI,GAAG,EAAE;AACb,iBAAS,KAAK,CAAC;AAAA,MACjB;AACA,WAAK,CAAC,IAAI;AAAA,IACZ,eAAW,yBAAS,CAAC,MAAM,oBAAK,QAAS,MAAK,CAAC,IAAI;AAAA,QAC9C,QAAO;AACZ,QAAI,IAAI,UAAU,QAAQ,EAAG;AAAA,EAC/B;AACA,QAAM,QAAQ,OAAO;AAAA,IAAc,SAAS;AAAA,IAAQ,CAAC,GAAG,MACtD,SAAS,QAAQ,SAAS,CAAC,GAAa,SAAS,CAAC,CAAW;AAAA,EAC/D;AACA,QAAM,SAAS,IAAI,aAAa,SAAS,MAAM;AAC/C,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,KAAK,MAAM,CAAC;AAClB,UAAM,IAAI,SAAS,EAAE;AACrB,QAAI,IAAI,KAAK,SAAS,QAAQ,SAAS,MAAM,IAAI,CAAC,CAAW,GAAa,CAAC,MAAM,EAAG;AACpF,WAAO,EAAE,IAAI;AACb,QAAI,IAAI,UAAU,QAAQ,EAAG;AAAA,EAC/B;AACA,QAAM,MAAM,IAAI,aAAa,CAAC;AAC9B,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,UAAM,KAAK,KAAK,CAAC;AACjB,QAAI,CAAC,IAAI,KAAK,IAAI,OAAO,MAAO,OAAO,EAAE;AAAA,EAC3C;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../../src/features/sorting/ranks.ts"],"sourcesContent":["import { RANK, typeRank } from './compare'\n\n/** Comparisons / rows between two `yield`s of the jobs below (§9.1). */\nconst CHUNK = 4096\n/**\n * Run length sorted natively before the merge passes (#97, was 512): each doubling saves a merge\n * pass, and one native run of collator calls stays within a slice even under a 4x CPU throttle.\n */\nconst RUN = 1024\n\n/**\n * Sorts `0..n-1` by `cmp` as a time-sliced job (§9.1): native sorts of `run`s, then bottom-up\n * merge passes that `yield` every {@link CHUNK} comparisons. Stable. Also returns `eq`:\n * `eq[k] = 1` when `order[k]` compares equal to `order[k - 1]`, from the comparisons the merges\n * make anyway (#97). Inside a native run it costs a second pass only when that sort saw a 0.\n */\nfunction* sortPositions(\n n: number,\n cmp: (a: number, b: number) => number,\n run: number,\n): Generator<undefined, { order: Uint32Array; eq: Uint8Array }, undefined> {\n let a = new Uint32Array(n)\n let ea = new Uint8Array(n)\n for (let i = 0; i < n; i++) a[i] = i\n for (let lo = 0; lo < n; lo += run) {\n const hi = Math.min(lo + run, n)\n let zero = false\n a.subarray(lo, hi).sort((x, y) => {\n const c = cmp(x, y)\n if (c === 0) zero = true\n return c\n })\n // Neighbours can only be equal if the sort saw a 0 (it must, to order them).\n if (zero)\n for (let k = lo + 1; k < hi; k++) ea[k] = cmp(a[k - 1] as number, a[k] as number) ? 0 : 1\n yield\n }\n let b = new Uint32Array(n)\n let eb = new Uint8Array(n)\n let ops = 0\n for (let width = run; width < n; width *= 2) {\n for (let lo = 0; lo < n; lo += 2 * width) {\n const mid = Math.min(lo + width, n)\n const hi = Math.min(lo + 2 * width, n)\n let i = lo\n let j = mid\n // The run of the last output (-1: none yet) and whether it equals the other run's head.\n let prev = -1\n let same = 0\n for (let k = lo; k < hi; k++) {\n let c = 1\n let src: number\n if (i < mid && j < hi) {\n c = cmp(a[j] as number, a[i] as number)\n src = c < 0 ? 1 : 0\n if (++ops === CHUNK) {\n ops = 0\n yield\n }\n } else src = i < mid ? 0 : 1\n const p = src ? j++ : i++\n b[k] = a[p] as number\n eb[k] = prev === src ? (ea[p] as number) : prev < 0 ? 0 : same\n same = src === 0 && c === 0 ? 1 : 0\n prev = src\n }\n }\n const t = a\n a = b\n b = t\n const e = ea\n ea = eb\n eb = e\n }\n return { order: a, eq: ea }\n}\n\n/**\n * What the next {@link collationRanks} of the same column starts from (#95): the values it ranked,\n * their ranks (the `ranks` it returned, shared) and the distinct strings in rank order (one per\n * rank, shared with the data). No string → id map: a row whose string is unchanged at its position\n * (or at a shifted one) keeps its rank, any other string is found by binary search over `reps`.\n * Never mutated (a job builds a new one), so a cancelled job leaves it usable.\n */\nexport interface RankState {\n readonly values: readonly unknown[]\n readonly ranks: ArrayLike<number>\n readonly reps: readonly string[]\n}\n\nexport interface Ranked {\n readonly ranks: Float64Array\n readonly state: RankState\n}\n\n/**\n * Collation ranks of a string column (§6.4, §9.1): per row, the dense rank of its string among\n * the column's distinct strings in `collator` order (strings the collator finds equal share a\n * rank), `NaN` for a missing value. Sorting by these numbers gives exactly the order of\n * `collator.compare`, with integer keys (counting / radix sort) instead of a collator call per\n * comparison. A time-sliced job; `null` when a present value is not a string.\n *\n * Incremental across data versions (#95): a string ranked in `prev` (the state of an earlier array\n * of the same column) keeps that rank as its sort key. It is read from the same row of `prev` when\n * the string is the same there (or at the row offset of the last match, for inserted / removed\n * rows), else found by binary search over the ranked strings; a new string is placed in the gap\n * below the first greater one. Only new strings are sorted (two in the same gap call the\n * collator): a one-row edit costs O(log d) collator calls (and a sort of numbers), not a cold\n * sort. The new state holds only the strings present now.\n */\nexport function* collationRanks(\n values: readonly unknown[],\n collator: Intl.Collator,\n prev: RankState = { values: [], ranks: [], reps: [] },\n): Generator<undefined, Ranked | null, undefined> {\n const n = values.length\n const { values: was, ranks: wasRank, reps } = prev\n const m = reps.length\n /** Per row: its rank in `prev`, `m + id` for the new string `fresh[id]`, NaN when missing. */\n const out = new Float64Array(n)\n /** Ranks of `prev` still present (the rest are dropped from the new state). */\n const used = new Uint8Array(m)\n // New strings only (a cold job: every distinct string), dropped with the job.\n const freshIds = new Map<string, number>()\n const fresh: string[] = []\n /** Per new string: `g - 0.5`, between ranks g-1 and g of `prev`. */\n const gap: number[] = []\n /** Per rank of `prev`: one row holding it, built on the first lookup that finds one. */\n let rowOf: Int32Array | undefined\n /** Offset to `prev`'s row of the last match: rows inserted / removed above shift it. */\n let shift = 0\n let ops = 0\n for (let r = 0; r < n; r++) {\n const v = values[r]\n if (typeof v === 'string') {\n let k: number | undefined\n const q = r + shift\n // Out of range: `undefined`, never a string.\n if (was[q] === v) k = wasRank[q] as number\n else {\n const id = freshIds.get(v)\n if (id !== undefined) k = m + id\n else {\n let lo = 0\n let hi = m\n while (lo < hi) {\n const mid = (lo + hi) >>> 1\n const c = collator.compare(v, reps[mid] as string)\n ops++\n if (c === 0) {\n k = mid\n break\n }\n if (c < 0) hi = mid\n else lo = mid + 1\n }\n if (k === undefined) {\n k = m + fresh.length\n freshIds.set(v, fresh.length)\n fresh.push(v)\n gap.push(lo - 0.5)\n } else {\n if (!rowOf) {\n rowOf = new Int32Array(m)\n for (let p = 0; p < was.length; p++) {\n const x = wasRank[p] as number\n if (!Number.isNaN(x)) rowOf[x] = p\n }\n }\n shift = (rowOf[k] as number) - r\n }\n }\n }\n if (k < m) used[k] = 1\n out[r] = k\n } else if (typeRank(v) === RANK.missing) out[r] = Number.NaN\n else return null\n if (++ops >= CHUNK) {\n ops = 0\n yield\n }\n }\n // The same strings as `prev` (a new array of equal content, rows moved): the same ranks.\n if (!fresh.length && !used.includes(0)) return { ranks: out, state: { values, ranks: out, reps } }\n\n const f = fresh.length\n const { order, eq } = yield* sortPositions(\n f,\n // By gap; two new strings in the same gap (cold: all of them) go to the collator.\n (a, b) =>\n (gap[a] as number) - (gap[b] as number) ||\n collator.compare(fresh[a] as string, fresh[b] as string),\n // Comparing numbers is far cheaper: longer native runs, fewer merge passes.\n m ? 8 * RUN : RUN,\n )\n // Merge the kept ranks of `prev` and the new strings (each before the rank above its gap). No\n // collator call: one pass, like the rank lookup below.\n const remap = new Float64Array(m + f)\n const kept: string[] = []\n let rank = -1\n let j = 0\n for (let g = 0; g <= m; g++) {\n while (j < f && (gap[order[j] as number] as number) < g) {\n const id = order[j] as number\n if (!eq[j]) kept[++rank] = fresh[id] as string\n remap[m + id] = rank\n j++\n }\n if (g < m && used[g]) {\n kept[++rank] = reps[g] as string\n remap[g] = rank\n }\n }\n for (let r = 0; r < n; r++) {\n const k = out[r] as number\n if (!Number.isNaN(k)) out[r] = remap[k] as number\n }\n return { ranks: out, state: { values, ranks: out, reps: kept } }\n}\n"],"mappings":"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","names":[]}
@@ -1,85 +1,152 @@
1
1
  // src/features/sorting/ranks.ts
2
2
  import { RANK, typeRank } from "./compare.js";
3
3
  var CHUNK = 4096;
4
- var RUN = 512;
5
- function* sortPositions(n, cmp) {
4
+ var RUN = 1024;
5
+ function* sortPositions(n, cmp, run) {
6
6
  let a = new Uint32Array(n);
7
+ let ea = new Uint8Array(n);
7
8
  for (let i = 0; i < n; i++) a[i] = i;
8
- for (let lo = 0; lo < n; lo += RUN) {
9
- a.subarray(lo, Math.min(lo + RUN, n)).sort(cmp);
9
+ for (let lo = 0; lo < n; lo += run) {
10
+ const hi = Math.min(lo + run, n);
11
+ let zero = false;
12
+ a.subarray(lo, hi).sort((x, y) => {
13
+ const c = cmp(x, y);
14
+ if (c === 0) zero = true;
15
+ return c;
16
+ });
17
+ if (zero)
18
+ for (let k = lo + 1; k < hi; k++) ea[k] = cmp(a[k - 1], a[k]) ? 0 : 1;
10
19
  yield;
11
20
  }
12
21
  let b = new Uint32Array(n);
22
+ let eb = new Uint8Array(n);
13
23
  let ops = 0;
14
- for (let width = RUN; width < n; width *= 2) {
24
+ for (let width = run; width < n; width *= 2) {
15
25
  for (let lo = 0; lo < n; lo += 2 * width) {
16
26
  const mid = Math.min(lo + width, n);
17
27
  const hi = Math.min(lo + 2 * width, n);
18
28
  let i = lo;
19
29
  let j = mid;
20
- let k = lo;
21
- while (i < mid && j < hi) {
22
- const x = a[i];
23
- const y = a[j];
24
- if (cmp(y, x) < 0) {
25
- b[k++] = y;
26
- j++;
27
- } else {
28
- b[k++] = x;
29
- i++;
30
- }
31
- if (++ops === CHUNK) {
32
- ops = 0;
33
- yield;
34
- }
30
+ let prev = -1;
31
+ let same = 0;
32
+ for (let k = lo; k < hi; k++) {
33
+ let c = 1;
34
+ let src;
35
+ if (i < mid && j < hi) {
36
+ c = cmp(a[j], a[i]);
37
+ src = c < 0 ? 1 : 0;
38
+ if (++ops === CHUNK) {
39
+ ops = 0;
40
+ yield;
41
+ }
42
+ } else src = i < mid ? 0 : 1;
43
+ const p = src ? j++ : i++;
44
+ b[k] = a[p];
45
+ eb[k] = prev === src ? ea[p] : prev < 0 ? 0 : same;
46
+ same = src === 0 && c === 0 ? 1 : 0;
47
+ prev = src;
35
48
  }
36
- while (i < mid) b[k++] = a[i++];
37
- while (j < hi) b[k++] = a[j++];
38
49
  }
39
50
  const t = a;
40
51
  a = b;
41
52
  b = t;
53
+ const e = ea;
54
+ ea = eb;
55
+ eb = e;
42
56
  }
43
- return a;
57
+ return { order: a, eq: ea };
44
58
  }
45
- function* collationRanks(values, collator) {
59
+ function* collationRanks(values, collator, prev = { values: [], ranks: [], reps: [] }) {
46
60
  const n = values.length;
47
- const ids = /* @__PURE__ */ new Map();
48
- const distinct = [];
49
- const slot = new Int32Array(n);
61
+ const { values: was, ranks: wasRank, reps } = prev;
62
+ const m = reps.length;
63
+ const out = new Float64Array(n);
64
+ const used = new Uint8Array(m);
65
+ const freshIds = /* @__PURE__ */ new Map();
66
+ const fresh = [];
67
+ const gap = [];
68
+ let rowOf;
69
+ let shift = 0;
70
+ let ops = 0;
50
71
  for (let r = 0; r < n; r++) {
51
72
  const v = values[r];
52
73
  if (typeof v === "string") {
53
- let id = ids.get(v);
54
- if (id === void 0) {
55
- id = distinct.length;
56
- ids.set(v, id);
57
- distinct.push(v);
74
+ let k;
75
+ const q = r + shift;
76
+ if (was[q] === v) k = wasRank[q];
77
+ else {
78
+ const id = freshIds.get(v);
79
+ if (id !== void 0) k = m + id;
80
+ else {
81
+ let lo = 0;
82
+ let hi = m;
83
+ while (lo < hi) {
84
+ const mid = lo + hi >>> 1;
85
+ const c = collator.compare(v, reps[mid]);
86
+ ops++;
87
+ if (c === 0) {
88
+ k = mid;
89
+ break;
90
+ }
91
+ if (c < 0) hi = mid;
92
+ else lo = mid + 1;
93
+ }
94
+ if (k === void 0) {
95
+ k = m + fresh.length;
96
+ freshIds.set(v, fresh.length);
97
+ fresh.push(v);
98
+ gap.push(lo - 0.5);
99
+ } else {
100
+ if (!rowOf) {
101
+ rowOf = new Int32Array(m);
102
+ for (let p = 0; p < was.length; p++) {
103
+ const x = wasRank[p];
104
+ if (!Number.isNaN(x)) rowOf[x] = p;
105
+ }
106
+ }
107
+ shift = rowOf[k] - r;
108
+ }
109
+ }
58
110
  }
59
- slot[r] = id;
60
- } else if (typeRank(v) === RANK.missing) slot[r] = -1;
111
+ if (k < m) used[k] = 1;
112
+ out[r] = k;
113
+ } else if (typeRank(v) === RANK.missing) out[r] = Number.NaN;
61
114
  else return null;
62
- if (r % CHUNK === CHUNK - 1) yield;
115
+ if (++ops >= CHUNK) {
116
+ ops = 0;
117
+ yield;
118
+ }
63
119
  }
64
- const order = yield* sortPositions(
65
- distinct.length,
66
- (a, b) => collator.compare(distinct[a], distinct[b])
120
+ if (!fresh.length && !used.includes(0)) return { ranks: out, state: { values, ranks: out, reps } };
121
+ const f = fresh.length;
122
+ const { order, eq } = yield* sortPositions(
123
+ f,
124
+ // By gap; two new strings in the same gap (cold: all of them) go to the collator.
125
+ (a, b) => gap[a] - gap[b] || collator.compare(fresh[a], fresh[b]),
126
+ // Comparing numbers is far cheaper: longer native runs, fewer merge passes.
127
+ m ? 8 * RUN : RUN
67
128
  );
68
- const rankOf = new Float64Array(distinct.length);
69
- let rank = 0;
70
- for (let k = 0; k < order.length; k++) {
71
- const id = order[k];
72
- const s = distinct[id];
73
- if (k > 0 && collator.compare(distinct[order[k - 1]], s) !== 0) rank++;
74
- rankOf[id] = rank;
75
- if (k % CHUNK === CHUNK - 1) yield;
129
+ const remap = new Float64Array(m + f);
130
+ const kept = [];
131
+ let rank = -1;
132
+ let j = 0;
133
+ for (let g = 0; g <= m; g++) {
134
+ while (j < f && gap[order[j]] < g) {
135
+ const id = order[j];
136
+ if (!eq[j]) kept[++rank] = fresh[id];
137
+ remap[m + id] = rank;
138
+ j++;
139
+ }
140
+ if (g < m && used[g]) {
141
+ kept[++rank] = reps[g];
142
+ remap[g] = rank;
143
+ }
76
144
  }
77
- const out = new Float64Array(n);
78
145
  for (let r = 0; r < n; r++) {
79
- const id = slot[r];
80
- out[r] = id < 0 ? Number.NaN : rankOf[id];
146
+ const k = out[r];
147
+ if (!Number.isNaN(k)) out[r] = remap[k];
81
148
  }
82
- return out;
149
+ return { ranks: out, state: { values, ranks: out, reps: kept } };
83
150
  }
84
151
  export {
85
152
  collationRanks
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/sorting/ranks.ts"],"sourcesContent":["import { RANK, typeRank } from './compare'\n\n/** Comparisons / rows between two `yield`s of the jobs below (§9.1). */\nconst CHUNK = 4096\n/** Run length sorted natively before the merge passes. */\nconst RUN = 512\n\n/**\n * Sorts `0..n-1` by `cmp` as a time-sliced job (§9.1): native sorts of short runs, then bottom-up\n * merge passes that `yield` every {@link CHUNK} comparisons. Stable.\n */\nfunction* sortPositions(\n n: number,\n cmp: (a: number, b: number) => number,\n): Generator<undefined, Uint32Array, undefined> {\n let a = new Uint32Array(n)\n for (let i = 0; i < n; i++) a[i] = i\n for (let lo = 0; lo < n; lo += RUN) {\n a.subarray(lo, Math.min(lo + RUN, n)).sort(cmp)\n yield\n }\n let b = new Uint32Array(n)\n let ops = 0\n for (let width = RUN; width < n; width *= 2) {\n for (let lo = 0; lo < n; lo += 2 * width) {\n const mid = Math.min(lo + width, n)\n const hi = Math.min(lo + 2 * width, n)\n let i = lo\n let j = mid\n let k = lo\n while (i < mid && j < hi) {\n const x = a[i] as number\n const y = a[j] as number\n if (cmp(y, x) < 0) {\n b[k++] = y\n j++\n } else {\n b[k++] = x\n i++\n }\n if (++ops === CHUNK) {\n ops = 0\n yield\n }\n }\n while (i < mid) b[k++] = a[i++] as number\n while (j < hi) b[k++] = a[j++] as number\n }\n const t = a\n a = b\n b = t\n }\n return a\n}\n\n/**\n * Collation ranks of a string column (§6.4, §9.1): per row, the dense rank of its string among\n * the column's distinct strings in `collator` order (strings the collator finds equal share a\n * rank), `NaN` for a missing value. Sorting by these numbers gives exactly the order of\n * `collator.compare`, with integer keys (counting / radix sort) instead of a collator call per\n * comparison. A time-sliced job; `null` when a present value is not a string.\n */\nexport function* collationRanks(\n values: readonly unknown[],\n collator: Intl.Collator,\n): Generator<undefined, Float64Array | null, undefined> {\n const n = values.length\n const ids = new Map<string, number>()\n const distinct: string[] = []\n /** Per row: its string's position in `distinct`, -1 when missing. */\n const slot = new Int32Array(n)\n for (let r = 0; r < n; r++) {\n const v = values[r]\n if (typeof v === 'string') {\n let id = ids.get(v)\n if (id === undefined) {\n id = distinct.length\n ids.set(v, id)\n distinct.push(v)\n }\n slot[r] = id\n } else if (typeRank(v) === RANK.missing) slot[r] = -1\n else return null\n if (r % CHUNK === CHUNK - 1) yield\n }\n const order = yield* sortPositions(distinct.length, (a, b) =>\n collator.compare(distinct[a] as string, distinct[b] as string),\n )\n const rankOf = new Float64Array(distinct.length)\n let rank = 0\n for (let k = 0; k < order.length; k++) {\n const id = order[k] as number\n const s = distinct[id] as string\n if (k > 0 && collator.compare(distinct[order[k - 1] as number] as string, s) !== 0) rank++\n rankOf[id] = rank\n if (k % CHUNK === CHUNK - 1) yield\n }\n const out = new Float64Array(n)\n for (let r = 0; r < n; r++) {\n const id = slot[r] as number\n out[r] = id < 0 ? Number.NaN : (rankOf[id] as number)\n }\n return out\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/features/sorting/ranks.ts"],"sourcesContent":["import { RANK, typeRank } from './compare'\n\n/** Comparisons / rows between two `yield`s of the jobs below (§9.1). */\nconst CHUNK = 4096\n/**\n * Run length sorted natively before the merge passes (#97, was 512): each doubling saves a merge\n * pass, and one native run of collator calls stays within a slice even under a 4x CPU throttle.\n */\nconst RUN = 1024\n\n/**\n * Sorts `0..n-1` by `cmp` as a time-sliced job (§9.1): native sorts of `run`s, then bottom-up\n * merge passes that `yield` every {@link CHUNK} comparisons. Stable. Also returns `eq`:\n * `eq[k] = 1` when `order[k]` compares equal to `order[k - 1]`, from the comparisons the merges\n * make anyway (#97). Inside a native run it costs a second pass only when that sort saw a 0.\n */\nfunction* sortPositions(\n n: number,\n cmp: (a: number, b: number) => number,\n run: number,\n): Generator<undefined, { order: Uint32Array; eq: Uint8Array }, undefined> {\n let a = new Uint32Array(n)\n let ea = new Uint8Array(n)\n for (let i = 0; i < n; i++) a[i] = i\n for (let lo = 0; lo < n; lo += run) {\n const hi = Math.min(lo + run, n)\n let zero = false\n a.subarray(lo, hi).sort((x, y) => {\n const c = cmp(x, y)\n if (c === 0) zero = true\n return c\n })\n // Neighbours can only be equal if the sort saw a 0 (it must, to order them).\n if (zero)\n for (let k = lo + 1; k < hi; k++) ea[k] = cmp(a[k - 1] as number, a[k] as number) ? 0 : 1\n yield\n }\n let b = new Uint32Array(n)\n let eb = new Uint8Array(n)\n let ops = 0\n for (let width = run; width < n; width *= 2) {\n for (let lo = 0; lo < n; lo += 2 * width) {\n const mid = Math.min(lo + width, n)\n const hi = Math.min(lo + 2 * width, n)\n let i = lo\n let j = mid\n // The run of the last output (-1: none yet) and whether it equals the other run's head.\n let prev = -1\n let same = 0\n for (let k = lo; k < hi; k++) {\n let c = 1\n let src: number\n if (i < mid && j < hi) {\n c = cmp(a[j] as number, a[i] as number)\n src = c < 0 ? 1 : 0\n if (++ops === CHUNK) {\n ops = 0\n yield\n }\n } else src = i < mid ? 0 : 1\n const p = src ? j++ : i++\n b[k] = a[p] as number\n eb[k] = prev === src ? (ea[p] as number) : prev < 0 ? 0 : same\n same = src === 0 && c === 0 ? 1 : 0\n prev = src\n }\n }\n const t = a\n a = b\n b = t\n const e = ea\n ea = eb\n eb = e\n }\n return { order: a, eq: ea }\n}\n\n/**\n * What the next {@link collationRanks} of the same column starts from (#95): the values it ranked,\n * their ranks (the `ranks` it returned, shared) and the distinct strings in rank order (one per\n * rank, shared with the data). No string → id map: a row whose string is unchanged at its position\n * (or at a shifted one) keeps its rank, any other string is found by binary search over `reps`.\n * Never mutated (a job builds a new one), so a cancelled job leaves it usable.\n */\nexport interface RankState {\n readonly values: readonly unknown[]\n readonly ranks: ArrayLike<number>\n readonly reps: readonly string[]\n}\n\nexport interface Ranked {\n readonly ranks: Float64Array\n readonly state: RankState\n}\n\n/**\n * Collation ranks of a string column (§6.4, §9.1): per row, the dense rank of its string among\n * the column's distinct strings in `collator` order (strings the collator finds equal share a\n * rank), `NaN` for a missing value. Sorting by these numbers gives exactly the order of\n * `collator.compare`, with integer keys (counting / radix sort) instead of a collator call per\n * comparison. A time-sliced job; `null` when a present value is not a string.\n *\n * Incremental across data versions (#95): a string ranked in `prev` (the state of an earlier array\n * of the same column) keeps that rank as its sort key. It is read from the same row of `prev` when\n * the string is the same there (or at the row offset of the last match, for inserted / removed\n * rows), else found by binary search over the ranked strings; a new string is placed in the gap\n * below the first greater one. Only new strings are sorted (two in the same gap call the\n * collator): a one-row edit costs O(log d) collator calls (and a sort of numbers), not a cold\n * sort. The new state holds only the strings present now.\n */\nexport function* collationRanks(\n values: readonly unknown[],\n collator: Intl.Collator,\n prev: RankState = { values: [], ranks: [], reps: [] },\n): Generator<undefined, Ranked | null, undefined> {\n const n = values.length\n const { values: was, ranks: wasRank, reps } = prev\n const m = reps.length\n /** Per row: its rank in `prev`, `m + id` for the new string `fresh[id]`, NaN when missing. */\n const out = new Float64Array(n)\n /** Ranks of `prev` still present (the rest are dropped from the new state). */\n const used = new Uint8Array(m)\n // New strings only (a cold job: every distinct string), dropped with the job.\n const freshIds = new Map<string, number>()\n const fresh: string[] = []\n /** Per new string: `g - 0.5`, between ranks g-1 and g of `prev`. */\n const gap: number[] = []\n /** Per rank of `prev`: one row holding it, built on the first lookup that finds one. */\n let rowOf: Int32Array | undefined\n /** Offset to `prev`'s row of the last match: rows inserted / removed above shift it. */\n let shift = 0\n let ops = 0\n for (let r = 0; r < n; r++) {\n const v = values[r]\n if (typeof v === 'string') {\n let k: number | undefined\n const q = r + shift\n // Out of range: `undefined`, never a string.\n if (was[q] === v) k = wasRank[q] as number\n else {\n const id = freshIds.get(v)\n if (id !== undefined) k = m + id\n else {\n let lo = 0\n let hi = m\n while (lo < hi) {\n const mid = (lo + hi) >>> 1\n const c = collator.compare(v, reps[mid] as string)\n ops++\n if (c === 0) {\n k = mid\n break\n }\n if (c < 0) hi = mid\n else lo = mid + 1\n }\n if (k === undefined) {\n k = m + fresh.length\n freshIds.set(v, fresh.length)\n fresh.push(v)\n gap.push(lo - 0.5)\n } else {\n if (!rowOf) {\n rowOf = new Int32Array(m)\n for (let p = 0; p < was.length; p++) {\n const x = wasRank[p] as number\n if (!Number.isNaN(x)) rowOf[x] = p\n }\n }\n shift = (rowOf[k] as number) - r\n }\n }\n }\n if (k < m) used[k] = 1\n out[r] = k\n } else if (typeRank(v) === RANK.missing) out[r] = Number.NaN\n else return null\n if (++ops >= CHUNK) {\n ops = 0\n yield\n }\n }\n // The same strings as `prev` (a new array of equal content, rows moved): the same ranks.\n if (!fresh.length && !used.includes(0)) return { ranks: out, state: { values, ranks: out, reps } }\n\n const f = fresh.length\n const { order, eq } = yield* sortPositions(\n f,\n // By gap; two new strings in the same gap (cold: all of them) go to the collator.\n (a, b) =>\n (gap[a] as number) - (gap[b] as number) ||\n collator.compare(fresh[a] as string, fresh[b] as string),\n // Comparing numbers is far cheaper: longer native runs, fewer merge passes.\n m ? 8 * RUN : RUN,\n )\n // Merge the kept ranks of `prev` and the new strings (each before the rank above its gap). No\n // collator call: one pass, like the rank lookup below.\n const remap = new Float64Array(m + f)\n const kept: string[] = []\n let rank = -1\n let j = 0\n for (let g = 0; g <= m; g++) {\n while (j < f && (gap[order[j] as number] as number) < g) {\n const id = order[j] as number\n if (!eq[j]) kept[++rank] = fresh[id] as string\n remap[m + id] = rank\n j++\n }\n if (g < m && used[g]) {\n kept[++rank] = reps[g] as string\n remap[g] = rank\n }\n }\n for (let r = 0; r < n; r++) {\n const k = out[r] as number\n if (!Number.isNaN(k)) out[r] = remap[k] as number\n }\n return { ranks: out, state: { values, ranks: out, reps: kept } }\n}\n"],"mappings":"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","names":[]}
@@ -39,6 +39,7 @@ var import_codes2 = require("../../intercept/codes.cjs");
39
39
  var import_columns = require("../../pipeline/columns.cjs");
40
40
  var import_env_codes = require("../../pipeline/env-codes.cjs");
41
41
  var import_indices = require("../../pipeline/indices.cjs");
42
+ var import_release = require("../../pipeline/release.cjs");
42
43
  var import_slice = require("../../pipeline/slice.cjs");
43
44
  var import_mode = require("../../source/mode.cjs");
44
45
  var import_capability = require("../../versioning/capability.cjs");
@@ -162,19 +163,31 @@ var sorting = (opts = {}) => {
162
163
  }
163
164
  return collator;
164
165
  };
165
- const rankKeys = /* @__PURE__ */ new WeakMap();
166
- function ranksFor(ctx, values) {
166
+ let rankKeys = /* @__PURE__ */ new WeakMap();
167
+ let history = /* @__PURE__ */ new Map();
168
+ function keepHistory(by) {
169
+ for (const id of history.keys()) {
170
+ if (!by.some((e) => String(e.id) === id)) history.delete(id);
171
+ }
172
+ }
173
+ function ranksFor(ctx, id, values) {
167
174
  let key = rankKeys.get(values);
168
175
  if (!key) {
169
176
  key = {};
170
177
  rankKeys.set(values, key);
171
178
  }
172
179
  const c = getCollator(ctx.emit);
173
- return (0, import_slice.sliced)(ctx, key, () => (0, import_ranks.collationRanks)(values, c));
180
+ const into = history;
181
+ return (0, import_slice.sliced)(ctx, key, function* () {
182
+ const r = yield* (0, import_ranks.collationRanks)(values, c, into.get(id));
183
+ if (!r) return null;
184
+ into.set(id, r.state);
185
+ return r.ranks;
186
+ });
174
187
  }
175
188
  let shown;
176
- const sortByCache = /* @__PURE__ */ new WeakMap();
177
- const pathCache = /* @__PURE__ */ new WeakMap();
189
+ let sortByCache = /* @__PURE__ */ new WeakMap();
190
+ let pathCache = /* @__PURE__ */ new WeakMap();
178
191
  let exsorted;
179
192
  const sortByFailures = new import_cell_batch.CellFailures();
180
193
  function sortByValues(ctx, col, sortBy) {
@@ -220,7 +233,7 @@ var sorting = (opts = {}) => {
220
233
  byId.set(id, out);
221
234
  return out;
222
235
  }
223
- return (0, import_define.defineFeature)({
236
+ const feature = (0, import_define.defineFeature)({
224
237
  name: "sorting",
225
238
  stage: "sort",
226
239
  mode: opts.mode ?? "client",
@@ -258,6 +271,7 @@ var sorting = (opts = {}) => {
258
271
  },
259
272
  rowModel: (indices, state, ctx) => {
260
273
  const by = entriesOf(state);
274
+ keepHistory(by);
261
275
  if (by.length === 0 || indices.length < 2) {
262
276
  shown = void 0;
263
277
  return indices;
@@ -270,7 +284,7 @@ var sorting = (opts = {}) => {
270
284
  const sortBy = col && fnField(col.def, "sortBy");
271
285
  const values = sortBy ? sortByValues(ctx, col, sortBy) : ctx.getColumnValues(id) ?? pathValues(ctx, id);
272
286
  const sortFn = col && !sortBy ? fnField(col.def, "sortFn") : void 0;
273
- return { col, desc: e.desc === true, values, sortFn, ranked: false };
287
+ return { id, col, desc: e.desc === true, values, sortFn, ranked: false };
274
288
  });
275
289
  if (process.env.NODE_ENV !== "production") {
276
290
  for (const e of entries) {
@@ -311,7 +325,7 @@ var sorting = (opts = {}) => {
311
325
  let computing = false;
312
326
  for (const e of entries) {
313
327
  if (e.sortFn || e.values instanceof Float64Array) continue;
314
- const ranks = ranksFor(ctx, e.values);
328
+ const ranks = ranksFor(ctx, e.id, e.values);
315
329
  if (ranks === void 0) computing = true;
316
330
  else if (ranks) {
317
331
  e.values = ranks;
@@ -465,6 +479,16 @@ var sorting = (opts = {}) => {
465
479
  }
466
480
  }
467
481
  });
482
+ const internal = feature;
483
+ internal[import_release.RELEASE] = () => {
484
+ rankKeys = /* @__PURE__ */ new WeakMap();
485
+ history = /* @__PURE__ */ new Map();
486
+ shown = void 0;
487
+ sortByCache = /* @__PURE__ */ new WeakMap();
488
+ pathCache = /* @__PURE__ */ new WeakMap();
489
+ };
490
+ if (process.env.NODE_ENV !== "production") internal[import_release.PROBE] = () => ({ rankKeys, history, shown, sortByCache, pathCache });
491
+ return feature;
468
492
  };
469
493
  // Annotate the CommonJS export names for ESM import in node:
470
494
  0 && (module.exports = {