@datakable/core 1.0.0 → 1.1.0

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 (290) hide show
  1. package/README.md +4 -2
  2. package/dist/catalog/codes.cjs +12 -8
  3. package/dist/catalog/codes.cjs.map +1 -1
  4. package/dist/catalog/codes.js +12 -8
  5. package/dist/catalog/codes.js.map +1 -1
  6. package/dist/catalog/slots.cjs +10 -6
  7. package/dist/catalog/slots.cjs.map +1 -1
  8. package/dist/catalog/slots.js +10 -6
  9. package/dist/catalog/slots.js.map +1 -1
  10. package/dist/{codes-BKdS9EiO.d.cts → codes-B9bCK73-.d.cts} +46 -8
  11. package/dist/{codes-BKdS9EiO.d.ts → codes-B9bCK73-.d.ts} +46 -8
  12. package/dist/column/column-def.cjs.map +1 -1
  13. package/dist/column/get-value.cjs +6 -1
  14. package/dist/column/get-value.cjs.map +1 -1
  15. package/dist/column/get-value.js +6 -1
  16. package/dist/column/get-value.js.map +1 -1
  17. package/dist/column-filters.d.cts +2 -2
  18. package/dist/column-filters.d.ts +2 -2
  19. package/dist/diagnostics/build.cjs +10 -4
  20. package/dist/diagnostics/build.cjs.map +1 -1
  21. package/dist/diagnostics/build.js +8 -3
  22. package/dist/diagnostics/build.js.map +1 -1
  23. package/dist/diagnostics/catalog.cjs +11 -5
  24. package/dist/diagnostics/catalog.cjs.map +1 -1
  25. package/dist/diagnostics/catalog.js +10 -5
  26. package/dist/diagnostics/catalog.js.map +1 -1
  27. package/dist/diagnostics/codes.cjs +12 -8
  28. package/dist/diagnostics/codes.cjs.map +1 -1
  29. package/dist/diagnostics/codes.js +12 -8
  30. package/dist/diagnostics/codes.js.map +1 -1
  31. package/dist/diagnostics/console.cjs +13 -6
  32. package/dist/diagnostics/console.cjs.map +1 -1
  33. package/dist/diagnostics/console.js +13 -6
  34. package/dist/diagnostics/console.js.map +1 -1
  35. package/dist/diagnostics/env.cjs +1 -0
  36. package/dist/diagnostics/env.cjs.map +1 -1
  37. package/dist/diagnostics/env.js +1 -0
  38. package/dist/diagnostics/env.js.map +1 -1
  39. package/dist/diagnostics/explain-text.cjs +2 -1
  40. package/dist/diagnostics/explain-text.cjs.map +1 -1
  41. package/dist/diagnostics/explain-text.js +2 -1
  42. package/dist/diagnostics/explain-text.js.map +1 -1
  43. package/dist/diagnostics/explain.cjs +3 -2
  44. package/dist/diagnostics/explain.cjs.map +1 -1
  45. package/dist/diagnostics/explain.js +3 -2
  46. package/dist/diagnostics/explain.js.map +1 -1
  47. package/dist/diagnostics/hub.cjs +6 -5
  48. package/dist/diagnostics/hub.cjs.map +1 -1
  49. package/dist/diagnostics/hub.js +7 -6
  50. package/dist/diagnostics/hub.js.map +1 -1
  51. package/dist/diagnostics/process-guard.cjs +26 -0
  52. package/dist/diagnostics/process-guard.cjs.map +1 -0
  53. package/dist/diagnostics/process-guard.js +9 -0
  54. package/dist/diagnostics/process-guard.js.map +1 -0
  55. package/dist/diagnostics/prose.cjs +120 -0
  56. package/dist/diagnostics/prose.cjs.map +1 -0
  57. package/dist/diagnostics/prose.js +93 -0
  58. package/dist/diagnostics/prose.js.map +1 -0
  59. package/dist/diagnostics/redact.cjs +7 -4
  60. package/dist/diagnostics/redact.cjs.map +1 -1
  61. package/dist/diagnostics/redact.js +6 -4
  62. package/dist/diagnostics/redact.js.map +1 -1
  63. package/dist/diagnostics/trace-store.cjs +6 -0
  64. package/dist/diagnostics/trace-store.cjs.map +1 -1
  65. package/dist/diagnostics/trace-store.js +4 -0
  66. package/dist/diagnostics/trace-store.js.map +1 -1
  67. package/dist/diagnostics/tracer.cjs +74 -13
  68. package/dist/diagnostics/tracer.cjs.map +1 -1
  69. package/dist/diagnostics/tracer.js +75 -14
  70. package/dist/diagnostics/tracer.js.map +1 -1
  71. package/dist/diagnostics/types.cjs.map +1 -1
  72. package/dist/{explain-aagaYHcy.d.cts → explain-Q8Cg0cZy.d.cts} +13 -4
  73. package/dist/{explain-DcUZfFh5.d.ts → explain-i0OATi_W.d.ts} +13 -4
  74. package/dist/feature/codes.cjs +1 -0
  75. package/dist/feature/codes.cjs.map +1 -1
  76. package/dist/feature/codes.js +1 -0
  77. package/dist/feature/codes.js.map +1 -1
  78. package/dist/feature/define.cjs +3 -1
  79. package/dist/feature/define.cjs.map +1 -1
  80. package/dist/feature/define.js +3 -1
  81. package/dist/feature/define.js.map +1 -1
  82. package/dist/feature/install.cjs +4 -3
  83. package/dist/feature/install.cjs.map +1 -1
  84. package/dist/feature/install.js +4 -3
  85. package/dist/feature/install.js.map +1 -1
  86. package/dist/feature/runtime.cjs.map +1 -1
  87. package/dist/feature/runtime.js.map +1 -1
  88. package/dist/features/column-filters/codes.cjs +1 -0
  89. package/dist/features/column-filters/codes.cjs.map +1 -1
  90. package/dist/features/column-filters/codes.js +1 -0
  91. package/dist/features/column-filters/codes.js.map +1 -1
  92. package/dist/features/column-filters/column-filters.cjs +4 -2
  93. package/dist/features/column-filters/column-filters.cjs.map +1 -1
  94. package/dist/features/column-filters/column-filters.js +4 -2
  95. package/dist/features/column-filters/column-filters.js.map +1 -1
  96. package/dist/features/column-filters/match.cjs +5 -5
  97. package/dist/features/column-filters/match.cjs.map +1 -1
  98. package/dist/features/column-filters/match.js +5 -5
  99. package/dist/features/column-filters/match.js.map +1 -1
  100. package/dist/features/pagination/codes.cjs +6 -3
  101. package/dist/features/pagination/codes.cjs.map +1 -1
  102. package/dist/features/pagination/codes.js +6 -3
  103. package/dist/features/pagination/codes.js.map +1 -1
  104. package/dist/features/pagination/pagination.cjs +19 -14
  105. package/dist/features/pagination/pagination.cjs.map +1 -1
  106. package/dist/features/pagination/pagination.js +19 -14
  107. package/dist/features/pagination/pagination.js.map +1 -1
  108. package/dist/features/search/codes.cjs +1 -0
  109. package/dist/features/search/codes.cjs.map +1 -1
  110. package/dist/features/search/codes.js +1 -0
  111. package/dist/features/search/codes.js.map +1 -1
  112. package/dist/features/search/search-index.cjs +105 -48
  113. package/dist/features/search/search-index.cjs.map +1 -1
  114. package/dist/features/search/search-index.js +106 -49
  115. package/dist/features/search/search-index.js.map +1 -1
  116. package/dist/features/search/search.cjs +82 -24
  117. package/dist/features/search/search.cjs.map +1 -1
  118. package/dist/features/search/search.js +82 -24
  119. package/dist/features/search/search.js.map +1 -1
  120. package/dist/features/search/skipped-codes.cjs +45 -0
  121. package/dist/features/search/skipped-codes.cjs.map +1 -0
  122. package/dist/features/search/skipped-codes.js +20 -0
  123. package/dist/features/search/skipped-codes.js.map +1 -0
  124. package/dist/features/sorting/codes.cjs +1 -0
  125. package/dist/features/sorting/codes.cjs.map +1 -1
  126. package/dist/features/sorting/codes.js +1 -0
  127. package/dist/features/sorting/codes.js.map +1 -1
  128. package/dist/features/sorting/sorting.cjs +19 -23
  129. package/dist/features/sorting/sorting.cjs.map +1 -1
  130. package/dist/features/sorting/sorting.js +19 -23
  131. package/dist/features/sorting/sorting.js.map +1 -1
  132. package/dist/features/sorting/strategy.cjs +4 -4
  133. package/dist/features/sorting/strategy.cjs.map +1 -1
  134. package/dist/features/sorting/strategy.js +4 -4
  135. package/dist/features/sorting/strategy.js.map +1 -1
  136. package/dist/features/validity/codes.cjs +1 -0
  137. package/dist/features/validity/codes.cjs.map +1 -1
  138. package/dist/features/validity/codes.js +1 -0
  139. package/dist/features/validity/codes.js.map +1 -1
  140. package/dist/features/validity/validity.cjs +3 -3
  141. package/dist/features/validity/validity.cjs.map +1 -1
  142. package/dist/features/validity/validity.js +3 -3
  143. package/dist/features/validity/validity.js.map +1 -1
  144. package/dist/index.d.cts +16 -11
  145. package/dist/index.d.ts +16 -11
  146. package/dist/ingest/data-check-codes.cjs +4 -2
  147. package/dist/ingest/data-check-codes.cjs.map +1 -1
  148. package/dist/ingest/data-check-codes.js +4 -2
  149. package/dist/ingest/data-check-codes.js.map +1 -1
  150. package/dist/ingest/data-checks.cjs +1 -1
  151. package/dist/ingest/data-checks.cjs.map +1 -1
  152. package/dist/ingest/data-checks.js +1 -1
  153. package/dist/ingest/data-checks.js.map +1 -1
  154. package/dist/ingest/row-key-codes.cjs +1 -0
  155. package/dist/ingest/row-key-codes.cjs.map +1 -1
  156. package/dist/ingest/row-key-codes.js +1 -0
  157. package/dist/ingest/row-key-codes.js.map +1 -1
  158. package/dist/ingest/row-key.cjs +23 -20
  159. package/dist/ingest/row-key.cjs.map +1 -1
  160. package/dist/ingest/row-key.js +23 -20
  161. package/dist/ingest/row-key.js.map +1 -1
  162. package/dist/ingest/schema-codes.cjs +1 -0
  163. package/dist/ingest/schema-codes.cjs.map +1 -1
  164. package/dist/ingest/schema-codes.js +1 -0
  165. package/dist/ingest/schema-codes.js.map +1 -1
  166. package/dist/ingest/unique-fields.cjs +2 -2
  167. package/dist/ingest/unique-fields.cjs.map +1 -1
  168. package/dist/ingest/unique-fields.js +2 -2
  169. package/dist/ingest/unique-fields.js.map +1 -1
  170. package/dist/intercept/boundary.cjs +4 -2
  171. package/dist/intercept/boundary.cjs.map +1 -1
  172. package/dist/intercept/boundary.js +4 -2
  173. package/dist/intercept/boundary.js.map +1 -1
  174. package/dist/intercept/cell-batch.cjs +7 -4
  175. package/dist/intercept/cell-batch.cjs.map +1 -1
  176. package/dist/intercept/cell-batch.js +7 -4
  177. package/dist/intercept/cell-batch.js.map +1 -1
  178. package/dist/intercept/codes.cjs +1 -0
  179. package/dist/intercept/codes.cjs.map +1 -1
  180. package/dist/intercept/codes.js +1 -0
  181. package/dist/intercept/codes.js.map +1 -1
  182. package/dist/intercept/fallbacks.cjs +12 -20
  183. package/dist/intercept/fallbacks.cjs.map +1 -1
  184. package/dist/intercept/fallbacks.js +12 -20
  185. package/dist/intercept/fallbacks.js.map +1 -1
  186. package/dist/intercept/runtime.cjs +7 -9
  187. package/dist/intercept/runtime.cjs.map +1 -1
  188. package/dist/intercept/runtime.js +7 -9
  189. package/dist/intercept/runtime.js.map +1 -1
  190. package/dist/messages/index.cjs +4 -1
  191. package/dist/messages/index.cjs.map +1 -1
  192. package/dist/messages/index.js +4 -1
  193. package/dist/messages/index.js.map +1 -1
  194. package/dist/pagination.d.cts +2 -2
  195. package/dist/pagination.d.ts +2 -2
  196. package/dist/pipeline/codes.cjs +1 -0
  197. package/dist/pipeline/codes.cjs.map +1 -1
  198. package/dist/pipeline/codes.js +1 -0
  199. package/dist/pipeline/codes.js.map +1 -1
  200. package/dist/pipeline/column-cache.cjs +94 -76
  201. package/dist/pipeline/column-cache.cjs.map +1 -1
  202. package/dist/pipeline/column-cache.js +92 -75
  203. package/dist/pipeline/column-cache.js.map +1 -1
  204. package/dist/pipeline/dev-checks.cjs +1 -0
  205. package/dist/pipeline/dev-checks.cjs.map +1 -1
  206. package/dist/pipeline/dev-checks.js +1 -0
  207. package/dist/pipeline/dev-checks.js.map +1 -1
  208. package/dist/pipeline/env-codes.cjs +3 -2
  209. package/dist/pipeline/env-codes.cjs.map +1 -1
  210. package/dist/pipeline/env-codes.js +3 -2
  211. package/dist/pipeline/env-codes.js.map +1 -1
  212. package/dist/pipeline/perf-codes.cjs +1 -0
  213. package/dist/pipeline/perf-codes.cjs.map +1 -1
  214. package/dist/pipeline/perf-codes.js +1 -0
  215. package/dist/pipeline/perf-codes.js.map +1 -1
  216. package/dist/pipeline/row.cjs +8 -7
  217. package/dist/pipeline/row.cjs.map +1 -1
  218. package/dist/pipeline/row.js +8 -7
  219. package/dist/pipeline/row.js.map +1 -1
  220. package/dist/pipeline/runner.cjs +17 -9
  221. package/dist/pipeline/runner.cjs.map +1 -1
  222. package/dist/pipeline/runner.js +17 -9
  223. package/dist/pipeline/runner.js.map +1 -1
  224. package/dist/pipeline/slice.cjs +3 -1
  225. package/dist/pipeline/slice.cjs.map +1 -1
  226. package/dist/pipeline/slice.js +3 -1
  227. package/dist/pipeline/slice.js.map +1 -1
  228. package/dist/schema.d.cts +2 -2
  229. package/dist/schema.d.ts +2 -2
  230. package/dist/{search-async-entry-C3mXxLP8.d.cts → search-async-entry-CuLYPfY_.d.cts} +2 -2
  231. package/dist/{search-async-entry-BmQYT120.d.ts → search-async-entry-Cx7xpuVZ.d.ts} +2 -2
  232. package/dist/search-async.d.cts +3 -3
  233. package/dist/search-async.d.ts +3 -3
  234. package/dist/search.d.cts +8 -6
  235. package/dist/search.d.ts +8 -6
  236. package/dist/server.d.cts +3 -3
  237. package/dist/server.d.ts +3 -3
  238. package/dist/sorting.d.cts +6 -3
  239. package/dist/sorting.d.ts +6 -3
  240. package/dist/source/codes.cjs +1 -0
  241. package/dist/source/codes.cjs.map +1 -1
  242. package/dist/source/codes.js +1 -0
  243. package/dist/source/codes.js.map +1 -1
  244. package/dist/source/fetch-codes.cjs +1 -0
  245. package/dist/source/fetch-codes.cjs.map +1 -1
  246. package/dist/source/fetch-codes.js +1 -0
  247. package/dist/source/fetch-codes.js.map +1 -1
  248. package/dist/table/codes.cjs +1 -0
  249. package/dist/table/codes.cjs.map +1 -1
  250. package/dist/table/codes.js +1 -0
  251. package/dist/table/codes.js.map +1 -1
  252. package/dist/table/create-table.cjs +39 -9
  253. package/dist/table/create-table.cjs.map +1 -1
  254. package/dist/table/create-table.js +39 -9
  255. package/dist/table/create-table.js.map +1 -1
  256. package/dist/table/data-mutation.cjs +1 -0
  257. package/dist/table/data-mutation.cjs.map +1 -1
  258. package/dist/table/data-mutation.js +1 -0
  259. package/dist/table/data-mutation.js.map +1 -1
  260. package/dist/table/internals.cjs.map +1 -1
  261. package/dist/table/internals.js.map +1 -1
  262. package/dist/table/ssr-codes.cjs +1 -0
  263. package/dist/table/ssr-codes.cjs.map +1 -1
  264. package/dist/table/ssr-codes.js +1 -0
  265. package/dist/table/ssr-codes.js.map +1 -1
  266. package/dist/table/ssr.cjs +4 -1
  267. package/dist/table/ssr.cjs.map +1 -1
  268. package/dist/table/ssr.js +4 -1
  269. package/dist/table/ssr.js.map +1 -1
  270. package/dist/table/state-feedback.cjs +19 -0
  271. package/dist/table/state-feedback.cjs.map +1 -1
  272. package/dist/table/state-feedback.js +19 -0
  273. package/dist/table/state-feedback.js.map +1 -1
  274. package/dist/table/types.cjs.map +1 -1
  275. package/dist/table/types.js.map +1 -1
  276. package/dist/validity.d.cts +2 -2
  277. package/dist/validity.d.ts +2 -2
  278. package/dist/version.cjs +1 -1
  279. package/dist/version.cjs.map +1 -1
  280. package/dist/version.js +1 -1
  281. package/dist/version.js.map +1 -1
  282. package/dist/versioning/codes.cjs +18 -0
  283. package/dist/versioning/codes.cjs.map +1 -1
  284. package/dist/versioning/codes.js +18 -0
  285. package/dist/versioning/codes.js.map +1 -1
  286. package/dist/versioning/registry.cjs +4 -2
  287. package/dist/versioning/registry.cjs.map +1 -1
  288. package/dist/versioning/registry.js +4 -2
  289. package/dist/versioning/registry.js.map +1 -1
  290. package/package.json +39 -3
