@datakable/core 2.0.2 → 2.0.3

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 (113) hide show
  1. package/dist/{codes-AX8XkMNY.d.cts → codes-4sYQv_7J.d.cts} +36 -10
  2. package/dist/{codes-AX8XkMNY.d.ts → codes-4sYQv_7J.d.ts} +36 -10
  3. package/dist/column-filters.d.cts +2 -2
  4. package/dist/column-filters.d.ts +2 -2
  5. package/dist/diagnostics/build.cjs +1 -1
  6. package/dist/diagnostics/build.cjs.map +1 -1
  7. package/dist/diagnostics/build.js +2 -2
  8. package/dist/diagnostics/build.js.map +1 -1
  9. package/dist/diagnostics/console.cjs +15 -4
  10. package/dist/diagnostics/console.cjs.map +1 -1
  11. package/dist/diagnostics/console.js +15 -4
  12. package/dist/diagnostics/console.js.map +1 -1
  13. package/dist/diagnostics/prose.cjs +10 -1
  14. package/dist/diagnostics/prose.cjs.map +1 -1
  15. package/dist/diagnostics/prose.js +9 -1
  16. package/dist/diagnostics/prose.js.map +1 -1
  17. package/dist/{explain-pKAnnxEW.d.cts → explain-DmwN_RG9.d.cts} +11 -4
  18. package/dist/{explain-BVE3ln3G.d.ts → explain-Dr3SmMNY.d.ts} +11 -4
  19. package/dist/feature/install.cjs +2 -1
  20. package/dist/feature/install.cjs.map +1 -1
  21. package/dist/feature/install.js +2 -1
  22. package/dist/feature/install.js.map +1 -1
  23. package/dist/feature/slice-guard.cjs +76 -0
  24. package/dist/feature/slice-guard.cjs.map +1 -0
  25. package/dist/feature/slice-guard.js +50 -0
  26. package/dist/feature/slice-guard.js.map +1 -0
  27. package/dist/features/column-filters/codes.cjs +2 -19
  28. package/dist/features/column-filters/codes.cjs.map +1 -1
  29. package/dist/features/column-filters/codes.js +2 -19
  30. package/dist/features/column-filters/codes.js.map +1 -1
  31. package/dist/features/column-filters/column-filters.cjs +37 -22
  32. package/dist/features/column-filters/column-filters.cjs.map +1 -1
  33. package/dist/features/column-filters/column-filters.js +37 -22
  34. package/dist/features/column-filters/column-filters.js.map +1 -1
  35. package/dist/features/column-filters/match.cjs +6 -2
  36. package/dist/features/column-filters/match.cjs.map +1 -1
  37. package/dist/features/column-filters/match.js +6 -2
  38. package/dist/features/column-filters/match.js.map +1 -1
  39. package/dist/features/pagination/pagination.cjs +2 -2
  40. package/dist/features/pagination/pagination.cjs.map +1 -1
  41. package/dist/features/pagination/pagination.js +2 -2
  42. package/dist/features/pagination/pagination.js.map +1 -1
  43. package/dist/features/search/codes.cjs +18 -2
  44. package/dist/features/search/codes.cjs.map +1 -1
  45. package/dist/features/search/codes.js +16 -1
  46. package/dist/features/search/codes.js.map +1 -1
  47. package/dist/features/search/search-engine.cjs +24 -5
  48. package/dist/features/search/search-engine.cjs.map +1 -1
  49. package/dist/features/search/search-engine.js +25 -6
  50. package/dist/features/search/search-engine.js.map +1 -1
  51. package/dist/features/sorting/sorting.cjs +55 -24
  52. package/dist/features/sorting/sorting.cjs.map +1 -1
  53. package/dist/features/sorting/sorting.js +55 -24
  54. package/dist/features/sorting/sorting.js.map +1 -1
  55. package/dist/index.d.cts +26 -17
  56. package/dist/index.d.ts +26 -17
  57. package/dist/intercept/runtime.cjs +3 -2
  58. package/dist/intercept/runtime.cjs.map +1 -1
  59. package/dist/intercept/runtime.js +3 -2
  60. package/dist/intercept/runtime.js.map +1 -1
  61. package/dist/messages/index.cjs +2 -0
  62. package/dist/messages/index.cjs.map +1 -1
  63. package/dist/messages/index.js +2 -0
  64. package/dist/messages/index.js.map +1 -1
  65. package/dist/pagination.d.cts +2 -2
  66. package/dist/pagination.d.ts +2 -2
  67. package/dist/schema.d.cts +2 -2
  68. package/dist/schema.d.ts +2 -2
  69. package/dist/{search-async-entry-ByMJbHjc.d.cts → search-async-entry-BiemO3JT.d.cts} +2 -2
  70. package/dist/{search-async-entry-CFQODE08.d.ts → search-async-entry-GRj0E06n.d.ts} +2 -2
  71. package/dist/search-async.d.cts +3 -3
  72. package/dist/search-async.d.ts +3 -3
  73. package/dist/search.d.cts +4 -4
  74. package/dist/search.d.ts +4 -4
  75. package/dist/server.d.cts +3 -3
  76. package/dist/server.d.ts +3 -3
  77. package/dist/sorting.d.cts +2 -2
  78. package/dist/sorting.d.ts +2 -2
  79. package/dist/source/async-control.cjs +2 -1
  80. package/dist/source/async-control.cjs.map +1 -1
  81. package/dist/source/async-control.js +2 -1
  82. package/dist/source/async-control.js.map +1 -1
  83. package/dist/source/fetcher.cjs.map +1 -1
  84. package/dist/source/fetcher.js.map +1 -1
  85. package/dist/source/server.cjs +58 -5
  86. package/dist/source/server.cjs.map +1 -1
  87. package/dist/source/server.js +58 -5
  88. package/dist/source/server.js.map +1 -1
  89. package/dist/table/create-table.cjs +30 -20
  90. package/dist/table/create-table.cjs.map +1 -1
  91. package/dist/table/create-table.js +30 -20
  92. package/dist/table/create-table.js.map +1 -1
  93. package/dist/table/index.cjs +3 -0
  94. package/dist/table/index.cjs.map +1 -1
  95. package/dist/table/index.js +5 -1
  96. package/dist/table/index.js.map +1 -1
  97. package/dist/table/state-codes.cjs +72 -0
  98. package/dist/table/state-codes.cjs.map +1 -0
  99. package/dist/table/state-codes.js +45 -0
  100. package/dist/table/state-codes.js.map +1 -0
  101. package/dist/table/types.cjs.map +1 -1
  102. package/dist/table/types.js.map +1 -1
  103. package/dist/validity.d.cts +2 -2
  104. package/dist/validity.d.ts +2 -2
  105. package/dist/version.cjs +1 -1
  106. package/dist/version.cjs.map +1 -1
  107. package/dist/version.js +1 -1
  108. package/dist/version.js.map +1 -1
  109. package/package.json +1 -1
  110. package/dist/ingest/test-schemas.cjs +0 -73
  111. package/dist/ingest/test-schemas.cjs.map +0 -1
  112. package/dist/ingest/test-schemas.js +0 -46
  113. package/dist/ingest/test-schemas.js.map +0 -1
@@ -26,7 +26,9 @@ module.exports = __toCommonJS(column_filters_exports);
26
26
  var import_options = require("../../catalog/options.cjs");
27
27
  var import_catalog = require("../../diagnostics/catalog.cjs");
28
28
  var import_process_guard = require("../../diagnostics/process-guard.cjs");
29
+ var import_redact = require("../../diagnostics/redact.cjs");
29
30
  var import_define = require("../../feature/define.cjs");
31
+ var import_slice_guard = require("../../feature/slice-guard.cjs");
30
32
  var import_cell_batch = require("../../intercept/cell-batch.cjs");
31
33
  var import_classify = require("../../intercept/classify.cjs");
32
34
  var import_codes = require("../../intercept/codes.cjs");
@@ -180,28 +182,41 @@ function filterRows(indices, state, ctx) {
180
182
  }
181
183
  return out ? out.subarray(0, n) : indices;
182
184
  }
183
- var columnFilters = (opts = {}) => (0, import_define.defineFeature)({
184
- name: "columnFilters",
185
- stage: "filter",
186
- mode: opts.mode ?? "client",
187
- options: (0, import_options.defineOptions)(
188
- // The option validator runs in development only (§9.3): production drops the schema.
189
- process.env.NODE_ENV !== "production" ? columnFiltersOptions : {},
190
- opts
191
- ),
192
- initialState: () => ({}),
193
- createApi: (ctx) => ({
194
- setColumnFilter: (id, filter) => ctx.setState((prev) => {
195
- if (prev[id] === filter) return prev;
196
- const next = { ...prev };
197
- if (filter === void 0) delete next[id];
198
- else next[id] = filter;
199
- return next;
200
- }),
201
- resetColumnFilters: () => ctx.setState((prev) => Object.keys(prev).length ? {} : prev)
202
- }),
203
- rowModel: filterRows
204
- });
185
+ var isSlice = (s) => s !== null && typeof s === "object" && !Array.isArray(s);
186
+ var fixSlice = (s) => isSlice(s) ? void 0 : [{}, (0, import_redact.describeValue)(s), "{}"];
187
+ var columnFilters = (opts = {}) => {
188
+ const guard = process.env.NODE_ENV !== "production" ? (0, import_slice_guard.sliceGuard)("columnFilters", fixSlice) : void 0;
189
+ return (0, import_define.defineFeature)({
190
+ name: "columnFilters",
191
+ stage: "filter",
192
+ mode: opts.mode ?? "client",
193
+ options: (0, import_options.defineOptions)(
194
+ // The option validator runs in development only (§9.3): production drops the schema.
195
+ process.env.NODE_ENV !== "production" ? columnFiltersOptions : {},
196
+ opts
197
+ ),
198
+ ...guard ? { interceptors: [guard.interceptor] } : void 0,
199
+ initialState: () => ({}),
200
+ createApi: (ctx) => {
201
+ guard?.install(ctx);
202
+ return {
203
+ setColumnFilter: (id, filter) => {
204
+ const s = ctx.getState();
205
+ const prev = s && typeof s === "object" ? s : {};
206
+ if (prev[id] === filter) return;
207
+ const next = { ...prev, [id]: filter };
208
+ if (filter === void 0) delete next[id];
209
+ ctx.setState(next);
210
+ },
211
+ resetColumnFilters: () => {
212
+ const s = ctx.getState();
213
+ if (!s || Object.keys(s).length > 0) ctx.setState({});
214
+ }
215
+ };
216
+ },
217
+ rowModel: filterRows
218
+ });
219
+ };
205
220
  // Annotate the CommonJS export names for ESM import in node:
