@datakable/core 2.0.1 → 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.
- package/README.md +3 -2
- package/dist/{codes-B9bCK73-.d.cts → codes-4sYQv_7J.d.cts} +42 -12
- package/dist/{codes-B9bCK73-.d.ts → codes-4sYQv_7J.d.ts} +42 -12
- package/dist/column-filters.d.cts +2 -2
- package/dist/column-filters.d.ts +2 -2
- package/dist/diagnostics/build.cjs +1 -1
- package/dist/diagnostics/build.cjs.map +1 -1
- package/dist/diagnostics/build.js +2 -2
- package/dist/diagnostics/build.js.map +1 -1
- package/dist/diagnostics/console.cjs +15 -4
- package/dist/diagnostics/console.cjs.map +1 -1
- package/dist/diagnostics/console.js +15 -4
- package/dist/diagnostics/console.js.map +1 -1
- package/dist/diagnostics/explain.cjs.map +1 -1
- package/dist/diagnostics/explain.js.map +1 -1
- package/dist/diagnostics/hub.cjs +7 -1
- package/dist/diagnostics/hub.cjs.map +1 -1
- package/dist/diagnostics/hub.js +7 -1
- package/dist/diagnostics/hub.js.map +1 -1
- package/dist/diagnostics/prose.cjs +10 -1
- package/dist/diagnostics/prose.cjs.map +1 -1
- package/dist/diagnostics/prose.js +9 -1
- package/dist/diagnostics/prose.js.map +1 -1
- package/dist/diagnostics/redact.cjs +1 -1
- package/dist/diagnostics/redact.cjs.map +1 -1
- package/dist/diagnostics/redact.js +1 -1
- package/dist/diagnostics/redact.js.map +1 -1
- package/dist/diagnostics/types.cjs.map +1 -1
- package/dist/{explain-i0OATi_W.d.ts → explain-DmwN_RG9.d.cts} +13 -6
- package/dist/{explain-Q8Cg0cZy.d.cts → explain-Dr3SmMNY.d.ts} +13 -6
- package/dist/feature/install.cjs +2 -1
- package/dist/feature/install.cjs.map +1 -1
- package/dist/feature/install.js +2 -1
- package/dist/feature/install.js.map +1 -1
- package/dist/feature/slice-guard.cjs +76 -0
- package/dist/feature/slice-guard.cjs.map +1 -0
- package/dist/feature/slice-guard.js +50 -0
- package/dist/feature/slice-guard.js.map +1 -0
- package/dist/features/column-filters/codes.cjs +2 -19
- package/dist/features/column-filters/codes.cjs.map +1 -1
- package/dist/features/column-filters/codes.js +2 -19
- package/dist/features/column-filters/codes.js.map +1 -1
- package/dist/features/column-filters/column-filters.cjs +37 -22
- package/dist/features/column-filters/column-filters.cjs.map +1 -1
- package/dist/features/column-filters/column-filters.js +37 -22
- package/dist/features/column-filters/column-filters.js.map +1 -1
- package/dist/features/column-filters/match.cjs +6 -2
- package/dist/features/column-filters/match.cjs.map +1 -1
- package/dist/features/column-filters/match.js +6 -2
- package/dist/features/column-filters/match.js.map +1 -1
- package/dist/features/pagination/pagination.cjs +2 -2
- package/dist/features/pagination/pagination.cjs.map +1 -1
- package/dist/features/pagination/pagination.js +2 -2
- package/dist/features/pagination/pagination.js.map +1 -1
- package/dist/features/search/codes.cjs +18 -2
- package/dist/features/search/codes.cjs.map +1 -1
- package/dist/features/search/codes.js +16 -1
- package/dist/features/search/codes.js.map +1 -1
- package/dist/features/search/search-engine.cjs +24 -5
- package/dist/features/search/search-engine.cjs.map +1 -1
- package/dist/features/search/search-engine.js +25 -6
- package/dist/features/search/search-engine.js.map +1 -1
- package/dist/features/sorting/sorting.cjs +55 -24
- package/dist/features/sorting/sorting.cjs.map +1 -1
- package/dist/features/sorting/sorting.js +55 -24
- package/dist/features/sorting/sorting.js.map +1 -1
- package/dist/index.d.cts +26 -17
- package/dist/index.d.ts +26 -17
- package/dist/intercept/runtime.cjs +3 -2
- package/dist/intercept/runtime.cjs.map +1 -1
- package/dist/intercept/runtime.js +3 -2
- package/dist/intercept/runtime.js.map +1 -1
- package/dist/messages/index.cjs +2 -0
- package/dist/messages/index.cjs.map +1 -1
- package/dist/messages/index.js +2 -0
- package/dist/messages/index.js.map +1 -1
- package/dist/pagination.d.cts +2 -2
- package/dist/pagination.d.ts +2 -2
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/{search-async-entry-CuLYPfY_.d.cts → search-async-entry-BiemO3JT.d.cts} +2 -2
- package/dist/{search-async-entry-Cx7xpuVZ.d.ts → search-async-entry-GRj0E06n.d.ts} +2 -2
- package/dist/search-async.d.cts +3 -3
- package/dist/search-async.d.ts +3 -3
- package/dist/search.d.cts +4 -4
- package/dist/search.d.ts +4 -4
- package/dist/server.d.cts +3 -3
- package/dist/server.d.ts +3 -3
- package/dist/sorting.d.cts +2 -2
- package/dist/sorting.d.ts +2 -2
- package/dist/source/async-control.cjs +2 -1
- package/dist/source/async-control.cjs.map +1 -1
- package/dist/source/async-control.js +2 -1
- package/dist/source/async-control.js.map +1 -1
- package/dist/source/fetcher.cjs.map +1 -1
- package/dist/source/fetcher.js.map +1 -1
- package/dist/source/server.cjs +58 -5
- package/dist/source/server.cjs.map +1 -1
- package/dist/source/server.js +58 -5
- package/dist/source/server.js.map +1 -1
- package/dist/table/create-table.cjs +30 -20
- package/dist/table/create-table.cjs.map +1 -1
- package/dist/table/create-table.js +30 -20
- package/dist/table/create-table.js.map +1 -1
- package/dist/table/index.cjs +3 -0
- package/dist/table/index.cjs.map +1 -1
- package/dist/table/index.js +5 -1
- package/dist/table/index.js.map +1 -1
- package/dist/table/state-codes.cjs +72 -0
- package/dist/table/state-codes.cjs.map +1 -0
- package/dist/table/state-codes.js +45 -0
- package/dist/table/state-codes.js.map +1 -0
- package/dist/table/types.cjs.map +1 -1
- package/dist/table/types.js.map +1 -1
- package/dist/validity.d.cts +2 -2
- package/dist/validity.d.ts +2 -2
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/dist/ingest/test-schemas.cjs +0 -73
- package/dist/ingest/test-schemas.cjs.map +0 -1
- package/dist/ingest/test-schemas.js +0 -46
- package/dist/ingest/test-schemas.js.map +0 -1
|
@@ -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":[]}
|
|
@@ -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.
|
|
39
|
-
var isSize = (v) => Number.
|
|
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.
|
|
15
|
-
var isSize = (v) => Number.
|
|
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" },
|
|
@@ -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":["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":["n","fixed"]}
|
|
@@ -21,7 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var codes_exports = {};
|
|
22
22
|
__export(codes_exports, {
|
|
23
23
|
searchDiagnostics: () => searchDiagnostics,
|
|
24
|
-
searchMessages: () => searchMessages
|
|
24
|
+
searchMessages: () => searchMessages,
|
|
25
|
+
unknownKeysDiagnostics: () => unknownKeysDiagnostics
|
|
25
26
|
});
|
|
26
27
|
module.exports = __toCommonJS(codes_exports);
|
|
27
28
|
var import_catalog = require("../../diagnostics/catalog.cjs");
|
|
@@ -46,9 +47,24 @@ var searchDiagnostics = [
|
|
|
46
47
|
process.env.NODE_ENV !== "production" && searchMessages.DK_SEARCH_FAILED
|
|
47
48
|
)
|
|
48
49
|
];
|
|
50
|
+
var unknownKeysDiagnostics = process.env.NODE_ENV !== "production" ? [
|
|
51
|
+
(0, import_catalog.defineDiagnostic)({
|
|
52
|
+
code: "DK_SEARCH_UNKNOWN_KEYS",
|
|
53
|
+
level: "warn",
|
|
54
|
+
where: () => ({ kind: "option", path: "searchFn" }),
|
|
55
|
+
redact: { keys: "rowKeys" },
|
|
56
|
+
problem: (d) => `${d.count} of the ${d.returned} keys searchFn returned match no row (first: ${d.keys.map((k) => JSON.stringify(k)).join(", ")}${d.count > d.keys.length ? ", \u2026" : ""})`,
|
|
57
|
+
impact: () => "those keys are ignored; the rows of the other keys are shown",
|
|
58
|
+
fix: () => [
|
|
59
|
+
'return the rowKey values of rows in ctx.rows (compared as strings: 2 and "2" match the same row); a key the data does not hold (another page, a stale index) matches nothing',
|
|
60
|
+
"a repeated rowKey value keeps its key on the first row only: later rows carry key#1, key#2, \u2026 (as row.key reports them, DK_DUPLICATE_ROW_KEY); make rowKey unique, or return those keys"
|
|
61
|
+
]
|
|
62
|
+
})
|
|
63
|
+
] : [];
|
|
49
64
|
// Annotate the CommonJS export names for ESM import in node:
|
|
50
65
|
0 && (module.exports = {
|
|
51
66
|
searchDiagnostics,
|
|
52
|
-
searchMessages
|
|
67
|
+
searchMessages,
|
|
68
|
+
unknownKeysDiagnostics
|
|
53
69
|
});
|
|
54
70
|
//# sourceMappingURL=codes.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/features/search/codes.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from '../../diagnostics/catalog'\nimport '../../diagnostics/process-guard'\n\ndeclare module '../../diagnostics/catalog' {\n interface DiagnosticCatalog {\n DK_SEARCH_FAILED: {\n /** `error.name` of the rejection / throw; the full error is `context.cause`. */\n errorName?: string\n /** `bad-result`: resolved with neither an array of rows nor `{ keys }`. */\n problem?: 'bad-result'\n /** The type of the bad result (`describeValue`), never the value. */\n received?: string\n /** Whether a previous `searchFn` result is shown (`false`: none applied yet). */\n hadResults: boolean\n }\n DK_SEARCH_COLUMN_SKIPPED: {\n /** Column id (`key` or `id`). */\n column: string\n /** Position in the table's columns (`options.columns`, else the derived columns). */\n index: number\n }\n }\n}\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const searchMessages: DiagnosticMessages<'DK_SEARCH_FAILED'> = {\n DK_SEARCH_FAILED: {\n problem: (d) =>\n d.problem === 'bad-result'\n ? `searchFn resolved with ${d.received}, not an array of rows (from ctx.rows) or ` +\n '{ keys: RowKey[] }'\n : `searchFn rejected or threw${d.errorName ? ` ${d.errorName}` : ''}`,\n impact: (d) =>\n d.hadResults\n ? \"the previous search results are kept (status: 'error')\"\n : \"the rows are shown unsearched (status: 'error')\",\n fix: (d) => [\n d.problem === 'bad-result'\n ? 'return ctx.rows.filter(...) (the row objects themselves) or { keys: [...] } with the ' +\n 'rowKey values of the matching rows'\n : 'check the request searchFn sends (the original error is context.cause, passed to ' +\n 'onDiagnostic)',\n 'retry from the UI with flushSearch(); the next query change runs searchFn again',\n ],\n },\n}\n\n/** Codes owned by the `search` feature (spec §6.3, §8.3). */\nexport const searchDiagnostics = [\n defineDiagnostic(\n {\n code: 'DK_SEARCH_FAILED',\n level: 'error',\n where: () => ({ kind: 'option', path: 'searchFn' }),\n },\n (process.env.NODE_ENV !== \"production\") && searchMessages.DK_SEARCH_FAILED,\n ),\n]\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA0D;AAC1D,2BAAO;
|
|
1
|
+
{"version":3,"sources":["../../../src/features/search/codes.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from '../../diagnostics/catalog'\nimport '../../diagnostics/process-guard'\n\ndeclare module '../../diagnostics/catalog' {\n interface DiagnosticCatalog {\n DK_SEARCH_FAILED: {\n /** `error.name` of the rejection / throw; the full error is `context.cause`. */\n errorName?: string\n /** `bad-result`: resolved with neither an array of rows nor `{ keys }`. */\n problem?: 'bad-result'\n /** The type of the bad result (`describeValue`), never the value. */\n received?: string\n /** Whether a previous `searchFn` result is shown (`false`: none applied yet). */\n hadResults: boolean\n }\n DK_SEARCH_UNKNOWN_KEYS: {\n /** Returned `{ keys }` that match no row's key (compared as strings). */\n count: number\n /** How many keys `searchFn` returned. */\n returned: number\n /** A sample of the unmatched keys (first few; redacted as row keys). */\n keys: readonly unknown[]\n }\n DK_SEARCH_COLUMN_SKIPPED: {\n /** Column id (`key` or `id`). */\n column: string\n /** Position in the table's columns (`options.columns`, else the derived columns). */\n index: number\n }\n }\n}\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const searchMessages: DiagnosticMessages<'DK_SEARCH_FAILED'> = {\n DK_SEARCH_FAILED: {\n problem: (d) =>\n d.problem === 'bad-result'\n ? `searchFn resolved with ${d.received}, not an array of rows (from ctx.rows) or ` +\n '{ keys: RowKey[] }'\n : `searchFn rejected or threw${d.errorName ? ` ${d.errorName}` : ''}`,\n impact: (d) =>\n d.hadResults\n ? \"the previous search results are kept (status: 'error')\"\n : \"the rows are shown unsearched (status: 'error')\",\n fix: (d) => [\n d.problem === 'bad-result'\n ? 'return ctx.rows.filter(...) (the row objects themselves) or { keys: [...] } with the ' +\n 'rowKey values of the matching rows'\n : 'check the request searchFn sends (the original error is context.cause, passed to ' +\n 'onDiagnostic)',\n 'retry from the UI with flushSearch(); the next query change runs searchFn again',\n ],\n },\n}\n\n/** Codes owned by the `search` feature (spec §6.3, §8.3). */\nexport const searchDiagnostics = [\n defineDiagnostic(\n {\n code: 'DK_SEARCH_FAILED',\n level: 'error',\n where: () => ({ kind: 'option', path: 'searchFn' }),\n },\n (process.env.NODE_ENV !== \"production\") && searchMessages.DK_SEARCH_FAILED,\n ),\n]\n\n/**\n * `DK_SEARCH_UNKNOWN_KEYS` (#86) is a development hint: some `{ keys }` of a `searchFn` result match\n * no row. Its definition is behind `(process.env.NODE_ENV !== \"production\")`, so a production bundle has none of it.\n */\nexport const unknownKeysDiagnostics = (process.env.NODE_ENV !== \"production\")\n ? [\n defineDiagnostic({\n code: 'DK_SEARCH_UNKNOWN_KEYS',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'searchFn' }),\n redact: { keys: 'rowKeys' },\n problem: (d) =>\n `${d.count} of the ${d.returned} keys searchFn returned match no row ` +\n `(first: ${d.keys.map((k) => JSON.stringify(k)).join(', ')}${d.count > d.keys.length ? ', …' : ''})`,\n impact: () => 'those keys are ignored; the rows of the other keys are shown',\n fix: () => [\n 'return the rowKey values of rows in ctx.rows (compared as strings: 2 and \"2\" match the ' +\n 'same row); a key the data does not hold (another page, a stale index) matches nothing',\n 'a repeated rowKey value keeps its key on the first row only: later rows carry key#1, ' +\n 'key#2, … (as row.key reports them, DK_DUPLICATE_ROW_KEY); make rowKey unique, or ' +\n 'return those keys',\n ],\n }),\n ]\n : []\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA0D;AAC1D,2BAAO;AAgCA,IAAM,iBAAyD;AAAA,EACpE,kBAAkB;AAAA,IAChB,SAAS,CAAC,MACR,EAAE,YAAY,eACV,0BAA0B,EAAE,QAAQ,iEAEpC,6BAA6B,EAAE,YAAY,IAAI,EAAE,SAAS,KAAK,EAAE;AAAA,IACvE,QAAQ,CAAC,MACP,EAAE,aACE,2DACA;AAAA,IACN,KAAK,CAAC,MAAM;AAAA,MACV,EAAE,YAAY,eACV,4HAEA;AAAA,MAEJ;AAAA,IACF;AAAA,EACF;AACF;AAGO,IAAM,oBAAoB;AAAA,MAC/B;AAAA,IACE;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,OAAO,OAAO,EAAE,MAAM,UAAU,MAAM,WAAW;AAAA,IACnD;AAAA,IACC,QAAQ,IAAI,aAAa,gBAAiB,eAAe;AAAA,EAC5D;AACF;AAMO,IAAM,yBAA0B,QAAQ,IAAI,aAAa,eAC5D;AAAA,MACE,iCAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,OAAO,OAAO,EAAE,MAAM,UAAU,MAAM,WAAW;AAAA,IACjD,QAAQ,EAAE,MAAM,UAAU;AAAA,IAC1B,SAAS,CAAC,MACR,GAAG,EAAE,KAAK,WAAW,EAAE,QAAQ,gDACpB,EAAE,KAAK,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,SAAS,aAAQ,EAAE;AAAA,IACnG,QAAQ,MAAM;AAAA,IACd,KAAK,MAAM;AAAA,MACT;AAAA,MAEA;AAAA,IAGF;AAAA,EACF,CAAC;AACH,IACA,CAAC;","names":[]}
|
|
@@ -21,8 +21,23 @@ var searchDiagnostics = [
|
|
|
21
21
|
process.env.NODE_ENV !== "production" && searchMessages.DK_SEARCH_FAILED
|
|
22
22
|
)
|
|
23
23
|
];
|
|
24
|
+
var unknownKeysDiagnostics = process.env.NODE_ENV !== "production" ? [
|
|
25
|
+
defineDiagnostic({
|
|
26
|
+
code: "DK_SEARCH_UNKNOWN_KEYS",
|
|
27
|
+
level: "warn",
|
|
28
|
+
where: () => ({ kind: "option", path: "searchFn" }),
|
|
29
|
+
redact: { keys: "rowKeys" },
|
|
30
|
+
problem: (d) => `${d.count} of the ${d.returned} keys searchFn returned match no row (first: ${d.keys.map((k) => JSON.stringify(k)).join(", ")}${d.count > d.keys.length ? ", \u2026" : ""})`,
|
|
31
|
+
impact: () => "those keys are ignored; the rows of the other keys are shown",
|
|
32
|
+
fix: () => [
|
|
33
|
+
'return the rowKey values of rows in ctx.rows (compared as strings: 2 and "2" match the same row); a key the data does not hold (another page, a stale index) matches nothing',
|
|
34
|
+
"a repeated rowKey value keeps its key on the first row only: later rows carry key#1, key#2, \u2026 (as row.key reports them, DK_DUPLICATE_ROW_KEY); make rowKey unique, or return those keys"
|
|
35
|
+
]
|
|
36
|
+
})
|
|
37
|
+
] : [];
|
|
24
38
|
export {
|
|
25
39
|
searchDiagnostics,
|
|
26
|
-
searchMessages
|
|
40
|
+
searchMessages,
|
|
41
|
+
unknownKeysDiagnostics
|
|
27
42
|
};
|
|
28
43
|
//# sourceMappingURL=codes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/features/search/codes.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from '../../diagnostics/catalog'\nimport '../../diagnostics/process-guard'\n\ndeclare module '../../diagnostics/catalog' {\n interface DiagnosticCatalog {\n DK_SEARCH_FAILED: {\n /** `error.name` of the rejection / throw; the full error is `context.cause`. */\n errorName?: string\n /** `bad-result`: resolved with neither an array of rows nor `{ keys }`. */\n problem?: 'bad-result'\n /** The type of the bad result (`describeValue`), never the value. */\n received?: string\n /** Whether a previous `searchFn` result is shown (`false`: none applied yet). */\n hadResults: boolean\n }\n DK_SEARCH_COLUMN_SKIPPED: {\n /** Column id (`key` or `id`). */\n column: string\n /** Position in the table's columns (`options.columns`, else the derived columns). */\n index: number\n }\n }\n}\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const searchMessages: DiagnosticMessages<'DK_SEARCH_FAILED'> = {\n DK_SEARCH_FAILED: {\n problem: (d) =>\n d.problem === 'bad-result'\n ? `searchFn resolved with ${d.received}, not an array of rows (from ctx.rows) or ` +\n '{ keys: RowKey[] }'\n : `searchFn rejected or threw${d.errorName ? ` ${d.errorName}` : ''}`,\n impact: (d) =>\n d.hadResults\n ? \"the previous search results are kept (status: 'error')\"\n : \"the rows are shown unsearched (status: 'error')\",\n fix: (d) => [\n d.problem === 'bad-result'\n ? 'return ctx.rows.filter(...) (the row objects themselves) or { keys: [...] } with the ' +\n 'rowKey values of the matching rows'\n : 'check the request searchFn sends (the original error is context.cause, passed to ' +\n 'onDiagnostic)',\n 'retry from the UI with flushSearch(); the next query change runs searchFn again',\n ],\n },\n}\n\n/** Codes owned by the `search` feature (spec §6.3, §8.3). */\nexport const searchDiagnostics = [\n defineDiagnostic(\n {\n code: 'DK_SEARCH_FAILED',\n level: 'error',\n where: () => ({ kind: 'option', path: 'searchFn' }),\n },\n (process.env.NODE_ENV !== \"production\") && searchMessages.DK_SEARCH_FAILED,\n ),\n]\n"],"mappings":";AAAA,SAAkC,wBAAwB;AAC1D,OAAO;
|
|
1
|
+
{"version":3,"sources":["../../../src/features/search/codes.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from '../../diagnostics/catalog'\nimport '../../diagnostics/process-guard'\n\ndeclare module '../../diagnostics/catalog' {\n interface DiagnosticCatalog {\n DK_SEARCH_FAILED: {\n /** `error.name` of the rejection / throw; the full error is `context.cause`. */\n errorName?: string\n /** `bad-result`: resolved with neither an array of rows nor `{ keys }`. */\n problem?: 'bad-result'\n /** The type of the bad result (`describeValue`), never the value. */\n received?: string\n /** Whether a previous `searchFn` result is shown (`false`: none applied yet). */\n hadResults: boolean\n }\n DK_SEARCH_UNKNOWN_KEYS: {\n /** Returned `{ keys }` that match no row's key (compared as strings). */\n count: number\n /** How many keys `searchFn` returned. */\n returned: number\n /** A sample of the unmatched keys (first few; redacted as row keys). */\n keys: readonly unknown[]\n }\n DK_SEARCH_COLUMN_SKIPPED: {\n /** Column id (`key` or `id`). */\n column: string\n /** Position in the table's columns (`options.columns`, else the derived columns). */\n index: number\n }\n }\n}\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const searchMessages: DiagnosticMessages<'DK_SEARCH_FAILED'> = {\n DK_SEARCH_FAILED: {\n problem: (d) =>\n d.problem === 'bad-result'\n ? `searchFn resolved with ${d.received}, not an array of rows (from ctx.rows) or ` +\n '{ keys: RowKey[] }'\n : `searchFn rejected or threw${d.errorName ? ` ${d.errorName}` : ''}`,\n impact: (d) =>\n d.hadResults\n ? \"the previous search results are kept (status: 'error')\"\n : \"the rows are shown unsearched (status: 'error')\",\n fix: (d) => [\n d.problem === 'bad-result'\n ? 'return ctx.rows.filter(...) (the row objects themselves) or { keys: [...] } with the ' +\n 'rowKey values of the matching rows'\n : 'check the request searchFn sends (the original error is context.cause, passed to ' +\n 'onDiagnostic)',\n 'retry from the UI with flushSearch(); the next query change runs searchFn again',\n ],\n },\n}\n\n/** Codes owned by the `search` feature (spec §6.3, §8.3). */\nexport const searchDiagnostics = [\n defineDiagnostic(\n {\n code: 'DK_SEARCH_FAILED',\n level: 'error',\n where: () => ({ kind: 'option', path: 'searchFn' }),\n },\n (process.env.NODE_ENV !== \"production\") && searchMessages.DK_SEARCH_FAILED,\n ),\n]\n\n/**\n * `DK_SEARCH_UNKNOWN_KEYS` (#86) is a development hint: some `{ keys }` of a `searchFn` result match\n * no row. Its definition is behind `(process.env.NODE_ENV !== \"production\")`, so a production bundle has none of it.\n */\nexport const unknownKeysDiagnostics = (process.env.NODE_ENV !== \"production\")\n ? [\n defineDiagnostic({\n code: 'DK_SEARCH_UNKNOWN_KEYS',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'searchFn' }),\n redact: { keys: 'rowKeys' },\n problem: (d) =>\n `${d.count} of the ${d.returned} keys searchFn returned match no row ` +\n `(first: ${d.keys.map((k) => JSON.stringify(k)).join(', ')}${d.count > d.keys.length ? ', …' : ''})`,\n impact: () => 'those keys are ignored; the rows of the other keys are shown',\n fix: () => [\n 'return the rowKey values of rows in ctx.rows (compared as strings: 2 and \"2\" match the ' +\n 'same row); a key the data does not hold (another page, a stale index) matches nothing',\n 'a repeated rowKey value keeps its key on the first row only: later rows carry key#1, ' +\n 'key#2, … (as row.key reports them, DK_DUPLICATE_ROW_KEY); make rowKey unique, or ' +\n 'return those keys',\n ],\n }),\n ]\n : []\n"],"mappings":";AAAA,SAAkC,wBAAwB;AAC1D,OAAO;AAgCA,IAAM,iBAAyD;AAAA,EACpE,kBAAkB;AAAA,IAChB,SAAS,CAAC,MACR,EAAE,YAAY,eACV,0BAA0B,EAAE,QAAQ,iEAEpC,6BAA6B,EAAE,YAAY,IAAI,EAAE,SAAS,KAAK,EAAE;AAAA,IACvE,QAAQ,CAAC,MACP,EAAE,aACE,2DACA;AAAA,IACN,KAAK,CAAC,MAAM;AAAA,MACV,EAAE,YAAY,eACV,4HAEA;AAAA,MAEJ;AAAA,IACF;AAAA,EACF;AACF;AAGO,IAAM,oBAAoB;AAAA,EAC/B;AAAA,IACE;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,OAAO,OAAO,EAAE,MAAM,UAAU,MAAM,WAAW;AAAA,IACnD;AAAA,IACC,QAAQ,IAAI,aAAa,gBAAiB,eAAe;AAAA,EAC5D;AACF;AAMO,IAAM,yBAA0B,QAAQ,IAAI,aAAa,eAC5D;AAAA,EACE,iBAAiB;AAAA,IACf,MAAM;AAAA,IACN,OAAO;AAAA,IACP,OAAO,OAAO,EAAE,MAAM,UAAU,MAAM,WAAW;AAAA,IACjD,QAAQ,EAAE,MAAM,UAAU;AAAA,IAC1B,SAAS,CAAC,MACR,GAAG,EAAE,KAAK,WAAW,EAAE,QAAQ,gDACpB,EAAE,KAAK,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,SAAS,aAAQ,EAAE;AAAA,IACnG,QAAQ,MAAM;AAAA,IACd,KAAK,MAAM;AAAA,MACT;AAAA,MAEA;AAAA,IAGF;AAAA,EACF,CAAC;AACH,IACA,CAAC;","names":[]}
|
|
@@ -24,6 +24,7 @@ __export(search_engine_exports, {
|
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(search_engine_exports);
|
|
26
26
|
var import_catalog = require("../../diagnostics/catalog.cjs");
|
|
27
|
+
var import_process_guard = require("../../diagnostics/process-guard.cjs");
|
|
27
28
|
var import_redact = require("../../diagnostics/redact.cjs");
|
|
28
29
|
var import_row_key = require("../../ingest/row-key.cjs");
|
|
29
30
|
var import_classify = require("../../intercept/classify.cjs");
|
|
@@ -32,10 +33,12 @@ var import_internals = require("../../table/internals.cjs");
|
|
|
32
33
|
var import_codes = require("./codes.cjs");
|
|
33
34
|
var import_search_fn_brand = require("./search-fn-brand.cjs");
|
|
34
35
|
(0, import_catalog.keepDiagnostics)(import_codes.searchDiagnostics);
|
|
36
|
+
(0, import_catalog.keepDiagnostics)(import_codes.unknownKeysDiagnostics);
|
|
35
37
|
var EMPTY = [];
|
|
36
38
|
var ERROR = Object.freeze({ status: "error" });
|
|
37
39
|
var NO_KEYS = () => {
|
|
38
40
|
};
|
|
41
|
+
var SAMPLE = 5;
|
|
39
42
|
var SearchFailure = class {
|
|
40
43
|
constructor(error) {
|
|
41
44
|
this.error = error;
|
|
@@ -61,34 +64,43 @@ function createSearchEngine(fn) {
|
|
|
61
64
|
function indicesOf(a, data, rowKey) {
|
|
62
65
|
const c = a.cache;
|
|
63
66
|
if (c && c.data === data && c.rowKey === rowKey) return c.indices;
|
|
64
|
-
let
|
|
67
|
+
let find;
|
|
65
68
|
let list;
|
|
66
69
|
if ("rows" in a.hits) {
|
|
67
70
|
list = a.hits.rows;
|
|
68
|
-
map = positions.get(data) ?? /* @__PURE__ */ new Map();
|
|
71
|
+
const map = positions.get(data) ?? /* @__PURE__ */ new Map();
|
|
69
72
|
if (!positions.has(data)) {
|
|
70
73
|
for (let i = 0; i < data.length; i++) if (!map.has(data[i])) map.set(data[i], i);
|
|
71
74
|
positions.set(data, map);
|
|
72
75
|
}
|
|
76
|
+
find = (v) => map.get(v);
|
|
73
77
|
} else {
|
|
74
78
|
list = a.hits.keys;
|
|
75
79
|
if (!keyIndex || keyIndex.data !== data || keyIndex.rowKey !== rowKey) {
|
|
76
80
|
const keyOf = (0, import_row_key.resolveRowKeys)(data, rowKey, NO_KEYS, NO_KEYS);
|
|
77
81
|
const m = /* @__PURE__ */ new Map();
|
|
78
82
|
for (let i = 0; i < data.length; i++) {
|
|
79
|
-
const k = keyOf(data[i], i);
|
|
83
|
+
const k = String(keyOf(data[i], i));
|
|
80
84
|
if (!m.has(k)) m.set(k, i);
|
|
81
85
|
}
|
|
82
86
|
keyIndex = { data, rowKey, map: m };
|
|
83
87
|
}
|
|
84
|
-
map = keyIndex.map;
|
|
88
|
+
const map = keyIndex.map;
|
|
89
|
+
find = (v) => map.get(String(v));
|
|
85
90
|
}
|
|
86
91
|
const out = new Uint32Array(list.length);
|
|
87
92
|
let n = 0;
|
|
93
|
+
let unknown;
|
|
88
94
|
for (const v of list) {
|
|
89
|
-
const i =
|
|
95
|
+
const i = find(v);
|
|
90
96
|
if (i !== void 0) out[n++] = i;
|
|
97
|
+
else if (process.env.NODE_ENV !== "production" && "keys" in a.hits) {
|
|
98
|
+
unknown ??= { count: 0, returned: list.length, keys: [] };
|
|
99
|
+
unknown.count++;
|
|
100
|
+
if (unknown.keys.length < SAMPLE) unknown.keys.push(v);
|
|
101
|
+
}
|
|
91
102
|
}
|
|
103
|
+
if (process.env.NODE_ENV !== "production") a.unknown = unknown;
|
|
92
104
|
const indices = out.subarray(0, n);
|
|
93
105
|
a.cache = { data, rowKey, indices };
|
|
94
106
|
return indices;
|
|
@@ -206,6 +218,13 @@ function createSearchEngine(fn) {
|
|
|
206
218
|
}
|
|
207
219
|
};
|
|
208
220
|
checks.set(table, () => {
|
|
221
|
+
if (process.env.NODE_ENV !== "production") {
|
|
222
|
+
const u = results.get(table);
|
|
223
|
+
if (u?.unknown && !dead) {
|
|
224
|
+
ctx.emit("DK_SEARCH_UNKNOWN_KEYS", u.unknown);
|
|
225
|
+
u.unknown = void 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
209
228
|
if (scheduled || dead) return;
|
|
210
229
|
const query = applied();
|
|
211
230
|
if (query.trim() === "") return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/features/search/search-engine.ts"],"sourcesContent":["import { keepDiagnostics } from '../../diagnostics/catalog'\nimport { describeValue } from '../../diagnostics/redact'\nimport type { RowModelContext } from '../../feature/types'\nimport { resolveRowKeys } from '../../ingest/row-key'\nimport { errorName } from '../../intercept/classify'\nimport type { RowKey } from '../../pipeline/row'\nimport type { AsyncStatus } from '../../source/async-control'\nimport { requestsOf } from '../../source/requests'\nimport { getTableInternals } from '../../table/internals'\nimport { searchDiagnostics } from './codes'\nimport {\n APPLIED,\n type SearchEngine,\n type SearchFn,\n type SearchFnContext,\n type SearchHost,\n type SearchOutcome,\n type SearchSession,\n SUPERSEDED,\n type Waiter,\n} from './search-fn-brand'\n\nkeepDiagnostics(searchDiagnostics)\n\nconst EMPTY: readonly never[] = []\nconst ERROR: SearchOutcome = Object.freeze({ status: 'error' })\nconst NO_KEYS = () => {}\n\n/** A `searchFn` that rejected or threw: resolved as a value so asyncControl reports 'applied'. */\nclass SearchFailure {\n constructor(readonly error: unknown) {}\n}\n\nconst isAbortError = (e: unknown) =>\n e != null && typeof e === 'object' && (e as { name?: unknown }).name === 'AbortError'\n\nasync function callSearch(fn: SearchFn<unknown>, c: SearchFnContext<unknown>): Promise<unknown> {\n try {\n return await fn(c)\n } catch (e) {\n if (c.signal.aborted || isAbortError(e)) throw e\n return new SearchFailure(e)\n }\n}\n\n/** A `searchFn` result in one of its two shapes, or `undefined` when malformed. */\ntype Hits = { rows: readonly unknown[] } | { keys: readonly RowKey[] }\nconst toHits = (v: unknown): Hits | undefined =>\n Array.isArray(v)\n ? { rows: v }\n : v !== null && typeof v === 'object' && Array.isArray((v as { keys?: unknown }).keys)\n ? { keys: (v as { keys: readonly RowKey[] }).keys }\n : undefined\n\n/** The applied `searchFn` result of a table, and its data indices per (data, rowKey). */\ninterface Applied {\n query: string\n /** The data `searchFn` searched. */\n data: readonly unknown[]\n hits: Hits\n cache?: { data: readonly unknown[]; rowKey: unknown; indices: Uint32Array }\n}\n\n/**\n * The async search engine of one `search({ searchFn: withSearchFn(fn) })` feature instance (§6.3):\n * each applied non-blank query runs `fn` through `async:request` (latest wins, abort, `timeout`);\n * while it runs `status` is `'searching'` and the previous results stay; a failure keeps them too\n * (`DK_SEARCH_FAILED`, `status: 'error'`); new data runs it again for the applied query.\n */\nexport function createSearchEngine(fn: SearchFn<unknown>): SearchEngine {\n /** Per table: the applied `searchFn` result (the previous results while searching / on error). */\n const results = new WeakMap<object, Applied>()\n /** Per table: `check` after a pipeline run. */\n const checks = new WeakMap<object, () => void>()\n /** The table whose `pipeline:run` is in progress (`rowModel` has no table reference). */\n let active: object | undefined\n /** Data → row index, to map result rows back (rows are compared by identity). */\n const positions = new WeakMap<readonly unknown[], Map<unknown, number>>()\n let keyIndex: { data: readonly unknown[]; rowKey: unknown; map: Map<unknown, number> } | undefined\n /** The last step: same input and hits → same output (no per-run allocation). */\n let last: { input: Uint32Array; hits: Uint32Array; output: Uint32Array } | undefined\n\n /** The data indices of an applied result over `data`, in result order (memoised). */\n function indicesOf(a: Applied, data: readonly unknown[], rowKey: unknown): Uint32Array {\n const c = a.cache\n if (c && c.data === data && c.rowKey === rowKey) return c.indices\n let map: Map<unknown, number>\n let list: readonly unknown[]\n if ('rows' in a.hits) {\n list = a.hits.rows\n map = positions.get(data) ?? new Map()\n if (!positions.has(data)) {\n for (let i = 0; i < data.length; i++) if (!map.has(data[i])) map.set(data[i], i)\n positions.set(data, map)\n }\n } else {\n list = a.hits.keys\n if (!keyIndex || keyIndex.data !== data || keyIndex.rowKey !== rowKey) {\n // Diagnostics about the keys are the pipeline's (it resolves the same keys).\n const keyOf = resolveRowKeys(data, rowKey, NO_KEYS, NO_KEYS)\n const m = new Map<unknown, number>()\n for (let i = 0; i < data.length; i++) {\n const k = keyOf(data[i], i)\n if (!m.has(k)) m.set(k, i)\n }\n keyIndex = { data, rowKey, map: m }\n }\n map = keyIndex.map\n }\n const out = new Uint32Array(list.length)\n let n = 0\n for (const v of list) {\n const i = map.get(v)\n if (i !== undefined) out[n++] = i\n }\n const indices = out.subarray(0, n)\n a.cache = { data, rowKey, indices }\n return indices\n }\n\n function step(input: Uint32Array, ctx: RowModelContext): Uint32Array {\n const a = active && results.get(active)\n // Nothing applied yet (the first search is in flight or failed): the rows stay unsearched.\n if (!a) return input\n const rowKey = (active as { getOptions(): { rowKey?: unknown } }).getOptions().rowKey\n const hits = indicesOf(a, ctx.data, rowKey)\n if (last && last.input === input && last.hits === hits) return last.output\n const take = new Uint8Array(ctx.data.length)\n for (let k = 0; k < input.length; k++) take[input[k] as number] = 1\n const out = new Uint32Array(Math.min(hits.length, input.length))\n let n = 0\n for (let k = 0; k < hits.length; k++) {\n const i = hits[k] as number\n if (take[i] === 1) {\n take[i] = 0\n out[n++] = i\n }\n }\n const output = out.subarray(0, n)\n last = { input, hits, output }\n return output\n }\n\n function run<R>(table: object, next: () => R): R {\n // `step` reads the table's applied searchFn result through `active`.\n const prev = active\n active = table\n try {\n return next()\n } finally {\n active = prev\n checks.get(table)?.()\n }\n }\n\n function session(host: SearchHost): SearchSession {\n const { ctx, write, applied, input, claim } = host\n const table = ctx.table as unknown as object\n let dead = false\n /** The in-flight search: its query, data, waiters and abort handle. */\n let attempt:\n | { query: string; data: readonly unknown[]; waiters: Waiter[]; ctl: AbortController }\n | undefined\n /** (query, data) last handed to searchFn: `check()` never retries it in a loop. */\n let tried: { query: string; data: readonly unknown[] } | undefined\n let scheduled = false\n /**\n * SSR (§7.2): the initial query whose result is `initialData` (no searchFn call for it). Without\n * initialData, a deferred table (`createDeferred`) waits for `startTable` to search it.\n */\n let seeded: string | undefined\n if (applied().trim() !== '') {\n const opts = ctx.table.getOptions()\n if (opts.initialData !== undefined && opts.data === undefined) seeded = applied()\n else requestsOf(table).defer('searchFn')\n }\n\n /** Ends the in-flight search without its result (its waiters: `'superseded'`). */\n const abort = () => {\n const a = attempt\n if (!a) return\n attempt = undefined\n a.ctl.abort()\n for (const w of a.waiters) w.resolve(SUPERSEDED)\n }\n\n const startSearch = (query: string, inp: string) => {\n const data = ctx.table.getData() as readonly unknown[]\n const waiters: Waiter[] = []\n const prev = attempt\n if (prev) {\n attempt = undefined\n // Same query (new data): its waiters follow the new request; else they are replaced.\n if (prev.query === query) waiters.push(...prev.waiters)\n else for (const w of prev.waiters) w.resolve(SUPERSEDED)\n prev.ctl.abort()\n }\n waiters.push(...claim(query))\n const a = { query, data, waiters, ctl: new AbortController() }\n attempt = a\n tried = { query, data }\n write(inp, applied(), 'searching')\n const c = {\n query: query.trim(),\n rows: data,\n columns: ctx.table.getOptions().columns ?? EMPTY,\n }\n requestsOf(table)\n .request('searchFn', (signal) => callSearch(fn, { ...c, signal }), {\n signal: a.ctl.signal,\n onSettle: (value, status) => settle(a, value, status),\n })\n // strict: the DatakableError went to the hub; the outcome was settled in onSettle.\n .catch(() => settle(a, undefined, 'error'))\n }\n\n const settle = (a: NonNullable<typeof attempt>, value: unknown, status: AsyncStatus) => {\n // Replaced, cancelled or destroyed: its waiters were resolved then.\n if (attempt !== a || dead) return\n attempt = undefined\n const hits = status === 'applied' && !(value instanceof SearchFailure) && toHits(value)\n if (hits) {\n results.set(table, { query: a.query, data: a.data, hits })\n write(input(), a.query, 'idle')\n for (const w of a.waiters) w.resolve(APPLIED)\n return\n }\n if (status === 'superseded' || status === 'aborted') {\n // An AbortError from searchFn (not ours): not an error (§6.3).\n write(input(), applied(), 'idle')\n for (const w of a.waiters) w.resolve(SUPERSEDED)\n return\n }\n const failure = value instanceof SearchFailure ? value : undefined\n const error =\n status === 'timeout'\n ? new DOMException('searchFn timed out.', 'TimeoutError')\n : failure?.error\n write(input(), applied(), 'error', error)\n for (const w of a.waiters) w.resolve(ERROR)\n // 'error' / 'timeout': already reported by the errorBoundary / asyncControl.\n if (status !== 'applied') return\n const hadResults = results.has(table)\n if (failure) {\n const name = errorName(failure.error)\n ctx.emit(\n 'DK_SEARCH_FAILED',\n { ...(name === undefined ? {} : { errorName: name }), hadResults },\n { cause: failure.error },\n )\n } else {\n ctx.emit('DK_SEARCH_FAILED', {\n problem: 'bad-result',\n received: describeValue(value),\n hadResults,\n })\n }\n }\n\n /** After a pipeline run: new data or an outside query without a result → search again. */\n checks.set(table, () => {\n if (scheduled || dead) return\n const query = applied()\n if (query.trim() === '') return\n const data = ctx.table.getData()\n const r = results.get(table)\n if (r && r.query === query && r.data === data) return\n if (tried && tried.query === query && tried.data === data) return\n if (seeded !== undefined) {\n const initial = seeded === query && getTableInternals(table).initialRows() === data\n seeded = undefined\n if (initial) {\n results.set(table, { query, data, hits: { rows: data } })\n return\n }\n }\n if (requestsOf(table).defer('searchFn')) return\n scheduled = true\n queueMicrotask(() => {\n scheduled = false\n const q = applied()\n const d = ctx.table.getData()\n if (dead || q.trim() === '' || (tried && tried.query === q && tried.data === d)) return\n const cur = results.get(table)\n if (cur && cur.query === q && cur.data === d) return\n startSearch(q, input())\n })\n })\n\n return {\n apply(query, inp) {\n const data = ctx.table.getData()\n const r = results.get(table)\n if (query.trim() === '' || (query === applied() && r?.query === query && r.data === data)) {\n abort()\n if (query.trim() === '') results.delete(table)\n return false\n }\n if (attempt && attempt.query === query && attempt.data === data) {\n attempt.waiters.push(...claim(query))\n write(inp, applied(), 'searching')\n return true\n }\n startSearch(query, inp)\n return true\n },\n busy: () => attempt !== undefined,\n abort,\n destroy() {\n dead = true\n abort()\n },\n }\n }\n\n return { step, run, session }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/features/search/search-engine.ts"],"sourcesContent":["import { keepDiagnostics } from '../../diagnostics/catalog'\nimport '../../diagnostics/process-guard'\nimport { describeValue } from '../../diagnostics/redact'\nimport type { RowModelContext } from '../../feature/types'\nimport { resolveRowKeys } from '../../ingest/row-key'\nimport { errorName } from '../../intercept/classify'\nimport type { RowKey } from '../../pipeline/row'\nimport type { AsyncStatus } from '../../source/async-control'\nimport { requestsOf } from '../../source/requests'\nimport { getTableInternals } from '../../table/internals'\nimport { searchDiagnostics, unknownKeysDiagnostics } from './codes'\nimport {\n APPLIED,\n type SearchEngine,\n type SearchFn,\n type SearchFnContext,\n type SearchHost,\n type SearchOutcome,\n type SearchSession,\n SUPERSEDED,\n type Waiter,\n} from './search-fn-brand'\n\nkeepDiagnostics(searchDiagnostics)\nkeepDiagnostics(unknownKeysDiagnostics)\n\nconst EMPTY: readonly never[] = []\nconst ERROR: SearchOutcome = Object.freeze({ status: 'error' })\nconst NO_KEYS = () => {}\n/** Unmatched `{ keys }` listed in a `DK_SEARCH_UNKNOWN_KEYS` sample. */\nconst SAMPLE = 5\n\n/** A `searchFn` that rejected or threw: resolved as a value so asyncControl reports 'applied'. */\nclass SearchFailure {\n constructor(readonly error: unknown) {}\n}\n\nconst isAbortError = (e: unknown) =>\n e != null && typeof e === 'object' && (e as { name?: unknown }).name === 'AbortError'\n\nasync function callSearch(fn: SearchFn<unknown>, c: SearchFnContext<unknown>): Promise<unknown> {\n try {\n return await fn(c)\n } catch (e) {\n if (c.signal.aborted || isAbortError(e)) throw e\n return new SearchFailure(e)\n }\n}\n\n/** A `searchFn` result in one of its two shapes, or `undefined` when malformed. */\ntype Hits = { rows: readonly unknown[] } | { keys: readonly RowKey[] }\nconst toHits = (v: unknown): Hits | undefined =>\n Array.isArray(v)\n ? { rows: v }\n : v !== null && typeof v === 'object' && Array.isArray((v as { keys?: unknown }).keys)\n ? { keys: (v as { keys: readonly RowKey[] }).keys }\n : undefined\n\n/** The applied `searchFn` result of a table, and its data indices per (data, rowKey). */\ninterface Applied {\n query: string\n /** The data `searchFn` searched. */\n data: readonly unknown[]\n hits: Hits\n cache?: { data: readonly unknown[]; rowKey: unknown; indices: Uint32Array }\n /** Development: returned `{ keys }` that match no row, until reported (`DK_SEARCH_UNKNOWN_KEYS`). */\n unknown?: { count: number; returned: number; keys: unknown[] } | undefined\n}\n\n/**\n * The async search engine of one `search({ searchFn: withSearchFn(fn) })` feature instance (§6.3):\n * each applied non-blank query runs `fn` through `async:request` (latest wins, abort, `timeout`);\n * while it runs `status` is `'searching'` and the previous results stay; a failure keeps them too\n * (`DK_SEARCH_FAILED`, `status: 'error'`); new data runs it again for the applied query.\n */\nexport function createSearchEngine(fn: SearchFn<unknown>): SearchEngine {\n /** Per table: the applied `searchFn` result (the previous results while searching / on error). */\n const results = new WeakMap<object, Applied>()\n /** Per table: `check` after a pipeline run. */\n const checks = new WeakMap<object, () => void>()\n /** The table whose `pipeline:run` is in progress (`rowModel` has no table reference). */\n let active: object | undefined\n /** Data → row index, to map result rows back (rows are compared by identity). */\n const positions = new WeakMap<readonly unknown[], Map<unknown, number>>()\n /** Row key, as a string → data index: keys compare as strings (`2` matches `'2'`), as in row-key.ts. */\n let keyIndex: { data: readonly unknown[]; rowKey: unknown; map: Map<string, number> } | undefined\n /** The last step: same input and hits → same output (no per-run allocation). */\n let last: { input: Uint32Array; hits: Uint32Array; output: Uint32Array } | undefined\n\n /** The data indices of an applied result over `data`, in result order (memoised). */\n function indicesOf(a: Applied, data: readonly unknown[], rowKey: unknown): Uint32Array {\n const c = a.cache\n if (c && c.data === data && c.rowKey === rowKey) return c.indices\n let find: (v: unknown) => number | undefined\n let list: readonly unknown[]\n if ('rows' in a.hits) {\n list = a.hits.rows\n const map = positions.get(data) ?? new Map<unknown, number>()\n if (!positions.has(data)) {\n for (let i = 0; i < data.length; i++) if (!map.has(data[i])) map.set(data[i], i)\n positions.set(data, map)\n }\n find = (v) => map.get(v)\n } else {\n list = a.hits.keys\n if (!keyIndex || keyIndex.data !== data || keyIndex.rowKey !== rowKey) {\n // Diagnostics about the keys are the pipeline's (it resolves the same keys).\n const keyOf = resolveRowKeys(data, rowKey, NO_KEYS, NO_KEYS)\n const m = new Map<string, number>()\n for (let i = 0; i < data.length; i++) {\n // Repeated keys are already unique here: later rows carry `key#1`, `key#2`, … (row.key).\n const k = String(keyOf(data[i], i))\n if (!m.has(k)) m.set(k, i)\n }\n keyIndex = { data, rowKey, map: m }\n }\n const map = keyIndex.map\n find = (v) => map.get(String(v))\n }\n const out = new Uint32Array(list.length)\n let n = 0\n let unknown: Applied['unknown']\n for (const v of list) {\n const i = find(v)\n if (i !== undefined) out[n++] = i\n else if ((process.env.NODE_ENV !== \"production\") && 'keys' in a.hits) {\n unknown ??= { count: 0, returned: list.length, keys: [] }\n unknown.count++\n if (unknown.keys.length < SAMPLE) unknown.keys.push(v)\n }\n }\n if ((process.env.NODE_ENV !== \"production\")) a.unknown = unknown\n const indices = out.subarray(0, n)\n a.cache = { data, rowKey, indices }\n return indices\n }\n\n function step(input: Uint32Array, ctx: RowModelContext): Uint32Array {\n const a = active && results.get(active)\n // Nothing applied yet (the first search is in flight or failed): the rows stay unsearched.\n if (!a) return input\n const rowKey = (active as { getOptions(): { rowKey?: unknown } }).getOptions().rowKey\n const hits = indicesOf(a, ctx.data, rowKey)\n if (last && last.input === input && last.hits === hits) return last.output\n const take = new Uint8Array(ctx.data.length)\n for (let k = 0; k < input.length; k++) take[input[k] as number] = 1\n const out = new Uint32Array(Math.min(hits.length, input.length))\n let n = 0\n for (let k = 0; k < hits.length; k++) {\n const i = hits[k] as number\n if (take[i] === 1) {\n take[i] = 0\n out[n++] = i\n }\n }\n const output = out.subarray(0, n)\n last = { input, hits, output }\n return output\n }\n\n function run<R>(table: object, next: () => R): R {\n // `step` reads the table's applied searchFn result through `active`.\n const prev = active\n active = table\n try {\n return next()\n } finally {\n active = prev\n checks.get(table)?.()\n }\n }\n\n function session(host: SearchHost): SearchSession {\n const { ctx, write, applied, input, claim } = host\n const table = ctx.table as unknown as object\n let dead = false\n /** The in-flight search: its query, data, waiters and abort handle. */\n let attempt:\n | { query: string; data: readonly unknown[]; waiters: Waiter[]; ctl: AbortController }\n | undefined\n /** (query, data) last handed to searchFn: `check()` never retries it in a loop. */\n let tried: { query: string; data: readonly unknown[] } | undefined\n let scheduled = false\n /**\n * SSR (§7.2): the initial query whose result is `initialData` (no searchFn call for it). Without\n * initialData, a deferred table (`createDeferred`) waits for `startTable` to search it.\n */\n let seeded: string | undefined\n if (applied().trim() !== '') {\n const opts = ctx.table.getOptions()\n if (opts.initialData !== undefined && opts.data === undefined) seeded = applied()\n else requestsOf(table).defer('searchFn')\n }\n\n /** Ends the in-flight search without its result (its waiters: `'superseded'`). */\n const abort = () => {\n const a = attempt\n if (!a) return\n attempt = undefined\n a.ctl.abort()\n for (const w of a.waiters) w.resolve(SUPERSEDED)\n }\n\n const startSearch = (query: string, inp: string) => {\n const data = ctx.table.getData() as readonly unknown[]\n const waiters: Waiter[] = []\n const prev = attempt\n if (prev) {\n attempt = undefined\n // Same query (new data): its waiters follow the new request; else they are replaced.\n if (prev.query === query) waiters.push(...prev.waiters)\n else for (const w of prev.waiters) w.resolve(SUPERSEDED)\n prev.ctl.abort()\n }\n waiters.push(...claim(query))\n const a = { query, data, waiters, ctl: new AbortController() }\n attempt = a\n tried = { query, data }\n write(inp, applied(), 'searching')\n const c = {\n query: query.trim(),\n rows: data,\n columns: ctx.table.getOptions().columns ?? EMPTY,\n }\n requestsOf(table)\n .request('searchFn', (signal) => callSearch(fn, { ...c, signal }), {\n signal: a.ctl.signal,\n onSettle: (value, status) => settle(a, value, status),\n })\n // strict: the DatakableError went to the hub; the outcome was settled in onSettle.\n .catch(() => settle(a, undefined, 'error'))\n }\n\n const settle = (a: NonNullable<typeof attempt>, value: unknown, status: AsyncStatus) => {\n // Replaced, cancelled or destroyed: its waiters were resolved then.\n if (attempt !== a || dead) return\n attempt = undefined\n const hits = status === 'applied' && !(value instanceof SearchFailure) && toHits(value)\n if (hits) {\n results.set(table, { query: a.query, data: a.data, hits })\n write(input(), a.query, 'idle')\n for (const w of a.waiters) w.resolve(APPLIED)\n return\n }\n if (status === 'superseded' || status === 'aborted') {\n // An AbortError from searchFn (not ours): not an error (§6.3).\n write(input(), applied(), 'idle')\n for (const w of a.waiters) w.resolve(SUPERSEDED)\n return\n }\n const failure = value instanceof SearchFailure ? value : undefined\n const error =\n status === 'timeout'\n ? new DOMException('searchFn timed out.', 'TimeoutError')\n : failure?.error\n write(input(), applied(), 'error', error)\n for (const w of a.waiters) w.resolve(ERROR)\n // 'error' / 'timeout': already reported by the errorBoundary / asyncControl.\n if (status !== 'applied') return\n const hadResults = results.has(table)\n if (failure) {\n const name = errorName(failure.error)\n ctx.emit(\n 'DK_SEARCH_FAILED',\n { ...(name === undefined ? {} : { errorName: name }), hadResults },\n { cause: failure.error },\n )\n } else {\n ctx.emit('DK_SEARCH_FAILED', {\n problem: 'bad-result',\n received: describeValue(value),\n hadResults,\n })\n }\n }\n\n /** After a pipeline run: new data or an outside query without a result → search again. */\n checks.set(table, () => {\n if ((process.env.NODE_ENV !== \"production\")) {\n const u = results.get(table)\n if (u?.unknown && !dead) {\n ctx.emit('DK_SEARCH_UNKNOWN_KEYS', u.unknown)\n u.unknown = undefined\n }\n }\n if (scheduled || dead) return\n const query = applied()\n if (query.trim() === '') return\n const data = ctx.table.getData()\n const r = results.get(table)\n if (r && r.query === query && r.data === data) return\n if (tried && tried.query === query && tried.data === data) return\n if (seeded !== undefined) {\n const initial = seeded === query && getTableInternals(table).initialRows() === data\n seeded = undefined\n if (initial) {\n results.set(table, { query, data, hits: { rows: data } })\n return\n }\n }\n if (requestsOf(table).defer('searchFn')) return\n scheduled = true\n queueMicrotask(() => {\n scheduled = false\n const q = applied()\n const d = ctx.table.getData()\n if (dead || q.trim() === '' || (tried && tried.query === q && tried.data === d)) return\n const cur = results.get(table)\n if (cur && cur.query === q && cur.data === d) return\n startSearch(q, input())\n })\n })\n\n return {\n apply(query, inp) {\n const data = ctx.table.getData()\n const r = results.get(table)\n if (query.trim() === '' || (query === applied() && r?.query === query && r.data === data)) {\n abort()\n if (query.trim() === '') results.delete(table)\n return false\n }\n if (attempt && attempt.query === query && attempt.data === data) {\n attempt.waiters.push(...claim(query))\n write(inp, applied(), 'searching')\n return true\n }\n startSearch(query, inp)\n return true\n },\n busy: () => attempt !== undefined,\n abort,\n destroy() {\n dead = true\n abort()\n },\n }\n }\n\n return { step, run, session }\n}\n"],"mappings":"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","names":[]}
|