@bilig/headless 0.1.102 → 0.3.0

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.
@@ -1,15 +1,68 @@
1
- import { SpreadsheetEngine, attachRuntimeSnapshot, makeCellKey, readRuntimeSnapshot, } from '@bilig/core';
1
+ import { SpreadsheetEngine, attachRuntimeSnapshot, makeCellKey, readRuntimeImage, readRuntimeSnapshot, } from '@bilig/core';
2
2
  import { ErrorCode, MAX_COLS, MAX_ROWS, ValueTag, } from '@bilig/protocol';
3
- import { excelSerialToDateParts, formatAddress, formatRangeAddress, installExternalFunctionAdapter, isArrayValue, isCellReferenceText, parseCellAddress, parseFormula, parseRangeAddress, serializeFormula, translateFormulaReferences, } from '@bilig/formula';
4
- import { loadInitialMixedSheet, tryLoadInitialLiteralSheet } from './initial-sheet-load.js';
3
+ import { excelSerialToDateParts, formatAddress, formatRangeAddress, indexToColumn, installExternalFunctionAdapter, isArrayValue, isCellReferenceText, parseCellAddress, parseFormula, parseRangeAddress, serializeFormula, translateFormulaReferences, } from '@bilig/formula';
4
+ import { loadInitialLiteralSheet, loadInitialMixedSheet, tryLoadInitialLiteralSheet } from './initial-sheet-load.js';
5
5
  import { orderWorkPaperCellChanges } from './change-order.js';
6
6
  import { WorkPaperConfigValueTooBigError, WorkPaperConfigValueTooSmallError, WorkPaperEvaluationSuspendedError, WorkPaperExpectedValueOfTypeError, WorkPaperOperationError, WorkPaperParseError, WorkPaperSheetError, WorkPaperExpectedOneOfValuesError, WorkPaperFunctionPluginValidationError, WorkPaperInvalidArgumentsError, WorkPaperLanguageAlreadyRegisteredError, WorkPaperLanguageNotRegisteredError, WorkPaperNamedExpressionDoesNotExistError, WorkPaperNamedExpressionNameIsAlreadyTakenError, WorkPaperNamedExpressionNameIsInvalidError, WorkPaperNoOperationToRedoError, WorkPaperNoOperationToUndoError, WorkPaperNoRelativeAddressesAllowedError, WorkPaperNoSheetWithIdError, WorkPaperNoSheetWithNameError, WorkPaperNotAFormulaError, WorkPaperNothingToPasteError, WorkPaperSheetNameAlreadyTakenError, WorkPaperSheetSizeLimitExceededError, WorkPaperUnableToParseError, } from './work-paper-errors.js';
7
7
  import { buildMatrixMutationPlan } from './matrix-mutation-plan.js';
8
+ import { readFastPhysicalRangeValues } from './fast-range-read.js';
8
9
  import { captureTrackedEngineEvent } from './tracked-engine-event-refs.js';
10
+ import { detachTrackedIndexChanges, hasDeferredTrackedIndexChanges, materializeTrackedIndexChangeSourcesWithMetadata, materializeTrackedIndexChangesWithMetadata, } from './tracked-cell-index-changes.js';
9
11
  import { calculateWorkPaperFormulaInScratchWorkbook } from './work-paper-scratch-evaluator.js';
10
12
  import { replaceWorkPaperSheetContent } from './work-paper-sheet-replacement.js';
13
+ const TRUSTED_TRACKED_PHYSICAL_SHEET_ID_PROPERTY = '__biligTrackedPhysicalSheetId';
14
+ const TRUSTED_TRACKED_PHYSICAL_SORTED_SPLIT_PROPERTY = '__biligTrackedPhysicalSortedSliceSplit';
15
+ function readTrustedPhysicalTrackedChangeMetadata(changedCellIndices) {
16
+ const trustedPhysicalSheetId = Reflect.get(changedCellIndices, TRUSTED_TRACKED_PHYSICAL_SHEET_ID_PROPERTY);
17
+ if (typeof trustedPhysicalSheetId !== 'number' || !Number.isInteger(trustedPhysicalSheetId) || trustedPhysicalSheetId < 0) {
18
+ return undefined;
19
+ }
20
+ const trustedSortedSliceSplit = Reflect.get(changedCellIndices, TRUSTED_TRACKED_PHYSICAL_SORTED_SPLIT_PROPERTY);
21
+ return typeof trustedSortedSliceSplit === 'number' && Number.isInteger(trustedSortedSliceSplit) && trustedSortedSliceSplit > 0
22
+ ? { trustedPhysicalSheetId, trustedSortedSliceSplit }
23
+ : { trustedPhysicalSheetId };
24
+ }
11
25
  const EMPTY_NAMED_EXPRESSION_VALUES = new Map();
26
+ const FAST_EXISTING_NUMERIC_LITERAL_FLAGS = 2 /* CellFlags.HasFormula */ | 4 /* CellFlags.JsOnly */ | 8 /* CellFlags.InCycle */ | 64 /* CellFlags.SpillChild */ | 128 /* CellFlags.PivotOutput */;
12
27
  const VISIBILITY_SHEET_STRIDE = MAX_ROWS * MAX_COLS;
