@bilig/headless 0.2.0 → 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,16 +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';
9
- import { materializeTrackedIndexChangesWithMetadata } from './tracked-cell-index-changes.js';
10
+ import { detachTrackedIndexChanges, hasDeferredTrackedIndexChanges, materializeTrackedIndexChangeSourcesWithMetadata, materializeTrackedIndexChangesWithMetadata, } from './tracked-cell-index-changes.js';
10
11
  import { calculateWorkPaperFormulaInScratchWorkbook } from './work-paper-scratch-evaluator.js';
11
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
+ }
12
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 */;
13
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
+ }
14
66
  const DEFAULT_CONFIG = Object.freeze({
15
67
  accentSensitive: false,
16
68
  caseSensitive: false,
@@ -282,9 +334,6 @@ function matrixContainsFormulaContent(content) {
282
334
  function isDeferredBatchLiteralContent(content) {
283
335
  return content === null || typeof content === 'boolean' || typeof content === 'number' || typeof content === 'string';
284
336
  }
285
- function canUseInitialMixedSheetFastPath(content) {
286
- return content.some((row) => row.some((value) => typeof value === 'string' && value.trim().startsWith('=')));
287
- }
288
337
  function stripLeadingEquals(formula) {
289
338
  return formula.trim().startsWith('=') ? formula.trim().slice(1) : formula.trim();
290
339
  }
@@ -509,17 +558,123 @@ function validateWorkPaperConfig(config) {
509
558
  }
510
559
  }
511
560
  }
512
- function validateSheetWithinLimits(sheetName, sheet, config) {
561
+ function inspectSheetDimensionsWithinLimits(sheetName, sheet, config) {
513
562
  const height = sheet.length;
514
- 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
+ }
515
579
  if (height > (config.maxRows ?? MAX_ROWS) || width > (config.maxColumns ?? MAX_COLS)) {
516
580
  throw new WorkPaperSheetSizeLimitExceededError();
517
581
  }
518
- 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];
519
624
  if (!Array.isArray(row)) {
520
625
  throw new WorkPaperUnableToParseError({ sheetName, reason: 'Rows must be arrays' });
521
626
  }
522
- });
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);
523
678
  }
524
679
  function functionPluginIds(config) {
525
680
  return (config.functionPlugins ?? []).map((plugin) => plugin.id).toSorted();
@@ -613,6 +768,9 @@ class WorkPaperEmitter {
613
768
  };
614
769
  this.onDetailed(eventName, wrapper);
615
770
  }
771
+ hasListeners(eventName) {
772
+ return this.listeners[eventName].size > 0 || this.detailedListeners[eventName].size > 0;
773
+ }
616
774
  emitDetailed(event) {
617
775
  this.dispatchDetailed(event);
618
776
  }
@@ -709,6 +867,8 @@ export class WorkPaper {
709
867
  visibilityCache = null;
710
868
  namedExpressionValueCache = null;
711
869
  sheetRecordsCache = null;
870
+ sheetDimensionsCache = new Map();
871
+ spillSheetIdsCache = null;
712
872
  batchDepth = 0;
713
873
  batchStartVisibility = null;
714
874
  batchStartNamedValues = null;
@@ -724,9 +884,124 @@ export class WorkPaper {
724
884
  suspendedCellMutationPotentialNewCells = 0;
725
885
  queuedEvents = [];
726
886
  trackedEngineEvents = [];
887
+ pendingLazyTrackedChanges = [];
727
888
  engineEventCaptureEnabled = true;
889
+ retainedTrackedEngineEventIndicesDepth = 0;
728
890
  unsubscribeEngineEvents = null;
729
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
+ }
730
1005
  constructor(configInput = {}) {
731
1006
  ensureCustomAdapterInstalled();
732
1007
  validateWorkPaperConfig(configInput);
@@ -739,6 +1014,7 @@ export class WorkPaper {
739
1014
  useColumnIndex: this.config.useColumnIndex,
740
1015
  trackReplicaVersions: false,
741
1016
  });
1017
+ this.invalidateAllSheetDimensions();
742
1018
  this.attachEngineEventTracking();
743
1019
  this.captureFunctionRegistry();
744
1020
  this.internals = Object.freeze({
@@ -812,38 +1088,69 @@ export class WorkPaper {
812
1088
  static buildFromSheets(sheets, configInput = {}, namedExpressions = []) {
813
1089
  const workbook = new WorkPaper(configInput);
814
1090
  const runtimeSnapshot = namedExpressions.length === 0 ? readRuntimeSnapshot(sheets) : undefined;
815
- const runtimeSnapshotMatchesSheets = runtimeSnapshot !== undefined &&
816
- runtimeSnapshot.sheets.length === Object.keys(sheets).length &&
817
- 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();
818
1099
  Object.entries(sheets).forEach(([sheetName, sheet]) => {
819
- 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));
820
1118
  });
821
1119
  workbook.withEngineEventCaptureDisabled(() => {
822
1120
  if (runtimeSnapshot && runtimeSnapshotMatchesSheets) {
823
1121
  workbook.engine.importSnapshot(runtimeSnapshot);
824
- return;
825
1122
  }
826
- Object.keys(sheets).forEach((sheetName) => {
827
- workbook.engine.createSheet(sheetName);
828
- });
829
- namedExpressions.forEach((expression) => {
830
- workbook.upsertNamedExpressionInternal(expression, { duringInitialization: true });
831
- });
832
- Object.entries(sheets).forEach(([sheetName, sheet]) => {
833
- const sheetId = workbook.requireSheetId(sheetName);
834
- if (tryLoadInitialLiteralSheet(workbook.engine, sheetId, sheet)) {
835
- return;
836
- }
837
- 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);
838
1140
  loadInitialMixedSheet({
839
1141
  engine: workbook.engine,
840
1142
  sheetId,
841
1143
  content: sheet,
842
- rewriteFormula: (formula, destination) => workbook.rewriteFormulaForStorage(formula, destination.sheet),
1144
+ rewriteFormula: rewriteInitialFormula,
1145
+ inspection: inspected,
843
1146
  });
844
- 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);
845
1153
  }
846
- workbook.replaceSheetContentInternal(sheetId, sheet, { duringInitialization: true });
847
1154
  });
848
1155
  });
