@bilig/headless 0.1.28 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +1 -2
  2. package/dist/change-order.d.ts +7 -0
  3. package/dist/change-order.js +64 -0
  4. package/dist/change-order.js.map +1 -0
  5. package/dist/deferred-literal-history.d.ts +13 -0
  6. package/dist/deferred-literal-history.js +67 -0
  7. package/dist/deferred-literal-history.js.map +1 -0
  8. package/dist/index.d.ts +0 -3
  9. package/dist/index.js +0 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/initial-sheet-load.d.ts +3 -0
  12. package/dist/initial-sheet-load.js +12 -0
  13. package/dist/initial-sheet-load.js.map +1 -0
  14. package/dist/matrix-mutation-plan.d.ts +11 -22
  15. package/dist/matrix-mutation-plan.js +59 -14
  16. package/dist/matrix-mutation-plan.js.map +1 -1
  17. package/dist/tracked-engine-event-refs.d.ts +11 -0
  18. package/dist/tracked-engine-event-refs.js +18 -0
  19. package/dist/tracked-engine-event-refs.js.map +1 -0
  20. package/dist/work-paper-errors.d.ts +106 -0
  21. package/dist/work-paper-errors.js +194 -0
  22. package/dist/work-paper-errors.js.map +1 -0
  23. package/dist/work-paper-runtime.d.ts +253 -0
  24. package/dist/{headless-workbook.js → work-paper-runtime.js} +581 -273
  25. package/dist/work-paper-runtime.js.map +1 -0
  26. package/dist/work-paper-scratch-evaluator.d.ts +29 -0
  27. package/dist/work-paper-scratch-evaluator.js +30 -0
  28. package/dist/work-paper-scratch-evaluator.js.map +1 -0
  29. package/dist/work-paper-sheet-replacement.d.ts +27 -0
  30. package/dist/work-paper-sheet-replacement.js +38 -0
  31. package/dist/work-paper-sheet-replacement.js.map +1 -0
  32. package/dist/work-paper-types.d.ts +267 -0
  33. package/dist/work-paper-types.js +2 -0
  34. package/dist/work-paper-types.js.map +1 -0
  35. package/dist/work-paper.d.ts +3 -48
  36. package/dist/work-paper.js +3 -2
  37. package/dist/work-paper.js.map +1 -1
  38. package/package.json +6 -6
  39. package/dist/errors.d.ts +0 -112
  40. package/dist/errors.js +0 -206
  41. package/dist/errors.js.map +0 -1
  42. package/dist/headless-workbook.d.ts +0 -239
  43. package/dist/headless-workbook.js.map +0 -1
  44. package/dist/types.d.ts +0 -252
  45. package/dist/types.js +0 -2
  46. package/dist/types.js.map +0 -1
@@ -1,8 +1,15 @@
1
- import { SpreadsheetEngine } from "@bilig/core";
1
+ import { SpreadsheetEngine, makeCellKey } from "@bilig/core";
2
2
  import { ErrorCode, MAX_COLS, MAX_ROWS, ValueTag, } from "@bilig/protocol";
3
3
  import { excelSerialToDateParts, formatAddress, formatRangeAddress, installExternalFunctionAdapter, isArrayValue, isCellReferenceText, parseCellAddress, parseFormula, parseRangeAddress, serializeFormula, translateFormulaReferences, } from "@bilig/formula";
4
- import { ConfigValueTooBigError, ConfigValueTooSmallError, HeadlessArgumentError, HeadlessEvaluationSuspendedError, HeadlessOperationError, HeadlessParseError, HeadlessSheetError, ExpectedOneOfValuesError, FunctionPluginValidationError, InvalidArgumentsError, LanguageAlreadyRegisteredError, LanguageNotRegisteredError, NamedExpressionDoesNotExistError, NamedExpressionNameIsAlreadyTakenError, NamedExpressionNameIsInvalidError, NoOperationToRedoError, NoOperationToUndoError, NoRelativeAddressesAllowedError, NoSheetWithIdError, NoSheetWithNameError, NotAFormulaError, NothingToPasteError, SheetNameAlreadyTakenError, SheetSizeLimitExceededError, UnableToParseError, } from "./errors.js";
4
+ import { tryLoadInitialLiteralSheet } from "./initial-sheet-load.js";
5
+ import { orderWorkPaperCellChanges } from "./change-order.js";
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";
5
7
  import { buildMatrixMutationPlan } from "./matrix-mutation-plan.js";
