@datakable/core 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/{codes-BKdS9EiO.d.cts → codes-OsjAR3Wz.d.cts} +16 -4
  2. package/dist/{codes-BKdS9EiO.d.ts → codes-OsjAR3Wz.d.ts} +16 -4
  3. package/dist/column-filters.d.cts +2 -2
  4. package/dist/column-filters.d.ts +2 -2
  5. package/dist/diagnostics/build.cjs +6 -3
  6. package/dist/diagnostics/build.cjs.map +1 -1
  7. package/dist/diagnostics/build.js +4 -2
  8. package/dist/diagnostics/build.js.map +1 -1
  9. package/dist/diagnostics/hub.cjs +1 -1
  10. package/dist/diagnostics/hub.cjs.map +1 -1
  11. package/dist/diagnostics/hub.js +2 -2
  12. package/dist/diagnostics/hub.js.map +1 -1
  13. package/dist/{explain-DcUZfFh5.d.ts → explain-Dn99IQt0.d.ts} +1 -1
  14. package/dist/{explain-aagaYHcy.d.cts → explain-vkD2cewE.d.cts} +1 -1
  15. package/dist/feature/define.cjs +3 -1
  16. package/dist/feature/define.cjs.map +1 -1
  17. package/dist/feature/define.js +3 -1
  18. package/dist/feature/define.js.map +1 -1
  19. package/dist/feature/runtime.cjs.map +1 -1
  20. package/dist/feature/runtime.js.map +1 -1
  21. package/dist/features/pagination/codes.cjs +5 -3
  22. package/dist/features/pagination/codes.cjs.map +1 -1
  23. package/dist/features/pagination/codes.js +5 -3
  24. package/dist/features/pagination/codes.js.map +1 -1
  25. package/dist/features/pagination/pagination.cjs +3 -1
  26. package/dist/features/pagination/pagination.cjs.map +1 -1
  27. package/dist/features/pagination/pagination.js +3 -1
  28. package/dist/features/pagination/pagination.js.map +1 -1
  29. package/dist/features/search/search-index.cjs +38 -34
  30. package/dist/features/search/search-index.cjs.map +1 -1
  31. package/dist/features/search/search-index.js +39 -35
  32. package/dist/features/search/search-index.js.map +1 -1
  33. package/dist/features/search/search.cjs +42 -16
  34. package/dist/features/search/search.cjs.map +1 -1
  35. package/dist/features/search/search.js +42 -16
  36. package/dist/features/search/search.js.map +1 -1
  37. package/dist/features/sorting/sorting.cjs +3 -3
  38. package/dist/features/sorting/sorting.cjs.map +1 -1
  39. package/dist/features/sorting/sorting.js +3 -3
  40. package/dist/features/sorting/sorting.js.map +1 -1
  41. package/dist/index.d.cts +9 -7
  42. package/dist/index.d.ts +9 -7
  43. package/dist/intercept/cell-batch.cjs +3 -0
  44. package/dist/intercept/cell-batch.cjs.map +1 -1
  45. package/dist/intercept/cell-batch.js +3 -0
  46. package/dist/intercept/cell-batch.js.map +1 -1
  47. package/dist/pagination.d.cts +2 -2
  48. package/dist/pagination.d.ts +2 -2
  49. package/dist/pipeline/row.cjs +8 -7
  50. package/dist/pipeline/row.cjs.map +1 -1
  51. package/dist/pipeline/row.js +8 -7
  52. package/dist/pipeline/row.js.map +1 -1
  53. package/dist/schema.d.cts +2 -2
  54. package/dist/schema.d.ts +2 -2
  55. package/dist/{search-async-entry-BmQYT120.d.ts → search-async-entry-DGSw2pDL.d.ts} +2 -2
  56. package/dist/{search-async-entry-C3mXxLP8.d.cts → search-async-entry-v5JdEk5Y.d.cts} +2 -2
  57. package/dist/search-async.d.cts +3 -3
  58. package/dist/search-async.d.ts +3 -3
  59. package/dist/search.d.cts +4 -4
  60. package/dist/search.d.ts +4 -4
  61. package/dist/server.d.cts +3 -3
  62. package/dist/server.d.ts +3 -3
  63. package/dist/sorting.d.cts +6 -3
  64. package/dist/sorting.d.ts +6 -3
  65. package/dist/validity.d.cts +2 -2
  66. package/dist/validity.d.ts +2 -2
  67. package/dist/version.cjs +1 -1
  68. package/dist/version.cjs.map +1 -1
  69. package/dist/version.js +1 -1
  70. package/dist/version.js.map +1 -1
  71. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/pagination/codes.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from '../../diagnostics/catalog'\n\ndeclare module '../../diagnostics/catalog' {\n interface DiagnosticCatalog {\n DK_INVALID_PAGINATION: {\n /** The API method that received the value, or `'state'` for a slice written to the store. */\n source: 'setPageIndex' | 'setPageSize' | 'state'\n field: 'pageIndex' | 'pageSize'\n /** The value's type only (`describeValue`), plus the number itself when it is one. */\n received: string\n /** What was done instead. */\n fallback: string\n }\n DK_ROW_COUNT_MISSING: {\n /** `data.length`, used as the total instead. */\n rows: number\n }\n }\n}\n\nconst ACCEPTED = {\n pageIndex: 'an integer ≥ 0',\n pageSize: 'an integer > 0',\n} as const\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const paginationMessages: DiagnosticMessages<\n 'DK_INVALID_PAGINATION' | 'DK_ROW_COUNT_MISSING'\n> = {\n DK_INVALID_PAGINATION: {\n problem: (d) =>\n d.source === 'state'\n ? `the pagination state has ${d.field} = ${d.received}; it must be ${ACCEPTED[d.field]}`\n : `${d.source}() received ${d.received}; ${d.field} must be ${ACCEPTED[d.field]}`,\n impact: (d) => d.fallback,\n fix: (d) =>\n d.source === 'state'\n ? [\n `write pagination.${d.field} as ${ACCEPTED[d.field]} (check initialState, setState and ` +\n 'the controlled options.state)',\n d.field === 'pageIndex'\n ? 'use table.setPageIndex(i): it clamps to the existing pages'\n : 'use table.setPageSize(n): it also keeps the first visible row on screen',\n ]\n : [\n `pass ${ACCEPTED[d.field]} to ${d.source}()` +\n (d.received.startsWith('string') ? ' (convert input values with Number(...))' : ''),\n ],\n },\n DK_ROW_COUNT_MISSING: {\n problem: () =>\n 'pagination is in server mode, but there is no fetcher and no rowCount option: the ' +\n 'total number of rows is unknown',\n impact: (d) =>\n `the page count falls back to data.length (${d.rows}) as the total: one page, ` +\n 'canNextPage() is false',\n fix: () => [\n 'pass the total from your API: setOptions({ data: page.rows, rowCount: page.total })',\n \"or pass a fetcher that resolves { rows, rowCount }, or use pagination({ mode: 'client' }) \" +\n 'with all rows as data',\n ],\n },\n}\n\n/** Codes owned by the `pagination` feature (§6.5). */\nexport const paginationDiagnostics = [\n defineDiagnostic(\n {\n code: 'DK_INVALID_PAGINATION',\n level: 'warn',\n where: (d) =>\n d.source === 'state'\n ? { kind: 'option', path: `state.pagination.${d.field}` }\n : { kind: 'operation', op: `table.${d.source}` },\n },\n (process.env.NODE_ENV !== \"production\") && paginationMessages.DK_INVALID_PAGINATION,\n ),\n defineDiagnostic(\n {\n code: 'DK_ROW_COUNT_MISSING',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'rowCount' }),\n },\n (process.env.NODE_ENV !== \"production\") && paginationMessages.DK_ROW_COUNT_MISSING,\n ),\n]\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA0D;AAoB1D,IAAM,WAAW;AAAA,EACf,WAAW;AAAA,EACX,UAAU;AACZ;AAGO,IAAM,qBAET;AAAA,EACF,uBAAuB;AAAA,IACrB,SAAS,CAAC,MACR,EAAE,WAAW,UACT,4BAA4B,EAAE,KAAK,MAAM,EAAE,QAAQ,gBAAgB,SAAS,EAAE,KAAK,CAAC,KACpF,GAAG,EAAE,MAAM,eAAe,EAAE,QAAQ,KAAK,EAAE,KAAK,YAAY,SAAS,EAAE,KAAK,CAAC;AAAA,IACnF,QAAQ,CAAC,MAAM,EAAE;AAAA,IACjB,KAAK,CAAC,MACJ,EAAE,WAAW,UACT;AAAA,MACE,oBAAoB,EAAE,KAAK,OAAO,SAAS,EAAE,KAAK,CAAC;AAAA,MAEnD,EAAE,UAAU,cACR,+DACA;AAAA,IACN,IACA;AAAA,MACE,QAAQ,SAAS,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,QACrC,EAAE,SAAS,WAAW,QAAQ,IAAI,6CAA6C;AAAA,IACpF;AAAA,EACR;AAAA,EACA,sBAAsB;AAAA,IACpB,SAAS,MACP;AAAA,IAEF,QAAQ,CAAC,MACP,6CAA6C,EAAE,IAAI;AAAA,IAErD,KAAK,MAAM;AAAA,MACT;AAAA,MACA;AAAA,IAEF;AAAA,EACF;AACF;AAGO,IAAM,wBAAwB;AAAA,MACnC;AAAA,IACE;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,OAAO,CAAC,MACN,EAAE,WAAW,UACT,EAAE,MAAM,UAAU,MAAM,oBAAoB,EAAE,KAAK,GAAG,IACtD,EAAE,MAAM,aAAa,IAAI,SAAS,EAAE,MAAM,GAAG;AAAA,IACrD;AAAA,IACC,QAAQ,IAAI,aAAa,gBAAiB,mBAAmB;AAAA,EAChE;AAAA,MACA;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,mBAAmB;AAAA,EAChE;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/features/pagination/codes.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from '../../diagnostics/catalog'\n\ndeclare module '../../diagnostics/catalog' {\n interface DiagnosticCatalog {\n DK_INVALID_PAGINATION: {\n /**\n * The API method that received the value, `'state'` for a slice written to the store, or\n * `'option'` for `pagination({ pageSize })` (checked here in production, where the option\n * validator is off).\n */\n source: 'setPageIndex' | 'setPageSize' | 'state' | 'option'\n field: 'pageIndex' | 'pageSize'\n /** The value's type only (`describeValue`), plus the number itself when it is one. */\n received: string\n /** What was done instead. */\n fallback: string\n }\n DK_ROW_COUNT_MISSING: {\n /** `data.length`, used as the total instead. */\n rows: number\n }\n }\n}\n\nconst ACCEPTED = {\n pageIndex: 'an integer ≥ 0',\n pageSize: 'an integer > 0',\n} as const\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const paginationMessages: DiagnosticMessages<\n 'DK_INVALID_PAGINATION' | 'DK_ROW_COUNT_MISSING'\n> = {\n DK_INVALID_PAGINATION: {\n problem: (d) =>\n d.source === 'option'\n ? `pagination({ pageSize: ${d.received} }); pageSize must be ${ACCEPTED.pageSize}`\n : d.source === 'state'\n ? `the pagination state has ${d.field} = ${d.received}; it must be ${ACCEPTED[d.field]}`\n : `${d.source}() received ${d.received}; ${d.field} must be ${ACCEPTED[d.field]}`,\n impact: (d) => d.fallback,\n fix: (d) =>\n d.source === 'option'\n ? [\n `pass ${ACCEPTED.pageSize} to pagination({ pageSize }), or leave it out for the ` +\n 'default of 10',\n ]\n : d.source === 'state'\n ? [\n `write pagination.${d.field} as ${ACCEPTED[d.field]} (check initialState, setState and ` +\n 'the controlled options.state)',\n d.field === 'pageIndex'\n ? 'use table.setPageIndex(i): it clamps to the existing pages'\n : 'use table.setPageSize(n): it also keeps the first visible row on screen',\n ]\n : [\n `pass ${ACCEPTED[d.field]} to ${d.source}()` +\n (d.received.startsWith('string') ? ' (convert input values with Number(...))' : ''),\n ],\n },\n DK_ROW_COUNT_MISSING: {\n problem: () =>\n 'pagination is in server mode, but there is no fetcher and no rowCount option: the ' +\n 'total number of rows is unknown',\n impact: (d) =>\n `the page count falls back to data.length (${d.rows}) as the total: one page, ` +\n 'canNextPage() is false',\n fix: () => [\n 'pass the total from your API: setOptions({ data: page.rows, rowCount: page.total })',\n \"or pass a fetcher that resolves { rows, rowCount }, or use pagination({ mode: 'client' }) \" +\n 'with all rows as data',\n ],\n },\n}\n\n/** Codes owned by the `pagination` feature (§6.5). */\nexport const paginationDiagnostics = [\n defineDiagnostic(\n {\n code: 'DK_INVALID_PAGINATION',\n level: 'warn',\n where: (d) =>\n d.source === 'option'\n ? { kind: 'feature', name: 'pagination' }\n : d.source === 'state'\n ? { kind: 'option', path: `state.pagination.${d.field}` }\n : { kind: 'operation', op: `table.${d.source}` },\n },\n (process.env.NODE_ENV !== \"production\") && paginationMessages.DK_INVALID_PAGINATION,\n ),\n defineDiagnostic(\n {\n code: 'DK_ROW_COUNT_MISSING',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'rowCount' }),\n },\n (process.env.NODE_ENV !== \"production\") && paginationMessages.DK_ROW_COUNT_MISSING,\n ),\n]\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA0D;AAwB1D,IAAM,WAAW;AAAA,EACf,WAAW;AAAA,EACX,UAAU;AACZ;AAGO,IAAM,qBAET;AAAA,EACF,uBAAuB;AAAA,IACrB,SAAS,CAAC,MACR,EAAE,WAAW,WACT,0BAA0B,EAAE,QAAQ,yBAAyB,SAAS,QAAQ,KAC9E,EAAE,WAAW,UACX,4BAA4B,EAAE,KAAK,MAAM,EAAE,QAAQ,gBAAgB,SAAS,EAAE,KAAK,CAAC,KACpF,GAAG,EAAE,MAAM,eAAe,EAAE,QAAQ,KAAK,EAAE,KAAK,YAAY,SAAS,EAAE,KAAK,CAAC;AAAA,IACrF,QAAQ,CAAC,MAAM,EAAE;AAAA,IACjB,KAAK,CAAC,MACJ,EAAE,WAAW,WACT;AAAA,MACE,QAAQ,SAAS,QAAQ;AAAA,IAE3B,IACA,EAAE,WAAW,UACX;AAAA,MACE,oBAAoB,EAAE,KAAK,OAAO,SAAS,EAAE,KAAK,CAAC;AAAA,MAEnD,EAAE,UAAU,cACR,+DACA;AAAA,IACN,IACA;AAAA,MACE,QAAQ,SAAS,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,QACrC,EAAE,SAAS,WAAW,QAAQ,IAAI,6CAA6C;AAAA,IACpF;AAAA,EACV;AAAA,EACA,sBAAsB;AAAA,IACpB,SAAS,MACP;AAAA,IAEF,QAAQ,CAAC,MACP,6CAA6C,EAAE,IAAI;AAAA,IAErD,KAAK,MAAM;AAAA,MACT;AAAA,MACA;AAAA,IAEF;AAAA,EACF;AACF;AAGO,IAAM,wBAAwB;AAAA,MACnC;AAAA,IACE;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,OAAO,CAAC,MACN,EAAE,WAAW,WACT,EAAE,MAAM,WAAW,MAAM,aAAa,IACtC,EAAE,WAAW,UACX,EAAE,MAAM,UAAU,MAAM,oBAAoB,EAAE,KAAK,GAAG,IACtD,EAAE,MAAM,aAAa,IAAI,SAAS,EAAE,MAAM,GAAG;AAAA,IACvD;AAAA,IACC,QAAQ,IAAI,aAAa,gBAAiB,mBAAmB;AAAA,EAChE;AAAA,MACA;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,mBAAmB;AAAA,EAChE;AACF;","names":[]}
@@ -6,9 +6,11 @@ var ACCEPTED = {
6
6
  };
7
7
  var paginationMessages = {
8
8
  DK_INVALID_PAGINATION: {
9
- problem: (d) => d.source === "state" ? `the pagination state has ${d.field} = ${d.received}; it must be ${ACCEPTED[d.field]}` : `${d.source}() received ${d.received}; ${d.field} must be ${ACCEPTED[d.field]}`,
9
+ problem: (d) => d.source === "option" ? `pagination({ pageSize: ${d.received} }); pageSize must be ${ACCEPTED.pageSize}` : d.source === "state" ? `the pagination state has ${d.field} = ${d.received}; it must be ${ACCEPTED[d.field]}` : `${d.source}() received ${d.received}; ${d.field} must be ${ACCEPTED[d.field]}`,
10
10
  impact: (d) => d.fallback,
11
- fix: (d) => d.source === "state" ? [
11
+ fix: (d) => d.source === "option" ? [
12
+ `pass ${ACCEPTED.pageSize} to pagination({ pageSize }), or leave it out for the default of 10`
13
+ ] : d.source === "state" ? [
12
14
  `write pagination.${d.field} as ${ACCEPTED[d.field]} (check initialState, setState and the controlled options.state)`,
13
15
  d.field === "pageIndex" ? "use table.setPageIndex(i): it clamps to the existing pages" : "use table.setPageSize(n): it also keeps the first visible row on screen"
14
16
  ] : [
@@ -29,7 +31,7 @@ var paginationDiagnostics = [
29
31
  {
30
32
  code: "DK_INVALID_PAGINATION",
31
33
  level: "warn",
32
- where: (d) => d.source === "state" ? { kind: "option", path: `state.pagination.${d.field}` } : { kind: "operation", op: `table.${d.source}` }
34
+ where: (d) => d.source === "option" ? { kind: "feature", name: "pagination" } : d.source === "state" ? { kind: "option", path: `state.pagination.${d.field}` } : { kind: "operation", op: `table.${d.source}` }
33
35
  },
34
36
  process.env.NODE_ENV !== "production" && paginationMessages.DK_INVALID_PAGINATION
35
37
  ),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/pagination/codes.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from '../../diagnostics/catalog'\n\ndeclare module '../../diagnostics/catalog' {\n interface DiagnosticCatalog {\n DK_INVALID_PAGINATION: {\n /** The API method that received the value, or `'state'` for a slice written to the store. */\n source: 'setPageIndex' | 'setPageSize' | 'state'\n field: 'pageIndex' | 'pageSize'\n /** The value's type only (`describeValue`), plus the number itself when it is one. */\n received: string\n /** What was done instead. */\n fallback: string\n }\n DK_ROW_COUNT_MISSING: {\n /** `data.length`, used as the total instead. */\n rows: number\n }\n }\n}\n\nconst ACCEPTED = {\n pageIndex: 'an integer ≥ 0',\n pageSize: 'an integer > 0',\n} as const\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const paginationMessages: DiagnosticMessages<\n 'DK_INVALID_PAGINATION' | 'DK_ROW_COUNT_MISSING'\n> = {\n DK_INVALID_PAGINATION: {\n problem: (d) =>\n d.source === 'state'\n ? `the pagination state has ${d.field} = ${d.received}; it must be ${ACCEPTED[d.field]}`\n : `${d.source}() received ${d.received}; ${d.field} must be ${ACCEPTED[d.field]}`,\n impact: (d) => d.fallback,\n fix: (d) =>\n d.source === 'state'\n ? [\n `write pagination.${d.field} as ${ACCEPTED[d.field]} (check initialState, setState and ` +\n 'the controlled options.state)',\n d.field === 'pageIndex'\n ? 'use table.setPageIndex(i): it clamps to the existing pages'\n : 'use table.setPageSize(n): it also keeps the first visible row on screen',\n ]\n : [\n `pass ${ACCEPTED[d.field]} to ${d.source}()` +\n (d.received.startsWith('string') ? ' (convert input values with Number(...))' : ''),\n ],\n },\n DK_ROW_COUNT_MISSING: {\n problem: () =>\n 'pagination is in server mode, but there is no fetcher and no rowCount option: the ' +\n 'total number of rows is unknown',\n impact: (d) =>\n `the page count falls back to data.length (${d.rows}) as the total: one page, ` +\n 'canNextPage() is false',\n fix: () => [\n 'pass the total from your API: setOptions({ data: page.rows, rowCount: page.total })',\n \"or pass a fetcher that resolves { rows, rowCount }, or use pagination({ mode: 'client' }) \" +\n 'with all rows as data',\n ],\n },\n}\n\n/** Codes owned by the `pagination` feature (§6.5). */\nexport const paginationDiagnostics = [\n defineDiagnostic(\n {\n code: 'DK_INVALID_PAGINATION',\n level: 'warn',\n where: (d) =>\n d.source === 'state'\n ? { kind: 'option', path: `state.pagination.${d.field}` }\n : { kind: 'operation', op: `table.${d.source}` },\n },\n (process.env.NODE_ENV !== \"production\") && paginationMessages.DK_INVALID_PAGINATION,\n ),\n defineDiagnostic(\n {\n code: 'DK_ROW_COUNT_MISSING',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'rowCount' }),\n },\n (process.env.NODE_ENV !== \"production\") && paginationMessages.DK_ROW_COUNT_MISSING,\n ),\n]\n"],"mappings":";AAAA,SAAkC,wBAAwB;AAoB1D,IAAM,WAAW;AAAA,EACf,WAAW;AAAA,EACX,UAAU;AACZ;AAGO,IAAM,qBAET;AAAA,EACF,uBAAuB;AAAA,IACrB,SAAS,CAAC,MACR,EAAE,WAAW,UACT,4BAA4B,EAAE,KAAK,MAAM,EAAE,QAAQ,gBAAgB,SAAS,EAAE,KAAK,CAAC,KACpF,GAAG,EAAE,MAAM,eAAe,EAAE,QAAQ,KAAK,EAAE,KAAK,YAAY,SAAS,EAAE,KAAK,CAAC;AAAA,IACnF,QAAQ,CAAC,MAAM,EAAE;AAAA,IACjB,KAAK,CAAC,MACJ,EAAE,WAAW,UACT;AAAA,MACE,oBAAoB,EAAE,KAAK,OAAO,SAAS,EAAE,KAAK,CAAC;AAAA,MAEnD,EAAE,UAAU,cACR,+DACA;AAAA,IACN,IACA;AAAA,MACE,QAAQ,SAAS,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,QACrC,EAAE,SAAS,WAAW,QAAQ,IAAI,6CAA6C;AAAA,IACpF;AAAA,EACR;AAAA,EACA,sBAAsB;AAAA,IACpB,SAAS,MACP;AAAA,IAEF,QAAQ,CAAC,MACP,6CAA6C,EAAE,IAAI;AAAA,IAErD,KAAK,MAAM;AAAA,MACT;AAAA,MACA;AAAA,IAEF;AAAA,EACF;AACF;AAGO,IAAM,wBAAwB;AAAA,EACnC;AAAA,IACE;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,OAAO,CAAC,MACN,EAAE,WAAW,UACT,EAAE,MAAM,UAAU,MAAM,oBAAoB,EAAE,KAAK,GAAG,IACtD,EAAE,MAAM,aAAa,IAAI,SAAS,EAAE,MAAM,GAAG;AAAA,IACrD;AAAA,IACC,QAAQ,IAAI,aAAa,gBAAiB,mBAAmB;AAAA,EAChE;AAAA,EACA;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,mBAAmB;AAAA,EAChE;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/features/pagination/codes.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from '../../diagnostics/catalog'\n\ndeclare module '../../diagnostics/catalog' {\n interface DiagnosticCatalog {\n DK_INVALID_PAGINATION: {\n /**\n * The API method that received the value, `'state'` for a slice written to the store, or\n * `'option'` for `pagination({ pageSize })` (checked here in production, where the option\n * validator is off).\n */\n source: 'setPageIndex' | 'setPageSize' | 'state' | 'option'\n field: 'pageIndex' | 'pageSize'\n /** The value's type only (`describeValue`), plus the number itself when it is one. */\n received: string\n /** What was done instead. */\n fallback: string\n }\n DK_ROW_COUNT_MISSING: {\n /** `data.length`, used as the total instead. */\n rows: number\n }\n }\n}\n\nconst ACCEPTED = {\n pageIndex: 'an integer ≥ 0',\n pageSize: 'an integer > 0',\n} as const\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const paginationMessages: DiagnosticMessages<\n 'DK_INVALID_PAGINATION' | 'DK_ROW_COUNT_MISSING'\n> = {\n DK_INVALID_PAGINATION: {\n problem: (d) =>\n d.source === 'option'\n ? `pagination({ pageSize: ${d.received} }); pageSize must be ${ACCEPTED.pageSize}`\n : d.source === 'state'\n ? `the pagination state has ${d.field} = ${d.received}; it must be ${ACCEPTED[d.field]}`\n : `${d.source}() received ${d.received}; ${d.field} must be ${ACCEPTED[d.field]}`,\n impact: (d) => d.fallback,\n fix: (d) =>\n d.source === 'option'\n ? [\n `pass ${ACCEPTED.pageSize} to pagination({ pageSize }), or leave it out for the ` +\n 'default of 10',\n ]\n : d.source === 'state'\n ? [\n `write pagination.${d.field} as ${ACCEPTED[d.field]} (check initialState, setState and ` +\n 'the controlled options.state)',\n d.field === 'pageIndex'\n ? 'use table.setPageIndex(i): it clamps to the existing pages'\n : 'use table.setPageSize(n): it also keeps the first visible row on screen',\n ]\n : [\n `pass ${ACCEPTED[d.field]} to ${d.source}()` +\n (d.received.startsWith('string') ? ' (convert input values with Number(...))' : ''),\n ],\n },\n DK_ROW_COUNT_MISSING: {\n problem: () =>\n 'pagination is in server mode, but there is no fetcher and no rowCount option: the ' +\n 'total number of rows is unknown',\n impact: (d) =>\n `the page count falls back to data.length (${d.rows}) as the total: one page, ` +\n 'canNextPage() is false',\n fix: () => [\n 'pass the total from your API: setOptions({ data: page.rows, rowCount: page.total })',\n \"or pass a fetcher that resolves { rows, rowCount }, or use pagination({ mode: 'client' }) \" +\n 'with all rows as data',\n ],\n },\n}\n\n/** Codes owned by the `pagination` feature (§6.5). */\nexport const paginationDiagnostics = [\n defineDiagnostic(\n {\n code: 'DK_INVALID_PAGINATION',\n level: 'warn',\n where: (d) =>\n d.source === 'option'\n ? { kind: 'feature', name: 'pagination' }\n : d.source === 'state'\n ? { kind: 'option', path: `state.pagination.${d.field}` }\n : { kind: 'operation', op: `table.${d.source}` },\n },\n (process.env.NODE_ENV !== \"production\") && paginationMessages.DK_INVALID_PAGINATION,\n ),\n defineDiagnostic(\n {\n code: 'DK_ROW_COUNT_MISSING',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'rowCount' }),\n },\n (process.env.NODE_ENV !== \"production\") && paginationMessages.DK_ROW_COUNT_MISSING,\n ),\n]\n"],"mappings":";AAAA,SAAkC,wBAAwB;AAwB1D,IAAM,WAAW;AAAA,EACf,WAAW;AAAA,EACX,UAAU;AACZ;AAGO,IAAM,qBAET;AAAA,EACF,uBAAuB;AAAA,IACrB,SAAS,CAAC,MACR,EAAE,WAAW,WACT,0BAA0B,EAAE,QAAQ,yBAAyB,SAAS,QAAQ,KAC9E,EAAE,WAAW,UACX,4BAA4B,EAAE,KAAK,MAAM,EAAE,QAAQ,gBAAgB,SAAS,EAAE,KAAK,CAAC,KACpF,GAAG,EAAE,MAAM,eAAe,EAAE,QAAQ,KAAK,EAAE,KAAK,YAAY,SAAS,EAAE,KAAK,CAAC;AAAA,IACrF,QAAQ,CAAC,MAAM,EAAE;AAAA,IACjB,KAAK,CAAC,MACJ,EAAE,WAAW,WACT;AAAA,MACE,QAAQ,SAAS,QAAQ;AAAA,IAE3B,IACA,EAAE,WAAW,UACX;AAAA,MACE,oBAAoB,EAAE,KAAK,OAAO,SAAS,EAAE,KAAK,CAAC;AAAA,MAEnD,EAAE,UAAU,cACR,+DACA;AAAA,IACN,IACA;AAAA,MACE,QAAQ,SAAS,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,QACrC,EAAE,SAAS,WAAW,QAAQ,IAAI,6CAA6C;AAAA,IACpF;AAAA,EACV;AAAA,EACA,sBAAsB;AAAA,IACpB,SAAS,MACP;AAAA,IAEF,QAAQ,CAAC,MACP,6CAA6C,EAAE,IAAI;AAAA,IAErD,KAAK,MAAM;AAAA,MACT;AAAA,MACA;AAAA,IAEF;AAAA,EACF;AACF;AAGO,IAAM,wBAAwB;AAAA,EACnC;AAAA,IACE;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,OAAO,CAAC,MACN,EAAE,WAAW,WACT,EAAE,MAAM,WAAW,MAAM,aAAa,IACtC,EAAE,WAAW,UACX,EAAE,MAAM,UAAU,MAAM,oBAAoB,EAAE,KAAK,GAAG,IACtD,EAAE,MAAM,aAAa,IAAI,SAAS,EAAE,MAAM,GAAG;AAAA,IACvD;AAAA,IACC,QAAQ,IAAI,aAAa,gBAAiB,mBAAmB;AAAA,EAChE;AAAA,EACA;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,mBAAmB;AAAA,EAChE;AACF;","names":[]}
@@ -54,7 +54,8 @@ function effectiveIndex(pageIndex, pageSize, rowCount) {
54
54
  return pageIndex > last ? last : pageIndex;
55
55
  }
56
56
  var pagination = (opts = {}) => {
57
- const initialSize = opts.pageSize ?? DEFAULT_PAGE_SIZE;
57
+ const badSize = opts.pageSize !== void 0 && !isSize(opts.pageSize);
58
+ const initialSize = badSize ? DEFAULT_PAGE_SIZE : opts.pageSize ?? DEFAULT_PAGE_SIZE;
58
59
  const autoReset = opts.autoResetPageIndex !== false;
59
60
  const mode = opts.mode ?? "client";
60
61
  const hosts = /* @__PURE__ */ new WeakMap();
@@ -148,6 +149,7 @@ var pagination = (opts = {}) => {
148
149
  }
149
150
  }
150
151
  });
152
+ if (badSize) report("option", "pageSize", opts.pageSize, `pageSize ${initialSize} was used`);
151
153
  const fixed = sanitize(ctx.getState(), void 0);
152
154
  if (fixed) ctx.setState(fixed);
153
155
  table.subscribe(() => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/pagination/pagination.ts"],"sourcesContent":["import { defineOptions } from '../../catalog/options'\nimport { keepDiagnostics } from '../../diagnostics/catalog'\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 const initialSize = 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, 'the pagination state was reset to its initial value')\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, 'pageIndex was replaced by 0')\n fixed = { ...(s as PaginationState), pageIndex: 0 }\n }\n if (!isSize(pageSize)) {\n report('state', 'pageSize', pageSize, `pageSize was replaced by ${fallbackSize}`)\n fixed = { ...(fixed ?? (s as PaginationState)), pageSize: fallbackSize }\n }\n return fixed\n }\n\n function report(\n source: 'setPageIndex' | 'setPageSize' | 'state',\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. An unknown count (a server count not\n * fetched yet) clamps nothing.\n */\n function current() {\n const s = ctx.getState()\n const count = rowCount()\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 // 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\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(() => {\n const { s, index } = current()\n if (index !== s.pageIndex) ctx.setState({ ...s, pageIndex: index })\n })\n\n return {\n setPageIndex(index) {\n if (!Number.isInteger(index)) {\n report('setPageIndex', 'pageIndex', index, 'the call was ignored')\n return\n }\n goTo(index)\n },\n setPageSize(size) {\n if (!isSize(size)) {\n report('setPageSize', 'pageSize', size, 'the call was 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","fixed"]}
1
+ {"version":3,"sources":["../../../src/features/pagination/pagination.ts"],"sourcesContent":["import { defineOptions } from '../../catalog/options'\nimport { keepDiagnostics } from '../../diagnostics/catalog'\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, 'the pagination state was reset to its initial value')\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, 'pageIndex was replaced by 0')\n fixed = { ...(s as PaginationState), pageIndex: 0 }\n }\n if (!isSize(pageSize)) {\n report('state', 'pageSize', pageSize, `pageSize was replaced by ${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. An unknown count (a server count not\n * fetched yet) clamps nothing.\n */\n function current() {\n const s = ctx.getState()\n const count = rowCount()\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\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(() => {\n const { s, index } = current()\n if (index !== s.pageIndex) ctx.setState({ ...s, pageIndex: index })\n })\n\n return {\n setPageIndex(index) {\n if (!Number.isInteger(index)) {\n report('setPageIndex', 'pageIndex', index, 'the call was ignored')\n return\n }\n goTo(index)\n },\n setPageSize(size) {\n if (!isSize(size)) {\n report('setPageSize', 'pageSize', size, 'the call was 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","fixed"]}
@@ -30,7 +30,8 @@ function effectiveIndex(pageIndex, pageSize, rowCount) {
30
30
  return pageIndex > last ? last : pageIndex;
31
31
  }
32
32
  var pagination = (opts = {}) => {
33
- const initialSize = opts.pageSize ?? DEFAULT_PAGE_SIZE;
33
+ const badSize = opts.pageSize !== void 0 && !isSize(opts.pageSize);
34
+ const initialSize = badSize ? DEFAULT_PAGE_SIZE : opts.pageSize ?? DEFAULT_PAGE_SIZE;
34
35
  const autoReset = opts.autoResetPageIndex !== false;
35
36
  const mode = opts.mode ?? "client";
36
37
  const hosts = /* @__PURE__ */ new WeakMap();
@@ -124,6 +125,7 @@ var pagination = (opts = {}) => {
124
125
  }
125
126
  }
126
127
  });
128
+ if (badSize) report("option", "pageSize", opts.pageSize, `pageSize ${initialSize} was used`);
127
129
  const fixed = sanitize(ctx.getState(), void 0);
128
130
  if (fixed) ctx.setState(fixed);
129
131
  table.subscribe(() => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/pagination/pagination.ts"],"sourcesContent":["import { defineOptions } from '../../catalog/options'\nimport { keepDiagnostics } from '../../diagnostics/catalog'\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 const initialSize = 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, 'the pagination state was reset to its initial value')\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, 'pageIndex was replaced by 0')\n fixed = { ...(s as PaginationState), pageIndex: 0 }\n }\n if (!isSize(pageSize)) {\n report('state', 'pageSize', pageSize, `pageSize was replaced by ${fallbackSize}`)\n fixed = { ...(fixed ?? (s as PaginationState)), pageSize: fallbackSize }\n }\n return fixed\n }\n\n function report(\n source: 'setPageIndex' | 'setPageSize' | 'state',\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. An unknown count (a server count not\n * fetched yet) clamps nothing.\n */\n function current() {\n const s = ctx.getState()\n const count = rowCount()\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 // 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\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(() => {\n const { s, index } = current()\n if (index !== s.pageIndex) ctx.setState({ ...s, pageIndex: index })\n })\n\n return {\n setPageIndex(index) {\n if (!Number.isInteger(index)) {\n report('setPageIndex', 'pageIndex', index, 'the call was ignored')\n return\n }\n goTo(index)\n },\n setPageSize(size) {\n if (!isSize(size)) {\n report('setPageSize', 'pageSize', size, 'the call was 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":["fixed"]}
1
+ {"version":3,"sources":["../../../src/features/pagination/pagination.ts"],"sourcesContent":["import { defineOptions } from '../../catalog/options'\nimport { keepDiagnostics } from '../../diagnostics/catalog'\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, 'the pagination state was reset to its initial value')\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, 'pageIndex was replaced by 0')\n fixed = { ...(s as PaginationState), pageIndex: 0 }\n }\n if (!isSize(pageSize)) {\n report('state', 'pageSize', pageSize, `pageSize was replaced by ${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. An unknown count (a server count not\n * fetched yet) clamps nothing.\n */\n function current() {\n const s = ctx.getState()\n const count = rowCount()\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\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(() => {\n const { s, index } = current()\n if (index !== s.pageIndex) ctx.setState({ ...s, pageIndex: index })\n })\n\n return {\n setPageIndex(index) {\n if (!Number.isInteger(index)) {\n report('setPageIndex', 'pageIndex', index, 'the call was ignored')\n return\n }\n goTo(index)\n },\n setPageSize(size) {\n if (!isSize(size)) {\n report('setPageSize', 'pageSize', size, 'the call was 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":["fixed"]}
@@ -30,14 +30,16 @@ var import_columns = require("../../pipeline/columns.cjs");
30
30
  var import_row = require("../../pipeline/row.cjs");
31
31
  var import_capability = require("../../versioning/capability.cjs");
32
32
  var CHUNK = 4096;
33
- var hasExSearch = () => typeof import_ex_search.ExSearch === "function" && typeof import_ex_search.ExSearch.prototype.search === "function" && typeof import_ex_search.ExSearch.prototype.setData === "function";
33
+ var hasExSearch = () => [import_ex_search.ExSearch.prototype.search, import_ex_search.ExSearch.prototype.setData, import_ex_search.Scorer.fuzzy].every(
34
+ (f) => typeof f === "function"
35
+ );
34
36
  function probeExSearch(emit) {
35
37
  return process.env.NODE_ENV !== "production" ? (0, import_capability.detectCapability)(
36
38
  hasExSearch,
37
39
  {
38
40
  feature: "search",
39
41
  dependency: "ex-search",
40
- capability: "ExSearch",
42
+ capability: "ExSearch, Scorer.fuzzy",
41
43
  supported: "^1.1.1",
42
44
  fallback: "case-insensitive exact / prefix / substring matching (no fuzzy matching)"
43
45
  },
@@ -73,12 +75,9 @@ function* buildIndex(ctx, config, useExSearch) {
73
75
  yield;
74
76
  }
75
77
  if (keys.length === 0) return null;
76
- const fold = !config.caseSensitive;
77
- const at = (k, r) => {
78
- const v = reads[k](r);
79
- return fold ? v.toLowerCase() : v;
80
- };
81
- return useExSearch ? exSearchIndex(ctx.data.length, keys, at, config) : containsIndex(keys, at, config);
78
+ const fold = (v) => config.caseSensitive ? v : v.toLowerCase();
79
+ const at = (k, r) => fold(reads[k](r));
80
+ return useExSearch ? exSearchIndex(ctx.data.length, keys, at, fold, config) : containsIndex(keys, at, fold, config);
82
81
  }
83
82
  function* chunked(rows, matchChunk) {
84
83
  if (rows.length <= CHUNK) return rankHits(matchChunk(rows));
@@ -100,12 +99,12 @@ function projection(size, keys, at) {
100
99
  return o;
101
100
  };
102
101
  const items = new Proxy(target, {
103
- get(t, p, receiver) {
102
+ get(t, p) {
104
103
  if (p === "length") return length();
105
104
  const j = (0, import_row.arrayIndex)(p, length());
106
- return j >= 0 ? item(j) : Reflect.get(t, p, receiver);
105
+ return j >= 0 ? item(j) : t[p];
107
106
  },
108
- has: (t, p) => (0, import_row.arrayIndex)(p, length()) >= 0 || Reflect.has(t, p)
107
+ has: (t, p) => (0, import_row.arrayIndex)(p, length()) >= 0 || p in t
109
108
  });
110
109
  return {
111
110
  items,
@@ -115,22 +114,39 @@ function projection(size, keys, at) {
115
114
  }
116
115
  };
117
116
  }
118
- function exSearchIndex(size, keys, at, config) {
117
+ function exSearchIndex(size, keys, at, fold, config) {
119
118
  const engine = new import_ex_search.ExSearch({
120
- keys: keys.map((k) => ({ name: k.name, weight: k.weight })),
121
- threshold: config.threshold,
122
- fuzzyMaxDistance: config.fuzzyMaxDistance,
119
+ keys,
120
+ ...config,
123
121
  caseSensitive: true,
124
122
  // DataKable re-ranks with exact scores and an index tie-break; tim keeps ex-search's own pass
125
123
  // stable and avoids radix's score quantisation.
126
124
  sortAlgorithm: "tim"
127
125
  });
128
- const view = projection(size, keys, at);
126
+ const max = config.fuzzyMaxDistance;
127
+ let q = "";
128
+ const near = (k, r) => {
129
+ const v = at(k, r);
130
+ if (v.includes(q) || /\s/.test(q)) return v;
131
+ let best = "";
132
+ let top = 0;
133
+ const words = v.split(/\s+/);
134
+ for (const w of words[1] ? [v, ...words] : words) {
135
+ const s = Math.abs(w.length - q.length) > max ? 0 : import_ex_search.Scorer.fuzzy(w, q, max, true);
136
+ if (s > top) {
137
+ best = w;
138
+ top = s;
139
+ }
140
+ }
141
+ return best;
142
+ };
143
+ const view = projection(size, keys, near);
129
144
  engine.setData(view.items);
130
145
  return {
131
146
  match(rows, query) {
132
- const q = config.caseSensitive ? query : query.toLowerCase();
147
+ const folded = fold(query);
133
148
  return chunked(rows, (chunk) => {
149
+ q = folded;
134
150
  view.show(chunk);
135
151
  try {
136
152
  return engine.search(q);
@@ -141,10 +157,10 @@ function exSearchIndex(size, keys, at, config) {
141
157
  }
142
158
  };
143
159
  }
144
- function containsIndex(keys, at, config) {
160
+ function containsIndex(keys, at, fold, config) {
145
161
  return {
146
162
  match(rows, query) {
147
- const q = config.caseSensitive ? query : query.toLowerCase();
163
+ const q = fold(query);
148
164
  return chunked(rows, (chunk) => {
149
165
  const hits = [];
150
166
  for (let j = 0; j < chunk.length; j++) {
@@ -164,22 +180,10 @@ function containsIndex(keys, at, config) {
164
180
  };
165
181
  }
166
182
  function rankHits(hits) {
167
- const n = hits.length;
168
- const index = new Uint32Array(n);
169
- const score = new Float64Array(n);
170
- const order = new Uint32Array(n);
171
- for (let k = 0; k < n; k++) {
172
- const h = hits[k];
173
- index[k] = h.i;
174
- score[k] = h.score;
175
- order[k] = k;
176
- }
177
- order.sort(
178
- (a, b) => score[b] - score[a] || index[a] - index[b]
183
+ return Uint32Array.from(
184
+ hits.sort((a, b) => b.score - a.score || a.i - b.i),
185
+ (h) => h.i
179
186
  );
180
- const out = new Uint32Array(n);
181
- for (let k = 0; k < n; k++) out[k] = index[order[k]];
182
- return out;
183
187
  }
184
188
  // Annotate the CommonJS export names for ESM import in node:
185
189
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/features/search/search-index.ts"],"sourcesContent":["import { ExSearch } from 'ex-search'\nimport type { Emit } from '../../diagnostics/types'\nimport type { RowModelContext } from '../../feature/types'\nimport { resolveColumns } from '../../pipeline/columns'\nimport { arrayIndex } from '../../pipeline/row'\nimport { detectCapability, hasCapability } from '../../versioning/capability'\n\n/** Matcher settings (the client options of `search()`, defaults applied). */\nexport interface MatchConfig {\n readonly threshold: number\n readonly fuzzyMaxDistance: number\n readonly caseSensitive: boolean\n}\n\n/**\n * One projected row as ex-search reads it: `i` is the row index (results map back by it, never by\n * identity: ex-search returns copies), `c0`, `c1`, … the formatted values of the searchable\n * columns. Flat keys also sidestep ex-search's top-level-key limitation for nested paths. Items\n * are built per chunk while matching and dropped after it (§9.3 memory): none is kept.\n */\nexport type Item = Record<string, string | number> & { readonly i: number }\n\n/** A match: the row index and its relevance (0–1, weight applied). */\nexport interface Hit {\n readonly i: number\n readonly score: number\n}\n\n/** The matcher of one (data, columns) pair: references to column values, no per-row copy. */\nexport interface SearchIndex {\n /**\n * A time-sliced job (§9.1) matching `query` (trimmed, non-blank) against the rows `rows` (row\n * indices), in chunks: the hits in relevance order ({@link rankHits}).\n */\n match(rows: Uint32Array, query: string): Generator<undefined, Uint32Array, undefined>\n}\n\n/** Rows per chunk between two `yield`s of the matching job. */\nconst CHUNK = 4096\n\ninterface Key {\n readonly name: string\n readonly weight: number\n}\n\n/** The formatted value of row `r` of one searchable column. */\ntype Read = (r: number) => string\n\nconst hasExSearch = () =>\n typeof ExSearch === 'function' &&\n typeof ExSearch.prototype.search === 'function' &&\n typeof ExSearch.prototype.setData === 'function'\n\n/** Feature-detects ex-search once (§8.5); `false` → the contains fallback. */\nexport function probeExSearch(emit: Emit): boolean {\n // DK_DEP_INCOMPATIBLE is a development check (§8.5); the fallback is not.\n return (process.env.NODE_ENV !== \"production\")\n ? detectCapability(\n hasExSearch,\n {\n feature: 'search',\n dependency: 'ex-search',\n capability: 'ExSearch',\n supported: '^1.1.1',\n fallback: 'case-insensitive exact / prefix / substring matching (no fuzzy matching)',\n },\n emit,\n )\n : hasCapability(hasExSearch)\n}\n\n/**\n * A numeric column's formatted value without the cached strings: `String(v)` is what the default\n * formatter shows for a number. Only when no value is `NaN` — a `NaN` cell is `null` / `undefined`\n * (shown as `''`) or a real `NaN` (shown as `'NaN'`), which only the formatted cache tells apart.\n */\nfunction numberReader(values: Float64Array): Read | undefined {\n for (let r = 0; r < values.length; r++) if (Number.isNaN(values[r])) return undefined\n return (r) => String(values[r])\n}\n\n/**\n * Builds the matcher over the formatted values of the searchable columns (what the user sees), once\n * per data + columns, as a time-sliced job (§9.1): one column read per slice. Searchable columns are\n * those with a `search` weight > 0; when no column declares `search`, every column is searchable at\n * weight 1. `null` when nothing is searchable. The values are read from the column cache, never\n * copied (§9.3 memory): a numeric column without `format` is stringified while matching, and the\n * case is folded per chunk while matching.\n */\nexport function* buildIndex(\n ctx: RowModelContext,\n config: MatchConfig,\n useExSearch: boolean,\n): Generator<undefined, SearchIndex | null, undefined> {\n const cols = resolveColumns(ctx.columns)\n const declared = cols.list.some((c) => (c.def as { search?: unknown }).search !== undefined)\n const keys: Key[] = []\n const reads: Read[] = []\n for (const col of cols.list) {\n if (cols.byId.get(col.id) !== col) continue\n const w = (col.def as { search?: unknown }).search\n const weight = declared ? (typeof w === 'number' && w > 0 ? w : 0) : 1\n if (weight === 0) continue\n let read: Read | undefined\n if (!col.format) {\n const values = ctx.getColumnValues(col.id)\n if (values instanceof Float64Array) read = numberReader(values)\n }\n if (!read) {\n const formatted = ctx.getFormattedValues(col.id)\n if (!formatted) continue\n read = (r) => formatted[r] ?? ''\n }\n keys.push({ name: `c${keys.length}`, weight })\n reads.push(read)\n yield\n }\n if (keys.length === 0) return null\n const fold = !config.caseSensitive\n const at = (k: number, r: number) => {\n const v = (reads[k] as Read)(r)\n return fold ? v.toLowerCase() : v\n }\n return useExSearch\n ? exSearchIndex(ctx.data.length, keys, at, config)\n : containsIndex(keys, at, config)\n}\n\n/** Matches `rows` chunk by chunk (`score` decides alone, so chunks match like the whole). */\nfunction* chunked(\n rows: Uint32Array,\n matchChunk: (chunk: Uint32Array) => readonly Hit[],\n): Generator<undefined, Uint32Array, undefined> {\n if (rows.length <= CHUNK) return rankHits(matchChunk(rows))\n const hits: Hit[] = []\n for (let k = 0; k < rows.length; k += CHUNK) {\n for (const h of matchChunk(rows.subarray(k, k + CHUNK))) hits.push(h)\n yield\n }\n return rankHits(hits)\n}\n\n/**\n * The projection as ex-search reads it: an array (iteration, `length`, index reads) of `{ i, c0, … }`\n * items, each built when read and not kept. It shows every row of `data` (`size`), or one chunk of\n * rows while that chunk is matched; ex-search holds this one view (`setData` once).\n */\nfunction projection(size: number, keys: readonly Key[], at: (k: number, r: number) => string) {\n let rows: Uint32Array | undefined\n // Never written, and its own `length` stays 0: `length` is read through the trap below (setting\n // a large `length` would allocate a backing store).\n const target: Item[] = []\n const length = () => (rows ? rows.length : size)\n const item = (j: number): Item => {\n const r = rows ? (rows[j] as number) : j\n const o = { i: r } as Record<string, string | number>\n for (let k = 0; k < keys.length; k++) o[(keys[k] as Key).name] = at(k, r)\n return o as Item\n }\n const items = new Proxy(target, {\n get(t, p, receiver) {\n if (p === 'length') return length()\n const j = arrayIndex(p, length())\n return j >= 0 ? item(j) : Reflect.get(t, p, receiver)\n },\n has: (t, p) => arrayIndex(p, length()) >= 0 || Reflect.has(t, p),\n })\n return {\n items,\n /** Shows `chunk` (row indices), or every row again (`undefined`). */\n show(chunk: Uint32Array | undefined) {\n rows = chunk\n },\n }\n}\n\nfunction exSearchIndex(\n size: number,\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n config: MatchConfig,\n): SearchIndex {\n // Values are folded per chunk when not case-sensitive (buildIndex), and so is the query below.\n const engine = new ExSearch<Item>({\n keys: keys.map((k) => ({ name: k.name, weight: k.weight })),\n threshold: config.threshold,\n fuzzyMaxDistance: config.fuzzyMaxDistance,\n caseSensitive: true,\n // DataKable re-ranks with exact scores and an index tie-break; tim keeps ex-search's own pass\n // stable and avoids radix's score quantisation.\n sortAlgorithm: 'tim',\n })\n const view = projection(size, keys, at)\n engine.setData(view.items)\n return {\n match(rows, query) {\n const q = config.caseSensitive ? query : query.toLowerCase()\n return chunked(rows, (chunk) => {\n view.show(chunk)\n try {\n // Results are copies of the items (`Omit<Item, …>` drops `i` from the type, not the value).\n return engine.search(q) as unknown as readonly Hit[]\n } finally {\n view.show(undefined)\n }\n })\n },\n }\n}\n\n/** The fallback when ex-search is missing or incompatible: exact 1 · prefix 0.8 · substring 0.65. */\nfunction containsIndex(\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n config: MatchConfig,\n): SearchIndex {\n return {\n match(rows, query) {\n // Values are folded per chunk (buildIndex).\n const q = config.caseSensitive ? query : query.toLowerCase()\n return chunked(rows, (chunk) => {\n const hits: Hit[] = []\n for (let j = 0; j < chunk.length; j++) {\n const r = chunk[j] as number\n let best = 0\n for (let k = 0; k < keys.length; k++) {\n const v = at(k, r)\n const raw = v === q ? 1 : v.startsWith(q) ? 0.8 : v.includes(q) ? 0.65 : 0\n const w = (keys[k] as Key).weight\n if (raw * w > best) best = raw * w\n }\n if (best > 0 && best >= config.threshold) hits.push({ i: r, score: best })\n }\n return hits\n })\n },\n }\n}\n\n/**\n * Hits in relevance order (§6.3): score descending, ties by original row index. Enforced here so\n * the order never depends on the matcher's own ranking.\n */\nexport function rankHits(hits: readonly Hit[]): Uint32Array {\n const n = hits.length\n const index = new Uint32Array(n)\n const score = new Float64Array(n)\n const order = new Uint32Array(n)\n for (let k = 0; k < n; k++) {\n const h = hits[k] as Hit\n index[k] = h.i\n score[k] = h.score\n order[k] = k\n }\n order.sort(\n (a, b) =>\n (score[b] as number) - (score[a] as number) || (index[a] as number) - (index[b] as number),\n )\n const out = new Uint32Array(n)\n for (let k = 0; k < n; k++) out[k] = index[order[k] as number] as number\n return out\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/features/search/search-index.ts"],"sourcesContent":["import { ExSearch, Scorer } from 'ex-search'\nimport type { Emit } from '../../diagnostics/types'\nimport type { RowModelContext } from '../../feature/types'\nimport { resolveColumns } from '../../pipeline/columns'\nimport { arrayIndex } from '../../pipeline/row'\nimport { detectCapability, hasCapability } from '../../versioning/capability'\n\n/** Matcher settings (the client options of `search()`, defaults applied). */\nexport interface MatchConfig {\n readonly threshold: number\n readonly fuzzyMaxDistance: number\n readonly caseSensitive: boolean\n}\n\n/**\n * One projected row as ex-search reads it: `i` is the row index (results map back by it, never by\n * identity: ex-search returns copies), `c0`, `c1`, … the formatted values of the searchable\n * columns. Flat keys also sidestep ex-search's top-level-key limitation for nested paths. Items\n * are built per chunk while matching and dropped after it (§9.3 memory): none is kept.\n */\nexport type Item = Record<string, string | number> & { readonly i: number }\n\n/** A match: the row index and its relevance (0–1, weight applied). */\nexport interface Hit {\n readonly i: number\n readonly score: number\n}\n\n/** The matcher of one (data, columns) pair: references to column values, no per-row copy. */\nexport interface SearchIndex {\n /**\n * A time-sliced job (§9.1) matching `query` (trimmed, non-blank) against the rows `rows` (row\n * indices), in chunks: the hits in relevance order ({@link rankHits}).\n */\n match(rows: Uint32Array, query: string): Generator<undefined, Uint32Array, undefined>\n}\n\n/** Rows per chunk between two `yield`s of the matching job. */\nconst CHUNK = 4096\n\ninterface Key {\n readonly name: string\n readonly weight: number\n}\n\n/** The formatted value of row `r` of one searchable column. */\ntype Read = (r: number) => string\n\n// A throw (no `ExSearch`) counts as missing (hasCapability).\nconst hasExSearch = () =>\n [ExSearch.prototype.search, ExSearch.prototype.setData, Scorer.fuzzy].every(\n (f) => typeof f === 'function',\n )\n\n/** Feature-detects ex-search once (§8.5); `false` → the contains fallback. */\nexport function probeExSearch(emit: Emit): boolean {\n // DK_DEP_INCOMPATIBLE is a development check (§8.5); the fallback is not.\n return (process.env.NODE_ENV !== \"production\")\n ? detectCapability(\n hasExSearch,\n {\n feature: 'search',\n dependency: 'ex-search',\n capability: 'ExSearch, Scorer.fuzzy',\n supported: '^1.1.1',\n fallback: 'case-insensitive exact / prefix / substring matching (no fuzzy matching)',\n },\n emit,\n )\n : hasCapability(hasExSearch)\n}\n\n/**\n * A numeric column's formatted value without the cached strings: `String(v)` is what the default\n * formatter shows for a number. Only when no value is `NaN` — a `NaN` cell is `null` / `undefined`\n * (shown as `''`) or a real `NaN` (shown as `'NaN'`), which only the formatted cache tells apart.\n */\nfunction numberReader(values: Float64Array): Read | undefined {\n for (let r = 0; r < values.length; r++) if (Number.isNaN(values[r])) return undefined\n return (r) => String(values[r])\n}\n\n/**\n * Builds the matcher over the formatted values of the searchable columns (what the user sees), once\n * per data + columns, as a time-sliced job (§9.1): one column read per slice. Searchable columns are\n * those with a `search` weight > 0; when no column declares `search`, every column is searchable at\n * weight 1. `null` when nothing is searchable. The values are read from the column cache, never\n * copied (§9.3 memory): a numeric column without `format` is stringified while matching, and the\n * case is folded per chunk while matching.\n */\nexport function* buildIndex(\n ctx: RowModelContext,\n config: MatchConfig,\n useExSearch: boolean,\n): Generator<undefined, SearchIndex | null, undefined> {\n const cols = resolveColumns(ctx.columns)\n const declared = cols.list.some((c) => (c.def as { search?: unknown }).search !== undefined)\n const keys: Key[] = []\n const reads: Read[] = []\n for (const col of cols.list) {\n if (cols.byId.get(col.id) !== col) continue\n const w = (col.def as { search?: unknown }).search\n const weight = declared ? (typeof w === 'number' && w > 0 ? w : 0) : 1\n if (weight === 0) continue\n let read: Read | undefined\n if (!col.format) {\n const values = ctx.getColumnValues(col.id)\n if (values instanceof Float64Array) read = numberReader(values)\n }\n if (!read) {\n const formatted = ctx.getFormattedValues(col.id)\n if (!formatted) continue\n read = (r) => formatted[r] ?? ''\n }\n keys.push({ name: `c${keys.length}`, weight })\n reads.push(read)\n yield\n }\n if (keys.length === 0) return null\n // Values are folded per chunk while matching, and so is the query.\n const fold = (v: string) => (config.caseSensitive ? v : v.toLowerCase())\n const at = (k: number, r: number) => fold((reads[k] as Read)(r))\n return useExSearch\n ? exSearchIndex(ctx.data.length, keys, at, fold, config)\n : containsIndex(keys, at, fold, config)\n}\n\n/** Matches `rows` chunk by chunk (`score` decides alone, so chunks match like the whole). */\nfunction* chunked(\n rows: Uint32Array,\n matchChunk: (chunk: Uint32Array) => Hit[],\n): Generator<undefined, Uint32Array, undefined> {\n if (rows.length <= CHUNK) return rankHits(matchChunk(rows))\n const hits: Hit[] = []\n for (let k = 0; k < rows.length; k += CHUNK) {\n for (const h of matchChunk(rows.subarray(k, k + CHUNK))) hits.push(h)\n yield\n }\n return rankHits(hits)\n}\n\n/**\n * The projection as ex-search reads it: an array (iteration, `length`, index reads) of `{ i, c0, … }`\n * items, each built when read and not kept. It shows every row of `data` (`size`), or one chunk of\n * rows while that chunk is matched; ex-search holds this one view (`setData` once).\n */\nfunction projection(size: number, keys: readonly Key[], at: (k: number, r: number) => string) {\n let rows: Uint32Array | undefined\n // Never written, and its own `length` stays 0: `length` is read through the trap below (setting\n // a large `length` would allocate a backing store).\n const target: Item[] = []\n const length = () => (rows ? rows.length : size)\n const item = (j: number): Item => {\n const r = rows ? (rows[j] as number) : j\n const o = { i: r } as Record<string, string | number>\n for (let k = 0; k < keys.length; k++) o[(keys[k] as Key).name] = at(k, r)\n return o as Item\n }\n const items = new Proxy(target, {\n get(t, p) {\n if (p === 'length') return length()\n const j = arrayIndex(p, length())\n return j >= 0 ? item(j) : t[p as keyof Item[]]\n },\n has: (t, p) => arrayIndex(p, length()) >= 0 || p in t,\n })\n return {\n items,\n /** Shows `chunk` (row indices), or every row again (`undefined`). */\n show(chunk: Uint32Array | undefined) {\n rows = chunk\n },\n }\n}\n\nfunction exSearchIndex(\n size: number,\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n fold: (v: string) => string,\n config: MatchConfig,\n): SearchIndex {\n // Values and query are folded already (buildIndex).\n const engine = new ExSearch<Item>({\n keys: keys as Key[],\n ...config,\n caseSensitive: true,\n // DataKable re-ranks with exact scores and an index tie-break; tim keeps ex-search's own pass\n // stable and avoids radix's score quantisation.\n sortAlgorithm: 'tim',\n })\n // ex-search measures the edit distance to the whole value, so a typo of one word of a multi-word\n // cell never matched (#10). For a one-word query, a value without it is shown as the best fuzzy\n // match of itself and its words, or as '' (scores 0, as the value would). A value with the query\n // stays whole for the exact / prefix / substring scores. Only strings within `max` of the query's\n // length are measured (an edit distance is at least the length difference).\n const max = config.fuzzyMaxDistance\n let q = ''\n const near = (k: number, r: number) => {\n const v = at(k, r)\n if (v.includes(q) || /\\s/.test(q)) return v\n let best = ''\n let top = 0\n const words = v.split(/\\s+/)\n for (const w of words[1] ? [v, ...words] : words) {\n const s = Math.abs(w.length - q.length) > max ? 0 : Scorer.fuzzy(w, q, max, true)\n if (s > top) {\n best = w\n top = s\n }\n }\n return best\n }\n const view = projection(size, keys, near)\n engine.setData(view.items)\n return {\n match(rows, query) {\n const folded = fold(query)\n return chunked(rows, (chunk) => {\n // Set per chunk, like the view: another job may have matched in between.\n q = folded\n view.show(chunk)\n try {\n // Results are copies of the items (`Omit<Item, …>` drops `i` from the type, not the value).\n return engine.search(q) as unknown as Hit[]\n } finally {\n view.show(undefined)\n }\n })\n },\n }\n}\n\n/** The fallback when ex-search is missing or incompatible: exact 1 · prefix 0.8 · substring 0.65. */\nfunction containsIndex(\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n fold: (v: string) => string,\n config: MatchConfig,\n): SearchIndex {\n return {\n match(rows, query) {\n const q = fold(query)\n return chunked(rows, (chunk) => {\n const hits: Hit[] = []\n for (let j = 0; j < chunk.length; j++) {\n const r = chunk[j] as number\n let best = 0\n for (let k = 0; k < keys.length; k++) {\n const v = at(k, r)\n const raw = v === q ? 1 : v.startsWith(q) ? 0.8 : v.includes(q) ? 0.65 : 0\n const w = (keys[k] as Key).weight\n if (raw * w > best) best = raw * w\n }\n if (best > 0 && best >= config.threshold) hits.push({ i: r, score: best })\n }\n return hits\n })\n },\n }\n}\n\n/**\n * Hits in relevance order (§6.3): score descending, ties by original row index. Enforced here so\n * the order never depends on the matcher's own ranking. Sorts `hits` (a fresh array) in place.\n */\nexport function rankHits(hits: Hit[]): Uint32Array {\n return Uint32Array.from(\n hits.sort((a, b) => b.score - a.score || a.i - b.i),\n (h) => h.i,\n )\n}\n"],"mappings":"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","names":[]}