@vueye-table/core 3.0.0-alpha.4 → 3.0.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -547,6 +547,16 @@ function createUndoStack(limit = 100) {
547
547
  clear() {
548
548
  past.length = 0;
549
549
  future.length = 0;
550
+ },
551
+ transform(map) {
552
+ const update = (entries) => entries.flatMap((entry) => {
553
+ const value = map(entry);
554
+ return value === void 0 ? [] : [value];
555
+ });
556
+ const nextPast = update(past);
557
+ const nextFuture = update(future);
558
+ past.splice(0, past.length, ...nextPast);
559
+ future.splice(0, future.length, ...nextFuture);
550
560
  }
551
561
  };
552
562
  }
@@ -721,6 +731,14 @@ function createVirtualizer(initialOptions) {
721
731
  notify();
722
732
  },
723
733
  getWindow,
734
+ getItem(index) {
735
+ return Number.isSafeInteger(index) && index >= 0 && index < count ? Object.freeze({
736
+ index,
737
+ key: keys[index],
738
+ start: startOf(index),
739
+ size: sizes[index]
740
+ }) : void 0;
741
+ },
724
742
  getOffsetForIndex(index, align = "auto") {
725
743
  if (count === 0) return 0;
726
744
  if (!Number.isFinite(index) || !Number.isInteger(index)) recover("invalid_virtual_option", "Scroll index must be a finite integer; it is clamped to a valid item.");
@@ -801,6 +819,10 @@ function filterRows(rows, columns, search, filters) {
801
819
  }
802
820
  /** Sort rows by each rule in turn. The sort is stable: ties keep their incoming order. */
803
821
  function sortRows(rows, columns, sorting) {
822
+ if (!sorting.some((rule) => columns.some((column) => column.id === rule.column))) return rows;
823
+ return rows.toSorted(compareRows(columns, sorting));
824
+ }
825
+ function compareRows(columns, sorting) {
804
826
  const byId = new Map(columns.map((column) => [column.id, column]));
805
827
  const rules = sorting.flatMap((rule) => {
806
828
  const column = byId.get(rule.column);
@@ -809,8 +831,7 @@ function sortRows(rows, columns, sorting) {
809
831
  sign: rule.direction === "desc" ? -1 : 1
810
832
  }] : [];
811
833
  });
812
- if (rules.length === 0) return rows;
813
- return rows.toSorted((left, right) => {
834
+ return (left, right) => {
814
835
  for (const { column, sign } of rules) {
815
836
  const leftValue = left.getValue(column.id);
816
837
  const rightValue = right.getValue(column.id);
@@ -820,7 +841,19 @@ function sortRows(rows, columns, sorting) {
820
841
  if (order !== 0) return order;
821
842
  }
822
843
  return 0;
823
- });
844
+ };
845
+ }
846
+ /** Stable merge: an existing row wins a tie against an incoming row. */
847
+ function mergeRows(previous, incoming, columns, sorting) {
848
+ const compare = compareRows(columns, sorting);
849
+ const added = sortRows(incoming, columns, sorting);
850
+ const result = [];
851
+ let left = 0;
852
+ let right = 0;
853
+ while (left < previous.length && right < added.length) result.push(compare(previous[left], added[right]) <= 0 ? previous[left++] : added[right++]);
854
+ while (left < previous.length) result.push(previous[left++]);
855
+ while (right < added.length) result.push(added[right++]);
856
+ return result;
824
857
  }
825
858
  /** The number of pages for a row count. An empty table still has one page. */
826
859
  function countPages(rowCount, pageSize) {
@@ -1128,6 +1161,7 @@ function createEditValidation(host) {
1128
1161
  });
1129
1162
  }
