@datakable/core 1.0.0 → 1.0.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 (71) hide show
  1. package/dist/{codes-BKdS9EiO.d.cts → codes-OsjAR3Wz.d.cts} +16 -4
  2. package/dist/{codes-BKdS9EiO.d.ts → codes-OsjAR3Wz.d.ts} +16 -4
  3. package/dist/column-filters.d.cts +2 -2
  4. package/dist/column-filters.d.ts +2 -2
  5. package/dist/diagnostics/build.cjs +6 -3
  6. package/dist/diagnostics/build.cjs.map +1 -1
  7. package/dist/diagnostics/build.js +4 -2
  8. package/dist/diagnostics/build.js.map +1 -1
  9. package/dist/diagnostics/hub.cjs +1 -1
  10. package/dist/diagnostics/hub.cjs.map +1 -1
  11. package/dist/diagnostics/hub.js +2 -2
  12. package/dist/diagnostics/hub.js.map +1 -1
  13. package/dist/{explain-DcUZfFh5.d.ts → explain-Dn99IQt0.d.ts} +1 -1
  14. package/dist/{explain-aagaYHcy.d.cts → explain-vkD2cewE.d.cts} +1 -1
  15. package/dist/feature/define.cjs +3 -1
  16. package/dist/feature/define.cjs.map +1 -1
  17. package/dist/feature/define.js +3 -1
  18. package/dist/feature/define.js.map +1 -1
  19. package/dist/feature/runtime.cjs.map +1 -1
  20. package/dist/feature/runtime.js.map +1 -1
  21. package/dist/features/pagination/codes.cjs +5 -3
  22. package/dist/features/pagination/codes.cjs.map +1 -1
  23. package/dist/features/pagination/codes.js +5 -3
  24. package/dist/features/pagination/codes.js.map +1 -1
  25. package/dist/features/pagination/pagination.cjs +3 -1
  26. package/dist/features/pagination/pagination.cjs.map +1 -1
  27. package/dist/features/pagination/pagination.js +3 -1
  28. package/dist/features/pagination/pagination.js.map +1 -1
  29. package/dist/features/search/search-index.cjs +38 -34
  30. package/dist/features/search/search-index.cjs.map +1 -1
  31. package/dist/features/search/search-index.js +39 -35
  32. package/dist/features/search/search-index.js.map +1 -1
  33. package/dist/features/search/search.cjs +42 -16
  34. package/dist/features/search/search.cjs.map +1 -1
  35. package/dist/features/search/search.js +42 -16
  36. package/dist/features/search/search.js.map +1 -1
  37. package/dist/features/sorting/sorting.cjs +3 -3
  38. package/dist/features/sorting/sorting.cjs.map +1 -1
  39. package/dist/features/sorting/sorting.js +3 -3
  40. package/dist/features/sorting/sorting.js.map +1 -1
  41. package/dist/index.d.cts +9 -7
  42. package/dist/index.d.ts +9 -7
  43. package/dist/intercept/cell-batch.cjs +3 -0
  44. package/dist/intercept/cell-batch.cjs.map +1 -1
  45. package/dist/intercept/cell-batch.js +3 -0
  46. package/dist/intercept/cell-batch.js.map +1 -1
  47. package/dist/pagination.d.cts +2 -2
  48. package/dist/pagination.d.ts +2 -2
  49. package/dist/pipeline/row.cjs +8 -7
  50. package/dist/pipeline/row.cjs.map +1 -1
  51. package/dist/pipeline/row.js +8 -7
  52. package/dist/pipeline/row.js.map +1 -1
  53. package/dist/schema.d.cts +2 -2
  54. package/dist/schema.d.ts +2 -2
  55. package/dist/{search-async-entry-BmQYT120.d.ts → search-async-entry-DGSw2pDL.d.ts} +2 -2
  56. package/dist/{search-async-entry-C3mXxLP8.d.cts → search-async-entry-v5JdEk5Y.d.cts} +2 -2
  57. package/dist/search-async.d.cts +3 -3
  58. package/dist/search-async.d.ts +3 -3
  59. package/dist/search.d.cts +4 -4
  60. package/dist/search.d.ts +4 -4
  61. package/dist/server.d.cts +3 -3
  62. package/dist/server.d.ts +3 -3
  63. package/dist/sorting.d.cts +6 -3
  64. package/dist/sorting.d.ts +6 -3
  65. package/dist/validity.d.cts +2 -2
  66. package/dist/validity.d.ts +2 -2
  67. package/dist/version.cjs +1 -1
  68. package/dist/version.cjs.map +1 -1
  69. package/dist/version.js +1 -1
  70. package/dist/version.js.map +1 -1
  71. package/package.json +1 -1
@@ -1,17 +1,19 @@
1
1
  // src/features/search/search-index.ts
2
- import { ExSearch } from "ex-search";
2
+ import { ExSearch, Scorer } from "ex-search";
3
3
  import { resolveColumns } from "../../pipeline/columns.js";
4
4
  import { arrayIndex } from "../../pipeline/row.js";
5
5
  import { detectCapability, hasCapability } from "../../versioning/capability.js";
6
6
  var CHUNK = 4096;
