fix(api): fix api FFormula.calculationResultApplied and FFormula.onCalculationResultApplied (#6522)

This commit is contained in:
wpxp123456
2026-01-27 19:23:27 +08:00
committed by GitHub
parent 975cdff46b
commit 39c9244cbf
8 changed files with 210 additions and 141 deletions
+40 -116
View File
@@ -15,10 +15,48 @@
*/
import type { ICommandInfo, IDisposable, IUnitRange } from '@univerjs/core';
import type { FormulaExecutedStateType, IExecutionInProgressParams, IExprTreeNode, IFormulaDependencyTreeFullJson, IFormulaDependencyTreeJson, IFormulaDependentsAndInRangeResults, IFormulaExecuteResultMap, IFormulaStringMap, ISequenceNode, ISetCellFormulaDependencyCalculationResultMutation, ISetFormulaCalculationNotificationMutation, ISetFormulaCalculationResultMutation, ISetFormulaCalculationStartMutation, ISetFormulaDependencyCalculationResultMutation, ISetFormulaStringBatchCalculationResultMutation, ISetQueryFormulaDependencyAllResultMutation } from '@univerjs/engine-formula';
import type {
FormulaExecutedStateType,
IExecutionInProgressParams,
IExprTreeNode,
IFormulaDependencyTreeFullJson,
IFormulaDependencyTreeJson,
IFormulaDependentsAndInRangeResults,
IFormulaExecuteResultMap,
IFormulaStringMap,
ISequenceNode,
ISetCellFormulaDependencyCalculationResultMutation,
ISetFormulaCalculationNotificationMutation,
ISetFormulaCalculationStartMutation,
ISetFormulaDependencyCalculationResultMutation,
ISetFormulaStringBatchCalculationResultMutation,
ISetQueryFormulaDependencyAllResultMutation,
} from '@univerjs/engine-formula';
import { ICommandService, IConfigService, Inject, Injector } from '@univerjs/core';
import { FBase } from '@univerjs/core/facade';
import { ENGINE_FORMULA_CYCLE_REFERENCE_COUNT, ENGINE_FORMULA_RETURN_DEPENDENCY_TREE, GlobalComputingStatusService, IDefinedNamesService, IFunctionService, ISuperTableService, LexerTreeBuilder, SetCellFormulaDependencyCalculationMutation, SetCellFormulaDependencyCalculationResultMutation, SetFormulaCalculationNotificationMutation, SetFormulaCalculationResultMutation, SetFormulaCalculationStartMutation, SetFormulaCalculationStopMutation, SetFormulaDependencyCalculationMutation, SetFormulaDependencyCalculationResultMutation, SetFormulaStringBatchCalculationMutation, SetFormulaStringBatchCalculationResultMutation, SetQueryFormulaDependencyAllMutation, SetQueryFormulaDependencyAllResultMutation, SetQueryFormulaDependencyMutation, SetQueryFormulaDependencyResultMutation, SetTriggerFormulaCalculationStartMutation } from '@univerjs/engine-formula';
import {
ENGINE_FORMULA_CYCLE_REFERENCE_COUNT,
ENGINE_FORMULA_RETURN_DEPENDENCY_TREE,
GlobalComputingStatusService,
IDefinedNamesService,
IFunctionService,
ISuperTableService,
LexerTreeBuilder,
SetCellFormulaDependencyCalculationMutation,
SetCellFormulaDependencyCalculationResultMutation,
SetFormulaCalculationNotificationMutation,
SetFormulaCalculationStartMutation,
SetFormulaCalculationStopMutation,
SetFormulaDependencyCalculationMutation,
SetFormulaDependencyCalculationResultMutation,
SetFormulaStringBatchCalculationMutation,
SetFormulaStringBatchCalculationResultMutation,
SetQueryFormulaDependencyAllMutation,
SetQueryFormulaDependencyAllResultMutation,
SetQueryFormulaDependencyMutation,
SetQueryFormulaDependencyResultMutation,
SetTriggerFormulaCalculationStartMutation,
} from '@univerjs/engine-formula';
import { filter, firstValueFrom, map, race, timer } from 'rxjs';
/**
@@ -240,120 +278,6 @@ export class FFormula extends FBase {
this._configService.setConfig(ENGINE_FORMULA_CYCLE_REFERENCE_COUNT, maxIteration);
}
/**
* Listens for the moment when formula-calculation results are applied.
*
* This event fires after the engine completes a calculation cycle and
* dispatches a `SetFormulaCalculationResultMutation`.
* The callback is invoked during an idle frame to avoid blocking UI updates.
*
* @param {Function} callback - A function called with the calculation result payload
* once the result-application mutation is emitted.
* @returns {IDisposable} A disposable used to unsubscribe from the event.
*
* @example
* ```ts
* const formulaEngine = univerAPI.getFormula();
*
* const dispose = formulaEngine.calculationResultApplied((result) => {
* console.log('Calculation results applied:', result);
* });
*
* // Later…
* dispose.dispose();
* ```
*/
calculationResultApplied(callback: (result: ISetFormulaCalculationResultMutation) => void): IDisposable {
return this._commandService.onCommandExecuted((command: ICommandInfo) => {
if (command.id !== SetFormulaCalculationResultMutation.id) {
return;
}
const params = command.params as ISetFormulaCalculationResultMutation;
if (params !== undefined) {
requestIdleCallback(() => {
callback(params);
});
}
});
}
/**
* Waits for formula-calculation results to be applied.
*
* This method resolves under three conditions:
* 1. A real calculation runs and the engine emits a "calculation started" signal,
* followed by a "calculation result applied" signal.
* 2. No calculation actually starts within 500 ms — the method assumes there is
* nothing to wait for and resolves automatically.
* 3. A global 30 s timeout triggers, in which case the promise rejects.
*
* The API internally listens to both “calculation in progress” events and
* “calculation result applied” events, ensuring it behaves correctly whether
* formulas are recalculated or skipped due to cache/state.
*
* @returns {Promise<void>} A promise that resolves when calculation results are applied
* or when no calculation occurs within the start-detection window.
*
* @example
* ```ts
* const formulaEngine = univerAPI.getFormula();
*
* // Wait for formula updates to apply before reading values.
* await formulaEngine.onCalculationResultApplied();
*
* const value = sheet.getRange("C24").getValue();
* console.log("Updated value:", value);
* ```
*/
onCalculationResultApplied(): Promise<void> {
return new Promise((resolve, reject) => {
let started = false;
let finished = false;
// Global timeout: reject if the whole calculation hangs
const mainTimer = setTimeout(() => {
cleanup();
reject(new Error('Calculation end timeout'));
}, 30_000);
// Watchdog: if no "calculation started" signal is received within 500ms,
// assume there is no real calculation running and resolve immediately.
const startWatchdog = setTimeout(() => {
if (!started) {
cleanup();
resolve();
}
}, 500);
// Listen for "calculation in progress" signal (stageInfo)
const processingDisposable = this.calculationProcessing(() => {
if (started) return;
started = true;
// A start signal is received → no need for the watchdog anymore
clearTimeout(startWatchdog);
});
// Listen for the "calculation completed" signal
const endDisposable = this.calculationResultApplied(() => {
if (finished) return;
finished = true;
cleanup();
resolve();
});
function cleanup(): void {
clearTimeout(mainTimer);
clearTimeout(startWatchdog);
processingDisposable.dispose();
endDisposable.dispose();
}
});
}
/**
* Execute a batch of formulas asynchronously and receive computed results.
*
@@ -120,7 +120,7 @@ export class DataSyncPrimaryController extends RxDisposable {
// do not sync mutations those are not meant to be synced
this._syncingMutations.has(id)
) {
this._remoteInstanceService.syncMutation({ mutationInfo: commandInfo as IMutationInfo });
this._remoteInstanceService.syncMutation({ mutationInfo: commandInfo as IMutationInfo }, options);
}
}));
}
@@ -62,7 +62,7 @@ export class DataSyncReplicaController extends Disposable {
if (commandInfo.type === CommandType.MUTATION && !(options as IRemoteSyncMutationOptions)?.fromSync) {
this._remoteSyncService.syncMutation({
mutationInfo: commandInfo as IMutationInfo,
});
}, options);
}
})
);
@@ -30,15 +30,17 @@ export const RemoteSyncServiceName = 'rpc.remote-sync.service';
*/
export const IRemoteSyncService = createIdentifier<IRemoteSyncService>(RemoteSyncServiceName);
export interface IRemoteSyncService {
syncMutation(params: { mutationInfo: IMutationInfo }): Promise<boolean>;
syncMutation(params: { mutationInfo: IMutationInfo }, options?: IExecutionOptions): Promise<boolean>;
}
export class RemoteSyncPrimaryService implements IRemoteSyncService {
constructor(@ICommandService private readonly _commandService: ICommandService) {
// empty
}
async syncMutation(params: { mutationInfo: IMutationInfo }): Promise<boolean> {
async syncMutation(params: { mutationInfo: IMutationInfo }, options?: IExecutionOptions): Promise<boolean> {
const { fromCollab, ...restOptions } = options || {};
return this._commandService.syncExecuteCommand(params.mutationInfo.id, params.mutationInfo.params, {
...restOptions,
onlyLocal: true,
fromSync: true,
});
@@ -60,7 +62,7 @@ export interface IRemoteInstanceService {
createInstance(params: { unitID: string; type: UniverInstanceType; snapshot: IWorkbookData }): Promise<boolean>;
disposeInstance(params: { unitID: string }): Promise<boolean>;
syncMutation(params: { mutationInfo: IMutationInfo }): Promise<boolean>;
syncMutation(params: { mutationInfo: IMutationInfo }, options?: IExecutionOptions): Promise<boolean>;
}
export class WebWorkerRemoteInstanceService implements IRemoteInstanceService {
@@ -76,8 +78,8 @@ export class WebWorkerRemoteInstanceService implements IRemoteInstanceService {
return Promise.resolve(true);
}
async syncMutation(params: { mutationInfo: IMutationInfo }): Promise<boolean> {
return this._applyMutation(params.mutationInfo);
async syncMutation(params: { mutationInfo: IMutationInfo }, options?: IExecutionOptions): Promise<boolean> {
return this._applyMutation(params.mutationInfo, options);
}
async createInstance(params: {
@@ -111,9 +113,11 @@ export class WebWorkerRemoteInstanceService implements IRemoteInstanceService {
return this._univerInstanceService.disposeUnit(params.unitID);
}
protected _applyMutation(mutationInfo: IMutationInfo): boolean {
protected _applyMutation(mutationInfo: IMutationInfo, options?: IExecutionOptions): boolean {
const { id, params: mutationParams } = mutationInfo;
const { fromCollab, ...restOptions } = options || {};
return this._commandService.syncExecuteCommand(id, mutationParams, {
...restOptions,
onlyLocal: true,
fromSync: true,
});
+143 -4
View File
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import type { IDisposable, ILocales } from '@univerjs/core';
import type { IFunctionInfo } from '@univerjs/engine-formula';
import type { ICommandInfo, IDisposable, ILocales } from '@univerjs/core';
import type { IFunctionInfo, ISetFormulaCalculationResultMutation } from '@univerjs/engine-formula';
import type { CalculationMode, IRegisterAsyncFunction, IRegisterFunction, ISingleFunctionRegisterParams, IUniverSheetsFormulaBaseConfig } from '@univerjs/sheets-formula';
import { debounce, IConfigService, ILogService, LifecycleService, LifecycleStages } from '@univerjs/core';
import { SetTriggerFormulaCalculationStartMutation } from '@univerjs/engine-formula';
import { FormulaExecuteStageType, SetFormulaCalculationResultMutation, SetTriggerFormulaCalculationStartMutation } from '@univerjs/engine-formula';
import { FFormula } from '@univerjs/engine-formula/facade';
import { SetRangeValuesMutation } from '@univerjs/sheets';
import { IRegisterFunctionService, PLUGIN_CONFIG_KEY_BASE, RegisterFunctionService } from '@univerjs/sheets-formula';
/**
@@ -271,6 +271,61 @@ export interface IFFormulaSheetsMixin {
* ```
*/
registerAsyncFunction(name: string, func: IRegisterAsyncFunction, { locales, description }: { locales?: ILocales; description?: string | IFunctionInfo }): IDisposable;
/**
* Listens for the moment when formula-calculation results are applied.
*
* This event fires after the engine completes a calculation cycle and
* dispatches a `SetFormulaCalculationResultMutation`.
* The callback is invoked during an idle frame to avoid blocking UI updates.
*
* @param {Function} callback - A function called with the calculation result payload
* once the result-application mutation is emitted.
* @returns {IDisposable} A disposable used to unsubscribe from the event.
*
* @example
* ```ts
* const formulaEngine = univerAPI.getFormula();
*
* const dispose = formulaEngine.calculationResultApplied((result) => {
* console.log('Calculation results applied:', result);
* });
*
* // Later…
* dispose.dispose();
* ```
*/
calculationResultApplied(callback: (result: ISetFormulaCalculationResultMutation) => void): IDisposable;
/**
* Waits for formula-calculation results to be applied.
*
* This method resolves under three conditions:
* 1. A real calculation runs and the engine emits a "calculation started" signal,
* followed by a "calculation result applied" signal.
* 2. No calculation actually starts within 500 ms — the method assumes there is
* nothing to wait for and resolves automatically.
* 3. A global 30 s timeout triggers, in which case the promise rejects.
*
* The API internally listens to both “calculation in progress” events and
* “calculation result applied” events, ensuring it behaves correctly whether
* formulas are recalculated or skipped due to cache/state.
*
* @returns {Promise<void>} A promise that resolves when calculation results are applied
* or when no calculation occurs within the start-detection window.
*
* @example
* ```ts
* const formulaEngine = univerAPI.getFormula();
*
* // Wait for formula updates to apply before reading values.
* await formulaEngine.onCalculationResultApplied();
*
* const value = sheet.getRange("C24").getValue();
* console.log("Updated value:", value);
* ```
*/
onCalculationResultApplied(): Promise<void>;
}
export class FFormulaSheetsMixin extends FFormula implements IFFormulaSheetsMixin {
@@ -370,6 +425,90 @@ export class FFormulaSheetsMixin extends FFormula implements IFFormulaSheetsMixi
this._debouncedFormulaCalculation();
return functionsDisposable;
}
override calculationResultApplied(callback: (result: ISetFormulaCalculationResultMutation) => void): IDisposable {
let setFormulaCalculationResult = false;
let applyFormulaCalculationResult = false;
let result: ISetFormulaCalculationResultMutation | null = null;
return this._commandService.onCommandExecuted((command: ICommandInfo, options) => {
if (command.id !== SetFormulaCalculationResultMutation.id && command.id !== SetRangeValuesMutation.id) {
return;
}
if (command.id === SetFormulaCalculationResultMutation.id) {
setFormulaCalculationResult = true;
result = command.params as ISetFormulaCalculationResultMutation;
}
if (command.id === SetRangeValuesMutation.id && options?.applyFormulaCalculationResult) {
applyFormulaCalculationResult = true;
}
if (!setFormulaCalculationResult || !applyFormulaCalculationResult) {
return;
}
requestIdleCallback(() => {
callback(result!);
});
});
}
override onCalculationResultApplied(): Promise<void> {
return new Promise((resolve, reject) => {
let started = false;
let finished = false;
// Global timeout: reject if the whole calculation hangs
const mainTimer = setTimeout(() => {
cleanup();
reject(new Error('Calculation end timeout'));
}, 60_000);
// Watchdog: if no "calculation started" signal is received within 500ms,
// assume there is no real calculation running and resolve immediately.
const startWatchdog = setTimeout(() => {
if (!started) {
cleanup();
resolve();
}
}, 500);
// Listen for "calculation in progress" signal (stageInfo)
const processingDisposable = this.calculationProcessing((stageInfo) => {
// If no formulas to calculate, resolve immediately
const { stage, totalArrayFormulasToCalculate, totalFormulasToCalculate } = stageInfo;
if (stage === FormulaExecuteStageType.START_CALCULATION && totalArrayFormulasToCalculate + totalFormulasToCalculate === 0) {
cleanup();
resolve();
return;
}
if (started) return;
started = true;
// A start signal is received → no need for the watchdog anymore
clearTimeout(startWatchdog);
});
// Listen for the "calculation completed" signal
const endDisposable = this.calculationResultApplied(() => {
if (finished) return;
finished = true;
cleanup();
resolve();
});
function cleanup(): void {
clearTimeout(mainTimer);
clearTimeout(startWatchdog);
processingDisposable.dispose();
endDisposable.dispose();
}
});
}
}
FFormula.extend(FFormulaSheetsMixin);
@@ -16,9 +16,8 @@
import type { ICellData, ICommandInfo, IObjectMatrixPrimitiveType, Nullable } from '@univerjs/core';
import type { ISetFormulaCalculationResultMutation } from '@univerjs/engine-formula';
import { Disposable, ICommandService, Inject, IUniverInstanceService, ObjectMatrix } from '@univerjs/core';
import { Disposable, ICommandService, Inject, IUniverInstanceService, ObjectMatrix, sequenceExecute } from '@univerjs/core';
import { handleNumfmtInCell, SetFormulaCalculationResultMutation } from '@univerjs/engine-formula';
import { SetRangeValuesMutation } from '../commands/mutations/set-range-values.mutation';
export class CalculateResultApplyController extends Disposable {
@@ -80,11 +79,14 @@ export class CalculateResultApplyController extends Disposable {
}
}
const result = redoMutationsInfo.every((m) =>
this._commandService.executeCommand(m.id, m.params, {
const result = sequenceExecute(
redoMutationsInfo,
this._commandService,
{
onlyLocal: true,
fromFormula: true,
})
applyFormulaCalculationResult: true,
}
);
return result;
})
@@ -66,12 +66,13 @@ export async function expectMoveFormulaRowsResultMatchesSnapshot() {
univerInstanceService.focusUnit(workbook.getId());
const worksheet = workbook.getActiveSheet();
await testBed.api.getFormula().onCalculationResultApplied();
// move row 3 to before row 5
const rowSpec = worksheet.getRange('3:3');
worksheet.moveRows(rowSpec, 4);
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
const resultSnapshot = workbook.save();
const snapshotFilePath = path.resolve(snapshotRootDir, `${getTestFilePath()}-result.json`);
@@ -88,7 +89,6 @@ export async function expectMoveFormulaRowsResultMatchesSnapshot() {
// perform undo operation
await testBed.api.undo();
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
// compare the result with the snapshot
const resultSnapshot_undo = workbook.save();
@@ -121,12 +121,13 @@ export async function expectMoveFormulaSiRowsResultMatchesSnapshot() {
univerInstanceService.focusUnit(workbook.getId());
const worksheet = workbook.getActiveSheet();
await testBed.api.getFormula().onCalculationResultApplied();
// move row 3 to before row 5
const rowSpec = worksheet.getRange('3:3');
worksheet.moveRows(rowSpec, 4);
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
const resultSnapshot = workbook.save();
const snapshotFilePath = path.resolve(snapshotRootDir, `${getTestFilePath()}-result.json`);
@@ -143,7 +144,6 @@ export async function expectMoveFormulaSiRowsResultMatchesSnapshot() {
// perform undo operation
await testBed.api.undo();
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
// compare the result with the snapshot
const resultSnapshot_undo = workbook.save();
@@ -176,6 +176,8 @@ export async function expectMoveFormulaCellResultMatchesSnapshot() {
univerInstanceService.focusUnit(workbook.getId());
const worksheet = workbook.getActiveSheet();
await testBed.api.getFormula().onCalculationResultApplied();
// move D4:D5 to G13:G14
const fromRange = worksheet.getRange('D4:D5').getRange();
const toRange = worksheet.getRange('G13:G14').getRange();
@@ -185,7 +187,6 @@ export async function expectMoveFormulaCellResultMatchesSnapshot() {
});
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
const resultSnapshot = workbook.save();
const snapshotFilePath = path.resolve(snapshotRootDir, `${getTestFilePath()}-result.json`);
@@ -202,7 +203,6 @@ export async function expectMoveFormulaCellResultMatchesSnapshot() {
// perform undo operation
await testBed.api.undo();
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
// compare the result with the snapshot
const resultSnapshot_undo = workbook.save();
@@ -66,11 +66,12 @@ export async function expectRemoveRowsOfFilterRowsResultMatchesSnapshot() {
univerInstanceService.focusUnit(workbook.getId());
const worksheet = workbook.getActiveSheet();
await testBed.api.getFormula().onCalculationResultApplied();
// remove rows 2 to 5, where the 3 to 4 rows are filtered rows
worksheet.deleteRows(1, 4);
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
const resultSnapshot = workbook.save();
const snapshotFilePath = path.resolve(snapshotRootDir, `${getTestFilePath()}-result.json`);
@@ -87,7 +88,6 @@ export async function expectRemoveRowsOfFilterRowsResultMatchesSnapshot() {
// perform undo operation
await testBed.api.undo();
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
// compare the result with the snapshot
const resultSnapshot_undo = workbook.save();