1130
1163
  return {
1164
+ hasPending: (key) => (pending.get(key)?.size ?? 0) > 0,
1131
1165
  cells: () => Object.freeze([...pending.values()].flatMap((row) => [...row.values()])),
1132
1166
  problems: () => new Map([...problems].filter(([, row]) => row.size).map(([key, row]) => [key, readonlyMap(row)])),
1133
1167
  cancel(keys) {
@@ -1328,6 +1362,161 @@ function createPendingChanges() {
1328
1362
  };
1329
1363
  }
1330
1364
 
1365
+ //#endregion
1366
+ //#region src/row-list.ts
1367
+ /** Read-only array views keep append publication independent of existing list length. */
1368
+ function indexOf(property) {
1369
+ if (typeof property !== "string" || !/^(0|[1-9]\d*)$/u.test(property)) return void 0;
1370
+ return Number(property);
1371
+ }
1372
+ function rowList(length, read) {
1373
+ return new Proxy([], {
1374
+ get(target, property, receiver) {
1375
+ if (property === "length") return length;
1376
+ const index = indexOf(property);
1377
+ return index === void 0 ? Reflect.get(target, property, receiver) : index < length ? read(index) : void 0;
1378
+ },
1379
+ has(target, property) {
1380
+ const index = indexOf(property);
1381
+ return index === void 0 ? Reflect.has(target, property) : index < length;
1382
+ },
1383
+ ownKeys: () => [...Array.from({ length }, (_, index) => String(index)), "length"],
1384
+ getOwnPropertyDescriptor(target, property) {
1385
+ if (property === "length") return {
1386
+ ...Reflect.getOwnPropertyDescriptor(target, property),
1387
+ value: length
1388
+ };
1389
+ const index = indexOf(property);
1390
+ return index === void 0 ? Reflect.getOwnPropertyDescriptor(target, property) : index < length ? {
1391
+ configurable: true,
1392
+ enumerable: true,
1393
+ writable: false,
1394
+ value: read(index)
1395
+ } : void 0;
1396
+ },
1397
+ set: () => false,
1398
+ deleteProperty: () => false,
1399
+ defineProperty: () => false,
1400
+ preventExtensions: () => false,
1401
+ setPrototypeOf: () => false
1402
+ });
1403
+ }
1404
+ const stores = /* @__PURE__ */ new WeakMap();
1405
+ function appendList(previous, incoming) {
1406
+ if (!incoming.length) return previous;
1407
+ let store = stores.get(previous);
1408
+ const last = store?.chunks.at(-1);
1409
+ if (!store || last && last.start + last.rows.length !== previous.length) store = { chunks: [{
1410
+ start: 0,
1411
+ rows: previous
1412
+ }] };
1413
+ store.chunks.push({
1414
+ start: previous.length,
1415
+ rows: incoming
1416
+ });
1417
+ const chunks = store.chunks;
1418
+ const result = rowList(previous.length + incoming.length, (index) => {
1419
+ let low = 0;
1420
+ let high = chunks.length - 1;
1421
+ while (low < high) {
1422
+ const mid = Math.ceil((low + high) / 2);
1423
+ if (chunks[mid].start <= index) low = mid;
1424
+ else high = mid - 1;
1425
+ }
1426
+ const chunk = chunks[low];
1427
+ return chunk.rows[index - chunk.start];
1428
+ });
1429
+ stores.set(result, store);
1430
+ return result;
1431
+ }
1432
+ function mapList(rows, map) {
1433
+ const cache = /* @__PURE__ */ new Map();
1434
+ return rowList(rows.length, (index) => {
1435
+ if (!cache.has(index)) cache.set(index, map(rows[index], index));
1436
+ return cache.get(index);
1437
+ });
1438
+ }
1439
+
1440
+ //#endregion
1441
+ //#region src/stream.ts
1442
+ function createStream(host) {
1443
+ let active;
1444
+ return {
1445
+ cancel() {
1446
+ active?.cancel();
1447
+ active = void 0;
1448
+ },
1449
+ async run(source, options = {}) {
1450
+ active?.cancel();
1451
+ const problems = [];
1452
+ const requested = options.batchSize ?? 1e3;
1453
+ const batchSize = Number.isSafeInteger(requested) && requested > 0 ? requested : 1e3;
1454
+ if (batchSize !== requested) problems.push(issue("invalid_stream_option", "batchSize must be a positive safe integer; 1,000 is used."));
1455
+ const requestedCount = options.expectedRowCount;
1456
+ const expected = requestedCount === void 0 || Number.isSafeInteger(requestedCount) && requestedCount >= 0 ? requestedCount : void 0;
1457
+ if (expected !== requestedCount) problems.push(issue("invalid_stream_option", "expectedRowCount must be a non-negative safe integer; it is unknown instead."));
1458
+ let cancelled = false;
1459
+ let wake = () => void 0;
1460
+ const cancellation = new Promise((resolve) => {
1461
+ wake = () => resolve(void 0);
1462
+ });
1463
+ const cancel = () => {
1464
+ cancelled = true;
1465
+ wake();
1466
+ };
1467
+ const run = { cancel };
1468
+ active = run;
1469
+ const signal = options.signal;
1470
+ signal?.addEventListener?.("abort", cancel, { once: true });
1471
+ if (signal?.aborted) run.cancel();
1472
+ let receivedRowCount = 0;
1473
+ let status = "done";
1474
+ let iterator;
1475
+ host.update(cancelled ? "aborted" : "loading", expected, problems, true);
1476
+ try {
1477
+ if (!cancelled) iterator = source[Symbol.asyncIterator]();
1478
+ while (true) {
1479
+ if (cancelled || !iterator) break;
1480
+ const next = await Promise.race([iterator.next(), cancellation]);
1481
+ if (!next || cancelled || signal?.aborted) {
1482
+ run.cancel();
1483
+ break;
1484
+ }
1485
+ if (next.done) break;
1486
+ const rows = Array.isArray(next.value) ? next.value : [next.value];
1487
+ for (let start = 0; start < rows.length; start += batchSize) {
1488
+ await Promise.resolve();
1489
+ if (cancelled || signal?.aborted) {
1490
+ run.cancel();
1491
+ break;
1492
+ }
1493
+ host.update("streaming", expected, problems, false);
1494
+ const result = host.ingest(rows.slice(start, start + batchSize), options.mode ?? "append");
1495
+ receivedRowCount += result.appended + result.replaced;
1496
+ problems.push(...result.issues);
1497
+ }
1498
+ }
1499
+ } catch (error) {
1500
+ status = "error";
1501
+ problems.push(issue("stream_error", error instanceof Error ? error.message : String(error)));
1502
+ } finally {
1503
+ signal?.removeEventListener?.("abort", cancel);
1504
+ if (cancelled) status = "aborted";
1505
+ if (active === run) {
1506
+ active = void 0;
1507
+ host.update(status, expected, problems, true);
1508
+ }
1509
+ if (status !== "done" && iterator?.return) Promise.resolve().then(() => iterator.return()).catch(() => void 0);
1510
+ }
1511
+ return Object.freeze({
1512
+ status,
1513
+ receivedRowCount,
1514
+ issues: Object.freeze([...problems])
1515
+ });
1516
+ }
1517
+ };
1518
+ }
1519
+
1331
1520
  //#endregion
1332
1521
  //#region src/tree.ts
1333
1522
  function buildTree(data, columns, readKey, options, lazy) {
@@ -1590,6 +1779,8 @@ function createTable(options) {
1590
1779
  const optionIssues = [];
1591
1780
  const maxPasteCells = pasteLimit(options.pasteLimit?.maxCells, 1e5, "maxCells", optionIssues);
1592
1781
  const maxPasteLength = pasteLimit(options.pasteLimit?.maxLength, 5e6, "maxLength", optionIssues);
1782
+ const sourceExpected = options.expectedRowCount === void 0 || Number.isSafeInteger(options.expectedRowCount) && options.expectedRowCount >= 0 ? options.expectedRowCount : void 0;
1783
+ if (sourceExpected !== options.expectedRowCount) optionIssues.push(issue("invalid_stream_option", "expectedRowCount must be a non-negative safe integer; it is unknown instead."));
1593
1784
  let data = options.data;
1594
1785
  let definitions = options.columns;
1595
1786
  let rowCount = options.rowCount;
@@ -1602,6 +1793,19 @@ function createTable(options) {
1602
1793
  });
1603
1794
  let state = initialState;
1604
1795
  let snapshot;
1796
+ let loadState = "idle";
1797
+ let expectedRowCount = sourceExpected;
1798
+ let ingestionIssues = [];
1799
+ let streamIssues = [];
1800
+ const streaming = createStream({
1801
+ ingest: (rows, mode) => ingestData(rows, mode),
1802
+ update(next, expected, problems, publish) {
1803
+ loadState = next;
1804
+ expectedRowCount = expected;
1805
+ streamIssues = [...problems];
1806
+ if (publish) notify();
1807
+ }
1808
+ });
1605
1809
  let transaction = false;
1606
1810
  let deferValidationNotify = false;
1607
1811
  let validationNotifyRequested = false;
@@ -1654,20 +1858,24 @@ function createTable(options) {
1654
1858
  issues
1655
1859
  };
1656
1860
  });
