@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.
- package/dist/{codes-BKdS9EiO.d.cts → codes-OsjAR3Wz.d.cts} +16 -4
- package/dist/{codes-BKdS9EiO.d.ts → codes-OsjAR3Wz.d.ts} +16 -4
- package/dist/column-filters.d.cts +2 -2
- package/dist/column-filters.d.ts +2 -2
- package/dist/diagnostics/build.cjs +6 -3
- package/dist/diagnostics/build.cjs.map +1 -1
- package/dist/diagnostics/build.js +4 -2
- package/dist/diagnostics/build.js.map +1 -1
- package/dist/diagnostics/hub.cjs +1 -1
- package/dist/diagnostics/hub.cjs.map +1 -1
- package/dist/diagnostics/hub.js +2 -2
- package/dist/diagnostics/hub.js.map +1 -1
- package/dist/{explain-DcUZfFh5.d.ts → explain-Dn99IQt0.d.ts} +1 -1
- package/dist/{explain-aagaYHcy.d.cts → explain-vkD2cewE.d.cts} +1 -1
- package/dist/feature/define.cjs +3 -1
- package/dist/feature/define.cjs.map +1 -1
- package/dist/feature/define.js +3 -1
- package/dist/feature/define.js.map +1 -1
- package/dist/feature/runtime.cjs.map +1 -1
- package/dist/feature/runtime.js.map +1 -1
- package/dist/features/pagination/codes.cjs +5 -3
- package/dist/features/pagination/codes.cjs.map +1 -1
- package/dist/features/pagination/codes.js +5 -3
- package/dist/features/pagination/codes.js.map +1 -1
- package/dist/features/pagination/pagination.cjs +3 -1
- package/dist/features/pagination/pagination.cjs.map +1 -1
- package/dist/features/pagination/pagination.js +3 -1
- package/dist/features/pagination/pagination.js.map +1 -1
- package/dist/features/search/search-index.cjs +38 -34
- package/dist/features/search/search-index.cjs.map +1 -1
- package/dist/features/search/search-index.js +39 -35
- package/dist/features/search/search-index.js.map +1 -1
- package/dist/features/search/search.cjs +42 -16
- package/dist/features/search/search.cjs.map +1 -1
- package/dist/features/search/search.js +42 -16
- package/dist/features/search/search.js.map +1 -1
- package/dist/features/sorting/sorting.cjs +3 -3
- package/dist/features/sorting/sorting.cjs.map +1 -1
- package/dist/features/sorting/sorting.js +3 -3
- package/dist/features/sorting/sorting.js.map +1 -1
- package/dist/index.d.cts +9 -7
- package/dist/index.d.ts +9 -7
- package/dist/intercept/cell-batch.cjs +3 -0
- package/dist/intercept/cell-batch.cjs.map +1 -1
- package/dist/intercept/cell-batch.js +3 -0
- package/dist/intercept/cell-batch.js.map +1 -1
- package/dist/pagination.d.cts +2 -2
- package/dist/pagination.d.ts +2 -2
- package/dist/pipeline/row.cjs +8 -7
- package/dist/pipeline/row.cjs.map +1 -1
- package/dist/pipeline/row.js +8 -7
- package/dist/pipeline/row.js.map +1 -1
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/{search-async-entry-BmQYT120.d.ts → search-async-entry-DGSw2pDL.d.ts} +2 -2
- package/dist/{search-async-entry-C3mXxLP8.d.cts → search-async-entry-v5JdEk5Y.d.cts} +2 -2
- package/dist/search-async.d.cts +3 -3
- package/dist/search-async.d.ts +3 -3
- package/dist/search.d.cts +4 -4
- package/dist/search.d.ts +4 -4
- package/dist/server.d.cts +3 -3
- package/dist/server.d.ts +3 -3
- package/dist/sorting.d.cts +6 -3
- package/dist/sorting.d.ts +6 -3
- package/dist/validity.d.cts +2 -2
- package/dist/validity.d.ts +2 -2
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- 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 = () =>
|
|
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 =
|
|
51
|
-
const at = (k, r) =>
|
|
52
|
-
|
|
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
|
|
76
|
+
get(t, p) {
|
|
78
77
|
if (p === "length") return length();
|
|
79
78
|
const j = arrayIndex(p, length());
|
|
80
|
-
return j >= 0 ? item(j) :
|
|
79
|
+
return j >= 0 ? item(j) : t[p];
|
|
81
80
|
},
|
|
82
|
-
has: (t, p) => arrayIndex(p, length()) >= 0 ||
|
|
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
|
|
95
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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) =>
|
|
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
|
|
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) =>
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
338
|
+
supersede();
|
|
313
339
|
const s = ctx.getState();
|
|
314
340
|
if (s?.status === "pending" || s?.status === "searching") {
|
|
315
|
-
ctx.setState({
|
|
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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAAgC;AAChC,sBAAyB;AACzB,mBAAkC;AAClC,qBAA8B;AAC9B,mBAAiC;AACjC,qBAAgC;AAChC,oBAA8B;AAE9B,IAAAA,iBAAkC;AAClC,mBAA+B;AAC/B,kBAA0C;AAC1C,6BAWO;AACP,0BAA8E;AAAA,IAE9E,gCAAgB,8BAAiB;AA6EjC,IAAM,eAAe;AAErB,IAAM,aAAa,CAAC,MAAwB;AAC1C,MAAI,OAAO,MAAM,SAAU,QAAO,OAAO,SAAS,CAAC,KAAK,KAAK;AAC7D,aAAW,KAAK,CAAC,SAAS,WAAW,WAAW,IAAI,GAAY;AAC9D,UAAM,IAAK,EAA8B,CAAC;AAC1C,QAAI,MAAM,UAAa,EAAE,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,KAAK,KAAK,GAAI,QAAO;AAAA,EAC1F;AACA,SAAO;AACT;AAEA,IAAM,gBAAgB;AAAA,EACpB,UAAU,EAAE,MAAM,UAAU,OAAO,CAAC,aAAa,YAAY,UAAU,EAAE;AAAA,EACzE,MAAM;AAAA,IACJ,MAAM,CAAC,UAAU,QAAQ;AAAA,IACzB,OAAO;AAAA,IACP,UAAU;AAAA,IACV,UAAU,CAAC,GAAY,MACrB,EAAE,aAAa,UACX,oDACA,EAAE,aAAa,cAAc,EAAE,aAAa,aAC1C,uDACA;AAAA,EACV;AAAA,EACA,UAAU;AAAA,IACR,MAAM;AAAA,IACN,OAAO,CAAC,YAAY,SAAS;AAAA,IAC7B,UAAU,CAAC,GAAY,MACrB,EAAE,aAAa,UACX,oDACA,EAAE,aAAa,aACb,qDACA;AAAA,EACV;AAAA,EACA,WAAW;AAAA,IACT,MAAM;AAAA,IACN,OAAO,CAAC,MAAc,OAAO,UAAU,CAAC,KAAK,KAAK;AAAA,IAClD,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,EAAE,MAAM,UAAU,OAAO,CAAC,QAAQ,OAAO,EAAE;AACvD;AAGA,IAAM,WAAW,CAAC,GAAY,MAC5B,EAAE,aAAa,SAAY,SAAY;AAEzC,IAAM,gBAAgB;AAAA,EACpB,WAAW;AAAA,IACT,MAAM;AAAA,IACN,OAAO,CAAC,MAAc,KAAK,KAAK,KAAK;AAAA,IACrC,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AAAA,EACA,kBAAkB;AAAA,IAChB,MAAM;AAAA,IACN,OAAO,CAAC,MAAc,OAAO,UAAU,CAAC,KAAK,KAAK;AAAA,IAClD,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AAAA,EACA,eAAe,EAAE,MAAM,WAAW,UAAU,SAAS;AAAA,EACrD,QAAQ,EAAE,MAAM,UAAU,OAAO,cAAc;AAAA,EAC/C,MAAM;AAAA,EACN,UAAU;AAAA,IACR,MAAM;AAAA,IACN,OAAO,CAAC,MAAc,OAAQ,EAAoC,oCAAa,MAAM;AAAA,IACrF,UACE;AAAA,IAEF,UAAU,CAAC,GAAY,MACrB,EAAE,SAAS,WACP,iFACA;AAAA,EACR;AACF;AAEA,IAAM,oBAAoB,MAAM,QAAQ,QAAQ,iCAAU;AAE1D,IAAM,SAAS,CAAC,MAAwB,OAAO,MAAM,WAAW,IAAI,KAAK,OAAO,KAAK,OAAO,CAAC;AAatF,IAAM,SAAS,CAAa,OAAyB,CAAC,MAAM;AACjE,QAAM,SAAsB;AAAA,IAC1B,WAAW,KAAK,aAAa;AAAA,IAC7B,kBAAkB,KAAK,oBAAoB;AAAA,IAC3C,eAAe,KAAK,iBAAiB;AAAA,EACvC;AAKA,QAAM,UAAU,oBAAI,QAAyC;AAE7D,QAAM,QAAQ,oBAAI,QAAoC;AAEtD,MAAI;AAEJ,QAAM,WAAW,oBAAI,QAAgB;AAErC,MAAI;AAIJ,MAAIC;AAEJ,MAAI;AACJ,QAAM,SAAS,KAAK,SAAS;AAG7B,QAAM,SAAuB,SACzB,EAAE,UAAU,YAAY,WAAW,GAAG,GAAG,KAAK,OAAO,IACpD,KAAK,UAAU,CAAC;AACrB,QAAM,WAAW,OAAO,aAAa,UAAU,UAAW,OAAO,YAAY;AAC7E,QAAM,OAAO,OAAO,QAAQ;AAC5B,QAAM,YAAY,OAAO,aAAa;AAEtC,QAAM,SAAS,CAAC,UAAmB,MAAM,KAAK,EAAE,SAAS,YAAY,KAAK;AAE1E,QAAM,SAAmC,KAAK,WAAW,oCAAa,IAAI,KAAK,QAAiB;AAGhG,MAAI,EAAE,QAAQ,IAAI,aAAa,iBAAiB,KAAK,YAAY,CAAC,QAAQ;AACxE,uCAAiB,qBAAqB;AAAA,MACpC,MAAM;AAAA,MACN,SAAS;AAAA,MACT,UAAU;AAAA,MACV,UAAU;AAAA,MACV,aAAa,CAAC;AAAA,MACd,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,QAAM,YAAY,oBAAI,QAA4B;AAGlD,WAAS,SAAS,KAAsD;AACtE,QAAI,YAAY,QAAQ,IAAI,IAAI,IAAI;AACpC,QAAI,CAAC,WAAW;AACd,kBAAY,oBAAI,QAAQ;AACxB,cAAQ,IAAI,IAAI,MAAM,SAAS;AAAA,IACjC;AACA,QAAI,MAAM,UAAU,IAAI,IAAI,OAAO;AACnC,QAAI,QAAQ,QAAW;AACrB,YAAM,CAAC;AACP,gBAAU,IAAI,IAAI,SAAS,GAAG;AAAA,IAChC;AACA,UAAM,OAAO,MAAM,IAAI,GAAG;AAC1B,QAAI,SAAS,OAAW,QAAO;AAC/B,wBAAgB,mCAAc,IAAI,IAAI;AACtC,UAAM,WAAW;AACjB,UAAM,YAAQ,qBAAO,KAAK,KAAK,UAAM,gCAAW,KAAK,QAAQ,QAAQ,CAAC;AACtE,QAAI,UAAU,OAAW,OAAM,IAAI,KAAK,KAAK;AAC7C,WAAO;AAAA,EACT;AAGA,WAAS,SACP,KACA,OACA,MACA,OACyB;AAEzB,QAAI,CAAC,IAAI,MAAO,YAAO,qBAAO,MAAM,MAAM,MAAM,MAAM,KAAK,CAAC;AAC5D,QACE,CAACA,aACDA,UAAS,SAAS,QAClBA,UAAS,UAAU,SACnBA,UAAS,UAAU,OACnB;AACA,MAAAA,YAAW,EAAE,MAAM,OAAO,OAAO,KAAK,CAAC,EAAE;AAAA,IAC3C;AACA,eAAO,qBAAO,KAAKA,UAAS,KAAK,MAAM,MAAM,MAAM,MAAM,KAAK,CAAC;AAAA,EACjE;AAGA,QAAM,UAAU,CAAC,YACf,SAAS,MAAM,UAAU,UAAU,MAAM,SAAS;AAGpD,WAAS,KAAK,SAAsB,OAAoB,KAAmC;AACzF,UAAM,QAAQ,OAAO,OAAO,UAAU,WAAW,MAAM,MAAM,KAAK,IAAI;AAEtE,QAAI,UAAU,GAAI,QAAO;AACzB,QAAI,OAAQ,QAAO,OAAO,KAAK,SAAS,GAAG;AAC3C,UAAM,QAAQ,SAAS,GAAG;AAE1B,QAAI,UAAU,OAAW,QAAO,QAAQ,OAAO;AAC/C,QAAI,UAAU,MAAM;AAClB,UAAI,CAAC,SAAS,IAAI,IAAI,OAAO,GAAG;AAC9B,iBAAS,IAAI,IAAI,OAAO;AACxB,YAAI,KAAK,uBAAuB;AAAA,UAC9B,MAAM;AAAA,UACN,SAAS;AAAA,UACT,QACE;AAAA,UAEF,MAAM;AAAA,QACR,CAAC;AAAA,MACH;AACA,aAAO;AAAA,IACT;AACA,QAAI,QAAQ,KAAK,UAAU,WAAW,KAAK,UAAU,SAAS,KAAK,UAAU,OAAO;AAClF,aAAO,KAAK;AAAA,IACd;AAEA,UAAM,SAAS,SAAS,KAAK,OAAO,SAAS,KAAK;AAClD,QAAI,WAAW,OAAW,QAAO,QAAQ,OAAO;AAChD,WAAO,EAAE,OAAO,SAAS,OAAO,OAAO,OAAO;AAC9C,WAAO;AAAA,EACT;AAEA,aAAO,6BAAc;AAAA,IACnB,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM,KAAK,QAAQ;AAAA,IACnB,aAAS;AAAA;AAAA,MAEN,QAAQ,IAAI,aAAa,eAAgB,gBAAiB,CAAC;AAAA,MAC5D,SAAS,EAAE,GAAG,MAAM,OAAO,IAAI;AAAA,IACjC;AAAA,IACA,cAAc,OAAoB,EAAE,OAAO,IAAI,OAAO,IAAI,QAAQ,OAAO;AAAA;AAAA,IAEzE,iBAAiB,EAAE,WAAW,mBAAmB,aAAa,kBAAkB;AAAA,IAChF,cAAc;AAAA,UACZ,kCAAkB;AAAA,QAChB,MAAM;AAAA,QACN,IAAI,SAAS,CAAC,iBAAiB,cAAc,IAAI,CAAC,eAAe;AAAA,QACjE,UAAU,KAAK,MAAM;AACnB,gBAAM,QAAQ,IAAI;AAClB,cAAI,IAAI,OAAO,iBAAiB;AAC9B,sBAAU,IAAI,KAAK,IAAI;AACvB,mBAAO,KAAK;AAAA,UACd;AACA,iBAAQ,OAAwB,IAAI,OAAO,IAAI;AAAA,QACjD;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,WAAW,CAAC,QAAmB;AAC7B,YAAM,QAAQ,IAAI;AAClB,YAAM,QAAQ,CAACC,QAAe,OAAe,QAAsB,UAAoB;AACrF,cAAM,IAAI,IAAI,SAAS;AACvB,YACE,WAAW,WACX,GAAG,UAAUA,UACb,EAAE,UAAU,SACZ,EAAE,WAAW,UACb,EAAE,WAAW,IACb;AACA;AAAA,QACF;AACA,YAAI;AAAA,UACF,WAAW,UAAU,EAAE,OAAAA,QAAO,OAAO,QAAQ,MAAM,IAAI,EAAE,OAAAA,QAAO,OAAO,OAAO;AAAA,QAChF;AAAA,MACF;AACA,YAAM,UAAU,MAAM,IAAI,SAAS,GAAG,SAAS;AAC/C,YAAM,QAAQ,MAAM,IAAI,SAAS,GAAG,SAAS;AAI7C,UAAI,UAAoB,CAAC;AACzB,YAAM,UAAU,CAAC,UACf,IAAI,QAAuB,CAAC,YAAY;AACtC,gBAAQ,KAAK,EAAE,OAAO,QAAQ,CAAC;AAAA,MACjC,CAAC;AAEH,YAAM,gBAAgB,CAAC,OAAe,YAA2B;AAC/D,cAAM,OAAO;AACb,kBAAU,CAAC;AACX,mBAAW,KAAK,KAAM,GAAE,QAAQ,EAAE,UAAU,QAAQ,UAAU,iCAAU;AAAA,MAC1E;AAEA,YAAM,iBAAiB,CAAC,UAAkB;AACxC,YAAI,QAAQ,WAAW,EAAG;AAC1B,cAAM,OAAiB,CAAC;AACxB,mBAAW,KAAK,SAAS;AACvB,cAAI,EAAE,UAAU,MAAO,MAAK,KAAK,CAAC;AAAA,cAC7B,GAAE,QAAQ,iCAAU;AAAA,QAC3B;AACA,kBAAU;AAAA,MACZ;AAGA,YAAM,QAAQ,CAAC,UAAkB;AAC/B,uBAAe,KAAK;AACpB,cAAM,OAAO;AACb,kBAAU,CAAC;AACX,eAAO;AAAA,MACT;AACA,YAAM,UAAqC,QAAQ,QAAQ;AAAA,QACzD;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAMD,YAAM,aAAa,CAAC,OAAe,MAAc,MAAM,MAAM;AAC3D,YAAI,SAAS,MAAM,OAAO,GAAG,EAAG;AAChC,cAAM,KAAK,OAAO,MAAM;AACxB,sBAAc,OAAO,8BAAO;AAAA,MAC9B;AAIA,UAAI,UAAU;AACd,UAAI;AACJ,YAAM,QAAQ,CAAC,UAAkB;AAC/B,YAAI,QAAS,cAAa;AAAA,YACrB,YAAW,KAAK;AAAA,MACvB;AACA,YAAM,QACJ,aAAa,iBACT,uCAAgB,SAAS,OAAO,MAAM,EAAE,UAAU,OAAO,YAAY,WAAW,CAAC,IACjF,aAAa,iBACX,0BAAS,OAAO,IAAI,IACpB;AAER,YAAM,UAAU,MAAqB,SAAS,KAAK,IAAI,cAAc;AAErE,gBAAU,IAAI,OAAO,MAAM;AACzB,eAAO,OAAO;AACd,iBAAS,QAAQ;AACjB,sBAAc,MAAM,iCAAU;AAAA,MAChC,CAAC;AAGD,YAAM,SAAS,CAAC,SAAyC;AACvD,cAAM,UAAU,QAAQ;AACxB,cAAM,QAAQ,OAAO,IAAI;AACzB,uBAAe,KAAK;AACpB,cAAM,IAAI,QAAQ,KAAK;AACvB,YAAI,aAAa,SAAS;AACxB,gBAAM,MAAM,SAAS,QAAQ,CAAC;AAC9B,iBAAO;AAAA,QACT;AACA,YAAI,CAAC,SAAS,UAAU,SAAS;AAC/B,iBAAO,OAAO;AACd,qBAAW,OAAO,IAAI;AACtB,iBAAO;AAAA,QACT;AACA,kBAAU;AACV,qBAAa;AACb,YAAI;AACF,gBAAM,KAAK;AAAA,QACb,UAAE;AACA,oBAAU;AAAA,QACZ;AACA,YAAI,eAAe,QAAW;AAC5B,qBAAW,YAAY,IAAI;AAC3B,iBAAO;AAAA,QACT;AAGA,YAAI,aAAa,cAAc,OAAO,aAAa,WAAW;AAC5D,gBAAM,MAAM,SAAS,QAAQ,CAAC;AAC9B,iBAAO;AAAA,QACT;AACA,cAAM,MAAM,SAAS,SAAS;AAC9B,eAAO;AAAA,MACT;AAEA,YAAM,WAAW,CAAC,SAAyC;AACzD,eAAO,OAAO;AACd,cAAM,QAAQ,OAAO,IAAI;AACzB,uBAAe,KAAK;AACpB,cAAM,IAAI,QAAQ,KAAK;AACvB,mBAAW,OAAO,IAAI;AACtB,eAAO;AAAA,MACT;AACA,aAAO;AAAA,QACL,WAAW,CAAC,UAAU,OAAO,OAAO,KAAK,CAAC;AAAA,QAC1C,aAAa,MAAM,SAAS,MAAM,CAAC;AAAA,QACnC,cAAc,MAAM;AAClB,iBAAO,OAAO;AACd,mBAAS,MAAM;AACf,wBAAc,MAAM,iCAAU;AAC9B,gBAAM,IAAI,IAAI,SAAS;AACvB,cAAI,GAAG,WAAW,aAAa,GAAG,WAAW,aAAa;AACxD,gBAAI,SAAS,EAAE,OAAO,EAAE,OAAO,OAAO,EAAE,OAAO,QAAQ,OAAO,CAAC;AAAA,UACjE;AAAA,QACF;AAAA,QACA,aAAa,MAAM;AACjB,eAAK,SAAS,EAAE;AAAA,QAClB;AAAA,MACF;AAAA,IACF;AAAA,IACA,UAAU,CAAC,SAAS,OAAO,QAAQ;AACjC,YAAM,SAAS,KAAK,SAAS,OAAO,GAAG;AACvC,cAAQ,EAAE,OAAO,SAAS,OAAO;AACjC,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;","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) =>
|
|
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
|
|
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) =>
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
318
|
+
supersede();
|
|
293
319
|
const s = ctx.getState();
|
|
294
320
|
if (s?.status === "pending" || s?.status === "searching") {
|
|
295
|
-
ctx.setState({
|
|
321
|
+
ctx.setState({ ...s, status: "idle" });
|
|
296
322
|
}
|
|
297
323
|
},
|
|
298
324
|
clearSearch: () => {
|