@vueye-table/core 3.0.0-alpha.3 → 3.0.0-alpha.5

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) {
@@ -898,6 +931,8 @@ function createRow(original, key, index, columns, canExpand = false) {
898
931
  childCount: 0,
899
932
  childStatus: "loaded",
900
933
  selection: "none",
934
+ isDirty: false,
935
+ cellIssues: void 0,
901
936
  getValue,
902
937
  getDisplay(columnId) {
903
938
  const cached = displays.get(columnId);
@@ -966,6 +1001,522 @@ function freezeState(state, previous) {
966
1001
  });
967
1002
  }
968
1003
 
1004
+ //#endregion
1005
+ //#region src/validation.ts
1006
+ function isPromise(value) {
1007
+ return typeof value === "object" && value !== null && "then" in value;
1008
+ }
1009
+ /** Validators receive values; their messages remain plain text. */
1010
+ function runValidation(validate, rowKey, column) {
1011
+ const fail = (message, validationCode) => issue("validation_failed", message, {
1012
+ rowKey,
1013
+ column,
1014
+ validationCode
1015
+ });
1016
+ const read = (result) => {
1017
+ if (result === true) return void 0;
1018
+ if (typeof result === "string") return fail(result);
1019
+ if (result && typeof result.message === "string") return fail(result.message, result.code);
1020
+ return fail("The validator returned an invalid result.");
1021
+ };
1022
+ const thrown = (error) => fail(error instanceof Error ? error.message : String(error));
1023
+ try {
1024
+ const result = validate();
1025
+ return isPromise(result) ? Promise.resolve(result).then(read, thrown) : read(result);
1026
+ } catch (error) {
1027
+ return thrown(error);
1028
+ }
1029
+ }
1030
+ function editorFailure(spec, value) {
1031
+ if (!spec) return void 0;
1032
+ if (spec.maxLength !== void 0 && (!Number.isSafeInteger(spec.maxLength) || spec.maxLength < 0)) return "Editor maxLength must be a non-negative safe integer.";
1033
+ if (spec.min !== void 0 && !Number.isFinite(spec.min) || spec.max !== void 0 && !Number.isFinite(spec.max) || spec.min !== void 0 && spec.max !== void 0 && spec.min > spec.max) return "Editor bounds must be finite and ordered.";
1034
+ if (spec.options && !spec.options.some((option) => Object.is(option, value))) return "Choose one of the allowed values.";
1035
+ if (spec.maxLength !== void 0 && typeof value === "string" && value.length > spec.maxLength) return `Use at most ${spec.maxLength} characters.`;
1036
+ if (spec.min !== void 0 || spec.max !== void 0) {
1037
+ const number = value instanceof Date ? value.getTime() : value;
1038
+ if (typeof number !== "number" || !Number.isFinite(number)) return "Enter a finite numeric value.";
1039
+ if (spec.min !== void 0 && number < spec.min) return `Use a value of at least ${spec.min}.`;
1040
+ if (spec.max !== void 0 && number > spec.max) return `Use a value of at most ${spec.max}.`;
1041
+ }
1042
+ if (spec.pattern !== void 0) try {
1043
+ if (typeof value !== "string" || !new RegExp(spec.pattern, "u").test(value)) return "The value does not match the required pattern.";
1044
+ } catch {
1045
+ return "The editor pattern is invalid.";
1046
+ }
1047
+ }
1048
+
1049
+ //#endregion
1050
+ //#region src/edit-validation.ts
1051
+ function readonlyMap(source) {
1052
+ const copy = new Map(source);
1053
+ const result = {
1054
+ size: copy.size,
1055
+ get: (key) => copy.get(key),
1056
+ has: (key) => copy.has(key),
1057
+ entries: () => copy.entries(),
1058
+ keys: () => copy.keys(),
1059
+ values: () => copy.values(),
1060
+ [Symbol.iterator]: () => copy[Symbol.iterator](),
1061
+ forEach(callback, thisArg) {
1062
+ copy.forEach((value, key) => callback.call(thisArg, value, key, result));
1063
+ }
1064
+ };
1065
+ return Object.freeze(result);
1066
+ }
1067
+ /** All promises in one edit settle together. Tokens fence every cell against stale results. */
1068
+ function createEditValidation(host) {
1069
+ const tokens = /* @__PURE__ */ new Map();
1070
+ const pending = /* @__PURE__ */ new Map();
1071
+ const problems = /* @__PURE__ */ new Map();
1072
+ const current = (candidate) => tokens.get(candidate.edit.rowKey)?.get(candidate.edit.column) === candidate.token;
1073
+ const tokenFor = (rowKey, column) => {
1074
+ const row = tokens.get(rowKey) ?? /* @__PURE__ */ new Map();
1075
+ const token = {};
1076
+ row.set(column, token);
1077
+ tokens.set(rowKey, row);
1078
+ pending.get(rowKey)?.delete(column);
1079
+ problems.get(rowKey)?.delete(column);
1080
+ return token;
1081
+ };
1082
+ const setIssues = (issues) => {
1083
+ for (const problem of issues) {
1084
+ if (problem.rowKey === void 0 || problem.column === void 0) continue;
1085
+ const row = problems.get(problem.rowKey) ?? /* @__PURE__ */ new Map();
1086
+ row.set(problem.column, problem);
1087
+ problems.set(problem.rowKey, row);
1088
+ }
1089
+ };
1090
+ function rowValidation(candidates, failures) {
1091
+ if (!host.validateRow) return [...candidates];
1092
+ const rows = /* @__PURE__ */ new Map();
1093
+ for (const candidate of candidates) {
1094
+ const group = rows.get(candidate.edit.rowKey) ?? [];
1095
+ group.push(candidate);
1096
+ rows.set(candidate.edit.rowKey, group);
1097
+ }
1098
+ const results = [];
1099
+ for (const [key, group] of rows) {
1100
+ const previous = group[0].original;
1101
+ let next = previous;
1102
+ try {
1103
+ for (const candidate of group) next = candidate.column.setValue(next, candidate.edit.value) ?? next;
1104
+ } catch (error) {
1105
+ for (const candidate of group) failures.push(issue("validation_failed", String(error), {
1106
+ rowKey: key,
1107
+ column: candidate.edit.column
1108
+ }));
1109
+ continue;
1110
+ }
1111
+ const validation = runValidation(() => host.validateRow(next, previous), key);
1112
+ const accept = (problem) => {
1113
+ if (!problem) return group;
1114
+ for (const candidate of group) if (current(candidate)) failures.push(Object.freeze({
1115
+ ...problem,
1116
+ column: candidate.edit.column
1117
+ }));
1118
+ return [];
1119
+ };
1120
+ results.push(isPromise(validation) ? validation.then(accept) : accept(validation));
1121
+ }
1122
+ return results.some(isPromise) ? Promise.all(results.map((result) => Promise.resolve(result))).then((groups) => groups.flat()) : results.flat();
1123
+ }
1124
+ function settle(accepted, all, failures, asynchronous) {
1125
+ return host.batch(() => {
1126
+ const live = accepted.filter(current);
1127
+ const liveKeys = new Set(live);
1128
+ const active = all.filter(current);
1129
+ for (const candidate of active) {
1130
+ const key = candidate.edit.rowKey;
1131
+ pending.get(key)?.delete(candidate.edit.column);
1132
+ if (pending.get(key)?.size === 0) pending.delete(key);
1133
+ }
1134
+ const visibleFailures = failures;
1135
+ setIssues(visibleFailures);
1136
+ let result;
1137
+ if (asynchronous && host.optimistic) {
1138
+ const rollback = active.filter((candidate) => !liveKeys.has(candidate)).map((candidate) => ({
1139
+ ...candidate.edit,
1140
+ value: candidate.previous
1141
+ }));
1142
+ result = host.apply(rollback, false, visibleFailures);
1143
+ const changes = live.map((candidate) => Object.freeze({
1144
+ rowKey: candidate.edit.rowKey,
1145
+ column: candidate.edit.column,
1146
+ previous: candidate.previous,
1147
+ value: candidate.edit.value,
1148
+ row: host.snapshot().getRow(candidate.edit.rowKey).original
1149
+ }));
1150
+ if (changes.length) host.record(changes);
1151
+ result = Object.freeze({
1152
+ ...result,
1153
+ changes: Object.freeze(changes),
1154
+ status: changes.length ? visibleFailures.length ? "partial" : "applied" : visibleFailures.length ? "rejected" : "unchanged"
1155
+ });
1156
+ } else result = host.apply(live.map((candidate) => candidate.edit), true, visibleFailures);
1157
+ setIssues(result.issues);
1158
+ if (!result.changes.length && (all.length || result.issues.length) || asynchronous && host.optimistic) host.notify();
1159
+ if (asynchronous && !result.issues.length && active.length) host.onIssues([]);
1160
+ return result;
1161
+ });
1162
+ }
1163
+ return {
1164
+ hasPending: (key) => (pending.get(key)?.size ?? 0) > 0,
1165
+ cells: () => Object.freeze([...pending.values()].flatMap((row) => [...row.values()])),
1166
+ problems: () => new Map([...problems].filter(([, row]) => row.size).map(([key, row]) => [key, readonlyMap(row)])),
1167
+ cancel(keys) {
1168
+ if (!keys) {
1169
+ tokens.clear();
1170
+ pending.clear();
1171
+ problems.clear();
1172
+ } else for (const key of keys) {
1173
+ tokens.delete(key);
1174
+ pending.delete(key);
1175
+ problems.delete(key);
1176
+ }
1177
+ },
1178
+ edit(edits, recovered = []) {
1179
+ const metadataChanged = edits.some((edit) => pending.get(edit.rowKey)?.has(edit.column) || problems.get(edit.rowKey)?.has(edit.column) || host.validateRow && pending.get(edit.rowKey)?.size);
1180
+ const snapshot = host.snapshot();
1181
+ const failures = [...recovered];
1182
+ const candidates = [];
1183
+ const drafts = /* @__PURE__ */ new Map();
1184
+ const samples = /* @__PURE__ */ new Map();
1185
+ if (host.validateRow) for (const key of new Set(edits.map((edit) => edit.rowKey))) {
1186
+ tokens.delete(key);
1187
+ pending.delete(key);
1188
+ }
1189
+ for (const edit of edits) {
1190
+ const token = tokenFor(edit.rowKey, edit.column);
1191
+ const row = snapshot.getRow(edit.rowKey);
1192
+ const column = snapshot.getColumn(edit.column);
1193
+ const fail = (code, message) => {
1194
+ failures.push(issue(code, message, {
1195
+ rowKey: edit.rowKey,
1196
+ column: edit.column
1197
+ }));
1198
+ };
1199
+ if (!isSafePath(edit.column)) {
1200
+ fail("unsafe_path", "This path addresses a prototype and cannot be written.");
1201
+ continue;
1202
+ }
1203
+ if (!row) {
1204
+ fail("unknown_row", `No row has the key "${String(edit.rowKey)}".`);
1205
+ continue;
1206
+ }
1207
+ if (!column) {
1208
+ fail("unknown_column", `No column has the id "${edit.column}".`);
1209
+ continue;
1210
+ }
1211
+ const original = drafts.get(row.key) ?? row.original;
1212
+ try {
1213
+ if (!column.isEditable(original)) {
1214
+ fail("read_only_cell", `The "${column.id}" cell is read-only.`);
1215
+ continue;
1216
+ }
1217
+ let value;
1218
+ if ("input" in edit) {
1219
+ if (!samples.has(column.id)) samples.set(column.id, snapshot.processedRows.find((candidate) => candidate.getValue(column.id) !== null && candidate.getValue(column.id) !== void 0)?.getValue(column.id));
1220
+ const parsed = column.parse(edit.input, original, samples.get(column.id));
1221
+ if (!parsed.ok) {
1222
+ fail("invalid_value", parsed.message);
1223
+ continue;
1224
+ }
1225
+ value = parsed.value;
1226
+ } else value = edit.value;
1227
+ const previous = column.getValue(original);
1228
+ if (Object.is(previous, value)) continue;
1229
+ const constraint = editorFailure(column.definition.editor, value);
1230
+ if (constraint) {
1231
+ fail("validation_failed", constraint);
1232
+ continue;
1233
+ }
1234
+ const next = column.setValue(original, value);
1235
+ if (next === void 0) {
1236
+ fail("read_only_cell", "The column has no way to write a value.");
1237
+ continue;
1238
+ }
1239
+ const writeProblem = host.writeIssue(row, next);
1240
+ if (writeProblem) {
1241
+ failures.push(Object.freeze({
1242
+ ...writeProblem,
1243
+ column: column.id
1244
+ }));
1245
+ continue;
1246
+ }
1247
+ const validate = column.definition.validate;
1248
+ const validation = validate ? runValidation(() => validate(value, row.original), row.key, column.id) : void 0;
1249
+ if (validation && !isPromise(validation)) {
1250
+ failures.push(validation);
1251
+ continue;
1252
+ }
1253
+ drafts.set(row.key, next);
1254
+ candidates.push({
1255
+ edit: {
1256
+ rowKey: row.key,
1257
+ column: column.id,
1258
+ value
1259
+ },
1260
+ previous: column.getValue(row.original),
1261
+ original: row.original,
1262
+ column,
1263
+ validation,
1264
+ token
1265
+ });
1266
+ } catch (error) {
1267
+ fail("invalid_value", error instanceof Error ? error.message : String(error));
1268
+ }
1269
+ }
1270
+ const asynchronous = candidates.some((candidate) => isPromise(candidate.validation));
1271
+ const validateRows = (accepted) => rowValidation(accepted.filter(current), failures);
1272
+ const outcome = asynchronous ? Promise.all(candidates.map(async (candidate) => {
1273
+ const problem = await candidate.validation;
1274
+ if (problem) {
1275
+ if (current(candidate)) failures.push(problem);
1276
+ return;
1277
+ }
1278
+ return candidate;
1279
+ })).then((accepted) => validateRows(accepted.filter((candidate) => candidate !== void 0))) : validateRows(candidates);
1280
+ if (!isPromise(outcome)) {
1281
+ const result = settle(outcome, candidates, failures, false);
1282
+ if (metadataChanged && !candidates.length && !failures.length) host.notify();
1283
+ return result;
1284
+ }
1285
+ for (const candidate of candidates) {
1286
+ const row = pending.get(candidate.edit.rowKey) ?? /* @__PURE__ */ new Map();
1287
+ row.set(candidate.edit.column, Object.freeze({ ...candidate.edit }));
1288
+ pending.set(candidate.edit.rowKey, row);
1289
+ }
1290
+ setIssues(failures);
1291
+ const optimistic = host.optimistic ? host.apply(candidates.map((candidate) => candidate.edit), false, failures) : void 0;
1292
+ if (!optimistic?.changes.length) host.notify();
1293
+ if (!host.optimistic && failures.length) host.onIssues(failures);
1294
+ outcome.then((accepted) => {
1295
+ if (candidates.some(current)) settle(accepted, candidates, failures, true);
1296
+ });
1297
+ return Object.freeze({
1298
+ status: "pending",
1299
+ changes: optimistic?.changes ?? Object.freeze([]),
1300
+ rowChanges: Object.freeze([]),
1301
+ issues: Object.freeze([...failures]),
1302
+ pendingCells: Object.freeze(candidates.map((candidate) => Object.freeze({ ...candidate.edit })))
1303
+ });
1304
+ }
1305
+ };
1306
+ }
1307
+
1308
+ //#endregion
1309
+ //#region src/pending-changes.ts
1310
+ /** A saved baseline only for touched rows; viewing changes never scans source data. */
1311
+ function createPendingChanges() {
1312
+ const entries = /* @__PURE__ */ new Map();
1313
+ return {
1314
+ entries,
1315
+ snapshot() {
1316
+ const inserted = [];
1317
+ const removed = [];
1318
+ const updated = [];
1319
+ for (const [rowKey, entry] of entries) if (entry.previous === void 0 && entry.row !== void 0) inserted.push(Object.freeze({
1320
+ kind: "inserted",
1321
+ rowKey,
1322
+ row: entry.row,
1323
+ position: entry.position
1324
+ }));
1325
+ else if (entry.row === void 0 && entry.previous !== void 0) removed.push(Object.freeze({
1326
+ kind: "removed",
1327
+ rowKey,
1328
+ row: entry.previous,
1329
+ position: entry.position
1330
+ }));
1331
+ else if (entry.row !== void 0 && entry.previous !== void 0) updated.push(Object.freeze({
1332
+ rowKey,
1333
+ row: entry.row,
1334
+ previous: entry.previous,
1335
+ changes: Object.freeze([...entry.cells.values()])
1336
+ }));
1337
+ return Object.freeze({
1338
+ inserted: Object.freeze(inserted),
1339
+ updated: Object.freeze(updated),
1340
+ removed: Object.freeze(removed)
1341
+ });
1342
+ },
1343
+ record(key, previous, row, position, changes) {
1344
+ const entry = entries.get(key) ?? {
1345
+ previous,
1346
+ row,
1347
+ position: Object.freeze({ ...position }),
1348
+ cells: /* @__PURE__ */ new Map()
1349
+ };
1350
+ entry.row = row;
1351
+ for (const change of changes) {
1352
+ const original = entry.cells.has(change.column) ? entry.cells.get(change.column).previous : change.previous;
1353
+ if (Object.is(original, change.value)) entry.cells.delete(change.column);
1354
+ else entry.cells.set(change.column, Object.freeze({
1355
+ ...change,
1356
+ previous: original
1357
+ }));
1358
+ }
1359
+ if (entry.previous === void 0 && row === void 0 || entry.previous !== void 0 && row !== void 0 && entry.cells.size === 0) entries.delete(key);
1360
+ else entries.set(key, entry);
1361
+ }
1362
+ };
1363
+ }
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
+
969
1520
  //#endregion
970
1521
  //#region src/tree.ts
971
1522
  function buildTree(data, columns, readKey, options, lazy) {
@@ -1223,10 +1774,13 @@ function createTable(options) {
1223
1774
  const pendingLoads = /* @__PURE__ */ new Map();
1224
1775
  let destroyed = false;
1225
1776
  const undoStack = createUndoStack(options.historyLimit);
1777
+ const pendingChanges = createPendingChanges();
1226
1778
  const listeners = /* @__PURE__ */ new Set();
1227
1779
  const optionIssues = [];
1228
1780
  const maxPasteCells = pasteLimit(options.pasteLimit?.maxCells, 1e5, "maxCells", optionIssues);
1229
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."));
1230
1784
  let data = options.data;
1231
1785
  let definitions = options.columns;
1232
1786
  let rowCount = options.rowCount;
@@ -1239,6 +1793,49 @@ function createTable(options) {
1239
1793
  });
1240
1794
  let state = initialState;
1241
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
+ });
1809
+ let transaction = false;
1810
+ let deferValidationNotify = false;
1811
+ let validationNotifyRequested = false;
1812
+ const validation = createEditValidation({
1813
+ snapshot: getSnapshot,
1814
+ apply: applyChanges,
1815
+ notify,
1816
+ writeIssue(row, next) {
1817
+ const node = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion).byKey.get(row.key) : void 0;
1818
+ if (node && (!node.writable || node.parent && options.getChildren && !options.setChildren)) return issue("read_only_cell", "This nested row has no immutable writable source path.", { rowKey: row.key });
1819
+ if (readKey(next, row.index) !== readKey(row.original, row.index)) return issue("invalid_row_operation", "Editing a row key is not supported; remove and insert the row instead.", { rowKey: row.key });
1820
+ },
1821
+ batch(run) {
1822
+ deferValidationNotify = true;
1823
+ validationNotifyRequested = false;
1824
+ try {
1825
+ return run();
1826
+ } finally {
1827
+ deferValidationNotify = false;
1828
+ if (validationNotifyRequested) notify();
1829
+ }
1830
+ },
1831
+ record: (changes) => undoStack.record({
1832
+ kind: "cells",
1833
+ changes
1834
+ }),
1835
+ onIssues: (issues) => options.onEditIssues?.(issues),
1836
+ optimistic: options.asyncValidation === "optimistic",
1837
+ validateRow: options.validateRow
1838
+ });
1242
1839
  const readKey = (row, index) => {
1243
1840
  const { rowKey } = options;
1244
1841
  if (typeof rowKey === "function") return rowKey(row, index);
@@ -1261,25 +1858,46 @@ function createTable(options) {
1261
1858
  issues
1262
1859
  };
1263
1860
  });