849
1156
  workbook.clearHistoryStacks();
@@ -1009,6 +1316,7 @@ export class WorkPaper {
1009
1316
  }
1010
1317
  updateConfig(next) {
1011
1318
  this.assertNotDisposed();
1319
+ this.materializePendingLazyTrackedChanges();
1012
1320
  const merged = {
1013
1321
  ...this.config,
1014
1322
  ...cloneConfig(next),
@@ -1044,9 +1352,11 @@ export class WorkPaper {
1044
1352
  }
1045
1353
  rebuildAndRecalculate() {
1046
1354
  this.assertNotDisposed();
1355
+ this.materializePendingLazyTrackedChanges();
1047
1356
  if (this.shouldSuppressEvents()) {
1048
1357
  try {
1049
1358
  this.engine.recalculateNow();
1359
+ this.invalidateAllSheetDimensions();
1050
1360
  }
1051
1361
  catch (error) {
1052
1362
  throw new WorkPaperOperationError(this.messageOf(error, 'Recalculation failed'));
@@ -1058,6 +1368,7 @@ export class WorkPaper {
1058
1368
  this.drainTrackedEngineEvents();
1059
1369
  try {
1060
1370
  this.engine.recalculateNow();
1371
+ this.invalidateAllSheetDimensions();
1061
1372
  }
1062
1373
  catch (error) {
1063
1374
  throw new WorkPaperOperationError(this.messageOf(error, 'Recalculation failed'));
@@ -1070,13 +1381,14 @@ export class WorkPaper {
1070
1381
  ...this.computeCellChanges(beforeVisibility, afterVisibility),
1071
1382
  ...this.computeNamedExpressionChanges(beforeNames, afterNames),
1072
1383
  ];
1073
- if (changes.length > 0) {
1384
+ if (changes.length > 0 && this.emitter.hasListeners('valuesUpdated')) {
1074
1385
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
1075
1386
  }
1076
1387
  return changes;
1077
1388
  }
1078
1389
  batch(batchOperations) {
1079
1390
  this.assertNotDisposed();
1391
+ this.materializePendingLazyTrackedChanges();
1080
1392
  const isOutermost = this.batchDepth === 0;
1081
1393
  if (isOutermost) {
1082
1394
  this.batchUsesTrackedFastPath = this.canUseTrackedMutationFastPath();
@@ -1098,22 +1410,32 @@ export class WorkPaper {
1098
1410
  finally {
1099
1411
  this.batchDepth -= 1;
1100
1412
  if (isOutermost) {
1101
- this.flushPendingBatchOps();
1413
+ if (this.batchUsesTrackedFastPath) {
1414
+ this.withRetainedTrackedEngineEventIndices(() => {
1415
+ this.flushPendingBatchOps();
1416
+ });
1417
+ }
1418
+ else {
1419
+ this.flushPendingBatchOps();
1420
+ }
1102
1421
  this.mergeUndoHistory(this.batchUndoStackLength);
1103
1422
  }
1104
1423
  }
1105
1424
  if (!isOutermost) {
1106
1425
  return [];
1107
1426
  }
1427
+ const shouldEmitValuesUpdated = this.emitter.hasListeners('valuesUpdated');
1108
1428
  const changes = this.batchUsesTrackedFastPath
1109
- ? this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents())
1429
+ ? this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents(), {
1430
+ preferLazyPublicChanges: !shouldEmitValuesUpdated,
1431
+ })
1110
1432
  : this.computeChangesAfterMutation(this.batchStartVisibility ?? new Map(), this.batchStartNamedValues ?? new Map());
1111
1433
  this.batchUsesTrackedFastPath = false;
1112
1434
  this.batchStartVisibility = null;
1113
1435
  this.batchStartNamedValues = null;
1114
1436
  if (!this.evaluationSuspended) {
1115
1437
  this.flushQueuedEvents();
1116
- if (changes.length > 0) {
1438
+ if (changes.length > 0 && shouldEmitValuesUpdated) {
1117
1439
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
1118
1440
  }
1119
1441
  }
@@ -1121,6 +1443,7 @@ export class WorkPaper {
1121
1443
  }
1122
1444
  suspendEvaluation() {
1123
1445
  this.assertNotDisposed();
1446
+ this.materializePendingLazyTrackedChanges();
1124
1447
  if (this.evaluationSuspended) {
1125
1448
  return;
1126
1449
  }
@@ -1141,12 +1464,23 @@ export class WorkPaper {
1141
1464
  }
1142
1465
  resumeEvaluation() {
1143
1466
  this.assertNotDisposed();
1467
+ this.materializePendingLazyTrackedChanges();
1144
1468
  if (!this.evaluationSuspended) {
1145
1469
  return [];
1146
1470
  }
1147
- 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');
1148
1480
  const changes = this.suspendedUsesTrackedFastPath
1149
- ? this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents())
1481
+ ? this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents(), {
1482
+ preferLazyPublicChanges: !shouldEmitValuesUpdated,
1483
+ })
1150
1484
  : this.computeChangesAfterMutation(this.suspendedVisibility ?? new Map(), this.suspendedNamedValues ?? new Map());
1151
1485
  this.evaluationSuspended = false;
1152
1486
  this.suspendedVisibility = null;
@@ -1154,7 +1488,7 @@ export class WorkPaper {
1154
1488
  this.suspendedUsesTrackedFastPath = false;
1155
1489
  this.flushQueuedEvents();
1156
1490
  this.emitter.emitDetailed({ eventName: 'evaluationResumed', payload: { changes } });
1157
- if (changes.length > 0) {
1491
+ if (changes.length > 0 && shouldEmitValuesUpdated) {
1158
1492
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
1159
1493
  }
1160
1494
  return changes;
@@ -1164,19 +1498,39 @@ export class WorkPaper {
1164
1498
  }
1165
1499
  undo() {
1166
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
+ }
1167
1510
  return this.captureChanges(undefined, () => {
1168
1511
  if (!this.engine.undo()) {
1169
1512
  throw new WorkPaperNoOperationToUndoError();
1170
1513
  }
1171
- });
1514
+ this.invalidateAllSheetDimensions();
1515
+ }, { preservePendingTrackedPositions: preservesPositions });
1172
1516
  }
1173
1517
  redo() {
1174
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
+ }
1175
1528
  return this.captureChanges(undefined, () => {
1176
1529
  if (!this.engine.redo()) {
1177
1530
  throw new WorkPaperNoOperationToRedoError();
1178
1531
  }
1179
- });
1532
+ this.invalidateAllSheetDimensions();
1533
+ }, { preservePendingTrackedPositions: preservesPositions });
1180
1534
  }
1181
1535
  isThereSomethingToUndo() {
1182
1536
  return this.getUndoStack().length > 0;
@@ -1255,7 +1609,11 @@ export class WorkPaper {
1255
1609
  }
1256
1610
  getCellValue(address) {
1257
1611
  this.assertReadable();
1258
- 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);
1259
1617
  }
1260
1618
  getCellFormula(address) {
1261
1619
  this.prepareReadableState();
@@ -1283,6 +1641,11 @@ export class WorkPaper {
1283
1641
  }
1284
1642
  getRangeValues(range) {
1285
1643
  this.assertReadable();
1644
+ assertRange(range);
1645
+ const fastValues = readFastPhysicalRangeValues(this.engine, range);
1646
+ if (fastValues !== undefined) {
1647
+ return fastValues;
1648
+ }
1286
1649
  const ref = this.rangeRef(range);
1287
1650
  return this.engine.getRangeValues(ref);
1288
1651
  }
@@ -1341,13 +1704,13 @@ export class WorkPaper {
1341
1704
  getSheetDimensions(sheetId) {
1342
1705
  this.prepareReadableState();
1343
1706
  const sheet = this.sheetRecord(sheetId);
1344
- let width = 0;
1345
- let height = 0;
1346
- sheet.grid.forEachCellEntry((_cellIndex, row, col) => {
1347
- height = Math.max(height, row + 1);
1348
- width = Math.max(width, col + 1);
1349
- });
1350
- 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;
1351
1714
  }
1352
1715
  simpleCellAddressFromString(value, defaultSheetId) {
1353
1716
  this.assertNotDisposed();
@@ -1745,6 +2108,81 @@ export class WorkPaper {
1745
2108
  const { hours, minutes, seconds } = dateTime;
1746
2109
  return { hours, minutes, seconds };
1747
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
+ }
1748
2186
  setCellContents(address, content) {
1749
2187
  this.assertNotDisposed();
1750
2188
  const sheet = this.sheetRecord(address.sheet);
@@ -1754,15 +2192,40 @@ export class WorkPaper {
1754
2192
  if (address.row >= (this.config.maxRows ?? MAX_ROWS) || address.col >= (this.config.maxColumns ?? MAX_COLS)) {
1755
2193
  throw new WorkPaperOperationError('Cell contents cannot be set');
1756
2194
  }
1757
- const existingCellIndex = sheet.grid.get(address.row, address.col);
1758
- 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)) {
1759
2198
  return [];
1760
2199
  }
1761
- 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)) {
1762
2201
  return [];
1763
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
+ }
1764
2214
  const mutate = () => {
1765
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
+ }
1766
2229
  const mutation = content === null
1767
2230
  ? { kind: 'clearCell', row: address.row, col: address.col }
1768
2231
  : typeof content === 'string' && content.trim().startsWith('=')
@@ -1778,16 +2241,26 @@ export class WorkPaper {
1778
2241
  col: address.col,
1779
2242
  value: content,
1780
2243
  };
1781
- this.applyCellMutationRefs([{ sheetId: address.sheet, mutation }], {
2244
+ this.applyCellMutationRefs([{ sheetId: address.sheet, mutation, ...(visibleCellIndex !== undefined ? { cellIndex: visibleCellIndex } : {}) }], {
1782
2245
  captureUndo: true,
1783
- potentialNewCells: content === null || existingCellIndex !== -1 ? 0 : 1,
2246
+ potentialNewCells: content === null || visibleCellIndex !== undefined ? 0 : 1,
1784
2247
  source: 'local',
1785
2248
  returnUndoOps: false,
1786
2249
  reuseRefs: true,
1787
2250
  });
1788
2251
  };
1789
2252
  if (this.canUseTrackedMutationFastPath()) {
1790
- 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
+ });
1791
2264
  }
1792
2265
  return this.captureChanges(undefined, () => {
1793
2266
  mutate();
@@ -1889,11 +2362,13 @@ export class WorkPaper {
1889
2362
  const [start, amount] = indexes[0];
1890
2363
  return this.captureTrackedChangesWithoutVisibilityCache(() => {
1891
2364
  this.engine.insertRows(this.sheetName(sheetId), start, amount);
2365
+ this.invalidateSheetDimensions(sheetId);
1892
2366
  });
1893
2367
  }
1894
2368
  return this.batchStructuralChanges(() => {
1895
2369
  indexes.forEach(([start, amount]) => {
1896
2370
  this.engine.insertRows(this.sheetName(sheetId), start, amount);
2371
+ this.invalidateSheetDimensions(sheetId);
1897
2372
  });
1898
2373
  });
1899
2374
  }
@@ -1906,6 +2381,7 @@ export class WorkPaper {
1906
2381
  const [start, amount] = indexes[0];
1907
2382
  return this.captureTrackedChangesWithoutVisibilityCache(() => {
1908
2383
  this.engine.deleteRows(this.sheetName(sheetId), start, amount);
2384
+ this.invalidateSheetDimensions(sheetId);
1909
2385
  });
1910
2386
  }
1911
2387
  return this.batchStructuralChanges(() => {
@@ -1913,6 +2389,7 @@ export class WorkPaper {
1913
2389
  .toSorted((left, right) => right[0] - left[0])
1914
2390
  .forEach(([start, amount]) => {
1915
2391
  this.engine.deleteRows(this.sheetName(sheetId), start, amount);
2392
+ this.invalidateSheetDimensions(sheetId);
1916
2393
  });
1917
2394
  });
1918
2395
  }
@@ -1925,11 +2402,13 @@ export class WorkPaper {
1925
2402
  const [start, amount] = indexes[0];
1926
2403
  return this.captureTrackedChangesWithoutVisibilityCache(() => {
1927
2404
  this.engine.insertColumns(this.sheetName(sheetId), start, amount);
2405
+ this.invalidateSheetDimensions(sheetId);
1928
2406
  });
1929
2407
  }
1930
2408
  return this.batchStructuralChanges(() => {
1931
2409
  indexes.forEach(([start, amount]) => {
1932
2410
  this.engine.insertColumns(this.sheetName(sheetId), start, amount);
2411
+ this.invalidateSheetDimensions(sheetId);
1933
2412
  });
1934
2413
  });
1935
2414
  }
@@ -1942,6 +2421,7 @@ export class WorkPaper {
1942
2421
  const [start, amount] = indexes[0];
1943
2422
  return this.captureTrackedChangesWithoutVisibilityCache(() => {
1944
2423
  this.engine.deleteColumns(this.sheetName(sheetId), start, amount);
2424
+ this.invalidateSheetDimensions(sheetId);
1945
2425
  });
1946
2426
  }
1947
2427
  return this.batchStructuralChanges(() => {
@@ -1949,6 +2429,7 @@ export class WorkPaper {
1949
2429
  .toSorted((left, right) => right[0] - left[0])
1950
2430
  .forEach(([start, amount]) => {
1951
2431
  this.engine.deleteColumns(this.sheetName(sheetId), start, amount);
2432
+ this.invalidateSheetDimensions(sheetId);
1952
2433
  });
1953
2434
  });
1954
2435
  }
@@ -1964,6 +2445,8 @@ export class WorkPaper {
1964
2445
  startAddress: formatAddress(target.row, target.col),
1965
2446
  endAddress: formatAddress(target.row + sourceHeight, target.col + sourceWidth),
1966
2447
  });
2448
+ this.invalidateSheetDimensions(source.start.sheet);
2449
+ this.invalidateSheetDimensions(target.sheet);
1967
2450
  });
1968
2451
  }
1969
2452
  moveRows(sheetId, start, count, target) {
@@ -1973,9 +2456,11 @@ export class WorkPaper {
1973
2456
  return this.canUseTrackedStructuralFastPath()
1974
2457
  ? this.captureTrackedChangesWithoutVisibilityCache(() => {
1975
2458
  this.engine.moveRows(this.sheetName(sheetId), start, count, target);
2459
+ this.invalidateSheetDimensions(sheetId);
1976
2460
  })
1977
2461
  : this.captureChanges(undefined, () => {
1978
2462
  this.engine.moveRows(this.sheetName(sheetId), start, count, target);
2463
+ this.invalidateSheetDimensions(sheetId);
1979
2464
  });
1980
2465
  }
1981
2466
  moveColumns(sheetId, start, count, target) {
@@ -1985,13 +2470,16 @@ export class WorkPaper {
1985
2470
  return this.canUseTrackedStructuralFastPath()
1986
2471
  ? this.captureTrackedChangesWithoutVisibilityCache(() => {
1987
2472
  this.engine.moveColumns(this.sheetName(sheetId), start, count, target);
2473
+ this.invalidateSheetDimensions(sheetId);
1988
2474
  })
1989
2475
  : this.captureChanges(undefined, () => {
1990
2476
  this.engine.moveColumns(this.sheetName(sheetId), start, count, target);
2477
+ this.invalidateSheetDimensions(sheetId);
1991
2478
  });
1992
2479
  }
1993
2480
  addSheet(sheetName) {
1994
2481
  this.assertNotDisposed();
2482
+ this.materializePendingLazyTrackedChanges();
1995
2483
  const name = sheetName?.trim() || this.nextSheetName();
1996
2484
  if (!this.isItPossibleToAddSheet(name)) {
1997
2485
  throw new WorkPaperSheetNameAlreadyTakenError(name);
@@ -2002,6 +2490,7 @@ export class WorkPaper {
2002
2490
  this.engine.createSheet(name);
2003
2491
  this.sheetRecordsCache = null;
2004
2492
  const sheetId = this.requireSheetId(name);
2493
+ this.cacheSheetDimensions(sheetId, { width: 0, height: 0 });
2005
2494
  const payload = { sheetId, sheetName: name };
2006
2495
  if (this.shouldSuppressEvents()) {
2007
2496
  this.queuedEvents.push({ eventName: 'sheetAdded', payload });
@@ -2010,7 +2499,7 @@ export class WorkPaper {
2010
2499
  this.emitter.emitDetailed({ eventName: 'sheetAdded', payload });
2011
2500
  }
2012
2501
  const changes = this.computeChangesAfterMutation(beforeVisibility, beforeNames);
2013
- if (!this.shouldSuppressEvents() && changes.length > 0) {
2502
+ if (!this.shouldSuppressEvents() && changes.length > 0 && this.emitter.hasListeners('valuesUpdated')) {
2014
2503
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
2015
2504
  }
2016
2505
  return name;
@@ -2030,6 +2519,7 @@ export class WorkPaper {
2030
2519
  }, () => {
2031
2520
  this.engine.deleteSheet(sheetName);
2032
2521
  this.sheetRecordsCache = null;
2522
+ this.invalidateSheetDimensions(sheetId);
2033
2523
  });
2034
2524
  }
2035
2525
  clearSheet(sheetId) {
@@ -2046,6 +2536,7 @@ export class WorkPaper {
2046
2536
  startAddress: 'A1',
2047
2537
  endAddress: formatAddress(dimensions.height - 1, dimensions.width - 1),
2048
2538
  });
2539
+ this.cacheSheetDimensions(sheetId, { width: 0, height: 0 });
2049
2540
  });
2050
2541
  }
2051
2542
  setSheetContent(sheetId, content) {
@@ -2232,6 +2723,7 @@ export class WorkPaper {
2232
2723
  if (this.disposed) {
2233
2724
  return;
2234
2725
  }
2726
+ this.materializePendingLazyTrackedChanges();
2235
2727
  this.disposed = true;
2236
2728
  this.unsubscribeEngineEvents?.();
2237
2729
  this.unsubscribeEngineEvents = null;
@@ -2240,8 +2732,11 @@ export class WorkPaper {
2240
2732
  this.clipboard = null;
2241
2733
  this.visibilityCache = null;
2242
2734
  this.namedExpressionValueCache = null;
2735
+ this.sheetDimensionsCache.clear();
2736
+ this.spillSheetIdsCache = null;
2243
2737
  this.queuedEvents = [];
2244
2738
  this.trackedEngineEvents = [];
2739
+ this.pendingLazyTrackedChanges = [];
2245
2740
  this.namedExpressions.clear();
2246
2741
  }
2247
2742
  attachEngineEventTracking() {
@@ -2254,9 +2749,21 @@ export class WorkPaper {
2254
2749
  if (!this.engineEventCaptureEnabled) {
2255
2750
  return;
2256
2751
  }
2257
- this.trackedEngineEvents.push(captureTrackedEngineEvent(event));
2752
+ this.trackedEngineEvents.push(captureTrackedEngineEvent(event, {
2753
+ borrowChangedCellIndexViews: this.retainedTrackedEngineEventIndicesDepth > 0,
2754
+ cloneChangedCellIndices: this.retainedTrackedEngineEventIndicesDepth === 0,
2755
+ }));
2258
2756
  });
2259
2757
  }
2758
+ withRetainedTrackedEngineEventIndices(callback) {
2759
+ this.retainedTrackedEngineEventIndicesDepth += 1;
2760
+ try {
2761
+ return callback();
2762
+ }
2763
+ finally {
2764
+ this.retainedTrackedEngineEventIndicesDepth -= 1;
2765
+ }
2766
+ }
2260
2767
  withEngineEventCaptureDisabled(callback) {
2261
2768
  const previous = this.engineEventCaptureEnabled;
2262
2769
  this.engineEventCaptureEnabled = false;
@@ -2274,6 +2781,77 @@ export class WorkPaper {
2274
2781
  this.trackedEngineEvents = [];
2275
2782
  return events;
2276
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
+ }
2277
2855
  resetChangeTrackingCaches() {
2278
2856
  this.sheetRecordsCache = null;
2279
2857
  this.visibilityCache = null;
@@ -2303,11 +2881,12 @@ export class WorkPaper {
2303
2881
  this.pendingBatchPotentialNewCells = 0;
2304
2882
  this.engine.applyCellMutationsAtWithOptions(ops, {
2305
2883
  captureUndo: true,
2306
- potentialNewCells: potentialNewCells > 0 ? potentialNewCells : undefined,
2884
+ potentialNewCells,
2307
2885
  source: 'local',
2308
2886
  returnUndoOps: false,
2309
2887
  reuseRefs: true,
2310
2888
  });
2889
+ this.updateSheetDimensionsAfterCellMutationRefs(ops);
2311
2890
  }
2312
2891
  applyCellMutationRefs(refs, options) {
2313
2892
  if (this.evaluationSuspended && (options.source ?? 'local') === 'local') {
@@ -2319,6 +2898,7 @@ export class WorkPaper {
2319
2898
  const mutation = ref.mutation;
2320
2899
  this.suspendedCellMutationRefs.push({
2321
2900
  sheetId: ref.sheetId,
2901
+ ...(ref.cellIndex !== undefined ? { cellIndex: ref.cellIndex } : {}),
2322
2902
  mutation: mutation.kind === 'setCellValue'
2323
2903
  ? {
2324
2904
  kind: 'setCellValue',
@@ -2340,11 +2920,13 @@ export class WorkPaper {
2340
2920
  },
2341
2921
  });
2342
2922
  }
2343
- this.suspendedCellMutationPotentialNewCells +=
2344
- options.potentialNewCells ?? refs.reduce((count, ref) => (ref?.mutation.kind === 'clearCell' ? count : count + 1), 0);
2923
+ this.suspendedCellMutationPotentialNewCells += options.potentialNewCells ?? countPotentialNewTrackedCellMutations(refs);
2345
2924
  return;
2346
2925
  }
2347
2926
  this.engine.applyCellMutationsAtWithOptions(refs, options);
2927
+ if (!canSkipDimensionUpdateAfterLiteralMutation(refs, options.potentialNewCells)) {
2928
+ this.updateSheetDimensionsAfterCellMutationRefs(refs);
2929
+ }
2348
2930
  }
2349
2931
  flushSuspendedCellMutations() {
2350
2932
  if (this.suspendedCellMutationRefs.length === 0) {
@@ -2356,42 +2938,53 @@ export class WorkPaper {
2356
2938
  this.suspendedCellMutationPotentialNewCells = 0;
2357
2939
  this.engine.applyCellMutationsAtWithOptions(refs, {
2358
2940
  captureUndo: true,
2359
- potentialNewCells: potentialNewCells > 0 ? potentialNewCells : undefined,
2941
+ potentialNewCells,
2360
2942
  source: 'local',
2361
2943
  returnUndoOps: false,
2362
2944
  reuseRefs: true,
2363
2945
  });
2946
+ this.updateSheetDimensionsAfterCellMutationRefs(refs);
2364
2947
  }
2365
- enqueueSuspendedLiteralMutation(sheetId, row, col, content, existingCellIndex = this.engine.workbook.getSheetById(sheetId)?.grid.get(row, col) ?? -1) {
2948
+ enqueueSuspendedLiteralMutation(sheetId, row, col, content, cellIndex) {
2366
2949
  if (!this.evaluationSuspended || !isDeferredBatchLiteralContent(content) || isFormulaContent(content)) {
2367
2950
  return false;
2368
2951
  }
2369
2952
  if (content === null) {
2370
- 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
+ });
2371
2958
  return true;
2372
2959
  }
2373
2960
  this.suspendedCellMutationRefs.push({
2374
2961
  sheetId,
2375
2962
  mutation: { kind: 'setCellValue', row, col, value: content },
2963
+ ...(cellIndex !== undefined ? { cellIndex } : {}),
2376
2964
  });
2377
- if (existingCellIndex === -1) {
2965
+ if (cellIndex === undefined) {
2378
2966
  this.suspendedCellMutationPotentialNewCells += 1;
2379
2967
  }
2380
2968
  return true;
2381
2969
  }
2382
- enqueueDeferredBatchLiteral(sheetId, row, col, content, existingCellIndex = this.engine.workbook.getSheetById(sheetId)?.grid.get(row, col) ?? -1) {
2970
+ enqueueDeferredBatchLiteral(sheetId, row, col, content, cellIndex) {
2383
2971
  if (this.batchDepth === 0 || this.evaluationSuspended || !isDeferredBatchLiteralContent(content) || isFormulaContent(content)) {
2384
2972
  return false;
2385
2973
  }
2386
2974
  if (content === null) {
2387
- 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
+ });
2388
2980
  return true;
2389
2981
  }
2390
2982
  this.pendingBatchOps.push({
2391
2983
  sheetId,
2392
2984
  mutation: { kind: 'setCellValue', row, col, value: content },
2985
+ ...(cellIndex !== undefined ? { cellIndex } : {}),
2393
2986
  });
2394
- if (existingCellIndex === -1) {
2987
+ if (cellIndex === undefined) {
2395
2988
  this.pendingBatchPotentialNewCells += 1;
2396
2989
  }
2397
2990
  return true;
@@ -2431,6 +3024,28 @@ export class WorkPaper {
2431
3024
  a1(address) {
2432
3025
  return formatAddress(address.row, address.col);
2433
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
+ }
2434
3049
  rangeRef(range) {
2435
3050
  assertRange(range);
2436
3051
  return sourceRangeRef(this.sheetName(range.start.sheet), range);
@@ -2525,14 +3140,22 @@ export class WorkPaper {
2525
3140
  });
2526
3141
  return orderWorkPaperCellChanges(cellChanges, this.listSheetRecords());
2527
3142
  }
2528
- materializeTrackedEventChanges(event) {
3143
+ materializeTrackedEventChanges(event, lazy = false) {
2529
3144
  if (event.patches && event.patches.length > 0) {
2530
3145
  const cellPatches = event.patches.filter((patch) => patch.kind === 'cell');
2531
3146
  return { changes: cellPatches, canReusePublicChanges: false, ordered: false };
2532
3147
  }
3148
+ const trustedPhysicalMetadata = lazy && event.changedCellIndices instanceof Uint32Array
3149
+ ? readTrustedPhysicalTrackedChangeMetadata(event.changedCellIndices)
3150
+ : undefined;
2533
3151
  const materialized = materializeTrackedIndexChangesWithMetadata(this.engine, event.changedCellIndices, {
2534
3152
  explicitChangedCount: event.explicitChangedCount,
3153
+ lazy,
3154
+ ...trustedPhysicalMetadata,
2535
3155
  });
3156
+ if (lazy) {
3157
+ this.trackLazyTrackedChanges(materialized.changes);
3158
+ }
2536
3159
  return {
2537
3160
  changes: materialized.changes,
2538
3161
  canReusePublicChanges: true,
@@ -2585,11 +3208,182 @@ export class WorkPaper {
2585
3208
  kind: 'cell',
2586
3209
  address: { sheet: sheetId, row, col },
2587
3210
  sheetName,
2588
- a1: formatAddress(row, col),
3211
+ a1: this.trackedA1(row, col),
2589
3212
  newValue,
2590
3213
  };
2591
3214
  }
2592
- computeCellChangesFromTrackedEvents(beforeVisibility, events, updateVisibility = true) {
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
+ ];
3274
+ }
3275
+ const changes = [];
3276
+ let previousRow = -1;
3277
+ let previousCol = -1;
3278
+ for (let index = 0; index < event.changedCellIndices.length; index += 1) {
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;
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;
3297
+ }
3298
+ return changes;
3299
+ }
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 = {}) {
2593
3387
  if (events.some((event) => event.invalidation === 'full')) {
2594
3388
  return null;
2595
3389
  }
@@ -2618,36 +3412,71 @@ export class WorkPaper {
2618
3412
  nextVisibility.set(sheetId, created);
2619
3413
  return created;
2620
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
+ };
2621
3467
  if (events.length === 1) {
2622
3468
  const event = events[0];
2623
- if (event.invalidation !== 'full' &&
2624
- event.patches === undefined &&
2625
- event.changedCellIndices.length === 1 &&
2626
- event.explicitChangedCount === 1 &&
2627
- event.changedInputCount === 1 &&
2628
- !event.hasInvalidatedRanges &&
2629
- !event.hasInvalidatedRows &&
2630
- !event.hasInvalidatedColumns) {
2631
- const change = this.readSingleTrackedCellChange(event.changedCellIndices[0]);
2632
- if (change) {
2633
- if (updateVisibility) {
2634
- const sheet = ensureMutableSheet(change.address.sheet, change.sheetName);
2635
- const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
2636
- if (change.newValue.tag === ValueTag.Empty) {
2637
- sheet.cells.delete(cellKey);
2638
- }
2639
- else {
2640
- sheet.cells.set(cellKey, change.newValue);
2641
- }
2642
- }
2643
- return { changes: [change], nextVisibility };
3469
+ if (!options.preferLazyPublicChanges) {
3470
+ const smallChanges = tryReadSmallTrackedEventChanges(event);
3471
+ if (smallChanges) {
3472
+ return { changes: smallChanges, nextVisibility };
2644
3473
  }
2645
3474
  }
2646
- const materializedEventChanges = this.materializeTrackedEventChanges(event);
3475
+ const materializedEventChanges = this.materializeTrackedEventChanges(event, !updateVisibility);
2647
3476
  const eventChanges = materializedEventChanges.changes;
2648
3477
  if (!updateVisibility && materializedEventChanges.canReusePublicChanges && materializedEventChanges.ordered) {
2649
3478
  return {
2650
- changes: [...eventChanges],
3479
+ changes: eventChanges,
2651
3480
  nextVisibility,
2652
3481
  };
2653
3482
  }
@@ -2718,6 +3547,33 @@ export class WorkPaper {
2718
3547
  };
2719
3548
  }
2720
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
+ }
2721
3577
  const latestChangesByKey = new Map();
2722
3578
  for (const event of events) {
2723
3579
  const eventChanges = this.materializeTrackedEventChanges(event).changes;
@@ -2786,16 +3642,31 @@ export class WorkPaper {
2786
3642
  this.batchStartNamedValues = this.namedExpressions.size > 0 ? this.ensureNamedExpressionValueCache() : EMPTY_NAMED_EXPRESSION_VALUES;
2787
3643
  this.batchUsesTrackedFastPath = false;
2788
3644
  }
2789
- computeTrackedChangesWithoutVisibilityCache(events) {
2790
- const fastPath = this.computeCellChangesFromTrackedEvents(new Map(), events, false);
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);
2791
3656
  if (!fastPath) {
2792
3657
  throw new WorkPaperOperationError('Mutation emitted an unsupported invalidation pattern for tracked changes');
2793
3658
  }
2794
3659
  return fastPath.changes;
2795
3660
  }
2796
- captureTrackedChangesWithoutVisibilityCache(mutate) {
3661
+ captureTrackedChangesWithoutVisibilityCache(mutate, options = {}) {
2797
3662
  this.assertNotDisposed();
2798
- 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;
2799
3670
  try {
2800
3671
  mutate();
2801
3672
  }
@@ -2805,13 +3676,159 @@ export class WorkPaper {
2805
3676
  }
2806
3677
  throw new WorkPaperOperationError(this.messageOf(error, 'Mutation failed'));
2807
3678
  }
2808
- const changes = this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents());
2809
- 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) {
2810
3695
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
2811
3696
  }
2812
3697
  return changes;
2813
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
+ }
2814
3830
  batchStructuralChanges(batchOperations) {
3831
+ this.materializePendingLazyTrackedChanges();
2815
3832
  if (!this.canUseTrackedStructuralFastPath()) {
2816
3833
  this.downgradeTrackedBatchFastPath();
2817
3834
  return this.batch(batchOperations);
@@ -2821,16 +3838,19 @@ export class WorkPaper {
2821
3838
  this.drainTrackedEngineEvents();
2822
3839
  this.batchDepth += 1;
2823
3840
  try {
2824
- batchOperations();
3841
+ this.withRetainedTrackedEngineEventIndices(batchOperations);
2825
3842
  }
2826
3843
  finally {
2827
3844
  this.batchDepth -= 1;
2828
3845
  this.flushPendingBatchOps();
2829
3846
  this.mergeUndoHistory(undoStackStart);
2830
3847
  }
2831
- const changes = this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents());
3848
+ const shouldEmitValuesUpdated = this.emitter.hasListeners('valuesUpdated');
3849
+ const changes = this.computeTrackedChangesWithoutVisibilityCache(this.drainTrackedEngineEvents(), {
3850
+ preferLazyPublicChanges: !shouldEmitValuesUpdated,
3851
+ });
2832
3852
  this.flushQueuedEvents();
2833
- if (changes.length > 0) {
3853
+ if (changes.length > 0 && shouldEmitValuesUpdated) {
2834
3854
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
2835
3855
  }
2836
3856
  return changes;
@@ -2852,8 +3872,9 @@ export class WorkPaper {
2852
3872
  this.namedExpressionValueCache = afterNames;
2853
3873
  return hasNamedExpressions ? [...cellChanges, ...this.computeNamedExpressionChanges(beforeNames, afterNames)] : cellChanges;
2854
3874
  }
2855
- captureChanges(semanticEvent, mutate) {
3875
+ captureChanges(semanticEvent, mutate, options = {}) {
2856
3876
  this.assertNotDisposed();
3877
+ this.materializePendingLazyTrackedChanges({ preservePositions: options.preservePendingTrackedPositions });
2857
3878
  this.downgradeTrackedBatchFastPath();
2858
3879
  if (semanticEvent !== undefined) {
2859
3880
  this.flushPendingBatchOps();
@@ -2906,7 +3927,7 @@ export class WorkPaper {
2906
3927
  this.emitter.emitDetailed(event);
2907
3928
  }
2908
3929
  }
2909
- if (!this.shouldSuppressEvents() && changes.length > 0) {
3930
+ if (!this.shouldSuppressEvents() && changes.length > 0 && this.emitter.hasListeners('valuesUpdated')) {
2910
3931
  this.emitter.emitDetailed({ eventName: 'valuesUpdated', payload: { changes } });
2911
3932
  }
2912
3933
  return changes;
@@ -2935,6 +3956,10 @@ export class WorkPaper {
2935
3956
  }
2936
3957
  return stack;
2937
3958
  }
3959
+ historyTopIsCellMutations(stack) {
3960
+ const kind = stack.at(-1)?.forward.kind;
3961
+ return kind === 'cell-mutations' || kind === 'single-existing-numeric-cell-mutation';
3962
+ }
2938
3963
  clearHistoryStacks() {
2939
3964
  this.getUndoStack().length = 0;
2940
3965
  this.getRedoStack().length = 0;
@@ -2945,6 +3970,19 @@ export class WorkPaper {
2945
3970
  return record.ops;
2946
3971
  case 'single-op':
2947
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
+ }
2948
3986
  case 'cell-mutations':
2949
3987
  return record.refs.flatMap((ref) => {
2950
3988
  const sheetName = this.getSheetName(ref.sheetId);
@@ -3120,6 +4158,7 @@ export class WorkPaper {
3120
4158
  });
3121
4159
  }
3122
4160
  replaceSheetContentInternal(sheetId, content, options) {
4161
+ const dimensions = inspectSheetDimensionsWithinLimits(this.sheetName(sheetId), content, this.config);
3123
4162
  replaceWorkPaperSheetContent({
3124
4163
  sheetId,
3125
4164
  sheetName: this.sheetName(sheetId),
@@ -3133,9 +4172,10 @@ export class WorkPaper {
3133
4172
  getUndoStackLength: () => this.getUndoStack().length,
3134
4173
  mergeUndoHistory: (undoStackStart) => this.mergeUndoHistory(undoStackStart),
3135
4174
  });
4175
+ this.cacheInitializedSheetDimensions(sheetId, dimensions);
3136
4176
  }
3137
4177
  applyRawContent(address, content) {
3138
- 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);
3139
4179
  const mutation = content === null
3140
4180
  ? { kind: 'clearCell', row: address.row, col: address.col }
3141
4181
  : typeof content === 'string' && content.trim().startsWith('=')
@@ -3151,9 +4191,9 @@ export class WorkPaper {
3151
4191
  col: address.col,
3152
4192
  value: content,
3153
4193
  };
3154
- this.applyCellMutationRefs([{ sheetId: address.sheet, mutation }], {
4194
+ this.applyCellMutationRefs([{ sheetId: address.sheet, mutation, ...(cellIndex !== undefined ? { cellIndex } : {}) }], {
3155
4195
  captureUndo: true,
3156
- potentialNewCells: content === null || existingCellIndex !== -1 ? 0 : 1,
4196
+ potentialNewCells: content === null || cellIndex !== undefined ? 0 : 1,
3157
4197
  source: 'local',
3158
4198
  returnUndoOps: false,
3159
4199
  reuseRefs: true,