7
- var hasExSearch = () => typeof ExSearch === "function" && typeof ExSearch.prototype.search === "function" && typeof ExSearch.prototype.setData === "function";
7
+ var hasExSearch = () => [ExSearch.prototype.search, ExSearch.prototype.setData, Scorer.fuzzy].every(
8
+ (f) => typeof f === "function"
9
+ );
8
10
  function probeExSearch(emit) {
9
11
  return process.env.NODE_ENV !== "production" ? detectCapability(
10
12
  hasExSearch,
11
13
  {
12
14
  feature: "search",
13
15
  dependency: "ex-search",
14
- capability: "ExSearch",
16
+ capability: "ExSearch, Scorer.fuzzy",
15
17
  supported: "^1.1.1",
16
18
  fallback: "case-insensitive exact / prefix / substring matching (no fuzzy matching)"
17
19
  },
@@ -47,12 +49,9 @@ function* buildIndex(ctx, config, useExSearch) {
47
49
  yield;
48
50
  }
49
51
  if (keys.length === 0) return null;
50
- const fold = !config.caseSensitive;
51
- const at = (k, r) => {
52
- const v = reads[k](r);
53
- return fold ? v.toLowerCase() : v;
54
- };
55
- return useExSearch ? exSearchIndex(ctx.data.length, keys, at, config) : containsIndex(keys, at, config);
52
+ const fold = (v) => config.caseSensitive ? v : v.toLowerCase();
53
+ const at = (k, r) => fold(reads[k](r));
54
+ return useExSearch ? exSearchIndex(ctx.data.length, keys, at, fold, config) : containsIndex(keys, at, fold, config);
56
55
  }
57
56
  function* chunked(rows, matchChunk) {
58
57
  if (rows.length <= CHUNK) return rankHits(matchChunk(rows));
@@ -74,12 +73,12 @@ function projection(size, keys, at) {
74
73
  return o;
75
74
  };
76
75
  const items = new Proxy(target, {
77
- get(t, p, receiver) {
76
+ get(t, p) {
78
77
  if (p === "length") return length();
79
78
  const j = arrayIndex(p, length());
80
- return j >= 0 ? item(j) : Reflect.get(t, p, receiver);
79
+ return j >= 0 ? item(j) : t[p];
81
80
  },
82
- has: (t, p) => arrayIndex(p, length()) >= 0 || Reflect.has(t, p)
81
+ has: (t, p) => arrayIndex(p, length()) >= 0 || p in t
83
82
  });
84
83
  return {
85
84
  items,
@@ -89,22 +88,39 @@ function projection(size, keys, at) {
89
88
  }
90
89
  };
91
90
  }
92
- function exSearchIndex(size, keys, at, config) {
91
+ function exSearchIndex(size, keys, at, fold, config) {
93
92
  const engine = new ExSearch({
94
- keys: keys.map((k) => ({ name: k.name, weight: k.weight })),
95
- threshold: config.threshold,
96
- fuzzyMaxDistance: config.fuzzyMaxDistance,
93
+ keys,
94
+ ...config,
97
95
  caseSensitive: true,
98
96
  // DataKable re-ranks with exact scores and an index tie-break; tim keeps ex-search's own pass
99
97
  // stable and avoids radix's score quantisation.
100
98
  sortAlgorithm: "tim"
101
99
  });
102
- const view = projection(size, keys, at);
100
+ const max = config.fuzzyMaxDistance;
101
+ let q = "";
102
+ const near = (k, r) => {
103
+ const v = at(k, r);
104
+ if (v.includes(q) || /\s/.test(q)) return v;
105
+ let best = "";
106
+ let top = 0;
107
+ const words = v.split(/\s+/);
108
+ for (const w of words[1] ? [v, ...words] : words) {
109
+ const s = Math.abs(w.length - q.length) > max ? 0 : Scorer.fuzzy(w, q, max, true);
110
+ if (s > top) {
111
+ best = w;
112
+ top = s;
113
+ }
114
+ }
115
+ return best;
116
+ };
117
+ const view = projection(size, keys, near);
103
118
  engine.setData(view.items);
104
119
  return {
105
120
  match(rows, query) {
106
- const q = config.caseSensitive ? query : query.toLowerCase();
121
+ const folded = fold(query);
107
122
  return chunked(rows, (chunk) => {
123
+ q = folded;
108
124
  view.show(chunk);
109
125
  try {
110
126
  return engine.search(q);
@@ -115,10 +131,10 @@ function exSearchIndex(size, keys, at, config) {
115
131
  }
116
132
  };
117
133
  }
118
- function containsIndex(keys, at, config) {
134
+ function containsIndex(keys, at, fold, config) {
119
135
  return {
120
136
  match(rows, query) {
121
- const q = config.caseSensitive ? query : query.toLowerCase();
137
+ const q = fold(query);
122
138
  return chunked(rows, (chunk) => {
123
139
  const hits = [];
124
140
  for (let j = 0; j < chunk.length; j++) {
@@ -138,22 +154,10 @@ function containsIndex(keys, at, config) {
138
154
  };
139
155
  }
140
156
  function rankHits(hits) {
141
- const n = hits.length;
142
- const index = new Uint32Array(n);
143
- const score = new Float64Array(n);
144
- const order = new Uint32Array(n);
145
- for (let k = 0; k < n; k++) {
146
- const h = hits[k];
147
- index[k] = h.i;
148
- score[k] = h.score;
149
- order[k] = k;
150
- }
151
- order.sort(
152
- (a, b) => score[b] - score[a] || index[a] - index[b]
157
+ return Uint32Array.from(
158
+ hits.sort((a, b) => b.score - a.score || a.i - b.i),
159
+ (h) => h.i
153
160
  );
154
- const out = new Uint32Array(n);
155
- for (let k = 0; k < n; k++) out[k] = index[order[k]];
156
- return out;
157
161
  }
158
162
  export {
159
163
  buildIndex,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/search/search-index.ts"],"sourcesContent":["import { ExSearch } from 'ex-search'\nimport type { Emit } from '../../diagnostics/types'\nimport type { RowModelContext } from '../../feature/types'\nimport { resolveColumns } from '../../pipeline/columns'\nimport { arrayIndex } from '../../pipeline/row'\nimport { detectCapability, hasCapability } from '../../versioning/capability'\n\n/** Matcher settings (the client options of `search()`, defaults applied). */\nexport interface MatchConfig {\n readonly threshold: number\n readonly fuzzyMaxDistance: number\n readonly caseSensitive: boolean\n}\n\n/**\n * One projected row as ex-search reads it: `i` is the row index (results map back by it, never by\n * identity: ex-search returns copies), `c0`, `c1`, … the formatted values of the searchable\n * columns. Flat keys also sidestep ex-search's top-level-key limitation for nested paths. Items\n * are built per chunk while matching and dropped after it (§9.3 memory): none is kept.\n */\nexport type Item = Record<string, string | number> & { readonly i: number }\n\n/** A match: the row index and its relevance (0–1, weight applied). */\nexport interface Hit {\n readonly i: number\n readonly score: number\n}\n\n/** The matcher of one (data, columns) pair: references to column values, no per-row copy. */\nexport interface SearchIndex {\n /**\n * A time-sliced job (§9.1) matching `query` (trimmed, non-blank) against the rows `rows` (row\n * indices), in chunks: the hits in relevance order ({@link rankHits}).\n */\n match(rows: Uint32Array, query: string): Generator<undefined, Uint32Array, undefined>\n}\n\n/** Rows per chunk between two `yield`s of the matching job. */\nconst CHUNK = 4096\n\ninterface Key {\n readonly name: string\n readonly weight: number\n}\n\n/** The formatted value of row `r` of one searchable column. */\ntype Read = (r: number) => string\n\nconst hasExSearch = () =>\n typeof ExSearch === 'function' &&\n typeof ExSearch.prototype.search === 'function' &&\n typeof ExSearch.prototype.setData === 'function'\n\n/** Feature-detects ex-search once (§8.5); `false` → the contains fallback. */\nexport function probeExSearch(emit: Emit): boolean {\n // DK_DEP_INCOMPATIBLE is a development check (§8.5); the fallback is not.\n return (process.env.NODE_ENV !== \"production\")\n ? detectCapability(\n hasExSearch,\n {\n feature: 'search',\n dependency: 'ex-search',\n capability: 'ExSearch',\n supported: '^1.1.1',\n fallback: 'case-insensitive exact / prefix / substring matching (no fuzzy matching)',\n },\n emit,\n )\n : hasCapability(hasExSearch)\n}\n\n/**\n * A numeric column's formatted value without the cached strings: `String(v)` is what the default\n * formatter shows for a number. Only when no value is `NaN` — a `NaN` cell is `null` / `undefined`\n * (shown as `''`) or a real `NaN` (shown as `'NaN'`), which only the formatted cache tells apart.\n */\nfunction numberReader(values: Float64Array): Read | undefined {\n for (let r = 0; r < values.length; r++) if (Number.isNaN(values[r])) return undefined\n return (r) => String(values[r])\n}\n\n/**\n * Builds the matcher over the formatted values of the searchable columns (what the user sees), once\n * per data + columns, as a time-sliced job (§9.1): one column read per slice. Searchable columns are\n * those with a `search` weight > 0; when no column declares `search`, every column is searchable at\n * weight 1. `null` when nothing is searchable. The values are read from the column cache, never\n * copied (§9.3 memory): a numeric column without `format` is stringified while matching, and the\n * case is folded per chunk while matching.\n */\nexport function* buildIndex(\n ctx: RowModelContext,\n config: MatchConfig,\n useExSearch: boolean,\n): Generator<undefined, SearchIndex | null, undefined> {\n const cols = resolveColumns(ctx.columns)\n const declared = cols.list.some((c) => (c.def as { search?: unknown }).search !== undefined)\n const keys: Key[] = []\n const reads: Read[] = []\n for (const col of cols.list) {\n if (cols.byId.get(col.id) !== col) continue\n const w = (col.def as { search?: unknown }).search\n const weight = declared ? (typeof w === 'number' && w > 0 ? w : 0) : 1\n if (weight === 0) continue\n let read: Read | undefined\n if (!col.format) {\n const values = ctx.getColumnValues(col.id)\n if (values instanceof Float64Array) read = numberReader(values)\n }\n if (!read) {\n const formatted = ctx.getFormattedValues(col.id)\n if (!formatted) continue\n read = (r) => formatted[r] ?? ''\n }\n keys.push({ name: `c${keys.length}`, weight })\n reads.push(read)\n yield\n }\n if (keys.length === 0) return null\n const fold = !config.caseSensitive\n const at = (k: number, r: number) => {\n const v = (reads[k] as Read)(r)\n return fold ? v.toLowerCase() : v\n }\n return useExSearch\n ? exSearchIndex(ctx.data.length, keys, at, config)\n : containsIndex(keys, at, config)\n}\n\n/** Matches `rows` chunk by chunk (`score` decides alone, so chunks match like the whole). */\nfunction* chunked(\n rows: Uint32Array,\n matchChunk: (chunk: Uint32Array) => readonly Hit[],\n): Generator<undefined, Uint32Array, undefined> {\n if (rows.length <= CHUNK) return rankHits(matchChunk(rows))\n const hits: Hit[] = []\n for (let k = 0; k < rows.length; k += CHUNK) {\n for (const h of matchChunk(rows.subarray(k, k + CHUNK))) hits.push(h)\n yield\n }\n return rankHits(hits)\n}\n\n/**\n * The projection as ex-search reads it: an array (iteration, `length`, index reads) of `{ i, c0, … }`\n * items, each built when read and not kept. It shows every row of `data` (`size`), or one chunk of\n * rows while that chunk is matched; ex-search holds this one view (`setData` once).\n */\nfunction projection(size: number, keys: readonly Key[], at: (k: number, r: number) => string) {\n let rows: Uint32Array | undefined\n // Never written, and its own `length` stays 0: `length` is read through the trap below (setting\n // a large `length` would allocate a backing store).\n const target: Item[] = []\n const length = () => (rows ? rows.length : size)\n const item = (j: number): Item => {\n const r = rows ? (rows[j] as number) : j\n const o = { i: r } as Record<string, string | number>\n for (let k = 0; k < keys.length; k++) o[(keys[k] as Key).name] = at(k, r)\n return o as Item\n }\n const items = new Proxy(target, {\n get(t, p, receiver) {\n if (p === 'length') return length()\n const j = arrayIndex(p, length())\n return j >= 0 ? item(j) : Reflect.get(t, p, receiver)\n },\n has: (t, p) => arrayIndex(p, length()) >= 0 || Reflect.has(t, p),\n })\n return {\n items,\n /** Shows `chunk` (row indices), or every row again (`undefined`). */\n show(chunk: Uint32Array | undefined) {\n rows = chunk\n },\n }\n}\n\nfunction exSearchIndex(\n size: number,\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n config: MatchConfig,\n): SearchIndex {\n // Values are folded per chunk when not case-sensitive (buildIndex), and so is the query below.\n const engine = new ExSearch<Item>({\n keys: keys.map((k) => ({ name: k.name, weight: k.weight })),\n threshold: config.threshold,\n fuzzyMaxDistance: config.fuzzyMaxDistance,\n caseSensitive: true,\n // DataKable re-ranks with exact scores and an index tie-break; tim keeps ex-search's own pass\n // stable and avoids radix's score quantisation.\n sortAlgorithm: 'tim',\n })\n const view = projection(size, keys, at)\n engine.setData(view.items)\n return {\n match(rows, query) {\n const q = config.caseSensitive ? query : query.toLowerCase()\n return chunked(rows, (chunk) => {\n view.show(chunk)\n try {\n // Results are copies of the items (`Omit<Item, …>` drops `i` from the type, not the value).\n return engine.search(q) as unknown as readonly Hit[]\n } finally {\n view.show(undefined)\n }\n })\n },\n }\n}\n\n/** The fallback when ex-search is missing or incompatible: exact 1 · prefix 0.8 · substring 0.65. */\nfunction containsIndex(\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n config: MatchConfig,\n): SearchIndex {\n return {\n match(rows, query) {\n // Values are folded per chunk (buildIndex).\n const q = config.caseSensitive ? query : query.toLowerCase()\n return chunked(rows, (chunk) => {\n const hits: Hit[] = []\n for (let j = 0; j < chunk.length; j++) {\n const r = chunk[j] as number\n let best = 0\n for (let k = 0; k < keys.length; k++) {\n const v = at(k, r)\n const raw = v === q ? 1 : v.startsWith(q) ? 0.8 : v.includes(q) ? 0.65 : 0\n const w = (keys[k] as Key).weight\n if (raw * w > best) best = raw * w\n }\n if (best > 0 && best >= config.threshold) hits.push({ i: r, score: best })\n }\n return hits\n })\n },\n }\n}\n\n/**\n * Hits in relevance order (§6.3): score descending, ties by original row index. Enforced here so\n * the order never depends on the matcher's own ranking.\n */\nexport function rankHits(hits: readonly Hit[]): Uint32Array {\n const n = hits.length\n const index = new Uint32Array(n)\n const score = new Float64Array(n)\n const order = new Uint32Array(n)\n for (let k = 0; k < n; k++) {\n const h = hits[k] as Hit\n index[k] = h.i\n score[k] = h.score\n order[k] = k\n }\n order.sort(\n (a, b) =>\n (score[b] as number) - (score[a] as number) || (index[a] as number) - (index[b] as number),\n )\n const out = new Uint32Array(n)\n for (let k = 0; k < n; k++) out[k] = index[order[k] as number] as number\n return out\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/features/search/search-index.ts"],"sourcesContent":["import { ExSearch, Scorer } from 'ex-search'\nimport type { Emit } from '../../diagnostics/types'\nimport type { RowModelContext } from '../../feature/types'\nimport { resolveColumns } from '../../pipeline/columns'\nimport { arrayIndex } from '../../pipeline/row'\nimport { detectCapability, hasCapability } from '../../versioning/capability'\n\n/** Matcher settings (the client options of `search()`, defaults applied). */\nexport interface MatchConfig {\n readonly threshold: number\n readonly fuzzyMaxDistance: number\n readonly caseSensitive: boolean\n}\n\n/**\n * One projected row as ex-search reads it: `i` is the row index (results map back by it, never by\n * identity: ex-search returns copies), `c0`, `c1`, … the formatted values of the searchable\n * columns. Flat keys also sidestep ex-search's top-level-key limitation for nested paths. Items\n * are built per chunk while matching and dropped after it (§9.3 memory): none is kept.\n */\nexport type Item = Record<string, string | number> & { readonly i: number }\n\n/** A match: the row index and its relevance (0–1, weight applied). */\nexport interface Hit {\n readonly i: number\n readonly score: number\n}\n\n/** The matcher of one (data, columns) pair: references to column values, no per-row copy. */\nexport interface SearchIndex {\n /**\n * A time-sliced job (§9.1) matching `query` (trimmed, non-blank) against the rows `rows` (row\n * indices), in chunks: the hits in relevance order ({@link rankHits}).\n */\n match(rows: Uint32Array, query: string): Generator<undefined, Uint32Array, undefined>\n}\n\n/** Rows per chunk between two `yield`s of the matching job. */\nconst CHUNK = 4096\n\ninterface Key {\n readonly name: string\n readonly weight: number\n}\n\n/** The formatted value of row `r` of one searchable column. */\ntype Read = (r: number) => string\n\n// A throw (no `ExSearch`) counts as missing (hasCapability).\nconst hasExSearch = () =>\n [ExSearch.prototype.search, ExSearch.prototype.setData, Scorer.fuzzy].every(\n (f) => typeof f === 'function',\n )\n\n/** Feature-detects ex-search once (§8.5); `false` → the contains fallback. */\nexport function probeExSearch(emit: Emit): boolean {\n // DK_DEP_INCOMPATIBLE is a development check (§8.5); the fallback is not.\n return (process.env.NODE_ENV !== \"production\")\n ? detectCapability(\n hasExSearch,\n {\n feature: 'search',\n dependency: 'ex-search',\n capability: 'ExSearch, Scorer.fuzzy',\n supported: '^1.1.1',\n fallback: 'case-insensitive exact / prefix / substring matching (no fuzzy matching)',\n },\n emit,\n )\n : hasCapability(hasExSearch)\n}\n\n/**\n * A numeric column's formatted value without the cached strings: `String(v)` is what the default\n * formatter shows for a number. Only when no value is `NaN` — a `NaN` cell is `null` / `undefined`\n * (shown as `''`) or a real `NaN` (shown as `'NaN'`), which only the formatted cache tells apart.\n */\nfunction numberReader(values: Float64Array): Read | undefined {\n for (let r = 0; r < values.length; r++) if (Number.isNaN(values[r])) return undefined\n return (r) => String(values[r])\n}\n\n/**\n * Builds the matcher over the formatted values of the searchable columns (what the user sees), once\n * per data + columns, as a time-sliced job (§9.1): one column read per slice. Searchable columns are\n * those with a `search` weight > 0; when no column declares `search`, every column is searchable at\n * weight 1. `null` when nothing is searchable. The values are read from the column cache, never\n * copied (§9.3 memory): a numeric column without `format` is stringified while matching, and the\n * case is folded per chunk while matching.\n */\nexport function* buildIndex(\n ctx: RowModelContext,\n config: MatchConfig,\n useExSearch: boolean,\n): Generator<undefined, SearchIndex | null, undefined> {\n const cols = resolveColumns(ctx.columns)\n const declared = cols.list.some((c) => (c.def as { search?: unknown }).search !== undefined)\n const keys: Key[] = []\n const reads: Read[] = []\n for (const col of cols.list) {\n if (cols.byId.get(col.id) !== col) continue\n const w = (col.def as { search?: unknown }).search\n const weight = declared ? (typeof w === 'number' && w > 0 ? w : 0) : 1\n if (weight === 0) continue\n let read: Read | undefined\n if (!col.format) {\n const values = ctx.getColumnValues(col.id)\n if (values instanceof Float64Array) read = numberReader(values)\n }\n if (!read) {\n const formatted = ctx.getFormattedValues(col.id)\n if (!formatted) continue\n read = (r) => formatted[r] ?? ''\n }\n keys.push({ name: `c${keys.length}`, weight })\n reads.push(read)\n yield\n }\n if (keys.length === 0) return null\n // Values are folded per chunk while matching, and so is the query.\n const fold = (v: string) => (config.caseSensitive ? v : v.toLowerCase())\n const at = (k: number, r: number) => fold((reads[k] as Read)(r))\n return useExSearch\n ? exSearchIndex(ctx.data.length, keys, at, fold, config)\n : containsIndex(keys, at, fold, config)\n}\n\n/** Matches `rows` chunk by chunk (`score` decides alone, so chunks match like the whole). */\nfunction* chunked(\n rows: Uint32Array,\n matchChunk: (chunk: Uint32Array) => Hit[],\n): Generator<undefined, Uint32Array, undefined> {\n if (rows.length <= CHUNK) return rankHits(matchChunk(rows))\n const hits: Hit[] = []\n for (let k = 0; k < rows.length; k += CHUNK) {\n for (const h of matchChunk(rows.subarray(k, k + CHUNK))) hits.push(h)\n yield\n }\n return rankHits(hits)\n}\n\n/**\n * The projection as ex-search reads it: an array (iteration, `length`, index reads) of `{ i, c0, … }`\n * items, each built when read and not kept. It shows every row of `data` (`size`), or one chunk of\n * rows while that chunk is matched; ex-search holds this one view (`setData` once).\n */\nfunction projection(size: number, keys: readonly Key[], at: (k: number, r: number) => string) {\n let rows: Uint32Array | undefined\n // Never written, and its own `length` stays 0: `length` is read through the trap below (setting\n // a large `length` would allocate a backing store).\n const target: Item[] = []\n const length = () => (rows ? rows.length : size)\n const item = (j: number): Item => {\n const r = rows ? (rows[j] as number) : j\n const o = { i: r } as Record<string, string | number>\n for (let k = 0; k < keys.length; k++) o[(keys[k] as Key).name] = at(k, r)\n return o as Item\n }\n const items = new Proxy(target, {\n get(t, p) {\n if (p === 'length') return length()\n const j = arrayIndex(p, length())\n return j >= 0 ? item(j) : t[p as keyof Item[]]\n },\n has: (t, p) => arrayIndex(p, length()) >= 0 || p in t,\n })\n return {\n items,\n /** Shows `chunk` (row indices), or every row again (`undefined`). */\n show(chunk: Uint32Array | undefined) {\n rows = chunk\n },\n }\n}\n\nfunction exSearchIndex(\n size: number,\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n fold: (v: string) => string,\n config: MatchConfig,\n): SearchIndex {\n // Values and query are folded already (buildIndex).\n const engine = new ExSearch<Item>({\n keys: keys as Key[],\n ...config,\n caseSensitive: true,\n // DataKable re-ranks with exact scores and an index tie-break; tim keeps ex-search's own pass\n // stable and avoids radix's score quantisation.\n sortAlgorithm: 'tim',\n })\n // ex-search measures the edit distance to the whole value, so a typo of one word of a multi-word\n // cell never matched (#10). For a one-word query, a value without it is shown as the best fuzzy\n // match of itself and its words, or as '' (scores 0, as the value would). A value with the query\n // stays whole for the exact / prefix / substring scores. Only strings within `max` of the query's\n // length are measured (an edit distance is at least the length difference).\n const max = config.fuzzyMaxDistance\n let q = ''\n const near = (k: number, r: number) => {\n const v = at(k, r)\n if (v.includes(q) || /\\s/.test(q)) return v\n let best = ''\n let top = 0\n const words = v.split(/\\s+/)\n for (const w of words[1] ? [v, ...words] : words) {\n const s = Math.abs(w.length - q.length) > max ? 0 : Scorer.fuzzy(w, q, max, true)\n if (s > top) {\n best = w\n top = s\n }\n }\n return best\n }\n const view = projection(size, keys, near)\n engine.setData(view.items)\n return {\n match(rows, query) {\n const folded = fold(query)\n return chunked(rows, (chunk) => {\n // Set per chunk, like the view: another job may have matched in between.\n q = folded\n view.show(chunk)\n try {\n // Results are copies of the items (`Omit<Item, …>` drops `i` from the type, not the value).\n return engine.search(q) as unknown as Hit[]\n } finally {\n view.show(undefined)\n }\n })\n },\n }\n}\n\n/** The fallback when ex-search is missing or incompatible: exact 1 · prefix 0.8 · substring 0.65. */\nfunction containsIndex(\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n fold: (v: string) => string,\n config: MatchConfig,\n): SearchIndex {\n return {\n match(rows, query) {\n const q = fold(query)\n return chunked(rows, (chunk) => {\n const hits: Hit[] = []\n for (let j = 0; j < chunk.length; j++) {\n const r = chunk[j] as number\n let best = 0\n for (let k = 0; k < keys.length; k++) {\n const v = at(k, r)\n const raw = v === q ? 1 : v.startsWith(q) ? 0.8 : v.includes(q) ? 0.65 : 0\n const w = (keys[k] as Key).weight\n if (raw * w > best) best = raw * w\n }\n if (best > 0 && best >= config.threshold) hits.push({ i: r, score: best })\n }\n return hits\n })\n },\n }\n}\n\n/**\n * Hits in relevance order (§6.3): score descending, ties by original row index. Enforced here so\n * the order never depends on the matcher's own ranking. Sorts `hits` (a fresh array) in place.\n */\nexport function rankHits(hits: Hit[]): Uint32Array {\n return Uint32Array.from(\n hits.sort((a, b) => b.score - a.score || a.i - b.i),\n (h) => h.i,\n )\n}\n"],"mappings":"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","names":[]}
@@ -90,7 +90,7 @@ var searchOptions = {
90
90
  }
91
91
  };
92
92
  var settledSuperseded = () => Promise.resolve(import_search_fn_brand.SUPERSEDED);
93
- var toText = (q) => typeof q === "string" ? q : q == null ? "" : String(q);
93
+ var toText = (q) => q == null ? "" : String(q);
94
94
  var search = (opts = {}) => {
95
95
  const config = {
96
96
  threshold: opts.threshold ?? 0.3,
@@ -122,6 +122,8 @@ var search = (opts = {}) => {
122
122
  });
123
123
  }
124
124
  const instances = /* @__PURE__ */ new WeakMap();
125
+ const ran = /* @__PURE__ */ new WeakMap();
126
+ let waited = false;
125
127
  function indexFor(ctx) {
126
128
  let byColumns = indexes.get(ctx.data);
127
129
  if (!byColumns) {
@@ -129,7 +131,7 @@ var search = (opts = {}) => {
129
131
  indexes.set(ctx.data, byColumns);
130
132
  }
131
133
  let key = byColumns.get(ctx.columns);
132
- if (key === void 0) {
134
+ if (!key) {
133
135
  key = {};
134
136
  byColumns.set(ctx.columns, key);
135
137
  }
@@ -148,7 +150,10 @@ var search = (opts = {}) => {
148
150
  }
149
151
  return (0, import_slice.sliced)(ctx, matching2.key, () => index.match(rows, query));
150
152
  }
151
- const standIn = (indices) => shown && shown.input === indices ? shown.output : indices;
153
+ const standIn = (indices) => {
154
+ waited = true;
155
+ return shown && shown.input === indices ? shown.output : indices;
156
+ };
152
157
  function step(indices, state, ctx) {
153
158
  const query = typeof state?.query === "string" ? state.query.trim() : "";
154
159
  if (query === "") return indices;
@@ -190,14 +195,22 @@ var search = (opts = {}) => {
190
195
  interceptors: [
191
196
  (0, import_define2.defineInterceptor)({
192
197
  name: "search",
193
- on: engine ? ["table:destroy", "pipeline:run"] : ["table:destroy"],
198
+ on: ["table:destroy", "pipeline:run"],
194
199
  intercept(ctx, next) {
195
200
  const table = ctx.table;
196
201
  if (ctx.op === "table:destroy") {
197
202
  instances.get(table)?.();
198
203
  return next();
199
204
  }
200
- return engine.run(table, next);
205
+ const prev = waited;
206
+ waited = false;
207
+ try {
208
+ return engine ? engine.run(table, next) : next();
209
+ } finally {
210
+ const w = waited;
211
+ waited = prev;
212
+ ran.get(table)?.(w);
213
+ }
201
214
  }
202
215
  })
203
216
  ],
@@ -206,11 +219,12 @@ var search = (opts = {}) => {
206
219
  const write = (input2, query, status, error) => {
207
220
  const s = ctx.getState();
208
221
  if (status !== "error" && s?.input === input2 && s.query === query && s.status === status && !("error" in s)) {
209
- return;
222
+ return false;
210
223
  }
211
224
  ctx.setState(
212
225
  status === "error" ? { input: input2, query, status, error } : { input: input2, query, status }
213
226
  );
227
+ return ctx.getState() !== s;
214
228
  };
215
229
  const applied = () => ctx.getState()?.query ?? "";
216
230
  const input = () => ctx.getState()?.input ?? "";
@@ -218,11 +232,6 @@ var search = (opts = {}) => {
218
232
  const waitFor = (query) => new Promise((resolve) => {
219
233
  waiting.push({ query, resolve });
220
234
  });
221
- const settleWaiting = (query, outcome) => {
222
- const list = waiting;
223
- waiting = [];
224
- for (const w of list) w.resolve(w.query === query ? outcome : import_search_fn_brand.SUPERSEDED);
225
- };
226
235
  const replaceWaiting = (query) => {
227
236
  if (waiting.length === 0) return;
228
237
  const keep = [];
@@ -238,6 +247,22 @@ var search = (opts = {}) => {
238
247
  waiting = [];
239
248
  return keep;
240
249
  };
250
+ let held = [];
251
+ let busy = false;
252
+ const release = (outcome) => {
253
+ const list = held;
254
+ held = [];
255
+ for (const w of list) w.resolve(outcome);
256
+ };
257
+ const supersede = () => {
258
+ held = [...waiting, ...held];
259
+ waiting = [];
260
+ release(import_search_fn_brand.SUPERSEDED);
261
+ };
262
+ ran.set(table, (w) => {
263
+ busy = w;
264
+ if (!w) release(import_search_fn_brand.APPLIED);
265
+ });
241
266
  const session = engine?.session({
242
267
  ctx,
243
268
  write,
@@ -247,8 +272,9 @@ var search = (opts = {}) => {
247
272
  });
248
273
  const applyQuery = (query, inp = input()) => {
249
274
  if (session?.apply(query, inp)) return;
250
- write(inp, query, "idle");
251
- settleWaiting(query, import_search_fn_brand.APPLIED);
275
+ if (held[0]?.query !== query) release(import_search_fn_brand.SUPERSEDED);
276
+ held.push(...claim(query));
277
+ if (!write(inp, query, "idle") && !busy) release(import_search_fn_brand.APPLIED);
252
278
  };
253
279
  let calling = false;
254
280
  let appliedNow;
@@ -261,7 +287,7 @@ var search = (opts = {}) => {
261
287
  instances.set(table, () => {
262
288
  timer?.cancel();
263
289
  session?.destroy();
264
- settleWaiting("\0", import_search_fn_brand.SUPERSEDED);
290
+ supersede();
265
291
  });
266
292
  const commit = (next) => {
267
293
  const current = applied();
@@ -309,10 +335,10 @@ var search = (opts = {}) => {
309
335
  cancelSearch: () => {
310
336
  timer?.cancel();
311
337
  session?.abort();
312
- settleWaiting("\0", import_search_fn_brand.SUPERSEDED);
338
+ supersede();
313
339
  const s = ctx.getState();
314
340
  if (s?.status === "pending" || s?.status === "searching") {
315
- ctx.setState({ input: s.input, query: s.query, status: "idle" });
341
+ ctx.setState({ ...s, status: "idle" });
316
342
  }
317
343
  },
318
344
  clearSearch: () => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/search/search.ts"],"sourcesContent":["import { debouncedSignal } from 'kk-debounce/debounceSignal'\nimport { throttle } from 'kk-debounce/throttle'\nimport { optionDiagnostics } from '../../catalog/codes'\nimport { defineOptions } from '../../catalog/options'\nimport { throwConfigError } from '../../diagnostics/build'\nimport { keepDiagnostics } from '../../diagnostics/catalog'\nimport { defineFeature } from '../../feature/define'\nimport type { AnyRow, RowModelContext } from '../../feature/types'\nimport { defineInterceptor } from '../../intercept/define'\nimport { runJob, sliced } from '../../pipeline/slice'\nimport { type DataMode, modeOption } from '../../source/mode'\nimport {\n APPLIED,\n type AsyncSearchFn,\n SEARCH_ENGINE,\n type SearchEngine,\n type SearchOutcome,\n type SearchSession,\n type SearchState,\n type SearchStatus,\n SUPERSEDED,\n type Waiter,\n} from './search-fn-brand'\nimport { buildIndex, type MatchConfig, probeExSearch, type SearchIndex } from './search-index'\n\nkeepDiagnostics(optionDiagnostics)\n\nexport type {\n AsyncSearchFn,\n SearchFn,\n SearchFnContext,\n SearchOutcome,\n SearchResult,\n SearchState,\n SearchStatus,\n} from './search-fn-brand'\n\nexport interface SearchApi {\n /** Sets `input` (and `query`, with the client's immediate typing strategy). */\n setSearch(query: string): Promise<SearchOutcome>\n /** Applies the current `input` now (Enter). */\n flushSearch(): Promise<SearchOutcome>\n /** Drops a pending (debounced) query; `input` and the applied `query` stay. */\n cancelSearch(): void\n /** Empties `input` and `query`: every row is shown again. */\n clearSearch(): void\n}\n\n/** A duration: milliseconds, or a kk-debounce temporal object (`{ seconds: 1, ms: 500 }`). */\nexport type SearchWait =\n | number\n | { hours?: number; minutes?: number; seconds?: number; ms?: number }\n\n/** When `query` follows `input` (§6.3). */\nexport interface SearchTyping {\n /**\n * `'immediate'` (default): every keystroke is applied. `'debounce'`: applied after `wait` of\n * quiet (kk-debounce `debouncedSignal`). `'throttle'`: at most once per `wait` (the first\n * keystroke at once, the latest one at the end of the window).\n */\n strategy?: 'immediate' | 'debounce' | 'throttle'\n /** Default 300 ms. Used by `'debounce'` and `'throttle'`. */\n wait?: SearchWait\n /**\n * Debounce only. `'trailing'` (default): apply after the burst. `'leading'`: apply the first\n * keystroke at once and drop the rest of the burst (`flushSearch()` applies them).\n */\n behavior?: 'trailing' | 'leading'\n /** A trimmed input shorter than this is applied as an empty query (all rows). Default 0. */\n minLength?: number\n /** `'type'` (default): typing commits. `'enter'`: only `flushSearch()` commits. */\n commitOn?: 'type' | 'enter'\n}\n\n/** Client matching options (§6.3). */\nexport interface SearchOptions<T = AnyRow> {\n /** Minimum relevance (0–1, weight applied) to keep a row. Default `0.3`. */\n threshold?: number\n /** Maximum Levenshtein distance for a fuzzy match. Default `3`. */\n fuzzyMaxDistance?: number\n /** Default `false`. */\n caseSensitive?: boolean\n /**\n * Typing timing: when `query` follows `input`. Default: immediate; with `mode: 'server'`,\n * debounce 300 ms and `minLength` 2 (§6.3).\n */\n typing?: SearchTyping\n /**\n * `'server'`: the server matches `query` (its state goes to `options.fetcher`, or the app\n * fetches); core skips this step (§7.1). Default `'client'`.\n */\n mode?: DataMode\n /**\n * Replaces the built-in matching (§6.3): called with each applied non-blank query; while it runs\n * `status` is `'searching'` and the previous results stay. A failure keeps them too\n * (`DK_SEARCH_FAILED`, `status: 'error'`); an `AbortError` is not a failure. New data runs it\n * again for the applied query. Wrap it with `withSearchFn()` from `@datakable/core/search-async`\n * (client search does not bundle the async engine): `searchFn: withSearchFn(fn)`.\n */\n searchFn?: AsyncSearchFn<T>\n}\n\nconst DEFAULT_WAIT = 300\n\nconst isDuration = (v: unknown): boolean => {\n if (typeof v === 'number') return Number.isFinite(v) && v >= 0\n for (const k of ['hours', 'minutes', 'seconds', 'ms'] as const) {\n const n = (v as Record<string, unknown>)[k]\n if (n !== undefined && !(typeof n === 'number' && Number.isFinite(n) && n >= 0)) return false\n }\n return true\n}\n\nconst typingOptions = {\n strategy: { type: 'string', oneOf: ['immediate', 'debounce', 'throttle'] },\n wait: {\n type: ['number', 'object'],\n check: isDuration,\n expected: 'milliseconds >= 0 or { hours?, minutes?, seconds?, ms? } with values >= 0',\n noEffect: (_: unknown, o: Readonly<Record<string, unknown>>) =>\n o.commitOn === 'enter'\n ? \"commitOn is 'enter': only flushSearch() commits\"\n : o.strategy !== 'debounce' && o.strategy !== 'throttle'\n ? \"only the 'debounce' and 'throttle' strategies wait\"\n : undefined,\n },\n behavior: {\n type: 'string',\n oneOf: ['trailing', 'leading'],\n noEffect: (_: unknown, o: Readonly<Record<string, unknown>>) =>\n o.commitOn === 'enter'\n ? \"commitOn is 'enter': only flushSearch() commits\"\n : o.strategy !== 'debounce'\n ? \"behavior applies to the 'debounce' strategy only\"\n : undefined,\n },\n minLength: {\n type: 'number',\n check: (v: number) => Number.isInteger(v) && v >= 0,\n expected: 'a non-negative integer',\n },\n commitOn: { type: 'string', oneOf: ['type', 'enter'] },\n} as const\n\n/** Built-in matching options do nothing when `searchFn` matches. */\nconst matching = (_: unknown, o: Readonly<Record<string, unknown>>) =>\n o.searchFn === undefined ? undefined : 'searchFn replaces the built-in matching'\n\nconst searchOptions = {\n threshold: {\n type: 'number',\n check: (v: number) => v >= 0 && v <= 1,\n expected: 'a number from 0 to 1',\n noEffect: matching,\n },\n fuzzyMaxDistance: {\n type: 'number',\n check: (v: number) => Number.isInteger(v) && v >= 0,\n expected: 'a non-negative integer',\n noEffect: matching,\n },\n caseSensitive: { type: 'boolean', noEffect: matching },\n typing: { type: 'object', shape: typingOptions },\n mode: modeOption,\n searchFn: {\n type: 'function',\n check: (v: object) => typeof (v as { [SEARCH_ENGINE]?: unknown })[SEARCH_ENGINE] === 'function',\n expected:\n \"a function wrapped with withSearchFn() from '@datakable/core/search-async': \" +\n 'searchFn: withSearchFn(fn)',\n noEffect: (_: unknown, o: Readonly<Record<string, unknown>>) =>\n o.mode === 'server'\n ? \"mode is 'server': the server (options.fetcher, or the app) matches the query\"\n : undefined,\n },\n} as const\n\nconst settledSuperseded = () => Promise.resolve(SUPERSEDED)\n\nconst toText = (q: unknown): string => (typeof q === 'string' ? q : q == null ? '' : String(q))\n\n/**\n * Global search (§6.3), client mode: fuzzy matching with `ex-search` on the formatted values of the\n * searchable columns (`search: weight`; every column at weight 1 when none declares one).\n *\n * - The matcher is built on the first non-blank query, once per data + columns (not per keystroke),\n * over the column cache's values; ex-search reads the projection `{ i, c0, c1, … }` one chunk of\n * rows at a time, built while matching and not kept (§9.3 memory); results map back by index.\n * - A blank or whitespace-only query keeps every row. The query is trimmed before matching.\n * - Rows are ordered by relevance (score descending, ties by original index); a sort stage after\n * search overrides that order.\n */\nexport const search = <T = AnyRow>(opts: SearchOptions<T> = {}) => {\n const config: MatchConfig = {\n threshold: opts.threshold ?? 0.3,\n fuzzyMaxDistance: opts.fuzzyMaxDistance ?? 3,\n caseSensitive: opts.caseSensitive ?? false,\n }\n /**\n * data → columns → the key of the projection job (`ctx.slice`, §9.1): its result is the index\n * (`null`: nothing searchable). Released with the data.\n */\n const indexes = new WeakMap<object, WeakMap<object, object>>()\n /** Job key → its finished index, also for runs without slicing (`explainRow` replays). */\n const built = new WeakMap<object, SearchIndex | null>()\n /** `ex-search` usable (probed once per feature instance, on the first build). */\n let useExSearch: boolean | undefined\n /** Columns references already reported as having no searchable column. */\n const reported = new WeakSet<object>()\n /** The last step: a slice change that keeps the query (typing, status) reuses its output. */\n let last:\n | { input: Uint32Array; query: string; index: SearchIndex; output: Uint32Array }\n | undefined\n /** The key of the latest matching job (one query over one set of rows). */\n let matching: { rows: Uint32Array; index: SearchIndex; query: string; key: object } | undefined\n /** The step's last output and its input: the stand-in while sliced work runs (§9.1). */\n let shown: { input: Uint32Array; output: Uint32Array } | undefined\n const server = opts.mode === 'server'\n // Server defaults (§6.3): debounce, minLength 2. Filled in before the option check, so `wait` /\n // `behavior` alone are not reported as having no effect.\n const typing: SearchTyping = server\n ? { strategy: 'debounce', minLength: 2, ...opts.typing }\n : (opts.typing ?? {})\n const strategy = typing.commitOn === 'enter' ? 'enter' : (typing.strategy ?? 'immediate')\n const wait = typing.wait ?? DEFAULT_WAIT\n const minLength = typing.minLength ?? 0\n /** The query an input applies (a shorter trimmed input is no search). */\n const target = (input: string) => (input.trim().length < minLength ? '' : input)\n /** The async engine a `withSearchFn` searchFn brings (§6.3); `undefined` for client search. */\n const engine: SearchEngine | undefined = opts.searchFn?.[SEARCH_ENGINE]?.(opts.searchFn as never)\n // Never silent (§8.6): in production (no option validator) an unwrapped searchFn would be\n // ignored; in development the validator reports it at createTable(), with the full text.\n if (!(process.env.NODE_ENV !== \"production\") && opts.searchFn && !engine) {\n throwConfigError('DK_INVALID_OPTION', {\n path: 'searchFn',\n feature: 'search',\n expected: 'withSearchFn(searchFn)',\n received: 'searchFn',\n suggestions: [],\n phase: 'init',\n })\n }\n /** Per table: ends its timers, searches and waiters on `table:destroy`. */\n const instances = new WeakMap<object, () => void>()\n\n /** The projection of (data, columns): built by a sliced job; `undefined` while it runs. */\n function indexFor(ctx: RowModelContext): SearchIndex | null | undefined {\n let byColumns = indexes.get(ctx.data)\n if (!byColumns) {\n byColumns = new WeakMap()\n indexes.set(ctx.data, byColumns)\n }\n let key = byColumns.get(ctx.columns)\n if (key === undefined) {\n key = {}\n byColumns.set(ctx.columns, key)\n }\n const done = built.get(key)\n if (done !== undefined) return done\n useExSearch ??= probeExSearch(ctx.emit)\n const exSearch = useExSearch\n const index = sliced(ctx, key, () => buildIndex(ctx, config, exSearch))\n if (index !== undefined) built.set(key, index)\n return index\n }\n\n /** The ranked matches of `query` over `rows`: a sliced job; `undefined` while it runs. */\n function matchFor(\n ctx: RowModelContext,\n index: SearchIndex,\n rows: Uint32Array,\n query: string,\n ): Uint32Array | undefined {\n // A replay (`explainRow`) matches its one row at once, leaving the table's job alone.\n if (!ctx.slice) return runJob(() => index.match(rows, query))\n if (\n !matching ||\n matching.rows !== rows ||\n matching.index !== index ||\n matching.query !== query\n ) {\n matching = { rows, index, query, key: {} }\n }\n return sliced(ctx, matching.key, () => index.match(rows, query))\n }\n\n /** While sliced work runs: the previous output for the same input, else the input. */\n const standIn = (indices: Uint32Array) =>\n shown && shown.input === indices ? shown.output : indices\n\n /** The client step (§6.3); the rowModel below records its output for {@link standIn}. */\n function step(indices: Uint32Array, state: SearchState, ctx: RowModelContext): Uint32Array {\n const query = typeof state?.query === 'string' ? state.query.trim() : ''\n // ex-search returns [] for a blank query: blank means \"no search\".\n if (query === '') return indices\n if (engine) return engine.step(indices, ctx)\n const index = indexFor(ctx)\n // The projection is still being built (time-sliced, §9.1): the row model is 'pending'.\n if (index === undefined) return standIn(indices)\n if (index === null) {\n if (!reported.has(ctx.columns)) {\n reported.add(ctx.columns)\n ctx.emit('DK_OPTION_NO_EFFECT', {\n path: 'query',\n feature: 'search',\n reason:\n 'no column is searchable: pass `columns` (give the columns to search a `search` ' +\n 'weight, or omit it everywhere to search every column)',\n once: false,\n })\n }\n return indices\n }\n if (last && last.input === indices && last.query === query && last.index === index) {\n return last.output\n }\n // Search runs after filters: only the input rows are matched (order is re-ranked anyway).\n const output = matchFor(ctx, index, indices, query)\n if (output === undefined) return standIn(indices)\n last = { input: indices, query, index, output }\n return output\n }\n\n return defineFeature({\n name: 'search',\n stage: 'search',\n mode: opts.mode ?? 'client',\n options: defineOptions(\n // The option validator runs in development only (§9.3): production drops the schema.\n (process.env.NODE_ENV !== \"production\") ? searchOptions : ({} as typeof searchOptions),\n server ? { ...opts, typing } : opts,\n ),\n initialState: (): SearchState => ({ input: '', query: '', status: 'idle' }),\n // After destroy(): still a promise, so `setSearch(q).then(…)` never throws.\n destroyedResult: { setSearch: settledSuperseded, flushSearch: settledSuperseded },\n interceptors: [\n defineInterceptor({\n name: 'search',\n on: engine ? ['table:destroy', 'pipeline:run'] : ['table:destroy'],\n intercept(ctx, next) {\n const table = ctx.table as object\n if (ctx.op === 'table:destroy') {\n instances.get(table)?.()\n return next()\n }\n return (engine as SearchEngine).run(table, next)\n },\n }),\n ],\n createApi: (ctx): SearchApi => {\n const table = ctx.table as unknown as object\n const write = (input: string, query: string, status: SearchStatus, error?: unknown) => {\n const s = ctx.getState()\n if (\n status !== 'error' &&\n s?.input === input &&\n s.query === query &&\n s.status === status &&\n !('error' in s)\n ) {\n return\n }\n ctx.setState(\n status === 'error' ? { input, query, status, error } : { input, query, status },\n )\n }\n const applied = () => ctx.getState()?.query ?? ''\n const input = () => ctx.getState()?.input ?? ''\n\n // --- promise outcomes (§6.3): resolved when the query is applied, replaced, or failed ---\n /** Waiters of queries not handed to a search yet (typing, pending debounce, Enter mode). */\n let waiting: Waiter[] = []\n const waitFor = (query: string) =>\n new Promise<SearchOutcome>((resolve) => {\n waiting.push({ query, resolve })\n })\n /** Resolves the waiters of `query` with `outcome`, every other one `'superseded'`. */\n const settleWaiting = (query: string, outcome: SearchOutcome) => {\n const list = waiting\n waiting = []\n for (const w of list) w.resolve(w.query === query ? outcome : SUPERSEDED)\n }\n /** A newer query replaces the waiting ones that differ from it. */\n const replaceWaiting = (query: string) => {\n if (waiting.length === 0) return\n const keep: Waiter[] = []\n for (const w of waiting) {\n if (w.query === query) keep.push(w)\n else w.resolve(SUPERSEDED)\n }\n waiting = keep\n }\n\n /** Takes the waiters of `query`; every other waiting one resolves `'superseded'`. */\n const claim = (query: string) => {\n replaceWaiting(query)\n const keep = waiting\n waiting = []\n return keep\n }\n const session: SearchSession | undefined = engine?.session({\n ctx,\n write,\n applied,\n input,\n claim,\n })\n\n /**\n * Applies `query` (typing committed it, Enter, clear): at once, or with `searchFn` through a\n * request (the slice shows `'searching'`).\n */\n const applyQuery = (query: string, inp: string = input()) => {\n if (session?.apply(query, inp)) return\n write(inp, query, 'idle')\n settleWaiting(query, APPLIED)\n }\n\n // --- typing (T17) ---\n /** Inside a strategy call: a synchronous apply is collected here (one update, not two). */\n let calling = false\n let appliedNow: string | undefined\n const apply = (query: string) => {\n if (calling) appliedNow = query\n else applyQuery(query)\n }\n const timer =\n strategy === 'debounce'\n ? debouncedSignal(applied, apply, wait, { behavior: typing.behavior ?? 'trailing' })\n : strategy === 'throttle'\n ? throttle(apply, wait)\n : undefined\n /** The slice status while nothing new is applied: a search may still be in flight. */\n const resting = (): SearchStatus => (session?.busy() ? 'searching' : 'idle')\n\n instances.set(table, () => {\n timer?.cancel()\n session?.destroy()\n settleWaiting('\\0', SUPERSEDED)\n })\n\n /** Typing: `input` at once; `query` through the strategy (`pending` while scheduled). */\n const commit = (next: string): Promise<SearchOutcome> => {\n const current = applied()\n const query = target(next)\n replaceWaiting(query)\n const p = waitFor(query)\n if (strategy === 'enter') {\n write(next, current, resting())\n return p\n }\n if (!timer || query === current) {\n timer?.cancel()\n applyQuery(query, next)\n return p\n }\n calling = true\n appliedNow = undefined\n try {\n timer(query)\n } finally {\n calling = false\n }\n if (appliedNow !== undefined) {\n applyQuery(appliedNow, next)\n return p\n }\n // A leading debounce drops the rest of the burst: nothing is scheduled to commit it (the\n // silence window keeps running; `flushSearch()` commits the input).\n if (strategy === 'debounce' && typing.behavior === 'leading') {\n write(next, current, resting())\n return p\n }\n write(next, current, 'pending')\n return p\n }\n /** Applies `input` now (Enter, clear): any scheduled commit is dropped. */\n const applyNow = (next: string): Promise<SearchOutcome> => {\n timer?.cancel()\n const query = target(next)\n replaceWaiting(query)\n const p = waitFor(query)\n applyQuery(query, next)\n return p\n }\n return {\n setSearch: (query) => commit(toText(query)),\n flushSearch: () => applyNow(input()),\n cancelSearch: () => {\n timer?.cancel()\n session?.abort()\n settleWaiting('\\0', SUPERSEDED)\n const s = ctx.getState()\n if (s?.status === 'pending' || s?.status === 'searching') {\n ctx.setState({ input: s.input, query: s.query, status: 'idle' })\n }\n },\n clearSearch: () => {\n void applyNow('')\n },\n }\n },\n rowModel: (indices, state, ctx) => {\n const output = step(indices, state, ctx)\n shown = { input: indices, output }\n return output\n },\n })\n}\n"],"mappings":"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","names":["import_define","matching","input"]}
1
+ {"version":3,"sources":["../../../src/features/search/search.ts"],"sourcesContent":["import { debouncedSignal } from 'kk-debounce/debounceSignal'\nimport { throttle } from 'kk-debounce/throttle'\nimport { optionDiagnostics } from '../../catalog/codes'\nimport { defineOptions } from '../../catalog/options'\nimport { throwConfigError } from '../../diagnostics/build'\nimport { keepDiagnostics } from '../../diagnostics/catalog'\nimport { defineFeature } from '../../feature/define'\nimport type { AnyRow, RowModelContext } from '../../feature/types'\nimport { defineInterceptor } from '../../intercept/define'\nimport { runJob, sliced } from '../../pipeline/slice'\nimport { type DataMode, modeOption } from '../../source/mode'\nimport {\n APPLIED,\n type AsyncSearchFn,\n SEARCH_ENGINE,\n type SearchEngine,\n type SearchOutcome,\n type SearchSession,\n type SearchState,\n type SearchStatus,\n SUPERSEDED,\n type Waiter,\n} from './search-fn-brand'\nimport { buildIndex, type MatchConfig, probeExSearch, type SearchIndex } from './search-index'\n\nkeepDiagnostics(optionDiagnostics)\n\nexport type {\n AsyncSearchFn,\n SearchFn,\n SearchFnContext,\n SearchOutcome,\n SearchResult,\n SearchState,\n SearchStatus,\n} from './search-fn-brand'\n\nexport interface SearchApi {\n /** Sets `input` (and `query`, with the client's immediate typing strategy). */\n setSearch(query: string): Promise<SearchOutcome>\n /** Applies the current `input` now (Enter). */\n flushSearch(): Promise<SearchOutcome>\n /** Drops a pending (debounced) query; `input` and the applied `query` stay. */\n cancelSearch(): void\n /** Empties `input` and `query`: every row is shown again. */\n clearSearch(): void\n}\n\n/** A duration: milliseconds, or a kk-debounce temporal object (`{ seconds: 1, ms: 500 }`). */\nexport type SearchWait =\n | number\n | { hours?: number; minutes?: number; seconds?: number; ms?: number }\n\n/** When `query` follows `input` (§6.3). */\nexport interface SearchTyping {\n /**\n * `'immediate'` (default): every keystroke is applied. `'debounce'`: applied after `wait` of\n * quiet (kk-debounce `debouncedSignal`). `'throttle'`: at most once per `wait` (the first\n * keystroke at once, the latest one at the end of the window).\n */\n strategy?: 'immediate' | 'debounce' | 'throttle'\n /** Default 300 ms. Used by `'debounce'` and `'throttle'`. */\n wait?: SearchWait\n /**\n * Debounce only. `'trailing'` (default): apply after the burst. `'leading'`: apply the first\n * keystroke at once and drop the rest of the burst (`flushSearch()` applies them).\n */\n behavior?: 'trailing' | 'leading'\n /** A trimmed input shorter than this is applied as an empty query (all rows). Default 0. */\n minLength?: number\n /** `'type'` (default): typing commits. `'enter'`: only `flushSearch()` commits. */\n commitOn?: 'type' | 'enter'\n}\n\n/** Client matching options (§6.3). */\nexport interface SearchOptions<T = AnyRow> {\n /** Minimum relevance (0–1, weight applied) to keep a row. Default `0.3`. */\n threshold?: number\n /** Maximum Levenshtein distance for a fuzzy match. Default `3`. */\n fuzzyMaxDistance?: number\n /** Default `false`. */\n caseSensitive?: boolean\n /**\n * Typing timing: when `query` follows `input`. Default: immediate; with `mode: 'server'`,\n * debounce 300 ms and `minLength` 2 (§6.3).\n */\n typing?: SearchTyping\n /**\n * `'server'`: the server matches `query` (its state goes to `options.fetcher`, or the app\n * fetches); core skips this step (§7.1). Default `'client'`.\n */\n mode?: DataMode\n /**\n * Replaces the built-in matching (§6.3): called with each applied non-blank query; while it runs\n * `status` is `'searching'` and the previous results stay. A failure keeps them too\n * (`DK_SEARCH_FAILED`, `status: 'error'`); an `AbortError` is not a failure. New data runs it\n * again for the applied query. Wrap it with `withSearchFn()` from `@datakable/core/search-async`\n * (client search does not bundle the async engine): `searchFn: withSearchFn(fn)`.\n */\n searchFn?: AsyncSearchFn<T>\n}\n\nconst DEFAULT_WAIT = 300\n\nconst isDuration = (v: unknown): boolean => {\n if (typeof v === 'number') return Number.isFinite(v) && v >= 0\n for (const k of ['hours', 'minutes', 'seconds', 'ms'] as const) {\n const n = (v as Record<string, unknown>)[k]\n if (n !== undefined && !(typeof n === 'number' && Number.isFinite(n) && n >= 0)) return false\n }\n return true\n}\n\nconst typingOptions = {\n strategy: { type: 'string', oneOf: ['immediate', 'debounce', 'throttle'] },\n wait: {\n type: ['number', 'object'],\n check: isDuration,\n expected: 'milliseconds >= 0 or { hours?, minutes?, seconds?, ms? } with values >= 0',\n noEffect: (_: unknown, o: Readonly<Record<string, unknown>>) =>\n o.commitOn === 'enter'\n ? \"commitOn is 'enter': only flushSearch() commits\"\n : o.strategy !== 'debounce' && o.strategy !== 'throttle'\n ? \"only the 'debounce' and 'throttle' strategies wait\"\n : undefined,\n },\n behavior: {\n type: 'string',\n oneOf: ['trailing', 'leading'],\n noEffect: (_: unknown, o: Readonly<Record<string, unknown>>) =>\n o.commitOn === 'enter'\n ? \"commitOn is 'enter': only flushSearch() commits\"\n : o.strategy !== 'debounce'\n ? \"behavior applies to the 'debounce' strategy only\"\n : undefined,\n },\n minLength: {\n type: 'number',\n check: (v: number) => Number.isInteger(v) && v >= 0,\n expected: 'a non-negative integer',\n },\n commitOn: { type: 'string', oneOf: ['type', 'enter'] },\n} as const\n\n/** Built-in matching options do nothing when `searchFn` matches. */\nconst matching = (_: unknown, o: Readonly<Record<string, unknown>>) =>\n o.searchFn === undefined ? undefined : 'searchFn replaces the built-in matching'\n\nconst searchOptions = {\n threshold: {\n type: 'number',\n check: (v: number) => v >= 0 && v <= 1,\n expected: 'a number from 0 to 1',\n noEffect: matching,\n },\n fuzzyMaxDistance: {\n type: 'number',\n check: (v: number) => Number.isInteger(v) && v >= 0,\n expected: 'a non-negative integer',\n noEffect: matching,\n },\n caseSensitive: { type: 'boolean', noEffect: matching },\n typing: { type: 'object', shape: typingOptions },\n mode: modeOption,\n searchFn: {\n type: 'function',\n check: (v: object) => typeof (v as { [SEARCH_ENGINE]?: unknown })[SEARCH_ENGINE] === 'function',\n expected:\n \"a function wrapped with withSearchFn() from '@datakable/core/search-async': \" +\n 'searchFn: withSearchFn(fn)',\n noEffect: (_: unknown, o: Readonly<Record<string, unknown>>) =>\n o.mode === 'server'\n ? \"mode is 'server': the server (options.fetcher, or the app) matches the query\"\n : undefined,\n },\n} as const\n\nconst settledSuperseded = () => Promise.resolve(SUPERSEDED)\n\nconst toText = (q: unknown): string => (q == null ? '' : String(q))\n\n/**\n * Global search (§6.3), client mode: fuzzy matching with `ex-search` on the formatted values of the\n * searchable columns (`search: weight`; every column at weight 1 when none declares one).\n *\n * - The matcher is built on the first non-blank query, once per data + columns (not per keystroke),\n * over the column cache's values; ex-search reads the projection `{ i, c0, c1, … }` one chunk of\n * rows at a time, built while matching and not kept (§9.3 memory); results map back by index.\n * - A blank or whitespace-only query keeps every row. The query is trimmed before matching.\n * - Rows are ordered by relevance (score descending, ties by original index); a sort stage after\n * search overrides that order.\n */\nexport const search = <T = AnyRow>(opts: SearchOptions<T> = {}) => {\n const config: MatchConfig = {\n threshold: opts.threshold ?? 0.3,\n fuzzyMaxDistance: opts.fuzzyMaxDistance ?? 3,\n caseSensitive: opts.caseSensitive ?? false,\n }\n /**\n * data → columns → the key of the projection job (`ctx.slice`, §9.1): its result is the index\n * (`null`: nothing searchable). Released with the data.\n */\n const indexes = new WeakMap<object, WeakMap<object, object>>()\n /** Job key → its finished index, also for runs without slicing (`explainRow` replays). */\n const built = new WeakMap<object, SearchIndex | null>()\n /** `ex-search` usable (probed once per feature instance, on the first build). */\n let useExSearch: boolean | undefined\n /** Columns references already reported as having no searchable column. */\n const reported = new WeakSet<object>()\n /** The last step: a slice change that keeps the query (typing, status) reuses its output. */\n let last:\n | { input: Uint32Array; query: string; index: SearchIndex; output: Uint32Array }\n | undefined\n /** The key of the latest matching job (one query over one set of rows). */\n let matching: { rows: Uint32Array; index: SearchIndex; query: string; key: object } | undefined\n /** The step's last output and its input: the stand-in while sliced work runs (§9.1). */\n let shown: { input: Uint32Array; output: Uint32Array } | undefined\n const server = opts.mode === 'server'\n // Server defaults (§6.3): debounce, minLength 2. Filled in before the option check, so `wait` /\n // `behavior` alone are not reported as having no effect.\n const typing: SearchTyping = server\n ? { strategy: 'debounce', minLength: 2, ...opts.typing }\n : (opts.typing ?? {})\n const strategy = typing.commitOn === 'enter' ? 'enter' : (typing.strategy ?? 'immediate')\n const wait = typing.wait ?? DEFAULT_WAIT\n const minLength = typing.minLength ?? 0\n /** The query an input applies (a shorter trimmed input is no search). */\n const target = (input: string) => (input.trim().length < minLength ? '' : input)\n /** The async engine a `withSearchFn` searchFn brings (§6.3); `undefined` for client search. */\n const engine: SearchEngine | undefined = opts.searchFn?.[SEARCH_ENGINE]?.(opts.searchFn as never)\n // Never silent (§8.6): in production (no option validator) an unwrapped searchFn would be\n // ignored; in development the validator reports it at createTable(), with the full text.\n if (!(process.env.NODE_ENV !== \"production\") && opts.searchFn && !engine) {\n throwConfigError('DK_INVALID_OPTION', {\n path: 'searchFn',\n feature: 'search',\n expected: 'withSearchFn(searchFn)',\n received: 'searchFn',\n suggestions: [],\n phase: 'init',\n })\n }\n /** Per table: ends its timers, searches and waiters on `table:destroy`. */\n const instances = new WeakMap<object, () => void>()\n /** Per table: after each `pipeline:run`, whether the step waited on sliced work (#6). */\n const ran = new WeakMap<object, (waited: boolean) => void>()\n /** The step of the run in progress returned a stand-in (sliced work runs, §9.1). */\n let waited = false\n\n /** The projection of (data, columns): built by a sliced job; `undefined` while it runs. */\n function indexFor(ctx: RowModelContext): SearchIndex | null | undefined {\n let byColumns = indexes.get(ctx.data)\n if (!byColumns) {\n byColumns = new WeakMap()\n indexes.set(ctx.data, byColumns)\n }\n let key = byColumns.get(ctx.columns)\n if (!key) {\n key = {}\n byColumns.set(ctx.columns, key)\n }\n const done = built.get(key)\n if (done !== undefined) return done\n useExSearch ??= probeExSearch(ctx.emit)\n const exSearch = useExSearch\n const index = sliced(ctx, key, () => buildIndex(ctx, config, exSearch))\n if (index !== undefined) built.set(key, index)\n return index\n }\n\n /** The ranked matches of `query` over `rows`: a sliced job; `undefined` while it runs. */\n function matchFor(\n ctx: RowModelContext,\n index: SearchIndex,\n rows: Uint32Array,\n query: string,\n ): Uint32Array | undefined {\n // A replay (`explainRow`) matches its one row at once, leaving the table's job alone.\n if (!ctx.slice) return runJob(() => index.match(rows, query))\n if (\n !matching ||\n matching.rows !== rows ||\n matching.index !== index ||\n matching.query !== query\n ) {\n matching = { rows, index, query, key: {} }\n }\n return sliced(ctx, matching.key, () => index.match(rows, query))\n }\n\n /** While sliced work runs: the previous output for the same input, else the input. */\n const standIn = (indices: Uint32Array) => {\n waited = true\n return shown && shown.input === indices ? shown.output : indices\n }\n\n /** The client step (§6.3); the rowModel below records its output for {@link standIn}. */\n function step(indices: Uint32Array, state: SearchState, ctx: RowModelContext): Uint32Array {\n const query = typeof state?.query === 'string' ? state.query.trim() : ''\n // ex-search returns [] for a blank query: blank means \"no search\".\n if (query === '') return indices\n if (engine) return engine.step(indices, ctx)\n const index = indexFor(ctx)\n // The projection is still being built (time-sliced, §9.1): the row model is 'pending'.\n if (index === undefined) return standIn(indices)\n if (index === null) {\n if (!reported.has(ctx.columns)) {\n reported.add(ctx.columns)\n ctx.emit('DK_OPTION_NO_EFFECT', {\n path: 'query',\n feature: 'search',\n reason:\n 'no column is searchable: pass `columns` (give the columns to search a `search` ' +\n 'weight, or omit it everywhere to search every column)',\n once: false,\n })\n }\n return indices\n }\n if (last && last.input === indices && last.query === query && last.index === index) {\n return last.output\n }\n // Search runs after filters: only the input rows are matched (order is re-ranked anyway).\n const output = matchFor(ctx, index, indices, query)\n if (output === undefined) return standIn(indices)\n last = { input: indices, query, index, output }\n return output\n }\n\n return defineFeature({\n name: 'search',\n stage: 'search',\n mode: opts.mode ?? 'client',\n options: defineOptions(\n // The option validator runs in development only (§9.3): production drops the schema.\n (process.env.NODE_ENV !== \"production\") ? searchOptions : ({} as typeof searchOptions),\n server ? { ...opts, typing } : opts,\n ),\n initialState: (): SearchState => ({ input: '', query: '', status: 'idle' }),\n // After destroy(): still a promise, so `setSearch(q).then(…)` never throws.\n destroyedResult: { setSearch: settledSuperseded, flushSearch: settledSuperseded },\n interceptors: [\n defineInterceptor({\n name: 'search',\n on: ['table:destroy', 'pipeline:run'],\n intercept(ctx, next) {\n const table = ctx.table as object\n if (ctx.op === 'table:destroy') {\n instances.get(table)?.()\n return next()\n }\n const prev = waited\n waited = false\n try {\n return engine ? engine.run(table, next) : next()\n } finally {\n const w = waited\n waited = prev\n ran.get(table)?.(w)\n }\n },\n }),\n ],\n createApi: (ctx): SearchApi => {\n const table = ctx.table as unknown as object\n /** Writes the slice; `true` when the state changed (a pipeline run follows). */\n const write = (input: string, query: string, status: SearchStatus, error?: unknown) => {\n const s = ctx.getState()\n if (\n status !== 'error' &&\n s?.input === input &&\n s.query === query &&\n s.status === status &&\n !('error' in s)\n ) {\n return false\n }\n ctx.setState(\n status === 'error' ? { input, query, status, error } : { input, query, status },\n )\n return ctx.getState() !== s\n }\n const applied = () => ctx.getState()?.query ?? ''\n const input = () => ctx.getState()?.input ?? ''\n\n // --- promise outcomes (§6.3): resolved when the query is applied, replaced, or failed ---\n /** Waiters of queries not handed to a search yet (typing, pending debounce, Enter mode). */\n let waiting: Waiter[] = []\n const waitFor = (query: string) =>\n new Promise<SearchOutcome>((resolve) => {\n waiting.push({ query, resolve })\n })\n /** A newer query replaces the waiting ones that differ from it. */\n const replaceWaiting = (query: string) => {\n if (waiting.length === 0) return\n const keep: Waiter[] = []\n for (const w of waiting) {\n if (w.query === query) keep.push(w)\n else w.resolve(SUPERSEDED)\n }\n waiting = keep\n }\n\n /** Takes the waiters of `query`; every other waiting one resolves `'superseded'`. */\n const claim = (query: string) => {\n replaceWaiting(query)\n const keep = waiting\n waiting = []\n return keep\n }\n /**\n * Waiters of the applied query until the row model reflects it (#6): sliced work (§9.1)\n * keeps the row model 'pending' after the query is written.\n */\n let held: Waiter[] = []\n /** The last run's step waited on sliced work. */\n let busy = false\n const release = (outcome: SearchOutcome) => {\n const list = held\n held = []\n for (const w of list) w.resolve(outcome)\n }\n /** Cancel, destroy: every waiter, waiting then held, resolves `'superseded'`. */\n const supersede = () => {\n held = [...waiting, ...held]\n waiting = []\n release(SUPERSEDED)\n }\n ran.set(table, (w) => {\n busy = w\n if (!w) release(APPLIED)\n })\n const session: SearchSession | undefined = engine?.session({\n ctx,\n write,\n applied,\n input,\n claim,\n })\n\n /**\n * Applies `query` (typing committed it, Enter, clear): at once, or with `searchFn` through a\n * request (the slice shows `'searching'`).\n */\n const applyQuery = (query: string, inp: string = input()) => {\n if (session?.apply(query, inp)) return\n if (held[0]?.query !== query) release(SUPERSEDED)\n held.push(...claim(query))\n // A changed state runs the pipeline (its end releases them); else the last run decides.\n if (!write(inp, query, 'idle') && !busy) release(APPLIED)\n }\n\n // --- typing (T17) ---\n /** Inside a strategy call: a synchronous apply is collected here (one update, not two). */\n let calling = false\n let appliedNow: string | undefined\n const apply = (query: string) => {\n if (calling) appliedNow = query\n else applyQuery(query)\n }\n const timer =\n strategy === 'debounce'\n ? debouncedSignal(applied, apply, wait, { behavior: typing.behavior ?? 'trailing' })\n : strategy === 'throttle'\n ? throttle(apply, wait)\n : undefined\n /** The slice status while nothing new is applied: a search may still be in flight. */\n const resting = (): SearchStatus => (session?.busy() ? 'searching' : 'idle')\n\n instances.set(table, () => {\n timer?.cancel()\n session?.destroy()\n supersede()\n })\n\n /** Typing: `input` at once; `query` through the strategy (`pending` while scheduled). */\n const commit = (next: string): Promise<SearchOutcome> => {\n const current = applied()\n const query = target(next)\n replaceWaiting(query)\n const p = waitFor(query)\n if (strategy === 'enter') {\n write(next, current, resting())\n return p\n }\n if (!timer || query === current) {\n timer?.cancel()\n applyQuery(query, next)\n return p\n }\n calling = true\n appliedNow = undefined\n try {\n timer(query)\n } finally {\n calling = false\n }\n if (appliedNow !== undefined) {\n applyQuery(appliedNow, next)\n return p\n }\n // A leading debounce drops the rest of the burst: nothing is scheduled to commit it (the\n // silence window keeps running; `flushSearch()` commits the input).\n if (strategy === 'debounce' && typing.behavior === 'leading') {\n write(next, current, resting())\n return p\n }\n write(next, current, 'pending')\n return p\n }\n /** Applies `input` now (Enter, clear): any scheduled commit is dropped. */\n const applyNow = (next: string): Promise<SearchOutcome> => {\n timer?.cancel()\n const query = target(next)\n replaceWaiting(query)\n const p = waitFor(query)\n applyQuery(query, next)\n return p\n }\n return {\n setSearch: (query) => commit(toText(query)),\n flushSearch: () => applyNow(input()),\n cancelSearch: () => {\n timer?.cancel()\n session?.abort()\n supersede()\n const s = ctx.getState()\n if (s?.status === 'pending' || s?.status === 'searching') {\n ctx.setState({ ...s, status: 'idle' })\n }\n },\n clearSearch: () => {\n void applyNow('')\n },\n }\n },\n rowModel: (indices, state, ctx) => {\n const output = step(indices, state, ctx)\n shown = { input: indices, output }\n return output\n },\n })\n}\n"],"mappings":"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","names":["import_define","matching","input"]}
@@ -70,7 +70,7 @@ var searchOptions = {
70
70
  }
71
71
  };
72
72
  var settledSuperseded = () => Promise.resolve(SUPERSEDED);
73
- var toText = (q) => typeof q === "string" ? q : q == null ? "" : String(q);
73
+ var toText = (q) => q == null ? "" : String(q);
74
74
  var search = (opts = {}) => {
75
75
  const config = {
76
76
  threshold: opts.threshold ?? 0.3,
@@ -102,6 +102,8 @@ var search = (opts = {}) => {
102
102
  });
103
103
  }
104
104
  const instances = /* @__PURE__ */ new WeakMap();
105
+ const ran = /* @__PURE__ */ new WeakMap();
106
+ let waited = false;
105
107
  function indexFor(ctx) {
106
108
  let byColumns = indexes.get(ctx.data);
107
109
  if (!byColumns) {
@@ -109,7 +111,7 @@ var search = (opts = {}) => {
109
111
  indexes.set(ctx.data, byColumns);
110
112
  }
111
113
  let key = byColumns.get(ctx.columns);
112
- if (key === void 0) {
114
+ if (!key) {
113
115
  key = {};
114
116
  byColumns.set(ctx.columns, key);
115
117
  }
@@ -128,7 +130,10 @@ var search = (opts = {}) => {
128
130
  }
129
131
  return sliced(ctx, matching2.key, () => index.match(rows, query));
130
132
  }
131
- const standIn = (indices) => shown && shown.input === indices ? shown.output : indices;
133
+ const standIn = (indices) => {
134
+ waited = true;
135
+ return shown && shown.input === indices ? shown.output : indices;
136
+ };
132
137
  function step(indices, state, ctx) {
133
138
  const query = typeof state?.query === "string" ? state.query.trim() : "";
134
139
  if (query === "") return indices;
@@ -170,14 +175,22 @@ var search = (opts = {}) => {
170
175
  interceptors: [
171
176
  defineInterceptor({
172
177
  name: "search",
173
- on: engine ? ["table:destroy", "pipeline:run"] : ["table:destroy"],
178
+ on: ["table:destroy", "pipeline:run"],
174
179
  intercept(ctx, next) {
175
180
  const table = ctx.table;
176
181
  if (ctx.op === "table:destroy") {
177
182
  instances.get(table)?.();
178
183
  return next();
179
184
  }
180
- return engine.run(table, next);
185
+ const prev = waited;
186
+ waited = false;
187
+ try {
188
+ return engine ? engine.run(table, next) : next();
189
+ } finally {
190
+ const w = waited;
191
+ waited = prev;
192
+ ran.get(table)?.(w);
193
+ }
181
194
  }
182
195
  })
183
196
  ],
@@ -186,11 +199,12 @@ var search = (opts = {}) => {
186
199
  const write = (input2, query, status, error) => {
187
200
  const s = ctx.getState();
188
201
  if (status !== "error" && s?.input === input2 && s.query === query && s.status === status && !("error" in s)) {
189
- return;
202
+ return false;
190
203
  }
191
204
  ctx.setState(
192
205
  status === "error" ? { input: input2, query, status, error } : { input: input2, query, status }
193
206
  );
207
+ return ctx.getState() !== s;
194
208
  };
195
209
  const applied = () => ctx.getState()?.query ?? "";
196
210
  const input = () => ctx.getState()?.input ?? "";
@@ -198,11 +212,6 @@ var search = (opts = {}) => {
198
212
  const waitFor = (query) => new Promise((resolve) => {
199
213
  waiting.push({ query, resolve });
200
214
  });
201
- const settleWaiting = (query, outcome) => {
202
- const list = waiting;
203
- waiting = [];
204
- for (const w of list) w.resolve(w.query === query ? outcome : SUPERSEDED);
205
- };
206
215
  const replaceWaiting = (query) => {
207
216
  if (waiting.length === 0) return;
208
217
  const keep = [];
@@ -218,6 +227,22 @@ var search = (opts = {}) => {
218
227
  waiting = [];
219
228
  return keep;
220
229
  };
230
+ let held = [];
231
+ let busy = false;
232
+ const release = (outcome) => {
233
+ const list = held;
234
+ held = [];
235
+ for (const w of list) w.resolve(outcome);
236
+ };
237
+ const supersede = () => {
238
+ held = [...waiting, ...held];
239
+ waiting = [];
240
+ release(SUPERSEDED);
241
+ };
242
+ ran.set(table, (w) => {
243
+ busy = w;
244
+ if (!w) release(APPLIED);
245
+ });
221
246
  const session = engine?.session({
222
247
  ctx,
223
248
  write,
@@ -227,8 +252,9 @@ var search = (opts = {}) => {
227
252
  });
228
253
  const applyQuery = (query, inp = input()) => {
229
254
  if (session?.apply(query, inp)) return;
230
- write(inp, query, "idle");
231
- settleWaiting(query, APPLIED);
255
+ if (held[0]?.query !== query) release(SUPERSEDED);
256
+ held.push(...claim(query));
257
+ if (!write(inp, query, "idle") && !busy) release(APPLIED);
232
258
  };
233
259
  let calling = false;
234
260
  let appliedNow;
@@ -241,7 +267,7 @@ var search = (opts = {}) => {
241
267
  instances.set(table, () => {
242
268
  timer?.cancel();
243
269
  session?.destroy();
244
- settleWaiting("\0", SUPERSEDED);
270
+ supersede();
245
271
  });
246
272
  const commit = (next) => {
247
273
  const current = applied();
@@ -289,10 +315,10 @@ var search = (opts = {}) => {
289
315
  cancelSearch: () => {
290
316
  timer?.cancel();
291
317
  session?.abort();
292
- settleWaiting("\0", SUPERSEDED);
318
+ supersede();
293
319
  const s = ctx.getState();
294
320
  if (s?.status === "pending" || s?.status === "searching") {
295
- ctx.setState({ input: s.input, query: s.query, status: "idle" });
321
+ ctx.setState({ ...s, status: "idle" });
296
322
  }
297
323
  },
298
324
  clearSearch: () => {