1861
+ let primedIndex;
1264
1862
  const indexRows = memo((rows, byId) => {
1863
+ if (primedIndex?.data === rows && primedIndex.columns === byId) return primedIndex.result;
1265
1864
  const byKey = /* @__PURE__ */ new Map();
1266
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));
1267
1877
  return {
1268
- rows: rows.map((original, index) => {
1269
- let key = readKey(original, index);
1270
- if (byKey.has(key)) {
1271
- issues.push(issue("duplicate_row_key", `Row ${index} repeats the key "${String(key)}"; it is keyed "${String(key)}#${index}" instead.`, { rowKey: key }));
1272
- key = `${String(key)}#${index}`;
1273
- }
1274
- const row = createRow(original, key, index, byId, options.getRowCanExpand?.(original) ?? false);
1275
- byKey.set(key, row);
1276
- return row;
1277
- }),
1878
+ rows: indexed,
1278
1879
  byKey,
1279
1880
  issues
1280
1881
  };
1281
1882
  });
1282
1883
  const treeStage = memo((rows, byId, _version) => buildTree(rows, byId, readKey, options, lazyChildren));
1884
+ const treeIndex = memo((model) => ({
1885
+ rows: model.rows,
1886
+ byKey: new Map(model.rows.map((row) => [row.key, row])),
1887
+ issues: model.issues
1888
+ }));
1889
+ const treePositions = memo((model) => {
1890
+ const result = /* @__PURE__ */ new Map();
1891
+ const index = (siblings, parent) => {
1892
+ siblings.forEach((node, position) => result.set(node.row.key, Object.freeze({
1893
+ parent,
1894
+ before: siblings[position + 1]?.row.key
1895
+ })));
1896
+ };
1897
+ index(model.roots);
1898
+ for (const node of model.nodes) index(node.children, node.row.key);
1899
+ return result;
1900
+ });
1283
1901
  const processedTreeStage = memo((model, columns, search, filters, sorting) => processTree(model, columns, {
1284
1902
  ...state,
1285
1903
  search,
@@ -1347,13 +1965,24 @@ function createTable(options) {
1347
1965
  visible: ordered.filter((column) => !hidden.has(column.id))
1348
1966
  };
1349
1967
  });
1350
- const filterStage = memo(filterRows);
1351
- 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
+ }
1352
1981
  const selectedSet = memo((selection) => new Set(selection));