1861
+ let primedIndex;
1657
1862
  const indexRows = memo((rows, byId) => {
1863
+ if (primedIndex?.data === rows && primedIndex.columns === byId) return primedIndex.result;
1658
1864
  const byKey = /* @__PURE__ */ new Map();
1659
1865
  const issues = [];
1866
+ const indexed = rows.map((original, index) => {
1867
+ let key = readKey(original, index);
1868
+ if (byKey.has(key)) {
1869
+ issues.push(issue("duplicate_row_key", `Row ${index} repeats the key "${String(key)}"; it is keyed "${String(key)}#${index}" instead.`, { rowKey: key }));
1870
+ key = `${String(key)}#${index}`;
1871
+ }
1872
+ const row = createRow(original, key, index, byId, options.getRowCanExpand?.(original) ?? false);
1873
+ byKey.set(key, row);
1874
+ return row;
1875
+ });
1876
+ stableSource.set(rows, stableRows(rows));
1660
1877
  return {
1661
- rows: rows.map((original, index) => {
1662
- let key = readKey(original, index);
1663
- if (byKey.has(key)) {
1664
- issues.push(issue("duplicate_row_key", `Row ${index} repeats the key "${String(key)}"; it is keyed "${String(key)}#${index}" instead.`, { rowKey: key }));
1665
- key = `${String(key)}#${index}`;
1666
- }
1667
- const row = createRow(original, key, index, byId, options.getRowCanExpand?.(original) ?? false);
1668
- byKey.set(key, row);
1669
- return row;
1670
- }),
1878
+ rows: indexed,
1671
1879
  byKey,
1672
1880
  issues
1673
1881
  };
@@ -1757,13 +1965,24 @@ function createTable(options) {
1757
1965
  visible: ordered.filter((column) => !hidden.has(column.id))
1758
1966
  };
1759
1967
  });