@@ -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 { getValue } from '../../column/get-value'\nimport '../../diagnostics/process-guard'\nimport type { Emit } from '../../diagnostics/types'\nimport type { RowModelContext } from '../../feature/types'\nimport { nullishOf, type SlicedFormatting } from '../../pipeline/column-cache'\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 a scan (one value per row). */\nconst CHUNK = 4096\n\n/**\n * Cells (rows × searchable columns) matched per chunk between two `yield`s (#40): a table with\n * many searchable columns matches fewer rows per chunk, so a chunk stays a few milliseconds.\n */\nconst CELLS = 16_384\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 cached strings (#27): what the default formatter\n * shows, `String(v)`, or `''` for a `null` / `undefined` cell (stored as `NaN`, like a real `NaN`,\n * which shows `'NaN'`).\n */\nfunction numberReader(values: Float64Array): Read {\n const nullish = nullishOf(values)\n return (r) => {\n const v = values[r] as number\n return !Number.isNaN(v) ? String(v) : nullish?.[r] ? '' : 'NaN'\n }\n}\n\n/**\n * One pass over a column's values (`get(r)` per row, time-sliced by `CHUNK` rows): their text as\n * the default formatter shows a primitive (`String(v)`, `''` when nullish), read while matching\n * (#27: no formatted copy per row). `false` at the first object or function (a `Date`, an array):\n * the column cache formats those. `undefined` when a read throws. The values are read in NFC (#30),\n * normalised here, never per keystroke: the rows whose value is not NFC (rare: NFD input from\n * macOS, Thai or Vietnamese keyboards) keep their NFC form in a sparse map. Only values with a code\n * unit from U+0300 (combining marks and up) can change.\n */\nfunction* scan(\n size: number,\n get: (r: number) => unknown,\n): Generator<undefined, Read | false | undefined, undefined> {\n const over = new Map<number, string>()\n try {\n for (let r = 0; r < size; r++) {\n const v = get(r)\n if (v !== null && (typeof v === 'object' || typeof v === 'function')) return false\n if (typeof v === 'string' && /[\\u0300-\\uffff]/.test(v)) {\n const n = v.normalize('NFC')\n if (n !== v) over.set(r, n)\n }\n if (r % CHUNK === CHUNK - 1) yield\n }\n } catch {\n return undefined\n }\n const read: Read = (r) => {\n const v = get(r)\n return v === null || v === undefined ? '' : String(v)\n }\n return over.size ? (r) => over.get(r) ?? read(r) : read\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`, the columns of primitives\n * (strings, numbers, booleans, nullable) are searchable at weight 1, and a column of dates, arrays\n * or other objects is not (#27: it would keep a formatted string per row; it opts in with a\n * weight; `DK_SEARCH_COLUMN_SKIPPED` in development). `null` when nothing is searchable. Nothing\n * is copied per row (§9.3 memory): a key column of primitives without `format` is read from the\n * rows and stringified while matching (no values array, #27); an accessor column of primitives\n * reads the column cache's values the same way; only a column with `format` or an opted-in object\n * column (dates, arrays) keeps its formatted strings in the column cache. A key column whose read\n * throws goes through the column cache too, which reports it. The case is folded per chunk while\n * 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 data = ctx.data\n const n = data.length\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 const path = col.format ? undefined : col.path\n let failed = false\n const onError = () => {\n failed = true\n }\n let read = path ? yield* scan(n, (r) => getValue(data[r], path, onError)) : undefined\n if (failed || (read === false && declared)) read = undefined\n if (read === undefined) {\n const values = ctx.getColumnValues(col.id)\n if (!values) continue\n yield // reading a column is a task of its own (#40)\n const numbers = values instanceof Float64Array\n if (numbers && !col.format) read = numberReader(values)\n else {\n // Zero-config: a column of objects is not searched, `format` or not (#27).\n const own =\n numbers || (declared && col.format) ? undefined : yield* scan(n, (r) => values[r])\n if (own === false && !declared) read = false\n else if (own && !col.format) read = own\n else {\n // Time-sliced in a table run (#40): dates or a heavy `format` are not one long task.\n const sliced = (ctx as RowModelContext & SlicedFormatting).formattedSliced\n const formatted = sliced ? yield* sliced(col.id) : ctx.getFormattedValues(col.id)\n if (!formatted) continue\n read = yield* scan(n, (r) => formatted[r])\n }\n }\n }\n if (!read) {\n if ((process.env.NODE_ENV !== \"production\") && read === false) {\n ctx.emit('DK_SEARCH_COLUMN_SKIPPED', { column: col.id, index: col.index })\n }\n continue\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 (NFC already) are folded per chunk while matching; the query is normalised as well, so\n // NFC and NFD spellings match each other like in the `contains` filter (#30).\n const fold = (v: string) => (config.caseSensitive ? v : v.toLowerCase())\n const foldQuery = (q: string) => fold(q.normalize('NFC'))\n const at = (k: number, r: number) => fold((reads[k] as Read)(r))\n return useExSearch\n ? exSearchIndex(ctx.data.length, keys, at, foldQuery, config)\n : containsIndex(keys, at, foldQuery, config)\n}\n\n/** Matches `rows` chunk by chunk (`score` decides alone, so chunks match like the whole). */\nfunction* chunked(\n rows: Uint32Array,\n columns: number,\n matchChunk: (chunk: Uint32Array) => Hit[],\n): Generator<undefined, Uint32Array, undefined> {\n const size = Math.max(256, Math.floor(CELLS / columns))\n if (rows.length <= size) return rankHits(matchChunk(rows))\n const hits: Hit[] = []\n for (let k = 0; k < rows.length; k += size) {\n for (const h of matchChunk(rows.subarray(k, k + size))) 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, keys.length, (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, keys.length, (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":[]}
@@ -1,17 +1,23 @@
1
1
  // src/features/search/search-index.ts
2
- import { ExSearch } from "ex-search";
2
+ import { ExSearch, Scorer } from "ex-search";
3
+ import { getValue } from "../../column/get-value.js";
4
+ import "../../diagnostics/process-guard.js";
5
+ import { nullishOf } from "../../pipeline/column-cache.js";
3
6
  import { resolveColumns } from "../../pipeline/columns.js";
4
7
  import { arrayIndex } from "../../pipeline/row.js";
5
8
  import { detectCapability, hasCapability } from "../../versioning/capability.js";
6
9
  var CHUNK = 4096;
7
- var hasExSearch = () => typeof ExSearch === "function" && typeof ExSearch.prototype.search === "function" && typeof ExSearch.prototype.setData === "function";
10
+ var CELLS = 16384;
11
+ var hasExSearch = () => [ExSearch.prototype.search, ExSearch.prototype.setData, Scorer.fuzzy].every(
12
+ (f) => typeof f === "function"
13
+ );
8
14
  function probeExSearch(emit) {
9
15
  return process.env.NODE_ENV !== "production" ? detectCapability(
10
16
  hasExSearch,
11
17
  {
12
18
  feature: "search",
13
19
  dependency: "ex-search",
14
- capability: "ExSearch",
20
+ capability: "ExSearch, Scorer.fuzzy",
15
21
  supported: "^1.1.1",
16
22
  fallback: "case-insensitive exact / prefix / substring matching (no fuzzy matching)"
17
23
  },
@@ -19,12 +25,38 @@ function probeExSearch(emit) {
19
25
  ) : hasCapability(hasExSearch);
20
26
  }
21
27
  function numberReader(values) {
22
- for (let r = 0; r < values.length; r++) if (Number.isNaN(values[r])) return void 0;
23
- return (r) => String(values[r]);
28
+ const nullish = nullishOf(values);
29
+ return (r) => {
30
+ const v = values[r];
31
+ return !Number.isNaN(v) ? String(v) : nullish?.[r] ? "" : "NaN";
32
+ };
33
+ }
34
+ function* scan(size, get) {
35
+ const over = /* @__PURE__ */ new Map();
36
+ try {
37
+ for (let r = 0; r < size; r++) {
38
+ const v = get(r);
39
+ if (v !== null && (typeof v === "object" || typeof v === "function")) return false;
40
+ if (typeof v === "string" && /[\u0300-\uffff]/.test(v)) {
41
+ const n = v.normalize("NFC");
42
+ if (n !== v) over.set(r, n);
43
+ }
44
+ if (r % CHUNK === CHUNK - 1) yield;
45
+ }
46
+ } catch {
47
+ return void 0;
48
+ }
49
+ const read = (r) => {
50
+ const v = get(r);
51
+ return v === null || v === void 0 ? "" : String(v);
52
+ };
53
+ return over.size ? (r) => over.get(r) ?? read(r) : read;
24
54
  }
25
55
  function* buildIndex(ctx, config, useExSearch) {
26
56
  const cols = resolveColumns(ctx.columns);
27
57
  const declared = cols.list.some((c) => c.def.search !== void 0);
58
+ const data = ctx.data;
59
+ const n = data.length;
28
60
  const keys = [];
29
61
  const reads = [];
30
62
  for (const col of cols.list) {
@@ -32,33 +64,53 @@ function* buildIndex(ctx, config, useExSearch) {
32
64
  const w = col.def.search;
33
65
  const weight = declared ? typeof w === "number" && w > 0 ? w : 0 : 1;
34
66
  if (weight === 0) continue;
35
- let read;
36
- if (!col.format) {
67
+ const path = col.format ? void 0 : col.path;
68
+ let failed = false;
69
+ const onError = () => {
70
+ failed = true;
71
+ };
72
+ let read = path ? yield* scan(n, (r) => getValue(data[r], path, onError)) : void 0;
73
+ if (failed || read === false && declared) read = void 0;
74
+ if (read === void 0) {
37
75
  const values = ctx.getColumnValues(col.id);
38
- if (values instanceof Float64Array) read = numberReader(values);
76
+ if (!values) continue;
77
+ yield;
78
+ const numbers = values instanceof Float64Array;
79
+ if (numbers && !col.format) read = numberReader(values);
80
+ else {
81
+ const own = numbers || declared && col.format ? void 0 : yield* scan(n, (r) => values[r]);
82
+ if (own === false && !declared) read = false;
83
+ else if (own && !col.format) read = own;
84
+ else {
85
+ const sliced = ctx.formattedSliced;
86
+ const formatted = sliced ? yield* sliced(col.id) : ctx.getFormattedValues(col.id);
87
+ if (!formatted) continue;
88
+ read = yield* scan(n, (r) => formatted[r]);
89
+ }
90
+ }
39
91
  }
40
92
  if (!read) {
41
- const formatted = ctx.getFormattedValues(col.id);
42
- if (!formatted) continue;
43
- read = (r) => formatted[r] ?? "";
93
+ if (process.env.NODE_ENV !== "production" && read === false) {
94
+ ctx.emit("DK_SEARCH_COLUMN_SKIPPED", { column: col.id, index: col.index });
95
+ }
96
+ continue;
44
97
  }
45
98
  keys.push({ name: `c${keys.length}`, weight });
46
99
  reads.push(read);
47
100
  yield;
48
101
  }
49
102
  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);
103
+ const fold = (v) => config.caseSensitive ? v : v.toLowerCase();
104
+ const foldQuery = (q) => fold(q.normalize("NFC"));
105
+ const at = (k, r) => fold(reads[k](r));
106
+ return useExSearch ? exSearchIndex(ctx.data.length, keys, at, foldQuery, config) : containsIndex(keys, at, foldQuery, config);
56
107
  }
57
- function* chunked(rows, matchChunk) {
58
- if (rows.length <= CHUNK) return rankHits(matchChunk(rows));
108
+ function* chunked(rows, columns, matchChunk) {
109
+ const size = Math.max(256, Math.floor(CELLS / columns));
110
+ if (rows.length <= size) return rankHits(matchChunk(rows));
59
111
  const hits = [];
60
- for (let k = 0; k < rows.length; k += CHUNK) {
61
- for (const h of matchChunk(rows.subarray(k, k + CHUNK))) hits.push(h);
112
+ for (let k = 0; k < rows.length; k += size) {
113
+ for (const h of matchChunk(rows.subarray(k, k + size))) hits.push(h);
62
114
  yield;
63
115
  }
64
116
  return rankHits(hits);
@@ -74,12 +126,12 @@ function projection(size, keys, at) {
74
126
  return o;
75
127
  };
76
128
  const items = new Proxy(target, {
77
- get(t, p, receiver) {
129
+ get(t, p) {
78
130
  if (p === "length") return length();
79
131
  const j = arrayIndex(p, length());
80
- return j >= 0 ? item(j) : Reflect.get(t, p, receiver);
132
+ return j >= 0 ? item(j) : t[p];
81
133
  },
82
- has: (t, p) => arrayIndex(p, length()) >= 0 || Reflect.has(t, p)
134
+ has: (t, p) => arrayIndex(p, length()) >= 0 || p in t
83
135
  });
84
136
  return {
85
137
  items,
@@ -89,22 +141,39 @@ function projection(size, keys, at) {
89
141
  }
90
142
  };
91
143
  }
92
- function exSearchIndex(size, keys, at, config) {
144
+ function exSearchIndex(size, keys, at, fold, config) {
93
145
  const engine = new ExSearch({
94
- keys: keys.map((k) => ({ name: k.name, weight: k.weight })),
95
- threshold: config.threshold,
96
- fuzzyMaxDistance: config.fuzzyMaxDistance,
146
+ keys,
147
+ ...config,
97
148
  caseSensitive: true,
98
149
  // DataKable re-ranks with exact scores and an index tie-break; tim keeps ex-search's own pass
99
150
  // stable and avoids radix's score quantisation.
100
151
  sortAlgorithm: "tim"
101
152
  });
102
- const view = projection(size, keys, at);
153
+ const max = config.fuzzyMaxDistance;
154
+ let q = "";
155
+ const near = (k, r) => {
156
+ const v = at(k, r);
157
+ if (v.includes(q) || /\s/.test(q)) return v;
158
+ let best = "";
159
+ let top = 0;
160
+ const words = v.split(/\s+/);
161
+ for (const w of words[1] ? [v, ...words] : words) {
162
+ const s = Math.abs(w.length - q.length) > max ? 0 : Scorer.fuzzy(w, q, max, true);
163
+ if (s > top) {
164
+ best = w;
165
+ top = s;
166
+ }
167
+ }
168
+ return best;
169
+ };
170
+ const view = projection(size, keys, near);
103
171
  engine.setData(view.items);
104
172
  return {
105
173
  match(rows, query) {
106
- const q = config.caseSensitive ? query : query.toLowerCase();
107
- return chunked(rows, (chunk) => {
174
+ const folded = fold(query);
175
+ return chunked(rows, keys.length, (chunk) => {
176
+ q = folded;
108
177
  view.show(chunk);
109
178
  try {
110
179
  return engine.search(q);
@@ -115,11 +184,11 @@ function exSearchIndex(size, keys, at, config) {
115
184
  }
116
185
  };
117
186
  }
118
- function containsIndex(keys, at, config) {
187
+ function containsIndex(keys, at, fold, config) {
119
188
  return {
120
189
  match(rows, query) {
121
- const q = config.caseSensitive ? query : query.toLowerCase();
122
- return chunked(rows, (chunk) => {
190
+ const q = fold(query);
191
+ return chunked(rows, keys.length, (chunk) => {
123
192
  const hits = [];
124
193
  for (let j = 0; j < chunk.length; j++) {
125
194
  const r = chunk[j];
@@ -138,22 +207,10 @@ function containsIndex(keys, at, config) {
138
207
  };
139
208
  }
140
209
  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]
210
+ return Uint32Array.from(
211
+ hits.sort((a, b) => b.score - a.score || a.i - b.i),
212
+ (h) => h.i
153
213
  );
154
- const out = new Uint32Array(n);
155
- for (let k = 0; k < n; k++) out[k] = index[order[k]];
156
- return out;
157
214
  }
158
215
  export {
159
216
  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 { getValue } from '../../column/get-value'\nimport '../../diagnostics/process-guard'\nimport type { Emit } from '../../diagnostics/types'\nimport type { RowModelContext } from '../../feature/types'\nimport { nullishOf, type SlicedFormatting } from '../../pipeline/column-cache'\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 a scan (one value per row). */\nconst CHUNK = 4096\n\n/**\n * Cells (rows × searchable columns) matched per chunk between two `yield`s (#40): a table with\n * many searchable columns matches fewer rows per chunk, so a chunk stays a few milliseconds.\n */\nconst CELLS = 16_384\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 cached strings (#27): what the default formatter\n * shows, `String(v)`, or `''` for a `null` / `undefined` cell (stored as `NaN`, like a real `NaN`,\n * which shows `'NaN'`).\n */\nfunction numberReader(values: Float64Array): Read {\n const nullish = nullishOf(values)\n return (r) => {\n const v = values[r] as number\n return !Number.isNaN(v) ? String(v) : nullish?.[r] ? '' : 'NaN'\n }\n}\n\n/**\n * One pass over a column's values (`get(r)` per row, time-sliced by `CHUNK` rows): their text as\n * the default formatter shows a primitive (`String(v)`, `''` when nullish), read while matching\n * (#27: no formatted copy per row). `false` at the first object or function (a `Date`, an array):\n * the column cache formats those. `undefined` when a read throws. The values are read in NFC (#30),\n * normalised here, never per keystroke: the rows whose value is not NFC (rare: NFD input from\n * macOS, Thai or Vietnamese keyboards) keep their NFC form in a sparse map. Only values with a code\n * unit from U+0300 (combining marks and up) can change.\n */\nfunction* scan(\n size: number,\n get: (r: number) => unknown,\n): Generator<undefined, Read | false | undefined, undefined> {\n const over = new Map<number, string>()\n try {\n for (let r = 0; r < size; r++) {\n const v = get(r)\n if (v !== null && (typeof v === 'object' || typeof v === 'function')) return false\n if (typeof v === 'string' && /[\\u0300-\\uffff]/.test(v)) {\n const n = v.normalize('NFC')\n if (n !== v) over.set(r, n)\n }\n if (r % CHUNK === CHUNK - 1) yield\n }\n } catch {\n return undefined\n }\n const read: Read = (r) => {\n const v = get(r)\n return v === null || v === undefined ? '' : String(v)\n }\n return over.size ? (r) => over.get(r) ?? read(r) : read\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`, the columns of primitives\n * (strings, numbers, booleans, nullable) are searchable at weight 1, and a column of dates, arrays\n * or other objects is not (#27: it would keep a formatted string per row; it opts in with a\n * weight; `DK_SEARCH_COLUMN_SKIPPED` in development). `null` when nothing is searchable. Nothing\n * is copied per row (§9.3 memory): a key column of primitives without `format` is read from the\n * rows and stringified while matching (no values array, #27); an accessor column of primitives\n * reads the column cache's values the same way; only a column with `format` or an opted-in object\n * column (dates, arrays) keeps its formatted strings in the column cache. A key column whose read\n * throws goes through the column cache too, which reports it. The case is folded per chunk while\n * 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 data = ctx.data\n const n = data.length\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 const path = col.format ? undefined : col.path\n let failed = false\n const onError = () => {\n failed = true\n }\n let read = path ? yield* scan(n, (r) => getValue(data[r], path, onError)) : undefined\n if (failed || (read === false && declared)) read = undefined\n if (read === undefined) {\n const values = ctx.getColumnValues(col.id)\n if (!values) continue\n yield // reading a column is a task of its own (#40)\n const numbers = values instanceof Float64Array\n if (numbers && !col.format) read = numberReader(values)\n else {\n // Zero-config: a column of objects is not searched, `format` or not (#27).\n const own =\n numbers || (declared && col.format) ? undefined : yield* scan(n, (r) => values[r])\n if (own === false && !declared) read = false\n else if (own && !col.format) read = own\n else {\n // Time-sliced in a table run (#40): dates or a heavy `format` are not one long task.\n const sliced = (ctx as RowModelContext & SlicedFormatting).formattedSliced\n const formatted = sliced ? yield* sliced(col.id) : ctx.getFormattedValues(col.id)\n if (!formatted) continue\n read = yield* scan(n, (r) => formatted[r])\n }\n }\n }\n if (!read) {\n if ((process.env.NODE_ENV !== \"production\") && read === false) {\n ctx.emit('DK_SEARCH_COLUMN_SKIPPED', { column: col.id, index: col.index })\n }\n continue\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 (NFC already) are folded per chunk while matching; the query is normalised as well, so\n // NFC and NFD spellings match each other like in the `contains` filter (#30).\n const fold = (v: string) => (config.caseSensitive ? v : v.toLowerCase())\n const foldQuery = (q: string) => fold(q.normalize('NFC'))\n const at = (k: number, r: number) => fold((reads[k] as Read)(r))\n return useExSearch\n ? exSearchIndex(ctx.data.length, keys, at, foldQuery, config)\n : containsIndex(keys, at, foldQuery, config)\n}\n\n/** Matches `rows` chunk by chunk (`score` decides alone, so chunks match like the whole). */\nfunction* chunked(\n rows: Uint32Array,\n columns: number,\n matchChunk: (chunk: Uint32Array) => Hit[],\n): Generator<undefined, Uint32Array, undefined> {\n const size = Math.max(256, Math.floor(CELLS / columns))\n if (rows.length <= size) return rankHits(matchChunk(rows))\n const hits: Hit[] = []\n for (let k = 0; k < rows.length; k += size) {\n for (const h of matchChunk(rows.subarray(k, k + size))) 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, keys.length, (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, keys.length, (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":[]}
@@ -23,19 +23,22 @@ __export(search_exports, {
23
23
  search: () => search
24
24
  });
25
25
  module.exports = __toCommonJS(search_exports);
26
+ var import_debounce = require("kk-debounce/debounce");
26
27
  var import_debounceSignal = require("kk-debounce/debounceSignal");
27
28
  var import_throttle = require("kk-debounce/throttle");
28
29
  var import_codes = require("../../catalog/codes.cjs");
29
30
  var import_options = require("../../catalog/options.cjs");
30
31
  var import_build = require("../../diagnostics/build.cjs");
31
32
  var import_catalog = require("../../diagnostics/catalog.cjs");
33
+ var import_process_guard = require("../../diagnostics/process-guard.cjs");
32
34
  var import_define = require("../../feature/define.cjs");
33
35
  var import_define2 = require("../../intercept/define.cjs");
34
36
  var import_slice = require("../../pipeline/slice.cjs");
35
37
  var import_mode = require("../../source/mode.cjs");
36
38
  var import_search_fn_brand = require("./search-fn-brand.cjs");
37
39
  var import_search_index = require("./search-index.cjs");
38
- (0, import_catalog.keepDiagnostics)(import_codes.optionDiagnostics);
40
+ var import_skipped_codes = require("./skipped-codes.cjs");
41
+ (0, import_catalog.keepDiagnostics)(import_codes.optionDiagnostics, import_skipped_codes.skippedColumnDiagnostics);
39
42
  var DEFAULT_WAIT = 300;
40
43
  var isDuration = (v) => {
41
44
  if (typeof v === "number") return Number.isFinite(v) && v >= 0;
@@ -90,7 +93,7 @@ var searchOptions = {
90
93
  }
91
94
  };
92
95
  var settledSuperseded = () => Promise.resolve(import_search_fn_brand.SUPERSEDED);
93
- var toText = (q) => typeof q === "string" ? q : q == null ? "" : String(q);
96
+ var toText = (q) => q == null ? "" : String(q);
94
97
  var search = (opts = {}) => {
95
98
  const config = {
96
99
  threshold: opts.threshold ?? 0.3,
@@ -115,13 +118,17 @@ var search = (opts = {}) => {
115
118
  (0, import_build.throwConfigError)("DK_INVALID_OPTION", {
116
119
  path: "searchFn",
117
120
  feature: "search",
118
- expected: "withSearchFn(searchFn)",
119
- received: "searchFn",
121
+ expected: "wrapped|withSearchFn|searchFn|search-async",
122
+ // prose key (diagnostics/prose.ts)
123
+ received: typeof opts.searchFn,
120
124
  suggestions: [],
121
125
  phase: "init"
122
126
  });
123
127
  }
124
128
  const instances = /* @__PURE__ */ new WeakMap();
129
+ const resumes = /* @__PURE__ */ new WeakMap();
130
+ const ran = /* @__PURE__ */ new WeakMap();
131
+ let waited = false;
125
132
  function indexFor(ctx) {
126
133
  let byColumns = indexes.get(ctx.data);
127
134
  if (!byColumns) {
@@ -129,7 +136,7 @@ var search = (opts = {}) => {
129
136
  indexes.set(ctx.data, byColumns);
130
137
  }
131
138
  let key = byColumns.get(ctx.columns);
132
- if (key === void 0) {
139
+ if (!key) {
133
140
  key = {};
134
141
  byColumns.set(ctx.columns, key);
135
142
  }
@@ -148,7 +155,10 @@ var search = (opts = {}) => {
148
155
  }
149
156
  return (0, import_slice.sliced)(ctx, matching2.key, () => index.match(rows, query));
150
157
  }
151
- const standIn = (indices) => shown && shown.input === indices ? shown.output : indices;
158
+ const standIn = (indices) => {
159
+ waited = true;
160
+ return shown && shown.input === indices ? shown.output : indices;
161
+ };
152
162
  function step(indices, state, ctx) {
153
163
  const query = typeof state?.query === "string" ? state.query.trim() : "";
154
164
  if (query === "") return indices;
@@ -161,7 +171,7 @@ var search = (opts = {}) => {
161
171
  ctx.emit("DK_OPTION_NO_EFFECT", {
162
172
  path: "query",
163
173
  feature: "search",
164
- reason: "no column is searchable: pass `columns` (give the columns to search a `search` weight, or omit it everywhere to search every column)",
174
+ reason: "not-searchable",
165
175
  once: false
166
176
  });
167
177
  }
@@ -190,14 +200,27 @@ var search = (opts = {}) => {
190
200
  interceptors: [
191
201
  (0, import_define2.defineInterceptor)({
192
202
  name: "search",
193
- on: engine ? ["table:destroy", "pipeline:run"] : ["table:destroy"],
203
+ on: ["table:destroy", "pipeline:run", "table:init", "state:dispatch", "table:setOptions"],
194
204
  intercept(ctx, next) {
195
205
  const table = ctx.table;
196
206
  if (ctx.op === "table:destroy") {
197
207
  instances.get(table)?.();
198
208
  return next();
199
209
  }
200
- return engine.run(table, next);
210
+ if (ctx.op !== "pipeline:run") {
211
+ const out = next();
212
+ resumes.get(table)?.();
213
+ return out;
214
+ }
215
+ const prev = waited;
216
+ waited = false;
217
+ try {
218
+ return engine ? engine.run(table, next) : next();
219
+ } finally {
220
+ const w = waited;
221
+ waited = prev;
222
+ ran.get(table)?.(w);
223
+ }
201
224
  }
202
225
  })
203
226
  ],
@@ -206,11 +229,12 @@ var search = (opts = {}) => {
206
229
  const write = (input2, query, status, error) => {
207
230
  const s = ctx.getState();
208
231
  if (status !== "error" && s?.input === input2 && s.query === query && s.status === status && !("error" in s)) {
209
- return;
232
+ return false;
210
233
  }
211
234
  ctx.setState(
212
235
  status === "error" ? { input: input2, query, status, error } : { input: input2, query, status }
213
236
  );
237
+ return ctx.getState() !== s;
214
238
  };
215
239
  const applied = () => ctx.getState()?.query ?? "";
216
240
  const input = () => ctx.getState()?.input ?? "";
@@ -218,11 +242,6 @@ var search = (opts = {}) => {
218
242
  const waitFor = (query) => new Promise((resolve) => {
219
243
  waiting.push({ query, resolve });
220
244
  });
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
245
  const replaceWaiting = (query) => {
227
246
  if (waiting.length === 0) return;
228
247
  const keep = [];
@@ -238,6 +257,22 @@ var search = (opts = {}) => {
238
257
  waiting = [];
239
258
  return keep;
240
259
  };
260
+ let held = [];
261
+ let busy = false;
262
+ const release = (outcome) => {
263
+ const list = held;
264
+ held = [];
265
+ for (const w of list) w.resolve(outcome);
266
+ };
267
+ const supersede = () => {
268
+ held = [...waiting, ...held];
269
+ waiting = [];
270
+ release(import_search_fn_brand.SUPERSEDED);
271
+ };
272
+ ran.set(table, (w) => {
273
+ busy = w;
274
+ if (!w) release(import_search_fn_brand.APPLIED);
275
+ });
241
276
  const session = engine?.session({
242
277
  ctx,
243
278
  write,
@@ -245,10 +280,13 @@ var search = (opts = {}) => {
245
280
  input,
246
281
  claim
247
282
  });
283
+ let scheduled = false;
248
284
  const applyQuery = (query, inp = input()) => {
285
+ scheduled = false;
249
286
  if (session?.apply(query, inp)) return;
250
- write(inp, query, "idle");
251
- settleWaiting(query, import_search_fn_brand.APPLIED);
287
+ if (held[0]?.query !== query) release(import_search_fn_brand.SUPERSEDED);
288
+ held.push(...claim(query));
289
+ if (!write(inp, query, "idle") && !busy) release(import_search_fn_brand.APPLIED);
252
290
  };
253
291
  let calling = false;
254
292
  let appliedNow;
@@ -258,10 +296,27 @@ var search = (opts = {}) => {
258
296
  };
259
297
  const timer = strategy === "debounce" ? (0, import_debounceSignal.debouncedSignal)(applied, apply, wait, { behavior: typing.behavior ?? "trailing" }) : strategy === "throttle" ? (0, import_throttle.throttle)(apply, wait) : void 0;
260
298
  const resting = () => session?.busy() ? "searching" : "idle";
261
- instances.set(table, () => {
299
+ resumes.set(table, () => {
300
+ const s = ctx.getState();
301
+ const pending = s?.status === "pending";
302
+ if (pending ? !scheduled : s?.status === "searching" && !session?.busy()) {
303
+ const inp = toText(s.input);
304
+ write(inp, pending && strategy !== "enter" ? target(inp) : toText(s.query), resting());
305
+ }
306
+ });
307
+ const drop = (0, import_debounce.debounce)(() => {
308
+ const list = waiting;
309
+ waiting = [];
310
+ for (const w of list) w.resolve(import_search_fn_brand.SUPERSEDED);
311
+ }, wait);
312
+ const stop = () => {
262
313
  timer?.cancel();
314
+ drop.cancel();
315
+ };
316
+ instances.set(table, () => {
317
+ stop();
263
318
  session?.destroy();
264
- settleWaiting("\0", import_search_fn_brand.SUPERSEDED);
319
+ supersede();
265
320
  });
266
321
  const commit = (next) => {
267
322
  const current = applied();
@@ -273,7 +328,7 @@ var search = (opts = {}) => {
273
328
  return p;
274
329
  }
275
330
  if (!timer || query === current) {
276
- timer?.cancel();
331
+ stop();
277
332
  applyQuery(query, next);
278
333
  return p;
279
334
  }
@@ -290,13 +345,15 @@ var search = (opts = {}) => {
290
345
  }
291
346
  if (strategy === "debounce" && typing.behavior === "leading") {
292
347
  write(next, current, resting());
348
+ drop();
293
349
  return p;
294
350
  }
351
+ scheduled = true;
295
352
  write(next, current, "pending");
296
353
  return p;
297
354
  };
298
355
  const applyNow = (next) => {
299
- timer?.cancel();
356
+ stop();
300
357
  const query = target(next);
301
358
  replaceWaiting(query);
302
359
  const p = waitFor(query);
@@ -307,12 +364,13 @@ var search = (opts = {}) => {
307
364
  setSearch: (query) => commit(toText(query)),
308
365
  flushSearch: () => applyNow(input()),
309
366
  cancelSearch: () => {
310
- timer?.cancel();
367
+ scheduled = false;
368
+ stop();
311
369
  session?.abort();
312
- settleWaiting("\0", import_search_fn_brand.SUPERSEDED);
370
+ supersede();
313
371
  const s = ctx.getState();
314
372
  if (s?.status === "pending" || s?.status === "searching") {
315
- ctx.setState({ input: s.input, query: s.query, status: "idle" });
373
+ ctx.setState({ ...s, status: "idle" });
316
374
  }
317
375
  },
318
376
  clearSearch: () => {