1353
1982
  const expansionStage = memo((indexed, expanded, selection) => {
1354
1983
  const keys = expanded === true ? void 0 : new Set(expanded);
1355
1984
  const selected = new Set(selection);
1356
- const rows = indexed.rows.map((row) => {
1985
+ const rows = mapList(indexed.rows, (row) => {
1357
1986
  const isExpanded = row.canExpand && (expanded === true || (keys?.has(row.key) ?? false));
1358
1987
  return isExpanded || selected.has(row.key) ? Object.freeze({
1359
1988
  ...row,
@@ -1363,10 +1992,13 @@ function createTable(options) {
1363
1992
  });
1364
1993
  return {
1365
1994
  rows,
1366
- 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
+ } }
1367
1999
  };
1368
2000
  });
1369
- 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)));
1370
2002
  function stateIssues(byId) {
1371
2003
  const issues = [];
1372
2004
  if (expandedIssue) issues.push(expandedIssue);
@@ -1382,6 +2014,13 @@ function createTable(options) {
1382
2014
  return issues;
1383
2015
  }
1384
2016
  function computeSnapshot() {
2017
+ const cellProblems = validation.problems();
2018
+ const dirty = new Set(pendingChanges.entries.keys());
2019
+ const metadata = (row) => dirty.has(row.key) || cellProblems.has(row.key) ? Object.freeze({
2020
+ ...row,
2021
+ isDirty: dirty.has(row.key),
2022
+ cellIssues: cellProblems.get(row.key)
2023
+ }) : row;
1385
2024
  const { columns, byId, issues: columnIssues } = resolveColumns(definitions);
1386
2025
  const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
1387
2026
  const indexed = model ? {
@@ -1392,11 +2031,14 @@ function createTable(options) {
1392
2031
  const ordered = orderColumns(columns, state.columnOrder, state.hiddenColumns);
1393
2032
  const issues = [
1394
2033
  ...optionIssues,
2034
+ ...ingestionIssues,
2035
+ ...streamIssues,
1395
2036
  ...columnIssues,
1396
2037
  ...indexed.issues,
1397
2038
  ...stateIssues(byId),
1398
2039
  ...loadIssues.values(),
1399
- ...Array.from(loadValidationIssues.values()).flat()
2040
+ ...Array.from(loadValidationIssues.values()).flat(),
2041
+ ...Array.from(cellProblems.values()).flatMap((row) => [...row.values()])
1400
2042
  ];
1401
2043
  const requestedSize = state.pagination.pageSize;
1402
2044
  let pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
@@ -1426,6 +2068,8 @@ function createTable(options) {
1426
2068
  const canExpand = node.children.length > 0 || status !== "loaded" || (options.getRowCanExpand?.(node.row.original) ?? false);
1427
2069
  const result = Object.freeze({
1428
2070
  ...node.row,
2071
+ isDirty: dirty.has(key),
2072
+ cellIssues: cellProblems.get(key),
1429
2073
  depth: node.depth,
1430
2074
  parentKey: node.parent?.row.key,
1431
2075
  childCount: status === "loaded" ? node.children.length : void 0,
@@ -1479,21 +2123,46 @@ function createTable(options) {
1479
2123
  pageCount = 1;
1480
2124
  pageSize = Math.max(1, rows.length);
1481
2125
  }
2126
+ const flatProjected = /* @__PURE__ */ new Map();
2127
+ if (!model) {
2128
+ const project = (row) => {
2129
+ let result = flatProjected.get(row.key);
2130
+ if (!result) {
2131
+ result = metadata(row);
2132
+ flatProjected.set(row.key, result);
2133
+ }
2134
+ return result;
2135
+ };
2136
+ const shared = rows === processedRows;
2137
+ processedRows = mapList(processedRows, project);
2138
+ rows = shared ? processedRows : rows.map(project);
2139
+ }
1482
2140
  const selected = selectedSet(state.selection);
1483
- 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;
1484
2145
  const scopeRows = selectScope === "page" || manual ? rows : processedRows;
1485
- 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;
1486
2147
  const pageStart = rows.length === 0 ? 0 : (page - 1) * pageSize + 1;
1487
2148
  const sortIndex = new Map(state.sorting.map((rule, priority) => [rule.column, {
1488
2149
  direction: rule.direction,
1489
2150
  priority
1490
2151
  }]));
1491
2152
  return Object.freeze({
2153
+ loadState,
2154
+ loadedRowCount: indexed.rows.length,
2155
+ expectedRowCount,
1492
2156
  state,
1493
2157
  columns: ordered.visible,
1494
2158
  allColumns: ordered.all,
1495
2159
  rows,
1496
- 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) => {
1497
2166
  const item = Object.freeze({
1498
2167
  kind: "row",
1499
2168
  key: getRowItemKey(row.key),
@@ -1526,10 +2195,11 @@ function createTable(options) {
1526
2195
  canUndo: undoStack.canUndo,
1527
2196
  canRedo: undoStack.canRedo,
1528
2197
  issues: Object.freeze(issues),
2198
+ pendingCells: validation.cells(),
1529
2199
  isSelected: (key) => selected.has(key),
1530
2200
  getSort: (columnId) => sortIndex.get(columnId),
1531
2201
  getColumn: (columnId) => byId.get(columnId),
1532
- getRow: (key) => treeRow ? treeRow(key) : expanded.byKey.get(key)
2202
+ getRow: (key) => treeRow ? treeRow(key) : flatProjected.get(key) ?? (expanded.byKey.get(key) ? metadata(expanded.byKey.get(key)) : void 0)
1533
2203
  });
1534
2204
  }
1535
2205
  function getSnapshot() {
@@ -1538,6 +2208,11 @@ function createTable(options) {
1538
2208
  }
1539
2209
  function notify() {
1540
2210
  snapshot = void 0;
2211
+ if (deferValidationNotify) {
2212
+ validationNotifyRequested = true;
2213
+ return;
2214
+ }
2215
+ if (transaction) return;
1541
2216
  if (listeners.size === 0) return;
1542
2217
  const next = getSnapshot();
1543
2218
  for (const listener of listeners) listener(next);
@@ -1702,6 +2377,7 @@ function createTable(options) {
1702
2377
  const working = [...data];
1703
2378
  const model = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion) : void 0;
1704
2379
  const replacements = /* @__PURE__ */ new Map();
2380
+ const cache = new Map(lazyChildren);
1705
2381
  const originalOf = (node) => replacements.get(node) ?? node.row.original;
1706
2382
  const issues = [...recoveredIssues];
1707
2383
  const changes = [];
@@ -1777,7 +2453,16 @@ function createTable(options) {
1777
2453
  } else value = edit.value;
1778
2454
  const previous = column.getValue(original);
1779
2455
  if (Object.is(previous, value)) continue;
1780
- const updated = column.setValue(original, value);
2456
+ let updated;
2457
+ try {
2458
+ updated = column.setValue(original, value);
2459
+ } catch (error) {
2460
+ issues.push(issue("invalid_value", error instanceof Error ? error.message : String(error), {
2461
+ rowKey: row.key,
2462
+ column: column.id
2463
+ }));
2464
+ continue;
2465
+ }
1781
2466
  if (updated === void 0) {
1782
2467
  issues.push(issue("read_only_cell", `The "${column.id}" column has no way to write a value.`, {
1783
2468
  rowKey: row.key,
@@ -1785,20 +2470,37 @@ function createTable(options) {
1785
2470
  }));
1786
2471
  continue;
1787
2472
  }
2473
+ if (readKey(updated, row.index) !== readKey(original, row.index)) {
2474
+ issues.push(issue("invalid_row_operation", "Editing a row key is not supported; remove and insert the row instead.", {
2475
+ rowKey: row.key,
2476
+ column: column.id
2477
+ }));
2478
+ continue;
2479
+ }
1788
2480
  if (!node) working[row.index] = updated;
1789
2481
  else {
2482
+ const planned = /* @__PURE__ */ new Map([[node, updated]]);
2483
+ const plannedCache = /* @__PURE__ */ new Map();
1790
2484
  let cursor = node;
1791
- replacements.set(cursor, updated);
1792
- if (!options.getChildren && !cursor.lazy) working[cursor.sourceIndex] = updated;
1793
- while (cursor.parent) {
1794
- const parent = cursor.parent;
1795
- const children = parent.children.map(originalOf);
1796
- if (cursor.lazy) lazyChildren.set(parent.row.key, children);
1797
- if (options.setChildren) replacements.set(parent, options.setChildren(originalOf(parent), children));
1798
- cursor = parent;
2485
+ try {
2486
+ while (cursor.parent) {
2487
+ const parent = cursor.parent;
2488
+ const children = parent.children.map((child) => planned.get(child) ?? originalOf(child));
2489
+ if (cursor.lazy) plannedCache.set(parent.row.key, children);
2490
+ if (options.setChildren) planned.set(parent, options.setChildren(originalOf(parent), children));
2491
+ cursor = parent;
2492
+ }
2493
+ } catch (error) {
2494
+ issues.push(issue("invalid_value", error instanceof Error ? error.message : String(error), {
2495
+ rowKey: row.key,
2496
+ column: column.id
2497
+ }));
2498
+ continue;
1799
2499
  }
2500
+ for (const [item, plannedRow] of planned) replacements.set(item, plannedRow);
2501
+ for (const [key, children] of plannedCache) cache.set(key, children);
2502
+ if (!options.getChildren && !node.lazy) working[node.sourceIndex] = updated;
1800
2503
  if (options.getChildren || options.setChildren) working[cursor.sourceIndex] = originalOf(cursor);
1801
- lazyVersion++;
1802
2504
  }
1803
2505
  changes.push({
1804
2506
  rowKey: row.key,
@@ -1813,17 +2515,390 @@ function createTable(options) {
1813
2515
  else {
1814
2516
  status = issues.length > 0 ? "partial" : "applied";
1815
2517
  data = working;
1816
- if (record) undoStack.record(changes);
1817
- options.onDataChange?.(data, changes);
2518
+ if (model) {
2519
+ lazyChildren.clear();
2520
+ for (const [key, children] of cache) lazyChildren.set(key, children);
2521
+ lazyVersion++;
2522
+ }
2523
+ if (record) {
2524
+ if (!transaction) undoStack.record({
2525
+ kind: "cells",
2526
+ changes
2527
+ });
2528
+ }
2529
+ const groups = /* @__PURE__ */ new Map();
2530
+ for (const change of changes) {
2531
+ const group = groups.get(change.rowKey) ?? [];
2532
+ group.push(change);
2533
+ groups.set(change.rowKey, group);
2534
+ }
2535
+ for (const [key, group] of groups) pendingChanges.record(key, current.getRow(key).original, group.at(-1).row, positionOf(key), group);
2536
+ if (!transaction) options.onDataChange?.(data, changes);
1818
2537
  notify();
1819
2538
  }
1820
- if (issues.length > 0) options.onEditIssues?.(issues);
2539
+ if (issues.length > 0 && !transaction) options.onEditIssues?.(issues);
1821
2540
  return Object.freeze({
1822
2541
  status,
1823
2542
  changes: Object.freeze(changes),
1824
- issues: Object.freeze(issues)
2543
+ issues: Object.freeze(issues),
2544
+ rowChanges: Object.freeze([]),
2545
+ pendingCells: Object.freeze([])
2546
+ });
2547
+ }
2548
+ function sourceRows() {
2549
+ const { byId } = resolveColumns(definitions);
2550
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2551
+ return model ? treeIndex(model) : indexRows(data, byId);
2552
+ }
2553
+ function positionOf(key) {
2554
+ const { byId } = resolveColumns(definitions);
2555
+ if (treeEnabled) {
2556
+ const model = treeStage(data, byId, lazyVersion);
2557
+ return treePositions(model).get(key) ?? Object.freeze({});
2558
+ }
2559
+ const indexed = indexRows(data, byId);
2560
+ const row = indexed.byKey.get(key);
2561
+ return Object.freeze({ before: row ? indexed.rows[row.index + 1]?.key : void 0 });
2562
+ }
2563
+ function scene() {
2564
+ return {
2565
+ data,
2566
+ lazy: new Map(lazyChildren)
2567
+ };
2568
+ }
2569
+ function resultForRows(changes, issues, cells = []) {
2570
+ return Object.freeze({
2571
+ status: changes.length || cells.length ? issues.length ? "partial" : "applied" : issues.length ? "rejected" : "unchanged",
2572
+ changes: Object.freeze([...cells]),
2573
+ rowChanges: Object.freeze([...changes]),
2574
+ pendingCells: Object.freeze([]),
2575
+ issues: Object.freeze([...issues])
2576
+ });
2577
+ }
2578
+ function rowProblem(message, rowKey) {
2579
+ return issue("invalid_row_operation", message, { rowKey });
2580
+ }
2581
+ function stableRows(rows) {
2582
+ return options.rowKey !== void 0 || rows.every((row) => {
2583
+ const key = getPath(row, "id");
2584
+ return typeof key === "string" || typeof key === "number";
2585
+ });
2586
+ }
2587
+ function cellDifferences(previous, row, key) {
2588
+ return resolveColumns(definitions).columns.flatMap((column) => {
2589
+ const before = column.getValue(previous);
2590
+ const value = column.getValue(row);
2591
+ return Object.is(before, value) ? [] : [{
2592
+ rowKey: key,
2593
+ column: column.id,
2594
+ previous: before,
2595
+ value,
2596
+ row
2597
+ }];
2598
+ });
2599
+ }
2600
+ function recordRows(previous, keys, positions, baselines = /* @__PURE__ */ new Map()) {
2601
+ const next = sourceRows();
2602
+ const rowChanges = [];
2603
+ for (const key of keys) {
2604
+ const before = previous.byKey.get(key)?.original;
2605
+ const row = next.byKey.get(key)?.original;
2606
+ const position = positions.get(key) ?? positionOf(key);
2607
+ const cells = before !== void 0 && row !== void 0 ? cellDifferences(before, row, key) : [];
2608
+ const baseline = baselines.get(key);
2609
+ const pending = pendingChanges.entries.get(key);
2610
+ if (row === void 0 && baseline !== void 0 && pending?.previous !== void 0) pending.previous = baseline;
2611
+ pendingChanges.record(key, row === void 0 ? baseline ?? before : before, row, position, cells);
2612
+ if (before === void 0 && row !== void 0) rowChanges.push(Object.freeze({
2613
+ kind: "inserted",
2614
+ rowKey: key,
2615
+ row,
2616
+ position: positionOf(key)
2617
+ }));
2618
+ else if (row === void 0 && before !== void 0) rowChanges.push(Object.freeze({
2619
+ kind: "removed",
2620
+ rowKey: key,
2621
+ row: before,
2622
+ position
2623
+ }));
2624
+ }
2625
+ return rowChanges;
2626
+ }
2627
+ function publishRows(before, previous, keys, positions, issues, baselines = /* @__PURE__ */ new Map()) {
2628
+ const rowChanges = recordRows(previous, keys, positions, baselines);
2629
+ if (!transaction) {
2630
+ undoStack.record({
2631
+ kind: "rows",
2632
+ before,
2633
+ after: scene(),
2634
+ keys
2635
+ });
2636
+ options.onDataChange?.(data, []);
2637
+ if (issues.length) options.onEditIssues?.(issues);
2638
+ }
2639
+ notify();
2640
+ return resultForRows(rowChanges, issues);
2641
+ }
2642
+ function restoreScene(entry, direction) {
2643
+ const previous = sourceRows();
2644
+ const baselines = removalBaselines();
2645
+ const positions = new Map(entry.keys.map((key) => [key, positionOf(key)]));
2646
+ const target = direction === "undo" ? entry.before : entry.after;
2647
+ data = target.data;
2648
+ const changed = /* @__PURE__ */ new Set([...entry.before.lazy.keys(), ...entry.after.lazy.keys()]);
2649
+ for (const key of changed) {
2650
+ if (entry.before.lazy.get(key) === entry.after.lazy.get(key)) continue;
2651
+ const rows = target.lazy.get(key);
2652
+ if (rows) lazyChildren.set(key, rows);
2653
+ else lazyChildren.delete(key);
2654
+ }
2655
+ lazyVersion++;
2656
+ recordRows(previous, entry.keys, positions, baselines);
2657
+ options.onDataChange?.(data, []);
2658
+ notify();
2659
+ }
2660
+ function removalBaselines() {
2661
+ const baselines = /* @__PURE__ */ new Map();
2662
+ if (!options.getChildren || !options.setChildren) return baselines;
2663
+ const model = treeStage(data, resolveColumns(definitions).byId, lazyVersion);
2664
+ for (const node of model.nodes.toReversed()) {
2665
+ const entry = pendingChanges.entries.get(node.row.key);
2666
+ if (entry && entry.previous === void 0) continue;
2667
+ let baseline = entry?.previous ?? node.row.original;
2668
+ const children = node.children.filter((child) => !pendingChanges.entries.has(child.row.key) || pendingChanges.entries.get(child.row.key).previous !== void 0).map((child) => baselines.get(child.row.key) ?? child.row.original);
2669
+ const originalChildren = options.getChildren(baseline) ?? [];
2670
+ if (children.length !== originalChildren.length || children.some((child, index) => child !== originalChildren[index])) baseline = options.setChildren(baseline, children);
2671
+ baselines.set(node.row.key, baseline);
2672
+ }
2673
+ return baselines;
2674
+ }
2675
+ function rewriteNested(model, removed, overrides, cache) {
2676
+ const replacements = /* @__PURE__ */ new Map();
2677
+ for (const node of model.nodes.toReversed()) {
2678
+ if (removed.has(node.row.key)) continue;
2679
+ const children = overrides.get(node.row.key) ?? node.children.filter((child) => !removed.has(child.row.key)).map((child) => replacements.get(child) ?? child.row.original);
2680
+ if (!(overrides.has(node.row.key) || children.length !== node.children.length || children.some((child, index) => child !== node.children[index]?.row.original))) continue;
2681
+ if (!options.setChildren) throw new Error("Changing nested children requires setChildren.");
2682
+ replacements.set(node, options.setChildren(node.row.original, children));
2683
+ if (cache.has(node.row.key)) cache.set(node.row.key, children);
2684
+ }
2685
+ const roots = new Map(model.roots.filter((node) => node.writable).map((node) => [node.sourceIndex, node]));
2686
+ return data.flatMap((row, index) => {
2687
+ const node = roots.get(index);
2688
+ return node && removed.has(node.row.key) ? [] : [node ? replacements.get(node) ?? row : row];
1825
2689
  });
1826
2690
  }
2691
+ function insertRows(rows, at = {}) {
2692
+ const before = scene();
2693
+ const previous = sourceRows();
2694
+ const problems = [];
2695
+ const reject = (problem) => {
2696
+ if (!transaction) options.onEditIssues?.([problem]);
2697
+ return resultForRows([], [problem]);
2698
+ };
2699
+ let incoming;
2700
+ try {
2701
+ incoming = rows ?? (options.createRow ? [options.createRow()] : []);
2702
+ } catch (error) {
2703
+ return reject(rowProblem(error instanceof Error ? error.message : String(error)));
2704
+ }
2705
+ if (rows === void 0 && !options.createRow) return reject(rowProblem("insertRows() requires rows or a createRow callback."));
2706
+ if (!incoming.length) return resultForRows([], []);
2707
+ if (!stableRows(data) || !stableRows(incoming)) return reject(rowProblem("Row operations require stable row keys; supply rowKey or an id on every row."));
2708
+ const { byId } = resolveColumns(definitions);
2709
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2710
+ const parent = at.parent === void 0 ? void 0 : model?.byKey.get(at.parent);
2711
+ const sibling = at.before === void 0 ? void 0 : previous.byKey.get(at.before);
2712
+ if (at.parent !== void 0 && !parent) return reject(issue("unknown_row", "The insertion parent does not exist.", { rowKey: at.parent }));
2713
+ if (at.before !== void 0 && !sibling) return reject(issue("unknown_row", "The insertion sibling does not exist.", { rowKey: at.before }));
2714
+ if (sibling && (model?.byKey.get(sibling.key)?.parent?.row.key ?? sibling.parentKey) !== at.parent) return reject(rowProblem("The before row must belong to the requested parent.", sibling.key));
2715
+ if (parent && !parent.writable) return reject(rowProblem("A recovered parent has no writable source path.", parent.row.key));
2716
+ if (parent && getSnapshot().getRow(parent.row.key)?.childStatus !== "loaded") return reject(rowProblem("Load the parent's children before inserting rows.", parent.row.key));
2717
+ const cache = new Map(lazyChildren);
2718
+ let next;
2719
+ try {
2720
+ if (options.getChildren && parent && model) {
2721
+ const children = parent.children.map((child) => child.row.original);
2722
+ const index = at.before === void 0 ? children.length : parent.children.findIndex((child) => child.row.key === at.before);
2723
+ children.splice(index, 0, ...incoming);
2724
+ next = rewriteNested(model, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map([[parent.row.key, children]]), cache);
2725
+ } else if (parent && cache.has(parent.row.key)) {
2726
+ incoming = incoming.map((row) => options.setParentKey ? options.setParentKey(row, parent.row.key) : row);
2727
+ const children = [...cache.get(parent.row.key)];
2728
+ const index = at.before === void 0 ? children.length : parent.children.findIndex((child) => child.row.key === at.before);
2729
+ children.splice(index, 0, ...incoming);
2730
+ cache.set(parent.row.key, children);
2731
+ next = [...data];
2732
+ } else {
2733
+ if (parent && options.getParentKey) {
2734
+ incoming = incoming.map((row) => options.setParentKey ? options.setParentKey(row, parent.row.key) : row);
2735
+ if (incoming.some((row) => options.getParentKey(row) !== parent.row.key)) return reject(rowProblem("Supply setParentKey or rows already addressed to the requested parent."));
2736
+ }
2737
+ const working = [...data];
2738
+ working.splice(sibling?.index ?? working.length, 0, ...incoming);
2739
+ next = working;
2740
+ }
2741
+ const candidate = treeEnabled ? buildTree(next, byId, readKey, options, cache) : indexRows(next, byId);
2742
+ if (!stableRows(candidate.rows.map((row) => row.original))) return reject(rowProblem("Every inserted descendant must have a stable key."));
2743
+ const baseline = /* @__PURE__ */ new Map();
2744
+ for (const problem of previous.issues) {
2745
+ const key = `${problem.code}:${String(problem.rowKey)}`;
2746
+ baseline.set(key, (baseline.get(key) ?? 0) + 1);
2747
+ }
2748
+ problems.push(...candidate.issues.filter((problem) => {
2749
+ const key = `${problem.code}:${String(problem.rowKey)}`;
2750
+ const remaining = baseline.get(key) ?? 0;
2751
+ if (!remaining) return true;
2752
+ baseline.set(key, remaining - 1);
2753
+ return false;
2754
+ }));
2755
+ if (problems.length) {
2756
+ if (!transaction) options.onEditIssues?.(problems);
2757
+ return resultForRows([], problems);
2758
+ }
2759
+ const keys = candidate.rows.filter((row) => !previous.byKey.has(row.key)).map((row) => row.key);
2760
+ if (!keys.length) return reject(rowProblem("No unique rows can be inserted."));
2761
+ data = next;
2762
+ lazyChildren.clear();
2763
+ for (const [key, value] of cache) lazyChildren.set(key, value);
2764
+ lazyVersion++;
2765
+ return publishRows(before, previous, keys, /* @__PURE__ */ new Map(), []);
2766
+ } catch (error) {
2767
+ return reject(rowProblem(error instanceof Error ? error.message : String(error)));
2768
+ }
2769
+ }
2770
+ function removeRows(keys) {
2771
+ const before = scene();
2772
+ const previous = sourceRows();
2773
+ const problems = [];
2774
+ const removed = /* @__PURE__ */ new Set();
2775
+ const { byId } = resolveColumns(definitions);
2776
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2777
+ if (!stableRows(data)) {
2778
+ const problem = rowProblem("Row operations require stable row keys.");
2779
+ if (!transaction) options.onEditIssues?.([problem]);
2780
+ return resultForRows([], [problem]);
2781
+ }
2782
+ for (const key of keys) {
2783
+ if (!previous.byKey.has(key)) {
2784
+ problems.push(issue("unknown_row", "The row to remove does not exist.", { rowKey: key }));
2785
+ continue;
2786
+ }
2787
+ const node = model?.byKey.get(key);
2788
+ if (node && !node.writable) {
2789
+ problems.push(rowProblem("A recovered row has no writable source path.", key));
2790
+ continue;
2791
+ }
2792
+ const stack = node ? [node] : [];
2793
+ removed.add(key);
2794
+ while (stack.length) {
2795
+ const item = stack.pop();
2796
+ removed.add(item.row.key);
2797
+ for (const child of item.children) stack.push(child);
2798
+ }
2799
+ }
2800
+ if (!removed.size) {
2801
+ if (problems.length && !transaction) options.onEditIssues?.(problems);
2802
+ return resultForRows([], problems);
2803
+ }
2804
+ const positions = new Map([...removed].map((key) => [key, positionOf(key)]));
2805
+ const cache = new Map(lazyChildren);
2806
+ try {
2807
+ const baselines = removalBaselines();
2808
+ const next = options.getChildren && model ? rewriteNested(model, removed, /* @__PURE__ */ new Map(), cache) : data.filter((row, index) => !removed.has(readKey(row, index)));
2809
+ for (const [key, children] of cache) if (removed.has(key)) cache.delete(key);
2810
+ else cache.set(key, children.filter((row, index) => !removed.has(readKey(row, index))));
2811
+ data = next;
2812
+ lazyChildren.clear();
2813
+ for (const [key, value] of cache) lazyChildren.set(key, value);
2814
+ lazyVersion++;
2815
+ validation.cancel([...removed]);
2816
+ abortLoads();
2817
+ return publishRows(before, previous, [...removed], positions, problems, baselines);
2818
+ } catch (error) {
2819
+ problems.push(rowProblem(error instanceof Error ? error.message : String(error)));
2820
+ if (!transaction) options.onEditIssues?.(problems);
2821
+ return resultForRows([], problems);
2822
+ }
2823
+ }
2824
+ function revert(keys) {
2825
+ const selected = new Set(keys ?? pendingChanges.entries.keys());
2826
+ const entries = [...pendingChanges.entries].filter(([key]) => selected.has(key)).map(([key, entry]) => [key, {
2827
+ ...entry,
2828
+ cells: new Map(entry.cells)
2829
+ }]);
2830
+ if (!entries.length) return resultForRows([], []);
2831
+ const before = scene();
2832
+ const previous = sourceRows();
2833
+ const positions = new Map([...selected].map((key) => [key, positionOf(key)]));
2834
+ const problems = [];
2835
+ const cells = [];
2836
+ transaction = true;
2837
+ try {
2838
+ problems.push(...removeRows(entries.filter(([, entry]) => entry.previous === void 0).map(([key]) => key)).issues);
2839
+ const missing = entries.filter(([, entry]) => entry.previous !== void 0 && entry.row === void 0);
2840
+ for (let pass = 0; missing.length && pass <= entries.length; pass++) {
2841
+ let progress = false;
2842
+ for (let index = missing.length - 1; index >= 0; index--) {
2843
+ const [key, entry] = missing[index];
2844
+ if (sourceRows().byKey.has(key)) {
2845
+ missing.splice(index, 1);
2846
+ progress = true;
2847
+ continue;
2848
+ }
2849
+ if (entry.position.parent !== void 0 && !sourceRows().byKey.has(entry.position.parent)) continue;
2850
+ const at = {
2851
+ ...entry.position,
2852
+ before: entry.position.before !== void 0 && sourceRows().byKey.has(entry.position.before) ? entry.position.before : void 0
2853
+ };
2854
+ const result = insertRows([entry.previous], at);
2855
+ problems.push(...result.issues);
2856
+ missing.splice(index, 1);
2857
+ progress = true;
2858
+ }
2859
+ if (!progress) break;
2860
+ }
2861
+ for (const [key] of missing) problems.push(rowProblem("The saved parent is missing; restore it before this child.", key));
2862
+ const result = applyChanges(entries.flatMap(([key, entry]) => entry.previous !== void 0 && sourceRows().byKey.has(key) ? [...entry.cells.values()].map((change) => ({
2863
+ rowKey: key,
2864
+ column: change.column,
2865
+ value: change.previous
2866
+ })) : []), false);
2867
+ cells.push(...result.changes);
2868
+ problems.push(...result.issues);
2869
+ for (const [key] of entries) if (!problems.some((problem) => problem.rowKey === key)) pendingChanges.entries.delete(key);
2870
+ validation.cancel([...selected]);
2871
+ } finally {
2872
+ transaction = false;
2873
+ }
2874
+ const after = scene();
2875
+ if (before.data !== after.data || cells.length) undoStack.record({
2876
+ kind: "rows",
2877
+ before,
2878
+ after,
2879
+ keys: [...selected]
2880
+ });
2881
+ const next = sourceRows();
2882
+ const rowChanges = [...selected].flatMap((key) => {
2883
+ const old = previous.byKey.get(key);
2884
+ const row = next.byKey.get(key);
2885
+ return !old && row ? [{
2886
+ kind: "inserted",
2887
+ rowKey: key,
2888
+ row: row.original,
2889
+ position: positionOf(key)
2890
+ }] : old && !row ? [{
2891
+ kind: "removed",
2892
+ rowKey: key,
2893
+ row: old.original,
2894
+ position: positions.get(key)
2895
+ }] : [];
2896
+ });
2897
+ if (before.data !== after.data) options.onDataChange?.(data, cells);
2898
+ if (problems.length) options.onEditIssues?.(problems);
2899
+ notify();
2900
+ return resultForRows(rowChanges, problems, cells);
2901
+ }
1827
2902
  function rangeEdits(origin, matrix) {
1828
2903
  const { rows, columns } = getSnapshot();
1829
2904
  const edits = [];
@@ -1848,12 +2923,350 @@ function createTable(options) {
1848
2923
  value: direction === "undo" ? change.previous : change.value
1849
2924
  })), false);
1850
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
+ }
1851
3241
  if (expandMode === "single") {
1852
3242
  initialState = mergeState(initialState, { expanded: canonicalExpanded(initialState.expanded) });
1853
3243
  state = initialState;
1854
3244
  snapshot = void 0;
1855
3245
  }
1856
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
+ },
3261
+ insertRows,
3262
+ removeRows,
3263
+ revert,
3264
+ getPendingChanges: () => pendingChanges.snapshot(),
3265
+ markSaved(keys) {
3266
+ if (!keys) pendingChanges.entries.clear();
3267
+ else for (const key of keys) pendingChanges.entries.delete(key);
3268
+ notify();
3269
+ },
1857
3270
  getSnapshot,
1858
3271
  getState: () => state,
1859
3272
  subscribe(listener) {
@@ -1875,6 +3288,13 @@ function createTable(options) {
1875
3288
  },
1876
3289
  setData(next) {
1877
3290
  if (next === data) return;
3291
+ validation.cancel();
3292
+ streaming.cancel();
3293
+ loadState = "idle";
3294
+ streamIssues = [];
3295
+ ingestionIssues = [];
3296
+ expectedRowCount = sourceExpected;
3297
+ pendingChanges.entries.clear();
1878
3298
  abortLoads(true);
1879
3299
  lazyChildren.clear();
1880
3300
  childStatuses.clear();
@@ -1889,6 +3309,7 @@ function createTable(options) {
1889
3309
  setColumns(next) {
1890
3310
  if (next === definitions) return;
1891
3311
  definitions = next;
3312
+ validation.cancel();
1892
3313
  notify();
1893
3314
  },
1894
3315
  setRowCount(next) {
@@ -2024,18 +3445,22 @@ function createTable(options) {
2024
3445
  commit({ columnOrder: order });
2025
3446
  },
2026
3447
  edit(edits) {
2027
- return applyChanges(Array.isArray(edits) ? edits : [edits], true);
3448
+ return validation.edit(Array.isArray(edits) ? edits : [edits]);
2028
3449
  },
2029
3450
  undo() {
2030
- const changes = undoStack.undo();
2031
- if (!changes) return false;
2032
- replay(changes, "undo");
3451
+ const entry = undoStack.undo();
3452
+ if (!entry) return false;
3453
+ validation.cancel();
3454
+ if (entry.kind === "cells") replay(entry.changes, "undo");
3455
+ else restoreScene(entry, "undo");
2033
3456
  return true;
2034
3457
  },
2035
3458
  redo() {
2036
- const changes = undoStack.redo();
2037
- if (!changes) return false;
2038
- replay(changes, "redo");
3459
+ const entry = undoStack.redo();
3460
+ if (!entry) return false;
3461
+ validation.cancel();
3462
+ if (entry.kind === "cells") replay(entry.changes, "redo");
3463
+ else restoreScene(entry, "redo");
2039
3464
  return true;
2040
3465
  },
2041
3466
  copy(range, { escapeFormulas = false } = {}) {
@@ -2057,7 +3482,7 @@ function createTable(options) {
2057
3482
  return toDelimited(matrix);
2058
3483
  },
2059
3484
  paste(origin, text) {
2060
- if (!Number.isInteger(origin.row) || !Number.isInteger(origin.column) || origin.row < 0 || origin.column < 0) return applyChanges([], true, [issue("invalid_value", "The paste origin must use non-negative whole row and column indices.")]);
3485
+ if (!Number.isInteger(origin.row) || !Number.isInteger(origin.column) || origin.row < 0 || origin.column < 0) return validation.edit([], [issue("invalid_value", "The paste origin must use non-negative whole row and column indices.")]);
2061
3486
  const { rows, columns } = getSnapshot();
2062
3487
  const parsed = readDelimited(text, {
2063
3488
  maxRows: Math.max(0, rows.length - origin.row),
@@ -2066,18 +3491,19 @@ function createTable(options) {
2066
3491
  maxLength: maxPasteLength
2067
3492
  });
2068
3493
  const issues = parsed.truncated ? [issue("paste_truncated", "The paste exceeds the visible grid or parsing limits; only complete cells within those limits are applied.")] : [];
2069
- return applyChanges(rangeEdits(origin, parsed.rows), true, issues);
3494
+ return validation.edit(rangeEdits(origin, parsed.rows), issues);
2070
3495
  },
2071
3496
  clear(range) {
2072
3497
  const matrix = Array.from({ length: range.bottom - range.top + 1 }, () => Array.from({ length: range.right - range.left + 1 }, () => ""));
2073
3498
  const current = getSnapshot();
2074
- return applyChanges(rangeEdits({
3499
+ const edits = rangeEdits({
2075
3500
  row: range.top,
2076
3501
  column: range.left
2077
3502
  }, matrix).filter((edit) => {
2078
3503
  const row = current.getRow(edit.rowKey);
2079
3504
  return row !== void 0 && current.getColumn(edit.column)?.isEditable(row.original) === true;
2080
- }), true);
3505
+ });
3506
+ return validation.edit(edits);
2081
3507
  },
2082
3508
  exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true, escapeFormulas = true, depth = false, indent = "" } = {}) {
2083
3509
  const current = getSnapshot();
@@ -2102,6 +3528,8 @@ function createTable(options) {
2102
3528
  },
2103
3529
  destroy() {
2104
3530
  destroyed = true;
3531
+ streaming.cancel();
3532
+ validation.cancel();
2105
3533
  abortLoads(true);
2106
3534
  listeners.clear();
2107
3535
  }