206
221
  0 && (module.exports = {
207
222
  columnFilters
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/column-filters/column-filters.ts"],"sourcesContent":["import { defineOptions } from '../../catalog/options'\nimport { keepDiagnostics } from '../../diagnostics/catalog'\nimport '../../diagnostics/process-guard'\nimport { defineFeature } from '../../feature/define'\nimport type { RowModelContext } from '../../feature/types'\nimport { CellFailures } from '../../intercept/cell-batch'\nimport { errorName } from '../../intercept/classify'\nimport { boundaryDiagnostics } from '../../intercept/codes'\nimport type { ColumnValues } from '../../pipeline/types'\nimport { type DataMode, modeOption } from '../../source/mode'\nimport { columnFiltersDiagnostics } from './codes'\nimport {\n BAD,\n type Compiled,\n compile,\n MATCH,\n NO,\n type Outcome,\n type RawFilter,\n type Test,\n typeName,\n} from './match'\nimport type { ColumnFiltersState, ColumnFiltersTypes } from './types'\n\nkeepDiagnostics(columnFiltersDiagnostics, boundaryDiagnostics)\n\nexport interface ColumnFiltersOptions {\n /**\n * `'server'`: the server filters (the slice goes to `options.fetcher` as `filters`, or the app\n * fetches); core skips this step (§7.1). Default `'client'`.\n */\n mode?: DataMode\n}\n\nconst columnFiltersOptions = { mode: modeOption } as const\n\n/** Position of column `id` in `options.columns` (diagnostic `where`); `-1` if absent. */\nfunction columnIndex(columns: readonly unknown[], id: string): number {\n for (let i = 0; i < columns.length; i++) {\n const c = columns[i] as { key?: unknown; id?: unknown } | null | undefined\n if (c && typeof c === 'object' && (typeof c.key === 'string' ? c.key : c.id) === id) return i\n }\n return -1\n}\n\nfunction columnIds(columns: readonly unknown[]): string[] {\n const out: string[] = []\n for (const c of columns as readonly ({ key?: unknown; id?: unknown } | null)[]) {\n const id = c && (typeof c.key === 'string' ? c.key : c.id)\n if (typeof id === 'string') out.push(id)\n }\n return out\n}\n\n/** Nullish and `NaN` (nullish in a numeric column cache) match no active filter (§6.1). */\nconst absent = (v: unknown) =>\n v === undefined || v === null || (typeof v === 'number' && Number.isNaN(v))\n\n/** One cell: an array cell matches if any element matches, else `BAD` if one had a wrong type. */\nfunction testCell(test: Test, v: unknown, r: number): Outcome {\n if (!Array.isArray(v)) return test(v, r)\n let out: Outcome = NO\n for (let i = 0; i < v.length; i++) {\n const e = v[i]\n if (absent(e)) continue\n const o = test(e, -1)\n if (o === MATCH) return MATCH\n if (o === BAD) out = BAD\n }\n return out\n}\n\n/**\n * Per-batch failures, recorded as numbers (§3.4) and reused across runs: wrong-type cells, or\n * throwing `custom` calls. Text and `where` are built only when something failed.\n */\nconst failures = new CellFailures()\nconst first = { type: '', error: undefined as unknown }\n\n/** Keeps the rows of `rows[0..n)` that pass `c`, compacted into `out` (may be `rows`). */\nfunction apply(\n c: Extract<Compiled, { kind: 'test' | 'custom' }>,\n values: ColumnValues,\n data: readonly unknown[],\n rows: Uint32Array,\n n: number,\n out: Uint32Array,\n): number {\n let k = 0\n if (c.kind === 'custom') {\n const { fn, value } = c\n for (let i = 0; i < n; i++) {\n const r = rows[i] as number\n const v = values[r]\n if (absent(v)) continue\n try {\n if (fn(v, value, data[r])) out[k++] = r\n } catch (e) {\n if (failures.count === 0) first.error = e\n failures.add(r, e)\n }\n }\n return k\n }\n const test = c.test\n for (let i = 0; i < n; i++) {\n const r = rows[i] as number\n const v = values[r]\n if (absent(v)) continue\n const o = testCell(test, v, r)\n if (o === MATCH) out[k++] = r\n else if (o === BAD) {\n if (failures.count === 0) first.type = typeName(v)\n failures.add(r, undefined)\n }\n }\n return k\n}\n\nfunction report(ctx: RowModelContext, c: Compiled, id: string, kind: string, index: number) {\n if (c.kind === 'custom') {\n // Like a column:batch filterFn failure (§3.4); `where` is the filter's state path.\n const name = errorName(first.error)\n ctx.emit(\n 'DK_CALLBACK_FAILED',\n {\n slot: `state.columnFilters.${id}.fn`,\n column: id,\n rows: Array.from(failures.sample()),\n count: failures.count,\n fallback: 'rows-unmatched',\n ...(name === undefined ? {} : { errorName: name }),\n },\n { cause: first.error },\n )\n } else if (c.kind === 'test') {\n ctx.emit('DK_FILTER_TYPE_MISMATCH', {\n column: id,\n index,\n filter: kind,\n source: 'cell',\n expected: c.expected,\n received: first.type,\n count: failures.count,\n rows: Array.from(failures.sample()),\n })\n }\n}\n\n/**\n * Applies every active column filter (AND) to `indices`: one pass per filtered column over the\n * column cache (§9.1), no per-cell allocation. Returns `indices` itself when nothing is active;\n * otherwise a new array (`indices` is never mutated).\n */\nfunction filterRows(\n indices: Uint32Array,\n state: Readonly<Record<string, unknown>> | undefined,\n ctx: RowModelContext,\n): Uint32Array {\n if (!state || typeof state !== 'object') return indices\n let out: Uint32Array | undefined\n let n = indices.length\n for (const id of Object.keys(state)) {\n const filter = state[id]\n if (filter === undefined) continue\n const values = ctx.getColumnValues(id)\n if (!values) {\n // Not a column (§7.2): ignored. No columns at all (zero-config, no rows yet) is not reported.\n if (ctx.columns.length > 0) {\n ctx.emit('DK_STATE_UNKNOWN_KEY', {\n slice: 'columnFilters',\n key: id,\n known: columnIds(ctx.columns),\n })\n }\n continue\n }\n const c = compile(filter, values)\n if (c.kind === 'off') continue\n const index = columnIndex(ctx.columns, id)\n if (c.kind === 'bad') {\n ctx.emit('DK_FILTER_TYPE_MISMATCH', {\n column: id,\n index,\n filter: c.filter,\n source: 'filter',\n expected: c.expected,\n received: c.received,\n count: 0,\n rows: [],\n })\n return new Uint32Array(0)\n }\n const src = out ?? indices\n out ??= new Uint32Array(indices.length)\n failures.reset()\n first.error = undefined\n try {\n if (c.kind === 'custom') {\n // A user callback per cell: one `filterFn` column:batch (§3.4). A skipped batch keeps\n // the slot fallback: no row matches.\n const rows = src.subarray(0, n)\n const dst = out\n let k = 0\n ctx.batch(id, 'filterFn', rows, () => {\n k = apply(c, values, ctx.data, rows, rows.length, dst)\n })\n n = k\n } else n = apply(c, values, ctx.data, src, n, out)\n if (failures.count > 0) report(ctx, c, id, String((filter as RawFilter).type), index)\n } finally {\n failures.reset()\n first.error = undefined\n }\n }\n return out ? out.subarray(0, n) : indices\n}\n\n/**\n * Column filters (§6.1): `equals`, `contains` (case-insensitive, NFC), `in`, `range` (inclusive),\n * `custom`. Filters combine with AND; nullish cells match no active filter; array cells match if\n * any element matches; a cell of the wrong runtime type is a non-match + `DK_FILTER_TYPE_MISMATCH`.\n *\n * ```ts\n * const table = createTable({ data: users, columns, features: [columnFilters()] })\n * table.setColumnFilter('age', { type: 'range', value: { min: 18 } })\n * table.setColumnFilter('name', { type: 'contains', value: 'ada' })\n * ```\n */\nexport const columnFilters = (opts: ColumnFiltersOptions = {}) =>\n defineFeature<'columnFilters', ColumnFiltersTypes>({\n name: 'columnFilters',\n stage: 'filter',\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\") ? columnFiltersOptions : ({} as typeof columnFiltersOptions),\n opts,\n ),\n initialState: () => ({}),\n createApi: (ctx) => ({\n setColumnFilter: (id, filter) =>\n ctx.setState((prev) => {\n // No change (the same filter, or clearing a column without one): keep the slice.\n if ((prev as Record<string, unknown>)[id] === filter) return prev\n const next: Record<string, unknown> = { ...prev }\n if (filter === undefined) delete next[id]\n else next[id] = filter\n return next as ColumnFiltersState<unknown>\n }),\n resetColumnFilters: () => ctx.setState((prev) => (Object.keys(prev).length ? {} : prev)),\n }),\n rowModel: filterRows,\n })\n"],"mappings":"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","names":["import_codes"]}
1
+ {"version":3,"sources":["../../../src/features/column-filters/column-filters.ts"],"sourcesContent":["import { defineOptions } from '../../catalog/options'\nimport { keepDiagnostics } from '../../diagnostics/catalog'\nimport '../../diagnostics/process-guard'\nimport { describeValue } from '../../diagnostics/redact'\nimport { defineFeature } from '../../feature/define'\nimport { type SliceFix, sliceGuard } from '../../feature/slice-guard'\nimport type { RowModelContext } from '../../feature/types'\nimport { CellFailures } from '../../intercept/cell-batch'\nimport { errorName } from '../../intercept/classify'\nimport { boundaryDiagnostics } from '../../intercept/codes'\nimport type { ColumnValues } from '../../pipeline/types'\nimport { type DataMode, modeOption } from '../../source/mode'\nimport { columnFiltersDiagnostics } from './codes'\nimport {\n BAD,\n type Compiled,\n compile,\n MATCH,\n NO,\n type Outcome,\n type RawFilter,\n type Test,\n typeName,\n} from './match'\nimport type { ColumnFiltersState, ColumnFiltersTypes } from './types'\n\nkeepDiagnostics(columnFiltersDiagnostics, boundaryDiagnostics)\n\nexport interface ColumnFiltersOptions {\n /**\n * `'server'`: the server filters (the slice goes to `options.fetcher` as `filters`, or the app\n * fetches); core skips this step (§7.1). Default `'client'`.\n */\n mode?: DataMode\n}\n\nconst columnFiltersOptions = { mode: modeOption } as const\n\n/** Position of column `id` in `options.columns` (diagnostic `where`); `-1` if absent. */\nfunction columnIndex(columns: readonly unknown[], id: string): number {\n for (let i = 0; i < columns.length; i++) {\n const c = columns[i] as { key?: unknown; id?: unknown } | null | undefined\n if (c && typeof c === 'object' && (typeof c.key === 'string' ? c.key : c.id) === id) return i\n }\n return -1\n}\n\nfunction columnIds(columns: readonly unknown[]): string[] {\n const out: string[] = []\n for (const c of columns as readonly ({ key?: unknown; id?: unknown } | null)[]) {\n const id = c && (typeof c.key === 'string' ? c.key : c.id)\n if (typeof id === 'string') out.push(id)\n }\n return out\n}\n\n/** Nullish and `NaN` (nullish in a numeric column cache) match no active filter (§6.1). */\nconst absent = (v: unknown) =>\n v === undefined || v === null || (typeof v === 'number' && Number.isNaN(v))\n\n/** One cell: an array cell matches if any element matches, else `BAD` if one had a wrong type. */\nfunction testCell(test: Test, v: unknown, r: number): Outcome {\n if (!Array.isArray(v)) return test(v, r)\n let out: Outcome = NO\n for (let i = 0; i < v.length; i++) {\n const e = v[i]\n if (absent(e)) continue\n const o = test(e, -1)\n if (o === MATCH) return MATCH\n if (o === BAD) out = BAD\n }\n return out\n}\n\n/**\n * Per-batch failures, recorded as numbers (§3.4) and reused across runs: wrong-type cells, or\n * throwing `custom` calls. Text and `where` are built only when something failed.\n */\nconst failures = new CellFailures()\nconst first = { type: '', error: undefined as unknown }\n\n/** Keeps the rows of `rows[0..n)` that pass `c`, compacted into `out` (may be `rows`). */\nfunction apply(\n c: Extract<Compiled, { kind: 'test' | 'custom' }>,\n values: ColumnValues,\n data: readonly unknown[],\n rows: Uint32Array,\n n: number,\n out: Uint32Array,\n): number {\n let k = 0\n if (c.kind === 'custom') {\n const { fn, value } = c\n for (let i = 0; i < n; i++) {\n const r = rows[i] as number\n const v = values[r]\n if (absent(v)) continue\n try {\n if (fn(v, value, data[r])) out[k++] = r\n } catch (e) {\n if (failures.count === 0) first.error = e\n failures.add(r, e)\n }\n }\n return k\n }\n const test = c.test\n for (let i = 0; i < n; i++) {\n const r = rows[i] as number\n const v = values[r]\n if (absent(v)) continue\n const o = testCell(test, v, r)\n if (o === MATCH) out[k++] = r\n else if (o === BAD) {\n if (failures.count === 0) first.type = typeName(v)\n failures.add(r, undefined)\n }\n }\n return k\n}\n\nfunction report(ctx: RowModelContext, c: Compiled, id: string, kind: string, index: number) {\n if (c.kind === 'custom') {\n // Like a column:batch filterFn failure (§3.4); `where` is the filter's state path.\n const name = errorName(first.error)\n ctx.emit(\n 'DK_CALLBACK_FAILED',\n {\n slot: `state.columnFilters.${id}.fn`,\n column: id,\n rows: Array.from(failures.sample()),\n count: failures.count,\n fallback: 'rows-unmatched',\n ...(name === undefined ? {} : { errorName: name }),\n },\n { cause: first.error },\n )\n } else if (c.kind === 'test') {\n ctx.emit('DK_FILTER_TYPE_MISMATCH', {\n column: id,\n index,\n filter: kind,\n source: 'cell',\n expected: c.expected,\n received: first.type,\n count: failures.count,\n rows: Array.from(failures.sample()),\n })\n }\n}\n\n/**\n * Applies every active column filter (AND) to `indices`: one pass per filtered column over the\n * column cache (§9.1), no per-cell allocation. Returns `indices` itself when nothing is active;\n * otherwise a new array (`indices` is never mutated).\n */\nfunction filterRows(\n indices: Uint32Array,\n state: Readonly<Record<string, unknown>> | undefined,\n ctx: RowModelContext,\n): Uint32Array {\n if (!state || typeof state !== 'object') return indices\n let out: Uint32Array | undefined\n let n = indices.length\n for (const id of Object.keys(state)) {\n const filter = state[id]\n if (filter === undefined) continue\n const values = ctx.getColumnValues(id)\n if (!values) {\n // Not a column (§7.2): ignored. No columns at all (zero-config, no rows yet) is not reported.\n if (ctx.columns.length > 0) {\n ctx.emit('DK_STATE_UNKNOWN_KEY', {\n slice: 'columnFilters',\n key: id,\n known: columnIds(ctx.columns),\n })\n }\n continue\n }\n const c = compile(filter, values)\n if (c.kind === 'off') continue\n const index = columnIndex(ctx.columns, id)\n if (c.kind === 'bad') {\n ctx.emit('DK_FILTER_TYPE_MISMATCH', {\n column: id,\n index,\n filter: c.filter,\n source: 'filter',\n expected: c.expected,\n received: c.received,\n count: 0,\n rows: [],\n })\n return new Uint32Array(0)\n }\n const src = out ?? indices\n out ??= new Uint32Array(indices.length)\n failures.reset()\n first.error = undefined\n try {\n if (c.kind === 'custom') {\n // A user callback per cell: one `filterFn` column:batch (§3.4). A skipped batch keeps\n // the slot fallback: no row matches.\n const rows = src.subarray(0, n)\n const dst = out\n let k = 0\n ctx.batch(id, 'filterFn', rows, () => {\n k = apply(c, values, ctx.data, rows, rows.length, dst)\n })\n n = k\n } else n = apply(c, values, ctx.data, src, n, out)\n if (failures.count > 0) report(ctx, c, id, String((filter as RawFilter).type), index)\n } finally {\n failures.reset()\n first.error = undefined\n }\n }\n return out ? out.subarray(0, n) : indices\n}\n\nconst isSlice = (s: unknown): s is Record<string, unknown> =>\n s !== null && typeof s === 'object' && !Array.isArray(s)\n\n/** A restored slice that is not an object (e.g. `null`) is replaced with no filters (#89). */\nconst fixSlice = (s: unknown): SliceFix<ColumnFiltersState<unknown>> | undefined =>\n isSlice(s) ? undefined : [{}, describeValue(s), '{}']\n\n/**\n * Column filters (§6.1): `equals`, `contains` (case-insensitive, NFC), `in`, `range` (inclusive),\n * `custom`. Filters combine with AND; nullish cells match no active filter; array cells match if\n * any element matches; a cell of the wrong runtime type is a non-match + `DK_FILTER_TYPE_MISMATCH`.\n *\n * ```ts\n * const table = createTable({ data: users, columns, features: [columnFilters()] })\n * table.setColumnFilter('age', { type: 'range', value: { min: 18 } })\n * table.setColumnFilter('name', { type: 'contains', value: 'ada' })\n * ```\n */\nexport const columnFilters = (opts: ColumnFiltersOptions = {}) => {\n // Dev only (§9.3): production reads a malformed slice as no filters and stays cheap.\n const guard = (process.env.NODE_ENV !== \"production\") ? sliceGuard('columnFilters', fixSlice) : undefined\n return defineFeature<'columnFilters', ColumnFiltersTypes>({\n name: 'columnFilters',\n stage: 'filter',\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\") ? columnFiltersOptions : ({} as typeof columnFiltersOptions),\n opts,\n ),\n ...(guard ? { interceptors: [guard.interceptor] } : undefined),\n initialState: () => ({}),\n createApi: (ctx) => {\n guard?.install(ctx)\n // Values, not updaters: a library step is never reported as a failing user `updater` (#89).\n return {\n setColumnFilter: (id, filter) => {\n // A controlled slice not fed back yet may still be malformed: it reads as no filters.\n const s = ctx.getState()\n const prev: Record<string, unknown> = s && typeof s === 'object' ? s : {}\n // No change (the same filter, or clearing a column without one): keep the slice.\n if (prev[id] === filter) return\n // A computed key defines an own property: `\"__proto__\"` never swaps the prototype\n // (#102); the row model reports it as DK_STATE_UNKNOWN_KEY like any unknown column.\n const next: Record<string, unknown> = { ...prev, [id]: filter }\n if (filter === undefined) delete next[id]\n ctx.setState(next as ColumnFiltersState<unknown>)\n },\n resetColumnFilters: () => {\n const s = ctx.getState()\n if (!s || Object.keys(s).length > 0) ctx.setState({})\n },\n }\n },\n rowModel: filterRows,\n })\n}\n"],"mappings":"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","names":["import_codes"]}
@@ -2,7 +2,9 @@
2
2
  import { defineOptions } from "../../catalog/options.js";
3
3
  import { keepDiagnostics } from "../../diagnostics/catalog.js";
4
4
  import "../../diagnostics/process-guard.js";
5
+ import { describeValue } from "../../diagnostics/redact.js";
5
6
  import { defineFeature } from "../../feature/define.js";
7
+ import { sliceGuard } from "../../feature/slice-guard.js";
6
8
  import { CellFailures } from "../../intercept/cell-batch.js";
7
9
  import { errorName } from "../../intercept/classify.js";
8
10
  import { boundaryDiagnostics } from "../../intercept/codes.js";
@@ -162,28 +164,41 @@ function filterRows(indices, state, ctx) {
162
164
  }
163
165
  return out ? out.subarray(0, n) : indices;
164
166
  }
165
- var columnFilters = (opts = {}) => defineFeature({
166
- name: "columnFilters",
167
- stage: "filter",
168
- mode: opts.mode ?? "client",
169
- options: defineOptions(
170
- // The option validator runs in development only (§9.3): production drops the schema.
171
- process.env.NODE_ENV !== "production" ? columnFiltersOptions : {},
172
- opts
173
- ),
174
- initialState: () => ({}),
175
- createApi: (ctx) => ({
176
- setColumnFilter: (id, filter) => ctx.setState((prev) => {
177
- if (prev[id] === filter) return prev;
178
- const next = { ...prev };
179
- if (filter === void 0) delete next[id];
180
- else next[id] = filter;
181
- return next;
182
- }),
183
- resetColumnFilters: () => ctx.setState((prev) => Object.keys(prev).length ? {} : prev)
184
- }),
185
- rowModel: filterRows
186
- });
167
+ var isSlice = (s) => s !== null && typeof s === "object" && !Array.isArray(s);
168
+ var fixSlice = (s) => isSlice(s) ? void 0 : [{}, describeValue(s), "{}"];
169
+ var columnFilters = (opts = {}) => {
170
+ const guard = process.env.NODE_ENV !== "production" ? sliceGuard("columnFilters", fixSlice) : void 0;
171
+ return defineFeature({
172
+ name: "columnFilters",
173
+ stage: "filter",
174
+ mode: opts.mode ?? "client",
175
+ options: defineOptions(
176
+ // The option validator runs in development only (§9.3): production drops the schema.
177
+ process.env.NODE_ENV !== "production" ? columnFiltersOptions : {},
178
+ opts
179
+ ),
180
+ ...guard ? { interceptors: [guard.interceptor] } : void 0,
181
+ initialState: () => ({}),
182
+ createApi: (ctx) => {
183
+ guard?.install(ctx);
184
+ return {
185
+ setColumnFilter: (id, filter) => {
186
+ const s = ctx.getState();
187
+ const prev = s && typeof s === "object" ? s : {};
188
+ if (prev[id] === filter) return;
189
+ const next = { ...prev, [id]: filter };
190
+ if (filter === void 0) delete next[id];
191
+ ctx.setState(next);
192
+ },
193
+ resetColumnFilters: () => {
194
+ const s = ctx.getState();
195
+ if (!s || Object.keys(s).length > 0) ctx.setState({});
196
+ }
197
+ };
198
+ },
199
+ rowModel: filterRows
200
+ });
201
+ };
187
202
  export {
188
203
  columnFilters
189
204
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/column-filters/column-filters.ts"],"sourcesContent":["import { defineOptions } from '../../catalog/options'\nimport { keepDiagnostics } from '../../diagnostics/catalog'\nimport '../../diagnostics/process-guard'\nimport { defineFeature } from '../../feature/define'\nimport type { RowModelContext } from '../../feature/types'\nimport { CellFailures } from '../../intercept/cell-batch'\nimport { errorName } from '../../intercept/classify'\nimport { boundaryDiagnostics } from '../../intercept/codes'\nimport type { ColumnValues } from '../../pipeline/types'\nimport { type DataMode, modeOption } from '../../source/mode'\nimport { columnFiltersDiagnostics } from './codes'\nimport {\n BAD,\n type Compiled,\n compile,\n MATCH,\n NO,\n type Outcome,\n type RawFilter,\n type Test,\n typeName,\n} from './match'\nimport type { ColumnFiltersState, ColumnFiltersTypes } from './types'\n\nkeepDiagnostics(columnFiltersDiagnostics, boundaryDiagnostics)\n\nexport interface ColumnFiltersOptions {\n /**\n * `'server'`: the server filters (the slice goes to `options.fetcher` as `filters`, or the app\n * fetches); core skips this step (§7.1). Default `'client'`.\n */\n mode?: DataMode\n}\n\nconst columnFiltersOptions = { mode: modeOption } as const\n\n/** Position of column `id` in `options.columns` (diagnostic `where`); `-1` if absent. */\nfunction columnIndex(columns: readonly unknown[], id: string): number {\n for (let i = 0; i < columns.length; i++) {\n const c = columns[i] as { key?: unknown; id?: unknown } | null | undefined\n if (c && typeof c === 'object' && (typeof c.key === 'string' ? c.key : c.id) === id) return i\n }\n return -1\n}\n\nfunction columnIds(columns: readonly unknown[]): string[] {\n const out: string[] = []\n for (const c of columns as readonly ({ key?: unknown; id?: unknown } | null)[]) {\n const id = c && (typeof c.key === 'string' ? c.key : c.id)\n if (typeof id === 'string') out.push(id)\n }\n return out\n}\n\n/** Nullish and `NaN` (nullish in a numeric column cache) match no active filter (§6.1). */\nconst absent = (v: unknown) =>\n v === undefined || v === null || (typeof v === 'number' && Number.isNaN(v))\n\n/** One cell: an array cell matches if any element matches, else `BAD` if one had a wrong type. */\nfunction testCell(test: Test, v: unknown, r: number): Outcome {\n if (!Array.isArray(v)) return test(v, r)\n let out: Outcome = NO\n for (let i = 0; i < v.length; i++) {\n const e = v[i]\n if (absent(e)) continue\n const o = test(e, -1)\n if (o === MATCH) return MATCH\n if (o === BAD) out = BAD\n }\n return out\n}\n\n/**\n * Per-batch failures, recorded as numbers (§3.4) and reused across runs: wrong-type cells, or\n * throwing `custom` calls. Text and `where` are built only when something failed.\n */\nconst failures = new CellFailures()\nconst first = { type: '', error: undefined as unknown }\n\n/** Keeps the rows of `rows[0..n)` that pass `c`, compacted into `out` (may be `rows`). */\nfunction apply(\n c: Extract<Compiled, { kind: 'test' | 'custom' }>,\n values: ColumnValues,\n data: readonly unknown[],\n rows: Uint32Array,\n n: number,\n out: Uint32Array,\n): number {\n let k = 0\n if (c.kind === 'custom') {\n const { fn, value } = c\n for (let i = 0; i < n; i++) {\n const r = rows[i] as number\n const v = values[r]\n if (absent(v)) continue\n try {\n if (fn(v, value, data[r])) out[k++] = r\n } catch (e) {\n if (failures.count === 0) first.error = e\n failures.add(r, e)\n }\n }\n return k\n }\n const test = c.test\n for (let i = 0; i < n; i++) {\n const r = rows[i] as number\n const v = values[r]\n if (absent(v)) continue\n const o = testCell(test, v, r)\n if (o === MATCH) out[k++] = r\n else if (o === BAD) {\n if (failures.count === 0) first.type = typeName(v)\n failures.add(r, undefined)\n }\n }\n return k\n}\n\nfunction report(ctx: RowModelContext, c: Compiled, id: string, kind: string, index: number) {\n if (c.kind === 'custom') {\n // Like a column:batch filterFn failure (§3.4); `where` is the filter's state path.\n const name = errorName(first.error)\n ctx.emit(\n 'DK_CALLBACK_FAILED',\n {\n slot: `state.columnFilters.${id}.fn`,\n column: id,\n rows: Array.from(failures.sample()),\n count: failures.count,\n fallback: 'rows-unmatched',\n ...(name === undefined ? {} : { errorName: name }),\n },\n { cause: first.error },\n )\n } else if (c.kind === 'test') {\n ctx.emit('DK_FILTER_TYPE_MISMATCH', {\n column: id,\n index,\n filter: kind,\n source: 'cell',\n expected: c.expected,\n received: first.type,\n count: failures.count,\n rows: Array.from(failures.sample()),\n })\n }\n}\n\n/**\n * Applies every active column filter (AND) to `indices`: one pass per filtered column over the\n * column cache (§9.1), no per-cell allocation. Returns `indices` itself when nothing is active;\n * otherwise a new array (`indices` is never mutated).\n */\nfunction filterRows(\n indices: Uint32Array,\n state: Readonly<Record<string, unknown>> | undefined,\n ctx: RowModelContext,\n): Uint32Array {\n if (!state || typeof state !== 'object') return indices\n let out: Uint32Array | undefined\n let n = indices.length\n for (const id of Object.keys(state)) {\n const filter = state[id]\n if (filter === undefined) continue\n const values = ctx.getColumnValues(id)\n if (!values) {\n // Not a column (§7.2): ignored. No columns at all (zero-config, no rows yet) is not reported.\n if (ctx.columns.length > 0) {\n ctx.emit('DK_STATE_UNKNOWN_KEY', {\n slice: 'columnFilters',\n key: id,\n known: columnIds(ctx.columns),\n })\n }\n continue\n }\n const c = compile(filter, values)\n if (c.kind === 'off') continue\n const index = columnIndex(ctx.columns, id)\n if (c.kind === 'bad') {\n ctx.emit('DK_FILTER_TYPE_MISMATCH', {\n column: id,\n index,\n filter: c.filter,\n source: 'filter',\n expected: c.expected,\n received: c.received,\n count: 0,\n rows: [],\n })\n return new Uint32Array(0)\n }\n const src = out ?? indices\n out ??= new Uint32Array(indices.length)\n failures.reset()\n first.error = undefined\n try {\n if (c.kind === 'custom') {\n // A user callback per cell: one `filterFn` column:batch (§3.4). A skipped batch keeps\n // the slot fallback: no row matches.\n const rows = src.subarray(0, n)\n const dst = out\n let k = 0\n ctx.batch(id, 'filterFn', rows, () => {\n k = apply(c, values, ctx.data, rows, rows.length, dst)\n })\n n = k\n } else n = apply(c, values, ctx.data, src, n, out)\n if (failures.count > 0) report(ctx, c, id, String((filter as RawFilter).type), index)\n } finally {\n failures.reset()\n first.error = undefined\n }\n }\n return out ? out.subarray(0, n) : indices\n}\n\n/**\n * Column filters (§6.1): `equals`, `contains` (case-insensitive, NFC), `in`, `range` (inclusive),\n * `custom`. Filters combine with AND; nullish cells match no active filter; array cells match if\n * any element matches; a cell of the wrong runtime type is a non-match + `DK_FILTER_TYPE_MISMATCH`.\n *\n * ```ts\n * const table = createTable({ data: users, columns, features: [columnFilters()] })\n * table.setColumnFilter('age', { type: 'range', value: { min: 18 } })\n * table.setColumnFilter('name', { type: 'contains', value: 'ada' })\n * ```\n */\nexport const columnFilters = (opts: ColumnFiltersOptions = {}) =>\n defineFeature<'columnFilters', ColumnFiltersTypes>({\n name: 'columnFilters',\n stage: 'filter',\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\") ? columnFiltersOptions : ({} as typeof columnFiltersOptions),\n opts,\n ),\n initialState: () => ({}),\n createApi: (ctx) => ({\n setColumnFilter: (id, filter) =>\n ctx.setState((prev) => {\n // No change (the same filter, or clearing a column without one): keep the slice.\n if ((prev as Record<string, unknown>)[id] === filter) return prev\n const next: Record<string, unknown> = { ...prev }\n if (filter === undefined) delete next[id]\n else next[id] = filter\n return next as ColumnFiltersState<unknown>\n }),\n resetColumnFilters: () => ctx.setState((prev) => (Object.keys(prev).length ? {} : prev)),\n }),\n rowModel: filterRows,\n })\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/features/column-filters/column-filters.ts"],"sourcesContent":["import { defineOptions } from '../../catalog/options'\nimport { keepDiagnostics } from '../../diagnostics/catalog'\nimport '../../diagnostics/process-guard'\nimport { describeValue } from '../../diagnostics/redact'\nimport { defineFeature } from '../../feature/define'\nimport { type SliceFix, sliceGuard } from '../../feature/slice-guard'\nimport type { RowModelContext } from '../../feature/types'\nimport { CellFailures } from '../../intercept/cell-batch'\nimport { errorName } from '../../intercept/classify'\nimport { boundaryDiagnostics } from '../../intercept/codes'\nimport type { ColumnValues } from '../../pipeline/types'\nimport { type DataMode, modeOption } from '../../source/mode'\nimport { columnFiltersDiagnostics } from './codes'\nimport {\n BAD,\n type Compiled,\n compile,\n MATCH,\n NO,\n type Outcome,\n type RawFilter,\n type Test,\n typeName,\n} from './match'\nimport type { ColumnFiltersState, ColumnFiltersTypes } from './types'\n\nkeepDiagnostics(columnFiltersDiagnostics, boundaryDiagnostics)\n\nexport interface ColumnFiltersOptions {\n /**\n * `'server'`: the server filters (the slice goes to `options.fetcher` as `filters`, or the app\n * fetches); core skips this step (§7.1). Default `'client'`.\n */\n mode?: DataMode\n}\n\nconst columnFiltersOptions = { mode: modeOption } as const\n\n/** Position of column `id` in `options.columns` (diagnostic `where`); `-1` if absent. */\nfunction columnIndex(columns: readonly unknown[], id: string): number {\n for (let i = 0; i < columns.length; i++) {\n const c = columns[i] as { key?: unknown; id?: unknown } | null | undefined\n if (c && typeof c === 'object' && (typeof c.key === 'string' ? c.key : c.id) === id) return i\n }\n return -1\n}\n\nfunction columnIds(columns: readonly unknown[]): string[] {\n const out: string[] = []\n for (const c of columns as readonly ({ key?: unknown; id?: unknown } | null)[]) {\n const id = c && (typeof c.key === 'string' ? c.key : c.id)\n if (typeof id === 'string') out.push(id)\n }\n return out\n}\n\n/** Nullish and `NaN` (nullish in a numeric column cache) match no active filter (§6.1). */\nconst absent = (v: unknown) =>\n v === undefined || v === null || (typeof v === 'number' && Number.isNaN(v))\n\n/** One cell: an array cell matches if any element matches, else `BAD` if one had a wrong type. */\nfunction testCell(test: Test, v: unknown, r: number): Outcome {\n if (!Array.isArray(v)) return test(v, r)\n let out: Outcome = NO\n for (let i = 0; i < v.length; i++) {\n const e = v[i]\n if (absent(e)) continue\n const o = test(e, -1)\n if (o === MATCH) return MATCH\n if (o === BAD) out = BAD\n }\n return out\n}\n\n/**\n * Per-batch failures, recorded as numbers (§3.4) and reused across runs: wrong-type cells, or\n * throwing `custom` calls. Text and `where` are built only when something failed.\n */\nconst failures = new CellFailures()\nconst first = { type: '', error: undefined as unknown }\n\n/** Keeps the rows of `rows[0..n)` that pass `c`, compacted into `out` (may be `rows`). */\nfunction apply(\n c: Extract<Compiled, { kind: 'test' | 'custom' }>,\n values: ColumnValues,\n data: readonly unknown[],\n rows: Uint32Array,\n n: number,\n out: Uint32Array,\n): number {\n let k = 0\n if (c.kind === 'custom') {\n const { fn, value } = c\n for (let i = 0; i < n; i++) {\n const r = rows[i] as number\n const v = values[r]\n if (absent(v)) continue\n try {\n if (fn(v, value, data[r])) out[k++] = r\n } catch (e) {\n if (failures.count === 0) first.error = e\n failures.add(r, e)\n }\n }\n return k\n }\n const test = c.test\n for (let i = 0; i < n; i++) {\n const r = rows[i] as number\n const v = values[r]\n if (absent(v)) continue\n const o = testCell(test, v, r)\n if (o === MATCH) out[k++] = r\n else if (o === BAD) {\n if (failures.count === 0) first.type = typeName(v)\n failures.add(r, undefined)\n }\n }\n return k\n}\n\nfunction report(ctx: RowModelContext, c: Compiled, id: string, kind: string, index: number) {\n if (c.kind === 'custom') {\n // Like a column:batch filterFn failure (§3.4); `where` is the filter's state path.\n const name = errorName(first.error)\n ctx.emit(\n 'DK_CALLBACK_FAILED',\n {\n slot: `state.columnFilters.${id}.fn`,\n column: id,\n rows: Array.from(failures.sample()),\n count: failures.count,\n fallback: 'rows-unmatched',\n ...(name === undefined ? {} : { errorName: name }),\n },\n { cause: first.error },\n )\n } else if (c.kind === 'test') {\n ctx.emit('DK_FILTER_TYPE_MISMATCH', {\n column: id,\n index,\n filter: kind,\n source: 'cell',\n expected: c.expected,\n received: first.type,\n count: failures.count,\n rows: Array.from(failures.sample()),\n })\n }\n}\n\n/**\n * Applies every active column filter (AND) to `indices`: one pass per filtered column over the\n * column cache (§9.1), no per-cell allocation. Returns `indices` itself when nothing is active;\n * otherwise a new array (`indices` is never mutated).\n */\nfunction filterRows(\n indices: Uint32Array,\n state: Readonly<Record<string, unknown>> | undefined,\n ctx: RowModelContext,\n): Uint32Array {\n if (!state || typeof state !== 'object') return indices\n let out: Uint32Array | undefined\n let n = indices.length\n for (const id of Object.keys(state)) {\n const filter = state[id]\n if (filter === undefined) continue\n const values = ctx.getColumnValues(id)\n if (!values) {\n // Not a column (§7.2): ignored. No columns at all (zero-config, no rows yet) is not reported.\n if (ctx.columns.length > 0) {\n ctx.emit('DK_STATE_UNKNOWN_KEY', {\n slice: 'columnFilters',\n key: id,\n known: columnIds(ctx.columns),\n })\n }\n continue\n }\n const c = compile(filter, values)\n if (c.kind === 'off') continue\n const index = columnIndex(ctx.columns, id)\n if (c.kind === 'bad') {\n ctx.emit('DK_FILTER_TYPE_MISMATCH', {\n column: id,\n index,\n filter: c.filter,\n source: 'filter',\n expected: c.expected,\n received: c.received,\n count: 0,\n rows: [],\n })\n return new Uint32Array(0)\n }\n const src = out ?? indices\n out ??= new Uint32Array(indices.length)\n failures.reset()\n first.error = undefined\n try {\n if (c.kind === 'custom') {\n // A user callback per cell: one `filterFn` column:batch (§3.4). A skipped batch keeps\n // the slot fallback: no row matches.\n const rows = src.subarray(0, n)\n const dst = out\n let k = 0\n ctx.batch(id, 'filterFn', rows, () => {\n k = apply(c, values, ctx.data, rows, rows.length, dst)\n })\n n = k\n } else n = apply(c, values, ctx.data, src, n, out)\n if (failures.count > 0) report(ctx, c, id, String((filter as RawFilter).type), index)\n } finally {\n failures.reset()\n first.error = undefined\n }\n }\n return out ? out.subarray(0, n) : indices\n}\n\nconst isSlice = (s: unknown): s is Record<string, unknown> =>\n s !== null && typeof s === 'object' && !Array.isArray(s)\n\n/** A restored slice that is not an object (e.g. `null`) is replaced with no filters (#89). */\nconst fixSlice = (s: unknown): SliceFix<ColumnFiltersState<unknown>> | undefined =>\n isSlice(s) ? undefined : [{}, describeValue(s), '{}']\n\n/**\n * Column filters (§6.1): `equals`, `contains` (case-insensitive, NFC), `in`, `range` (inclusive),\n * `custom`. Filters combine with AND; nullish cells match no active filter; array cells match if\n * any element matches; a cell of the wrong runtime type is a non-match + `DK_FILTER_TYPE_MISMATCH`.\n *\n * ```ts\n * const table = createTable({ data: users, columns, features: [columnFilters()] })\n * table.setColumnFilter('age', { type: 'range', value: { min: 18 } })\n * table.setColumnFilter('name', { type: 'contains', value: 'ada' })\n * ```\n */\nexport const columnFilters = (opts: ColumnFiltersOptions = {}) => {\n // Dev only (§9.3): production reads a malformed slice as no filters and stays cheap.\n const guard = (process.env.NODE_ENV !== \"production\") ? sliceGuard('columnFilters', fixSlice) : undefined\n return defineFeature<'columnFilters', ColumnFiltersTypes>({\n name: 'columnFilters',\n stage: 'filter',\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\") ? columnFiltersOptions : ({} as typeof columnFiltersOptions),\n opts,\n ),\n ...(guard ? { interceptors: [guard.interceptor] } : undefined),\n initialState: () => ({}),\n createApi: (ctx) => {\n guard?.install(ctx)\n // Values, not updaters: a library step is never reported as a failing user `updater` (#89).\n return {\n setColumnFilter: (id, filter) => {\n // A controlled slice not fed back yet may still be malformed: it reads as no filters.\n const s = ctx.getState()\n const prev: Record<string, unknown> = s && typeof s === 'object' ? s : {}\n // No change (the same filter, or clearing a column without one): keep the slice.\n if (prev[id] === filter) return\n // A computed key defines an own property: `\"__proto__\"` never swaps the prototype\n // (#102); the row model reports it as DK_STATE_UNKNOWN_KEY like any unknown column.\n const next: Record<string, unknown> = { ...prev, [id]: filter }\n if (filter === undefined) delete next[id]\n ctx.setState(next as ColumnFiltersState<unknown>)\n },\n resetColumnFilters: () => {\n const s = ctx.getState()\n if (!s || Object.keys(s).length > 0) ctx.setState({})\n },\n }\n },\n rowModel: filterRows,\n })\n}\n"],"mappings":"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","names":[]}
@@ -44,11 +44,13 @@ function foldedFor(values) {
44
44
  }
45
45
  return f;
46
46
  }
47
+ var isNaNValue = (v) => typeof v === "number" && Number.isNaN(v);
47
48
  var bad = (filter, expected, received) => ({
48
49
  kind: "bad",
49
50
  filter,
50
51
  expected,
51
- received: typeName(received)
52
+ // `number(NaN)`, as `describeValue` names it: "a number, expects a number" would read as a bug.
53
+ received: isNaNValue(received) ? "number(NaN)" : typeName(received)
52
54
  });
53
55
  function equals(fv) {
54
56
  if (empty(fv)) return OFF;
@@ -60,6 +62,7 @@ function equals(fv) {
60
62
  test: (e) => e instanceof Date ? e.getTime() === t ? MATCH : NO : BAD
61
63
  };
62
64
  }
65
+ if (isNaNValue(fv)) return bad("equals", "not-nan", fv);
63
66
  const type = typeof fv;
64
67
  if (type === "object" || type === "function" || type === "symbol") {
65
68
  return bad("equals", "prim-or-date", fv);
@@ -120,7 +123,8 @@ function inList(fv) {
120
123
  }
121
124
  function bound(v) {
122
125
  if (empty(v)) return null;
123
- if (typeof v === "number" || typeof v === "bigint") return { family: "number", at: v };
126
+ if (typeof v === "number" && !Number.isNaN(v) || typeof v === "bigint")
127
+ return { family: "number", at: v };
124
128
  if (v instanceof Date && !Number.isNaN(v.getTime())) return { family: "Date", at: v.getTime() };
125
129
  return false;
126
130
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/column-filters/match.ts"],"sourcesContent":["import type { ColumnValues } from '../../pipeline/types'\nimport { FILTER_KINDS } from './codes'\n\n/** Result of testing one value: a match, no match, or a value of the wrong type (non-match). */\nexport const NO = 0\nexport const MATCH = 1\nexport const BAD = 2\nexport type Outcome = typeof NO | typeof MATCH | typeof BAD\n\n/**\n * Tests one non-nullish value. `r` is the row index when the value is the whole cell, `-1` for an\n * element of an array cell (per-row caches apply to whole cells only).\n */\nexport type Test = (value: unknown, r: number) => Outcome\n\n/** A runtime filter, as read from state (runtime JSON ignores the types). */\nexport interface RawFilter {\n readonly type?: unknown\n readonly value?: unknown\n readonly fn?: unknown\n}\n\nexport type Compiled =\n /** Inactive: the step skips it. */\n | { readonly kind: 'off' }\n /** A value test with what it accepts (for `DK_FILTER_TYPE_MISMATCH`). */\n | { readonly kind: 'test'; readonly test: Test; readonly expected: string }\n | {\n readonly kind: 'custom'\n readonly fn: (v: unknown, fv: unknown, row: unknown) => unknown\n readonly value: unknown\n }\n /** Malformed filter value: matches nothing; reported once with `source: 'filter'`. */\n | {\n readonly kind: 'bad'\n readonly filter: string\n readonly expected: string\n readonly received: string\n }\n\nconst OFF: Compiled = { kind: 'off' }\n\n/** A short type name for diagnostics: `null`, `array`, `Date`, else `typeof`. Never a value. */\nexport const typeName = (v: unknown): string =>\n v === null ? 'null' : Array.isArray(v) ? 'array' : v instanceof Date ? 'Date' : typeof v\n\n/** `undefined`, `null` and `''` leave a filter (or a range bound) inactive (§6.1). */\nconst empty = (v: unknown) => v === undefined || v === null || v === ''\n\n/** Case-insensitive, NFC-normalised form for `contains` (Thai combining marks compare correctly). */\nconst fold = (s: string) => s.toLowerCase().normalize('NFC')\n\n/**\n * Folded cell strings per column-values array: the column cache returns the same array for the same\n * data and column, so typing into a `contains` filter folds each cell once.\n */\nconst folded = new WeakMap<ColumnValues, (string | undefined)[]>()\n\nfunction foldedFor(values: ColumnValues): (string | undefined)[] {\n let f = folded.get(values)\n if (!f) {\n f = new Array<string | undefined>(values.length)\n folded.set(values, f)\n }\n return f\n}\n\nconst bad = (filter: string, expected: string, received: unknown): Compiled => ({\n kind: 'bad',\n filter,\n expected,\n received: typeName(received),\n})\n\nfunction equals(fv: unknown): Compiled {\n if (empty(fv)) return OFF\n if (fv instanceof Date) {\n const t = fv.getTime()\n return {\n kind: 'test',\n expected: 'Date',\n test: (e) => (e instanceof Date ? (e.getTime() === t ? MATCH : NO) : BAD),\n }\n }\n const type = typeof fv\n if (type === 'object' || type === 'function' || type === 'symbol') {\n return bad('equals', 'prim-or-date', fv)\n }\n return {\n kind: 'test',\n expected: type,\n test: (e) => (typeof e === type ? (e === fv ? MATCH : NO) : BAD),\n }\n}\n\nfunction contains(fv: unknown, values: ColumnValues): Compiled {\n if (empty(fv)) return OFF\n if (typeof fv !== 'string') return bad('contains', 'string', fv)\n const needle = fold(fv)\n const cache = foldedFor(values)\n return {\n kind: 'test',\n expected: 'string',\n test: (e, r) => {\n if (typeof e !== 'string') return BAD\n let s: string | undefined\n if (r < 0) s = fold(e)\n else {\n s = cache[r]\n if (s === undefined) {\n s = fold(e)\n cache[r] = s\n }\n }\n return s.includes(needle) ? MATCH : NO\n },\n }\n}\n\nfunction inList(fv: unknown): Compiled {\n if (fv === undefined || fv === null) return OFF\n if (!Array.isArray(fv)) return bad('in', 'array', fv)\n const prims = new Set<unknown>()\n const times = new Set<number>()\n const types = new Set<string>()\n for (const item of fv as unknown[]) {\n if (item === undefined || item === null) continue\n if (item instanceof Date) times.add(item.getTime())\n else if (typeof item === 'object' || typeof item === 'function') {\n return bad('in', 'prims-or-dates', item)\n } else prims.add(item)\n types.add(typeName(item))\n }\n if (types.size === 0) return OFF\n const hasDate = times.size > 0\n return {\n kind: 'test',\n expected: [...types].join(' | '),\n test: (e) => {\n if (e instanceof Date) return hasDate ? (times.has(e.getTime()) ? MATCH : NO) : BAD\n return types.has(typeof e) ? (prims.has(e) ? MATCH : NO) : BAD\n },\n }\n}\n\ntype Family = 'number' | 'Date'\n\n/** A range bound: absent, its family + comparable value, or malformed. */\nfunction bound(v: unknown): { family: Family; at: number | bigint } | null | false {\n if (empty(v)) return null\n if (typeof v === 'number' || typeof v === 'bigint') return { family: 'number', at: v }\n if (v instanceof Date && !Number.isNaN(v.getTime())) return { family: 'Date', at: v.getTime() }\n return false\n}\n\nconst NUMERIC = 'number | bigint'\n\nfunction range(fv: unknown): Compiled {\n if (fv === undefined || fv === null) return OFF\n if (typeof fv !== 'object' || Array.isArray(fv) || fv instanceof Date) {\n return bad('range', '{ min?, max? }', fv)\n }\n const { min: rawMin, max: rawMax } = fv as { min?: unknown; max?: unknown }\n const min = bound(rawMin)\n const max = bound(rawMax)\n // `expected` prose keys (diagnostics/prose.ts).\n if (min === false) return bad('range', 'range-bound', rawMin)\n if (max === false) return bad('range', 'range-bound', rawMax)\n if (!min && !max) return OFF\n const family = (min || max) as { family: Family }\n if (min && max && min.family !== max.family) {\n return bad('range', `same-type|${min.family}`, rawMax)\n }\n // Mixed number / bigint comparisons are exact in JS (`3n >= 2.5`).\n const lo = min ? (min.at as number) : undefined\n const hi = max ? (max.at as number) : undefined\n const inside = (x: number) => (lo === undefined || x >= lo) && (hi === undefined || x <= hi)\n if (family.family === 'Date') {\n return {\n kind: 'test',\n expected: 'Date',\n test: (e) => (e instanceof Date ? (inside(e.getTime()) ? MATCH : NO) : BAD),\n }\n }\n return {\n kind: 'test',\n expected: NUMERIC,\n test: (e) =>\n typeof e === 'number' || typeof e === 'bigint' ? (inside(e as number) ? MATCH : NO) : BAD,\n }\n}\n\n/** Compiles one column's filter for this run (cold: once per active filter per run). */\nexport function compile(filter: unknown, values: ColumnValues): Compiled {\n if (filter === undefined || filter === null) return OFF\n if (typeof filter !== 'object') return bad('(none)', '{ type, value }', filter)\n const f = filter as RawFilter\n switch (f.type) {\n case 'equals':\n return equals(f.value)\n case 'contains':\n return contains(f.value, values)\n case 'in':\n return inList(f.value)\n case 'range':\n return range(f.value)\n case 'custom':\n return typeof f.fn === 'function'\n ? {\n kind: 'custom',\n fn: f.fn as (v: unknown, fv: unknown, row: unknown) => unknown,\n value: f.value,\n }\n : bad('custom', 'fn: (value, filterValue, row) => boolean', f.fn)\n default:\n return {\n kind: 'bad',\n filter: typeof f.type === 'string' ? f.type : '(none)',\n expected: FILTER_KINDS.join(' | '),\n received: 'unknown filter kind',\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/features/column-filters/match.ts"],"sourcesContent":["import type { ColumnValues } from '../../pipeline/types'\nimport { FILTER_KINDS } from './codes'\n\n/** Result of testing one value: a match, no match, or a value of the wrong type (non-match). */\nexport const NO = 0\nexport const MATCH = 1\nexport const BAD = 2\nexport type Outcome = typeof NO | typeof MATCH | typeof BAD\n\n/**\n * Tests one non-nullish value. `r` is the row index when the value is the whole cell, `-1` for an\n * element of an array cell (per-row caches apply to whole cells only).\n */\nexport type Test = (value: unknown, r: number) => Outcome\n\n/** A runtime filter, as read from state (runtime JSON ignores the types). */\nexport interface RawFilter {\n readonly type?: unknown\n readonly value?: unknown\n readonly fn?: unknown\n}\n\nexport type Compiled =\n /** Inactive: the step skips it. */\n | { readonly kind: 'off' }\n /** A value test with what it accepts (for `DK_FILTER_TYPE_MISMATCH`). */\n | { readonly kind: 'test'; readonly test: Test; readonly expected: string }\n | {\n readonly kind: 'custom'\n readonly fn: (v: unknown, fv: unknown, row: unknown) => unknown\n readonly value: unknown\n }\n /** Malformed filter value: matches nothing; reported once with `source: 'filter'`. */\n | {\n readonly kind: 'bad'\n readonly filter: string\n readonly expected: string\n readonly received: string\n }\n\nconst OFF: Compiled = { kind: 'off' }\n\n/** A short type name for diagnostics: `null`, `array`, `Date`, else `typeof`. Never a value. */\nexport const typeName = (v: unknown): string =>\n v === null ? 'null' : Array.isArray(v) ? 'array' : v instanceof Date ? 'Date' : typeof v\n\n/** `undefined`, `null` and `''` leave a filter (or a range bound) inactive (§6.1). */\nconst empty = (v: unknown) => v === undefined || v === null || v === ''\n\n/** Case-insensitive, NFC-normalised form for `contains` (Thai combining marks compare correctly). */\nconst fold = (s: string) => s.toLowerCase().normalize('NFC')\n\n/**\n * Folded cell strings per column-values array: the column cache returns the same array for the same\n * data and column, so typing into a `contains` filter folds each cell once.\n */\nconst folded = new WeakMap<ColumnValues, (string | undefined)[]>()\n\nfunction foldedFor(values: ColumnValues): (string | undefined)[] {\n let f = folded.get(values)\n if (!f) {\n f = new Array<string | undefined>(values.length)\n folded.set(values, f)\n }\n return f\n}\n\nconst isNaNValue = (v: unknown) => typeof v === 'number' && Number.isNaN(v)\n\nconst bad = (filter: string, expected: string, received: unknown): Compiled => ({\n kind: 'bad',\n filter,\n expected,\n // `number(NaN)`, as `describeValue` names it: \"a number, expects a number\" would read as a bug.\n received: isNaNValue(received) ? 'number(NaN)' : typeName(received),\n})\n\nfunction equals(fv: unknown): Compiled {\n if (empty(fv)) return OFF\n if (fv instanceof Date) {\n const t = fv.getTime()\n return {\n kind: 'test',\n expected: 'Date',\n test: (e) => (e instanceof Date ? (e.getTime() === t ? MATCH : NO) : BAD),\n }\n }\n // NaN equals nothing (`NaN !== NaN`): reported, not a silent empty table (#91).\n if (isNaNValue(fv)) return bad('equals', 'not-nan', fv)\n const type = typeof fv\n if (type === 'object' || type === 'function' || type === 'symbol') {\n return bad('equals', 'prim-or-date', fv)\n }\n return {\n kind: 'test',\n expected: type,\n test: (e) => (typeof e === type ? (e === fv ? MATCH : NO) : BAD),\n }\n}\n\nfunction contains(fv: unknown, values: ColumnValues): Compiled {\n if (empty(fv)) return OFF\n if (typeof fv !== 'string') return bad('contains', 'string', fv)\n const needle = fold(fv)\n const cache = foldedFor(values)\n return {\n kind: 'test',\n expected: 'string',\n test: (e, r) => {\n if (typeof e !== 'string') return BAD\n let s: string | undefined\n if (r < 0) s = fold(e)\n else {\n s = cache[r]\n if (s === undefined) {\n s = fold(e)\n cache[r] = s\n }\n }\n return s.includes(needle) ? MATCH : NO\n },\n }\n}\n\nfunction inList(fv: unknown): Compiled {\n if (fv === undefined || fv === null) return OFF\n if (!Array.isArray(fv)) return bad('in', 'array', fv)\n const prims = new Set<unknown>()\n const times = new Set<number>()\n const types = new Set<string>()\n for (const item of fv as unknown[]) {\n if (item === undefined || item === null) continue\n if (item instanceof Date) times.add(item.getTime())\n else if (typeof item === 'object' || typeof item === 'function') {\n return bad('in', 'prims-or-dates', item)\n } else prims.add(item)\n types.add(typeName(item))\n }\n if (types.size === 0) return OFF\n const hasDate = times.size > 0\n return {\n kind: 'test',\n expected: [...types].join(' | '),\n test: (e) => {\n if (e instanceof Date) return hasDate ? (times.has(e.getTime()) ? MATCH : NO) : BAD\n return types.has(typeof e) ? (prims.has(e) ? MATCH : NO) : BAD\n },\n }\n}\n\ntype Family = 'number' | 'Date'\n\n/**\n * A range bound: absent, its family + comparable value, or malformed. NaN is malformed: every\n * comparison with it is false, so it would hide every row without a report (#91).\n */\nfunction bound(v: unknown): { family: Family; at: number | bigint } | null | false {\n if (empty(v)) return null\n if ((typeof v === 'number' && !Number.isNaN(v)) || typeof v === 'bigint')\n return { family: 'number', at: v }\n if (v instanceof Date && !Number.isNaN(v.getTime())) return { family: 'Date', at: v.getTime() }\n return false\n}\n\nconst NUMERIC = 'number | bigint'\n\nfunction range(fv: unknown): Compiled {\n if (fv === undefined || fv === null) return OFF\n if (typeof fv !== 'object' || Array.isArray(fv) || fv instanceof Date) {\n return bad('range', '{ min?, max? }', fv)\n }\n const { min: rawMin, max: rawMax } = fv as { min?: unknown; max?: unknown }\n const min = bound(rawMin)\n const max = bound(rawMax)\n // `expected` prose keys (diagnostics/prose.ts).\n if (min === false) return bad('range', 'range-bound', rawMin)\n if (max === false) return bad('range', 'range-bound', rawMax)\n if (!min && !max) return OFF\n const family = (min || max) as { family: Family }\n if (min && max && min.family !== max.family) {\n return bad('range', `same-type|${min.family}`, rawMax)\n }\n // Mixed number / bigint comparisons are exact in JS (`3n >= 2.5`).\n const lo = min ? (min.at as number) : undefined\n const hi = max ? (max.at as number) : undefined\n const inside = (x: number) => (lo === undefined || x >= lo) && (hi === undefined || x <= hi)\n if (family.family === 'Date') {\n return {\n kind: 'test',\n expected: 'Date',\n test: (e) => (e instanceof Date ? (inside(e.getTime()) ? MATCH : NO) : BAD),\n }\n }\n return {\n kind: 'test',\n expected: NUMERIC,\n test: (e) =>\n typeof e === 'number' || typeof e === 'bigint' ? (inside(e as number) ? MATCH : NO) : BAD,\n }\n}\n\n/** Compiles one column's filter for this run (cold: once per active filter per run). */\nexport function compile(filter: unknown, values: ColumnValues): Compiled {\n if (filter === undefined || filter === null) return OFF\n if (typeof filter !== 'object') return bad('(none)', '{ type, value }', filter)\n const f = filter as RawFilter\n switch (f.type) {\n case 'equals':\n return equals(f.value)\n case 'contains':\n return contains(f.value, values)\n case 'in':\n return inList(f.value)\n case 'range':\n return range(f.value)\n case 'custom':\n return typeof f.fn === 'function'\n ? {\n kind: 'custom',\n fn: f.fn as (v: unknown, fv: unknown, row: unknown) => unknown,\n value: f.value,\n }\n : bad('custom', 'fn: (value, filterValue, row) => boolean', f.fn)\n default:\n return {\n kind: 'bad',\n filter: typeof f.type === 'string' ? f.type : '(none)',\n expected: FILTER_KINDS.join(' | '),\n received: 'unknown filter kind',\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -16,11 +16,13 @@ function foldedFor(values) {
16
16
  }
17
17
  return f;
18
18
  }
19
+ var isNaNValue = (v) => typeof v === "number" && Number.isNaN(v);
19
20
  var bad = (filter, expected, received) => ({
20
21
  kind: "bad",
21
22
  filter,
22
23
  expected,
23
- received: typeName(received)
24
+ // `number(NaN)`, as `describeValue` names it: "a number, expects a number" would read as a bug.
25
+ received: isNaNValue(received) ? "number(NaN)" : typeName(received)
24
26
  });
25
27
  function equals(fv) {
26
28
  if (empty(fv)) return OFF;
@@ -32,6 +34,7 @@ function equals(fv) {
32
34
  test: (e) => e instanceof Date ? e.getTime() === t ? MATCH : NO : BAD
33
35
  };
34
36
  }
37
+ if (isNaNValue(fv)) return bad("equals", "not-nan", fv);
35
38
  const type = typeof fv;
36
39
  if (type === "object" || type === "function" || type === "symbol") {
37
40
  return bad("equals", "prim-or-date", fv);
@@ -92,7 +95,8 @@ function inList(fv) {
92
95
  }
93
96
  function bound(v) {
94
97
  if (empty(v)) return null;
95
- if (typeof v === "number" || typeof v === "bigint") return { family: "number", at: v };
98
+ if (typeof v === "number" && !Number.isNaN(v) || typeof v === "bigint")
99
+ return { family: "number", at: v };
96
100
  if (v instanceof Date && !Number.isNaN(v.getTime())) return { family: "Date", at: v.getTime() };
97
101
  return false;
98
102
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/column-filters/match.ts"],"sourcesContent":["import type { ColumnValues } from '../../pipeline/types'\nimport { FILTER_KINDS } from './codes'\n\n/** Result of testing one value: a match, no match, or a value of the wrong type (non-match). */\nexport const NO = 0\nexport const MATCH = 1\nexport const BAD = 2\nexport type Outcome = typeof NO | typeof MATCH | typeof BAD\n\n/**\n * Tests one non-nullish value. `r` is the row index when the value is the whole cell, `-1` for an\n * element of an array cell (per-row caches apply to whole cells only).\n */\nexport type Test = (value: unknown, r: number) => Outcome\n\n/** A runtime filter, as read from state (runtime JSON ignores the types). */\nexport interface RawFilter {\n readonly type?: unknown\n readonly value?: unknown\n readonly fn?: unknown\n}\n\nexport type Compiled =\n /** Inactive: the step skips it. */\n | { readonly kind: 'off' }\n /** A value test with what it accepts (for `DK_FILTER_TYPE_MISMATCH`). */\n | { readonly kind: 'test'; readonly test: Test; readonly expected: string }\n | {\n readonly kind: 'custom'\n readonly fn: (v: unknown, fv: unknown, row: unknown) => unknown\n readonly value: unknown\n }\n /** Malformed filter value: matches nothing; reported once with `source: 'filter'`. */\n | {\n readonly kind: 'bad'\n readonly filter: string\n readonly expected: string\n readonly received: string\n }\n\nconst OFF: Compiled = { kind: 'off' }\n\n/** A short type name for diagnostics: `null`, `array`, `Date`, else `typeof`. Never a value. */\nexport const typeName = (v: unknown): string =>\n v === null ? 'null' : Array.isArray(v) ? 'array' : v instanceof Date ? 'Date' : typeof v\n\n/** `undefined`, `null` and `''` leave a filter (or a range bound) inactive (§6.1). */\nconst empty = (v: unknown) => v === undefined || v === null || v === ''\n\n/** Case-insensitive, NFC-normalised form for `contains` (Thai combining marks compare correctly). */\nconst fold = (s: string) => s.toLowerCase().normalize('NFC')\n\n/**\n * Folded cell strings per column-values array: the column cache returns the same array for the same\n * data and column, so typing into a `contains` filter folds each cell once.\n */\nconst folded = new WeakMap<ColumnValues, (string | undefined)[]>()\n\nfunction foldedFor(values: ColumnValues): (string | undefined)[] {\n let f = folded.get(values)\n if (!f) {\n f = new Array<string | undefined>(values.length)\n folded.set(values, f)\n }\n return f\n}\n\nconst bad = (filter: string, expected: string, received: unknown): Compiled => ({\n kind: 'bad',\n filter,\n expected,\n received: typeName(received),\n})\n\nfunction equals(fv: unknown): Compiled {\n if (empty(fv)) return OFF\n if (fv instanceof Date) {\n const t = fv.getTime()\n return {\n kind: 'test',\n expected: 'Date',\n test: (e) => (e instanceof Date ? (e.getTime() === t ? MATCH : NO) : BAD),\n }\n }\n const type = typeof fv\n if (type === 'object' || type === 'function' || type === 'symbol') {\n return bad('equals', 'prim-or-date', fv)\n }\n return {\n kind: 'test',\n expected: type,\n test: (e) => (typeof e === type ? (e === fv ? MATCH : NO) : BAD),\n }\n}\n\nfunction contains(fv: unknown, values: ColumnValues): Compiled {\n if (empty(fv)) return OFF\n if (typeof fv !== 'string') return bad('contains', 'string', fv)\n const needle = fold(fv)\n const cache = foldedFor(values)\n return {\n kind: 'test',\n expected: 'string',\n test: (e, r) => {\n if (typeof e !== 'string') return BAD\n let s: string | undefined\n if (r < 0) s = fold(e)\n else {\n s = cache[r]\n if (s === undefined) {\n s = fold(e)\n cache[r] = s\n }\n }\n return s.includes(needle) ? MATCH : NO\n },\n }\n}\n\nfunction inList(fv: unknown): Compiled {\n if (fv === undefined || fv === null) return OFF\n if (!Array.isArray(fv)) return bad('in', 'array', fv)\n const prims = new Set<unknown>()\n const times = new Set<number>()\n const types = new Set<string>()\n for (const item of fv as unknown[]) {\n if (item === undefined || item === null) continue\n if (item instanceof Date) times.add(item.getTime())\n else if (typeof item === 'object' || typeof item === 'function') {\n return bad('in', 'prims-or-dates', item)\n } else prims.add(item)\n types.add(typeName(item))\n }\n if (types.size === 0) return OFF\n const hasDate = times.size > 0\n return {\n kind: 'test',\n expected: [...types].join(' | '),\n test: (e) => {\n if (e instanceof Date) return hasDate ? (times.has(e.getTime()) ? MATCH : NO) : BAD\n return types.has(typeof e) ? (prims.has(e) ? MATCH : NO) : BAD\n },\n }\n}\n\ntype Family = 'number' | 'Date'\n\n/** A range bound: absent, its family + comparable value, or malformed. */\nfunction bound(v: unknown): { family: Family; at: number | bigint } | null | false {\n if (empty(v)) return null\n if (typeof v === 'number' || typeof v === 'bigint') return { family: 'number', at: v }\n if (v instanceof Date && !Number.isNaN(v.getTime())) return { family: 'Date', at: v.getTime() }\n return false\n}\n\nconst NUMERIC = 'number | bigint'\n\nfunction range(fv: unknown): Compiled {\n if (fv === undefined || fv === null) return OFF\n if (typeof fv !== 'object' || Array.isArray(fv) || fv instanceof Date) {\n return bad('range', '{ min?, max? }', fv)\n }\n const { min: rawMin, max: rawMax } = fv as { min?: unknown; max?: unknown }\n const min = bound(rawMin)\n const max = bound(rawMax)\n // `expected` prose keys (diagnostics/prose.ts).\n if (min === false) return bad('range', 'range-bound', rawMin)\n if (max === false) return bad('range', 'range-bound', rawMax)\n if (!min && !max) return OFF\n const family = (min || max) as { family: Family }\n if (min && max && min.family !== max.family) {\n return bad('range', `same-type|${min.family}`, rawMax)\n }\n // Mixed number / bigint comparisons are exact in JS (`3n >= 2.5`).\n const lo = min ? (min.at as number) : undefined\n const hi = max ? (max.at as number) : undefined\n const inside = (x: number) => (lo === undefined || x >= lo) && (hi === undefined || x <= hi)\n if (family.family === 'Date') {\n return {\n kind: 'test',\n expected: 'Date',\n test: (e) => (e instanceof Date ? (inside(e.getTime()) ? MATCH : NO) : BAD),\n }\n }\n return {\n kind: 'test',\n expected: NUMERIC,\n test: (e) =>\n typeof e === 'number' || typeof e === 'bigint' ? (inside(e as number) ? MATCH : NO) : BAD,\n }\n}\n\n/** Compiles one column's filter for this run (cold: once per active filter per run). */\nexport function compile(filter: unknown, values: ColumnValues): Compiled {\n if (filter === undefined || filter === null) return OFF\n if (typeof filter !== 'object') return bad('(none)', '{ type, value }', filter)\n const f = filter as RawFilter\n switch (f.type) {\n case 'equals':\n return equals(f.value)\n case 'contains':\n return contains(f.value, values)\n case 'in':\n return inList(f.value)\n case 'range':\n return range(f.value)\n case 'custom':\n return typeof f.fn === 'function'\n ? {\n kind: 'custom',\n fn: f.fn as (v: unknown, fv: unknown, row: unknown) => unknown,\n value: f.value,\n }\n : bad('custom', 'fn: (value, filterValue, row) => boolean', f.fn)\n default:\n return {\n kind: 'bad',\n filter: typeof f.type === 'string' ? f.type : '(none)',\n expected: FILTER_KINDS.join(' | '),\n received: 'unknown filter kind',\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/features/column-filters/match.ts"],"sourcesContent":["import type { ColumnValues } from '../../pipeline/types'\nimport { FILTER_KINDS } from './codes'\n\n/** Result of testing one value: a match, no match, or a value of the wrong type (non-match). */\nexport const NO = 0\nexport const MATCH = 1\nexport const BAD = 2\nexport type Outcome = typeof NO | typeof MATCH | typeof BAD\n\n/**\n * Tests one non-nullish value. `r` is the row index when the value is the whole cell, `-1` for an\n * element of an array cell (per-row caches apply to whole cells only).\n */\nexport type Test = (value: unknown, r: number) => Outcome\n\n/** A runtime filter, as read from state (runtime JSON ignores the types). */\nexport interface RawFilter {\n readonly type?: unknown\n readonly value?: unknown\n readonly fn?: unknown\n}\n\nexport type Compiled =\n /** Inactive: the step skips it. */\n | { readonly kind: 'off' }\n /** A value test with what it accepts (for `DK_FILTER_TYPE_MISMATCH`). */\n | { readonly kind: 'test'; readonly test: Test; readonly expected: string }\n | {\n readonly kind: 'custom'\n readonly fn: (v: unknown, fv: unknown, row: unknown) => unknown\n readonly value: unknown\n }\n /** Malformed filter value: matches nothing; reported once with `source: 'filter'`. */\n | {\n readonly kind: 'bad'\n readonly filter: string\n readonly expected: string\n readonly received: string\n }\n\nconst OFF: Compiled = { kind: 'off' }\n\n/** A short type name for diagnostics: `null`, `array`, `Date`, else `typeof`. Never a value. */\nexport const typeName = (v: unknown): string =>\n v === null ? 'null' : Array.isArray(v) ? 'array' : v instanceof Date ? 'Date' : typeof v\n\n/** `undefined`, `null` and `''` leave a filter (or a range bound) inactive (§6.1). */\nconst empty = (v: unknown) => v === undefined || v === null || v === ''\n\n/** Case-insensitive, NFC-normalised form for `contains` (Thai combining marks compare correctly). */\nconst fold = (s: string) => s.toLowerCase().normalize('NFC')\n\n/**\n * Folded cell strings per column-values array: the column cache returns the same array for the same\n * data and column, so typing into a `contains` filter folds each cell once.\n */\nconst folded = new WeakMap<ColumnValues, (string | undefined)[]>()\n\nfunction foldedFor(values: ColumnValues): (string | undefined)[] {\n let f = folded.get(values)\n if (!f) {\n f = new Array<string | undefined>(values.length)\n folded.set(values, f)\n }\n return f\n}\n\nconst isNaNValue = (v: unknown) => typeof v === 'number' && Number.isNaN(v)\n\nconst bad = (filter: string, expected: string, received: unknown): Compiled => ({\n kind: 'bad',\n filter,\n expected,\n // `number(NaN)`, as `describeValue` names it: \"a number, expects a number\" would read as a bug.\n received: isNaNValue(received) ? 'number(NaN)' : typeName(received),\n})\n\nfunction equals(fv: unknown): Compiled {\n if (empty(fv)) return OFF\n if (fv instanceof Date) {\n const t = fv.getTime()\n return {\n kind: 'test',\n expected: 'Date',\n test: (e) => (e instanceof Date ? (e.getTime() === t ? MATCH : NO) : BAD),\n }\n }\n // NaN equals nothing (`NaN !== NaN`): reported, not a silent empty table (#91).\n if (isNaNValue(fv)) return bad('equals', 'not-nan', fv)\n const type = typeof fv\n if (type === 'object' || type === 'function' || type === 'symbol') {\n return bad('equals', 'prim-or-date', fv)\n }\n return {\n kind: 'test',\n expected: type,\n test: (e) => (typeof e === type ? (e === fv ? MATCH : NO) : BAD),\n }\n}\n\nfunction contains(fv: unknown, values: ColumnValues): Compiled {\n if (empty(fv)) return OFF\n if (typeof fv !== 'string') return bad('contains', 'string', fv)\n const needle = fold(fv)\n const cache = foldedFor(values)\n return {\n kind: 'test',\n expected: 'string',\n test: (e, r) => {\n if (typeof e !== 'string') return BAD\n let s: string | undefined\n if (r < 0) s = fold(e)\n else {\n s = cache[r]\n if (s === undefined) {\n s = fold(e)\n cache[r] = s\n }\n }\n return s.includes(needle) ? MATCH : NO\n },\n }\n}\n\nfunction inList(fv: unknown): Compiled {\n if (fv === undefined || fv === null) return OFF\n if (!Array.isArray(fv)) return bad('in', 'array', fv)\n const prims = new Set<unknown>()\n const times = new Set<number>()\n const types = new Set<string>()\n for (const item of fv as unknown[]) {\n if (item === undefined || item === null) continue\n if (item instanceof Date) times.add(item.getTime())\n else if (typeof item === 'object' || typeof item === 'function') {\n return bad('in', 'prims-or-dates', item)\n } else prims.add(item)\n types.add(typeName(item))\n }\n if (types.size === 0) return OFF\n const hasDate = times.size > 0\n return {\n kind: 'test',\n expected: [...types].join(' | '),\n test: (e) => {\n if (e instanceof Date) return hasDate ? (times.has(e.getTime()) ? MATCH : NO) : BAD\n return types.has(typeof e) ? (prims.has(e) ? MATCH : NO) : BAD\n },\n }\n}\n\ntype Family = 'number' | 'Date'\n\n/**\n * A range bound: absent, its family + comparable value, or malformed. NaN is malformed: every\n * comparison with it is false, so it would hide every row without a report (#91).\n */\nfunction bound(v: unknown): { family: Family; at: number | bigint } | null | false {\n if (empty(v)) return null\n if ((typeof v === 'number' && !Number.isNaN(v)) || typeof v === 'bigint')\n return { family: 'number', at: v }\n if (v instanceof Date && !Number.isNaN(v.getTime())) return { family: 'Date', at: v.getTime() }\n return false\n}\n\nconst NUMERIC = 'number | bigint'\n\nfunction range(fv: unknown): Compiled {\n if (fv === undefined || fv === null) return OFF\n if (typeof fv !== 'object' || Array.isArray(fv) || fv instanceof Date) {\n return bad('range', '{ min?, max? }', fv)\n }\n const { min: rawMin, max: rawMax } = fv as { min?: unknown; max?: unknown }\n const min = bound(rawMin)\n const max = bound(rawMax)\n // `expected` prose keys (diagnostics/prose.ts).\n if (min === false) return bad('range', 'range-bound', rawMin)\n if (max === false) return bad('range', 'range-bound', rawMax)\n if (!min && !max) return OFF\n const family = (min || max) as { family: Family }\n if (min && max && min.family !== max.family) {\n return bad('range', `same-type|${min.family}`, rawMax)\n }\n // Mixed number / bigint comparisons are exact in JS (`3n >= 2.5`).\n const lo = min ? (min.at as number) : undefined\n const hi = max ? (max.at as number) : undefined\n const inside = (x: number) => (lo === undefined || x >= lo) && (hi === undefined || x <= hi)\n if (family.family === 'Date') {\n return {\n kind: 'test',\n expected: 'Date',\n test: (e) => (e instanceof Date ? (inside(e.getTime()) ? MATCH : NO) : BAD),\n }\n }\n return {\n kind: 'test',\n expected: NUMERIC,\n test: (e) =>\n typeof e === 'number' || typeof e === 'bigint' ? (inside(e as number) ? MATCH : NO) : BAD,\n }\n}\n\n/** Compiles one column's filter for this run (cold: once per active filter per run). */\nexport function compile(filter: unknown, values: ColumnValues): Compiled {\n if (filter === undefined || filter === null) return OFF\n if (typeof filter !== 'object') return bad('(none)', '{ type, value }', filter)\n const f = filter as RawFilter\n switch (f.type) {\n case 'equals':\n return equals(f.value)\n case 'contains':\n return contains(f.value, values)\n case 'in':\n return inList(f.value)\n case 'range':\n return range(f.value)\n case 'custom':\n return typeof f.fn === 'function'\n ? {\n kind: 'custom',\n fn: f.fn as (v: unknown, fv: unknown, row: unknown) => unknown,\n value: f.value,\n }\n : bad('custom', 'fn: (value, filterValue, row) => boolean', f.fn)\n default:\n return {\n kind: 'bad',\n filter: typeof f.type === 'string' ? f.type : '(none)',\n expected: FILTER_KINDS.join(' | '),\n received: 'unknown filter kind',\n }\n }\n}\n"],"mappings":";AACA,SAAS,oBAAoB;AAGtB,IAAM,KAAK;AACX,IAAM,QAAQ;AACd,IAAM,MAAM;AAkCnB,IAAM,MAAgB,EAAE,MAAM,MAAM;AAG7B,IAAM,WAAW,CAAC,MACvB,MAAM,OAAO,SAAS,MAAM,QAAQ,CAAC,IAAI,UAAU,aAAa,OAAO,SAAS,OAAO;AAGzF,IAAM,QAAQ,CAAC,MAAe,MAAM,UAAa,MAAM,QAAQ,MAAM;AAGrE,IAAM,OAAO,CAAC,MAAc,EAAE,YAAY,EAAE,UAAU,KAAK;AAM3D,IAAM,SAAS,oBAAI,QAA8C;AAEjE,SAAS,UAAU,QAA8C;AAC/D,MAAI,IAAI,OAAO,IAAI,MAAM;AACzB,MAAI,CAAC,GAAG;AACN,QAAI,IAAI,MAA0B,OAAO,MAAM;AAC/C,WAAO,IAAI,QAAQ,CAAC;AAAA,EACtB;AACA,SAAO;AACT;AAEA,IAAM,aAAa,CAAC,MAAe,OAAO,MAAM,YAAY,OAAO,MAAM,CAAC;AAE1E,IAAM,MAAM,CAAC,QAAgB,UAAkB,cAAiC;AAAA,EAC9E,MAAM;AAAA,EACN;AAAA,EACA;AAAA;AAAA,EAEA,UAAU,WAAW,QAAQ,IAAI,gBAAgB,SAAS,QAAQ;AACpE;AAEA,SAAS,OAAO,IAAuB;AACrC,MAAI,MAAM,EAAE,EAAG,QAAO;AACtB,MAAI,cAAc,MAAM;AACtB,UAAM,IAAI,GAAG,QAAQ;AACrB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU;AAAA,MACV,MAAM,CAAC,MAAO,aAAa,OAAQ,EAAE,QAAQ,MAAM,IAAI,QAAQ,KAAM;AAAA,IACvE;AAAA,EACF;AAEA,MAAI,WAAW,EAAE,EAAG,QAAO,IAAI,UAAU,WAAW,EAAE;AACtD,QAAM,OAAO,OAAO;AACpB,MAAI,SAAS,YAAY,SAAS,cAAc,SAAS,UAAU;AACjE,WAAO,IAAI,UAAU,gBAAgB,EAAE;AAAA,EACzC;AACA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,MAAM,CAAC,MAAO,OAAO,MAAM,OAAQ,MAAM,KAAK,QAAQ,KAAM;AAAA,EAC9D;AACF;AAEA,SAAS,SAAS,IAAa,QAAgC;AAC7D,MAAI,MAAM,EAAE,EAAG,QAAO;AACtB,MAAI,OAAO,OAAO,SAAU,QAAO,IAAI,YAAY,UAAU,EAAE;AAC/D,QAAM,SAAS,KAAK,EAAE;AACtB,QAAM,QAAQ,UAAU,MAAM;AAC9B,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,MAAM,CAAC,GAAG,MAAM;AACd,UAAI,OAAO,MAAM,SAAU,QAAO;AAClC,UAAI;AACJ,UAAI,IAAI,EAAG,KAAI,KAAK,CAAC;AAAA,WAChB;AACH,YAAI,MAAM,CAAC;AACX,YAAI,MAAM,QAAW;AACnB,cAAI,KAAK,CAAC;AACV,gBAAM,CAAC,IAAI;AAAA,QACb;AAAA,MACF;AACA,aAAO,EAAE,SAAS,MAAM,IAAI,QAAQ;AAAA,IACtC;AAAA,EACF;AACF;AAEA,SAAS,OAAO,IAAuB;AACrC,MAAI,OAAO,UAAa,OAAO,KAAM,QAAO;AAC5C,MAAI,CAAC,MAAM,QAAQ,EAAE,EAAG,QAAO,IAAI,MAAM,SAAS,EAAE;AACpD,QAAM,QAAQ,oBAAI,IAAa;AAC/B,QAAM,QAAQ,oBAAI,IAAY;AAC9B,QAAM,QAAQ,oBAAI,IAAY;AAC9B,aAAW,QAAQ,IAAiB;AAClC,QAAI,SAAS,UAAa,SAAS,KAAM;AACzC,QAAI,gBAAgB,KAAM,OAAM,IAAI,KAAK,QAAQ,CAAC;AAAA,aACzC,OAAO,SAAS,YAAY,OAAO,SAAS,YAAY;AAC/D,aAAO,IAAI,MAAM,kBAAkB,IAAI;AAAA,IACzC,MAAO,OAAM,IAAI,IAAI;AACrB,UAAM,IAAI,SAAS,IAAI,CAAC;AAAA,EAC1B;AACA,MAAI,MAAM,SAAS,EAAG,QAAO;AAC7B,QAAM,UAAU,MAAM,OAAO;AAC7B,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,CAAC,GAAG,KAAK,EAAE,KAAK,KAAK;AAAA,IAC/B,MAAM,CAAC,MAAM;AACX,UAAI,aAAa,KAAM,QAAO,UAAW,MAAM,IAAI,EAAE,QAAQ,CAAC,IAAI,QAAQ,KAAM;AAChF,aAAO,MAAM,IAAI,OAAO,CAAC,IAAK,MAAM,IAAI,CAAC,IAAI,QAAQ,KAAM;AAAA,IAC7D;AAAA,EACF;AACF;AAQA,SAAS,MAAM,GAAoE;AACjF,MAAI,MAAM,CAAC,EAAG,QAAO;AACrB,MAAK,OAAO,MAAM,YAAY,CAAC,OAAO,MAAM,CAAC,KAAM,OAAO,MAAM;AAC9D,WAAO,EAAE,QAAQ,UAAU,IAAI,EAAE;AACnC,MAAI,aAAa,QAAQ,CAAC,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO,EAAE,QAAQ,QAAQ,IAAI,EAAE,QAAQ,EAAE;AAC9F,SAAO;AACT;AAEA,IAAM,UAAU;AAEhB,SAAS,MAAM,IAAuB;AACpC,MAAI,OAAO,UAAa,OAAO,KAAM,QAAO;AAC5C,MAAI,OAAO,OAAO,YAAY,MAAM,QAAQ,EAAE,KAAK,cAAc,MAAM;AACrE,WAAO,IAAI,SAAS,kBAAkB,EAAE;AAAA,EAC1C;AACA,QAAM,EAAE,KAAK,QAAQ,KAAK,OAAO,IAAI;AACrC,QAAM,MAAM,MAAM,MAAM;AACxB,QAAM,MAAM,MAAM,MAAM;AAExB,MAAI,QAAQ,MAAO,QAAO,IAAI,SAAS,eAAe,MAAM;AAC5D,MAAI,QAAQ,MAAO,QAAO,IAAI,SAAS,eAAe,MAAM;AAC5D,MAAI,CAAC,OAAO,CAAC,IAAK,QAAO;AACzB,QAAM,SAAU,OAAO;AACvB,MAAI,OAAO,OAAO,IAAI,WAAW,IAAI,QAAQ;AAC3C,WAAO,IAAI,SAAS,aAAa,IAAI,MAAM,IAAI,MAAM;AAAA,EACvD;AAEA,QAAM,KAAK,MAAO,IAAI,KAAgB;AACtC,QAAM,KAAK,MAAO,IAAI,KAAgB;AACtC,QAAM,SAAS,CAAC,OAAe,OAAO,UAAa,KAAK,QAAQ,OAAO,UAAa,KAAK;AACzF,MAAI,OAAO,WAAW,QAAQ;AAC5B,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU;AAAA,MACV,MAAM,CAAC,MAAO,aAAa,OAAQ,OAAO,EAAE,QAAQ,CAAC,IAAI,QAAQ,KAAM;AAAA,IACzE;AAAA,EACF;AACA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,MAAM,CAAC,MACL,OAAO,MAAM,YAAY,OAAO,MAAM,WAAY,OAAO,CAAW,IAAI,QAAQ,KAAM;AAAA,EAC1F;AACF;AAGO,SAAS,QAAQ,QAAiB,QAAgC;AACvE,MAAI,WAAW,UAAa,WAAW,KAAM,QAAO;AACpD,MAAI,OAAO,WAAW,SAAU,QAAO,IAAI,UAAU,mBAAmB,MAAM;AAC9E,QAAM,IAAI;AACV,UAAQ,EAAE,MAAM;AAAA,IACd,KAAK;AACH,aAAO,OAAO,EAAE,KAAK;AAAA,IACvB,KAAK;AACH,aAAO,SAAS,EAAE,OAAO,MAAM;AAAA,IACjC,KAAK;AACH,aAAO,OAAO,EAAE,KAAK;AAAA,IACvB,KAAK;AACH,aAAO,MAAM,EAAE,KAAK;AAAA,IACtB,KAAK;AACH,aAAO,OAAO,EAAE,OAAO,aACnB;AAAA,QACE,MAAM;AAAA,QACN,IAAI,EAAE;AAAA,QACN,OAAO,EAAE;AAAA,MACX,IACA,IAAI,UAAU,4CAA4C,EAAE,EAAE;AAAA,IACpE;AACE,aAAO;AAAA,QACL,MAAM;AAAA,QACN,QAAQ,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;AAAA,QAC9C,UAAU,aAAa,KAAK,KAAK;AAAA,QACjC,UAAU;AAAA,MACZ;AAAA,EACJ;AACF;","names":[]}
@@ -35,8 +35,8 @@ var import_codes = require("./codes.cjs");
35
35
  (0, import_catalog.keepDiagnostics)(import_codes.paginationDiagnostics);
36
36
  var DEFAULT_PAGE_SIZE = 10;
37
37
  var NAME = "pagination";
38
- var isIndex = (v) => Number.isInteger(v) && v >= 0;
39
- var isSize = (v) => Number.isInteger(v) && v > 0;
38
+ var isIndex = (v) => Number.isSafeInteger(v) && v >= 0;
39
+ var isSize = (v) => Number.isSafeInteger(v) && v > 0;
40
40
  var received = (v) => typeof v === "number" ? String(v) : (0, import_redact.describeValue)(v);
41
41
  var paginationOptions = {
42
42
  pageSize: { type: "number", check: isSize, expected: "an integer > 0" },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/pagination/pagination.ts"],"sourcesContent":["import { defineOptions } from '../../catalog/options'\nimport { keepDiagnostics } from '../../diagnostics/catalog'\nimport '../../diagnostics/process-guard'\nimport { describeValue } from '../../diagnostics/redact'\nimport { defineFeature } from '../../feature/define'\nimport type { FeatureContext } from '../../feature/types'\nimport { defineInterceptor } from '../../intercept/define'\nimport { type DataMode, modeOption } from '../../source/mode'\nimport { getTableInternals } from '../../table/internals'\nimport type { Table, TableState } from '../../table/types'\nimport { paginationDiagnostics } from './codes'\n\nkeepDiagnostics(paginationDiagnostics)\n\nexport interface PaginationState {\n pageIndex: number\n pageSize: number\n}\n\nexport interface PaginationApi {\n /** Goes to page `index`, clamped to `[0, getPageCount() - 1]`. */\n setPageIndex(index: number): void\n /** Changes the page size and keeps the first visible row on screen (`floor(firstRow / size)`). */\n setPageSize(size: number): void\n /** Next page; no-op on the last page. */\n nextPage(): void\n /** Previous page; no-op on the first page. */\n prevPage(): void\n /** `ceil(rowCount / pageSize)`; `0` when there are no rows. */\n getPageCount(): number\n canNextPage(): boolean\n canPrevPage(): boolean\n}\n\nexport interface PaginationOptions {\n /** Starting page size (default 10). `options.initialState.pagination` takes precedence. */\n pageSize?: number\n /** Go back to page 0 when search, filters or sort change (default `true`). */\n autoResetPageIndex?: boolean\n /**\n * `'server'`: the server pages (its state goes to `options.fetcher`, or the app passes the\n * current page as `data`); the page count is `ceil(rowCount / pageSize)` with `rowCount` from the\n * fetcher result or the `rowCount` option (§6.5, §7.1). Default `'client'`.\n */\n mode?: DataMode\n}\n\nconst DEFAULT_PAGE_SIZE = 10\nconst NAME = 'pagination'\n\nconst isIndex = (v: unknown): v is number => Number.isInteger(v) && (v as number) >= 0\nconst isSize = (v: unknown): v is number => Number.isInteger(v) && (v as number) > 0\nconst received = (v: unknown) => (typeof v === 'number' ? String(v) : describeValue(v))\n\nconst paginationOptions = {\n pageSize: { type: 'number', check: isSize, expected: 'an integer > 0' },\n autoResetPageIndex: { type: 'boolean' },\n mode: modeOption,\n} as const\n\n/** Stages whose state changes send the table back to page 0 (`autoResetPageIndex`). */\nconst RESET_STAGES = new Set(['filter', 'search', 'sort'])\n\n/**\n * A slice change that resets the page. For a search slice only the applied `query` counts: typing\n * (`input`) and `status` alone keep the page (a debounced keystroke must not jump to page 0).\n */\nfunction changed(stage: string, a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return false\n if (stage === 'search' && hasQuery(a) && hasQuery(b)) return !Object.is(a.query, b.query)\n return true\n}\n\nconst hasQuery = (v: unknown): v is { query: unknown } =>\n v !== null && typeof v === 'object' && 'query' in v\n\n/** The page actually shown: `pageIndex` clamped to the last page (0 for no rows). */\nfunction effectiveIndex(pageIndex: number, pageSize: number, rowCount: number): number {\n const last = rowCount === 0 ? 0 : Math.ceil(rowCount / pageSize) - 1\n return pageIndex > last ? last : pageIndex\n}\n\n/** What the pagination interceptor needs of one table (registered by `createApi`). */\ninterface Host {\n /** Runs after `state:dispatch` / `table:setOptions` committed: validation, then auto-reset. */\n after(before: TableState): void\n}\n\n/**\n * Client-side pagination (§6.5). State `{ pageIndex, pageSize }`; the `paginate` step returns the\n * rows of the current page (`RowModel.filteredCount` stays the count before paging).\n *\n * - `setPageSize` keeps the first visible row on screen; `setPageIndex` clamps.\n * - `autoResetPageIndex` (default `true`): a change of a filter / search / sort slice resets\n * `pageIndex` to 0 in the same update (one notification). An update that also sets the\n * pagination slice keeps the page it sets.\n * - `pageIndex` is clamped when the row count shrinks (data change, filters with auto-reset off).\n * - Invalid values are never silent: API arguments are ignored and slices written to the store are\n * replaced, both with `DK_INVALID_PAGINATION`.\n */\nexport const pagination = (opts: PaginationOptions = {}) => {\n // Production skips the option validator: an invalid size must not reach the state, or sanitize()\n // would \"fix\" a slice with the same bad size and re-dispatch forever (#13). Reported in createApi.\n const badSize = opts.pageSize !== undefined && !isSize(opts.pageSize)\n const initialSize = badSize ? DEFAULT_PAGE_SIZE : (opts.pageSize ?? DEFAULT_PAGE_SIZE)\n const autoReset = opts.autoResetPageIndex !== false\n const mode = opts.mode ?? 'client'\n // One feature instance may serve several tables: per-table state lives here.\n const hosts = new WeakMap<object, Host>()\n\n const interceptor = defineInterceptor({\n name: NAME,\n on: ['state:dispatch', 'table:setOptions'],\n intercept(ctx, next) {\n const host = hosts.get(ctx.table as object)\n if (!host) return next()\n const before = (ctx.table as Table<unknown, TableState>).getState()\n const out = next()\n host.after(before)\n return out\n },\n })\n\n return defineFeature({\n name: NAME,\n stage: 'paginate',\n mode,\n options: defineOptions(\n // The option validator runs in development only (§9.3): production drops the schema.\n (process.env.NODE_ENV !== \"production\") ? paginationOptions : ({} as typeof paginationOptions),\n opts,\n ),\n interceptors: [interceptor],\n initialState: (): PaginationState => ({ pageIndex: 0, pageSize: initialSize }),\n // After destroy(): the getters still return their declared types (no pages, no navigation).\n destroyedResult: { getPageCount: () => 0, canNextPage: () => false, canPrevPage: () => false },\n createApi: (ctx: FeatureContext<PaginationState>): PaginationApi => {\n const table = ctx.table\n\n const internals = getTableInternals(table)\n let missingReported = false\n /**\n * Rows to page over. Server mode (§6.5, §7.1): the fetcher result's `rowCount`\n * (`undefined` until the first one applied), else the `rowCount` option, else\n * `DK_ROW_COUNT_MISSING` and `data.length` as the total.\n */\n const rowCount = (): number | undefined => {\n if (mode !== 'server') return table.getRowModel().filteredCount\n const n = internals.rowCount()\n if (n !== undefined || internals.hasFetcher()) return n\n const rows = table.getRowModel().totalCount\n if (!missingReported) {\n missingReported = true\n ctx.emit('DK_ROW_COUNT_MISSING', { rows })\n }\n return rows\n }\n\n /** The slice with invalid fields replaced (`prev` = the last valid slice, if any). */\n function sanitize(s: unknown, prev: unknown): PaginationState | undefined {\n const fallbackSize = isSize((prev as PaginationState | undefined)?.pageSize)\n ? (prev as PaginationState).pageSize\n : initialSize\n if (s === null || typeof s !== 'object') {\n report('state', 'pageIndex', s, 'page-reset')\n return { pageIndex: 0, pageSize: initialSize }\n }\n const { pageIndex, pageSize } = s as Partial<PaginationState>\n let fixed: PaginationState | undefined\n if (!isIndex(pageIndex)) {\n report('state', 'pageIndex', pageIndex, 'page-index-0')\n fixed = { ...(s as PaginationState), pageIndex: 0 }\n }\n if (!isSize(pageSize)) {\n report('state', 'pageSize', pageSize, `page-size|${fallbackSize}`)\n fixed = { ...(fixed ?? (s as PaginationState)), pageSize: fallbackSize }\n }\n return fixed\n }\n\n function report(\n source: 'setPageIndex' | 'setPageSize' | 'state' | 'option',\n field: 'pageIndex' | 'pageSize',\n value: unknown,\n fallback: string,\n ) {\n ctx.emit('DK_INVALID_PAGINATION', { source, field, received: received(value), fallback })\n }\n\n /**\n * The current slice and the page actually shown (for `count` rows, default: the current\n * count). An unknown count (a server count not fetched yet) clamps nothing.\n */\n function current(count = rowCount()) {\n const s = ctx.getState()\n const index =\n count === undefined ? s.pageIndex : effectiveIndex(s.pageIndex, s.pageSize, count)\n return { s, count, index }\n }\n\n function goTo(index: number) {\n const { s, count } = current()\n const target =\n index < 0 ? 0 : count === undefined ? index : effectiveIndex(index, s.pageSize, count)\n if (target !== s.pageIndex) ctx.setState({ ...s, pageIndex: target })\n }\n\n hosts.set(table, {\n after(before) {\n const now = table.getState()\n const slice = now[NAME]\n if (!Object.is(before[NAME], slice)) {\n const fixed = sanitize(slice, before[NAME])\n if (fixed) ctx.setState(fixed)\n return // an update that sets the pagination slice keeps its page\n }\n if (!autoReset || (slice as PaginationState).pageIndex === 0) return\n for (const f of getTableInternals(table).features) {\n if (RESET_STAGES.has(f.stage) && changed(f.stage, before[f.name], now[f.name])) {\n ctx.setState({ ...(slice as PaginationState), pageIndex: 0 })\n return\n }\n }\n },\n })\n\n if (badSize) report('option', 'pageSize', opts.pageSize, `pageSize ${initialSize} was used`)\n // A slice given at creation (initialState / controlled state) is checked once, here.\n const fixed = sanitize(ctx.getState(), undefined)\n if (fixed) ctx.setState(fixed)\n // ... and clamped to the last page, so the state never says \"page 100 of 3\" (#19). Client:\n // the paged rows are a subset of `data`, whose length bounds the last page without running\n // the pipeline here (restored filters leaving fewer rows: the listener below); no data yet\n // clamps nothing. Server: a known `rowCount`.\n const clamp = (count?: number) => {\n const { s, index } = current(count)\n if (index < s.pageIndex) ctx.setState({ ...s, pageIndex: index })\n }\n const n =\n mode === 'server' ? internals.rowCount() : (table.getOptions().data as unknown[])?.length\n if (n) clamp(n)\n\n // Clamp when the row count shrinks. A listener sees every change (data, async ingest,\n // filters with auto-reset off); its update is notified in the next round.\n table.subscribe(() => clamp())\n\n return {\n setPageIndex(index) {\n if (!Number.isInteger(index)) {\n report('setPageIndex', 'pageIndex', index, 'call-ignored')\n return\n }\n goTo(index)\n },\n setPageSize(size) {\n if (!isSize(size)) {\n report('setPageSize', 'pageSize', size, 'call-ignored')\n return\n }\n const { s, index } = current()\n if (size === s.pageSize) return\n const firstRow = index * s.pageSize\n ctx.setState({ pageIndex: Math.floor(firstRow / size), pageSize: size })\n },\n nextPage() {\n goTo(current().index + 1)\n },\n prevPage() {\n goTo(current().index - 1)\n },\n getPageCount() {\n const count = rowCount()\n return !count ? 0 : Math.ceil(count / ctx.getState().pageSize)\n },\n canNextPage() {\n const { s, count, index } = current()\n return count !== undefined && (index + 1) * s.pageSize < count\n },\n canPrevPage() {\n return current().index > 0\n },\n }\n },\n rowModel: (indices, s) => {\n const size = isSize(s?.pageSize) ? s.pageSize : initialSize\n const index = effectiveIndex(isIndex(s?.pageIndex) ? s.pageIndex : 0, size, indices.length)\n const start = index * size\n if (start === 0 && size >= indices.length) return indices\n return indices.subarray(start, start + size)\n },\n })\n}\n"],"mappings":"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","names":["import_define","n","fixed"]}
1
+ {"version":3,"sources":["../../../src/features/pagination/pagination.ts"],"sourcesContent":["import { defineOptions } from '../../catalog/options'\nimport { keepDiagnostics } from '../../diagnostics/catalog'\nimport '../../diagnostics/process-guard'\nimport { describeValue } from '../../diagnostics/redact'\nimport { defineFeature } from '../../feature/define'\nimport type { FeatureContext } from '../../feature/types'\nimport { defineInterceptor } from '../../intercept/define'\nimport { type DataMode, modeOption } from '../../source/mode'\nimport { getTableInternals } from '../../table/internals'\nimport type { Table, TableState } from '../../table/types'\nimport { paginationDiagnostics } from './codes'\n\nkeepDiagnostics(paginationDiagnostics)\n\nexport interface PaginationState {\n pageIndex: number\n pageSize: number\n}\n\nexport interface PaginationApi {\n /** Goes to page `index`, clamped to `[0, getPageCount() - 1]`. */\n setPageIndex(index: number): void\n /** Changes the page size and keeps the first visible row on screen (`floor(firstRow / size)`). */\n setPageSize(size: number): void\n /** Next page; no-op on the last page. */\n nextPage(): void\n /** Previous page; no-op on the first page. */\n prevPage(): void\n /** `ceil(rowCount / pageSize)`; `0` when there are no rows. */\n getPageCount(): number\n canNextPage(): boolean\n canPrevPage(): boolean\n}\n\nexport interface PaginationOptions {\n /** Starting page size (default 10). `options.initialState.pagination` takes precedence. */\n pageSize?: number\n /** Go back to page 0 when search, filters or sort change (default `true`). */\n autoResetPageIndex?: boolean\n /**\n * `'server'`: the server pages (its state goes to `options.fetcher`, or the app passes the\n * current page as `data`); the page count is `ceil(rowCount / pageSize)` with `rowCount` from the\n * fetcher result or the `rowCount` option (§6.5, §7.1). Default `'client'`.\n */\n mode?: DataMode\n}\n\nconst DEFAULT_PAGE_SIZE = 10\nconst NAME = 'pagination'\n\n// Safe integers (#100): restored state is untrusted, and 1e308 is an \"integer\".\nconst isIndex = (v: unknown): v is number => Number.isSafeInteger(v) && (v as number) >= 0\nconst isSize = (v: unknown): v is number => Number.isSafeInteger(v) && (v as number) > 0\nconst received = (v: unknown) => (typeof v === 'number' ? String(v) : describeValue(v))\n\nconst paginationOptions = {\n pageSize: { type: 'number', check: isSize, expected: 'an integer > 0' },\n autoResetPageIndex: { type: 'boolean' },\n mode: modeOption,\n} as const\n\n/** Stages whose state changes send the table back to page 0 (`autoResetPageIndex`). */\nconst RESET_STAGES = new Set(['filter', 'search', 'sort'])\n\n/**\n * A slice change that resets the page. For a search slice only the applied `query` counts: typing\n * (`input`) and `status` alone keep the page (a debounced keystroke must not jump to page 0).\n */\nfunction changed(stage: string, a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return false\n if (stage === 'search' && hasQuery(a) && hasQuery(b)) return !Object.is(a.query, b.query)\n return true\n}\n\nconst hasQuery = (v: unknown): v is { query: unknown } =>\n v !== null && typeof v === 'object' && 'query' in v\n\n/** The page actually shown: `pageIndex` clamped to the last page (0 for no rows). */\nfunction effectiveIndex(pageIndex: number, pageSize: number, rowCount: number): number {\n const last = rowCount === 0 ? 0 : Math.ceil(rowCount / pageSize) - 1\n return pageIndex > last ? last : pageIndex\n}\n\n/** What the pagination interceptor needs of one table (registered by `createApi`). */\ninterface Host {\n /** Runs after `state:dispatch` / `table:setOptions` committed: validation, then auto-reset. */\n after(before: TableState): void\n}\n\n/**\n * Client-side pagination (§6.5). State `{ pageIndex, pageSize }`; the `paginate` step returns the\n * rows of the current page (`RowModel.filteredCount` stays the count before paging).\n *\n * - `setPageSize` keeps the first visible row on screen; `setPageIndex` clamps.\n * - `autoResetPageIndex` (default `true`): a change of a filter / search / sort slice resets\n * `pageIndex` to 0 in the same update (one notification). An update that also sets the\n * pagination slice keeps the page it sets.\n * - `pageIndex` is clamped when the row count shrinks (data change, filters with auto-reset off).\n * - Invalid values are never silent: API arguments are ignored and slices written to the store are\n * replaced, both with `DK_INVALID_PAGINATION`.\n */\nexport const pagination = (opts: PaginationOptions = {}) => {\n // Production skips the option validator: an invalid size must not reach the state, or sanitize()\n // would \"fix\" a slice with the same bad size and re-dispatch forever (#13). Reported in createApi.\n const badSize = opts.pageSize !== undefined && !isSize(opts.pageSize)\n const initialSize = badSize ? DEFAULT_PAGE_SIZE : (opts.pageSize ?? DEFAULT_PAGE_SIZE)\n const autoReset = opts.autoResetPageIndex !== false\n const mode = opts.mode ?? 'client'\n // One feature instance may serve several tables: per-table state lives here.\n const hosts = new WeakMap<object, Host>()\n\n const interceptor = defineInterceptor({\n name: NAME,\n on: ['state:dispatch', 'table:setOptions'],\n intercept(ctx, next) {\n const host = hosts.get(ctx.table as object)\n if (!host) return next()\n const before = (ctx.table as Table<unknown, TableState>).getState()\n const out = next()\n host.after(before)\n return out\n },\n })\n\n return defineFeature({\n name: NAME,\n stage: 'paginate',\n mode,\n options: defineOptions(\n // The option validator runs in development only (§9.3): production drops the schema.\n (process.env.NODE_ENV !== \"production\") ? paginationOptions : ({} as typeof paginationOptions),\n opts,\n ),\n interceptors: [interceptor],\n initialState: (): PaginationState => ({ pageIndex: 0, pageSize: initialSize }),\n // After destroy(): the getters still return their declared types (no pages, no navigation).\n destroyedResult: { getPageCount: () => 0, canNextPage: () => false, canPrevPage: () => false },\n createApi: (ctx: FeatureContext<PaginationState>): PaginationApi => {\n const table = ctx.table\n\n const internals = getTableInternals(table)\n let missingReported = false\n /**\n * Rows to page over. Server mode (§6.5, §7.1): the fetcher result's `rowCount`\n * (`undefined` until the first one applied), else the `rowCount` option, else\n * `DK_ROW_COUNT_MISSING` and `data.length` as the total.\n */\n const rowCount = (): number | undefined => {\n if (mode !== 'server') return table.getRowModel().filteredCount\n const n = internals.rowCount()\n if (n !== undefined || internals.hasFetcher()) return n\n const rows = table.getRowModel().totalCount\n if (!missingReported) {\n missingReported = true\n ctx.emit('DK_ROW_COUNT_MISSING', { rows })\n }\n return rows\n }\n\n /** The slice with invalid fields replaced (`prev` = the last valid slice, if any). */\n function sanitize(s: unknown, prev: unknown): PaginationState | undefined {\n const fallbackSize = isSize((prev as PaginationState | undefined)?.pageSize)\n ? (prev as PaginationState).pageSize\n : initialSize\n if (s === null || typeof s !== 'object') {\n report('state', 'pageIndex', s, 'page-reset')\n return { pageIndex: 0, pageSize: initialSize }\n }\n const { pageIndex, pageSize } = s as Partial<PaginationState>\n let fixed: PaginationState | undefined\n if (!isIndex(pageIndex)) {\n report('state', 'pageIndex', pageIndex, 'page-index-0')\n fixed = { ...(s as PaginationState), pageIndex: 0 }\n }\n if (!isSize(pageSize)) {\n report('state', 'pageSize', pageSize, `page-size|${fallbackSize}`)\n fixed = { ...(fixed ?? (s as PaginationState)), pageSize: fallbackSize }\n }\n return fixed\n }\n\n function report(\n source: 'setPageIndex' | 'setPageSize' | 'state' | 'option',\n field: 'pageIndex' | 'pageSize',\n value: unknown,\n fallback: string,\n ) {\n ctx.emit('DK_INVALID_PAGINATION', { source, field, received: received(value), fallback })\n }\n\n /**\n * The current slice and the page actually shown (for `count` rows, default: the current\n * count). An unknown count (a server count not fetched yet) clamps nothing.\n */\n function current(count = rowCount()) {\n const s = ctx.getState()\n const index =\n count === undefined ? s.pageIndex : effectiveIndex(s.pageIndex, s.pageSize, count)\n return { s, count, index }\n }\n\n function goTo(index: number) {\n const { s, count } = current()\n const target =\n index < 0 ? 0 : count === undefined ? index : effectiveIndex(index, s.pageSize, count)\n if (target !== s.pageIndex) ctx.setState({ ...s, pageIndex: target })\n }\n\n hosts.set(table, {\n after(before) {\n const now = table.getState()\n const slice = now[NAME]\n if (!Object.is(before[NAME], slice)) {\n const fixed = sanitize(slice, before[NAME])\n if (fixed) ctx.setState(fixed)\n return // an update that sets the pagination slice keeps its page\n }\n if (!autoReset || (slice as PaginationState).pageIndex === 0) return\n for (const f of getTableInternals(table).features) {\n if (RESET_STAGES.has(f.stage) && changed(f.stage, before[f.name], now[f.name])) {\n ctx.setState({ ...(slice as PaginationState), pageIndex: 0 })\n return\n }\n }\n },\n })\n\n if (badSize) report('option', 'pageSize', opts.pageSize, `pageSize ${initialSize} was used`)\n // A slice given at creation (initialState / controlled state) is checked once, here.\n const fixed = sanitize(ctx.getState(), undefined)\n if (fixed) ctx.setState(fixed)\n // ... and clamped to the last page, so the state never says \"page 100 of 3\" (#19). Client:\n // the paged rows are a subset of `data`, whose length bounds the last page without running\n // the pipeline here (restored filters leaving fewer rows: the listener below); no data yet\n // clamps nothing. Server: a known `rowCount`.\n const clamp = (count?: number) => {\n const { s, index } = current(count)\n if (index < s.pageIndex) ctx.setState({ ...s, pageIndex: index })\n }\n const n =\n mode === 'server' ? internals.rowCount() : (table.getOptions().data as unknown[])?.length\n if (n) clamp(n)\n\n // Clamp when the row count shrinks. A listener sees every change (data, async ingest,\n // filters with auto-reset off); its update is notified in the next round.\n table.subscribe(() => clamp())\n\n return {\n setPageIndex(index) {\n if (!Number.isInteger(index)) {\n report('setPageIndex', 'pageIndex', index, 'call-ignored')\n return\n }\n goTo(index)\n },\n setPageSize(size) {\n if (!isSize(size)) {\n report('setPageSize', 'pageSize', size, 'call-ignored')\n return\n }\n const { s, index } = current()\n if (size === s.pageSize) return\n const firstRow = index * s.pageSize\n ctx.setState({ pageIndex: Math.floor(firstRow / size), pageSize: size })\n },\n nextPage() {\n goTo(current().index + 1)\n },\n prevPage() {\n goTo(current().index - 1)\n },\n getPageCount() {\n const count = rowCount()\n return !count ? 0 : Math.ceil(count / ctx.getState().pageSize)\n },\n canNextPage() {\n const { s, count, index } = current()\n return count !== undefined && (index + 1) * s.pageSize < count\n },\n canPrevPage() {\n return current().index > 0\n },\n }\n },\n rowModel: (indices, s) => {\n const size = isSize(s?.pageSize) ? s.pageSize : initialSize\n const index = effectiveIndex(isIndex(s?.pageIndex) ? s.pageIndex : 0, size, indices.length)\n const start = index * size\n if (start === 0 && size >= indices.length) return indices\n return indices.subarray(start, start + size)\n },\n })\n}\n"],"mappings":"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","names":["import_define","n","fixed"]}
@@ -11,8 +11,8 @@ import { paginationDiagnostics } from "./codes.js";
11
11
  keepDiagnostics(paginationDiagnostics);
12
12
  var DEFAULT_PAGE_SIZE = 10;
13
13
  var NAME = "pagination";
14
- var isIndex = (v) => Number.isInteger(v) && v >= 0;
15
- var isSize = (v) => Number.isInteger(v) && v > 0;
14
+ var isIndex = (v) => Number.isSafeInteger(v) && v >= 0;
15
+ var isSize = (v) => Number.isSafeInteger(v) && v > 0;
16
16
  var received = (v) => typeof v === "number" ? String(v) : describeValue(v);
17
17
  var paginationOptions = {
18
18
  pageSize: { type: "number", check: isSize, expected: "an integer > 0" },