@zengrid/core 1.2.1 → 1.2.2

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 (57) hide show
  1. package/dist/index.cjs.js +564 -159
  2. package/dist/index.esm.js +564 -159
  3. package/dist/package.json +2 -2
  4. package/dist/src/data/data-manager.d.ts.map +1 -1
  5. package/dist/src/features/filtering/filter-ast-compiler.d.ts +1 -1
  6. package/dist/src/features/filtering/filter-ast-compiler.d.ts.map +1 -1
  7. package/dist/src/features/filtering/filter-ast.d.ts +6 -8
  8. package/dist/src/features/filtering/filter-ast.d.ts.map +1 -1
  9. package/dist/src/features/filtering/filter-execution-error.d.ts +2 -0
  10. package/dist/src/features/filtering/filter-execution-error.d.ts.map +1 -0
  11. package/dist/src/features/filtering/filter-expression.d.ts.map +1 -1
  12. package/dist/src/features/filtering/filter-manager/filter-core.d.ts +12 -4
  13. package/dist/src/features/filtering/filter-manager/filter-core.d.ts.map +1 -1
  14. package/dist/src/features/filtering/filter-manager/filter-query.d.ts +6 -1
  15. package/dist/src/features/filtering/filter-manager/filter-query.d.ts.map +1 -1
  16. package/dist/src/features/filtering/filter-manager/index.d.ts +1 -0
  17. package/dist/src/features/filtering/filter-manager/index.d.ts.map +1 -1
  18. package/dist/src/features/filtering/filter-signature.d.ts +6 -0
  19. package/dist/src/features/filtering/filter-signature.d.ts.map +1 -0
  20. package/dist/src/features/query/query-signature.d.ts +3 -0
  21. package/dist/src/features/query/query-signature.d.ts.map +1 -0
  22. package/dist/src/grid/api/filter-api.d.ts.map +1 -1
  23. package/dist/src/grid/api/sort-api.d.ts.map +1 -1
  24. package/dist/src/grid/filter-ui.d.ts +1 -0
  25. package/dist/src/grid/filter-ui.d.ts.map +1 -1
  26. package/dist/src/grid/grid-context.d.ts +2 -0
  27. package/dist/src/grid/grid-context.d.ts.map +1 -1
  28. package/dist/src/grid/grid-core.d.ts.map +1 -1
  29. package/dist/src/grid/grid-setup.d.ts.map +1 -1
  30. package/dist/src/grid/options-manager.d.ts +14 -0
  31. package/dist/src/grid/options-manager.d.ts.map +1 -0
  32. package/dist/src/grid/plugin-host.d.ts +3 -2
  33. package/dist/src/grid/plugin-host.d.ts.map +1 -1
  34. package/dist/src/grid/query-mode.d.ts +4 -0
  35. package/dist/src/grid/query-mode.d.ts.map +1 -0
  36. package/dist/src/index.d.ts +2 -1
  37. package/dist/src/index.d.ts.map +1 -1
  38. package/dist/src/plugins/data-plugin.d.ts.map +1 -1
  39. package/dist/src/plugins/filter-plugin.d.ts.map +1 -1
  40. package/dist/src/plugins/filter-ui/filter-ui-plugin.d.ts +1 -1
  41. package/dist/src/plugins/filter-ui/filter-ui-plugin.d.ts.map +1 -1
  42. package/dist/src/plugins/sort-plugin.d.ts.map +1 -1
  43. package/dist/src/reactive/index.d.ts +1 -1
  44. package/dist/src/reactive/index.d.ts.map +1 -1
  45. package/dist/src/reactive/types.d.ts +16 -1
  46. package/dist/src/reactive/types.d.ts.map +1 -1
  47. package/dist/src/types/data.d.ts +1 -0
  48. package/dist/src/types/data.d.ts.map +1 -1
  49. package/dist/src/types/filter.d.ts +12 -13
  50. package/dist/src/types/filter.d.ts.map +1 -1
  51. package/dist/src/types/grid.d.ts +5 -4
  52. package/dist/src/types/grid.d.ts.map +1 -1
  53. package/dist/src/types/query.d.ts +3 -43
  54. package/dist/src/types/query.d.ts.map +1 -1
  55. package/dist/src/utils/stable-hash.d.ts +2 -0
  56. package/dist/src/utils/stable-hash.d.ts.map +1 -0
  57. package/package.json +2 -2
package/dist/index.cjs.js CHANGED
@@ -11575,10 +11575,18 @@ class GridApiImpl {
11575
11575
  }
11576
11576
 
