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