28
+ const TINY_TRACKED_CHANGE_LIMIT = 4;
29
+ const RUNTIME_COLUMN_LABEL_CACHE = [];
30
+ const RUNTIME_A1_CACHE_COLUMN_LIMIT = 64;
31
+ const RUNTIME_A1_CACHE_ROW_LIMIT = 4096;
32
+ const RUNTIME_A1_CACHE = [];
33
+ function countPotentialNewTrackedCellMutations(refs) {
34
+ let count = 0;
35
+ for (let index = 0; index < refs.length; index += 1) {
36
+ const ref = refs[index];
37
+ if (ref && ref.cellIndex === undefined && ref.mutation.kind !== 'clearCell') {
38
+ count += 1;
39
+ }
40
+ }
41
+ return count;
42
+ }
43
+ function canSkipDimensionUpdateAfterLiteralMutation(refs, potentialNewCells) {
44
+ if (potentialNewCells !== 0 || refs.length !== 1) {
45
+ return false;
46
+ }
47
+ const ref = refs[0];
48
+ return ref?.cellIndex !== undefined && ref.mutation.kind === 'setCellValue' && ref.mutation.value !== null;
49
+ }
50
+ function readTrackedRuntimeCellValue(cellStore, cellIndex, strings) {
51
+ const tag = cellStore.tags[cellIndex] ?? ValueTag.Empty;
52
+ switch (tag) {
53
+ case ValueTag.Number:
54
+ return { tag: ValueTag.Number, value: cellStore.numbers[cellIndex] ?? 0 };
55
+ case ValueTag.Boolean:
56
+ return { tag: ValueTag.Boolean, value: (cellStore.numbers[cellIndex] ?? 0) !== 0 };
57
+ case ValueTag.String:
58
+ return cellStore.getValue(cellIndex, (stringId) => strings.get(stringId));
59
+ case ValueTag.Error:
60
+ return { tag: ValueTag.Error, code: cellStore.errors[cellIndex] };
61
+ case ValueTag.Empty:
62
+ default:
63
+ return { tag: ValueTag.Empty };
64
+ }
65
+ }
13
66
  const DEFAULT_CONFIG = Object.freeze({
14
67
  accentSensitive: false,
15
68
  caseSensitive: false,
@@ -281,9 +334,6 @@ function matrixContainsFormulaContent(content) {
281
334
  function isDeferredBatchLiteralContent(content) {
282
335
  return content === null || typeof content === 'boolean' || typeof content === 'number' || typeof content === 'string';
283
336
  }
284
- function canUseInitialMixedSheetFastPath(content) {
285
- return content.some((row) => row.some((value) => typeof value === 'string' && value.trim().startsWith('=')));
286
- }
287
337
  function stripLeadingEquals(formula) {
288
338
  return formula.trim().startsWith('=') ? formula.trim().slice(1) : formula.trim();
289
339
  }
@@ -508,17 +558,123 @@ function validateWorkPaperConfig(config) {
508
558
  }
509
559
  }
510
560
  }
511
- function validateSheetWithinLimits(sheetName, sheet, config) {
561
+ function inspectSheetDimensionsWithinLimits(sheetName, sheet, config) {
512
562
  const height = sheet.length;
513
- const width = Math.max(0, ...sheet.map((row) => row.length));
563
+ let width = 0;
564
+ let materializedHeight = 0;
565
+ let materializedWidth = 0;
566
+ for (let rowIndex = 0; rowIndex < sheet.length; rowIndex += 1) {
567
+ const row = sheet[rowIndex];
568
+ if (!Array.isArray(row)) {
569
+ throw new WorkPaperUnableToParseError({ sheetName, reason: 'Rows must be arrays' });
570
+ }
571
+ width = Math.max(width, row.length);
572
+ for (let colIndex = 0; colIndex < row.length; colIndex += 1) {
573
+ if (row[colIndex] !== null) {
574
+ materializedHeight = Math.max(materializedHeight, rowIndex + 1);
575
+ materializedWidth = Math.max(materializedWidth, colIndex + 1);
576
+ }
577
+ }
578
+ }
514
579
  if (height > (config.maxRows ?? MAX_ROWS) || width > (config.maxColumns ?? MAX_COLS)) {
515
580
  throw new WorkPaperSheetSizeLimitExceededError();
516
581
  }
517
- sheet.forEach((row) => {
582
+ return { width: materializedWidth, height: materializedHeight };
583
+ }
584
+ function inspectRuntimeSnapshotSheetDimensionsWithinLimits(args) {
585
+ let materializedHeight = 0;
586
+ let materializedWidth = 0;
587
+ const dimensions = args.runtimeSheetCells?.dimensions;
588
+ if (dimensions &&
589
+ Number.isInteger(dimensions.width) &&
590
+ Number.isInteger(dimensions.height) &&
591
+ dimensions.width >= 0 &&
592
+ dimensions.height >= 0) {
593
+ materializedWidth = dimensions.width;
594
+ materializedHeight = dimensions.height;
595
+ }
596
+ else if (args.runtimeSheetCells?.coords) {
597
+ for (const coords of args.runtimeSheetCells.coords) {
598
+ materializedHeight = Math.max(materializedHeight, coords.row + 1);
599
+ materializedWidth = Math.max(materializedWidth, coords.col + 1);
600
+ }
601
+ }
602
+ else {
603
+ for (const cell of args.snapshotSheet.cells) {
604
+ const parsed = parseCellAddress(cell.address, args.sheetName);
605
+ materializedHeight = Math.max(materializedHeight, parsed.row + 1);
606
+ materializedWidth = Math.max(materializedWidth, parsed.col + 1);
607
+ }
608
+ }
609
+ if (materializedHeight > (args.config.maxRows ?? MAX_ROWS) || materializedWidth > (args.config.maxColumns ?? MAX_COLS)) {
610
+ throw new WorkPaperSheetSizeLimitExceededError();
611
+ }
612
+ return { width: materializedWidth, height: materializedHeight };
613
+ }
614
+ function inspectSheetWithinLimits(sheetName, sheet, config) {
615
+ const height = sheet.length;
616
+ let width = 0;
617
+ let materializedHeight = 0;
618
+ let materializedWidth = 0;
619
+ let materializedCellCount = 0;
620
+ let formulaCellCount = 0;
621
+ let hasFormula = false;
622
+ for (let rowIndex = 0; rowIndex < sheet.length; rowIndex += 1) {
623
+ const row = sheet[rowIndex];
518
624
  if (!Array.isArray(row)) {
519
625
  throw new WorkPaperUnableToParseError({ sheetName, reason: 'Rows must be arrays' });
520
626
  }
521
- });
627
+ width = Math.max(width, row.length);
628
+ for (let colIndex = 0; colIndex < row.length; colIndex += 1) {
629
+ const cell = row[colIndex];
630
+ if (cell !== null) {
631
+ materializedCellCount += 1;
632
+ materializedHeight = Math.max(materializedHeight, rowIndex + 1);
633
+ materializedWidth = Math.max(materializedWidth, colIndex + 1);
634
+ }
635
+ if (typeof cell === 'string' && cellHasFormulaPrefix(cell)) {
636
+ formulaCellCount += 1;
637
+ hasFormula = true;
638
+ }
639
+ }
640
+ }
641
+ if (height > (config.maxRows ?? MAX_ROWS) || width > (config.maxColumns ?? MAX_COLS)) {
642
+ throw new WorkPaperSheetSizeLimitExceededError();
643
+ }
644
+ return {
645
+ hasFormula,
646
+ dimensions: { width: materializedWidth, height: materializedHeight },
647
+ materializedCellCount,
648
+ maxColumnCount: width,
649
+ formulaCellCount,
650
+ };
651
+ }
652
+ function cellHasFormulaPrefix(value) {
653
+ const first = value.charCodeAt(0);
654
+ if (first === 61) {
655
+ return true;
656
+ }
657
+ if (first !== 32 && first !== 9 && first !== 10 && first !== 13) {
658
+ return false;
659
+ }
660
+ return value.trimStart().charCodeAt(0) === 61;
661
+ }
662
+ function runtimeSnapshotMatchesSheetNames(sheets, runtimeSnapshot) {
663
+ const sheetNames = Object.keys(sheets);
664
+ if (runtimeSnapshot.sheets.length !== sheetNames.length) {
665
+ return false;
666
+ }
667
+ const matchedNames = new Set();
668
+ for (const snapshotSheet of runtimeSnapshot.sheets) {
669
+ if (!Object.prototype.hasOwnProperty.call(sheets, snapshotSheet.name) || matchedNames.has(snapshotSheet.name)) {
670
+ return false;
671
+ }
672
+ matchedNames.add(snapshotSheet.name);
673
+ }
674
+ return true;
675
+ }
676
+ function validateSheetWithinLimits(sheetName, sheet, config) {
677
+ inspectSheetWithinLimits(sheetName, sheet, config);
522
678
  }
523
679
  function functionPluginIds(config) {
524
680
  return (config.functionPlugins ?? []).map((plugin) => plugin.id).toSorted();
@@ -612,6 +768,9 @@ class WorkPaperEmitter {
612
768
  };
613
769
  this.onDetailed(eventName, wrapper);
614
770
  }
771
+ hasListeners(eventName) {
772
+ return this.listeners[eventName].size > 0 || this.detailedListeners[eventName].size > 0;
773
+ }
615
774
  emitDetailed(event) {
616
775
  this.dispatchDetailed(event);
617
776
  }
@@ -708,6 +867,8 @@ export class WorkPaper {
708
867
  visibilityCache = null;
709
868
  namedExpressionValueCache = null;
710
869
  sheetRecordsCache = null;
870
+ sheetDimensionsCache = new Map();
871
+ spillSheetIdsCache = null;
711
872
  batchDepth = 0;
712
873
  batchStartVisibility = null;
713
874
  batchStartNamedValues = null;
@@ -723,9 +884,124 @@ export class WorkPaper {
723
884
  suspendedCellMutationPotentialNewCells = 0;
724
885
  queuedEvents = [];
725
886
  trackedEngineEvents = [];
887
+ pendingLazyTrackedChanges = [];
726
888
  engineEventCaptureEnabled = true;
889
+ retainedTrackedEngineEventIndicesDepth = 0;
727
890
  unsubscribeEngineEvents = null;
728
891
  disposed = false;
892
+ getVisibleCellIndex(sheetId, row, col) {
893
+ const sheet = this.engine.workbook.getSheetById(sheetId);
894
+ if (!sheet) {
895
+ return undefined;
896
+ }
897
+ return this.getVisibleCellIndexInSheet(sheet, row, col);
898
+ }
899
+ getVisibleCellIndexInSheet(sheet, row, col) {
900
+ if (sheet.structureVersion === 1) {
901
+ const cellIndex = sheet.grid.getPhysical(row, col);
902
+ return cellIndex === -1 ? undefined : cellIndex;
903
+ }
904
+ return sheet.logical.getVisibleCell(row, col);
905
+ }
906
+ cacheSheetDimensions(sheetId, dimensions) {
907
+ this.sheetDimensionsCache.set(sheetId, { width: dimensions.width, height: dimensions.height });
908
+ }
909
+ cacheInitializedSheetDimensions(sheetId, dimensions) {
910
+ if (this.sheetHasSpills(sheetId)) {
911
+ this.sheetDimensionsCache.delete(sheetId);
912
+ return;
913
+ }
914
+ this.cacheSheetDimensions(sheetId, dimensions);
915
+ }
916
+ sheetHasSpills(sheetId) {
917
+ if (this.spillSheetIdsCache === null) {
918
+ const spillSheetIds = new Set();
919
+ this.engine.workbook.listSpills().forEach((spill) => {
920
+ const spillSheet = this.engine.workbook.getSheet(spill.sheetName);
921
+ if (spillSheet) {
922
+ spillSheetIds.add(spillSheet.id);
923
+ }
924
+ });
925
+ this.spillSheetIdsCache = spillSheetIds;
926
+ }
927
+ return this.spillSheetIdsCache.has(sheetId);
928
+ }
929
+ scanSheetDimensions(sheet) {
930
+ let width = 0;
931
+ let height = 0;
932
+ sheet.grid.forEachCellEntry((_cellIndex, row, col) => {
933
+ height = Math.max(height, row + 1);
934
+ width = Math.max(width, col + 1);
935
+ });
936
+ return { width, height };
937
+ }
938
+ invalidateSheetDimensions(sheetId) {
939
+ this.sheetDimensionsCache.delete(sheetId);
940
+ }
941
+ invalidateAllSheetDimensions() {
942
+ this.sheetDimensionsCache.clear();
943
+ this.spillSheetIdsCache = null;
944
+ }
945
+ expandCachedSheetDimensions(sheetId, row, col) {
946
+ const cached = this.sheetDimensionsCache.get(sheetId);
947
+ if (!cached) {
948
+ return;
949
+ }
950
+ cached.height = Math.max(cached.height, row + 1);
951
+ cached.width = Math.max(cached.width, col + 1);
952
+ }
953
+ invalidateCachedSheetDimensionsIfEdge(sheetId, row, col) {
954
+ const cached = this.sheetDimensionsCache.get(sheetId);
955
+ if (!cached) {
956
+ return;
957
+ }
958
+ if (row + 1 >= cached.height || col + 1 >= cached.width) {
959
+ this.invalidateSheetDimensions(sheetId);
960
+ }
961
+ }
962
+ updateSheetDimensionsAfterCellMutationRefs(refs) {
963
+ if (this.sheetDimensionsCache.size === 0) {
964
+ return;
965
+ }
966
+ if (refs.length === 1) {
967
+ const ref = refs[0];
968
+ const mutation = ref?.mutation;
969
+ if (ref && mutation && mutation.kind !== 'setCellFormula') {
970
+ const cached = this.sheetDimensionsCache.get(ref.sheetId);
971
+ if (!cached) {
972
+ return;
973
+ }
974
+ const noKnownSpills = this.spillSheetIdsCache !== null && !this.spillSheetIdsCache.has(ref.sheetId);
975
+ if (noKnownSpills &&
976
+ (mutation.kind === 'setCellValue'
977
+ ? mutation.row + 1 <= cached.height && mutation.col + 1 <= cached.width
978
+ : mutation.row + 1 < cached.height && mutation.col + 1 < cached.width)) {
979
+ return;
980
+ }
981
+ }
982
+ }
983
+ for (let index = 0; index < refs.length; index += 1) {
984
+ const ref = refs[index];
985
+ if (!ref) {
986
+ continue;
987
+ }
988
+ const mutation = ref.mutation;
989
+ if (mutation.kind === 'setCellFormula') {
990
+ this.spillSheetIdsCache = null;
991
+ this.invalidateSheetDimensions(ref.sheetId);
992
+ continue;
993
+ }
994
+ if (this.sheetHasSpills(ref.sheetId)) {
995
+ this.invalidateSheetDimensions(ref.sheetId);
996
+ continue;
997
+ }
998
+ if (mutation.kind === 'clearCell') {
999
+ this.invalidateCachedSheetDimensionsIfEdge(ref.sheetId, mutation.row, mutation.col);
1000
+ continue;
1001
+ }
1002
+ this.expandCachedSheetDimensions(ref.sheetId, mutation.row, mutation.col);
1003
+ }
1004
+ }
729
1005
  constructor(configInput = {}) {
730
1006
  ensureCustomAdapterInstalled();
731
1007
  validateWorkPaperConfig(configInput);
@@ -738,6 +1014,7 @@ export class WorkPaper {
738
1014
  useColumnIndex: this.config.useColumnIndex,
739
1015
  trackReplicaVersions: false,
740
1016
  });
1017
+ this.invalidateAllSheetDimensions();
741
1018
  this.attachEngineEventTracking();
742
1019
  this.captureFunctionRegistry();
743
1020
  this.internals = Object.freeze({
@@ -811,38 +1088,69 @@ export class WorkPaper {
811
1088
  static buildFromSheets(sheets, configInput = {}, namedExpressions = []) {
812
1089
  const workbook = new WorkPaper(configInput);
813
1090
  const runtimeSnapshot = namedExpressions.length === 0 ? readRuntimeSnapshot(sheets) : undefined;
814
- const runtimeSnapshotMatchesSheets = runtimeSnapshot !== undefined &&
815
- runtimeSnapshot.sheets.length === Object.keys(sheets).length &&
816
- runtimeSnapshot.sheets.every((sheet) => Object.prototype.hasOwnProperty.call(sheets, sheet.name));
1091
+ const runtimeSnapshotMatchesSheets = runtimeSnapshot !== undefined && runtimeSnapshotMatchesSheetNames(sheets, runtimeSnapshot);
1092
+ const runtimeSnapshotSheetsByName = runtimeSnapshotMatchesSheets
1093
+ ? new Map(runtimeSnapshot.sheets.map((sheet) => [sheet.name, sheet]))
1094
+ : undefined;
1095
+ const runtimeImageSheetCellsByName = runtimeSnapshotMatchesSheets && runtimeSnapshot
1096
+ ? new Map((readRuntimeImage(runtimeSnapshot)?.sheetCells ?? []).map((sheet) => [sheet.sheetName, sheet]))
1097
+ : undefined;
1098
+ const inspectedSheets = new Map();
817
1099
  Object.entries(sheets).forEach(([sheetName, sheet]) => {
818
- validateSheetWithinLimits(sheetName, sheet, workbook.config);
1100
+ const snapshotSheet = runtimeSnapshotSheetsByName?.get(sheetName);
1101
+ inspectedSheets.set(sheetName, snapshotSheet
1102
+ ? (() => {
1103
+ const dimensions = inspectRuntimeSnapshotSheetDimensionsWithinLimits({
1104
+ sheetName,
1105
+ snapshotSheet,
1106
+ runtimeSheetCells: runtimeImageSheetCellsByName?.get(sheetName),
1107
+ config: workbook.config,
1108
+ });
1109
+ return {
1110
+ hasFormula: false,
1111
+ dimensions,
1112
+ materializedCellCount: runtimeImageSheetCellsByName?.get(sheetName)?.cellCount ?? 0,
1113
+ maxColumnCount: dimensions.width,
1114
+ formulaCellCount: 0,
1115
+ };
1116
+ })()
1117
+ : inspectSheetWithinLimits(sheetName, sheet, workbook.config));
819
1118
  });
820
1119
  workbook.withEngineEventCaptureDisabled(() => {
821
1120
  if (runtimeSnapshot && runtimeSnapshotMatchesSheets) {
822
1121
  workbook.engine.importSnapshot(runtimeSnapshot);
823
- return;
824
1122
  }
825
- Object.keys(sheets).forEach((sheetName) => {
826
- workbook.engine.createSheet(sheetName);
827
- });
828
- namedExpressions.forEach((expression) => {
829
- workbook.upsertNamedExpressionInternal(expression, { duringInitialization: true });
830
- });
831
- Object.entries(sheets).forEach(([sheetName, sheet]) => {
832
- const sheetId = workbook.requireSheetId(sheetName);
833
- if (tryLoadInitialLiteralSheet(workbook.engine, sheetId, sheet)) {
834
- return;
835
- }
836
- if (canUseInitialMixedSheetFastPath(sheet)) {
1123
+ else {
1124
+ Object.keys(sheets).forEach((sheetName) => {
1125
+ workbook.engine.createSheet(sheetName);
1126
+ });
1127
+ namedExpressions.forEach((expression) => {
1128
+ workbook.upsertNamedExpressionInternal(expression, { duringInitialization: true });
1129
+ });
1130
+ Object.entries(sheets).forEach(([sheetName, sheet]) => {
1131
+ const sheetId = workbook.requireSheetId(sheetName);
1132
+ const inspected = inspectedSheets.get(sheetName);
1133
+ if (!inspected?.hasFormula) {
1134
+ loadInitialLiteralSheet(workbook.engine, sheetId, sheet, inspected);
1135
+ return;
1136
+ }
1137
+ const rewriteInitialFormula = workbook.namedExpressions.size === 0 && workbook.functionAliasLookup.size === 0
1138
+ ? (formula) => formula
1139
+ : (formula) => workbook.rewriteFormulaForStorage(formula, sheetId);
837
1140
  loadInitialMixedSheet({
838
1141
  engine: workbook.engine,
839
1142
  sheetId,
840
1143
  content: sheet,
841
- rewriteFormula: (formula, destination) => workbook.rewriteFormulaForStorage(formula, destination.sheet),
1144
+ rewriteFormula: rewriteInitialFormula,
1145
+ inspection: inspected,
842
1146
  });
843
- return;
1147
+ });
1148
+ }
1149
+ Object.keys(sheets).forEach((sheetName) => {
1150
+ const inspected = inspectedSheets.get(sheetName);
1151
+ if (inspected) {
1152
+ workbook.cacheInitializedSheetDimensions(workbook.requireSheetId(sheetName), inspected.dimensions);
844
1153
  }
845
- workbook.replaceSheetContentInternal(sheetId, sheet, { duringInitialization: true });
846
1154
  });
847
1155
  });
848
1156
  workbook.clearHistoryStacks();
@@ -1008,6 +1316,7 @@ export class WorkPaper {
1008
1316
  }
1009
1317
  updateConfig(next) {
1010
1318
  this.assertNotDisposed();
1319
+ this.materializePendingLazyTrackedChanges();
1011
1320
  const merged = {
1012
1321
  ...this.config,
1013
1322
  ...cloneConfig(next),
@@ -1043,9 +1352,11 @@ export class WorkPaper {
1043
1352
  }
1044
1353
  rebuildAndRecalculate() {
1045
1354
  this.assertNotDisposed();
1355
+ this.materializePendingLazyTrackedChanges();
1046
1356
  if (this.shouldSuppressEvents()) {
1047
1357
  try {
1048
1358
  this.engine.recalculateNow();
1359
+ this.invalidateAllSheetDimensions();
1049
1360
  }
1050
1361
  catch (error) {
1051
1362
  throw new WorkPaperOperationError(this.messageOf(error, 'Recalculation failed'));
@@ -1057,6 +1368,7 @@ export class WorkPaper {
1057
1368
  this.drainTrackedEngineEvents();
1058
1369
  try {
1059
1370
  this.engine.recalculateNow();
1371
+ this.invalidateAllSheetDimensions();
1060
1372
  }
1061
1373
  catch (error) {
1062
1374
  throw new WorkPaperOperationError(this.messageOf(error, 'Recalculation failed'));
@@ -1069,13 +1381,14 @@ export class WorkPaper {
1069
1381
  ...this.computeCellChanges(beforeVisibility, afterVisibility),
1070
1382
  ...this.computeNamedExpressionChanges(beforeNames, afterNames),
1071
1383
  ];
1072
- if (changes.length > 0) {
1384
+ if (changes.length > 0 && this.emitter.hasListeners('valuesUpdated')) {
1073
1385
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
1074
1386
  }
1075
1387
  return changes;
1076
1388
  }
1077
1389
  batch(batchOperations) {
1078
1390
  this.assertNotDisposed();
1391
+ this.materializePendingLazyTrackedChanges();
1079
1392
  const isOutermost = this.batchDepth === 0;
1080
1393
  if (isOutermost) {
1081
1394
  this.batchUsesTrackedFastPath = this.canUseTrackedMutationFastPath();
@@ -1097,22 +1410,32 @@ export class WorkPaper {
1097
1410
  finally {
1098
1411
  this.batchDepth -= 1;
1099
1412
  if (isOutermost) {
1100
- this.flushPendingBatchOps();
1413
+ if (this.batchUsesTrackedFastPath) {
1414
+ this.withRetainedTrackedEngineEventIndices(() => {
1415
+ this.flushPendingBatchOps();
1416
+ });
1417
+ }
1418
+ else {
1419
+ this.flushPendingBatchOps();
1420
+ }
1101
1421
  this.mergeUndoHistory(this.batchUndoStackLength);
1102
1422
  }
1103
1423
  }
1104
1424
  if (!isOutermost) {
1105
1425
  return [];
1106
1426
  }
1427
+ const shouldEmitValuesUpdated = this.emitter.hasListeners('valuesUpdated');
1107
1428
  const changes = this.batchUsesTrackedFastPath
1108
- ? this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents())
1429
+ ? this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents(), {
1430
+ preferLazyPublicChanges: !shouldEmitValuesUpdated,
1431
+ })
1109
1432
  : this.computeChangesAfterMutation(this.batchStartVisibility ?? new Map(), this.batchStartNamedValues ?? new Map());
1110
1433
  this.batchUsesTrackedFastPath = false;
1111
1434
  this.batchStartVisibility = null;
1112
1435
  this.batchStartNamedValues = null;
1113
1436
  if (!this.evaluationSuspended) {
1114
1437
  this.flushQueuedEvents();
1115
- if (changes.length > 0) {
1438
+ if (changes.length > 0 && shouldEmitValuesUpdated) {
1116
1439
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
1117
1440
  }
1118
1441
  }
@@ -1120,6 +1443,7 @@ export class WorkPaper {
1120
1443
  }
1121
1444
  suspendEvaluation() {
1122
1445
  this.assertNotDisposed();
1446
+ this.materializePendingLazyTrackedChanges();
1123
1447
  if (this.evaluationSuspended) {
1124
1448
  return;
1125
1449
  }
@@ -1140,12 +1464,23 @@ export class WorkPaper {
1140
1464
  }
1141
1465
  resumeEvaluation() {
1142
1466
  this.assertNotDisposed();
1467
+ this.materializePendingLazyTrackedChanges();
1143
1468
  if (!this.evaluationSuspended) {
1144
1469
  return [];
1145
1470
  }
1146
- this.flushSuspendedCellMutations();
1471
+ if (this.suspendedUsesTrackedFastPath) {
1472
+ this.withRetainedTrackedEngineEventIndices(() => {
1473
+ this.flushSuspendedCellMutations();
1474
+ });
1475
+ }
1476
+ else {
1477
+ this.flushSuspendedCellMutations();
1478
+ }
1479
+ const shouldEmitValuesUpdated = this.emitter.hasListeners('valuesUpdated');
1147
1480
  const changes = this.suspendedUsesTrackedFastPath
1148
- ? this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents())
1481
+ ? this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents(), {
1482
+ preferLazyPublicChanges: !shouldEmitValuesUpdated,
1483
+ })
1149
1484
  : this.computeChangesAfterMutation(this.suspendedVisibility ?? new Map(), this.suspendedNamedValues ?? new Map());
1150
1485
  this.evaluationSuspended = false;
1151
1486
  this.suspendedVisibility = null;
@@ -1153,7 +1488,7 @@ export class WorkPaper {
1153
1488
  this.suspendedUsesTrackedFastPath = false;
1154
1489
  this.flushQueuedEvents();
1155
1490
  this.emitter.emitDetailed({ eventName: 'evaluationResumed', payload: { changes } });
1156
- if (changes.length > 0) {
1491
+ if (changes.length > 0 && shouldEmitValuesUpdated) {
1157
1492
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
1158
1493
  }
1159
1494
  return changes;
@@ -1163,19 +1498,39 @@ export class WorkPaper {
1163
1498
  }
1164
1499
  undo() {
1165
1500
  this.assertNotDisposed();
1501
+ const preservesPositions = !this.historyTopIsCellMutations(this.getUndoStack());
1502
+ if (this.canUseTrackedMutationFastPath()) {
1503
+ return this.captureTrackedChangesWithoutVisibilityCache(() => {
1504
+ if (!this.engine.undo()) {
1505
+ throw new WorkPaperNoOperationToUndoError();
1506
+ }
1507
+ this.invalidateAllSheetDimensions();
1508
+ }, { preservePendingTrackedPositions: preservesPositions });
1509
+ }
1166
1510
  return this.captureChanges(undefined, () => {
1167
1511
  if (!this.engine.undo()) {
1168
1512
  throw new WorkPaperNoOperationToUndoError();
1169
1513
  }
1170
- });
1514
+ this.invalidateAllSheetDimensions();
1515
+ }, { preservePendingTrackedPositions: preservesPositions });
1171
1516
  }
1172
1517
  redo() {
1173
1518
  this.assertNotDisposed();
1519
+ const preservesPositions = !this.historyTopIsCellMutations(this.getRedoStack());
1520
+ if (this.canUseTrackedMutationFastPath()) {
1521
+ return this.captureTrackedChangesWithoutVisibilityCache(() => {
1522
+ if (!this.engine.redo()) {
1523
+ throw new WorkPaperNoOperationToRedoError();
1524
+ }
1525
+ this.invalidateAllSheetDimensions();
1526
+ }, { preservePendingTrackedPositions: preservesPositions });
1527
+ }
1174
1528
  return this.captureChanges(undefined, () => {
1175
1529
  if (!this.engine.redo()) {
1176
1530
  throw new WorkPaperNoOperationToRedoError();
1177
1531
  }
1178
- });
1532
+ this.invalidateAllSheetDimensions();
1533
+ }, { preservePendingTrackedPositions: preservesPositions });
1179
1534
  }
1180
1535
  isThereSomethingToUndo() {
1181
1536
  return this.getUndoStack().length > 0;
@@ -1254,7 +1609,11 @@ export class WorkPaper {
1254
1609
  }
1255
1610
  getCellValue(address) {
1256
1611
  this.assertReadable();
1257
- return cloneCellValue(this.engine.getCellValue(this.sheetName(address.sheet), this.a1(address)));
1612
+ const sheet = this.sheetRecord(address.sheet);
1613
+ const cellIndex = this.getVisibleCellIndexInSheet(sheet, address.row, address.col);
1614
+ return cellIndex === undefined
1615
+ ? emptyValue()
1616
+ : readTrackedRuntimeCellValue(this.engine.workbook.cellStore, cellIndex, this.engine.strings);
1258
1617
  }
1259
1618
  getCellFormula(address) {
1260
1619
  this.prepareReadableState();
@@ -1282,6 +1641,11 @@ export class WorkPaper {
1282
1641
  }
1283
1642
  getRangeValues(range) {
1284
1643
  this.assertReadable();
1644
+ assertRange(range);
1645
+ const fastValues = readFastPhysicalRangeValues(this.engine, range);
1646
+ if (fastValues !== undefined) {
1647
+ return fastValues;
1648
+ }
1285
1649
  const ref = this.rangeRef(range);
1286
1650
  return this.engine.getRangeValues(ref);
1287
1651
  }
@@ -1340,13 +1704,13 @@ export class WorkPaper {
1340
1704
  getSheetDimensions(sheetId) {
1341
1705
  this.prepareReadableState();
1342
1706
  const sheet = this.sheetRecord(sheetId);
1343
- let width = 0;
1344
- let height = 0;
1345
- sheet.grid.forEachCellEntry((_cellIndex, row, col) => {
1346
- height = Math.max(height, row + 1);
1347
- width = Math.max(width, col + 1);
1348
- });
1349
- return { width, height };
1707
+ const cached = this.sheetDimensionsCache.get(sheetId);
1708
+ if (cached) {
1709
+ return { width: cached.width, height: cached.height };
1710
+ }
1711
+ const dimensions = this.scanSheetDimensions(sheet);
1712
+ this.cacheSheetDimensions(sheetId, dimensions);
1713
+ return dimensions;
1350
1714
  }
1351
1715
  simpleCellAddressFromString(value, defaultSheetId) {
1352
1716
  this.assertNotDisposed();
@@ -1744,6 +2108,81 @@ export class WorkPaper {
1744
2108
  const { hours, minutes, seconds } = dateTime;
1745
2109
  return { hours, minutes, seconds };
1746
2110
  }
2111
+ trySetExistingNumericCellContentsWithTrackedFastPath(args) {
2112
+ if (!this.canUseTrackedMutationFastPath() || args.sheet.structureVersion !== 1) {
2113
+ return null;
2114
+ }
2115
+ const cellStore = this.engine.workbook.cellStore;
2116
+ if (cellStore.sheetIds[args.cellIndex] !== args.address.sheet ||
2117
+ cellStore.rows[args.cellIndex] !== args.address.row ||
2118
+ cellStore.cols[args.cellIndex] !== args.address.col ||
2119
+ (cellStore.formulaIds[args.cellIndex] ?? 0) !== 0 ||
2120
+ ((cellStore.flags[args.cellIndex] ?? 0) & FAST_EXISTING_NUMERIC_LITERAL_FLAGS) !== 0 ||
2121
+ cellStore.tags[args.cellIndex] !== ValueTag.Number) {
2122
+ return null;
2123
+ }
2124
+ const existingNumericMutationEngine = this.engine;
2125
+ if (typeof existingNumericMutationEngine.tryApplyExistingNumericCellMutationAt !== 'function') {
2126
+ return null;
2127
+ }
2128
+ if (this.pendingLazyTrackedChanges.length > 0) {
2129
+ this.materializePendingLazyTrackedChanges();
2130
+ }
2131
+ if (this.trackedEngineEvents.length > 0) {
2132
+ this.drainTrackedEngineEvents();
2133
+ }
2134
+ let result = null;
2135
+ const oldNumericValue = cellStore.numbers[args.cellIndex] ?? 0;
2136
+ const previousCaptureEnabled = this.engineEventCaptureEnabled;
2137
+ this.engineEventCaptureEnabled = false;
2138
+ try {
2139
+ if (this.pendingBatchOps.length > 0) {
2140
+ this.flushPendingBatchOps();
2141
+ }
2142
+ const request = {
2143
+ sheetId: args.address.sheet,
2144
+ row: args.address.row,
2145
+ col: args.address.col,
2146
+ cellIndex: args.cellIndex,
2147
+ value: args.value,
2148
+ emitTracked: false,
2149
+ trustedExistingNumericLiteral: true,
2150
+ oldNumericValue,
2151
+ };
2152
+ result = existingNumericMutationEngine.tryApplyExistingNumericCellMutationAt(request);
2153
+ }
2154
+ catch (error) {
2155
+ if (error instanceof Error && WORKPAPER_PUBLIC_ERROR_NAMES.has(error.name)) {
2156
+ throw error;
2157
+ }
2158
+ throw new WorkPaperOperationError(this.messageOf(error, 'Mutation failed'));
2159
+ }
2160
+ finally {
2161
+ this.engineEventCaptureEnabled = previousCaptureEnabled;
2162
+ }
2163
+ if (!result) {
2164
+ return null;
2165
+ }
2166
+ if (this.trackedEngineEvents.length > 0) {
2167
+ this.trackedEngineEvents = [];
2168
+ }
2169
+ let changes = this.tryBuildDirectExistingNumericTrackedChanges(result, args.address, args.cellIndex, true, args.sheet.name, args.value);
2170
+ if (changes === null) {
2171
+ const shouldEmitValuesUpdated = this.emitter.hasListeners('valuesUpdated');
2172
+ const events = [this.trackedEventFromExistingNumericMutationResult(result)];
2173
+ changes = this.computeTrackedChangesWithoutVisibilityCache(events, {
2174
+ preferLazyPublicChanges: !shouldEmitValuesUpdated,
2175
+ });
2176
+ if (changes.length > 0 && shouldEmitValuesUpdated) {
2177
+ this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
2178
+ }
2179
+ return changes;
2180
+ }
2181
+ if (changes.length > 0 && this.emitter.hasListeners('valuesUpdated')) {
2182
+ this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
2183
+ }
2184
+ return changes;
2185
+ }
1747
2186
  setCellContents(address, content) {
1748
2187
  this.assertNotDisposed();
1749
2188
  const sheet = this.sheetRecord(address.sheet);
@@ -1753,15 +2192,40 @@ export class WorkPaper {
1753
2192
  if (address.row >= (this.config.maxRows ?? MAX_ROWS) || address.col >= (this.config.maxColumns ?? MAX_COLS)) {
1754
2193
  throw new WorkPaperOperationError('Cell contents cannot be set');
1755
2194
  }
1756
- const existingCellIndex = sheet.grid.get(address.row, address.col);
1757
- if (this.enqueueSuspendedLiteralMutation(address.sheet, address.row, address.col, content, existingCellIndex)) {
2195
+ const visibleCellIndex = this.getVisibleCellIndexInSheet(sheet, address.row, address.col);
2196
+ if (this.evaluationSuspended &&
2197
+ this.enqueueSuspendedLiteralMutation(address.sheet, address.row, address.col, content, visibleCellIndex)) {
1758
2198
  return [];
1759
2199
  }
1760
- if (this.enqueueDeferredBatchLiteral(address.sheet, address.row, address.col, content, existingCellIndex)) {
2200
+ if (this.batchDepth !== 0 && this.enqueueDeferredBatchLiteral(address.sheet, address.row, address.col, content, visibleCellIndex)) {
1761
2201
  return [];
1762
2202
  }
2203
+ if (typeof content === 'number' && visibleCellIndex !== undefined) {
2204
+ const fastPathChanges = this.trySetExistingNumericCellContentsWithTrackedFastPath({
2205
+ sheet,
2206
+ address,
2207
+ cellIndex: visibleCellIndex,
2208
+ value: content,
2209
+ });
2210
+ if (fastPathChanges !== null) {
2211
+ return fastPathChanges;
2212
+ }
2213
+ }
1763
2214
  const mutate = () => {
1764
2215
  this.flushPendingBatchOps();
2216
+ const existingNumericMutationEngine = this.engine;
2217
+ if (typeof content === 'number' &&
2218
+ visibleCellIndex !== undefined &&
2219
+ sheet.structureVersion === 1 &&
2220
+ existingNumericMutationEngine.tryApplyExistingNumericCellMutationAt?.({
2221
+ sheetId: address.sheet,
2222
+ row: address.row,
2223
+ col: address.col,
2224
+ cellIndex: visibleCellIndex,
2225
+ value: content,
2226
+ })) {
2227
+ return;
2228
+ }
1765
2229
  const mutation = content === null
1766
2230
  ? { kind: 'clearCell', row: address.row, col: address.col }
1767
2231
  : typeof content === 'string' && content.trim().startsWith('=')
@@ -1777,16 +2241,26 @@ export class WorkPaper {
1777
2241
  col: address.col,
1778
2242
  value: content,
1779
2243
  };
1780
- this.applyCellMutationRefs([{ sheetId: address.sheet, mutation }], {
2244
+ this.applyCellMutationRefs([{ sheetId: address.sheet, mutation, ...(visibleCellIndex !== undefined ? { cellIndex: visibleCellIndex } : {}) }], {
1781
2245
  captureUndo: true,
1782
- potentialNewCells: content === null || existingCellIndex !== -1 ? 0 : 1,
2246
+ potentialNewCells: content === null || visibleCellIndex !== undefined ? 0 : 1,
1783
2247
  source: 'local',
1784
2248
  returnUndoOps: false,
1785
2249
  reuseRefs: true,
1786
2250
  });
1787
2251
  };
1788
2252
  if (this.canUseTrackedMutationFastPath()) {
1789
- return this.captureTrackedChangesWithoutVisibilityCache(mutate);
2253
+ return this.captureTrackedChangesWithoutVisibilityCache(mutate, {
2254
+ singleLiteralChange: isFormulaContent(content)
2255
+ ? undefined
2256
+ : {
2257
+ address: { sheet: address.sheet, row: address.row, col: address.col },
2258
+ ...(visibleCellIndex === undefined ? {} : { cellIndex: visibleCellIndex }),
2259
+ isPhysicalSheet: sheet.structureVersion === 1,
2260
+ sheetName: sheet.name,
2261
+ value: content,
2262
+ },
2263
+ });
1790
2264
  }
1791
2265
  return this.captureChanges(undefined, () => {
1792
2266
  mutate();
@@ -1884,9 +2358,17 @@ export class WorkPaper {
1884
2358
  if (!this.isItPossibleToAddRows(sheetId, ...indexes)) {
1885
2359
  throw new WorkPaperOperationError('Rows cannot be added');
1886
2360
  }
2361
+ if (indexes.length === 1 && this.canUseTrackedStructuralFastPath()) {
2362
+ const [start, amount] = indexes[0];
2363
+ return this.captureTrackedChangesWithoutVisibilityCache(() => {
2364
+ this.engine.insertRows(this.sheetName(sheetId), start, amount);
2365
+ this.invalidateSheetDimensions(sheetId);
2366
+ });
2367
+ }
1887
2368
  return this.batchStructuralChanges(() => {
1888
2369
  indexes.forEach(([start, amount]) => {
1889
2370
  this.engine.insertRows(this.sheetName(sheetId), start, amount);
2371
+ this.invalidateSheetDimensions(sheetId);
1890
2372
  });
1891
2373
  });
1892
2374
  }
@@ -1895,11 +2377,19 @@ export class WorkPaper {
1895
2377
  if (!this.isItPossibleToRemoveRows(sheetId, ...indexes)) {
1896
2378
  throw new WorkPaperOperationError('Rows cannot be removed');
1897
2379
  }
2380
+ if (indexes.length === 1 && this.canUseTrackedStructuralFastPath()) {
2381
+ const [start, amount] = indexes[0];
2382
+ return this.captureTrackedChangesWithoutVisibilityCache(() => {
2383
+ this.engine.deleteRows(this.sheetName(sheetId), start, amount);
2384
+ this.invalidateSheetDimensions(sheetId);
2385
+ });
2386
+ }
1898
2387
  return this.batchStructuralChanges(() => {
1899
2388
  indexes
1900
2389
  .toSorted((left, right) => right[0] - left[0])
1901
2390
  .forEach(([start, amount]) => {
1902
2391
  this.engine.deleteRows(this.sheetName(sheetId), start, amount);
2392
+ this.invalidateSheetDimensions(sheetId);
1903
2393
  });
1904
2394
  });
1905
2395
  }
@@ -1908,9 +2398,17 @@ export class WorkPaper {
1908
2398
  if (!this.isItPossibleToAddColumns(sheetId, ...indexes)) {
1909
2399
  throw new WorkPaperOperationError('Columns cannot be added');
1910
2400
  }
2401
+ if (indexes.length === 1 && this.canUseTrackedStructuralFastPath()) {
2402
+ const [start, amount] = indexes[0];
2403
+ return this.captureTrackedChangesWithoutVisibilityCache(() => {
2404
+ this.engine.insertColumns(this.sheetName(sheetId), start, amount);
2405
+ this.invalidateSheetDimensions(sheetId);
2406
+ });
2407
+ }
1911
2408
  return this.batchStructuralChanges(() => {
1912
2409
  indexes.forEach(([start, amount]) => {
1913
2410
  this.engine.insertColumns(this.sheetName(sheetId), start, amount);
2411
+ this.invalidateSheetDimensions(sheetId);
1914
2412
  });
1915
2413
  });
1916
2414
  }
@@ -1919,11 +2417,19 @@ export class WorkPaper {
1919
2417
  if (!this.isItPossibleToRemoveColumns(sheetId, ...indexes)) {
1920
2418
  throw new WorkPaperOperationError('Columns cannot be removed');
1921
2419
  }
2420
+ if (indexes.length === 1 && this.canUseTrackedStructuralFastPath()) {
2421
+ const [start, amount] = indexes[0];
2422
+ return this.captureTrackedChangesWithoutVisibilityCache(() => {
2423
+ this.engine.deleteColumns(this.sheetName(sheetId), start, amount);
2424
+ this.invalidateSheetDimensions(sheetId);
2425
+ });
2426
+ }
1922
2427
  return this.batchStructuralChanges(() => {
1923
2428
  indexes
1924
2429
  .toSorted((left, right) => right[0] - left[0])
1925
2430
  .forEach(([start, amount]) => {
1926
2431
  this.engine.deleteColumns(this.sheetName(sheetId), start, amount);
2432
+ this.invalidateSheetDimensions(sheetId);
1927
2433
  });
1928
2434
  });
1929
2435
  }
@@ -1939,6 +2445,8 @@ export class WorkPaper {
1939
2445
  startAddress: formatAddress(target.row, target.col),
1940
2446
  endAddress: formatAddress(target.row + sourceHeight, target.col + sourceWidth),
1941
2447
  });
2448
+ this.invalidateSheetDimensions(source.start.sheet);
2449
+ this.invalidateSheetDimensions(target.sheet);
1942
2450
  });
1943
2451
  }
1944
2452
  moveRows(sheetId, start, count, target) {
@@ -1946,11 +2454,13 @@ export class WorkPaper {
1946
2454
  throw new WorkPaperOperationError('Rows cannot be moved');
1947
2455
  }
1948
2456
  return this.canUseTrackedStructuralFastPath()
1949
- ? this.batchStructuralChanges(() => {
2457
+ ? this.captureTrackedChangesWithoutVisibilityCache(() => {
1950
2458
  this.engine.moveRows(this.sheetName(sheetId), start, count, target);
2459
+ this.invalidateSheetDimensions(sheetId);
1951
2460
  })
1952
2461
  : this.captureChanges(undefined, () => {
1953
2462
  this.engine.moveRows(this.sheetName(sheetId), start, count, target);
2463
+ this.invalidateSheetDimensions(sheetId);
1954
2464
  });
1955
2465
  }
1956
2466
  moveColumns(sheetId, start, count, target) {
@@ -1958,15 +2468,18 @@ export class WorkPaper {
1958
2468
  throw new WorkPaperOperationError('Columns cannot be moved');
1959
2469
  }
1960
2470
  return this.canUseTrackedStructuralFastPath()
1961
- ? this.batchStructuralChanges(() => {
2471
+ ? this.captureTrackedChangesWithoutVisibilityCache(() => {
1962
2472
  this.engine.moveColumns(this.sheetName(sheetId), start, count, target);
2473
+ this.invalidateSheetDimensions(sheetId);
1963
2474
  })
1964
2475
  : this.captureChanges(undefined, () => {
1965
2476
  this.engine.moveColumns(this.sheetName(sheetId), start, count, target);
2477
+ this.invalidateSheetDimensions(sheetId);
1966
2478
  });
1967
2479
  }
1968
2480
  addSheet(sheetName) {
1969
2481
  this.assertNotDisposed();
2482
+ this.materializePendingLazyTrackedChanges();
1970
2483
  const name = sheetName?.trim() || this.nextSheetName();
1971
2484
  if (!this.isItPossibleToAddSheet(name)) {
1972
2485
  throw new WorkPaperSheetNameAlreadyTakenError(name);
@@ -1977,6 +2490,7 @@ export class WorkPaper {
1977
2490
  this.engine.createSheet(name);
1978
2491
  this.sheetRecordsCache = null;
1979
2492
  const sheetId = this.requireSheetId(name);
2493
+ this.cacheSheetDimensions(sheetId, { width: 0, height: 0 });
1980
2494
  const payload = { sheetId, sheetName: name };
1981
2495
  if (this.shouldSuppressEvents()) {
1982
2496
  this.queuedEvents.push({ eventName: 'sheetAdded', payload });
@@ -1985,7 +2499,7 @@ export class WorkPaper {
1985
2499
  this.emitter.emitDetailed({ eventName: 'sheetAdded', payload });
1986
2500
  }
1987
2501
  const changes = this.computeChangesAfterMutation(beforeVisibility, beforeNames);
1988
- if (!this.shouldSuppressEvents() && changes.length > 0) {
2502
+ if (!this.shouldSuppressEvents() && changes.length > 0 && this.emitter.hasListeners('valuesUpdated')) {
1989
2503
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
1990
2504
  }
1991
2505
  return name;
@@ -2005,6 +2519,7 @@ export class WorkPaper {
2005
2519
  }, () => {
2006
2520
  this.engine.deleteSheet(sheetName);
2007
2521
  this.sheetRecordsCache = null;
2522
+ this.invalidateSheetDimensions(sheetId);
2008
2523
  });
2009
2524
  }
2010
2525
  clearSheet(sheetId) {
@@ -2021,6 +2536,7 @@ export class WorkPaper {
2021
2536
  startAddress: 'A1',
2022
2537
  endAddress: formatAddress(dimensions.height - 1, dimensions.width - 1),
2023
2538
  });
2539
+ this.cacheSheetDimensions(sheetId, { width: 0, height: 0 });
2024
2540
  });
2025
2541
  }
2026
2542
  setSheetContent(sheetId, content) {
@@ -2207,6 +2723,7 @@ export class WorkPaper {
2207
2723
  if (this.disposed) {
2208
2724
  return;
2209
2725
  }
2726
+ this.materializePendingLazyTrackedChanges();
2210
2727
  this.disposed = true;
2211
2728
  this.unsubscribeEngineEvents?.();
2212
2729
  this.unsubscribeEngineEvents = null;
@@ -2215,8 +2732,11 @@ export class WorkPaper {
2215
2732
  this.clipboard = null;
2216
2733
  this.visibilityCache = null;
2217
2734
  this.namedExpressionValueCache = null;
2735
+ this.sheetDimensionsCache.clear();
2736
+ this.spillSheetIdsCache = null;
2218
2737
  this.queuedEvents = [];
2219
2738
  this.trackedEngineEvents = [];
2739
+ this.pendingLazyTrackedChanges = [];
2220
2740
  this.namedExpressions.clear();
2221
2741
  }
2222
2742
  attachEngineEventTracking() {
@@ -2229,9 +2749,21 @@ export class WorkPaper {
2229
2749
  if (!this.engineEventCaptureEnabled) {
2230
2750
  return;
2231
2751
  }
2232
- this.trackedEngineEvents.push(captureTrackedEngineEvent(event));
2752
+ this.trackedEngineEvents.push(captureTrackedEngineEvent(event, {
2753
+ borrowChangedCellIndexViews: this.retainedTrackedEngineEventIndicesDepth > 0,
2754
+ cloneChangedCellIndices: this.retainedTrackedEngineEventIndicesDepth === 0,
2755
+ }));
2233
2756
  });
2234
2757
  }
2758
+ withRetainedTrackedEngineEventIndices(callback) {
2759
+ this.retainedTrackedEngineEventIndicesDepth += 1;
2760
+ try {
2761
+ return callback();
2762
+ }
2763
+ finally {
2764
+ this.retainedTrackedEngineEventIndicesDepth -= 1;
2765
+ }
2766
+ }
2235
2767
  withEngineEventCaptureDisabled(callback) {
2236
2768
  const previous = this.engineEventCaptureEnabled;
2237
2769
  this.engineEventCaptureEnabled = false;
@@ -2249,6 +2781,77 @@ export class WorkPaper {
2249
2781
  this.trackedEngineEvents = [];
2250
2782
  return events;
2251
2783
  }
2784
+ existingNumericMutationChangedCellCount(result) {
2785
+ return result.changedCellIndices?.length ?? result.changedCellCount ?? 0;
2786
+ }
2787
+ existingNumericMutationChangedCellAt(result, index) {
2788
+ if (result.changedCellIndices) {
2789
+ return result.changedCellIndices[index];
2790
+ }
2791
+ if (index === 0) {
2792
+ return result.firstChangedCellIndex;
2793
+ }
2794
+ if (index === 1) {
2795
+ return result.secondChangedCellIndex;
2796
+ }
2797
+ return undefined;
2798
+ }
2799
+ materializeExistingNumericMutationChangedCellIndices(result) {
2800
+ if (result.changedCellIndices) {
2801
+ return result.changedCellIndices;
2802
+ }
2803
+ const count = this.existingNumericMutationChangedCellCount(result);
2804
+ const changed = new Uint32Array(count);
2805
+ for (let index = 0; index < count; index += 1) {
2806
+ changed[index] = this.existingNumericMutationChangedCellAt(result, index) ?? 0;
2807
+ }
2808
+ return changed;
2809
+ }
2810
+ trackedEventFromExistingNumericMutationResult(result) {
2811
+ let sortedDisjoint = true;
2812
+ let previous = -1;
2813
+ let firstChangedCellIndex;
2814
+ let lastChangedCellIndex;
2815
+ const changedCellCount = this.existingNumericMutationChangedCellCount(result);
2816
+ for (let index = 0; index < changedCellCount; index += 1) {
2817
+ const cellIndex = this.existingNumericMutationChangedCellAt(result, index) ?? -1;
2818
+ if (index === 0) {
2819
+ firstChangedCellIndex = cellIndex;
2820
+ }
2821
+ if (!Number.isInteger(cellIndex) || cellIndex < 0 || cellIndex <= previous) {
2822
+ sortedDisjoint = false;
2823
+ }
2824
+ previous = cellIndex;
2825
+ lastChangedCellIndex = cellIndex;
2826
+ }
2827
+ return {
2828
+ invalidation: 'cells',
2829
+ changedCellIndices: this.materializeExistingNumericMutationChangedCellIndices(result),
2830
+ changedInputCount: 1,
2831
+ explicitChangedCount: result.explicitChangedCount,
2832
+ changedCellIndicesSortedDisjoint: sortedDisjoint,
2833
+ ...(firstChangedCellIndex === undefined ? {} : { firstChangedCellIndex }),
2834
+ ...(lastChangedCellIndex === undefined ? {} : { lastChangedCellIndex }),
2835
+ hasInvalidatedRanges: false,
2836
+ hasInvalidatedRows: false,
2837
+ hasInvalidatedColumns: false,
2838
+ };
2839
+ }
2840
+ trackLazyTrackedChanges(changes) {
2841
+ if (hasDeferredTrackedIndexChanges(changes)) {
2842
+ this.pendingLazyTrackedChanges.push(changes);
2843
+ }
2844
+ }
2845
+ materializePendingLazyTrackedChanges(options = {}) {
2846
+ if (this.pendingLazyTrackedChanges.length === 0) {
2847
+ return;
2848
+ }
2849
+ const pending = this.pendingLazyTrackedChanges;
2850
+ this.pendingLazyTrackedChanges = [];
2851
+ for (let index = 0; index < pending.length; index += 1) {
2852
+ detachTrackedIndexChanges(pending[index], { preservePositions: options.preservePositions });
2853
+ }
2854
+ }
2252
2855
  resetChangeTrackingCaches() {
2253
2856
  this.sheetRecordsCache = null;
2254
2857
  this.visibilityCache = null;
@@ -2278,11 +2881,12 @@ export class WorkPaper {
2278
2881
  this.pendingBatchPotentialNewCells = 0;
2279
2882
  this.engine.applyCellMutationsAtWithOptions(ops, {
2280
2883
  captureUndo: true,
2281
- potentialNewCells: potentialNewCells > 0 ? potentialNewCells : undefined,
2884
+ potentialNewCells,
2282
2885
  source: 'local',
2283
2886
  returnUndoOps: false,
2284
2887
  reuseRefs: true,
2285
2888
  });
2889
+ this.updateSheetDimensionsAfterCellMutationRefs(ops);
2286
2890
  }
2287
2891
  applyCellMutationRefs(refs, options) {
2288
2892
  if (this.evaluationSuspended && (options.source ?? 'local') === 'local') {
@@ -2294,6 +2898,7 @@ export class WorkPaper {
2294
2898
  const mutation = ref.mutation;
2295
2899
  this.suspendedCellMutationRefs.push({
2296
2900
  sheetId: ref.sheetId,
2901
+ ...(ref.cellIndex !== undefined ? { cellIndex: ref.cellIndex } : {}),
2297
2902
  mutation: mutation.kind === 'setCellValue'
2298
2903
  ? {
2299
2904
  kind: 'setCellValue',
@@ -2315,11 +2920,13 @@ export class WorkPaper {
2315
2920
  },
2316
2921
  });
2317
2922
  }
2318
- this.suspendedCellMutationPotentialNewCells +=
2319
- options.potentialNewCells ?? refs.reduce((count, ref) => (ref?.mutation.kind === 'clearCell' ? count : count + 1), 0);
2923
+ this.suspendedCellMutationPotentialNewCells += options.potentialNewCells ?? countPotentialNewTrackedCellMutations(refs);
2320
2924
  return;
2321
2925
  }
2322
2926
  this.engine.applyCellMutationsAtWithOptions(refs, options);
2927
+ if (!canSkipDimensionUpdateAfterLiteralMutation(refs, options.potentialNewCells)) {
2928
+ this.updateSheetDimensionsAfterCellMutationRefs(refs);
2929
+ }
2323
2930
  }
2324
2931
  flushSuspendedCellMutations() {
2325
2932
  if (this.suspendedCellMutationRefs.length === 0) {
@@ -2331,42 +2938,53 @@ export class WorkPaper {
2331
2938
  this.suspendedCellMutationPotentialNewCells = 0;
2332
2939
  this.engine.applyCellMutationsAtWithOptions(refs, {
2333
2940
  captureUndo: true,
2334
- potentialNewCells: potentialNewCells > 0 ? potentialNewCells : undefined,
2941
+ potentialNewCells,
2335
2942
  source: 'local',
2336
2943
  returnUndoOps: false,
2337
2944
  reuseRefs: true,
2338
2945
  });
2946
+ this.updateSheetDimensionsAfterCellMutationRefs(refs);
2339
2947
  }
2340
- enqueueSuspendedLiteralMutation(sheetId, row, col, content, existingCellIndex = this.engine.workbook.getSheetById(sheetId)?.grid.get(row, col) ?? -1) {
2948
+ enqueueSuspendedLiteralMutation(sheetId, row, col, content, cellIndex) {
2341
2949
  if (!this.evaluationSuspended || !isDeferredBatchLiteralContent(content) || isFormulaContent(content)) {
2342
2950
  return false;
2343
2951
  }
2344
2952
  if (content === null) {
2345
- this.suspendedCellMutationRefs.push({ sheetId, mutation: { kind: 'clearCell', row, col } });
2953
+ this.suspendedCellMutationRefs.push({
2954
+ sheetId,
2955
+ mutation: { kind: 'clearCell', row, col },
2956
+ ...(cellIndex !== undefined ? { cellIndex } : {}),
2957
+ });
2346
2958
  return true;
2347
2959
  }
2348
2960
  this.suspendedCellMutationRefs.push({
2349
2961
  sheetId,
2350
2962
  mutation: { kind: 'setCellValue', row, col, value: content },
2963
+ ...(cellIndex !== undefined ? { cellIndex } : {}),
2351
2964
  });
2352
- if (existingCellIndex === -1) {
2965
+ if (cellIndex === undefined) {
2353
2966
  this.suspendedCellMutationPotentialNewCells += 1;
2354
2967
  }
2355
2968
  return true;
2356
2969
  }
2357
- enqueueDeferredBatchLiteral(sheetId, row, col, content, existingCellIndex = this.engine.workbook.getSheetById(sheetId)?.grid.get(row, col) ?? -1) {
2970
+ enqueueDeferredBatchLiteral(sheetId, row, col, content, cellIndex) {
2358
2971
  if (this.batchDepth === 0 || this.evaluationSuspended || !isDeferredBatchLiteralContent(content) || isFormulaContent(content)) {
2359
2972
  return false;
2360
2973
  }
2361
2974
  if (content === null) {
2362
- this.pendingBatchOps.push({ sheetId, mutation: { kind: 'clearCell', row, col } });
2975
+ this.pendingBatchOps.push({
2976
+ sheetId,
2977
+ mutation: { kind: 'clearCell', row, col },
2978
+ ...(cellIndex !== undefined ? { cellIndex } : {}),
2979
+ });
2363
2980
  return true;
2364
2981
  }
2365
2982
  this.pendingBatchOps.push({
2366
2983
  sheetId,
2367
2984
  mutation: { kind: 'setCellValue', row, col, value: content },
2985
+ ...(cellIndex !== undefined ? { cellIndex } : {}),
2368
2986
  });
2369
- if (existingCellIndex === -1) {
2987
+ if (cellIndex === undefined) {
2370
2988
  this.pendingBatchPotentialNewCells += 1;
2371
2989
  }
2372
2990
  return true;
@@ -2406,6 +3024,28 @@ export class WorkPaper {
2406
3024
  a1(address) {
2407
3025
  return formatAddress(address.row, address.col);
2408
3026
  }
3027
+ trackedA1(row, col) {
3028
+ if (row >= 0 && row < RUNTIME_A1_CACHE_ROW_LIMIT && col >= 0 && col < RUNTIME_A1_CACHE_COLUMN_LIMIT) {
3029
+ const cacheKey = row * RUNTIME_A1_CACHE_COLUMN_LIMIT + col;
3030
+ let cached = RUNTIME_A1_CACHE[cacheKey];
3031
+ if (cached === undefined) {
3032
+ let column = RUNTIME_COLUMN_LABEL_CACHE[col];
3033
+ if (column === undefined) {
3034
+ column = indexToColumn(col);
3035
+ RUNTIME_COLUMN_LABEL_CACHE[col] = column;
3036
+ }
3037
+ cached = `${column}${row + 1}`;
3038
+ RUNTIME_A1_CACHE[cacheKey] = cached;
3039
+ }
3040
+ return cached;
3041
+ }
3042
+ let column = RUNTIME_COLUMN_LABEL_CACHE[col];
3043
+ if (column === undefined) {
3044
+ column = indexToColumn(col);
3045
+ RUNTIME_COLUMN_LABEL_CACHE[col] = column;
3046
+ }
3047
+ return `${column}${row + 1}`;
3048
+ }
2409
3049
  rangeRef(range) {
2410
3050
  assertRange(range);
2411
3051
  return sourceRangeRef(this.sheetName(range.start.sheet), range);
@@ -2500,44 +3140,264 @@ export class WorkPaper {
2500
3140
  });
2501
3141
  return orderWorkPaperCellChanges(cellChanges, this.listSheetRecords());
2502
3142
  }
2503
- readTrackedCellChange(cellIndex) {
2504
- const sheetId = this.engine.workbook.cellStore.sheetIds[cellIndex];
2505
- const row = this.engine.workbook.cellStore.rows[cellIndex];
2506
- const col = this.engine.workbook.cellStore.cols[cellIndex];
2507
- if (sheetId === undefined || row === undefined || col === undefined) {
2508
- return undefined;
3143
+ materializeTrackedEventChanges(event, lazy = false) {
3144
+ if (event.patches && event.patches.length > 0) {
3145
+ const cellPatches = event.patches.filter((patch) => patch.kind === 'cell');
3146
+ return { changes: cellPatches, canReusePublicChanges: false, ordered: false };
3147
+ }
3148
+ const trustedPhysicalMetadata = lazy && event.changedCellIndices instanceof Uint32Array
3149
+ ? readTrustedPhysicalTrackedChangeMetadata(event.changedCellIndices)
3150
+ : undefined;
3151
+ const materialized = materializeTrackedIndexChangesWithMetadata(this.engine, event.changedCellIndices, {
3152
+ explicitChangedCount: event.explicitChangedCount,
3153
+ lazy,
3154
+ ...trustedPhysicalMetadata,
3155
+ });
3156
+ if (lazy) {
3157
+ this.trackLazyTrackedChanges(materialized.changes);
2509
3158
  }
2510
- const sheetName = this.engine.workbook.getSheetNameById(sheetId);
2511
- if (sheetName === undefined) {
3159
+ return {
3160
+ changes: materialized.changes,
3161
+ canReusePublicChanges: true,
3162
+ ordered: materialized.ordered,
3163
+ };
3164
+ }
3165
+ readSingleTrackedCellChange(cellIndex) {
3166
+ const cellStore = this.engine.workbook.cellStore;
3167
+ const sheetId = cellStore.sheetIds[cellIndex];
3168
+ if (sheetId === undefined) {
2512
3169
  return undefined;
2513
3170
  }
3171
+ const sheet = this.engine.workbook.getSheetById(sheetId);
3172
+ const sheetName = sheet?.name ?? this.engine.workbook.getSheetNameById(sheetId);
3173
+ let row;
3174
+ let col;
3175
+ if (!sheet || sheet.structureVersion === 1) {
3176
+ row = cellStore.rows[cellIndex];
3177
+ col = cellStore.cols[cellIndex];
3178
+ }
3179
+ else {
3180
+ const position = this.engine.workbook.getCellPosition(cellIndex);
3181
+ if (!position) {
3182
+ return undefined;
3183
+ }
3184
+ row = position.row;
3185
+ col = position.col;
3186
+ }
3187
+ const tag = cellStore.tags[cellIndex] ?? ValueTag.Empty;
3188
+ let newValue;
3189
+ switch (tag) {
3190
+ case ValueTag.Number:
3191
+ newValue = { tag: ValueTag.Number, value: cellStore.numbers[cellIndex] ?? 0 };
3192
+ break;
3193
+ case ValueTag.Boolean:
3194
+ newValue = { tag: ValueTag.Boolean, value: (cellStore.numbers[cellIndex] ?? 0) !== 0 };
3195
+ break;
3196
+ case ValueTag.String:
3197
+ newValue = cellStore.getValue(cellIndex, (stringId) => this.engine.strings.get(stringId));
3198
+ break;
3199
+ case ValueTag.Error:
3200
+ newValue = { tag: ValueTag.Error, code: cellStore.errors[cellIndex] };
3201
+ break;
3202
+ case ValueTag.Empty:
3203
+ default:
3204
+ newValue = { tag: ValueTag.Empty };
3205
+ break;
3206
+ }
2514
3207
  return {
2515
3208
  kind: 'cell',
2516
3209
  address: { sheet: sheetId, row, col },
2517
3210
  sheetName,
2518
- a1: formatAddress(row, col),
2519
- newValue: this.engine.workbook.cellStore.getValue(cellIndex, (id) => this.engine.strings.get(id)),
3211
+ a1: this.trackedA1(row, col),
3212
+ newValue,
2520
3213
  };
2521
3214
  }
2522
- materializeTrackedEventChanges(event) {
2523
- if (event.patches && event.patches.length > 0) {
2524
- const cellPatches = event.patches.filter((patch) => patch.kind === 'cell');
2525
- return cellPatches;
3215
+ readTinySortedPhysicalTrackedEventChanges(event) {
3216
+ if (!event.changedCellIndicesSortedDisjoint) {
3217
+ return null;
3218
+ }
3219
+ const cellStore = this.engine.workbook.cellStore;
3220
+ const firstCellIndex = event.changedCellIndices[0];
3221
+ if (firstCellIndex === undefined) {
3222
+ return [];
3223
+ }
3224
+ const sheetId = cellStore.sheetIds[firstCellIndex];
3225
+ if (sheetId === undefined) {
3226
+ return [];
3227
+ }
3228
+ const sheet = this.engine.workbook.getSheetById(sheetId);
3229
+ if (sheet && sheet.structureVersion !== 1) {
3230
+ return null;
3231
+ }
3232
+ const sheetName = sheet?.name ?? this.engine.workbook.getSheetNameById(sheetId);
3233
+ if (event.changedCellIndices.length === 1) {
3234
+ const row = cellStore.rows[firstCellIndex];
3235
+ const col = cellStore.cols[firstCellIndex];
3236
+ return [
3237
+ {
3238
+ kind: 'cell',
3239
+ address: { sheet: sheetId, row, col },
3240
+ sheetName,
3241
+ a1: this.trackedA1(row, col),
3242
+ newValue: readTrackedRuntimeCellValue(cellStore, firstCellIndex, this.engine.strings),
3243
+ },
3244
+ ];
3245
+ }
3246
+ if (event.changedCellIndices.length === 2) {
3247
+ const secondCellIndex = event.changedCellIndices[1];
3248
+ if (cellStore.sheetIds[secondCellIndex] !== sheetId) {
3249
+ return null;
3250
+ }
3251
+ const firstRow = cellStore.rows[firstCellIndex];
3252
+ const firstCol = cellStore.cols[firstCellIndex];
3253
+ const secondRow = cellStore.rows[secondCellIndex];
3254
+ const secondCol = cellStore.cols[secondCellIndex];
3255
+ if (secondRow < firstRow || (secondRow === firstRow && secondCol < firstCol)) {
3256
+ return null;
3257
+ }
3258
+ return [
3259
+ {
3260
+ kind: 'cell',
3261
+ address: { sheet: sheetId, row: firstRow, col: firstCol },
3262
+ sheetName,
3263
+ a1: this.trackedA1(firstRow, firstCol),
3264
+ newValue: readTrackedRuntimeCellValue(cellStore, firstCellIndex, this.engine.strings),
3265
+ },
3266
+ {
3267
+ kind: 'cell',
3268
+ address: { sheet: sheetId, row: secondRow, col: secondCol },
3269
+ sheetName,
3270
+ a1: this.trackedA1(secondRow, secondCol),
3271
+ newValue: readTrackedRuntimeCellValue(cellStore, secondCellIndex, this.engine.strings),
3272
+ },
3273
+ ];
2526
3274
  }
2527
3275
  const changes = [];
3276
+ let previousRow = -1;
3277
+ let previousCol = -1;
2528
3278
  for (let index = 0; index < event.changedCellIndices.length; index += 1) {
2529
- const change = this.readTrackedCellChange(event.changedCellIndices[index]);
2530
- if (change) {
2531
- changes.push(change);
3279
+ const cellIndex = event.changedCellIndices[index];
3280
+ if (cellStore.sheetIds[cellIndex] !== sheetId) {
3281
+ return null;
3282
+ }
3283
+ const row = cellStore.rows[cellIndex];
3284
+ const col = cellStore.cols[cellIndex];
3285
+ if (row < previousRow || (row === previousRow && col < previousCol)) {
3286
+ return null;
2532
3287
  }
3288
+ changes.push({
3289
+ kind: 'cell',
3290
+ address: { sheet: sheetId, row, col },
3291
+ sheetName,
3292
+ a1: this.trackedA1(row, col),
3293
+ newValue: readTrackedRuntimeCellValue(cellStore, cellIndex, this.engine.strings),
3294
+ });
3295
+ previousRow = row;
3296
+ previousCol = col;
2533
3297
  }
2534
3298
  return changes;
2535
3299
  }
2536
- computeCellChangesFromTrackedEvents(beforeVisibility, events) {
3300
+ tryReadTinyTrackedEventChangesWithoutVisibility(event) {
3301
+ if (event.patches !== undefined &&
3302
+ event.invalidation !== 'full' &&
3303
+ event.patches.length <= TINY_TRACKED_CHANGE_LIMIT &&
3304
+ !event.hasInvalidatedRanges &&
3305
+ !event.hasInvalidatedRows &&
3306
+ !event.hasInvalidatedColumns) {
3307
+ const changes = [];
3308
+ let alreadySorted = true;
3309
+ let previousSheetId = -1;
3310
+ let previousSheetOrder = -1;
3311
+ let previousRow = -1;
3312
+ let previousCol = -1;
3313
+ for (let index = 0; index < event.patches.length; index += 1) {
3314
+ const patch = event.patches[index];
3315
+ if (!patch || patch.kind !== 'cell') {
3316
+ return null;
3317
+ }
3318
+ const sheetOrder = patch.address.sheet === previousSheetId ? previousSheetOrder : this.sheetRecord(patch.address.sheet).order;
3319
+ if (sheetOrder < previousSheetOrder ||
3320
+ (sheetOrder === previousSheetOrder &&
3321
+ (patch.address.row < previousRow || (patch.address.row === previousRow && patch.address.col < previousCol)))) {
3322
+ alreadySorted = false;
3323
+ }
3324
+ changes.push({
3325
+ kind: 'cell',
3326
+ address: patch.address,
3327
+ sheetName: patch.sheetName,
3328
+ a1: patch.a1,
3329
+ newValue: patch.newValue,
3330
+ });
3331
+ previousSheetId = patch.address.sheet;
3332
+ previousSheetOrder = sheetOrder;
3333
+ previousRow = patch.address.row;
3334
+ previousCol = patch.address.col;
3335
+ }
3336
+ return alreadySorted ? changes : orderWorkPaperCellChanges(changes, this.listSheetRecords(), event.explicitChangedCount);
3337
+ }
3338
+ if (event.invalidation === 'full' ||
3339
+ event.patches !== undefined ||
3340
+ event.changedCellIndices.length > TINY_TRACKED_CHANGE_LIMIT ||
3341
+ event.hasInvalidatedRanges ||
3342
+ event.hasInvalidatedRows ||
3343
+ event.hasInvalidatedColumns) {
3344
+ return null;
3345
+ }
3346
+ if (event.changedCellIndices.length === 0) {
3347
+ return [];
3348
+ }
3349
+ const sortedPhysicalChanges = this.readTinySortedPhysicalTrackedEventChanges(event);
3350
+ if (sortedPhysicalChanges) {
3351
+ return sortedPhysicalChanges;
3352
+ }
3353
+ const changes = [];
3354
+ const cellKeys = [];
3355
+ let alreadySorted = true;
3356
+ let previousSheetId = -1;
3357
+ let previousSheetOrder = -1;
3358
+ let previousRow = -1;
3359
+ let previousCol = -1;
3360
+ for (let index = 0; index < event.changedCellIndices.length; index += 1) {
3361
+ const change = this.readSingleTrackedCellChange(event.changedCellIndices[index]);
3362
+ if (!change) {
3363
+ continue;
3364
+ }
3365
+ const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
3366
+ for (let priorIndex = 0; priorIndex < cellKeys.length; priorIndex += 1) {
3367
+ if (cellKeys[priorIndex] === cellKey) {
3368
+ return null;
3369
+ }
3370
+ }
3371
+ cellKeys.push(cellKey);
3372
+ const sheetOrder = change.address.sheet === previousSheetId ? previousSheetOrder : this.sheetRecord(change.address.sheet).order;
3373
+ if (sheetOrder < previousSheetOrder ||
3374
+ (sheetOrder === previousSheetOrder &&
3375
+ (change.address.row < previousRow || (change.address.row === previousRow && change.address.col < previousCol)))) {
3376
+ alreadySorted = false;
3377
+ }
3378
+ changes.push(change);
3379
+ previousSheetId = change.address.sheet;
3380
+ previousSheetOrder = sheetOrder;
3381
+ previousRow = change.address.row;
3382
+ previousCol = change.address.col;
3383
+ }
3384
+ return alreadySorted ? changes : orderWorkPaperCellChanges(changes, this.listSheetRecords(), event.explicitChangedCount);
3385
+ }
3386
+ computeCellChangesFromTrackedEvents(beforeVisibility, events, updateVisibility = true, options = {}) {
2537
3387
  if (events.some((event) => event.invalidation === 'full')) {
2538
3388
  return null;
2539
3389
  }
2540
3390
  const nextVisibility = beforeVisibility;
3391
+ const sheetOrders = new Map();
3392
+ const sheetOrderFor = (sheetId) => {
3393
+ const existing = sheetOrders.get(sheetId);
3394
+ if (existing !== undefined) {
3395
+ return existing;
3396
+ }
3397
+ const order = this.sheetRecord(sheetId).order;
3398
+ sheetOrders.set(sheetId, order);
3399
+ return order;
3400
+ };
2541
3401
  const ensureMutableSheet = (sheetId, sheetName) => {
2542
3402
  const existing = nextVisibility.get(sheetId);
2543
3403
  if (existing) {
@@ -2552,17 +3412,95 @@ export class WorkPaper {
2552
3412
  nextVisibility.set(sheetId, created);
2553
3413
  return created;
2554
3414
  };
3415
+ const tryReadSmallTrackedEventChanges = (event) => {
3416
+ if (event.invalidation === 'full' ||
3417
+ event.patches !== undefined ||
3418
+ event.changedCellIndices.length > 4 ||
3419
+ event.hasInvalidatedRanges ||
3420
+ event.hasInvalidatedRows ||
3421
+ event.hasInvalidatedColumns) {
3422
+ return null;
3423
+ }
3424
+ if (event.changedCellIndices.length === 0) {
3425
+ return [];
3426
+ }
3427
+ const changes = [];
3428
+ const cellKeys = [];
3429
+ let alreadySorted = true;
3430
+ let previousSheetOrder = -1;
3431
+ let previousRow = -1;
3432
+ let previousCol = -1;
3433
+ for (let index = 0; index < event.changedCellIndices.length; index += 1) {
3434
+ const change = this.readSingleTrackedCellChange(event.changedCellIndices[index]);
3435
+ if (!change) {
3436
+ continue;
3437
+ }
3438
+ const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
3439
+ for (let priorIndex = 0; priorIndex < cellKeys.length; priorIndex += 1) {
3440
+ if (cellKeys[priorIndex] === cellKey) {
3441
+ return null;
3442
+ }
3443
+ }
3444
+ cellKeys.push(cellKey);
3445
+ const sheet = updateVisibility ? ensureMutableSheet(change.address.sheet, change.sheetName) : undefined;
3446
+ const sheetOrder = sheet?.order ?? sheetOrderFor(change.address.sheet);
3447
+ if (sheetOrder < previousSheetOrder ||
3448
+ (sheetOrder === previousSheetOrder &&
3449
+ (change.address.row < previousRow || (change.address.row === previousRow && change.address.col < previousCol)))) {
3450
+ alreadySorted = false;
3451
+ }
3452
+ if (sheet) {
3453
+ if (change.newValue.tag === ValueTag.Empty) {
3454
+ sheet.cells.delete(cellKey);
3455
+ }
3456
+ else {
3457
+ sheet.cells.set(cellKey, change.newValue);
3458
+ }
3459
+ }
3460
+ changes.push(change);
3461
+ previousSheetOrder = sheetOrder;
3462
+ previousRow = change.address.row;
3463
+ previousCol = change.address.col;
3464
+ }
3465
+ return alreadySorted ? changes : orderWorkPaperCellChanges(changes, this.listSheetRecords(), event.explicitChangedCount);
3466
+ };
2555
3467
  if (events.length === 1) {
2556
3468
  const event = events[0];
2557
- const eventChanges = this.materializeTrackedEventChanges(event);
3469
+ if (!options.preferLazyPublicChanges) {
3470
+ const smallChanges = tryReadSmallTrackedEventChanges(event);
3471
+ if (smallChanges) {
3472
+ return { changes: smallChanges, nextVisibility };
3473
+ }
3474
+ }
3475
+ const materializedEventChanges = this.materializeTrackedEventChanges(event, !updateVisibility);
3476
+ const eventChanges = materializedEventChanges.changes;
3477
+ if (!updateVisibility && materializedEventChanges.canReusePublicChanges && materializedEventChanges.ordered) {
3478
+ return {
3479
+ changes: eventChanges,
3480
+ nextVisibility,
3481
+ };
3482
+ }
3483
+ const directChanges = [];
3484
+ const seenCellKeys = eventChanges.length > 4 && eventChanges.length <= 64 ? new Set() : undefined;
3485
+ const smallCellKeys = eventChanges.length > 1 && eventChanges.length <= 4 ? [] : undefined;
2558
3486
  let hasDuplicateCellKey = false;
2559
- if (eventChanges.length <= 4) {
2560
- for (let index = 0; index < eventChanges.length; index += 1) {
2561
- const change = eventChanges[index];
2562
- const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
3487
+ let alreadySorted = true;
3488
+ let previousSheetOrder = -1;
3489
+ let previousRow = -1;
3490
+ let previousCol = -1;
3491
+ for (let index = 0; index < eventChanges.length; index += 1) {
3492
+ const change = eventChanges[index];
3493
+ const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
3494
+ if (seenCellKeys) {
3495
+ if (seenCellKeys.has(cellKey)) {
3496
+ hasDuplicateCellKey = true;
3497
+ break;
3498
+ }
3499
+ seenCellKeys.add(cellKey);
3500
+ }
3501
+ else if (smallCellKeys) {
2563
3502
  for (let priorIndex = 0; priorIndex < index; priorIndex += 1) {
2564
- const prior = eventChanges[priorIndex];
2565
- if (makeCellKey(prior.address.sheet, prior.address.row, prior.address.col) === cellKey) {
3503
+ if (smallCellKeys[priorIndex] === cellKey) {
2566
3504
  hasDuplicateCellKey = true;
2567
3505
  break;
2568
3506
  }
@@ -2570,52 +3508,37 @@ export class WorkPaper {
2570
3508
  if (hasDuplicateCellKey) {
2571
3509
  break;
2572
3510
  }
3511
+ smallCellKeys[index] = cellKey;
2573
3512
  }
2574
- }
2575
- else {
2576
- const seenCellKeys = new Set();
2577
- for (let index = 0; index < eventChanges.length; index += 1) {
2578
- const change = eventChanges[index];
2579
- const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
2580
- if (seenCellKeys.has(cellKey)) {
2581
- hasDuplicateCellKey = true;
2582
- break;
2583
- }
2584
- seenCellKeys.add(cellKey);
3513
+ const sheet = updateVisibility ? ensureMutableSheet(change.address.sheet, change.sheetName) : undefined;
3514
+ const sheetOrder = sheet?.order ?? sheetOrderFor(change.address.sheet);
3515
+ if (sheetOrder < previousSheetOrder ||
3516
+ (sheetOrder === previousSheetOrder &&
3517
+ (change.address.row < previousRow || (change.address.row === previousRow && change.address.col < previousCol)))) {
3518
+ alreadySorted = false;
2585
3519
  }
2586
- }
2587
- if (!hasDuplicateCellKey) {
2588
- const directChanges = [];
2589
- let alreadySorted = true;
2590
- let previousSheetOrder = -1;
2591
- let previousRow = -1;
2592
- let previousCol = -1;
2593
- for (let index = 0; index < eventChanges.length; index += 1) {
2594
- const change = eventChanges[index];
2595
- const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
2596
- const sheet = ensureMutableSheet(change.address.sheet, change.sheetName);
2597
- if (sheet.order < previousSheetOrder ||
2598
- (sheet.order === previousSheetOrder &&
2599
- (change.address.row < previousRow || (change.address.row === previousRow && change.address.col < previousCol)))) {
2600
- alreadySorted = false;
2601
- }
3520
+ if (sheet) {
2602
3521
  if (change.newValue.tag === ValueTag.Empty) {
2603
3522
  sheet.cells.delete(cellKey);
2604
3523
  }
2605
3524
  else {
2606
3525
  sheet.cells.set(cellKey, change.newValue);
2607
3526
  }
2608
- directChanges[index] = {
3527
+ }
3528
+ directChanges[index] = materializedEventChanges.canReusePublicChanges
3529
+ ? change
3530
+ : {
2609
3531
  kind: 'cell',
2610
3532
  address: change.address,
2611
3533
  sheetName: change.sheetName,
2612
3534
  a1: change.a1,
2613
3535
  newValue: change.newValue,
2614
3536
  };
2615
- previousSheetOrder = sheet.order;
2616
- previousRow = change.address.row;
2617
- previousCol = change.address.col;
2618
- }
3537
+ previousSheetOrder = sheetOrder;
3538
+ previousRow = change.address.row;
3539
+ previousCol = change.address.col;
3540
+ }
3541
+ if (!hasDuplicateCellKey) {
2619
3542
  return {
2620
3543
  changes: alreadySorted
2621
3544
  ? directChanges
@@ -2624,9 +3547,36 @@ export class WorkPaper {
2624
3547
  };
2625
3548
  }
2626
3549
  }
3550
+ const materializedSources = updateVisibility
3551
+ ? null
3552
+ : materializeTrackedIndexChangeSourcesWithMetadata(this.engine, events, {
3553
+ deferLazyDetach: true,
3554
+ lazy: options.preferLazyPublicChanges,
3555
+ });
3556
+ if (materializedSources) {
3557
+ this.trackLazyTrackedChanges(materializedSources.changes);
3558
+ if (updateVisibility) {
3559
+ for (const change of materializedSources.changes) {
3560
+ const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
3561
+ const sheet = ensureMutableSheet(change.address.sheet, change.sheetName);
3562
+ if (change.newValue.tag === ValueTag.Empty) {
3563
+ sheet.cells.delete(cellKey);
3564
+ }
3565
+ else {
3566
+ sheet.cells.set(cellKey, change.newValue);
3567
+ }
3568
+ }
3569
+ }
3570
+ return {
3571
+ changes: materializedSources.ordered
3572
+ ? materializedSources.changes
3573
+ : orderWorkPaperCellChanges(materializedSources.changes, this.listSheetRecords()),
3574
+ nextVisibility,
3575
+ };
3576
+ }
2627
3577
  const latestChangesByKey = new Map();
2628
3578
  for (const event of events) {
2629
- const eventChanges = this.materializeTrackedEventChanges(event);
3579
+ const eventChanges = this.materializeTrackedEventChanges(event).changes;
2630
3580
  for (let index = 0; index < eventChanges.length; index += 1) {
2631
3581
  const change = eventChanges[index];
2632
3582
  const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
@@ -2640,14 +3590,16 @@ export class WorkPaper {
2640
3590
  });
2641
3591
  }
2642
3592
  }
2643
- for (const change of latestChangesByKey.values()) {
2644
- const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
2645
- const sheet = ensureMutableSheet(change.address.sheet, change.sheetName);
2646
- if (change.newValue.tag === ValueTag.Empty) {
2647
- sheet.cells.delete(cellKey);
2648
- }
2649
- else {
2650
- sheet.cells.set(cellKey, change.newValue);
3593
+ if (updateVisibility) {
3594
+ for (const change of latestChangesByKey.values()) {
3595
+ const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
3596
+ const sheet = ensureMutableSheet(change.address.sheet, change.sheetName);
3597
+ if (change.newValue.tag === ValueTag.Empty) {
3598
+ sheet.cells.delete(cellKey);
3599
+ }
3600
+ else {
3601
+ sheet.cells.set(cellKey, change.newValue);
3602
+ }
2651
3603
  }
2652
3604
  }
2653
3605
  const directChanges = [...latestChangesByKey.values()];
@@ -2690,16 +3642,31 @@ export class WorkPaper {
2690
3642
  this.batchStartNamedValues = this.namedExpressions.size > 0 ? this.ensureNamedExpressionValueCache() : EMPTY_NAMED_EXPRESSION_VALUES;
2691
3643
  this.batchUsesTrackedFastPath = false;
2692
3644
  }
2693
- computeTrackedChangesWithoutVisibilityCache(events) {
2694
- const fastPath = this.computeCellChangesFromTrackedEvents(new Map(), events);
3645
+ computeTrackedChangesWithoutVisibilityCache(events, options = {}) {
3646
+ if (events.length === 1) {
3647
+ const event = events[0];
3648
+ if (!options.preferLazyPublicChanges || event.changedCellIndices.length <= TINY_TRACKED_CHANGE_LIMIT) {
3649
+ const tinyChanges = this.tryReadTinyTrackedEventChangesWithoutVisibility(event);
3650
+ if (tinyChanges) {
3651
+ return tinyChanges;
3652
+ }
3653
+ }
3654
+ }
3655
+ const fastPath = this.computeCellChangesFromTrackedEvents(new Map(), events, false, options);
2695
3656
  if (!fastPath) {
2696
3657
  throw new WorkPaperOperationError('Mutation emitted an unsupported invalidation pattern for tracked changes');
2697
3658
  }
2698
3659
  return fastPath.changes;
2699
3660
  }
2700
- captureTrackedChangesWithoutVisibilityCache(mutate) {
3661
+ captureTrackedChangesWithoutVisibilityCache(mutate, options = {}) {
2701
3662
  this.assertNotDisposed();
2702
- this.drainTrackedEngineEvents();
3663
+ if (this.pendingLazyTrackedChanges.length > 0) {
3664
+ this.materializePendingLazyTrackedChanges({ preservePositions: options.preservePendingTrackedPositions });
3665
+ }
3666
+ if (this.trackedEngineEvents.length > 0) {
3667
+ this.drainTrackedEngineEvents();
3668
+ }
3669
+ this.retainedTrackedEngineEventIndicesDepth += 1;
2703
3670
  try {
2704
3671
  mutate();
2705
3672
  }
@@ -2709,13 +3676,159 @@ export class WorkPaper {
2709
3676
  }
2710
3677
  throw new WorkPaperOperationError(this.messageOf(error, 'Mutation failed'));
2711
3678
  }
2712
- const changes = this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents());
2713
- if (changes.length > 0) {
3679
+ finally {
3680
+ this.retainedTrackedEngineEventIndicesDepth -= 1;
3681
+ }
3682
+ const events = this.drainTrackedEngineEvents();
3683
+ const directSingleLiteralChanges = this.tryBuildDirectSingleLiteralTrackedChange(events, options.singleLiteralChange);
3684
+ if (directSingleLiteralChanges) {
3685
+ if (directSingleLiteralChanges.length > 0 && this.emitter.hasListeners('valuesUpdated')) {
3686
+ this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes: directSingleLiteralChanges } });
3687
+ }
3688
+ return directSingleLiteralChanges;
3689
+ }
3690
+ const shouldEmitValuesUpdated = this.emitter.hasListeners('valuesUpdated');
3691
+ const changes = this.computeTrackedChangesWithoutVisibilityCache(events, {
3692
+ preferLazyPublicChanges: !shouldEmitValuesUpdated,
3693
+ });
3694
+ if (changes.length > 0 && shouldEmitValuesUpdated) {
2714
3695
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
2715
3696
  }
2716
3697
  return changes;
2717
3698
  }
3699
+ tryBuildDirectSingleLiteralTrackedChange(events, expected) {
3700
+ if (expected === undefined || expected.cellIndex === undefined || events.length !== 1) {
3701
+ return null;
3702
+ }
3703
+ const event = events[0];
3704
+ if (event.invalidation === 'full' ||
3705
+ event.patches !== undefined ||
3706
+ event.changedCellIndices.length < 1 ||
3707
+ event.changedCellIndices.length > 2 ||
3708
+ event.changedCellIndices[0] !== expected.cellIndex ||
3709
+ event.hasInvalidatedRanges ||
3710
+ event.hasInvalidatedRows ||
3711
+ event.hasInvalidatedColumns) {
3712
+ return null;
3713
+ }
3714
+ const literalChange = {
3715
+ kind: 'cell',
3716
+ address: { sheet: expected.address.sheet, row: expected.address.row, col: expected.address.col },
3717
+ sheetName: expected.sheetName,
3718
+ a1: this.trackedA1(expected.address.row, expected.address.col),
3719
+ newValue: scalarValueFromLiteral(expected.value),
3720
+ };
3721
+ if (event.changedCellIndices.length === 1) {
3722
+ return [literalChange];
3723
+ }
3724
+ const formulaCellIndex = event.changedCellIndices[1];
3725
+ const cellStore = this.engine.workbook.cellStore;
3726
+ if (cellStore.sheetIds[formulaCellIndex] !== expected.address.sheet) {
3727
+ return null;
3728
+ }
3729
+ if (!expected.isPhysicalSheet) {
3730
+ return null;
3731
+ }
3732
+ const formulaRow = cellStore.rows[formulaCellIndex];
3733
+ const formulaCol = cellStore.cols[formulaCellIndex];
3734
+ if (formulaRow < expected.address.row || (formulaRow === expected.address.row && formulaCol < expected.address.col)) {
3735
+ return null;
3736
+ }
3737
+ return [
3738
+ literalChange,
3739
+ {
3740
+ kind: 'cell',
3741
+ address: { sheet: expected.address.sheet, row: formulaRow, col: formulaCol },
3742
+ sheetName: expected.sheetName,
3743
+ a1: this.trackedA1(formulaRow, formulaCol),
3744
+ newValue: readTrackedRuntimeCellValue(cellStore, formulaCellIndex, this.engine.strings),
3745
+ },
3746
+ ];
3747
+ }
3748
+ tryBuildDirectExistingNumericTrackedChanges(result, address, cellIndex, isPhysicalSheet, sheetName, value) {
3749
+ const changedCellCount = result.changedCellIndices?.length ?? result.changedCellCount ?? 0;
3750
+ const firstChangedCellIndex = result.changedCellIndices?.[0] ?? result.firstChangedCellIndex;
3751
+ if (changedCellCount < 1 || firstChangedCellIndex !== cellIndex) {
3752
+ return null;
3753
+ }
3754
+ const literalChange = {
3755
+ kind: 'cell',
3756
+ address: { sheet: address.sheet, row: address.row, col: address.col },
3757
+ sheetName,
3758
+ a1: this.trackedA1(address.row, address.col),
3759
+ newValue: { tag: ValueTag.Number, value },
3760
+ };
3761
+ if (changedCellCount === 1) {
3762
+ return [literalChange];
3763
+ }
3764
+ if (!isPhysicalSheet) {
3765
+ return null;
3766
+ }
3767
+ if (changedCellCount > 2) {
3768
+ const changedCellIndices = result.changedCellIndices;
3769
+ if (changedCellIndices === undefined) {
3770
+ return null;
3771
+ }
3772
+ const cellStore = this.engine.workbook.cellStore;
3773
+ const changes = [];
3774
+ changes.length = changedCellCount;
3775
+ changes[0] = literalChange;
3776
+ let alreadySorted = true;
3777
+ let previousRow = address.row;
3778
+ let previousCol = address.col;
3779
+ for (let index = 1; index < changedCellCount; index += 1) {
3780
+ const changedCellIndex = changedCellIndices[index];
3781
+ if (cellStore.sheetIds[changedCellIndex] !== address.sheet) {
3782
+ return null;
3783
+ }
3784
+ const row = cellStore.rows[changedCellIndex];
3785
+ const col = cellStore.cols[changedCellIndex];
3786
+ if (row === undefined || col === undefined) {
3787
+ return null;
3788
+ }
3789
+ if (row < previousRow || (row === previousRow && col < previousCol)) {
3790
+ alreadySorted = false;
3791
+ }
3792
+ changes[index] = {
3793
+ kind: 'cell',
3794
+ address: { sheet: address.sheet, row, col },
3795
+ sheetName,
3796
+ a1: this.trackedA1(row, col),
3797
+ newValue: readTrackedRuntimeCellValue(cellStore, changedCellIndex, this.engine.strings),
3798
+ };
3799
+ previousRow = row;
3800
+ previousCol = col;
3801
+ }
3802
+ return alreadySorted ? changes : orderWorkPaperCellChanges(changes, this.listSheetRecords(), result.explicitChangedCount);
3803
+ }
3804
+ const formulaCellIndex = result.changedCellIndices?.[1] ?? result.secondChangedCellIndex;
3805
+ if (formulaCellIndex === undefined) {
3806
+ return null;
3807
+ }
3808
+ const cellStore = this.engine.workbook.cellStore;
3809
+ if (cellStore.sheetIds[formulaCellIndex] !== address.sheet) {
3810
+ return null;
3811
+ }
3812
+ const formulaRow = result.secondChangedRow ?? cellStore.rows[formulaCellIndex];
3813
+ const formulaCol = result.secondChangedCol ?? cellStore.cols[formulaCellIndex];
3814
+ if (formulaRow < address.row || (formulaRow === address.row && formulaCol < address.col)) {
3815
+ return null;
3816
+ }
3817
+ return [
3818
+ literalChange,
3819
+ {
3820
+ kind: 'cell',
3821
+ address: { sheet: address.sheet, row: formulaRow, col: formulaCol },
3822
+ sheetName,
3823
+ a1: this.trackedA1(formulaRow, formulaCol),
3824
+ newValue: result.secondChangedNumericValue === undefined
3825
+ ? readTrackedRuntimeCellValue(cellStore, formulaCellIndex, this.engine.strings)
3826
+ : { tag: ValueTag.Number, value: result.secondChangedNumericValue },
3827
+ },
3828
+ ];
3829
+ }
2718
3830
  batchStructuralChanges(batchOperations) {
3831
+ this.materializePendingLazyTrackedChanges();
2719
3832
  if (!this.canUseTrackedStructuralFastPath()) {
2720
3833
  this.downgradeTrackedBatchFastPath();
2721
3834
  return this.batch(batchOperations);
@@ -2725,16 +3838,19 @@ export class WorkPaper {
2725
3838
  this.drainTrackedEngineEvents();
2726
3839
  this.batchDepth += 1;
2727
3840
  try {
2728
- batchOperations();
3841
+ this.withRetainedTrackedEngineEventIndices(batchOperations);
2729
3842
  }
2730
3843
  finally {
2731
3844
  this.batchDepth -= 1;
2732
3845
  this.flushPendingBatchOps();
2733
3846
  this.mergeUndoHistory(undoStackStart);
2734
3847
  }
2735
- const changes = this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents());
3848
+ const shouldEmitValuesUpdated = this.emitter.hasListeners('valuesUpdated');
3849
+ const changes = this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents(), {
3850
+ preferLazyPublicChanges: !shouldEmitValuesUpdated,
3851
+ });
2736
3852
  this.flushQueuedEvents();
2737
- if (changes.length > 0) {
3853
+ if (changes.length > 0 && shouldEmitValuesUpdated) {
2738
3854
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
2739
3855
  }
2740
3856
  return changes;
@@ -2756,8 +3872,9 @@ export class WorkPaper {
2756
3872
  this.namedExpressionValueCache = afterNames;
2757
3873
  return hasNamedExpressions ? [...cellChanges, ...this.computeNamedExpressionChanges(beforeNames, afterNames)] : cellChanges;
2758
3874
  }
2759
- captureChanges(semanticEvent, mutate) {
3875
+ captureChanges(semanticEvent, mutate, options = {}) {
2760
3876
  this.assertNotDisposed();
3877
+ this.materializePendingLazyTrackedChanges({ preservePositions: options.preservePendingTrackedPositions });
2761
3878
  this.downgradeTrackedBatchFastPath();
2762
3879
  if (semanticEvent !== undefined) {
2763
3880
  this.flushPendingBatchOps();
@@ -2810,7 +3927,7 @@ export class WorkPaper {
2810
3927
  this.emitter.emitDetailed(event);
2811
3928
  }
2812
3929
  }
2813
- if (!this.shouldSuppressEvents() && changes.length > 0) {
3930
+ if (!this.shouldSuppressEvents() && changes.length > 0 && this.emitter.hasListeners('valuesUpdated')) {
2814
3931
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
2815
3932
  }
2816
3933
  return changes;
@@ -2839,6 +3956,10 @@ export class WorkPaper {
2839
3956
  }
2840
3957
  return stack;
2841
3958
  }
3959
+ historyTopIsCellMutations(stack) {
3960
+ const kind = stack.at(-1)?.forward.kind;
3961
+ return kind === 'cell-mutations' || kind === 'single-existing-numeric-cell-mutation';
3962
+ }
2842
3963
  clearHistoryStacks() {
2843
3964
  this.getUndoStack().length = 0;
2844
3965
  this.getRedoStack().length = 0;
@@ -2849,6 +3970,19 @@ export class WorkPaper {
2849
3970
  return record.ops;
2850
3971
  case 'single-op':
2851
3972
  return [record.op];
3973
+ case 'single-existing-numeric-cell-mutation': {
3974
+ const sheetName = this.getSheetName(record.sheetId);
3975
+ return sheetName
3976
+ ? [
3977
+ {
3978
+ kind: 'setCellValue',
3979
+ sheetName,
3980
+ address: formatAddress(record.row, record.col),
3981
+ value: record.value,
3982
+ },
3983
+ ]
3984
+ : [];
3985
+ }
2852
3986
  case 'cell-mutations':
2853
3987
  return record.refs.flatMap((ref) => {
2854
3988
  const sheetName = this.getSheetName(ref.sheetId);
@@ -3024,6 +4158,7 @@ export class WorkPaper {
3024
4158
  });
3025
4159
  }
3026
4160
  replaceSheetContentInternal(sheetId, content, options) {
4161
+ const dimensions = inspectSheetDimensionsWithinLimits(this.sheetName(sheetId), content, this.config);
3027
4162
  replaceWorkPaperSheetContent({
3028
4163
  sheetId,
3029
4164
  sheetName: this.sheetName(sheetId),
@@ -3037,9 +4172,10 @@ export class WorkPaper {
3037
4172
  getUndoStackLength: () => this.getUndoStack().length,
3038
4173
  mergeUndoHistory: (undoStackStart) => this.mergeUndoHistory(undoStackStart),
3039
4174
  });
4175
+ this.cacheInitializedSheetDimensions(sheetId, dimensions);
3040
4176
  }
3041
4177
  applyRawContent(address, content) {
3042
- const existingCellIndex = this.engine.workbook.getSheetById(address.sheet)?.grid.get(address.row, address.col) ?? -1;
4178
+ const cellIndex = this.getVisibleCellIndex(address.sheet, address.row, address.col);
3043
4179
  const mutation = content === null
3044
4180
  ? { kind: 'clearCell', row: address.row, col: address.col }
3045
4181
  : typeof content === 'string' && content.trim().startsWith('=')
@@ -3055,9 +4191,9 @@ export class WorkPaper {
3055
4191
  col: address.col,
3056
4192
  value: content,
3057
4193
  };
3058
- this.applyCellMutationRefs([{ sheetId: address.sheet, mutation }], {
4194
+ this.applyCellMutationRefs([{ sheetId: address.sheet, mutation, ...(cellIndex !== undefined ? { cellIndex } : {}) }], {
3059
4195
  captureUndo: true,
3060
- potentialNewCells: content === null || existingCellIndex !== -1 ? 0 : 1,
4196
+ potentialNewCells: content === null || cellIndex !== undefined ? 0 : 1,
3061
4197
  source: 'local',
3062
4198
  returnUndoOps: false,
3063
4199
  reuseRefs: true,