11577
11577
  class PluginHost {
11578
- constructor(store, api) {
11578
+ constructor(store, api, setupContext) {
11579
11579
  this.store = store;
11580
11580
  this.api = api;
11581
11581
  this.plugins = new Map();
11582
+ const options = setupContext?.options ?? {};
11583
+ this.setupContext = setupContext ?? {
11584
+ options,
11585
+ getOptions: () => options,
11586
+ getOptionsSnapshot: () => ({ ...options }),
11587
+ updateOptions: () => null,
11588
+ subscribeOptions: () => () => { },
11589
+ };
11582
11590
  }
11583
11591
  use(plugin) {
11584
11592
  if (this.plugins.has(plugin.name)) {
@@ -11594,7 +11602,7 @@ class PluginHost {
11594
11602
  const entry = { plugin };
11595
11603
  this.plugins.set(plugin.name, entry);
11596
11604
  const start = performance.now();
11597
- const disposable = plugin.setup(this.store, this.api);
11605
+ const disposable = plugin.setup(this.store, this.api, this.setupContext);
11598
11606
  const duration = performance.now() - start;
11599
11607
  recordPluginTiming(plugin.name, duration);
11600
11608
  if (disposable) {
@@ -11659,6 +11667,13 @@ function createCorePlugin(options) {
11659
11667
  };
11660
11668
  }
11661
11669
 
11670
+ function resolveQueryExecutionMode(mode) {
11671
+ return mode === 'backend' ? 'backend' : 'frontend';
11672
+ }
11673
+ function shouldDelegateQueryState(mode) {
11674
+ return resolveQueryExecutionMode(mode) === 'backend';
11675
+ }
11676
+
11662
11677
  function toggleSortState(currentState, column, enableMultiSort, additive) {
11663
11678
  const nextState = currentState.map((entry) => ({ ...entry }));
11664
11679
  if (!enableMultiSort || !additive) {
@@ -11695,7 +11710,7 @@ function createSortPlugin(options) {
11695
11710
  name: 'sort',
11696
11711
  phase: 10,
11697
11712
  dependencies: ['core'],
11698
- setup(store, api) {
11713
+ setup(store, api, context) {
11699
11714
  const usePipeline = options?.usePipeline !== false;
11700
11715
  const initialSort = normalizeSortState(options?.initialSort ?? []);
11701
11716
  const enableMultiSort = options?.enableMultiSort ?? false;
@@ -11713,9 +11728,8 @@ function createSortPlugin(options) {
11713
11728
  initialSort,
11714
11729
  });
11715
11730
  }
11716
- function getDataMode() {
11717
- const getMode = api.getMethod('data', 'getMode');
11718
- return getMode ? getMode() : 'frontend';
11731
+ function getQueryMode() {
11732
+ return resolveQueryExecutionMode(context.options.dataMode);
11719
11733
  }
11720
11734
  function syncToStore() {
11721
11735
  if (!usePipeline || !mgr) {
@@ -11800,7 +11814,7 @@ function createSortPlugin(options) {
11800
11814
  return sortState.find((entry) => entry.column === column)?.direction ?? null;
11801
11815
  },
11802
11816
  getIcons: () => ({ asc: '▲', desc: '▼' }),
11803
- getMode: () => getDataMode(),
11817
+ getMode: () => getQueryMode(),
11804
11818
  });
11805
11819
  if (usePipeline && initialSort.length) {
11806
11820
  syncToStore();
@@ -11812,6 +11826,13 @@ function createSortPlugin(options) {
11812
11826
  };
11813
11827
  }
11814
11828
 
11829
+ function buildUnexecutableFilterError(warnings = []) {
11830
+ const detail = warnings.filter(Boolean).join(' ');
11831
+ return new Error(detail
11832
+ ? `Structured filter cannot be executed in frontend mode. ${detail}`
11833
+ : 'Structured filter cannot be executed in frontend mode.');
11834
+ }
11835
+
11815
11836
  /**
11816
11837
  * FilterCompiler - Compiles filter conditions to optimized predicates
11817
11838
  *
@@ -12225,10 +12246,16 @@ function buildFilterAstFromModels(models, options = {}) {
12225
12246
  return null;
12226
12247
  }
12227
12248
  const children = models
12228
- .map((model) => modelToAstNode(model, options.columns))
12249
+ .map((model) => buildFilterAstNodeFromModel(model, options))
12229
12250
  .filter((node) => node !== null);
12230
12251
  return createFilterAst(children, 'AND', 'model');
12231
12252
  }
12253
+ function buildFilterAstNodeFromModel(model, options = {}) {
12254
+ return modelToAstNode(model, options.columns);
12255
+ }
12256
+ function buildFilterAstFromNodes(nodes, options = {}) {
12257
+ return createFilterAst(nodes ?? [], options.logic ?? 'AND', options.source ?? 'api');
12258
+ }
12232
12259
  function buildFilterAstFromFieldState(state, options = {}) {
12233
12260
  if (!state?.root) {
12234
12261
  return null;
@@ -12238,24 +12265,6 @@ function buildFilterAstFromFieldState(state, options = {}) {
12238
12265
  root: fieldGroupToAstGroup(state.root, options.columns),
12239
12266
  };
12240
12267
  }
12241
- function buildFilterAstFromSqlQuery(query, options = {}) {
12242
- const node = {
12243
- kind: 'sql',
12244
- source: 'sql',
12245
- sql: query.sql,
12246
- boundSql: options.boundSql,
12247
- params: options.params ? [...options.params] : undefined,
12248
- };
12249
- return {
12250
- version: 1,
12251
- root: {
12252
- kind: 'group',
12253
- logic: 'AND',
12254
- children: [node],
12255
- source: 'sql',
12256
- },
12257
- };
12258
- }
12259
12268
  function buildFilterAst(options) {
12260
12269
  const nodes = [];
12261
12270
  const baseAst = options.ast?.root
@@ -12264,12 +12273,7 @@ function buildFilterAst(options) {
12264
12273
  ? buildFilterAstFromFieldState(options.fieldState, options)
12265
12274
  : options.models && options.models.length > 0
12266
12275
  ? buildFilterAstFromModels(options.models, options)
12267
- : options.sqlQuery?.sql
12268
- ? buildFilterAstFromSqlQuery(options.sqlQuery, {
12269
- boundSql: options.boundSql,
12270
- params: options.params,
12271
- })
12272
- : null;
12276
+ : null;
12273
12277
  if (baseAst?.root) {
12274
12278
  nodes.push(baseAst.root);
12275
12279
  }
@@ -12552,7 +12556,7 @@ function collectActiveFields(group, activeFields) {
12552
12556
  }
12553
12557
  }
12554
12558
  function normalizeConditionValue(operator, value) {
12555
- if (operator === 'between' && Array.isArray(value)) {
12559
+ if (operator === 'between' && Array.isArray(value) && value.length >= 2) {
12556
12560
  return {
12557
12561
  value: value[0],
12558
12562
  valueTo: value[1],
@@ -12663,7 +12667,12 @@ class FilterAstCompiler {
12663
12667
  this.conditionCompiler = new FilterCompiler();
12664
12668
  this.colCount = options.colCount;
12665
12669
  this.getValue = options.getValue;
12666
- this.columns = options.columns;
12670
+ this.fieldToColumn = new Map();
12671
+ options.columns?.forEach((column, index) => {
12672
+ if (column.field) {
12673
+ this.fieldToColumn.set(column.field, index);
12674
+ }
12675
+ });
12667
12676
  }
12668
12677
  compile(ast) {
12669
12678
  const warnings = [];
@@ -12708,9 +12717,6 @@ class FilterAstCompiler {
12708
12717
  }
12709
12718
  return (row) => !child(row);
12710
12719
  }
12711
- case 'sql':
12712
- warnings.push('SQL AST nodes cannot be executed in frontend filtering.');
12713
- return null;
12714
12720
  }
12715
12721
  }
12716
12722
  compilePredicateNode(node, warnings) {
@@ -12735,15 +12741,33 @@ class FilterAstCompiler {
12735
12741
  if (!columns || columns.length === 0) {
12736
12742
  return null;
12737
12743
  }
12744
+ const requiresJoinedSearch = /\s/.test(query);
12745
+ if (!requiresJoinedSearch) {
12746
+ return (row) => {
12747
+ for (const column of columns) {
12748
+ const value = this.getValue(row, column);
12749
+ if (value !== undefined &&
12750
+ value !== null &&
12751
+ String(value).toLowerCase().includes(query)) {
12752
+ return true;
12753
+ }
12754
+ }
12755
+ return false;
12756
+ };
12757
+ }
12738
12758
  return (row) => {
12739
- const parts = [];
12759
+ let searchableText = '';
12740
12760
  for (const column of columns) {
12741
12761
  const value = this.getValue(row, column);
12742
12762
  if (value !== undefined && value !== null) {
12743
- parts.push(String(value));
12763
+ const normalized = String(value).toLowerCase();
12764
+ if (normalized.includes(query)) {
12765
+ return true;
12766
+ }
12767
+ searchableText = searchableText ? `${searchableText} ${normalized}` : normalized;
12744
12768
  }
12745
12769
  }
12746
- return parts.join(' ').toLowerCase().includes(query);
12770
+ return searchableText.includes(query);
12747
12771
  };
12748
12772
  }
12749
12773
  resolveSearchColumns(targets, warnings) {
@@ -12751,13 +12775,17 @@ class FilterAstCompiler {
12751
12775
  return Array.from({ length: this.colCount }, (_, index) => index);
12752
12776
  }
12753
12777
  const columns = [];
12778
+ const seenColumns = new Set();
12754
12779
  for (const target of targets) {
12755
12780
  const column = this.resolveColumn(target);
12756
12781
  if (column === undefined) {
12757
12782
  warnings.push('AST search target is missing a column reference for frontend execution.');
12758
12783
  return null;
12759
12784
  }
12760
- columns.push(column);
12785
+ if (!seenColumns.has(column)) {
12786
+ seenColumns.add(column);
12787
+ columns.push(column);
12788
+ }
12761
12789
  }
12762
12790
  return columns;
12763
12791
  }
@@ -12765,11 +12793,10 @@ class FilterAstCompiler {
12765
12793
  if (typeof target.column === 'number') {
12766
12794
  return target.column;
12767
12795
  }
12768
- if (!target.field || !this.columns) {
12796
+ if (!target.field) {
12769
12797
  return undefined;
12770
12798
  }
12771
- const column = this.columns.findIndex((candidate) => candidate.field === target.field);
12772
- return column >= 0 ? column : undefined;
12799
+ return this.fieldToColumn.get(target.field);
12773
12800
  }
12774
12801
  toFilterCondition(node, warnings) {
12775
12802
  const sourceOperator = typeof node.sourceOperator === 'string' && isSupportedFilterOperator(node.sourceOperator)
@@ -12808,6 +12835,120 @@ function dedupeWarnings$1(warnings) {
12808
12835
  return Array.from(new Set(warnings));
12809
12836
  }
12810
12837
 
12838
+ const FNV_OFFSET_BASIS = 0x811c9dc5;
12839
+ const FNV_PRIME = 0x01000193;
12840
+ function stableHashValue(value) {
12841
+ const hasher = new StableHasher();
12842
+ const seen = new WeakSet();
12843
+ hashValue(hasher, value, seen);
12844
+ return hasher.digest();
12845
+ }
12846
+ class StableHasher {
12847
+ constructor() {
12848
+ this.hash = FNV_OFFSET_BASIS;
12849
+ }
12850
+ write(token) {
12851
+ for (let index = 0; index < token.length; index += 1) {
12852
+ this.hash ^= token.charCodeAt(index);
12853
+ this.hash = Math.imul(this.hash, FNV_PRIME);
12854
+ }
12855
+ }
12856
+ digest() {
12857
+ return (this.hash >>> 0).toString(16).padStart(8, '0');
12858
+ }
12859
+ }
12860
+ function hashValue(hasher, value, seen) {
12861
+ if (value === null) {
12862
+ hasher.write('null');
12863
+ return;
12864
+ }
12865
+ if (value === undefined) {
12866
+ hasher.write('undefined');
12867
+ return;
12868
+ }
12869
+ if (value instanceof Date) {
12870
+ hasher.write('date');
12871
+ hasher.write(value.toISOString());
12872
+ return;
12873
+ }
12874
+ if (Array.isArray(value)) {
12875
+ hasher.write('array[');
12876
+ for (const item of value) {
12877
+ hashValue(hasher, item, seen);
12878
+ hasher.write(',');
12879
+ }
12880
+ hasher.write(']');
12881
+ return;
12882
+ }
12883
+ switch (typeof value) {
12884
+ case 'string':
12885
+ hasher.write('string');
12886
+ hasher.write(value);
12887
+ return;
12888
+ case 'number':
12889
+ hasher.write('number');
12890
+ hasher.write(Number.isFinite(value) ? value.toString() : String(value));
12891
+ return;
12892
+ case 'boolean':
12893
+ hasher.write(value ? 'true' : 'false');
12894
+ return;
12895
+ case 'bigint':
12896
+ hasher.write('bigint');
12897
+ hasher.write(value.toString());
12898
+ return;
12899
+ case 'symbol':
12900
+ hasher.write('symbol');
12901
+ hasher.write(value.description ?? '');
12902
+ return;
12903
+ case 'function':
12904
+ hasher.write('function');
12905
+ hasher.write(value.name);
12906
+ return;
12907
+ case 'object':
12908
+ break;
12909
+ default:
12910
+ hasher.write(String(value));
12911
+ return;
12912
+ }
12913
+ if (seen.has(value)) {
12914
+ hasher.write('[Circular]');
12915
+ return;
12916
+ }
12917
+ seen.add(value);
12918
+ hasher.write('{');
12919
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
12920
+ for (const [key, nested] of entries) {
12921
+ hasher.write(key);
12922
+ hasher.write(':');
12923
+ hashValue(hasher, nested, seen);
12924
+ hasher.write(';');
12925
+ }
12926
+ hasher.write('}');
12927
+ seen.delete(value);
12928
+ }
12929
+
12930
+ const filterAstSignatureCache = new WeakMap();
12931
+ function getFilterAstSignature(ast) {
12932
+ if (!ast?.root) {
12933
+ return '';
12934
+ }
12935
+ const cached = filterAstSignatureCache.get(ast);
12936
+ if (cached) {
12937
+ return cached;
12938
+ }
12939
+ const signature = stableHashValue(ast.root);
12940
+ filterAstSignatureCache.set(ast, signature);
12941
+ return signature;
12942
+ }
12943
+ function buildFilterStateSignature(executionMode, models, ast) {
12944
+ if (executionMode === 'none' && models.length === 0 && !ast?.root) {
12945
+ return '';
12946
+ }
12947
+ const modelSignature = models.length > 0 ? stableHashValue(models) : '';
12948
+ const astSignature = executionMode === 'column' || executionMode === 'none' ? '' : getFilterAstSignature(ast);
12949
+ return `mode:${executionMode}|models:${modelSignature}|ast:${astSignature}`;
12950
+ }
12951
+
12811
12952
  /**
12812
12953
  * FilterCore - Core filter state management
12813
12954
  *
@@ -12817,7 +12958,9 @@ function dedupeWarnings$1(warnings) {
12817
12958
  class FilterCore {
12818
12959
  constructor(colCount, getValue, columns, events, onFilterStateChange) {
12819
12960
  this.columnFilters = new Map();
12961
+ this.columnAstNodes = new Map();
12820
12962
  this.filterState = [];
12963
+ this.filterStateSnapshot = [];
12821
12964
  this.filterStateHash = '';
12822
12965
  this.filterAst = null;
12823
12966
  this.astProjectionWarnings = [];
@@ -12829,6 +12972,7 @@ class FilterCore {
12829
12972
  this.columns = columns;
12830
12973
  this.events = events;
12831
12974
  this.onFilterStateChange = onFilterStateChange;
12975
+ this.astCompiler = this.createAstCompiler();
12832
12976
  }
12833
12977
  /**
12834
12978
  * Initialize with filter models
@@ -12848,8 +12992,9 @@ class FilterCore {
12848
12992
  this.clearColumnFilter(column);
12849
12993
  return;
12850
12994
  }
12851
- this.columnFilters.set(column, this.createColumnFilter(column, conditions, logic));
12852
- this.syncStateFromColumnFilters();
12995
+ const filter = this.createColumnFilter(column, conditions, logic);
12996
+ this.columnFilters.set(column, filter);
12997
+ this.syncStateFromColumnModel(filter.getModel());
12853
12998
  this.emitFilterChange();
12854
12999
  }
12855
13000
  /**
@@ -12857,18 +13002,25 @@ class FilterCore {
12857
13002
  */
12858
13003
  setFilterModels(models, options = {}) {
12859
13004
  this.columnFilters.clear();
13005
+ this.columnAstNodes.clear();
13006
+ const nextState = [];
12860
13007
  for (const model of models) {
12861
13008
  if (!model.conditions || model.conditions.length === 0) {
12862
13009
  continue;
12863
13010
  }
12864
13011
  this.assertColumnInBounds(model.column);
12865
- this.columnFilters.set(model.column, this.createColumnFilter(model.column, model.conditions, model.logic ?? 'AND'));
12866
- }
12867
- this.filterState = Array.from(this.columnFilters.values()).map((filter) => filter.getModel());
13012
+ const filter = this.createColumnFilter(model.column, model.conditions, model.logic ?? 'AND');
13013
+ const nextModel = filter.getModel();
13014
+ this.columnFilters.set(nextModel.column, filter);
13015
+ this.upsertFilterState(nextModel, nextState);
13016
+ this.upsertColumnAstNode(nextModel);
13017
+ }
13018
+ this.filterState = nextState;
13019
+ this.refreshFilterStateSnapshot();
12868
13020
  this.filterAst =
12869
13021
  options.ast !== undefined
12870
13022
  ? options.ast
12871
- : buildFilterAstFromModels(this.filterState, { columns: this.columns });
13023
+ : buildFilterAstFromNodes(Array.from(this.columnAstNodes.values()), { source: 'model' });
12872
13024
  this.astProjectionWarnings = dedupeWarnings(options.projectionWarnings ?? []);
12873
13025
  this.syncExecutionState();
12874
13026
  this.filterStateHash = this.buildFilterStateHash();
@@ -12892,16 +13044,40 @@ class FilterCore {
12892
13044
  */
12893
13045
  setColumns(columns) {
12894
13046
  this.columns = columns;
13047
+ this.astCompiler = this.createAstCompiler();
12895
13048
  if (this.filterAst) {
13049
+ if (this.executionMode === 'column' && this.filterState.length > 0) {
13050
+ this.rebuildColumnAstNodes();
13051
+ this.filterAst = buildFilterAstFromNodes(Array.from(this.columnAstNodes.values()), {
13052
+ source: 'model',
13053
+ });
13054
+ this.filterStateHash = this.buildFilterStateHash();
13055
+ return;
13056
+ }
12896
13057
  this.setFilterAst(this.filterAst);
12897
13058
  return;
12898
13059
  }
12899
13060
  if (this.filterState.length > 0) {
12900
- this.filterAst = buildFilterAstFromModels(this.filterState, { columns: this.columns });
12901
- this.syncExecutionState();
13061
+ this.rebuildColumnAstNodes();
13062
+ this.filterAst = buildFilterAstFromNodes(Array.from(this.columnAstNodes.values()), {
13063
+ source: 'model',
13064
+ });
12902
13065
  this.filterStateHash = this.buildFilterStateHash();
12903
13066
  }
12904
13067
  }
13068
+ setColCount(colCount) {
13069
+ this.colCount = colCount;
13070
+ this.astCompiler = this.createAstCompiler();
13071
+ if (!this.filterAst) {
13072
+ return;
13073
+ }
13074
+ if (this.executionMode === 'column') {
13075
+ this.filterStateHash = this.buildFilterStateHash();
13076
+ return;
13077
+ }
13078
+ this.syncExecutionState();
13079
+ this.filterStateHash = this.buildFilterStateHash();
13080
+ }
12905
13081
  /**
12906
13082
  * Clear filter for a column
12907
13083
  * @param column - Column index
@@ -12909,7 +13085,7 @@ class FilterCore {
12909
13085
  clearColumnFilter(column) {
12910
13086
  if (this.columnFilters.has(column)) {
12911
13087
  this.columnFilters.delete(column);
12912
- this.syncStateFromColumnFilters();
13088
+ this.removeColumnState(column);
12913
13089
  this.emitFilterChange();
12914
13090
  }
12915
13091
  }
@@ -12918,7 +13094,9 @@ class FilterCore {
12918
13094
  */
12919
13095
  clearAll() {
12920
13096
  this.columnFilters.clear();
13097
+ this.columnAstNodes.clear();
12921
13098
  this.filterState = [];
13099
+ this.refreshFilterStateSnapshot();
12922
13100
  this.filterStateHash = '';
12923
13101
  this.filterAst = null;
12924
13102
  this.astProjectionWarnings = [];
@@ -12934,9 +13112,12 @@ class FilterCore {
12934
13112
  * @returns True if row passes filter
12935
13113
  */
12936
13114
  testRow(row) {
12937
- if (this.executionMode === 'none' || this.executionMode === 'unexecutable') {
13115
+ if (this.executionMode === 'none') {
12938
13116
  return true;
12939
13117
  }
13118
+ if (this.executionMode === 'unexecutable') {
13119
+ throw buildUnexecutableFilterError(this.astExecutionWarnings);
13120
+ }
12940
13121
  if (this.executionMode === 'ast') {
12941
13122
  return this.compiledAstPredicate ? this.compiledAstPredicate(row) : true;
12942
13123
  }
@@ -12954,6 +13135,9 @@ class FilterCore {
12954
13135
  * @returns Array of visible row indices
12955
13136
  */
12956
13137
  getVisibleRowsBasic(rowCount) {
13138
+ if (this.executionMode === 'unexecutable') {
13139
+ throw buildUnexecutableFilterError(this.astExecutionWarnings);
13140
+ }
12957
13141
  const visible = [];
12958
13142
  for (let row = 0; row < rowCount; row++) {
12959
13143
  if (this.testRow(row)) {
@@ -12966,7 +13150,7 @@ class FilterCore {
12966
13150
  * Get current legacy FilterModel state
12967
13151
  */
12968
13152
  getFilterState() {
12969
- return [...this.filterState];
13153
+ return this.filterStateSnapshot;
12970
13154
  }
12971
13155
  /**
12972
13156
  * Get the canonical filter AST state
@@ -12996,7 +13180,7 @@ class FilterCore {
12996
13180
  * Check if any frontend-evaluable filters are active
12997
13181
  */
12998
13182
  hasActiveFilters() {
12999
- return this.executionMode === 'column' || this.executionMode === 'ast';
13183
+ return this.executionMode !== 'none';
13000
13184
  }
13001
13185
  /**
13002
13186
  * Check if any canonical filters are active, even if the current frontend evaluator
@@ -13047,23 +13231,12 @@ class FilterCore {
13047
13231
  previousFilterState: [],
13048
13232
  });
13049
13233
  }
13050
- /**
13051
- * Rebuild legacy models and the canonical AST from column filters.
13052
- */
13053
- syncStateFromColumnFilters() {
13054
- this.filterState = Array.from(this.columnFilters.values()).map((filter) => filter.getModel());
13055
- this.filterAst = buildFilterAstFromModels(this.filterState, { columns: this.columns });
13056
- this.astProjectionWarnings = [];
13057
- this.syncExecutionState();
13058
- this.filterStateHash = this.buildFilterStateHash();
13059
- this.notifyFilterStateChange();
13060
- }
13061
13234
  createColumnFilter(column, conditions, logic) {
13062
13235
  return new ColumnFilter({
13063
13236
  column,
13064
13237
  conditions,
13065
13238
  logic,
13066
- onChange: () => this.syncStateFromColumnFilters(),
13239
+ onChange: (model) => this.syncStateFromColumnModel(model),
13067
13240
  });
13068
13241
  }
13069
13242
  syncExecutionState() {
@@ -13077,11 +13250,7 @@ class FilterCore {
13077
13250
  this.executionMode = 'column';
13078
13251
  return;
13079
13252
  }
13080
- const compiled = new FilterAstCompiler({
13081
- colCount: this.colCount,
13082
- getValue: this.getValue,
13083
- columns: this.columns,
13084
- }).compile(this.filterAst);
13253
+ const compiled = this.astCompiler.compile(this.filterAst);
13085
13254
  this.astExecutionWarnings = compiled.warnings;
13086
13255
  if (compiled.predicate) {
13087
13256
  this.compiledAstPredicate = compiled.predicate;
@@ -13091,14 +13260,7 @@ class FilterCore {
13091
13260
  this.executionMode = 'unexecutable';
13092
13261
  }
13093
13262
  buildFilterStateHash() {
13094
- if (!this.filterAst?.root && this.filterState.length === 0) {
13095
- return '';
13096
- }
13097
- return JSON.stringify({
13098
- mode: this.executionMode,
13099
- models: this.filterState,
13100
- ast: this.filterAst,
13101
- });
13263
+ return buildFilterStateSignature(this.executionMode, this.filterState, this.filterAst);
13102
13264
  }
13103
13265
  assertColumnInBounds(column) {
13104
13266
  if (column < 0 || column >= this.colCount) {
@@ -13110,6 +13272,64 @@ class FilterCore {
13110
13272
  this.onFilterStateChange();
13111
13273
  }
13112
13274
  }
13275
+ syncStateFromColumnModel(model) {
13276
+ this.upsertFilterState(model);
13277
+ this.upsertColumnAstNode(model);
13278
+ this.syncColumnDerivedState();
13279
+ }
13280
+ removeColumnState(column) {
13281
+ const index = this.filterState.findIndex((model) => model.column === column);
13282
+ if (index >= 0) {
13283
+ this.filterState.splice(index, 1);
13284
+ }
13285
+ this.columnAstNodes.delete(column);
13286
+ this.syncColumnDerivedState();
13287
+ }
13288
+ syncColumnDerivedState() {
13289
+ this.filterAst = buildFilterAstFromNodes(Array.from(this.columnAstNodes.values()), {
13290
+ source: 'model',
13291
+ });
13292
+ this.astProjectionWarnings = [];
13293
+ this.astExecutionWarnings = [];
13294
+ this.compiledAstPredicate = null;
13295
+ this.executionMode = this.filterState.length === 0 ? 'none' : 'column';
13296
+ this.refreshFilterStateSnapshot();
13297
+ this.filterStateHash = this.buildFilterStateHash();
13298
+ this.notifyFilterStateChange();
13299
+ }
13300
+ upsertFilterState(model, target = this.filterState) {
13301
+ const index = target.findIndex((candidate) => candidate.column === model.column);
13302
+ if (index >= 0) {
13303
+ target[index] = model;
13304
+ return;
13305
+ }
13306
+ target.push(model);
13307
+ }
13308
+ upsertColumnAstNode(model) {
13309
+ const astNode = buildFilterAstNodeFromModel(model, { columns: this.columns });
13310
+ if (astNode) {
13311
+ this.columnAstNodes.set(model.column, astNode);
13312
+ }
13313
+ else {
13314
+ this.columnAstNodes.delete(model.column);
13315
+ }
13316
+ }
13317
+ rebuildColumnAstNodes() {
13318
+ this.columnAstNodes.clear();
13319
+ for (const model of this.filterState) {
13320
+ this.upsertColumnAstNode(model);
13321
+ }
13322
+ }
13323
+ refreshFilterStateSnapshot() {
13324
+ this.filterStateSnapshot = Object.freeze([...this.filterState]);
13325
+ }
13326
+ createAstCompiler() {
13327
+ return new FilterAstCompiler({
13328
+ colCount: this.colCount,
13329
+ getValue: this.getValue,
13330
+ columns: this.columns,
13331
+ });
13332
+ }
13113
13333
  }
13114
13334
  function dedupeWarnings(warnings) {
13115
13335
  return Array.from(new Set(warnings));
@@ -15593,12 +15813,14 @@ function inferValueType(value, valueTo) {
15593
15813
  * FilterQueryHandler - SQL-like query parsing and expression management.
15594
15814
  */
15595
15815
  class FilterQueryHandler {
15596
- constructor(core, columnNames, columns, events) {
15816
+ constructor(core, columnNames, columns, events, getFieldFilterState, getFilterExport) {
15597
15817
  this.currentExpression = null;
15598
15818
  this.core = core;
15599
15819
  this.columnNames = columnNames;
15600
15820
  this.columns = columns;
15601
15821
  this.events = events;
15822
+ this.getFieldFilterState = getFieldFilterState;
15823
+ this.getFilterExport = getFilterExport;
15602
15824
  this.parser = new FilterQueryParser();
15603
15825
  if (this.columnNames.length > 0) {
15604
15826
  const columnMapping = {};
@@ -15618,16 +15840,17 @@ class FilterQueryHandler {
15618
15840
  return;
15619
15841
  }
15620
15842
  try {
15621
- const expression = this.parseFilterInput(filter);
15843
+ const parsedExpression = this.parseFilterInput(filter);
15844
+ this.core.setFilterAst(parsedExpression.ast ?? null);
15845
+ const expression = this.buildCurrentExpression(parsedExpression);
15622
15846
  this.currentExpression = expression;
15623
- this.core.setFilterAst(expression.ast ?? null);
15624
15847
  if (this.events) {
15625
15848
  this.events.emit('filter:start', {
15626
15849
  timestamp: Date.now(),
15627
15850
  filter: expression,
15628
15851
  });
15629
15852
  }
15630
- await this.applyFrontendFilter(expression);
15853
+ await this.applyFrontendFilter();
15631
15854
  if (this.events) {
15632
15855
  this.events.emit('filter:end', {
15633
15856
  timestamp: Date.now(),
@@ -15658,6 +15881,20 @@ class FilterQueryHandler {
15658
15881
  models: filter,
15659
15882
  };
15660
15883
  }
15884
+ buildCurrentExpression(parsedExpression) {
15885
+ const hasColumns = Array.isArray(this.columns) && this.columns.length > 0;
15886
+ return {
15887
+ type: parsedExpression.type,
15888
+ ast: this.core.getFilterAst(),
15889
+ sql: parsedExpression.sql,
15890
+ boundSql: parsedExpression.boundSql,
15891
+ params: parsedExpression.params,
15892
+ models: this.core.getFilterState(),
15893
+ quickFilter: parsedExpression.quickFilter,
15894
+ fieldState: hasColumns ? (this.getFieldFilterState?.() ?? null) : null,
15895
+ filterExport: hasColumns ? (this.getFilterExport?.() ?? null) : null,
15896
+ };
15897
+ }
15661
15898
  /**
15662
15899
  * Type guard for FilterQuery.
15663
15900
  */
@@ -15667,18 +15904,11 @@ class FilterQueryHandler {
15667
15904
  /**
15668
15905
  * Apply frontend filtering (in-memory).
15669
15906
  */
15670
- async applyFrontendFilter(expression) {
15907
+ async applyFrontendFilter() {
15671
15908
  const projectionWarnings = this.core.getAstProjectionWarnings();
15672
15909
  const executionWarnings = this.core.getAstExecutionWarnings();
15673
15910
  if (this.core.hasUnexecutableFilters()) {
15674
- if (expression.type === 'sql') {
15675
- console.warn('Frontend SQL filtering remains compatibility-only. ' +
15676
- 'The canonical filter is preserved in filter.ast and filter.expression.');
15677
- }
15678
- for (const warning of executionWarnings) {
15679
- console.warn(`FilterQueryHandler: ${warning}`);
15680
- }
15681
- return;
15911
+ throw buildUnexecutableFilterError(executionWarnings);
15682
15912
  }
15683
15913
  if (!this.core.isUsingAstExecution() || projectionWarnings.length === 0) {
15684
15914
  return;
@@ -15720,8 +15950,8 @@ class FilterQueryHandler {
15720
15950
  class FilterManager {
15721
15951
  constructor(options) {
15722
15952
  this.core = new FilterCore(options.colCount, options.getValue, options.columns, options.events, () => this.optimizer?.invalidateCache());
15723
- this.query = new FilterQueryHandler(this.core, options.columnNames || [], options.columns, options.events);
15724
15953
  this.exporter = new FilterExporter(this.core, options.enableExport ?? true, options.columns, options.exportConfig);
15954
+ this.query = new FilterQueryHandler(this.core, options.columnNames || [], options.columns, options.events, () => this.exporter.getFieldFilterState(), () => this.exporter.getFilterExport());
15725
15955
  this.optimizer = new FilterOptimizer(this.core, options.getValue, options.enableOptimizations ?? true, {
15726
15956
  enableCache: options.enableCache,
15727
15957
  cacheCapacity: options.cacheCapacity,
@@ -15818,6 +16048,9 @@ class FilterManager {
15818
16048
  this.query.setColumns(columns);
15819
16049
  this.exporter.setColumns(columns);
15820
16050
  }
16051
+ setColCount(colCount) {
16052
+ this.core.setColCount(colCount);
16053
+ }
15821
16054
  // ==================== Optimization Methods ====================
15822
16055
  setDataVersion(version) {
15823
16056
  this.optimizer.setDataVersion(version);
@@ -15865,22 +16098,23 @@ function createFilterPlugin(options) {
15865
16098
  name: 'filter',
15866
16099
  phase: 20,
15867
16100
  dependencies: ['core'],
15868
- setup(store, api) {
16101
+ setup(store, api, context) {
15869
16102
  store.extend('filter.state', [], 'filter');
15870
16103
  store.extend('filter.ast', null, 'filter');
15871
16104
  store.extend('filter.quickFilter', { query: '', columns: null }, 'filter');
15872
16105
  store.extend('filter.expression', null, 'filter');
15873
16106
  store.extend('pipeline.filter', undefined, 'filter', 20);
15874
16107
  const usePipeline = options?.usePipeline !== false;
15875
- let mgr = createManager(options?.colCount ?? 0);
16108
+ const getColumns = () => context.options.columns ?? options?.columns;
16109
+ const getColCount = () => context.options.colCount ?? options?.colCount ?? 0;
16110
+ let mgr = createManager(getColCount());
15876
16111
  // Quick filter state
15877
16112
  let quickFilterQuery = '';
15878
16113
  let quickFilterColumns = null;
15879
16114
  let quickFilterCache = null;
15880
16115
  let quickFilterCacheKey = null;
15881
- function getDataMode() {
15882
- const getMode = api.getMethod('data', 'getMode');
15883
- return getMode ? getMode() : 'frontend';
16116
+ function getQueryMode() {
16117
+ return resolveQueryExecutionMode(context.options.dataMode);
15884
16118
  }
15885
16119
  function createManager(colCount, dataAccessor) {
15886
16120
  const getValue = dataAccessor
@@ -15892,7 +16126,7 @@ function createFilterPlugin(options) {
15892
16126
  return new FilterManager({
15893
16127
  colCount,
15894
16128
  getValue,
15895
- columns: options?.columns,
16129
+ columns: getColumns(),
15896
16130
  enableExport: options?.enableExport ?? true,
15897
16131
  });
15898
16132
  }
@@ -15907,14 +16141,15 @@ function createFilterPlugin(options) {
15907
16141
  };
15908
16142
  }
15909
16143
  function buildExpression() {
15910
- const hasColumns = Array.isArray(options?.columns) && options.columns.length > 0;
16144
+ const columns = getColumns();
16145
+ const hasColumns = Array.isArray(columns) && columns.length > 0;
15911
16146
  return buildModelFilterExpression({
15912
16147
  models: mgr.getFilterState(),
15913
16148
  quickFilter: getQuickFilterState(),
15914
16149
  fieldState: hasColumns ? mgr.getFieldFilterState() : null,
15915
16150
  filterExport: hasColumns ? mgr.getFilterExport() : null,
15916
16151
  ast: mgr.getFilterAst(),
15917
- columns: options?.columns,
16152
+ columns,
15918
16153
  });
15919
16154
  }
15920
16155
  function persistState() {
@@ -15939,12 +16174,22 @@ function createFilterPlugin(options) {
15939
16174
  if (quickFilterColumns && quickFilterColumns.length > 0) {
15940
16175
  return quickFilterColumns;
15941
16176
  }
15942
- const colCount = options?.colCount ?? 0;
15943
- if (options?.columns && options.columns.length > 0) {
15944
- return options.columns.map((_c, index) => index);
16177
+ const columns = getColumns();
16178
+ const colCount = getColCount();
16179
+ if (columns && columns.length > 0) {
16180
+ return columns.map((_c, index) => index);
15945
16181
  }
15946
16182
  return Array.from({ length: colCount }, (_, i) => i);
15947
16183
  }
16184
+ const unsubscribeOptions = context.subscribeOptions((change) => {
16185
+ if (!change.changedKeys.includes('columns') && !change.changedKeys.includes('colCount')) {
16186
+ return;
16187
+ }
16188
+ mgr.setColumns(getColumns() ?? []);
16189
+ mgr.setColCount(getColCount());
16190
+ clearQuickFilterCache();
16191
+ applyFilter();
16192
+ });
15948
16193
  function ensureQuickFilterCache(columns) {
15949
16194
  const cacheKey = columns.join(',');
15950
16195
  if (quickFilterCache && quickFilterCacheKey === cacheKey) {
@@ -15991,23 +16236,31 @@ function createFilterPlugin(options) {
15991
16236
  return matches;
15992
16237
  }
15993
16238
  function applyFilter() {
15994
- const hasColumnFilters = mgr.hasActiveFilters();
16239
+ const hasStructuredFilters = mgr.hasStructuredFilters();
15995
16240
  const hasQuickFilter = quickFilterQuery.trim().length > 0;
15996
16241
  const expression = persistState();
15997
16242
  if (!usePipeline) {
15998
16243
  store.set('pipeline.filter', undefined);
15999
16244
  return expression;
16000
16245
  }
16001
- if (!hasColumnFilters && !hasQuickFilter) {
16246
+ if (!hasStructuredFilters && !hasQuickFilter) {
16002
16247
  store.set('pipeline.filter', undefined);
16003
16248
  warnOnStructuredProjection();
16004
16249
  return expression;
16005
16250
  }
16251
+ if (mgr.hasUnexecutableFilters()) {
16252
+ const error = buildUnexecutableFilterError(mgr.getAstExecutionWarnings());
16253
+ api.fireEvent('filter:error', {
16254
+ timestamp: Date.now(),
16255
+ error,
16256
+ });
16257
+ throw error;
16258
+ }
16006
16259
  const rowCount = store.get('rows.count');
16007
16260
  const input = store.getUnphased('pipeline.sort') ??
16008
16261
  store.get('rows.indices');
16009
16262
  let result = input;
16010
- if (hasColumnFilters) {
16263
+ if (hasStructuredFilters) {
16011
16264
  const visibleSet = new Set(mgr.getVisibleRows(rowCount));
16012
16265
  result = result.filter((idx) => visibleSet.has(idx));
16013
16266
  }
@@ -16022,7 +16275,10 @@ function createFilterPlugin(options) {
16022
16275
  const prev = mgr.getFilterState();
16023
16276
  mgr.setColumnFilter(col, conditions, logic);
16024
16277
  applyFilter();
16025
- api.fireEvent('filter:change', { filterState: mgr.getFilterState(), previousFilterState: prev });
16278
+ api.fireEvent('filter:change', {
16279
+ filterState: mgr.getFilterState(),
16280
+ previousFilterState: prev,
16281
+ });
16026
16282
  }, 'filter');
16027
16283
  store.action('filter:clear', () => {
16028
16284
  const prev = mgr.getFilterState();
@@ -16038,7 +16294,10 @@ function createFilterPlugin(options) {
16038
16294
  const prev = mgr.getFilterState();
16039
16295
  mgr.clearColumnFilter(col);
16040
16296
  applyFilter();
16041
- api.fireEvent('filter:change', { filterState: mgr.getFilterState(), previousFilterState: prev });
16297
+ api.fireEvent('filter:change', {
16298
+ filterState: mgr.getFilterState(),
16299
+ previousFilterState: prev,
16300
+ });
16042
16301
  }, 'filter');
16043
16302
  store.action('filter:reapply', () => {
16044
16303
  applyFilter();
@@ -16055,7 +16314,10 @@ function createFilterPlugin(options) {
16055
16314
  quickFilterColumns = columns ?? null;
16056
16315
  store.set('filter.quickFilter', { query: quickFilterQuery, columns: quickFilterColumns });
16057
16316
  applyFilter();
16058
- api.fireEvent('filter:change', { filterState: mgr.getFilterState(), previousFilterState: prev });
16317
+ api.fireEvent('filter:change', {
16318
+ filterState: mgr.getFilterState(),
16319
+ previousFilterState: prev,
16320
+ });
16059
16321
  }, 'filter');
16060
16322
  store.action('filter:clearQuickFilter', () => {
16061
16323
  const prev = mgr.getFilterState();
@@ -16064,7 +16326,10 @@ function createFilterPlugin(options) {
16064
16326
  clearQuickFilterCache();
16065
16327
  store.set('filter.quickFilter', { query: '', columns: null });
16066
16328
  applyFilter();
16067
- api.fireEvent('filter:change', { filterState: mgr.getFilterState(), previousFilterState: prev });
16329
+ api.fireEvent('filter:change', {
16330
+ filterState: mgr.getFilterState(),
16331
+ previousFilterState: prev,
16332
+ });
16068
16333
  }, 'filter');
16069
16334
  store.action('filter:setState', (models) => {
16070
16335
  const prev = mgr.getFilterState();
@@ -16073,13 +16338,19 @@ function createFilterPlugin(options) {
16073
16338
  mgr.setColumnFilter(model.column, model.conditions, model.logic ?? 'AND');
16074
16339
  }
16075
16340
  applyFilter();
16076
- api.fireEvent('filter:change', { filterState: mgr.getFilterState(), previousFilterState: prev });
16341
+ api.fireEvent('filter:change', {
16342
+ filterState: mgr.getFilterState(),
16343
+ previousFilterState: prev,
16344
+ });
16077
16345
  }, 'filter');
16078
16346
  store.action('filter:setFieldState', (state) => {
16079
16347
  const prev = mgr.getFilterState();
16080
16348
  mgr.setFieldFilter(state);
16081
16349
  applyFilter();
16082
- api.fireEvent('filter:change', { filterState: mgr.getFilterState(), previousFilterState: prev });
16350
+ api.fireEvent('filter:change', {
16351
+ filterState: mgr.getFilterState(),
16352
+ previousFilterState: prev,
16353
+ });
16083
16354
  }, 'filter');
16084
16355
  persistState();
16085
16356
  api.register('filter', {
@@ -16094,9 +16365,9 @@ function createFilterPlugin(options) {
16094
16365
  getFilterExports: () => mgr.getFilterExport(),
16095
16366
  getQuickFilter: () => store.get('filter.quickFilter'),
16096
16367
  getExpression: () => store.get('filter.expression'),
16097
- getMode: () => getDataMode(),
16368
+ getMode: () => getQueryMode(),
16098
16369
  });
16099
- return { teardown: [() => mgr.destroy()] };
16370
+ return { teardown: [unsubscribeOptions, () => mgr.destroy()] };
16100
16371
  },
16101
16372
  };
16102
16373
  }
@@ -24179,6 +24450,37 @@ function buildQueryState(options = {}) {
24179
24450
  };
24180
24451
  }
24181
24452
 
24453
+ const querySignatureCache = new WeakMap();
24454
+ function getQueryStateSignature(query) {
24455
+ if (!query) {
24456
+ return 'version:1';
24457
+ }
24458
+ const cached = querySignatureCache.get(query);
24459
+ if (cached) {
24460
+ return cached;
24461
+ }
24462
+ const signature = [
24463
+ `version:${query.version ?? 1}`,
24464
+ `sort:${getSortSignature(query.sort)}`,
24465
+ `filter:${getFilterAstSignature(query.filter)}`,
24466
+ `pagination:${getPaginationSignature(query.pagination)}`,
24467
+ ].join('|');
24468
+ querySignatureCache.set(query, signature);
24469
+ return signature;
24470
+ }
24471
+ function getSortSignature(sort) {
24472
+ if (!sort || sort.length === 0) {
24473
+ return '';
24474
+ }
24475
+ return stableHashValue(sort);
24476
+ }
24477
+ function getPaginationSignature(pagination) {
24478
+ if (!pagination) {
24479
+ return '';
24480
+ }
24481
+ return [pagination.page, pagination.pageSize, pagination.offset, pagination.cursor ?? ''].join(':');
24482
+ }
24483
+
24182
24484
  function createAbortError(message) {
24183
24485
  const error = new Error(message);
24184
24486
  error.name = 'AbortError';
@@ -24363,7 +24665,7 @@ class DataManager extends shared.OperationModeManager {
24363
24665
  pagination: buildRangeQueryPaginationState(startRow, endRow),
24364
24666
  });
24365
24667
  // Generate cache key
24366
- const cacheKey = `${startRow}-${endRow}-${JSON.stringify(effectiveQuery)}`;
24668
+ const cacheKey = `${startRow}-${endRow}-${getQueryStateSignature(effectiveQuery)}`;
24367
24669
  // Check cache first
24368
24670
  if (this.cachedRanges.has(cacheKey)) {
24369
24671
  const cachedData = this.cachedRanges.get(cacheKey);
@@ -24682,7 +24984,7 @@ function createDataPlugin(opts) {
24682
24984
  name: 'data',
24683
24985
  phase: 35,
24684
24986
  dependencies: ['core', 'dom', 'rendering'],
24685
- setup(store, api) {
24987
+ setup(store, api, context) {
24686
24988
  const { options, state, container, events, setDataAccessor } = opts;
24687
24989
  const mode = shared.resolveOperationMode({
24688
24990
  mode: options.dataMode,
@@ -24727,6 +25029,9 @@ function createDataPlugin(opts) {
24727
25029
  function getStatus() {
24728
25030
  return store.get('data.status');
24729
25031
  }
25032
+ function delegatesQueryState() {
25033
+ return shouldDelegateQueryState(context.options.dataMode);
25034
+ }
24730
25035
  function setStatus(partial) {
24731
25036
  store.set('data.status', {
24732
25037
  ...getStatus(),
@@ -24844,15 +25149,19 @@ function createDataPlugin(opts) {
24844
25149
  function syncManagerData() {
24845
25150
  const data = dataManager.getData();
24846
25151
  const totalRows = dataManager.getTotalRows();
25152
+ const previousTotalRows = knownTotalRows;
24847
25153
  const { top, left } = state.scrollPosition;
24848
25154
  knownTotalRows = totalRows;
25155
+ context.updateOptions({ rowCount: totalRows });
24849
25156
  state.data = data;
24850
25157
  setDataAccessor(new ArrayAccessor(data));
24851
25158
  store.exec('core:setData', data);
24852
25159
  store.exec('rendering:setRowCount', totalRows);
24853
25160
  store.exec('rendering:updateCanvasSize');
24854
25161
  store.exec('rendering:clearCache');
24855
- store.exec('rendering:renderCells', top, left);
25162
+ if (totalRows !== previousTotalRows) {
25163
+ store.exec('rendering:renderCells', top, left);
25164
+ }
24856
25165
  store.exec('rendering:refresh');
24857
25166
  if (api.getMethod('pagination', 'update')) {
24858
25167
  store.exec('pagination:update');
@@ -24924,8 +25233,12 @@ function createDataPlugin(opts) {
24924
25233
  };
24925
25234
  }
24926
25235
  function buildRequest(range) {
24927
- const sortState = store.get('sort.state') ?? [];
24928
- const filterAst = store.get('filter.ast') ?? null;
25236
+ const sortState = delegatesQueryState()
25237
+ ? (store.get('sort.state') ?? [])
25238
+ : null;
25239
+ const filterAst = delegatesQueryState()
25240
+ ? (store.get('filter.ast') ?? null)
25241
+ : null;
24929
25242
  return {
24930
25243
  startRow: range.startRow,
24931
25244
  endRow: range.endRow,
@@ -25050,12 +25363,12 @@ function createDataPlugin(opts) {
25050
25363
  }
25051
25364
  void ensureVisibleRange('scroll', payload.visibleRange);
25052
25365
  }), events.on('sort:change', () => {
25053
- if (!isRendered()) {
25366
+ if (!isRendered() || !delegatesQueryState()) {
25054
25367
  return;
25055
25368
  }
25056
25369
  void reloadCurrentContext('sort');
25057
25370
  }), events.on('filter:change', () => {
25058
- if (!isRendered()) {
25371
+ if (!isRendered() || !delegatesQueryState()) {
25059
25372
  return;
25060
25373
  }
25061
25374
  void reloadCurrentContext('filter');
@@ -26774,6 +27087,11 @@ class GridFilterUI {
26774
27087
  });
26775
27088
  this.renderFilterChips();
26776
27089
  }
27090
+ setColumns(columns) {
27091
+ this.columns = [...columns];
27092
+ this.closePopup();
27093
+ this.renderFilterChips();
27094
+ }
26777
27095
  openFilterPopup(payload) {
26778
27096
  const dataCol = this.mapVisualToDataCol(payload.columnIndex);
26779
27097
  const columnDef = this.getColumnDef(dataCol);
@@ -26794,9 +27112,9 @@ class GridFilterUI {
26794
27112
  logicRow.innerHTML = `<label>Match</label>`;
26795
27113
  const logicSelect = document.createElement('select');
26796
27114
  logicSelect.className = 'zg-filter-popup__select';
26797
- logicSelect.innerHTML = `
26798
- <option value="AND">AND</option>
26799
- <option value="OR">OR</option>
27115
+ logicSelect.innerHTML = `
27116
+ <option value="AND">AND</option>
27117
+ <option value="OR">OR</option>
26800
27118
  `;
26801
27119
  logicSelect.value = existing?.logic ?? 'AND';
26802
27120
  logicRow.appendChild(logicSelect);
@@ -27358,7 +27676,7 @@ function createFilterUIPlugin(options) {
27358
27676
  name: 'filter-ui',
27359
27677
  phase: 25,
27360
27678
  dependencies: ['filter'],
27361
- setup(store, api) {
27679
+ setup(store, api, context) {
27362
27680
  let filterUI = null;
27363
27681
  store.action('filterUI:attach', (container, events) => {
27364
27682
  if (filterUI) {
@@ -27367,7 +27685,7 @@ function createFilterUIPlugin(options) {
27367
27685
  filterUI = new GridFilterUI({
27368
27686
  container,
27369
27687
  events,
27370
- columns: options.columns,
27688
+ columns: options.getColumns(),
27371
27689
  getFilterState: () => store.get('filter.state'),
27372
27690
  getFilterExpression: () => store.get('filter.expression'),
27373
27691
  setColumnFilter: (column, conditions, logic) => {
@@ -27398,6 +27716,12 @@ function createFilterUIPlugin(options) {
27398
27716
  },
27399
27717
  });
27400
27718
  }, 'filter-ui');
27719
+ const unsubscribeOptions = context.subscribeOptions((change) => {
27720
+ if (!change.changedKeys.includes('columns')) {
27721
+ return;
27722
+ }
27723
+ filterUI?.setColumns(options.getColumns() ?? []);
27724
+ });
27401
27725
  api.register('filterUI', {
27402
27726
  destroy: () => {
27403
27727
  if (filterUI) {
@@ -27408,6 +27732,7 @@ function createFilterUIPlugin(options) {
27408
27732
  });
27409
27733
  return {
27410
27734
  teardown: [
27735
+ unsubscribeOptions,
27411
27736
  () => {
27412
27737
  if (filterUI) {
27413
27738
  filterUI.destroy();
@@ -27740,6 +28065,65 @@ function createLifecyclePlugin(opts) {
27740
28065
  };
27741
28066
  }
27742
28067
 
28068
+ class GridOptionsManager {
28069
+ constructor(options) {
28070
+ this.options = options;
28071
+ this.listeners = new Set();
28072
+ }
28073
+ getOptions() {
28074
+ return this.options;
28075
+ }
28076
+ getOptionsSnapshot() {
28077
+ return { ...this.options };
28078
+ }
28079
+ update(patch) {
28080
+ const changedKeys = Object.keys(patch).filter((key) => {
28081
+ const optionKey = key;
28082
+ return !Object.is(this.options[optionKey], patch[optionKey]);
28083
+ });
28084
+ if (changedKeys.length === 0) {
28085
+ return null;
28086
+ }
28087
+ const previousOptions = this.getOptionsSnapshot();
28088
+ const changedPatch = {};
28089
+ const mutableOptions = this.options;
28090
+ const mutablePatch = changedPatch;
28091
+ for (const key of changedKeys) {
28092
+ const value = patch[key];
28093
+ mutableOptions[key] = value;
28094
+ mutablePatch[key] = value;
28095
+ }
28096
+ const change = {
28097
+ currentOptions: this.options,
28098
+ previousOptions,
28099
+ changedKeys,
28100
+ patch: changedPatch,
28101
+ };
28102
+ for (const listener of this.listeners) {
28103
+ listener(change);
28104
+ }
28105
+ return change;
28106
+ }
28107
+ subscribe(listener) {
28108
+ this.listeners.add(listener);
28109
+ return () => {
28110
+ this.listeners.delete(listener);
28111
+ };
28112
+ }
28113
+ createPluginSetupContext() {
28114
+ return {
28115
+ options: this.options,
28116
+ getOptions: () => this.getOptions(),
28117
+ getOptionsSnapshot: () => this.getOptionsSnapshot(),
28118
+ updateOptions: (patch) => this.update(patch),
28119
+ subscribeOptions: (listener) => this.subscribe(listener),
28120
+ };
28121
+ }
28122
+ destroy() {
28123
+ this.listeners.clear();
28124
+ }
28125
+ }
28126
+
27743
28127
  function createSlimContext(container, options) {
27744
28128
  if (!container)
27745
28129
  throw new Error('Container element is required');
@@ -27761,13 +28145,20 @@ function createSlimContext(container, options) {
27761
28145
  };
27762
28146
  const store = new GridStoreImpl();
27763
28147
  const gridApi = new GridApiImpl(store, events);
27764
- const pluginHost = new PluginHost(store, gridApi);
28148
+ const optionsManager = new GridOptionsManager(normalizedOptions);
28149
+ const pluginHost = new PluginHost(store, gridApi, optionsManager.createPluginSetupContext());
27765
28150
  const pipelineRegistry = new PipelineRegistry();
27766
28151
  pipelineRegistry.registerPhase('sort', 10, 'pipeline.sort');
27767
28152
  pipelineRegistry.registerPhase('filter', 20, 'pipeline.filter');
28153
+ gridApi.register('options', {
28154
+ get: () => optionsManager.getOptions(),
28155
+ getSnapshot: () => optionsManager.getOptionsSnapshot(),
28156
+ subscribe: (listener) => optionsManager.subscribe(listener),
28157
+ });
27768
28158
  return {
27769
28159
  container,
27770
28160
  options: normalizedOptions,
28161
+ optionsManager,
27771
28162
  state,
27772
28163
  events,
27773
28164
  store,
@@ -27779,12 +28170,6 @@ function createSlimContext(container, options) {
27779
28170
  }
27780
28171
  function installAllPlugins(ctx) {
27781
28172
  const { options, events, pluginHost, store } = ctx;
27782
- const resolvedDataMode = shared.resolveOperationMode({
27783
- mode: options.dataMode,
27784
- callback: options.onDataRequest,
27785
- }, {
27786
- rowCount: options.rowCount,
27787
- });
27788
28173
  // Phase 0: Core
27789
28174
  pluginHost.use(createCorePlugin());
27790
28175
  // Phase 5: DOM
@@ -27793,13 +28178,13 @@ function installAllPlugins(ctx) {
27793
28178
  pluginHost.use(createSortPlugin({
27794
28179
  enableMultiSort: true,
27795
28180
  columns: options.columns,
27796
- usePipeline: resolvedDataMode !== 'backend',
28181
+ usePipeline: resolveQueryExecutionMode(options.dataMode) !== 'backend',
27797
28182
  }));
27798
28183
  // Phase 20: Filter
27799
28184
  pluginHost.use(createFilterPlugin({
27800
28185
  colCount: options.colCount,
27801
28186
  columns: options.columns,
27802
- usePipeline: resolvedDataMode !== 'backend',
28187
+ usePipeline: resolveQueryExecutionMode(options.dataMode) !== 'backend',
27803
28188
  }));
27804
28189
  // Phase 30: Rendering
27805
28190
  let columnModel = null;
@@ -27819,7 +28204,9 @@ function installAllPlugins(ctx) {
27819
28204
  state: ctx.state,
27820
28205
  container: ctx.container,
27821
28206
  events,
27822
- setDataAccessor: (da) => { dataAccessor = da; },
28207
+ setDataAccessor: (da) => {
28208
+ dataAccessor = da;
28209
+ },
27823
28210
  }));
27824
28211
  // Phase 40: Selection
27825
28212
  if (options.enableSelection !== false) {
@@ -27866,14 +28253,22 @@ function installAllPlugins(ctx) {
27866
28253
  container: ctx.container,
27867
28254
  events,
27868
28255
  getDataAccessor: () => dataAccessor,
27869
- setColumnModel: (cm) => { columnModel = cm; },
28256
+ setColumnModel: (cm) => {
28257
+ columnModel = cm;
28258
+ },
27870
28259
  }));
27871
28260
  // Setup pipeline computeds
27872
28261
  ctx.pipelineRegistry.setupCoreComputeds(store);
27873
28262
  // Expose closures for late-binding references
27874
- ctx._setColumnModel = (cm) => { columnModel = cm; };
27875
- ctx._setDataAccessor = (da) => { dataAccessor = da; };
27876
- ctx._setSelectionChecker = (sc) => { selectionChecker = sc; };
28263
+ ctx._setColumnModel = (cm) => {
28264
+ columnModel = cm;
28265
+ };
28266
+ ctx._setDataAccessor = (da) => {
28267
+ dataAccessor = da;
28268
+ };
28269
+ ctx._setSelectionChecker = (sc) => {
28270
+ selectionChecker = sc;
28271
+ };
27877
28272
  ctx._getColumnModel = () => columnModel;
27878
28273
  ctx._getDataAccessor = () => dataAccessor;
27879
28274
  // Wire selection checker updates from lifecycle plugin
@@ -27881,9 +28276,12 @@ function installAllPlugins(ctx) {
27881
28276
  selectionChecker = checker;
27882
28277
  });
27883
28278
  // Install filter UI if columns + filter are present
27884
- if (options.columns && options.columns.length > 0 && pluginHost.has('filter') && !pluginHost.has('filter-ui')) {
28279
+ if (options.columns &&
28280
+ options.columns.length > 0 &&
28281
+ pluginHost.has('filter') &&
28282
+ !pluginHost.has('filter-ui')) {
27885
28283
  pluginHost.use(createFilterUIPlugin({
27886
- columns: options.columns,
28284
+ getColumns: () => options.columns,
27887
28285
  getColumnDef: (dataCol) => options.columns?.[dataCol],
27888
28286
  getColumnModel: () => columnModel,
27889
28287
  getContainer: () => ctx.container,
@@ -27917,7 +28315,12 @@ function installAllPlugins(ctx) {
27917
28315
  },
27918
28316
  getDOM: () => {
27919
28317
  const domApi = ctx.gridApi.getMethod('dom', 'getCanvas');
27920
- return domApi ? { canvas: domApi(), updateCanvasSize: (w, h) => store.exec('dom:updateCanvasSize', w, h) } : null;
28318
+ return domApi
28319
+ ? {
28320
+ canvas: domApi(),
28321
+ updateCanvasSize: (w, h) => store.exec('dom:updateCanvasSize', w, h),
28322
+ }
28323
+ : null;
27921
28324
  },
27922
28325
  setData: (data) => {
27923
28326
  store.exec('core:setData', data);
@@ -27927,7 +28330,7 @@ function installAllPlugins(ctx) {
27927
28330
  },
27928
28331
  getState: () => ({ data: ctx.state.data, rowCount: options.rowCount }),
27929
28332
  setRowCount: (count) => {
27930
- options.rowCount = count;
28333
+ ctx.optionsManager.update({ rowCount: count });
27931
28334
  },
27932
28335
  }));
27933
28336
  }
@@ -28759,10 +29162,7 @@ function createSortApi(ctx) {
28759
29162
  const getMode = ctx.gridApi.getMethod('sort', 'getMode');
28760
29163
  if (getMode)
28761
29164
  return getMode();
28762
- const mode = ctx.options.dataMode ?? 'frontend';
28763
- if (mode === 'auto')
28764
- return ctx.options.onDataRequest ? 'backend' : 'frontend';
28765
- return mode;
29165
+ return resolveQueryExecutionMode(ctx.options.dataMode);
28766
29166
  },
28767
29167
  };
28768
29168
  }
@@ -28831,10 +29231,7 @@ function createFilterApi(ctx) {
28831
29231
  const api = ctx.gridApi.getMethod('filter', 'getMode');
28832
29232
  if (api)
28833
29233
  return api();
28834
- const mode = ctx.options.dataMode ?? 'frontend';
28835
- if (mode === 'auto')
28836
- return ctx.options.onDataRequest ? 'backend' : 'frontend';
28837
- return mode;
29234
+ return resolveQueryExecutionMode(ctx.options.dataMode);
28838
29235
  },
28839
29236
  setQuick(query, columns) {
28840
29237
  if (!hasFilterPlugin())
@@ -29244,7 +29641,7 @@ class Grid {
29244
29641
  setData(data) {
29245
29642
  this.ctx.store.exec('core:setData', data);
29246
29643
  this.ctx.state.data = data;
29247
- this.ctx.options.rowCount = data.length;
29644
+ this.ctx.optionsManager.update({ rowCount: data.length });
29248
29645
  const dataAccessor = new ArrayAccessor(data);
29249
29646
  this.ctx._setDataAccessor?.(dataAccessor);
29250
29647
  if (this.ctx.pluginHost.has('infinite-scroll'))
@@ -29312,7 +29709,8 @@ class Grid {
29312
29709
  if (this.ctx.isDestroyed)
29313
29710
  return;
29314
29711
  this._theme.destroyAutoTheme();
29315
- this.ctx.pluginHost.destroy();
29712
+ this.ctx.pluginHost?.destroy?.();
29713
+ this.ctx.optionsManager?.destroy?.();
29316
29714
  this.ctx.container.innerHTML = '';
29317
29715
  this.ctx.container.classList.remove('zg-grid');
29318
29716
  this.ctx.state.data = [];
@@ -29324,7 +29722,9 @@ class Grid {
29324
29722
  this.ctx.store.exec('rendering:registerRenderer', name, renderer);
29325
29723
  }
29326
29724
  updateOptions(options) {
29327
- Object.assign(this.ctx.options, options);
29725
+ const change = this.ctx.optionsManager.update(options);
29726
+ if (!change)
29727
+ return;
29328
29728
  this.ctx.store.exec('rendering:updateScroller');
29329
29729
  this.ctx.store.exec('rendering:refresh');
29330
29730
  }
@@ -29361,7 +29761,12 @@ class Grid {
29361
29761
  getSlidingWindowStats() {
29362
29762
  if (this.ctx.pluginHost.has('infinite-scroll'))
29363
29763
  return this.ctx.store.get('infiniteScroll.stats');
29364
- return { virtualOffset: 0, rowsInMemory: this.ctx.state.data.length, totalRowsLoaded: 0, prunedRows: 0 };
29764
+ return {
29765
+ virtualOffset: 0,
29766
+ rowsInMemory: this.ctx.state.data.length,
29767
+ totalRowsLoaded: 0,
29768
+ prunedRows: 0,
29769
+ };
29365
29770
  }
29366
29771
  // --- Theme ---
29367
29772
  setTheme(nameOrObject) {