1760
- const filterStage = memo(filterRows);
1761
- const sortStage = memo(sortRows);
1968
+ let primedFilter;
1969
+ let primedSort;
1970
+ const filterStage = memo((rows, columns, search, filters) => primedFilter?.rows === rows && primedFilter.columns === columns && primedFilter.search === search && primedFilter.filters === filters ? primedFilter.result : filterRows(rows, columns, search, filters));
1971
+ const sortStage = memo((rows, columns, sorting) => primedSort?.rows === rows && primedSort.columns === columns && primedSort.sorting === sorting ? primedSort.result : sortRows(rows, columns, sorting));
1972
+ const rowMembership = /* @__PURE__ */ new WeakMap();
1973
+ function membership(rows) {
1974
+ let keys = rowMembership.get(rows);
1975
+ if (!keys) {
1976
+ keys = new Set(rows.map((row) => row.key));
1977
+ rowMembership.set(rows, keys);
1978
+ }
1979
+ return keys;
1980
+ }
1762
1981
  const selectedSet = memo((selection) => new Set(selection));
1763
1982
  const expansionStage = memo((indexed, expanded, selection) => {
1764
1983
  const keys = expanded === true ? void 0 : new Set(expanded);
1765
1984
  const selected = new Set(selection);
1766
- const rows = indexed.rows.map((row) => {
1985
+ const rows = mapList(indexed.rows, (row) => {
1767
1986
  const isExpanded = row.canExpand && (expanded === true || (keys?.has(row.key) ?? false));
1768
1987
  return isExpanded || selected.has(row.key) ? Object.freeze({
1769
1988
  ...row,
@@ -1773,10 +1992,13 @@ function createTable(options) {
1773
1992
  });
1774
1993
  return {
1775
1994
  rows,
1776
- byKey: new Map(rows.map((row) => [row.key, row]))
1995
+ byKey: { get(key) {
1996
+ const row = indexed.byKey.get(key);
1997
+ return row && row.index < rows.length ? rows[row.index] : void 0;
1998
+ } }
1777
1999
  };
1778
2000
  });
1779
- const expandedOrder = memo((rows, byKey) => rows.map((row) => byKey.get(row.key)));
2001
+ const expandedOrder = memo((rows, byKey) => mapList(rows, (row) => byKey.get(row.key)));
1780
2002
  function stateIssues(byId) {
1781
2003
  const issues = [];
1782
2004
  if (expandedIssue) issues.push(expandedIssue);
@@ -1809,6 +2031,8 @@ function createTable(options) {
1809
2031
  const ordered = orderColumns(columns, state.columnOrder, state.hiddenColumns);
1810
2032
  const issues = [
1811
2033
  ...optionIssues,
2034
+ ...ingestionIssues,
2035
+ ...streamIssues,
1812
2036
  ...columnIssues,
1813
2037
  ...indexed.issues,
1814
2038
  ...stateIssues(byId),
@@ -1910,24 +2134,35 @@ function createTable(options) {
1910
2134
  return result;
1911
2135
  };
1912
2136
  const shared = rows === processedRows;
1913
- processedRows = processedRows.map(project);
2137
+ processedRows = mapList(processedRows, project);
1914
2138
  rows = shared ? processedRows : rows.map(project);
1915
2139
  }
1916
2140
  const selected = selectedSet(state.selection);
1917
- const pageSelected = rows.filter((row) => selected.has(row.key)).length;
2141
+ const allData = model ? model.byKey : indexed.byKey;
2142
+ const allProcessedKeys = model ? void 0 : manual ? void 0 : membership(filterStage(indexed.rows, columns, state.search, state.filters));
2143
+ const selectedInProcessed = state.selection.filter((key) => allData.has(key) && (!allProcessedKeys || allProcessedKeys.has(key))).length;
2144
+ const pageSelected = rows === processedRows ? selectedInProcessed : rows.filter((row) => selected.has(row.key)).length;
1918
2145
  const scopeRows = selectScope === "page" || manual ? rows : processedRows;
1919
- const scopeSelected = scopeRows === rows ? pageSelected : scopeRows.filter((row) => selected.has(row.key)).length;
2146
+ const scopeSelected = scopeRows === rows ? pageSelected : model ? scopeRows.filter((row) => selected.has(row.key)).length : selectedInProcessed;
1920
2147
  const pageStart = rows.length === 0 ? 0 : (page - 1) * pageSize + 1;
1921
2148
  const sortIndex = new Map(state.sorting.map((rule, priority) => [rule.column, {
1922
2149
  direction: rule.direction,
1923
2150
  priority
1924
2151
  }]));
1925
2152
  return Object.freeze({
2153
+ loadState,
2154
+ loadedRowCount: indexed.rows.length,
2155
+ expectedRowCount,
1926
2156
  state,
1927
2157
  columns: ordered.visible,
1928
2158
  allColumns: ordered.all,
1929
2159
  rows,
1930
- renderItems: Object.freeze(rows.flatMap((row, rowIndex) => {
2160
+ renderItems: !model && state.expanded !== true && state.expanded.length === 0 ? mapList(rows, (row, rowIndex) => Object.freeze({
2161
+ kind: "row",
2162
+ key: getRowItemKey(row.key),
2163
+ row,
2164
+ rowIndex
2165
+ })) : Object.freeze(rows.flatMap((row, rowIndex) => {
1931
2166
  const item = Object.freeze({
1932
2167
  kind: "row",
1933
2168
  key: getRowItemKey(row.key),
@@ -2688,12 +2923,341 @@ function createTable(options) {
2688
2923
  value: direction === "undo" ? change.previous : change.value
2689
2924
  })), false);
2690
2925
  }
2926
+ function ingestionResult(appended, replaced, removed, problems) {
2927
+ return Object.freeze({
2928
+ status: appended + replaced + removed ? problems.length ? "partial" : "applied" : problems.length ? "rejected" : "unchanged",
2929
+ appended,
2930
+ replaced,
2931
+ removed,
2932
+ issues: Object.freeze([...problems])
2933
+ });
2934
+ }
2935
+ const stableSource = /* @__PURE__ */ new WeakMap();
2936
+ function ingestionKeys(rows) {
2937
+ let valid = stableSource.get(rows);
2938
+ if (valid === void 0) {
2939
+ valid = stableRows(rows);
2940
+ stableSource.set(rows, valid);
2941
+ }
2942
+ return valid;
2943
+ }
2944
+ function isProtectedSource(key) {
2945
+ return pendingChanges.entries.has(key) || validation.hasPending(key);
2946
+ }
2947
+ function affectedBranch(model, key) {
2948
+ const result = [key];
2949
+ const node = model?.byKey.get(key);
2950
+ if (!node) return result;
2951
+ let parent = node.parent;
2952
+ while (parent) {
2953
+ result.push(parent.row.key);
2954
+ parent = parent.parent;
2955
+ }
2956
+ const stack = [...node.children];
2957
+ while (stack.length) {
2958
+ const child = stack.pop();
2959
+ result.push(child.row.key);
2960
+ for (const descendant of child.children) stack.push(descendant);
2961
+ }
2962
+ return result;
2963
+ }
2964
+ function transformScene(source, updates, removed) {
2965
+ const seen = /* @__PURE__ */ new Set();
2966
+ const replace = (rows) => rows.flatMap((row, index) => {
2967
+ const key = readKey(row, index);
2968
+ seen.add(key);
2969
+ return removed.has(key) ? [] : [updates.get(key) ?? row];
2970
+ });
2971
+ const cache = /* @__PURE__ */ new Map();
2972
+ for (const [key, children] of source.lazy) if (!removed.has(key)) cache.set(key, replace(children));
2973
+ let next;
2974
+ if (!options.getChildren) next = replace(source.data);
2975
+ else {
2976
+ const model = buildTree(source.data, resolveColumns(definitions).byId, readKey, options, source.lazy);
2977
+ const replacements = /* @__PURE__ */ new Map();
2978
+ for (const node of model.nodes.toReversed()) {
2979
+ const key = node.row.key;
2980
+ seen.add(key);
2981
+ if (removed.has(key)) continue;
2982
+ let row = updates.get(key) ?? node.row.original;
2983
+ const supplied = updates.has(key) ? options.getChildren(row) : void 0;
2984
+ if (supplied !== void 0) cache.delete(key);
2985
+ const children = supplied ?? node.children.filter((child) => !removed.has(child.row.key)).map((child) => replacements.get(child) ?? child.row.original);
2986
+ const old = options.getChildren(row) ?? node.children.map((child) => child.row.original);
2987
+ if (supplied === void 0 && (updates.has(key) && node.children.length > 0 && !cache.has(key) || children.length !== old.length || children.some((child, index) => child !== old[index]))) {
2988
+ if (!options.setChildren) throw new Error("Updating nested source children requires setChildren.");
2989
+ row = options.setChildren(row, children);
2990
+ if (cache.has(key)) cache.set(key, children);
2991
+ }
2992
+ replacements.set(node, row);
2993
+ }
2994
+ const roots = new Map(model.roots.filter((node) => node.writable).map((node) => [node.sourceIndex, node]));
2995
+ next = source.data.flatMap((row, index) => {
2996
+ const node = roots.get(index);
2997
+ return node && removed.has(node.row.key) ? [] : [node ? replacements.get(node) ?? row : row];
2998
+ });
2999
+ }
3000
+ const additions = [...updates].filter(([key]) => !seen.has(key) && !removed.has(key)).map(([, row]) => row);
3001
+ return {
3002
+ data: appendList(next, additions),
3003
+ lazy: cache
3004
+ };
3005
+ }
3006
+ function rebaseBatches(transform, removed) {
3007
+ undoStack.transform((entry) => {
3008
+ if (entry.kind === "cells") {
3009
+ if (!removed.size) return entry;
3010
+ const changes = entry.changes.filter((change) => !removed.has(change.rowKey));
3011
+ return changes.length ? {
3012
+ ...entry,
3013
+ changes
3014
+ } : void 0;
3015
+ }
3016
+ const keys = entry.keys.filter((key) => !removed.has(key));
3017
+ return keys.length ? {
3018
+ ...entry,
3019
+ keys,
3020
+ before: transform(entry.before),
3021
+ after: transform(entry.after)
3022
+ } : void 0;
3023
+ });
3024
+ }
3025
+ function newTreeIssues(previous, next) {
3026
+ const counts = /* @__PURE__ */ new Map();
3027
+ for (const problem of previous) {
3028
+ const key = JSON.stringify([problem.code, problem.rowKey]);
3029
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3030
+ }
3031
+ return next.filter((problem) => {
3032
+ const key = JSON.stringify([problem.code, problem.rowKey]);
3033
+ const remaining = counts.get(key) ?? 0;
3034
+ if (!remaining) return true;
3035
+ counts.set(key, remaining - 1);
3036
+ return false;
3037
+ });
3038
+ }
3039
+ function ingestData(incoming, mode) {
3040
+ const problems = [];
3041
+ const reject = (problem) => {
3042
+ ingestionIssues = [problem];
3043
+ notify();
3044
+ return ingestionResult(0, 0, 0, ingestionIssues);
3045
+ };
3046
+ if (destroyed) return reject(issue("invalid_ingestion", "A disposed table cannot receive source data."));
3047
+ if (!incoming.length) return ingestionResult(0, 0, 0, []);
3048
+ try {
3049
+ if (!ingestionKeys(data) || !stableRows(incoming)) return reject(issue("invalid_ingestion", "Source ingestion requires stable rowKey values or ids on every row."));
3050
+ } catch (error) {
3051
+ return reject(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
3052
+ }
3053
+ const previous = sourceRows();
3054
+ const { byId, columns } = resolveColumns(definitions);
3055
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
3056
+ const seenBatch = /* @__PURE__ */ new Set();
3057
+ const updates = /* @__PURE__ */ new Map();
3058
+ for (const row of incoming) try {
3059
+ const key = readKey(row, previous.rows.length + updates.size);
3060
+ if (typeof key !== "string" && (typeof key !== "number" || !Number.isFinite(key))) {
3061
+ problems.push(issue("invalid_ingestion", "An incoming row key must be a string or finite number."));
3062
+ continue;
3063
+ }
3064
+ const existing = previous.byKey.get(key);
3065
+ if (seenBatch.has(key) || mode === "append" && existing) {
3066
+ problems.push(issue("duplicate_row_key", "An incoming source row repeats an existing key and is refused.", { rowKey: key }));
3067
+ continue;
3068
+ }
3069
+ seenBatch.add(key);
3070
+ const parent = options.getParentKey?.(row);
3071
+ const affected = affectedBranch(model, key);
3072
+ if (parent !== void 0 && parent !== null) affected.push(...affectedBranch(model, parent));
3073
+ if (affected.some(isProtectedSource)) {
3074
+ problems.push(issue("ingestion_conflict", "Incoming source data would overwrite a local change or pending validation; retry after save or revert.", { rowKey: key }));
3075
+ continue;
3076
+ }
3077
+ const node = model?.byKey.get(key);
3078
+ if (node && (!node.writable || node.lazy && options.getParentKey && options.getParentKey(row) !== node.parent?.row.key)) {
3079
+ problems.push(issue("invalid_ingestion", "This recovered or lazy tree row has no writable source position.", { rowKey: key }));
3080
+ continue;
3081
+ }
3082
+ if (existing?.original !== row) updates.set(key, row);
3083
+ } catch (error) {
3084
+ problems.push(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
3085
+ }
3086
+ if (!updates.size) {
3087
+ ingestionIssues = problems;
3088
+ notify();
3089
+ return ingestionResult(0, 0, 0, problems);
3090
+ }
3091
+ const replaced = [...updates.keys()].filter((key) => previous.byKey.has(key)).length;
3092
+ const appended = updates.size - replaced;
3093
+ const previousState = state;
3094
+ try {
3095
+ if (!model && !replaced) {
3096
+ const added = Object.freeze([...updates.values()]);
3097
+ const indexed = added.map((row, offset) => createRow(row, readKey(row, previous.rows.length + offset), previous.rows.length + offset, byId, options.getRowCanExpand?.(row) ?? false));
3098
+ const filtered = manual ? previous.rows : filterStage(previous.rows, columns, state.search, state.filters);
3099
+ const addedFiltered = manual ? indexed : filterRows(indexed, columns, state.search, state.filters);
3100
+ const allRows = appendList(previous.rows, indexed);
3101
+ const allFiltered = appendList(filtered, addedFiltered);
3102
+ const keys = membership(filtered);
3103
+ for (const row of addedFiltered) keys.add(row.key);
3104
+ rowMembership.set(allFiltered, keys);
3105
+ const sorted = manual || !state.sorting.some((rule) => columns.some((column) => column.id === rule.column)) ? allFiltered : mergeRows(sortStage(filtered, columns, state.sorting), addedFiltered, columns, state.sorting);
3106
+ const next = appendList(data, added);
3107
+ rebaseBatches((source) => ({
3108
+ ...source,
3109
+ data: appendList(source.data, added)
3110
+ }), /* @__PURE__ */ new Set());
3111
+ for (const row of indexed) previous.byKey.set(row.key, row);
3112
+ primedIndex = {
3113
+ data: next,
3114
+ columns: byId,
3115
+ result: {
3116
+ rows: allRows,
3117
+ byKey: previous.byKey,
3118
+ issues: previous.issues
3119
+ }
3120
+ };
3121
+ primedFilter = {
3122
+ rows: allRows,
3123
+ columns,
3124
+ search: state.search,
3125
+ filters: state.filters,
3126
+ result: allFiltered
3127
+ };
3128
+ primedSort = {
3129
+ rows: allFiltered,
3130
+ columns,
3131
+ sorting: state.sorting,
3132
+ result: sorted
3133
+ };
3134
+ stableSource.set(next, true);
3135
+ data = next;
3136
+ } else {
3137
+ const next = transformScene(scene(), updates, /* @__PURE__ */ new Set());
3138
+ const candidate = treeEnabled ? buildTree(next.data, byId, readKey, options, next.lazy) : indexRows(next.data, byId);
3139
+ const fresh = newTreeIssues(previous.issues, candidate.issues);
3140
+ if (fresh.length || !stableRows(candidate.rows.map((row) => row.original))) {
3141
+ problems.push(...fresh, ...!stableRows(candidate.rows.map((row) => row.original)) ? [issue("invalid_ingestion", "Every incoming descendant needs a stable key.")] : []);
3142
+ ingestionIssues = problems;
3143
+ notify();
3144
+ return ingestionResult(0, 0, 0, problems);
3145
+ }
3146
+ const nextKeys = new Set(candidate.rows.map((row) => row.key));
3147
+ const removedKeys = new Set(previous.rows.filter((row) => !nextKeys.has(row.key)).map((row) => row.key));
3148
+ rebaseBatches((source) => transformScene(source, updates, removedKeys), removedKeys);
3149
+ data = next.data;
3150
+ lazyChildren.clear();
3151
+ for (const [key, rows] of next.lazy) lazyChildren.set(key, rows);
3152
+ lazyVersion++;
3153
+ for (const key of [...updates.keys(), ...removedKeys]) {
3154
+ pendingLoads.get(key)?.abort();
3155
+ pendingLoads.delete(key);
3156
+ childStatuses.delete(key);
3157
+ loadIssues.delete(key);
3158
+ loadValidationIssues.delete(key);
3159
+ }
3160
+ validation.cancel([...removedKeys]);
3161
+ const selection = state.selection.filter((key) => !removedKeys.has(key));
3162
+ state = mergeState(state, {
3163
+ selection: selection.length === state.selection.length ? state.selection : selection,
3164
+ expanded: state.expanded === true ? true : state.expanded.filter((key) => !removedKeys.has(key))
3165
+ });
3166
+ }
3167
+ validation.cancel([...updates.keys()]);
3168
+ ingestionIssues = problems;
3169
+ } catch (error) {
3170
+ return reject(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
3171
+ }
3172
+ if (state !== previousState) options.onStateChange?.(state, previousState);
3173
+ options.onDataChange?.(data, []);
3174
+ notify();
3175
+ startVisibleLoads();
3176
+ return ingestionResult(appended, replaced, 0, problems);
3177
+ }
3178
+ function removeData(keys) {
3179
+ const previousState = state;
3180
+ const problems = [];
3181
+ const removed = /* @__PURE__ */ new Set();
3182
+ const previous = sourceRows();
3183
+ const model = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion) : void 0;
3184
+ if (destroyed || !ingestionKeys(data)) problems.push(issue("invalid_ingestion", "Source removal requires a live table and stable row keys."));
3185
+ else for (const key of new Set(keys)) {
3186
+ if (!previous.byKey.has(key)) {
3187
+ problems.push(issue("unknown_row", "The source row to remove does not exist.", { rowKey: key }));
3188
+ continue;
3189
+ }
3190
+ if (affectedBranch(model, key).some(isProtectedSource)) {
3191
+ problems.push(issue("ingestion_conflict", "Source removal would discard local changes or pending validation.", { rowKey: key }));
3192
+ continue;
3193
+ }
3194
+ const node = model?.byKey.get(key);
3195
+ if (node && !node.writable) {
3196
+ problems.push(issue("invalid_ingestion", "A recovered tree row has no writable source position.", { rowKey: key }));
3197
+ continue;
3198
+ }
3199
+ removed.add(key);
3200
+ const stack = node ? [...node.children] : [];
3201
+ while (stack.length) {
3202
+ const child = stack.pop();
3203
+ removed.add(child.row.key);
3204
+ for (const descendant of child.children) stack.push(descendant);
3205
+ }
3206
+ }
3207
+ try {
3208
+ if (removed.size) {
3209
+ const next = transformScene(scene(), /* @__PURE__ */ new Map(), removed);
3210
+ rebaseBatches((source) => transformScene(source, /* @__PURE__ */ new Map(), removed), removed);
3211
+ data = next.data;
3212
+ lazyChildren.clear();
3213
+ for (const [key, rows] of next.lazy) lazyChildren.set(key, rows);
3214
+ lazyVersion++;
3215
+ validation.cancel([...removed]);
3216
+ for (const key of removed) {
3217
+ pendingLoads.get(key)?.abort();
3218
+ pendingLoads.delete(key);
3219
+ childStatuses.delete(key);
3220
+ loadIssues.delete(key);
3221
+ loadValidationIssues.delete(key);
3222
+ }
3223
+ const selection = state.selection.filter((key) => !removed.has(key));
3224
+ state = mergeState(state, {
3225
+ selection: selection.length === state.selection.length ? state.selection : selection,
3226
+ expanded: state.expanded === true ? true : state.expanded.filter((key) => !removed.has(key))
3227
+ });
3228
+ }
3229
+ ingestionIssues = problems;
3230
+ } catch (error) {
3231
+ ingestionIssues = [issue("invalid_ingestion", error instanceof Error ? error.message : String(error))];
3232
+ notify();
3233
+ return ingestionResult(0, 0, 0, ingestionIssues);
3234
+ }
3235
+ if (state !== previousState) options.onStateChange?.(state, previousState);
3236
+ if (removed.size) options.onDataChange?.(data, []);
3237
+ notify();
3238
+ startVisibleLoads();
3239
+ return ingestionResult(0, 0, removed.size, problems);
3240
+ }
2691
3241
  if (expandMode === "single") {
2692
3242
  initialState = mergeState(initialState, { expanded: canonicalExpanded(initialState.expanded) });
2693
3243
  state = initialState;
2694
3244
  snapshot = void 0;
2695
3245
  }
2696
3246
  const table = {
3247
+ appendData: (rows) => ingestData(rows, "append"),
3248
+ upsertData: (rows) => ingestData(rows, "upsert"),
3249
+ removeData,
3250
+ stream(source, streamOptions) {
3251
+ if (destroyed) return Promise.resolve(Object.freeze({
3252
+ status: "aborted",
3253
+ receivedRowCount: 0,
3254
+ issues: Object.freeze([issue("invalid_ingestion", "A disposed table cannot start a source.")])
3255
+ }));
3256
+ return streaming.run(source, {
3257
+ expectedRowCount: sourceExpected,
3258
+ ...streamOptions
3259
+ });
3260
+ },
2697
3261
  insertRows,
2698
3262
  removeRows,
2699
3263
  revert,
@@ -2725,6 +3289,11 @@ function createTable(options) {
2725
3289
  setData(next) {
2726
3290
  if (next === data) return;
2727
3291
  validation.cancel();
3292
+ streaming.cancel();
3293
+ loadState = "idle";
3294
+ streamIssues = [];
3295
+ ingestionIssues = [];
3296
+ expectedRowCount = sourceExpected;
2728
3297
  pendingChanges.entries.clear();
2729
3298
  abortLoads(true);
2730
3299
  lazyChildren.clear();
@@ -2959,6 +3528,7 @@ function createTable(options) {
2959
3528
  },
2960
3529
  destroy() {
2961
3530
  destroyed = true;
3531
+ streaming.cancel();
2962
3532
  validation.cancel();
2963
3533
  abortLoads(true);
2964
3534
  listeners.clear();