From b58978f437d6a5556c11a1eb0bc3f974494c80df Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Wed, 11 Jun 2025 19:33:20 -0700 Subject: [PATCH 1/2] feat: add onCellsEvaluated event emitter --- base/src/lib.rs | 8 +- base/src/model.rs | 14 ++++ base/src/user_model/common.rs | 77 +++++++++++------- base/src/user_model/history.rs | 8 ++ base/src/user_model/mod.rs | 11 +-- base/src/user_model/ui.rs | 7 ++ bindings/wasm/README.pkg.md | 22 ++++++ bindings/wasm/src/lib.rs | 48 ++++++++++-- bindings/wasm/tests/test.mjs | 137 +++++++++++++++++++++++++++++++++ bindings/wasm/types.ts | 8 +- 10 files changed, 297 insertions(+), 43 deletions(-) diff --git a/base/src/lib.rs b/base/src/lib.rs index a62006228..1ccc3abef 100644 --- a/base/src/lib.rs +++ b/base/src/lib.rs @@ -59,4 +59,10 @@ pub mod mock_time; pub use model::get_milliseconds_since_epoch; pub use model::Model; -pub use user_model::{BorderArea, ClipboardData, Diff, EventEmitter, Subscription, UserModel}; +pub use user_model::{ + common::{BorderArea, ClipboardData, ModelEvent}, + event::{EventEmitter, Subscription}, + history::Diff, + ui::SelectedView, + UserModel, +}; diff --git a/base/src/model.rs b/base/src/model.rs index f2879cb3a..88aad4655 100644 --- a/base/src/model.rs +++ b/base/src/model.rs @@ -1797,6 +1797,20 @@ impl Model { } } + /// Returns a list of all cells that have been changed or are being evaluated + pub fn get_changed_cells(&self) -> Vec { + self.cells + .keys() + .map( + |&(sheet, row, column)| crate::user_model::history::CellReference { + sheet, + row, + column, + }, + ) + .collect() + } + /// Removes the content of the cell but leaves the style. /// /// See also: diff --git a/base/src/user_model/common.rs b/base/src/user_model/common.rs index 8c66ff1fd..1100bf41f 100644 --- a/base/src/user_model/common.rs +++ b/base/src/user_model/common.rs @@ -6,7 +6,7 @@ use csv::{ReaderBuilder, WriterBuilder}; use serde::{Deserialize, Serialize}; use crate::{ - constants::{self, LAST_COLUMN, LAST_ROW}, + constants::{COLUMN_WIDTH_FACTOR, LAST_COLUMN, LAST_ROW}, expressions::{ types::{Area, CellReferenceIndex}, utils::{is_valid_column_number, is_valid_row}, @@ -16,15 +16,25 @@ use crate::{ Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, SheetState, Style, VerticalAlignment, }, + user_model::{ + event::{EventEmitter, Subscription}, + history::{ + CellReference, ColumnData, Diff, DiffList, DiffType, History, QueueDiffs, RowData, + }, + }, utils::is_valid_hex_color, }; -use crate::user_model::history::{ - ColumnData, Diff, DiffList, DiffType, History, QueueDiffs, RowData, -}; - use super::border_utils::is_max_border; -use super::event::{EventEmitter, Subscription}; + +/// Events that the `UserModel` can emit. +pub enum ModelEvent { + /// A diff that can be undone/redone. + Diff(Diff), + /// An informational event that cells have been evaluated. + CellsEvaluated(Vec), +} + /// Data for the clipboard pub type ClipboardData = HashMap>; @@ -224,7 +234,7 @@ pub struct UserModel { history: History, send_queue: Vec, pause_evaluation: bool, - event_emitter: EventEmitter, + event_emitter: EventEmitter, } impl Debug for UserModel { @@ -359,9 +369,9 @@ impl UserModel { /// Subscribes to diff events. /// Returns a Subscription handle that automatically unsubscribes when dropped. #[cfg(any(target_arch = "wasm32", feature = "single_threaded"))] - pub fn subscribe(&self, listener: F) -> Subscription + pub fn subscribe(&self, listener: F) -> Subscription where - F: Fn(&Diff) + 'static, + F: Fn(&ModelEvent) + 'static, { self.event_emitter.subscribe(listener) } @@ -369,9 +379,9 @@ impl UserModel { /// Subscribes to diff events. /// Returns a Subscription handle that automatically unsubscribes when dropped. #[cfg(not(any(target_arch = "wasm32", feature = "single_threaded")))] - pub fn subscribe(&self, listener: F) -> Subscription + pub fn subscribe(&self, listener: F) -> Subscription where - F: Fn(&Diff) + Send + Sync + 'static, + F: Fn(&ModelEvent) + Send + Sync + 'static, { self.event_emitter.subscribe(listener) } @@ -382,7 +392,22 @@ impl UserModel { /// * [Model::evaluate] /// * [UserModel::pause_evaluation] pub fn evaluate(&mut self) { - self.model.evaluate() + // Perform evaluation + self.model.evaluate(); + + // Get the list of cells that were just evaluated + let evaluated_cells = self.model.get_changed_cells(); + + // Emit cells evaluated event if there are any cells that were evaluated + if !evaluated_cells.is_empty() { + self.event_emitter + .emit(&ModelEvent::CellsEvaluated(evaluated_cells)); + } + } + + /// Returns a list of all cells that have been changed or are being evaluated + pub fn get_changed_cells(&self) -> Vec { + self.model.get_changed_cells() } /// Returns the list of pending diffs and removes them from the queue @@ -729,7 +754,7 @@ impl UserModel { sheet, row, column, - old_style: Box::new(None), + old_style: Box::new(Some(old_style)), }); } } @@ -759,7 +784,7 @@ impl UserModel { sheet, row: row.r, column, - old_style: Box::new(None), + old_style: Box::new(Some(old_style)), }); } } @@ -811,7 +836,7 @@ impl UserModel { sheet, row, column, - old_style: Box::new(None), + old_style: Box::new(Some(old_style)), }); } } @@ -866,7 +891,7 @@ impl UserModel { sheet, row, column, - old_style: Box::new(None), + old_style: Box::new(Some(old_style)), }); } } @@ -1705,13 +1730,13 @@ impl UserModel { // remain in the copied area let source = &CellReferenceIndex { sheet, - column: *source_column, row: *source_row, + column: *source_column, }; let target = &CellReferenceIndex { sheet, - column: target_column, row: target_row, + column: target_column, }; let new_value = if is_cut { self.model @@ -1899,7 +1924,7 @@ impl UserModel { pub(crate) fn push_diff_list(&mut self, diff_list: DiffList) { // Emit events for each diff before storing them for diff in &diff_list { - self.event_emitter.emit(diff); + self.event_emitter.emit(&ModelEvent::Diff(diff.clone())); } self.send_queue.push(QueueDiffs { @@ -1918,7 +1943,7 @@ impl UserModel { fn apply_undo_diff_list(&mut self, diff_list: &DiffList) -> Result<(), String> { let mut needs_evaluation = false; for diff in diff_list.iter().rev() { - self.event_emitter.emit(diff); + self.event_emitter.emit(&ModelEvent::Diff(diff.clone())); match diff { Diff::SetCellValue { sheet, @@ -2034,7 +2059,7 @@ impl UserModel { } // makes sure that the width and style is correct if let Some(col) = &old_data.column { - let width = col.width * constants::COLUMN_WIDTH_FACTOR; + let width = col.width * COLUMN_WIDTH_FACTOR; let style = col.style; worksheet.set_column_width_and_style(*column, width, style)?; } @@ -2186,13 +2211,9 @@ impl UserModel { Diff::DeleteRowStyle { sheet, row, - old_value, + old_value: _, } => { - if let Some(s) = old_value.as_ref() { - self.model.set_row_style(*sheet, *row, s)?; - } else { - self.model.delete_row_style(*sheet, *row)?; - } + self.model.delete_row_style(*sheet, *row)?; } } } @@ -2206,7 +2227,7 @@ impl UserModel { fn apply_diff_list(&mut self, diff_list: &DiffList) -> Result<(), String> { let mut needs_evaluation = false; for diff in diff_list { - self.event_emitter.emit(diff); + self.event_emitter.emit(&ModelEvent::Diff(diff.clone())); match diff { Diff::SetCellValue { sheet, diff --git a/base/src/user_model/history.rs b/base/src/user_model/history.rs index 4befeefba..a5b6cab94 100644 --- a/base/src/user_model/history.rs +++ b/base/src/user_model/history.rs @@ -17,6 +17,14 @@ pub struct ColumnData { pub data: HashMap, } +#[allow(missing_docs)] +#[derive(Clone, Encode, Decode, Serialize)] +pub struct CellReference { + pub sheet: u32, + pub row: i32, + pub column: i32, +} + #[allow(missing_docs)] #[derive(Clone, Encode, Decode, Serialize)] pub enum Diff { diff --git a/base/src/user_model/mod.rs b/base/src/user_model/mod.rs index 99d0cb7b9..7280db88e 100644 --- a/base/src/user_model/mod.rs +++ b/base/src/user_model/mod.rs @@ -2,17 +2,14 @@ mod border; mod border_utils; -mod common; -mod event; -mod history; -mod ui; +pub mod common; +pub mod event; +pub mod history; +pub mod ui; pub use common::UserModel; -pub use event::{EventEmitter, Subscription}; -pub use history::Diff; #[cfg(test)] pub use ui::SelectedView; pub use common::BorderArea; -pub use common::ClipboardData; diff --git a/base/src/user_model/ui.rs b/base/src/user_model/ui.rs index b217b1b4e..fea424427 100644 --- a/base/src/user_model/ui.rs +++ b/base/src/user_model/ui.rs @@ -8,12 +8,19 @@ use super::common::UserModel; #[derive(Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] +/// Represents the user's current view of the worksheet, including selection and scroll position. pub struct SelectedView { + /// The index of the selected sheet. pub sheet: u32, + /// The selected row. pub row: i32, + /// The selected column. pub column: i32, + /// The selected range, as `[start_row, start_column, end_row, end_column]`. pub range: [i32; 4], + /// The first row visible in the viewport. pub top_row: i32, + /// The first column visible in the viewport. pub left_column: i32, } diff --git a/bindings/wasm/README.pkg.md b/bindings/wasm/README.pkg.md index 79378b78d..7e2388058 100644 --- a/bindings/wasm/README.pkg.md +++ b/bindings/wasm/README.pkg.md @@ -48,3 +48,25 @@ model.onDiffs(() => { model.setUserInput(0, 1, 1, "=1+1"); ``` + +To listen to cells that change during evaluation (formulas that recalculate): + +```TypeScript +import init, { Model } from "@ironcalc/wasm"; + +await init(); + +const model = new Model("Sheet1", "en", "UTC"); + +model.onCellsEvaluated((cellReferences) => { + // cellReferences is an array of {sheet, row, column} objects + // that represent cells that were recalculated during evaluation + cellReferences.forEach(cell => { + console.log(`Cell ${cell.sheet}:${cell.row}:${cell.column} was evaluated`); + }); +}); + +// Setting a formula will trigger evaluation +model.setUserInput(0, 1, 1, "=SUM(A2:A5)"); +model.setUserInput(0, 2, 1, "10"); // This will trigger re-evaluation of A1 +``` diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 71ce9cfe9..0a8c0c2aa 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -88,13 +88,49 @@ impl Model { #[wasm_bindgen(js_name = "onDiffs")] pub fn on_diffs(&mut self, callback: Function) -> Function { - let subscription = self.model.subscribe(move |diff| { - match serde_wasm_bindgen::to_value(diff) { - Ok(js_diff) => { - let _ = callback.call1(&JsValue::NULL, &js_diff); + let subscription = self.model.subscribe(move |event| { + if let ironcalc_base::ModelEvent::Diff(diff) = event { + match serde_wasm_bindgen::to_value(diff) { + Ok(js_diff) => { + let _ = callback.call1(&JsValue::NULL, &js_diff); + } + Err(_e) => { + // Silent skip: if serialization fails, we skip this diff event + } } - Err(_e) => { - // Silent skip: if serialization fails, we skip this diff event + } + }); + + // Store subscription in an Rc> so it can be moved into the closure + let subscription_rc = Rc::new(RefCell::new(Some(subscription))); + let subscription_clone = subscription_rc.clone(); + + // Create the unsubscribe function + let unsubscribe_fn = wasm_bindgen::closure::Closure::wrap(Box::new(move || { + if let Ok(mut sub) = subscription_clone.try_borrow_mut() { + if let Some(subscription) = sub.take() { + subscription.unsubscribe(); + } + } + }) as Box); + + let js_function = unsubscribe_fn.as_ref().unchecked_ref::().clone(); + unsubscribe_fn.forget(); // Prevent the closure from being dropped + + js_function + } + + #[wasm_bindgen(js_name = "onCellsEvaluated")] + pub fn on_cells_evaluated(&mut self, callback: Function) -> Function { + let subscription = self.model.subscribe(move |event| { + if let ironcalc_base::ModelEvent::CellsEvaluated(cells) = event { + match serde_wasm_bindgen::to_value(cells) { + Ok(js_cells) => { + let _ = callback.call1(&JsValue::NULL, &js_cells); + } + Err(_e) => { + // Silent skip: if serialization fails, we skip this event + } } } }); diff --git a/bindings/wasm/tests/test.mjs b/bindings/wasm/tests/test.mjs index f40190742..893d92521 100644 --- a/bindings/wasm/tests/test.mjs +++ b/bindings/wasm/tests/test.mjs @@ -5,6 +5,13 @@ import { setTimeout } from 'node:timers/promises'; const DEFAULT_ROW_HEIGHT = 28; +// Helper to sort cells for consistent comparison +const sortCells = (cells) => cells.sort((a, b) => { + if (a.sheet !== b.sheet) return a.sheet - b.sheet; + if (a.row !== b.row) return a.row - b.row; + return a.column - b.column; +}); + test('Frozen rows and columns', () => { let model = new Model('Workbook1', 'en', 'UTC'); assert.strictEqual(model.getFrozenRowsCount(0), 0); @@ -512,4 +519,134 @@ test('onDiffs returns unregister function that works correctly', async () => { // Call the second unregister too unregister2(); +}); + +test('onCellsEvaluated tracks formula evaluation', async () => { + const model = new Model('Workbook1', 'en', 'UTC'); + const evaluatedCells = []; + + model.onCellsEvaluated(cells => { + evaluatedCells.push(...cells); + }); + + // Set up a formula and its dependencies + model.setUserInput(0, 1, 1, '=A2+A3'); // A1 = A2 + A3 + model.setUserInput(0, 2, 1, '10'); // A2 = 10 + model.setUserInput(0, 3, 1, '20'); // A3 = 20 + model.evaluate(); + + // Update a dependency to trigger re-evaluation + model.setUserInput(0, 2, 1, '15'); // Change A2 from 10 to 15 + model.evaluate(); + + await setTimeout(0); + + const expectedCells = [ + // First evaluation of A1 + { sheet: 0, row: 1, column: 1 }, + // Re-evaluation of A1 after dependency changed + { sheet: 0, row: 1, column: 1 }, + ]; + + assert.strictEqual(evaluatedCells.length, expectedCells.length, `Should have exactly ${expectedCells.length} cell evaluation events`); + assert.deepStrictEqual(sortCells(evaluatedCells), sortCells(expectedCells), 'The evaluated cells should match the expected cells'); + + // Verify the formula calculated correctly + const result = model.getFormattedCellValue(0, 1, 1); + assert.strictEqual(result, '35', 'Formula should calculate 15 + 20 = 35'); +}); + +test('onCellsEvaluated tracks complex formula dependencies', async () => { + const model = new Model('Workbook1', 'en', 'UTC'); + const evaluatedCells = []; + + model.onCellsEvaluated(cells => { + evaluatedCells.push(...cells); + }); + + // Set up a chain of formulas: A1 -> B1 -> C1 + model.setUserInput(0, 1, 1, '10'); // A1 = 10 (base value) + model.setUserInput(0, 1, 2, '=A1*2'); // B1 = A1 * 2 + model.setUserInput(0, 1, 3, '=B1+5'); // C1 = B1 + 5 + model.evaluate(); + + // Update the root value to trigger cascade re-evaluation + model.setUserInput(0, 1, 1, '5'); // Change A1 from 10 to 5 + model.evaluate(); + + await setTimeout(0); + + const expectedCells = [ + // Initial evaluation + { sheet: 0, row: 1, column: 2 }, // B1 + { sheet: 0, row: 1, column: 3 }, // C1 + // Re-evaluation after dependency change + { sheet: 0, row: 1, column: 2 }, // B1 + { sheet: 0, row: 1, column: 3 }, // C1 + ]; + + assert.strictEqual(evaluatedCells.length, expectedCells.length, `Should have exactly ${expectedCells.length} cell evaluation events`); + assert.deepStrictEqual(sortCells(evaluatedCells), sortCells(expectedCells), 'The evaluated cells should match the expected cells'); + + // Verify the formulas calculated correctly + assert.strictEqual(model.getFormattedCellValue(0, 1, 2), '10', 'B1 should be 5 * 2 = 10'); + assert.strictEqual(model.getFormattedCellValue(0, 1, 3), '15', 'C1 should be 10 + 5 = 15'); +}); + +test('onCellsEvaluated only fires after evaluate', async () => { + const model = new Model('Workbook1', 'en', 'UTC'); + const evaluatedCells = []; + + model.onCellsEvaluated(cells => { + evaluatedCells.push(...cells); + }); + + // Set a formula + model.setUserInput(0, 1, 1, '=1+1'); + + // No cells should be evaluated yet because model.evaluate() has not been called + assert.strictEqual(evaluatedCells.length, 0, 'evaluatedCells should be empty before evaluate()'); + + // Now, trigger the evaluation + model.evaluate(); + await setTimeout(0); + + // Now, the cell should be evaluated + const expectedCells = [ + { sheet: 0, row: 1, column: 1 }, + ]; + + assert.deepStrictEqual(sortCells(evaluatedCells), sortCells(expectedCells), 'evaluatedCells should contain the evaluated cell after evaluate()'); +}); + +test('onCellsEvaluated returns unsubscribe function', async () => { + const model = new Model('Workbook1', 'en', 'UTC'); + const evaluatedCells = []; + + const unsubscribe = model.onCellsEvaluated(cells => { + evaluatedCells.push(...cells); + }); + + assert.strictEqual(typeof unsubscribe, 'function', 'onCellsEvaluated should return a function'); + + // Set up a formula + model.setUserInput(0, 1, 1, '=A2*2'); + model.setUserInput(0, 2, 1, '5'); + + model.evaluate(); + await setTimeout(0); + + assert.ok(evaluatedCells.length > 0, 'Should have tracked evaluated cells'); + + // Unsubscribe + unsubscribe(); + evaluatedCells.length = 0; + + // Update to trigger re-evaluation + model.setUserInput(0, 2, 1, '10'); + + model.evaluate(); + await setTimeout(0); + + assert.strictEqual(evaluatedCells.length, 0, 'Should not track cells after unsubscribing'); }); \ No newline at end of file diff --git a/bindings/wasm/types.ts b/bindings/wasm/types.ts index b5cef2590..170050e91 100644 --- a/bindings/wasm/types.ts +++ b/bindings/wasm/types.ts @@ -235,6 +235,12 @@ export interface DefinedName { formula: string; } +export interface CellReference { + sheet: number; + row: number; + column: number; +} + export interface Cell { type: string; // e.g., "NumberCell", "SharedString", "BooleanCell" v?: number | boolean | string; // value, if applicable @@ -504,4 +510,4 @@ export type Diff = | { SetShowGridLines: SetShowGridLinesDiff } | { CreateDefinedName: CreateDefinedNameDiff } | { DeleteDefinedName: DeleteDefinedNameDiff } - | { UpdateDefinedName: UpdateDefinedNameDiff }; \ No newline at end of file + | { UpdateDefinedName: UpdateDefinedNameDiff }; From 991465a429606c4aa18bb3332fbb16e7d44ce7e9 Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Fri, 13 Jun 2025 01:20:15 -0700 Subject: [PATCH 2/2] refactor: push diffs array instead of diff --- base/src/user_model/common.rs | 18 ++++++++---------- bindings/wasm/src/lib.rs | 8 ++++---- bindings/wasm/tests/test.mjs | 24 ++++++++++++------------ 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/base/src/user_model/common.rs b/base/src/user_model/common.rs index 1100bf41f..5ceecc0bd 100644 --- a/base/src/user_model/common.rs +++ b/base/src/user_model/common.rs @@ -29,8 +29,8 @@ use super::border_utils::is_max_border; /// Events that the `UserModel` can emit. pub enum ModelEvent { - /// A diff that can be undone/redone. - Diff(Diff), + /// A list of diffs (a single user action) that can be undone/redone. + Diffs(DiffList), /// An informational event that cells have been evaluated. CellsEvaluated(Vec), } @@ -1922,15 +1922,12 @@ impl UserModel { // **** Private methods ****** // pub(crate) fn push_diff_list(&mut self, diff_list: DiffList) { - // Emit events for each diff before storing them - for diff in &diff_list { - self.event_emitter.emit(&ModelEvent::Diff(diff.clone())); - } - self.send_queue.push(QueueDiffs { r#type: DiffType::Redo, list: diff_list.clone(), }); + self.event_emitter + .emit(&ModelEvent::Diffs(diff_list.clone())); self.history.push(diff_list); } @@ -1943,7 +1940,6 @@ impl UserModel { fn apply_undo_diff_list(&mut self, diff_list: &DiffList) -> Result<(), String> { let mut needs_evaluation = false; for diff in diff_list.iter().rev() { - self.event_emitter.emit(&ModelEvent::Diff(diff.clone())); match diff { Diff::SetCellValue { sheet, @@ -2217,6 +2213,8 @@ impl UserModel { } } } + self.event_emitter + .emit(&ModelEvent::Diffs(diff_list.clone())); if needs_evaluation { self.evaluate_if_not_paused(); } @@ -2227,7 +2225,6 @@ impl UserModel { fn apply_diff_list(&mut self, diff_list: &DiffList) -> Result<(), String> { let mut needs_evaluation = false; for diff in diff_list { - self.event_emitter.emit(&ModelEvent::Diff(diff.clone())); match diff { Diff::SetCellValue { sheet, @@ -2419,7 +2416,8 @@ impl UserModel { } } } - + self.event_emitter + .emit(&ModelEvent::Diffs(diff_list.clone())); if needs_evaluation { self.evaluate_if_not_paused(); } diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 0a8c0c2aa..c766fc0bd 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -89,10 +89,10 @@ impl Model { #[wasm_bindgen(js_name = "onDiffs")] pub fn on_diffs(&mut self, callback: Function) -> Function { let subscription = self.model.subscribe(move |event| { - if let ironcalc_base::ModelEvent::Diff(diff) = event { - match serde_wasm_bindgen::to_value(diff) { - Ok(js_diff) => { - let _ = callback.call1(&JsValue::NULL, &js_diff); + if let ironcalc_base::ModelEvent::Diffs(diffs) = event { + match serde_wasm_bindgen::to_value(diffs) { + Ok(js_diffs) => { + let _ = callback.call1(&JsValue::NULL, &js_diffs); } Err(_e) => { // Silent skip: if serialization fails, we skip this diff event diff --git a/bindings/wasm/tests/test.mjs b/bindings/wasm/tests/test.mjs index 893d92521..a387d4bb4 100644 --- a/bindings/wasm/tests/test.mjs +++ b/bindings/wasm/tests/test.mjs @@ -143,8 +143,8 @@ test('onDiffs', async () => { const model = new Model('Workbook1', 'en', 'UTC'); const events = []; - model.onDiffs(diff => { - events.push(diff); + model.onDiffs(diffs => { + events.push(...diffs); }); model.setUserInput(0, 1, 1, 'test'); @@ -176,8 +176,8 @@ test('onDiffs emits correct diff types for various operations', async () => { const model = new Model('Workbook1', 'en', 'UTC'); const events = []; - model.onDiffs(diff => { - events.push(diff); + model.onDiffs(diffs => { + events.push(...diffs); }); // Test various operations that should emit different diff types @@ -274,8 +274,8 @@ test('onDiffs emits full diff objects for undo/redo operations', async () => { const model = new Model('Workbook1', 'en', 'UTC'); const events = []; - model.onDiffs(diff => { - events.push(diff); + model.onDiffs(diffs => { + events.push(...diffs); }); // Perform initial operations @@ -366,8 +366,8 @@ test('onDiffs handles multiple subscribers and provides full diff objects', asyn const model = new Model('Workbook1', 'en', 'UTC'); const events = []; - model.onDiffs(diff => { - events.push(diff); + model.onDiffs(diffs => { + events.push(...diffs); }); // Perform complex operations that generate multiple diffs @@ -484,12 +484,12 @@ test('onDiffs returns unregister function that works correctly', async () => { const events2 = []; // Register two listeners - const unregister1 = model.onDiffs(diff => { - events1.push(diff); + const unregister1 = model.onDiffs(diffs => { + events1.push(...diffs); }); - const unregister2 = model.onDiffs(diff => { - events2.push(diff); + const unregister2 = model.onDiffs(diffs => { + events2.push(...diffs); }); // Both should be functions