8
+ import { captureTrackedEngineEvent } from "./tracked-engine-event-refs.js";
9
+ import { calculateWorkPaperFormulaInScratchWorkbook } from "./work-paper-scratch-evaluator.js";
10
+ import { replaceWorkPaperSheetContent } from "./work-paper-sheet-replacement.js";
11
+ const EMPTY_NAMED_EXPRESSION_VALUES = new Map();
12
+ const VISIBILITY_SHEET_STRIDE = MAX_ROWS * MAX_COLS;
6
13
  const DEFAULT_CONFIG = Object.freeze({
7
14
  accentSensitive: false,
8
15
  caseSensitive: false,
@@ -43,7 +50,7 @@ const DEFAULT_CONFIG = Object.freeze({
43
50
  useRegularExpressions: true,
44
51
  useWildcards: true,
45
52
  });
46
- const HEADLESS_CONFIG_KEYS = [
53
+ const WORKPAPER_CONFIG_KEYS = [
47
54
  "accentSensitive",
48
55
  "caseSensitive",
49
56
  "caseFirst",
@@ -83,46 +90,44 @@ const HEADLESS_CONFIG_KEYS = [
83
90
  "useRegularExpressions",
84
91
  "useWildcards",
85
92
  ];
86
- const HEADLESS_PUBLIC_ERROR_NAMES = new Set([
87
- "ConfigValueTooBigError",
88
- "ConfigValueTooSmallError",
89
- "EvaluationSuspendedError",
90
- "ExpectedOneOfValuesError",
91
- "ExpectedValueOfTypeError",
92
- "FunctionPluginValidationError",
93
- "InvalidAddressError",
94
- "InvalidArgumentsError",
95
- "LanguageAlreadyRegisteredError",
96
- "LanguageNotRegisteredError",
97
- "MissingTranslationError",
98
- "NamedExpressionDoesNotExistError",
99
- "NamedExpressionNameIsAlreadyTakenError",
100
- "NamedExpressionNameIsInvalidError",
101
- "NoOperationToRedoError",
102
- "NoOperationToUndoError",
103
- "NoRelativeAddressesAllowedError",
104
- "NoSheetWithIdError",
105
- "NoSheetWithNameError",
106
- "NotAFormulaError",
107
- "NothingToPasteError",
108
- "ProtectedFunctionTranslationError",
109
- "SheetNameAlreadyTakenError",
110
- "SheetSizeLimitExceededError",
111
- "SourceLocationHasArrayError",
112
- "TargetLocationHasArrayError",
113
- "UnableToParseError",
114
- "HeadlessArgumentError",
115
- "HeadlessConfigError",
116
- "HeadlessSheetError",
117
- "HeadlessNamedExpressionError",
118
- "HeadlessClipboardError",
119
- "HeadlessEvaluationSuspendedError",
120
- "HeadlessParseError",
121
- "HeadlessOperationError",
93
+ const WORKPAPER_PUBLIC_ERROR_NAMES = new Set([
94
+ "WorkPaperConfigValueTooBigError",
95
+ "WorkPaperConfigValueTooSmallError",
96
+ "WorkPaperEvaluationSuspendedError",
97
+ "WorkPaperExpectedOneOfValuesError",
98
+ "WorkPaperExpectedValueOfTypeError",
99
+ "WorkPaperFunctionPluginValidationError",
100
+ "WorkPaperInvalidAddressError",
101
+ "WorkPaperInvalidArgumentsError",
102
+ "WorkPaperLanguageAlreadyRegisteredError",
103
+ "WorkPaperLanguageNotRegisteredError",
104
+ "WorkPaperMissingTranslationError",
105
+ "WorkPaperNamedExpressionDoesNotExistError",
106
+ "WorkPaperNamedExpressionNameIsAlreadyTakenError",
107
+ "WorkPaperNamedExpressionNameIsInvalidError",
108
+ "WorkPaperNoOperationToRedoError",
109
+ "WorkPaperNoOperationToUndoError",
110
+ "WorkPaperNoRelativeAddressesAllowedError",
111
+ "WorkPaperNoSheetWithIdError",
112
+ "WorkPaperNoSheetWithNameError",
113
+ "WorkPaperNotAFormulaError",
114
+ "WorkPaperNothingToPasteError",
115
+ "WorkPaperProtectedFunctionTranslationError",
116
+ "WorkPaperSheetNameAlreadyTakenError",
117
+ "WorkPaperSheetSizeLimitExceededError",
118
+ "WorkPaperSourceLocationHasArrayError",
119
+ "WorkPaperTargetLocationHasArrayError",
120
+ "WorkPaperUnableToParseError",
121
+ "WorkPaperConfigError",
122
+ "WorkPaperSheetError",
123
+ "WorkPaperNamedExpressionError",
124
+ "WorkPaperClipboardError",
125
+ "WorkPaperParseError",
126
+ "WorkPaperOperationError",
122
127
  ]);
123
- const HEADLESS_VERSION = "0.1.2";
124
- const HEADLESS_BUILD_DATE = "2026-04-10";
125
- const HEADLESS_RELEASE_DATE = "2026-04-10";
128
+ const WORKPAPER_VERSION = "0.1.2";
129
+ const WORKPAPER_BUILD_DATE = "2026-04-10";
130
+ const WORKPAPER_RELEASE_DATE = "2026-04-10";
126
131
  const globalCustomFunctions = new Map();
127
132
  let customAdapterInstalled = false;
128
133
  let nextWorkbookId = 1;
@@ -165,6 +170,10 @@ function clonePluginDefinition(plugin) {
165
170
  function cloneConfig(config) {
166
171
  return {
167
172
  ...config,
173
+ chooseAddressMappingPolicy: config.chooseAddressMappingPolicy
174
+ ? { ...config.chooseAddressMappingPolicy }
175
+ : undefined,
176
+ context: config.context !== undefined ? structuredClone(config.context) : undefined,
168
177
  currencySymbol: config.currencySymbol ? [...config.currencySymbol] : undefined,
169
178
  dateFormats: config.dateFormats ? [...config.dateFormats] : undefined,
170
179
  functionPlugins: config.functionPlugins
@@ -259,7 +268,7 @@ function makeNamedExpressionKey(name, scope) {
259
268
  return `${scope ?? "workbook"}:${normalizeName(name)}`;
260
269
  }
261
270
  function makeInternalScopedName(scope, name) {
262
- return `__BILIG_HEADLESS_SCOPE_${scope}_${normalizeName(name)}`;
271
+ return `__BILIG_WORKPAPER_SCOPE_${scope}_${normalizeName(name)}`;
263
272
  }
264
273
  function isFormulaContent(content) {
265
274
  return typeof content === "string" && content.trim().startsWith("=");
@@ -267,7 +276,7 @@ function isFormulaContent(content) {
267
276
  function isCellValueMatrix(value) {
268
277
  return Array.isArray(value);
269
278
  }
270
- function isHeadlessSheetMatrix(value) {
279
+ function isWorkPaperSheetMatrix(value) {
271
280
  return Array.isArray(value);
272
281
  }
273
282
  function matrixContainsFormulaContent(content) {
@@ -284,7 +293,7 @@ function stripLeadingEquals(formula) {
284
293
  }
285
294
  function assertRowAndColumn(value, label) {
286
295
  if (!Number.isInteger(value) || value < 0) {
287
- throw new InvalidArgumentsError(`${label} to be a non-negative integer`);
296
+ throw new WorkPaperInvalidArgumentsError(`${label} to be a non-negative integer`);
288
297
  }
289
298
  }
290
299
  function assertRange(range) {
@@ -295,7 +304,7 @@ function assertRange(range) {
295
304
  assertRowAndColumn(range.end.row, "end.row");
296
305
  assertRowAndColumn(range.end.col, "end.col");
297
306
  if (range.start.sheet !== range.end.sheet) {
298
- throw new HeadlessArgumentError("Ranges must stay on a single sheet");
307
+ throw new WorkPaperInvalidArgumentsError("Ranges must stay on a single sheet");
299
308
  }
300
309
  }
301
310
  function isCellRange(value) {
@@ -443,7 +452,7 @@ function formulaHasRelativeReferences(node) {
443
452
  function compareSheetNames(left, right) {
444
453
  return left.localeCompare(right);
445
454
  }
446
- function checkHeadlessLicenseKeyValidity(licenseKey) {
455
+ function checkWorkPaperLicenseKeyValidity(licenseKey) {
447
456
  if (!licenseKey || licenseKey.trim().length === 0) {
448
457
  return "missing";
449
458
  }
@@ -454,56 +463,80 @@ function checkHeadlessLicenseKeyValidity(licenseKey) {
454
463
  }
455
464
  return "invalid";
456
465
  }
457
- function validateHeadlessConfig(config) {
466
+ function validateWorkPaperConfig(config) {
458
467
  if (config.maxRows !== undefined && (!Number.isInteger(config.maxRows) || config.maxRows < 1)) {
459
- throw new ConfigValueTooSmallError("maxRows", 1);
468
+ throw new WorkPaperConfigValueTooSmallError("maxRows", 1);
460
469
  }
461
470
  if (config.maxColumns !== undefined &&
462
471
  (!Number.isInteger(config.maxColumns) || config.maxColumns < 1)) {
463
- throw new ConfigValueTooSmallError("maxColumns", 1);
472
+ throw new WorkPaperConfigValueTooSmallError("maxColumns", 1);
464
473
  }
465
474
  if ((config.maxRows ?? MAX_ROWS) > MAX_ROWS) {
466
- throw new ConfigValueTooBigError("maxRows", MAX_ROWS);
475
+ throw new WorkPaperConfigValueTooBigError("maxRows", MAX_ROWS);
467
476
  }
468
477
  if ((config.maxColumns ?? MAX_COLS) > MAX_COLS) {
469
- throw new ConfigValueTooBigError("maxColumns", MAX_COLS);
478
+ throw new WorkPaperConfigValueTooBigError("maxColumns", MAX_COLS);
470
479
  }
471
480
  if (config.decimalSeparator !== undefined &&
472
481
  config.decimalSeparator !== "." &&
473
482
  config.decimalSeparator !== ",") {
474
- throw new ExpectedOneOfValuesError('".", ","', "decimalSeparator");
483
+ throw new WorkPaperExpectedOneOfValuesError('".", ","', "decimalSeparator");
475
484
  }
476
485
  if (config.arrayColumnSeparator !== undefined &&
477
486
  config.arrayColumnSeparator !== "," &&
478
487
  config.arrayColumnSeparator !== ";") {
479
- throw new ExpectedOneOfValuesError('",", ";"', "arrayColumnSeparator");
488
+ throw new WorkPaperExpectedOneOfValuesError('",", ";"', "arrayColumnSeparator");
480
489
  }
481
490
  if (config.arrayRowSeparator !== undefined &&
482
491
  config.arrayRowSeparator !== ";" &&
483
492
  config.arrayRowSeparator !== "|") {
484
- throw new ExpectedOneOfValuesError('";", "|"', "arrayRowSeparator");
493
+ throw new WorkPaperExpectedOneOfValuesError('";", "|"', "arrayRowSeparator");
485
494
  }
486
495
  if (config.ignoreWhiteSpace !== undefined &&
487
496
  config.ignoreWhiteSpace !== "standard" &&
488
497
  config.ignoreWhiteSpace !== "any") {
489
- throw new ExpectedOneOfValuesError('"standard", "any"', "ignoreWhiteSpace");
498
+ throw new WorkPaperExpectedOneOfValuesError('"standard", "any"', "ignoreWhiteSpace");
490
499
  }
491
500
  if (config.caseFirst !== undefined &&
492
501
  config.caseFirst !== "upper" &&
493
502
  config.caseFirst !== "lower" &&
494
503
  config.caseFirst !== "false") {
495
- throw new ExpectedOneOfValuesError('"upper", "lower", "false"', "caseFirst");
504
+ throw new WorkPaperExpectedOneOfValuesError('"upper", "lower", "false"', "caseFirst");
505
+ }
506
+ if (config.chooseAddressMappingPolicy !== undefined &&
507
+ (typeof config.chooseAddressMappingPolicy !== "object" ||
508
+ config.chooseAddressMappingPolicy === null ||
509
+ (config.chooseAddressMappingPolicy.mode !== "dense" &&
510
+ config.chooseAddressMappingPolicy.mode !== "sparse"))) {
511
+ throw new WorkPaperExpectedOneOfValuesError('"dense", "sparse"', "chooseAddressMappingPolicy.mode");
512
+ }
513
+ if (config.parseDateTime !== undefined && typeof config.parseDateTime !== "function") {
514
+ throw new WorkPaperExpectedValueOfTypeError("function", "parseDateTime");
515
+ }
516
+ if (config.stringifyDateTime !== undefined && typeof config.stringifyDateTime !== "function") {
517
+ throw new WorkPaperExpectedValueOfTypeError("function", "stringifyDateTime");
518
+ }
519
+ if (config.stringifyDuration !== undefined && typeof config.stringifyDuration !== "function") {
520
+ throw new WorkPaperExpectedValueOfTypeError("function", "stringifyDuration");
521
+ }
522
+ if (config.context !== undefined) {
523
+ try {
524
+ structuredClone(config.context);
525
+ }
526
+ catch {
527
+ throw new WorkPaperExpectedValueOfTypeError("structured-cloneable value", "context");
528
+ }
496
529
  }
497
530
  }
498
531
  function validateSheetWithinLimits(sheetName, sheet, config) {
499
532
  const height = sheet.length;
500
533
  const width = Math.max(0, ...sheet.map((row) => row.length));
501
534
  if (height > (config.maxRows ?? MAX_ROWS) || width > (config.maxColumns ?? MAX_COLS)) {
502
- throw new SheetSizeLimitExceededError();
535
+ throw new WorkPaperSheetSizeLimitExceededError();
503
536
  }
504
537
  sheet.forEach((row) => {
505
538
  if (!Array.isArray(row)) {
506
- throw new UnableToParseError({ sheetName, reason: "Rows must be arrays" });
539
+ throw new WorkPaperUnableToParseError({ sheetName, reason: "Rows must be arrays" });
507
540
  }
508
541
  });
509
542
  }
@@ -549,7 +582,7 @@ function formatQualifiedCellAddress(sheetName, row, col) {
549
582
  const base = formatAddress(row, col);
550
583
  return sheetName ? `${quoteSheetNameIfNeeded(sheetName)}!${base}` : base;
551
584
  }
552
- class HeadlessEmitter {
585
+ class WorkPaperEmitter {
553
586
  listeners = {
554
587
  sheetAdded: new Set(),
555
588
  sheetRemoved: new Set(),
@@ -671,17 +704,17 @@ class HeadlessEmitter {
671
704
  Object.values(this.detailedListeners).forEach((listeners) => listeners.clear());
672
705
  }
673
706
  }
674
- export class HeadlessWorkbook {
675
- static version = HEADLESS_VERSION;
676
- static buildDate = HEADLESS_BUILD_DATE;
677
- static releaseDate = HEADLESS_RELEASE_DATE;
707
+ export class WorkPaper {
708
+ static version = WORKPAPER_VERSION;
709
+ static buildDate = WORKPAPER_BUILD_DATE;
710
+ static releaseDate = WORKPAPER_RELEASE_DATE;
678
711
  static languages = {};
679
712
  static defaultConfig = cloneConfig(DEFAULT_CONFIG);
680
713
  static languageRegistry = new Map();
681
714
  static functionPluginRegistry = new Map();
682
715
  workbookId = nextWorkbookId++;
683
- engine = new SpreadsheetEngine({ workbookName: "Workbook" });
684
- emitter = new HeadlessEmitter();
716
+ engine;
717
+ emitter = new WorkPaperEmitter();
685
718
  namedExpressions = new Map();
686
719
  functionSnapshot = new Map();
687
720
  functionAliasLookup = new Map();
@@ -689,6 +722,8 @@ export class HeadlessWorkbook {
689
722
  internals;
690
723
  config;
691
724
  clipboard = null;
725
+ visibilityCache = null;
726
+ namedExpressionValueCache = null;
692
727
  batchDepth = 0;
693
728
  batchStartVisibility = null;
694
729
  batchStartNamedValues = null;
@@ -699,14 +734,23 @@ export class HeadlessWorkbook {
699
734
  suspendedVisibility = null;
700
735
  suspendedNamedValues = null;
701
736
  queuedEvents = [];
737
+ trackedEngineEvents = [];
738
+ engineEventCaptureEnabled = true;
739
+ unsubscribeEngineEvents = null;
702
740
  disposed = false;
703
741
  constructor(configInput = {}) {
704
742
  ensureCustomAdapterInstalled();
705
- validateHeadlessConfig(configInput);
743
+ validateWorkPaperConfig(configInput);
706
744
  this.config = {
707
745
  ...cloneConfig(DEFAULT_CONFIG),
708
746
  ...cloneConfig(configInput),
709
747
  };
748
+ this.engine = new SpreadsheetEngine({
749
+ workbookName: "Workbook",
750
+ useColumnIndex: this.config.useColumnIndex,
751
+ trackReplicaVersions: false,
752
+ });
753
+ this.attachEngineEventTracking();
710
754
  this.captureFunctionRegistry();
711
755
  this.internals = Object.freeze({
712
756
  graph: Object.freeze({
@@ -765,51 +809,59 @@ export class HeadlessWorkbook {
765
809
  });
766
810
  }
767
811
  static buildEmpty(configInput = {}, namedExpressions = []) {
768
- const workbook = new HeadlessWorkbook(configInput);
769
- namedExpressions.forEach((expression) => {
770
- workbook.upsertNamedExpressionInternal(expression, { duringInitialization: true });
812
+ const workbook = new WorkPaper(configInput);
813
+ workbook.withEngineEventCaptureDisabled(() => {
814
+ namedExpressions.forEach((expression) => {
815
+ workbook.upsertNamedExpressionInternal(expression, { duringInitialization: true });
816
+ });
771
817
  });
772
818
  workbook.clearHistoryStacks();
819
+ workbook.primeChangeTrackingCaches();
773
820
  return workbook;
774
821
  }
775
822
  static buildFromArray(sheet, configInput = {}, namedExpressions = []) {
776
823
  return this.buildFromSheets({ Sheet1: sheet }, configInput, namedExpressions);
777
824
  }
778
825
  static buildFromSheets(sheets, configInput = {}, namedExpressions = []) {
779
- const workbook = new HeadlessWorkbook(configInput);
826
+ const workbook = new WorkPaper(configInput);
780
827
  Object.entries(sheets).forEach(([sheetName, sheet]) => {
781
828
  validateSheetWithinLimits(sheetName, sheet, workbook.config);
782
829
  });
783
- Object.keys(sheets).forEach((sheetName) => {
784
- workbook.engine.createSheet(sheetName);
785
- });
786
- namedExpressions.forEach((expression) => {
787
- workbook.upsertNamedExpressionInternal(expression, { duringInitialization: true });
788
- });
789
- Object.entries(sheets).forEach(([sheetName, sheet]) => {
790
- const sheetId = workbook.requireSheetId(sheetName);
791
- workbook.replaceSheetContentInternal(sheetId, sheet, { duringInitialization: true });
830
+ workbook.withEngineEventCaptureDisabled(() => {
831
+ Object.keys(sheets).forEach((sheetName) => {
832
+ workbook.engine.createSheet(sheetName);
833
+ });
834
+ namedExpressions.forEach((expression) => {
835
+ workbook.upsertNamedExpressionInternal(expression, { duringInitialization: true });
836
+ });
837
+ Object.entries(sheets).forEach(([sheetName, sheet]) => {
838
+ const sheetId = workbook.requireSheetId(sheetName);
839
+ if (!tryLoadInitialLiteralSheet(workbook.engine, sheetId, sheet)) {
840
+ workbook.replaceSheetContentInternal(sheetId, sheet, { duringInitialization: true });
841
+ }
842
+ });
792
843
  });
793
844
  workbook.clearHistoryStacks();
845
+ workbook.primeChangeTrackingCaches();
794
846
  return workbook;
795
847
  }
796
848
  static getLanguage(languageCode) {
797
849
  const language = this.languageRegistry.get(languageCode);
798
850
  if (!language) {
799
- throw new LanguageNotRegisteredError(languageCode);
851
+ throw new WorkPaperLanguageNotRegisteredError(languageCode);
800
852
  }
801
853
  return structuredClone(language);
802
854
  }
803
855
  static registerLanguage(languageCode, languagePackage) {
804
856
  if (this.languageRegistry.has(languageCode)) {
805
- throw new LanguageAlreadyRegisteredError(languageCode);
857
+ throw new WorkPaperLanguageAlreadyRegisteredError(languageCode);
806
858
  }
807
859
  this.languageRegistry.set(languageCode, structuredClone(languagePackage));
808
860
  this.languages[languageCode] = structuredClone(languagePackage);
809
861
  }
810
862
  static unregisterLanguage(languageCode) {
811
863
  if (!this.languageRegistry.delete(languageCode)) {
812
- throw new LanguageNotRegisteredError(languageCode);
864
+ throw new WorkPaperLanguageNotRegisteredError(languageCode);
813
865
  }
814
866
  delete this.languages[languageCode];
815
867
  }
@@ -830,7 +882,7 @@ export class HeadlessWorkbook {
830
882
  const existing = this.functionPluginRegistry.get(plugin.id);
831
883
  const nextPlugin = clonePluginDefinition(existing ?? plugin);
832
884
  if (!nextPlugin.implementedFunctions[functionId]) {
833
- throw FunctionPluginValidationError.functionNotDeclaredInPlugin(functionId, plugin.id);
885
+ throw WorkPaperFunctionPluginValidationError.functionNotDeclaredInPlugin(functionId, plugin.id);
834
886
  }
835
887
  this.functionPluginRegistry.set(nextPlugin.id, nextPlugin);
836
888
  if (translations) {
@@ -884,7 +936,7 @@ export class HeadlessWorkbook {
884
936
  Object.entries(translations).forEach(([languageCode, functionTranslations]) => {
885
937
  const existing = this.languageRegistry.get(languageCode);
886
938
  if (!existing) {
887
- throw new LanguageNotRegisteredError(languageCode);
939
+ throw new WorkPaperLanguageNotRegisteredError(languageCode);
888
940
  }
889
941
  const nextLanguage = {
890
942
  ...structuredClone(existing),
@@ -950,7 +1002,7 @@ export class HeadlessWorkbook {
950
1002
  return this.internals.lazilyTransformingAstService;
951
1003
  }
952
1004
  get licenseKeyValidityState() {
953
- return checkHeadlessLicenseKeyValidity(this.config.licenseKey);
1005
+ return checkWorkPaperLicenseKeyValidity(this.config.licenseKey);
954
1006
  }
955
1007
  updateConfig(next) {
956
1008
  this.assertNotDisposed();
@@ -958,7 +1010,7 @@ export class HeadlessWorkbook {
958
1010
  ...this.config,
959
1011
  ...cloneConfig(next),
960
1012
  };
961
- const hasChanges = HEADLESS_CONFIG_KEYS.some((key) => Object.hasOwn(next, key) && this.config[key] !== next[key]);
1013
+ const hasChanges = WORKPAPER_CONFIG_KEYS.some((key) => Object.hasOwn(next, key) && this.config[key] !== next[key]);
962
1014
  if (!hasChanges) {
963
1015
  return;
964
1016
  }
@@ -981,9 +1033,10 @@ export class HeadlessWorkbook {
981
1033
  this.assertNotDisposed();
982
1034
  const isOutermost = this.batchDepth === 0;
983
1035
  if (isOutermost) {
984
- this.batchStartVisibility = this.captureVisibilitySnapshot();
985
- this.batchStartNamedValues = this.captureNamedExpressionValueSnapshot();
1036
+ this.batchStartVisibility = this.ensureVisibilityCache();
1037
+ this.batchStartNamedValues = this.ensureNamedExpressionValueCache();
986
1038
  this.batchUndoStackLength = this.getUndoStack().length;
1039
+ this.drainTrackedEngineEvents();
987
1040
  }
988
1041
  this.batchDepth += 1;
989
1042
  try {
@@ -999,7 +1052,7 @@ export class HeadlessWorkbook {
999
1052
  if (!isOutermost) {
1000
1053
  return [];
1001
1054
  }
1002
- const changes = this.computeChanges(this.batchStartVisibility ?? new Map(), this.captureVisibilitySnapshot(), this.batchStartNamedValues ?? new Map(), this.captureNamedExpressionValueSnapshot());
1055
+ const changes = this.computeChangesAfterMutation(this.batchStartVisibility ?? new Map(), this.batchStartNamedValues ?? new Map());
1003
1056
  this.batchStartVisibility = null;
1004
1057
  this.batchStartNamedValues = null;
1005
1058
  if (!this.evaluationSuspended) {
@@ -1016,8 +1069,9 @@ export class HeadlessWorkbook {
1016
1069
  return;
1017
1070
  }
1018
1071
  this.evaluationSuspended = true;
1019
- this.suspendedVisibility = this.captureVisibilitySnapshot();
1020
- this.suspendedNamedValues = this.captureNamedExpressionValueSnapshot();
1072
+ this.suspendedVisibility = this.ensureVisibilityCache();
1073
+ this.suspendedNamedValues = this.ensureNamedExpressionValueCache();
1074
+ this.drainTrackedEngineEvents();
1021
1075
  this.emitter.emitDetailed({ eventName: "evaluationSuspended", payload: {} });
1022
1076
  }
1023
1077
  resumeEvaluation() {
@@ -1025,7 +1079,7 @@ export class HeadlessWorkbook {
1025
1079
  if (!this.evaluationSuspended) {
1026
1080
  return [];
1027
1081
  }
1028
- const changes = this.computeChanges(this.suspendedVisibility ?? new Map(), this.captureVisibilitySnapshot(), this.suspendedNamedValues ?? new Map(), this.captureNamedExpressionValueSnapshot());
1082
+ const changes = this.computeChangesAfterMutation(this.suspendedVisibility ?? new Map(), this.suspendedNamedValues ?? new Map());
1029
1083
  this.evaluationSuspended = false;
1030
1084
  this.suspendedVisibility = null;
1031
1085
  this.suspendedNamedValues = null;
@@ -1043,7 +1097,7 @@ export class HeadlessWorkbook {
1043
1097
  this.assertNotDisposed();
1044
1098
  return this.captureChanges(undefined, () => {
1045
1099
  if (!this.engine.undo()) {
1046
- throw new NoOperationToUndoError();
1100
+ throw new WorkPaperNoOperationToUndoError();
1047
1101
  }
1048
1102
  });
1049
1103
  }
@@ -1051,7 +1105,7 @@ export class HeadlessWorkbook {
1051
1105
  this.assertNotDisposed();
1052
1106
  return this.captureChanges(undefined, () => {
1053
1107
  if (!this.engine.redo()) {
1054
- throw new NoOperationToRedoError();
1108
+ throw new WorkPaperNoOperationToRedoError();
1055
1109
  }
1056
1110
  });
1057
1111
  }
@@ -1090,7 +1144,7 @@ export class HeadlessWorkbook {
1090
1144
  paste(targetLeftCorner) {
1091
1145
  this.assertNotDisposed();
1092
1146
  if (!this.clipboard) {
1093
- throw new NothingToPasteError();
1147
+ throw new WorkPaperNothingToPasteError();
1094
1148
  }
1095
1149
  return this.captureChanges(undefined, () => {
1096
1150
  this.applySerializedMatrix(targetLeftCorner, this.clipboard.serialized, this.clipboard.sourceAnchor);
@@ -1441,7 +1495,7 @@ export class HeadlessWorkbook {
1441
1495
  }
1442
1496
  addNamedExpression(expressionName, expression, scope, options) {
1443
1497
  if (!this.isItPossibleToAddNamedExpression(expressionName, expression, scope)) {
1444
- throw new NamedExpressionNameIsAlreadyTakenError(expressionName);
1498
+ throw new WorkPaperNamedExpressionNameIsAlreadyTakenError(expressionName);
1445
1499
  }
1446
1500
  return this.captureChanges({
1447
1501
  eventName: "namedExpressionAdded",
@@ -1456,7 +1510,7 @@ export class HeadlessWorkbook {
1456
1510
  }
1457
1511
  changeNamedExpression(expressionName, expression, scope, options) {
1458
1512
  if (!this.isItPossibleToChangeNamedExpression(expressionName, expression, scope)) {
1459
- throw new NamedExpressionDoesNotExistError(expressionName);
1513
+ throw new WorkPaperNamedExpressionDoesNotExistError(expressionName);
1460
1514
  }
1461
1515
  return this.captureChanges(undefined, () => {
1462
1516
  this.upsertNamedExpressionInternal({ name: expressionName, expression, scope, options }, { duringInitialization: false });
@@ -1464,7 +1518,7 @@ export class HeadlessWorkbook {
1464
1518
  }
1465
1519
  removeNamedExpression(expressionName, scope) {
1466
1520
  if (!this.isItPossibleToRemoveNamedExpression(expressionName, scope)) {
1467
- throw new NamedExpressionDoesNotExistError(expressionName);
1521
+ throw new WorkPaperNamedExpressionDoesNotExistError(expressionName);
1468
1522
  }
1469
1523
  const existing = this.namedExpressionRecord(expressionName, scope);
1470
1524
  return this.captureChanges({
@@ -1497,44 +1551,57 @@ export class HeadlessWorkbook {
1497
1551
  }
1498
1552
  normalizeFormula(formula) {
1499
1553
  if (!formula.trim().startsWith("=")) {
1500
- throw new NotAFormulaError();
1554
+ throw new WorkPaperNotAFormulaError();
1501
1555
  }
1502
1556
  try {
1503
1557
  return `=${serializeFormula(parseFormula(stripLeadingEquals(formula)))}`;
1504
1558
  }
1505
1559
  catch (error) {
1506
- throw new HeadlessParseError(this.messageOf(error, `Unable to normalize formula`));
1560
+ throw new WorkPaperParseError(this.messageOf(error, `Unable to normalize formula`));
1507
1561
  }
1508
1562
  }
1509
1563
  calculateFormula(formula, scope) {
1510
1564
  if (!formula.trim().startsWith("=")) {
1511
- throw new NotAFormulaError();
1565
+ throw new WorkPaperNotAFormulaError();
1512
1566
  }
1513
1567
  try {
1514
- const temporaryWorkbook = HeadlessWorkbook.buildFromSheets(this.getAllSheetsSerialized(), this.getConfig(), this.getAllNamedExpressionsSerialized());
1515
- const scratchSheetName = scope !== undefined ? `__HEADLESS_CALC_${scope}__` : "__HEADLESS_CALC__";
1516
- temporaryWorkbook.engine.createSheet(scratchSheetName);
1517
- const scratchSheetId = temporaryWorkbook.requireSheetId(scratchSheetName);
1518
- temporaryWorkbook.applyRawContent(scratchSheetName, "A1", formula.trim().startsWith("=") ? formula : `=${formula}`, scratchSheetId);
1519
- const spill = temporaryWorkbook.engine
1520
- .getSpillRanges()
1521
- .find((candidate) => candidate.sheetName === scratchSheetName && candidate.address === "A1");
1522
- const value = spill
1523
- ? temporaryWorkbook.getRangeValues({
1524
- start: { sheet: scratchSheetId, row: 0, col: 0 },
1525
- end: { sheet: scratchSheetId, row: spill.rows - 1, col: spill.cols - 1 },
1526
- })
1527
- : temporaryWorkbook.getCellValue({ sheet: scratchSheetId, row: 0, col: 0 });
1528
- temporaryWorkbook.dispose();
1529
- return value;
1568
+ return calculateWorkPaperFormulaInScratchWorkbook({
1569
+ createWorkbook: (config) => {
1570
+ const temporaryWorkbook = new WorkPaper(config);
1571
+ return {
1572
+ engine: temporaryWorkbook.engine,
1573
+ registerNamedExpression: (expression) => {
1574
+ temporaryWorkbook.upsertNamedExpressionInternal(expression, {
1575
+ duringInitialization: true,
1576
+ });
1577
+ },
1578
+ requireSheetId: (sheetName) => temporaryWorkbook.requireSheetId(sheetName),
1579
+ replaceSheetContent: (sheetId, sheet) => {
1580
+ temporaryWorkbook.replaceSheetContentInternal(sheetId, sheet, {
1581
+ duringInitialization: true,
1582
+ });
1583
+ },
1584
+ clearHistoryStacks: () => temporaryWorkbook.clearHistoryStacks(),
1585
+ applyRawContent: (address, content) => temporaryWorkbook.applyRawContent(address, content),
1586
+ getRangeValues: (range) => temporaryWorkbook.getRangeValues(range),
1587
+ getCellValue: (address) => temporaryWorkbook.getCellValue(address),
1588
+ dispose: () => temporaryWorkbook.dispose(),
1589
+ };
1590
+ },
1591
+ config: this.getConfig(),
1592
+ serializedSheets: this.getAllSheetsSerialized(),
1593
+ namedExpressions: this.getAllNamedExpressionsSerialized(),
1594
+ formula,
1595
+ scope,
1596
+ });
1530
1597
  }
1531
1598
  catch (error) {
1532
- throw new HeadlessParseError(this.messageOf(error, "Unable to calculate formula"));
1599
+ throw new WorkPaperParseError(this.messageOf(error, "Unable to calculate formula"));
1533
1600
  }
1534
1601
  }
1535
1602
  getNamedExpressionsFromFormula(formula) {
1536
1603
  if (!formula.trim().startsWith("=")) {
1537
- throw new NotAFormulaError();
1604
+ throw new WorkPaperNotAFormulaError();
1538
1605
  }
1539
1606
  try {
1540
1607
  const parsed = parseFormula(stripLeadingEquals(formula));
@@ -1543,7 +1610,7 @@ export class HeadlessWorkbook {
1543
1610
  return [...names].toSorted(compareSheetNames);
1544
1611
  }
1545
1612
  catch (error) {
1546
- throw new HeadlessParseError(this.messageOf(error, "Unable to inspect formula"));
1613
+ throw new WorkPaperParseError(this.messageOf(error, "Unable to inspect formula"));
1547
1614
  }
1548
1615
  }
1549
1616
  validateFormula(formula) {
@@ -1560,7 +1627,7 @@ export class HeadlessWorkbook {
1560
1627
  }
1561
1628
  getRegisteredFunctionNames(languageCode) {
1562
1629
  const code = languageCode ?? this.config.language ?? "enGB";
1563
- const language = HeadlessWorkbook.languageRegistry.get(code);
1630
+ const language = WorkPaper.languageRegistry.get(code);
1564
1631
  const functions = [...this.functionSnapshot.values()]
1565
1632
  .filter((binding) => binding.publicName === binding.publicName.toUpperCase())
1566
1633
  .map((binding) => binding.publicName)
@@ -1575,13 +1642,13 @@ export class HeadlessWorkbook {
1575
1642
  if (!binding) {
1576
1643
  return undefined;
1577
1644
  }
1578
- const plugin = HeadlessWorkbook.functionPluginRegistry.get(binding.pluginId);
1645
+ const plugin = WorkPaper.functionPluginRegistry.get(binding.pluginId);
1579
1646
  return plugin ? clonePluginDefinition(plugin) : undefined;
1580
1647
  }
1581
1648
  getAllFunctionPlugins() {
1582
1649
  const pluginIds = new Set([...this.functionSnapshot.values()].map((binding) => binding.pluginId));
1583
1650
  return [...pluginIds]
1584
- .map((pluginId) => HeadlessWorkbook.functionPluginRegistry.get(pluginId))
1651
+ .map((pluginId) => WorkPaper.functionPluginRegistry.get(pluginId))
1585
1652
  .filter((plugin) => plugin !== undefined)
1586
1653
  .map((plugin) => clonePluginDefinition(plugin));
1587
1654
  }
@@ -1622,28 +1689,28 @@ export class HeadlessWorkbook {
1622
1689
  return { hours, minutes, seconds };
1623
1690
  }
1624
1691
  setCellContents(address, content) {
1692
+ this.assertNotDisposed();
1625
1693
  if (!this.isItPossibleToSetCellContents(address, content)) {
1626
- throw new HeadlessOperationError("Cell contents cannot be set");
1694
+ throw new WorkPaperOperationError("Cell contents cannot be set");
1695
+ }
1696
+ if (!isWorkPaperSheetMatrix(content) &&
1697
+ this.enqueueDeferredBatchLiteral(address.sheet, address.row, address.col, content)) {
1698
+ return [];
1627
1699
  }
1628
1700
  return this.captureChanges(undefined, () => {
1629
- if (isHeadlessSheetMatrix(content)) {
1701
+ if (isWorkPaperSheetMatrix(content)) {
1630
1702
  this.flushPendingBatchOps();
1631
1703
  this.applyMatrixContents(address, content);
1632
1704
  return;
1633
1705
  }
1634
- const sheetName = this.sheetName(address.sheet);
1635
- const a1 = this.a1(address);
1636
- if (this.enqueueDeferredBatchLiteral(sheetName, a1, content)) {
1637
- return;
1638
- }
1639
1706
  this.flushPendingBatchOps();
1640
- this.applyRawContent(sheetName, a1, content, address.sheet);
1707
+ this.applyRawContent(address, content);
1641
1708
  });
1642
1709
  }
1643
1710
  swapRowIndexes(sheetId, rowAOrMappings, rowB) {
1644
1711
  const mappings = this.normalizeAxisSwapMappings("row", rowAOrMappings, rowB);
1645
1712
  if (!this.isItPossibleToSwapRowIndexes(sheetId, mappings)) {
1646
- throw new HeadlessOperationError("Rows cannot be swapped");
1713
+ throw new WorkPaperOperationError("Rows cannot be swapped");
1647
1714
  }
1648
1715
  return this.batch(() => {
1649
1716
  mappings.forEach(([rowA, mappedRowB]) => {
@@ -1663,7 +1730,7 @@ export class HeadlessWorkbook {
1663
1730
  }
1664
1731
  setRowOrder(sheetId, rowOrder) {
1665
1732
  if (!this.isItPossibleToSetRowOrder(sheetId, rowOrder)) {
1666
- throw new HeadlessOperationError("Row order is invalid");
1733
+ throw new WorkPaperOperationError("Row order is invalid");
1667
1734
  }
1668
1735
  const current = rowOrder.toSorted((left, right) => left - right);
1669
1736
  return this.batch(() => {
@@ -1681,7 +1748,7 @@ export class HeadlessWorkbook {
1681
1748
  swapColumnIndexes(sheetId, columnAOrMappings, columnB) {
1682
1749
  const mappings = this.normalizeAxisSwapMappings("column", columnAOrMappings, columnB);
1683
1750
  if (!this.isItPossibleToSwapColumnIndexes(sheetId, mappings)) {
1684
- throw new HeadlessOperationError("Columns cannot be swapped");
1751
+ throw new WorkPaperOperationError("Columns cannot be swapped");
1685
1752
  }
1686
1753
  return this.batch(() => {
1687
1754
  mappings.forEach(([columnA, mappedColumnB]) => {
@@ -1701,7 +1768,7 @@ export class HeadlessWorkbook {
1701
1768
  }
1702
1769
  setColumnOrder(sheetId, columnOrder) {
1703
1770
  if (!this.isItPossibleToSetColumnOrder(sheetId, columnOrder)) {
1704
- throw new HeadlessOperationError("Column order is invalid");
1771
+ throw new WorkPaperOperationError("Column order is invalid");
1705
1772
  }
1706
1773
  const current = columnOrder.toSorted((left, right) => left - right);
1707
1774
  return this.batch(() => {
@@ -1719,7 +1786,7 @@ export class HeadlessWorkbook {
1719
1786
  addRows(sheetId, startOrInterval, countOrInterval, ...restIntervals) {
1720
1787
  const indexes = this.normalizeAxisIntervals(startOrInterval, countOrInterval, restIntervals);
1721
1788
  if (!this.isItPossibleToAddRows(sheetId, ...indexes)) {
1722
- throw new HeadlessOperationError("Rows cannot be added");
1789
+ throw new WorkPaperOperationError("Rows cannot be added");
1723
1790
  }
1724
1791
  return this.batch(() => {
1725
1792
  indexes.forEach(([start, amount]) => {
@@ -1730,7 +1797,7 @@ export class HeadlessWorkbook {
1730
1797
  removeRows(sheetId, startOrInterval, countOrInterval, ...restIntervals) {
1731
1798
  const indexes = this.normalizeAxisIntervals(startOrInterval, countOrInterval, restIntervals);
1732
1799
  if (!this.isItPossibleToRemoveRows(sheetId, ...indexes)) {
1733
- throw new HeadlessOperationError("Rows cannot be removed");
1800
+ throw new WorkPaperOperationError("Rows cannot be removed");
1734
1801
  }
1735
1802
  return this.batch(() => {
1736
1803
  indexes
@@ -1743,7 +1810,7 @@ export class HeadlessWorkbook {
1743
1810
  addColumns(sheetId, startOrInterval, countOrInterval, ...restIntervals) {
1744
1811
  const indexes = this.normalizeAxisIntervals(startOrInterval, countOrInterval, restIntervals);
1745
1812
  if (!this.isItPossibleToAddColumns(sheetId, ...indexes)) {
1746
- throw new HeadlessOperationError("Columns cannot be added");
1813
+ throw new WorkPaperOperationError("Columns cannot be added");
1747
1814
  }
1748
1815
  return this.batch(() => {
1749
1816
  indexes.forEach(([start, amount]) => {
@@ -1754,7 +1821,7 @@ export class HeadlessWorkbook {
1754
1821
  removeColumns(sheetId, startOrInterval, countOrInterval, ...restIntervals) {
1755
1822
  const indexes = this.normalizeAxisIntervals(startOrInterval, countOrInterval, restIntervals);
1756
1823
  if (!this.isItPossibleToRemoveColumns(sheetId, ...indexes)) {
1757
- throw new HeadlessOperationError("Columns cannot be removed");
1824
+ throw new WorkPaperOperationError("Columns cannot be removed");
1758
1825
  }
1759
1826
  return this.batch(() => {
1760
1827
  indexes
@@ -1766,7 +1833,7 @@ export class HeadlessWorkbook {
1766
1833
  }
1767
1834
  moveCells(source, target) {
1768
1835
  if (!this.isItPossibleToMoveCells(source, target)) {
1769
- throw new HeadlessOperationError("Cells cannot be moved");
1836
+ throw new WorkPaperOperationError("Cells cannot be moved");
1770
1837
  }
1771
1838
  const sourceHeight = source.end.row - source.start.row;
1772
1839
  const sourceWidth = source.end.col - source.start.col;
@@ -1780,7 +1847,7 @@ export class HeadlessWorkbook {
1780
1847
  }
1781
1848
  moveRows(sheetId, start, count, target) {
1782
1849
  if (!this.isItPossibleToMoveRows(sheetId, start, count, target)) {
1783
- throw new HeadlessOperationError("Rows cannot be moved");
1850
+ throw new WorkPaperOperationError("Rows cannot be moved");
1784
1851
  }
1785
1852
  return this.captureChanges(undefined, () => {
1786
1853
  this.engine.moveRows(this.sheetName(sheetId), start, count, target);
@@ -1788,7 +1855,7 @@ export class HeadlessWorkbook {
1788
1855
  }
1789
1856
  moveColumns(sheetId, start, count, target) {
1790
1857
  if (!this.isItPossibleToMoveColumns(sheetId, start, count, target)) {
1791
- throw new HeadlessOperationError("Columns cannot be moved");
1858
+ throw new WorkPaperOperationError("Columns cannot be moved");
1792
1859
  }
1793
1860
  return this.captureChanges(undefined, () => {
1794
1861
  this.engine.moveColumns(this.sheetName(sheetId), start, count, target);
@@ -1798,10 +1865,11 @@ export class HeadlessWorkbook {
1798
1865
  this.assertNotDisposed();
1799
1866
  const name = sheetName?.trim() || this.nextSheetName();
1800
1867
  if (!this.isItPossibleToAddSheet(name)) {
1801
- throw new SheetNameAlreadyTakenError(name);
1868
+ throw new WorkPaperSheetNameAlreadyTakenError(name);
1802
1869
  }
1803
- const beforeVisibility = this.captureVisibilitySnapshot();
1804
- const beforeNames = this.captureNamedExpressionValueSnapshot();
1870
+ const beforeVisibility = this.ensureVisibilityCache();
1871
+ const beforeNames = this.ensureNamedExpressionValueCache();
1872
+ this.drainTrackedEngineEvents();
1805
1873
  this.engine.createSheet(name);
1806
1874
  const sheetId = this.requireSheetId(name);
1807
1875
  const payload = { sheetId, sheetName: name };
@@ -1811,7 +1879,7 @@ export class HeadlessWorkbook {
1811
1879
  else {
1812
1880
  this.emitter.emitDetailed({ eventName: "sheetAdded", payload });
1813
1881
  }
1814
- const changes = this.computeChanges(beforeVisibility, this.captureVisibilitySnapshot(), beforeNames, this.captureNamedExpressionValueSnapshot());
1882
+ const changes = this.computeChangesAfterMutation(beforeVisibility, beforeNames);
1815
1883
  if (!this.shouldSuppressEvents() && changes.length > 0) {
1816
1884
  this.emitter.emitDetailed({ eventName: "valuesUpdated", payload: { changes } });
1817
1885
  }
@@ -1819,7 +1887,7 @@ export class HeadlessWorkbook {
1819
1887
  }
1820
1888
  removeSheet(sheetId) {
1821
1889
  if (!this.isItPossibleToRemoveSheet(sheetId)) {
1822
- throw new HeadlessSheetError(`Sheet '${sheetId}' cannot be removed`);
1890
+ throw new WorkPaperSheetError(`Sheet '${sheetId}' cannot be removed`);
1823
1891
  }
1824
1892
  const sheetName = this.sheetName(sheetId);
1825
1893
  return this.captureChanges({
@@ -1835,7 +1903,7 @@ export class HeadlessWorkbook {
1835
1903
  }
1836
1904
  clearSheet(sheetId) {
1837
1905
  if (!this.isItPossibleToClearSheet(sheetId)) {
1838
- throw new HeadlessSheetError(`Sheet '${sheetId}' cannot be cleared`);
1906
+ throw new WorkPaperSheetError(`Sheet '${sheetId}' cannot be cleared`);
1839
1907
  }
1840
1908
  return this.captureChanges(undefined, () => {
1841
1909
  const dimensions = this.getSheetDimensions(sheetId);
@@ -1851,7 +1919,7 @@ export class HeadlessWorkbook {
1851
1919
  }
1852
1920
  setSheetContent(sheetId, content) {
1853
1921
  if (!this.isItPossibleToReplaceSheetContent(sheetId, content)) {
1854
- throw new HeadlessSheetError(`Sheet '${sheetId}' cannot be replaced`);
1922
+ throw new WorkPaperSheetError(`Sheet '${sheetId}' cannot be replaced`);
1855
1923
  }
1856
1924
  return this.captureChanges(undefined, () => {
1857
1925
  this.replaceSheetContentInternal(sheetId, content, { duringInitialization: false });
@@ -1859,7 +1927,7 @@ export class HeadlessWorkbook {
1859
1927
  }
1860
1928
  renameSheet(sheetId, nextName) {
1861
1929
  if (!this.isItPossibleToRenameSheet(sheetId, nextName)) {
1862
- throw new HeadlessSheetError(`Sheet '${sheetId}' cannot be renamed to '${nextName}'`);
1930
+ throw new WorkPaperSheetError(`Sheet '${sheetId}' cannot be renamed to '${nextName}'`);
1863
1931
  }
1864
1932
  const oldName = this.sheetName(sheetId);
1865
1933
  const newName = nextName.trim();
@@ -1891,7 +1959,7 @@ export class HeadlessWorkbook {
1891
1959
  }
1892
1960
  if (Array.isArray(content)) {
1893
1961
  if (!content.every((row) => Array.isArray(row))) {
1894
- throw new HeadlessArgumentError("Content matrix must be a two-dimensional array");
1962
+ throw new WorkPaperInvalidArgumentsError("Content matrix must be a two-dimensional array");
1895
1963
  }
1896
1964
  const height = content.length;
1897
1965
  const width = Math.max(0, ...content.map((row) => row.length));
@@ -1991,7 +2059,7 @@ export class HeadlessWorkbook {
1991
2059
  isItPossibleToAddSheet(sheetName) {
1992
2060
  const trimmed = sheetName.trim();
1993
2061
  if (trimmed.length === 0) {
1994
- throw new HeadlessArgumentError("Sheet name must be non-empty");
2062
+ throw new WorkPaperInvalidArgumentsError("Sheet name must be non-empty");
1995
2063
  }
1996
2064
  return !this.doesSheetExist(trimmed);
1997
2065
  }
@@ -2004,7 +2072,7 @@ export class HeadlessWorkbook {
2004
2072
  isItPossibleToReplaceSheetContent(sheetId, content) {
2005
2073
  this.sheetRecord(sheetId);
2006
2074
  if (!content.every((row) => Array.isArray(row))) {
2007
- throw new HeadlessArgumentError("Sheet content must be a two-dimensional array");
2075
+ throw new WorkPaperInvalidArgumentsError("Sheet content must be a two-dimensional array");
2008
2076
  }
2009
2077
  const height = content.length;
2010
2078
  const width = Math.max(0, ...content.map((row) => row.length));
@@ -2014,7 +2082,7 @@ export class HeadlessWorkbook {
2014
2082
  this.sheetRecord(sheetId);
2015
2083
  const trimmed = nextName.trim();
2016
2084
  if (trimmed.length === 0) {
2017
- throw new HeadlessArgumentError("Sheet name must be non-empty");
2085
+ throw new WorkPaperInvalidArgumentsError("Sheet name must be non-empty");
2018
2086
  }
2019
2087
  const existing = this.engine.workbook.getSheet(trimmed);
2020
2088
  return !existing || existing.id === sheetId;
@@ -2038,12 +2106,61 @@ export class HeadlessWorkbook {
2038
2106
  return;
2039
2107
  }
2040
2108
  this.disposed = true;
2109
+ this.unsubscribeEngineEvents?.();
2110
+ this.unsubscribeEngineEvents = null;
2041
2111
  this.emitter.clear();
2042
2112
  this.clearFunctionBindings();
2043
2113
  this.clipboard = null;
2114
+ this.visibilityCache = null;
2115
+ this.namedExpressionValueCache = null;
2044
2116
  this.queuedEvents = [];
2117
+ this.trackedEngineEvents = [];
2045
2118
  this.namedExpressions.clear();
2046
2119
  }
2120
+ attachEngineEventTracking() {
2121
+ this.unsubscribeEngineEvents?.();
2122
+ this.trackedEngineEvents = [];
2123
+ this.unsubscribeEngineEvents = this.engine.subscribe((event) => {
2124
+ if (!this.engineEventCaptureEnabled) {
2125
+ return;
2126
+ }
2127
+ this.trackedEngineEvents.push(captureTrackedEngineEvent(event));
2128
+ });
2129
+ }
2130
+ withEngineEventCaptureDisabled(callback) {
2131
+ const previous = this.engineEventCaptureEnabled;
2132
+ this.engineEventCaptureEnabled = false;
2133
+ this.trackedEngineEvents = [];
2134
+ try {
2135
+ return callback();
2136
+ }
2137
+ finally {
2138
+ this.engineEventCaptureEnabled = previous;
2139
+ this.trackedEngineEvents = [];
2140
+ }
2141
+ }
2142
+ drainTrackedEngineEvents() {
2143
+ const events = this.trackedEngineEvents;
2144
+ this.trackedEngineEvents = [];
2145
+ return events;
2146
+ }
2147
+ primeChangeTrackingCaches() {
2148
+ this.visibilityCache = this.captureVisibilitySnapshot();
2149
+ this.namedExpressionValueCache = this.captureNamedExpressionValueSnapshot();
2150
+ this.drainTrackedEngineEvents();
2151
+ }
2152
+ ensureVisibilityCache() {
2153
+ if (!this.visibilityCache) {
2154
+ this.visibilityCache = this.captureVisibilitySnapshot();
2155
+ }
2156
+ return this.visibilityCache;
2157
+ }
2158
+ ensureNamedExpressionValueCache() {
2159
+ if (!this.namedExpressionValueCache) {
2160
+ this.namedExpressionValueCache = this.captureNamedExpressionValueSnapshot();
2161
+ }
2162
+ return this.namedExpressionValueCache;
2163
+ }
2047
2164
  flushPendingBatchOps() {
2048
2165
  if (this.pendingBatchOps.length === 0) {
2049
2166
  return;
@@ -2052,22 +2169,22 @@ export class HeadlessWorkbook {
2052
2169
  const potentialNewCells = this.pendingBatchPotentialNewCells;
2053
2170
  this.pendingBatchOps = [];
2054
2171
  this.pendingBatchPotentialNewCells = 0;
2055
- this.engine.applyOps(ops, {
2056
- captureUndo: true,
2057
- potentialNewCells: potentialNewCells > 0 ? potentialNewCells : undefined,
2058
- });
2172
+ this.engine.applyCellMutationsAt(ops, potentialNewCells > 0 ? potentialNewCells : undefined);
2059
2173
  }
2060
- enqueueDeferredBatchLiteral(sheetName, address, content) {
2174
+ enqueueDeferredBatchLiteral(sheetId, row, col, content) {
2061
2175
  if (this.batchDepth === 0 ||
2062
2176
  !isDeferredBatchLiteralContent(content) ||
2063
2177
  isFormulaContent(content)) {
2064
2178
  return false;
2065
2179
  }
2066
2180
  if (content === null) {
2067
- this.pendingBatchOps.push({ kind: "clearCell", sheetName, address });
2181
+ this.pendingBatchOps.push({ sheetId, mutation: { kind: "clearCell", row, col } });
2068
2182
  return true;
2069
2183
  }
2070
- this.pendingBatchOps.push({ kind: "setCellValue", sheetName, address, value: content });
2184
+ this.pendingBatchOps.push({
2185
+ sheetId,
2186
+ mutation: { kind: "setCellValue", row, col, value: content },
2187
+ });
2071
2188
  this.pendingBatchPotentialNewCells += 1;
2072
2189
  return true;
2073
2190
  }
@@ -2077,19 +2194,19 @@ export class HeadlessWorkbook {
2077
2194
  }
2078
2195
  assertNotDisposed() {
2079
2196
  if (this.disposed) {
2080
- throw new HeadlessOperationError("Workbook has been disposed");
2197
+ throw new WorkPaperOperationError("Workbook has been disposed");
2081
2198
  }
2082
2199
  }
2083
2200
  assertReadable() {
2084
2201
  this.prepareReadableState();
2085
2202
  if (this.evaluationSuspended) {
2086
- throw new HeadlessEvaluationSuspendedError();
2203
+ throw new WorkPaperEvaluationSuspendedError();
2087
2204
  }
2088
2205
  }
2089
2206
  sheetRecord(sheetId) {
2090
2207
  const sheet = this.engine.workbook.getSheetById(sheetId);
2091
2208
  if (!sheet) {
2092
- throw new NoSheetWithIdError(sheetId);
2209
+ throw new WorkPaperNoSheetWithIdError(sheetId);
2093
2210
  }
2094
2211
  return sheet;
2095
2212
  }
@@ -2099,7 +2216,7 @@ export class HeadlessWorkbook {
2099
2216
  requireSheetId(name) {
2100
2217
  const sheetId = this.getSheetId(name);
2101
2218
  if (sheetId === undefined) {
2102
- throw new NoSheetWithNameError(name);
2219
+ throw new WorkPaperNoSheetWithNameError(name);
2103
2220
  }
2104
2221
  return sheetId;
2105
2222
  }
@@ -2138,15 +2255,16 @@ export class HeadlessWorkbook {
2138
2255
  }
2139
2256
  captureVisibilitySnapshot() {
2140
2257
  const snapshot = new Map();
2258
+ const strings = this.engine.strings;
2259
+ const cellStore = this.engine.workbook.cellStore;
2141
2260
  this.listSheetRecords().forEach((sheet) => {
2142
2261
  const cells = new Map();
2143
- sheet.grid.forEachCellEntry((_cellIndex, row, col) => {
2144
- const address = formatAddress(row, col);
2145
- const value = this.engine.getCellValue(sheet.name, address);
2262
+ sheet.grid.forEachCellEntry((cellIndex, row, col) => {
2263
+ const value = cellStore.getValue(cellIndex, (id) => strings.get(id));
2146
2264
  if (value.tag === ValueTag.Empty) {
2147
2265
  return;
2148
2266
  }
2149
- cells.set(address, cloneCellValue(value));
2267
+ cells.set(makeCellKey(sheet.id, row, col), value);
2150
2268
  });
2151
2269
  snapshot.set(sheet.id, {
2152
2270
  sheetId: sheet.id,
@@ -2158,36 +2276,178 @@ export class HeadlessWorkbook {
2158
2276
  return snapshot;
2159
2277
  }
2160
2278
  captureNamedExpressionValueSnapshot() {
2279
+ if (this.namedExpressions.size === 0) {
2280
+ return EMPTY_NAMED_EXPRESSION_VALUES;
2281
+ }
2161
2282
  const snapshot = new Map();
2162
2283
  [...this.namedExpressions.values()].forEach((expression) => {
2163
2284
  snapshot.set(makeNamedExpressionKey(expression.publicName, expression.scope), cloneNamedExpressionValue(this.evaluateNamedExpression(expression)));
2164
2285
  });
2165
2286
  return snapshot;
2166
2287
  }
2167
- computeChanges(beforeVisibility, afterVisibility, beforeNames, afterNames) {
2288
+ computeCellChanges(beforeVisibility, afterVisibility) {
2168
2289
  const cellChanges = [];
2169
2290
  afterVisibility.forEach((afterSheet, sheetId) => {
2170
2291
  const beforeSheet = beforeVisibility.get(sheetId);
2171
- const addresses = new Set([
2292
+ const cellKeys = new Set([
2172
2293
  ...(beforeSheet?.cells.keys() ?? []),
2173
2294
  ...afterSheet.cells.keys(),
2174
2295
  ]);
2175
- [...addresses].toSorted(compareSheetNames).forEach((address) => {
2176
- const beforeValue = beforeSheet?.cells.get(address) ?? emptyValue();
2177
- const afterValue = afterSheet.cells.get(address) ?? emptyValue();
2296
+ [...cellKeys]
2297
+ .toSorted((left, right) => left - right)
2298
+ .forEach((cellKey) => {
2299
+ const beforeValue = beforeSheet?.cells.get(cellKey) ?? emptyValue();
2300
+ const afterValue = afterSheet.cells.get(cellKey) ?? emptyValue();
2178
2301
  if (valuesEqual(beforeValue, afterValue)) {
2179
2302
  return;
2180
2303
  }
2181
- const parsed = parseCellAddress(address, afterSheet.sheetName);
2304
+ const localKey = cellKey - afterSheet.sheetId * VISIBILITY_SHEET_STRIDE;
2305
+ const row = Math.floor(localKey / MAX_COLS);
2306
+ const col = localKey % MAX_COLS;
2307
+ const address = formatAddress(row, col);
2182
2308
  cellChanges.push({
2183
2309
  kind: "cell",
2184
- address: { sheet: sheetId, row: parsed.row, col: parsed.col },
2310
+ address: { sheet: sheetId, row, col },
2185
2311
  sheetName: afterSheet.sheetName,
2186
2312
  a1: address,
2187
- newValue: cloneCellValue(afterValue),
2313
+ newValue: afterValue,
2188
2314
  });
2189
2315
  });
2190
2316
  });
2317
+ return orderWorkPaperCellChanges(cellChanges, this.listSheetRecords());
2318
+ }
2319
+ computeCellChangesFromTrackedEvents(beforeVisibility, events) {
2320
+ if (events.some((event) => event.invalidation === "full" ||
2321
+ event.hasInvalidatedRanges ||
2322
+ event.hasInvalidatedRows ||
2323
+ event.hasInvalidatedColumns)) {
2324
+ return null;
2325
+ }
2326
+ const cellStore = this.engine.workbook.cellStore;
2327
+ const strings = this.engine.strings;
2328
+ const nextVisibility = beforeVisibility;
2329
+ const sheetNames = new Map();
2330
+ const ensureSheetName = (sheetId) => {
2331
+ const cached = sheetNames.get(sheetId);
2332
+ if (cached !== undefined) {
2333
+ return cached;
2334
+ }
2335
+ const sheet = this.engine.workbook.getSheetById(sheetId);
2336
+ if (!sheet) {
2337
+ return null;
2338
+ }
2339
+ sheetNames.set(sheetId, sheet.name);
2340
+ return sheet.name;
2341
+ };
2342
+ const ensureMutableSheet = (sheetId, sheetName) => {
2343
+ const existing = nextVisibility.get(sheetId);
2344
+ if (existing) {
2345
+ return existing;
2346
+ }
2347
+ const created = {
2348
+ sheetId,
2349
+ sheetName,
2350
+ order: this.sheetRecord(sheetId).order,
2351
+ cells: new Map(),
2352
+ };
2353
+ nextVisibility.set(sheetId, created);
2354
+ return created;
2355
+ };
2356
+ const collectDirectChanges = (changedCellIndices, seen) => {
2357
+ const changes = [];
2358
+ let previousSheetId = -1;
2359
+ let previousRow = -1;
2360
+ let previousCol = -1;
2361
+ let isSorted = true;
2362
+ for (let index = 0; index < changedCellIndices.length; index += 1) {
2363
+ const cellIndex = changedCellIndices[index];
2364
+ const sheetId = cellStore.sheetIds[cellIndex];
2365
+ const row = cellStore.rows[cellIndex];
2366
+ const col = cellStore.cols[cellIndex];
2367
+ if (sheetId === undefined || row === undefined || col === undefined) {
2368
+ return null;
2369
+ }
2370
+ if (seen) {
2371
+ const logicalCellKey = makeCellKey(sheetId, row, col);
2372
+ if (seen.has(logicalCellKey)) {
2373
+ continue;
2374
+ }
2375
+ seen.add(logicalCellKey);
2376
+ }
2377
+ const sheetName = ensureSheetName(sheetId);
2378
+ if (!sheetName) {
2379
+ return null;
2380
+ }
2381
+ const cellKey = makeCellKey(sheetId, row, col);
2382
+ const address = formatAddress(row, col);
2383
+ const beforeValue = beforeVisibility.get(sheetId)?.cells.get(cellKey) ?? emptyValue();
2384
+ const afterValue = cellStore.getValue(cellIndex, (id) => strings.get(id));
2385
+ if (valuesEqual(beforeValue, afterValue)) {
2386
+ continue;
2387
+ }
2388
+ if (previousSheetId > sheetId ||
2389
+ (previousSheetId === sheetId &&
2390
+ (previousRow > row || (previousRow === row && previousCol > col)))) {
2391
+ isSorted = false;
2392
+ }
2393
+ previousSheetId = sheetId;
2394
+ previousRow = row;
2395
+ previousCol = col;
2396
+ const sheet = ensureMutableSheet(sheetId, sheetName);
2397
+ if (afterValue.tag === ValueTag.Empty) {
2398
+ sheet.cells.delete(cellKey);
2399
+ }
2400
+ else {
2401
+ sheet.cells.set(cellKey, afterValue);
2402
+ }
2403
+ changes.push({
2404
+ kind: "cell",
2405
+ address: { sheet: sheetId, row, col },
2406
+ sheetName,
2407
+ a1: address,
2408
+ newValue: afterValue,
2409
+ });
2410
+ }
2411
+ return { changes, isSorted };
2412
+ };
2413
+ if (events.length === 1) {
2414
+ const event = events[0];
2415
+ const direct = collectDirectChanges(event.changedCellIndices, new Set());
2416
+ if (direct === null) {
2417
+ return null;
2418
+ }
2419
+ return {
2420
+ changes: direct.isSorted
2421
+ ? direct.changes
2422
+ : orderWorkPaperCellChanges(direct.changes, this.listSheetRecords(), event.explicitChangedCount),
2423
+ nextVisibility,
2424
+ };
2425
+ }
2426
+ const latestCellIndicesByKey = new Map();
2427
+ for (const event of events) {
2428
+ for (let index = 0; index < event.changedCellIndices.length; index += 1) {
2429
+ const cellIndex = event.changedCellIndices[index];
2430
+ const sheetId = cellStore.sheetIds[cellIndex];
2431
+ const row = cellStore.rows[cellIndex];
2432
+ const col = cellStore.cols[cellIndex];
2433
+ if (sheetId === undefined || row === undefined || col === undefined) {
2434
+ return null;
2435
+ }
2436
+ latestCellIndicesByKey.set(makeCellKey(sheetId, row, col), cellIndex);
2437
+ }
2438
+ }
2439
+ const direct = collectDirectChanges(Uint32Array.from(latestCellIndicesByKey.values()), null);
2440
+ if (direct === null) {
2441
+ return null;
2442
+ }
2443
+ return {
2444
+ changes: direct.isSorted
2445
+ ? direct.changes
2446
+ : orderWorkPaperCellChanges(direct.changes, this.listSheetRecords()),
2447
+ nextVisibility,
2448
+ };
2449
+ }
2450
+ computeNamedExpressionChanges(beforeNames, afterNames) {
2191
2451
  const namedExpressionChanges = [];
2192
2452
  afterNames.forEach((afterValue, key) => {
2193
2453
  const beforeValue = beforeNames.get(key);
@@ -2205,40 +2465,73 @@ export class HeadlessWorkbook {
2205
2465
  newValue: cloneNamedExpressionValue(afterValue),
2206
2466
  });
2207
2467
  });
2208
- return [
2209
- ...cellChanges.toSorted(compareHeadlessCellChanges(this.listSheetRecords())),
2210
- ...namedExpressionChanges.toSorted(compareHeadlessNamedExpressionChanges),
2211
- ];
2468
+ return namedExpressionChanges.toSorted(compareWorkPaperNamedExpressionChanges);
2469
+ }
2470
+ computeChangesAfterMutation(beforeVisibility, beforeNames) {
2471
+ const hasNamedExpressions = this.namedExpressions.size > 0;
2472
+ const afterNames = hasNamedExpressions
2473
+ ? this.captureNamedExpressionValueSnapshot()
2474
+ : EMPTY_NAMED_EXPRESSION_VALUES;
2475
+ const fastPath = this.computeCellChangesFromTrackedEvents(beforeVisibility, this.drainTrackedEngineEvents());
2476
+ let cellChanges;
2477
+ if (fastPath) {
2478
+ cellChanges = fastPath.changes;
2479
+ this.visibilityCache = fastPath.nextVisibility;
2480
+ }
2481
+ else {
2482
+ const afterVisibility = this.captureVisibilitySnapshot();
2483
+ cellChanges = this.computeCellChanges(beforeVisibility, afterVisibility);
2484
+ this.visibilityCache = afterVisibility;
2485
+ }
2486
+ this.namedExpressionValueCache = afterNames;
2487
+ return hasNamedExpressions
2488
+ ? [...cellChanges, ...this.computeNamedExpressionChanges(beforeNames, afterNames)]
2489
+ : cellChanges;
2212
2490
  }
2213
2491
  captureChanges(semanticEvent, mutate) {
2214
2492
  this.assertNotDisposed();
2215
2493
  if (semanticEvent !== undefined) {
2216
2494
  this.flushPendingBatchOps();
2217
2495
  }
2218
- if (semanticEvent === undefined && this.shouldSuppressEvents()) {
2496
+ if (this.shouldSuppressEvents()) {
2219
2497
  try {
2220
2498
  mutate();
2221
2499
  }
2222
2500
  catch (error) {
2223
- if (error instanceof Error && HEADLESS_PUBLIC_ERROR_NAMES.has(error.name)) {
2501
+ if (error instanceof Error && WORKPAPER_PUBLIC_ERROR_NAMES.has(error.name)) {
2224
2502
  throw error;
2225
2503
  }
2226
- throw new HeadlessOperationError(this.messageOf(error, "Mutation failed"));
2504
+ throw new WorkPaperOperationError(this.messageOf(error, "Mutation failed"));
2505
+ }
2506
+ if (semanticEvent) {
2507
+ this.queuedEvents.push(semanticEvent);
2227
2508
  }
2228
2509
  return [];
2229
2510
  }
2230
- const beforeVisibility = this.captureVisibilitySnapshot();
2231
- const beforeNames = this.captureNamedExpressionValueSnapshot();
2511
+ const beforeVisibility = this.ensureVisibilityCache();
2512
+ const beforeNames = this.ensureNamedExpressionValueCache();
2513
+ this.drainTrackedEngineEvents();
2232
2514
  try {
2233
2515
  mutate();
2234
2516
  }
2235
2517
  catch (error) {
2236
- if (error instanceof Error && HEADLESS_PUBLIC_ERROR_NAMES.has(error.name)) {
2518
+ if (error instanceof Error && WORKPAPER_PUBLIC_ERROR_NAMES.has(error.name)) {
2237
2519
  throw error;
2238
2520
  }
2239
- throw new HeadlessOperationError(this.messageOf(error, "Mutation failed"));
2240
- }
2241
- const changes = this.computeChanges(beforeVisibility, this.captureVisibilitySnapshot(), beforeNames, this.captureNamedExpressionValueSnapshot());
2521
+ throw new WorkPaperOperationError(this.messageOf(error, "Mutation failed"));
2522
+ }
2523
+ const changes = semanticEvent === undefined
2524
+ ? this.computeChangesAfterMutation(beforeVisibility, beforeNames)
2525
+ : (() => {
2526
+ const afterVisibility = this.captureVisibilitySnapshot();
2527
+ const afterNames = this.captureNamedExpressionValueSnapshot();
2528
+ this.visibilityCache = afterVisibility;
2529
+ this.namedExpressionValueCache = afterNames;
2530
+ return [
2531
+ ...this.computeCellChanges(beforeVisibility, afterVisibility),
2532
+ ...this.computeNamedExpressionChanges(beforeNames, afterNames),
2533
+ ];
2534
+ })();
2242
2535
  if (semanticEvent) {
2243
2536
  const event = withEventChanges(semanticEvent, changes);
2244
2537
  if (this.shouldSuppressEvents()) {
@@ -2325,99 +2618,109 @@ export class HeadlessWorkbook {
2325
2618
  if (typeof raw === "string" && raw.startsWith("=")) {
2326
2619
  nextValue = `=${translateFormulaReferences(raw.slice(1), destination.row - (sourceAnchor.row + rowOffset), destination.col - (sourceAnchor.col + columnOffset))}`;
2327
2620
  }
2328
- this.applyRawContent(this.sheetName(destination.sheet), this.a1(destination), nextValue, destination.sheet);
2621
+ this.applyRawContent(destination, nextValue);
2329
2622
  });
2330
2623
  });
2331
2624
  return;
2332
2625
  }
2333
- const sheetName = this.sheetName(targetLeftCorner.sheet);
2334
- const { ops, potentialNewCells } = buildMatrixMutationPlan({
2626
+ const { refs, potentialNewCells } = buildMatrixMutationPlan({
2335
2627
  target: targetLeftCorner,
2336
- targetSheetName: sheetName,
2337
2628
  content: serialized,
2338
2629
  rewriteFormula: (formula, destination, rowOffset, columnOffset) => this.rewriteFormulaForStorage(translateFormulaReferences(stripLeadingEquals(formula), destination.row - (sourceAnchor.row + rowOffset), destination.col - (sourceAnchor.col + columnOffset)), destination.sheet),
2339
2630
  });
2340
- if (ops.length === 0) {
2631
+ if (refs.length === 0) {
2341
2632
  return;
2342
2633
  }
2343
- this.engine.applyOps(ops, { potentialNewCells });
2634
+ this.engine.applyCellMutationsAtWithOptions(refs, {
2635
+ captureUndo: true,
2636
+ potentialNewCells,
2637
+ source: "local",
2638
+ });
2344
2639
  }
2345
2640
  applyMatrixContents(address, content, options = {}) {
2346
2641
  this.flushPendingBatchOps();
2347
- if (matrixContainsFormulaContent(content)) {
2348
- content.forEach((row, rowOffset) => {
2349
- row.forEach((raw, columnOffset) => {
2350
- if (raw === null && options.skipNulls) {
2351
- return;
2352
- }
2353
- this.applyRawContent(this.sheetName(address.sheet), formatAddress(address.row + rowOffset, address.col + columnOffset), raw, address.sheet);
2354
- });
2355
- });
2356
- return;
2357
- }
2358
- const sheetName = this.sheetName(address.sheet);
2359
- const { ops, potentialNewCells } = buildMatrixMutationPlan({
2642
+ const { leadingRefs, formulaRefs, refs, potentialNewCells, trailingLiteralRefs } = buildMatrixMutationPlan({
2360
2643
  target: address,
2361
- targetSheetName: sheetName,
2362
2644
  content,
2645
+ deferLiteralAddresses: options.deferLiteralAddresses,
2363
2646
  skipNulls: options.skipNulls,
2364
2647
  rewriteFormula: (formula, destination) => this.rewriteFormulaForStorage(stripLeadingEquals(formula), destination.sheet),
2365
2648
  });
2366
- if (ops.length === 0) {
2649
+ if (refs.length === 0) {
2650
+ return;
2651
+ }
2652
+ const applyPlannedRefs = (phaseRefs, applyOptions) => {
2653
+ if (phaseRefs.length === 0) {
2654
+ return;
2655
+ }
2656
+ this.engine.applyCellMutationsAtWithOptions(phaseRefs, applyOptions);
2657
+ };
2658
+ const phaseSource = options.captureUndo === false ? "restore" : "local";
2659
+ if (formulaRefs.length === 0) {
2660
+ applyPlannedRefs(refs, {
2661
+ captureUndo: options.captureUndo,
2662
+ potentialNewCells,
2663
+ source: phaseSource,
2664
+ });
2367
2665
  return;
2368
2666
  }
2369
- this.engine.applyOps(ops, {
2667
+ applyPlannedRefs(leadingRefs, {
2668
+ captureUndo: options.captureUndo,
2669
+ potentialNewCells,
2670
+ source: phaseSource,
2671
+ });
2672
+ applyPlannedRefs(formulaRefs, {
2673
+ captureUndo: options.captureUndo,
2674
+ potentialNewCells,
2675
+ source: phaseSource,
2676
+ });
2677
+ applyPlannedRefs(trailingLiteralRefs, {
2370
2678
  captureUndo: options.captureUndo,
2371
2679
  potentialNewCells,
2680
+ source: phaseSource,
2372
2681
  });
2373
2682
  }
2374
2683
  replaceSheetContentInternal(sheetId, content, options) {
2375
- const sheetName = this.sheetName(sheetId);
2376
- const undoStackStart = options.duringInitialization ? 0 : this.getUndoStack().length;
2377
- const dimensions = this.getSheetDimensions(sheetId);
2378
- if (dimensions.width > 0 && dimensions.height > 0) {
2379
- this.engine.clearRange({
2380
- sheetName,
2381
- startAddress: "A1",
2382
- endAddress: formatAddress(dimensions.height - 1, dimensions.width - 1),
2383
- });
2384
- }
2385
- this.applyMatrixContents({ sheet: sheetId, row: 0, col: 0 }, content, {
2386
- captureUndo: !options.duringInitialization,
2387
- skipNulls: true,
2684
+ replaceWorkPaperSheetContent({
2685
+ sheetId,
2686
+ sheetName: this.sheetName(sheetId),
2687
+ content,
2688
+ duringInitialization: options.duringInitialization,
2689
+ listSpills: () => this.engine.workbook.listSpills(),
2690
+ getSheetDimensions: (nextSheetId) => this.getSheetDimensions(nextSheetId),
2691
+ clearRange: (input) => this.engine.clearRange(input),
2692
+ applyMatrixContents: (address, nextContent, applyOptions) => this.applyMatrixContents(address, nextContent, applyOptions),
2693
+ clearHistoryStacks: () => this.clearHistoryStacks(),
2694
+ getUndoStackLength: () => this.getUndoStack().length,
2695
+ mergeUndoHistory: (undoStackStart) => this.mergeUndoHistory(undoStackStart),
2388
2696
  });
2389
- if (options.duringInitialization) {
2390
- this.clearHistoryStacks();
2391
- return;
2392
- }
2393
- this.mergeUndoHistory(undoStackStart);
2394
2697
  }
2395
- applyRawContent(sheetName, address, content, ownerSheetId) {
2698
+ applyRawContent(address, content) {
2396
2699
  if (content === null) {
2397
- this.engine.clearCell(sheetName, address);
2700
+ this.engine.clearCellAt(address.sheet, address.row, address.col);
2398
2701
  return;
2399
2702
  }
2400
2703
  if (typeof content === "boolean" || typeof content === "number") {
2401
- this.engine.setCellValue(sheetName, address, content);
2704
+ this.engine.setCellValueAt(address.sheet, address.row, address.col, content);
2402
2705
  return;
2403
2706
  }
2404
2707
  if (typeof content === "string" && content.trim().startsWith("=")) {
2405
- this.engine.setCellFormula(sheetName, address, this.rewriteFormulaForStorage(stripLeadingEquals(content), ownerSheetId));
2708
+ this.engine.setCellFormulaAt(address.sheet, address.row, address.col, this.rewriteFormulaForStorage(stripLeadingEquals(content), address.sheet));
2406
2709
  return;
2407
2710
  }
2408
- this.engine.setCellValue(sheetName, address, content);
2711
+ this.engine.setCellValueAt(address.sheet, address.row, address.col, content);
2409
2712
  }
2410
2713
  captureFunctionRegistry() {
2411
2714
  const allowedPluginIds = this.config.functionPlugins && this.config.functionPlugins.length > 0
2412
2715
  ? new Set(this.config.functionPlugins.map((plugin) => plugin.id))
2413
2716
  : undefined;
2414
- HeadlessWorkbook.functionPluginRegistry.forEach((plugin) => {
2717
+ WorkPaper.functionPluginRegistry.forEach((plugin) => {
2415
2718
  if (allowedPluginIds && !allowedPluginIds.has(plugin.id)) {
2416
2719
  return;
2417
2720
  }
2418
2721
  Object.keys(plugin.implementedFunctions).forEach((functionId) => {
2419
2722
  const normalized = functionId.trim().toUpperCase();
2420
- const internalName = `__BILIG_HEADLESS_FN_${this.workbookId}_${normalized}`;
2723
+ const internalName = `__BILIG_WORKPAPER_FN_${this.workbookId}_${normalized}`;
2421
2724
  const implementation = plugin.functions?.[normalized];
2422
2725
  const binding = {
2423
2726
  pluginId: plugin.id,
@@ -2450,7 +2753,7 @@ export class HeadlessWorkbook {
2450
2753
  this.internalFunctionLookup.clear();
2451
2754
  }
2452
2755
  rebuildWithConfig(nextConfig) {
2453
- validateHeadlessConfig(nextConfig);
2756
+ validateWorkPaperConfig(nextConfig);
2454
2757
  const serializedSheets = this.getAllSheetsSerialized();
2455
2758
  Object.entries(serializedSheets).forEach(([sheetName, sheet]) => {
2456
2759
  validateSheetWithinLimits(sheetName, sheet, nextConfig);
@@ -2466,43 +2769,53 @@ export class HeadlessWorkbook {
2466
2769
  : null;
2467
2770
  this.clearFunctionBindings();
2468
2771
  this.namedExpressions.clear();
2469
- this.engine = new SpreadsheetEngine({ workbookName: "Workbook" });
2772
+ this.engine = new SpreadsheetEngine({
2773
+ workbookName: "Workbook",
2774
+ useColumnIndex: this.config.useColumnIndex,
2775
+ trackReplicaVersions: false,
2776
+ });
2777
+ this.attachEngineEventTracking();
2470
2778
  this.config = cloneConfig(nextConfig);
2471
2779
  this.captureFunctionRegistry();
2472
- Object.keys(serializedSheets).forEach((sheetName) => {
2473
- this.engine.createSheet(sheetName);
2474
- });
2475
- serializedNamedExpressions.forEach((expression) => {
2476
- this.upsertNamedExpressionInternal(expression, { duringInitialization: true });
2477
- });
2478
- Object.entries(serializedSheets).forEach(([sheetName, sheet]) => {
2479
- const sheetId = this.requireSheetId(sheetName);
2480
- this.replaceSheetContentInternal(sheetId, sheet, { duringInitialization: true });
2780
+ this.withEngineEventCaptureDisabled(() => {
2781
+ Object.keys(serializedSheets).forEach((sheetName) => {
2782
+ this.engine.createSheet(sheetName);
2783
+ });
2784
+ serializedNamedExpressions.forEach((expression) => {
2785
+ this.upsertNamedExpressionInternal(expression, { duringInitialization: true });
2786
+ });
2787
+ Object.entries(serializedSheets).forEach(([sheetName, sheet]) => {
2788
+ const sheetId = this.requireSheetId(sheetName);
2789
+ if (!tryLoadInitialLiteralSheet(this.engine, sheetId, sheet)) {
2790
+ this.replaceSheetContentInternal(sheetId, sheet, { duringInitialization: true });
2791
+ }
2792
+ });
2481
2793
  });
2482
2794
  this.clearHistoryStacks();
2795
+ this.primeChangeTrackingCaches();
2483
2796
  this.clipboard = clipboard;
2484
2797
  if (suspended) {
2485
- this.suspendedVisibility = this.captureVisibilitySnapshot();
2486
- this.suspendedNamedValues = this.captureNamedExpressionValueSnapshot();
2798
+ this.suspendedVisibility = this.ensureVisibilityCache();
2799
+ this.suspendedNamedValues = this.ensureNamedExpressionValueCache();
2487
2800
  }
2488
2801
  }
2489
2802
  normalizeAxisIntervals(startOrInterval, countOrInterval, restIntervals = []) {
2490
2803
  if (typeof startOrInterval === "number") {
2491
2804
  if (Array.isArray(countOrInterval)) {
2492
- throw new HeadlessArgumentError("Axis interval count must be a number");
2805
+ throw new WorkPaperInvalidArgumentsError("Axis interval count must be a number");
2493
2806
  }
2494
2807
  const resolvedCount = typeof countOrInterval === "number" ? countOrInterval : 1;
2495
2808
  return [[startOrInterval, resolvedCount]];
2496
2809
  }
2497
2810
  if (typeof countOrInterval === "number") {
2498
- throw new HeadlessArgumentError("Axis interval count is only valid with a numeric start");
2811
+ throw new WorkPaperInvalidArgumentsError("Axis interval count is only valid with a numeric start");
2499
2812
  }
2500
2813
  return [startOrInterval, ...(countOrInterval ? [countOrInterval] : []), ...restIntervals].map(([start, count]) => [start, count ?? 1]);
2501
2814
  }
2502
2815
  normalizeAxisSwapMappings(label, startOrMappings, end) {
2503
2816
  if (typeof startOrMappings === "number") {
2504
2817
  if (end === undefined) {
2505
- throw new HeadlessArgumentError(`${label} swap requires two indexes`);
2818
+ throw new WorkPaperInvalidArgumentsError(`${label} swap requires two indexes`);
2506
2819
  }
2507
2820
  return [[startOrMappings, end]];
2508
2821
  }
@@ -2531,6 +2844,9 @@ export class HeadlessWorkbook {
2531
2844
  return collected;
2532
2845
  }
2533
2846
  rewriteFormulaForStorage(formula, ownerSheetId) {
2847
+ if (this.namedExpressions.size === 0 && this.functionAliasLookup.size === 0) {
2848
+ return formula;
2849
+ }
2534
2850
  try {
2535
2851
  const transformed = transformFormulaNode(parseFormula(stripLeadingEquals(formula)), (node) => {
2536
2852
  if (node.kind === "NameRef") {
@@ -2544,10 +2860,13 @@ export class HeadlessWorkbook {
2544
2860
  return serializeFormula(transformed);
2545
2861
  }
2546
2862
  catch (error) {
2547
- throw new HeadlessParseError(this.messageOf(error, "Unable to store formula"));
2863
+ throw new WorkPaperParseError(this.messageOf(error, "Unable to store formula"));
2548
2864
  }
2549
2865
  }
2550
2866
  restorePublicFormula(formula, ownerSheetId) {
2867
+ if (this.namedExpressions.size === 0 && this.functionAliasLookup.size === 0) {
2868
+ return formula;
2869
+ }
2551
2870
  const transformed = transformFormulaNode(parseFormula(formula), (node) => {
2552
2871
  if (node.kind === "NameRef") {
2553
2872
  return this.rewriteNameRefForPublic(node, ownerSheetId);
@@ -2594,7 +2913,7 @@ export class HeadlessWorkbook {
2594
2913
  validateNamedExpression(expressionName, expression, scope) {
2595
2914
  const trimmed = expressionName.trim();
2596
2915
  if (!/^[A-Za-z_][A-Za-z0-9_.]*$/.test(trimmed) || isCellReferenceText(trimmed)) {
2597
- throw new NamedExpressionNameIsInvalidError(expressionName);
2916
+ throw new WorkPaperNamedExpressionNameIsInvalidError(expressionName);
2598
2917
  }
2599
2918
  if (scope !== undefined) {
2600
2919
  this.sheetRecord(scope);
@@ -2603,14 +2922,14 @@ export class HeadlessWorkbook {
2603
2922
  try {
2604
2923
  const parsed = parseFormula(stripLeadingEquals(expression));
2605
2924
  if (formulaHasRelativeReferences(parsed)) {
2606
- throw new NoRelativeAddressesAllowedError();
2925
+ throw new WorkPaperNoRelativeAddressesAllowedError();
2607
2926
  }
2608
2927
  }
2609
2928
  catch (error) {
2610
- if (error instanceof NoRelativeAddressesAllowedError) {
2929
+ if (error instanceof WorkPaperNoRelativeAddressesAllowedError) {
2611
2930
  throw error;
2612
2931
  }
2613
- throw new UnableToParseError({
2932
+ throw new WorkPaperUnableToParseError({
2614
2933
  expressionName,
2615
2934
  reason: this.messageOf(error, `Invalid named expression formula for '${expressionName}'`),
2616
2935
  });
@@ -2652,7 +2971,7 @@ export class HeadlessWorkbook {
2652
2971
  if (direct) {
2653
2972
  return direct;
2654
2973
  }
2655
- throw new NamedExpressionDoesNotExistError(name);
2974
+ throw new WorkPaperNamedExpressionDoesNotExistError(name);
2656
2975
  }
2657
2976
  evaluateNamedExpression(expression) {
2658
2977
  const raw = expression.expression;
@@ -2734,23 +3053,12 @@ function cloneNamedExpressionValue(value) {
2734
3053
  }
2735
3054
  return value.map((row) => row.map((cell) => cloneCellValue(cell)));
2736
3055
  }
2737
- function compareHeadlessNamedExpressionChanges(left, right) {
3056
+ function compareWorkPaperNamedExpressionChanges(left, right) {
2738
3057
  if (left.kind !== "named-expression" || right.kind !== "named-expression") {
2739
3058
  return 0;
2740
3059
  }
2741
3060
  return (left.scope ?? -1) - (right.scope ?? -1) || left.name.localeCompare(right.name);
2742
3061
  }
2743
- function compareHeadlessCellChanges(sheets) {
2744
- const orderBySheet = new Map(sheets.map((sheet) => [sheet.id, sheet.order]));
2745
- return (left, right) => {
2746
- if (left.kind !== "cell" || right.kind !== "cell") {
2747
- return 0;
2748
- }
2749
- return ((orderBySheet.get(left.address.sheet) ?? 0) - (orderBySheet.get(right.address.sheet) ?? 0) ||
2750
- left.address.row - right.address.row ||
2751
- left.address.col - right.address.col);
2752
- };
2753
- }
2754
3062
  function sourceRangeRef(sheetName, range) {
2755
3063
  return {
2756
3064
  sheetName,
@@ -2765,4 +3073,4 @@ function sumNumbers(values) {
2765
3073
  }
2766
3074
  return filtered.reduce((sum, value) => sum + value, 0);
2767
3075
  }
2768
- //# sourceMappingURL=headless-workbook.js.map
3076
+ //# sourceMappingURL=work-paper-runtime.js.map