mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-25 07:28:23 +00:00
fix(editor): 修复 collectIdle 批次结算与中断时 Promise 挂起问题
引入批次级 Promise 结算,避免快速连续触发或 clearIdleTasks 后 collecting 卡死。 collectIdle 返回 boolean 表示是否完整完成,initService 据此跳过被中断的 stage 更新。
This commit is contained in:
parent
bf8df74864
commit
b5abf31066
@ -333,11 +333,11 @@ export const initServiceEvents = (
|
||||
Promise.all(
|
||||
nodes.map((node) => {
|
||||
if (node.type === NodeType.ROOT) {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return depService.collectIdle([node], { pageId: getPageIdByNode(node) }, deep, type);
|
||||
}),
|
||||
);
|
||||
).then((results) => results.every(Boolean));
|
||||
|
||||
watch(
|
||||
() => editorService.get('stage'),
|
||||
@ -351,7 +351,7 @@ export const initServiceEvents = (
|
||||
|
||||
if (!node) return;
|
||||
|
||||
await collectIdle([node], true, DepTargetType.DATA_SOURCE);
|
||||
if (!(await collectIdle([node], true, DepTargetType.DATA_SOURCE))) return;
|
||||
updateStageNode(node);
|
||||
});
|
||||
},
|
||||
@ -494,8 +494,10 @@ export const initServiceEvents = (
|
||||
|
||||
// 新增节点,收集依赖
|
||||
const nodeAddHandler = (nodes: MComponent[]) => {
|
||||
collectIdle(nodes, true).then(() => {
|
||||
updateStageNodes(nodes);
|
||||
collectIdle(nodes, true).then((completed) => {
|
||||
if (completed) {
|
||||
updateStageNodes(nodes);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -549,8 +551,8 @@ export const initServiceEvents = (
|
||||
if (needRecollectNodes.length) {
|
||||
// 有数据源依赖,需要等依赖重新收集完才更新stage
|
||||
const handler = async () => {
|
||||
await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE);
|
||||
await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE_COND);
|
||||
if (!(await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE))) return;
|
||||
if (!(await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE_COND))) return;
|
||||
updateStageNodes(needRecollectNodes);
|
||||
};
|
||||
handler();
|
||||
@ -572,8 +574,10 @@ export const initServiceEvents = (
|
||||
|
||||
// 历史记录变化时,需要重新收集依赖
|
||||
const historyChangeHandler = (page: MPage | MPageFragment) => {
|
||||
collectIdle([page], true).then(() => {
|
||||
updateStageNode(page);
|
||||
collectIdle([page], true).then((completed) => {
|
||||
if (completed) {
|
||||
updateStageNode(page);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -654,7 +658,7 @@ export const initServiceEvents = (
|
||||
if (Array.isArray(root?.items)) {
|
||||
depService.clearIdleTasks();
|
||||
|
||||
let collectIdlePromises: Promise<void[]>[] = [];
|
||||
let collectIdlePromises: Promise<boolean>[] = [];
|
||||
if (isModifyField) {
|
||||
depService.removeTarget(config.id, DepTargetType.DATA_SOURCE);
|
||||
depService.removeTarget(config.id, DepTargetType.DATA_SOURCE_COND);
|
||||
@ -679,10 +683,15 @@ export const initServiceEvents = (
|
||||
|
||||
collectIdlePromises = [collectIdle(root.items, true, DepTargetType.DATA_SOURCE_METHOD)];
|
||||
}
|
||||
Promise.all(collectIdlePromises)
|
||||
.then(() => updateDataSourceSchema())
|
||||
.then(() => updateDsData())
|
||||
.then(() => updateStageNodes(root.items));
|
||||
const handler = async () => {
|
||||
const results = await Promise.all(collectIdlePromises);
|
||||
if (!results.every(Boolean)) return;
|
||||
|
||||
updateDataSourceSchema();
|
||||
await updateDsData();
|
||||
updateStageNodes(root.items);
|
||||
};
|
||||
handler();
|
||||
}
|
||||
} else if (root?.dataSources) {
|
||||
updateDsData();
|
||||
@ -706,11 +715,12 @@ export const initServiceEvents = (
|
||||
const nodeIds = Object.keys(root.dataSourceDeps?.[id] || {});
|
||||
const nodes = getNodes(nodeIds, root.items);
|
||||
|
||||
await Promise.all([
|
||||
const results = await Promise.all([
|
||||
collectIdle(nodes, false, DepTargetType.DATA_SOURCE),
|
||||
collectIdle(nodes, false, DepTargetType.DATA_SOURCE_COND),
|
||||
collectIdle(nodes, false, DepTargetType.DATA_SOURCE_METHOD),
|
||||
]);
|
||||
if (!results.every(Boolean)) return;
|
||||
|
||||
updateDataSourceSchema();
|
||||
|
||||
|
||||
@ -42,6 +42,21 @@ interface State {
|
||||
|
||||
type StateKey = keyof State;
|
||||
|
||||
/**
|
||||
* 一次 collectIdle 调用对应一个批次。批次自行统计入队/完成的任务数,在自身任务全部完成或被中断时
|
||||
* 结算对应的 Promise,避免多次快速触发时共用 idleTask 的 finish 事件造成的串扰、提前 resolve,
|
||||
* 以及 clearTasks 后 Promise 永不 resolve(collecting 卡死、监听器泄漏)。
|
||||
*/
|
||||
interface CollectBatch {
|
||||
nodes: MNode[];
|
||||
deep: boolean;
|
||||
pending: number;
|
||||
dsPending: number;
|
||||
collectedEmitted: boolean;
|
||||
dsSettled: boolean;
|
||||
resolve: (completed: boolean) => void;
|
||||
}
|
||||
|
||||
class Dep extends BaseService {
|
||||
private state = shallowReactive<State>({
|
||||
collecting: false,
|
||||
@ -54,6 +69,12 @@ class Dep extends BaseService {
|
||||
|
||||
private waitingWorker?: Promise<void>;
|
||||
|
||||
private resolveWaitingWorker?: () => void;
|
||||
|
||||
private workerGeneration = 0;
|
||||
|
||||
private activeBatches = new Set<CollectBatch>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@ -137,38 +158,44 @@ class Dep extends BaseService {
|
||||
await this.waitingWorker;
|
||||
}
|
||||
|
||||
this.set('collecting', true);
|
||||
let startTask = false;
|
||||
this.watcher.collectByCallback(nodes, type, ({ node, target }) => {
|
||||
startTask = true;
|
||||
const batch: CollectBatch = {
|
||||
nodes,
|
||||
deep,
|
||||
pending: 0,
|
||||
dsPending: 0,
|
||||
collectedEmitted: false,
|
||||
dsSettled: false,
|
||||
resolve: () => {},
|
||||
};
|
||||
|
||||
this.enqueueTask(node, target, depExtendedData, deep);
|
||||
this.watcher.collectByCallback(nodes, type, ({ node, target }) => {
|
||||
this.enqueueTask(node, target, depExtendedData, deep, batch);
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
if (!startTask) {
|
||||
this.emit('collected', nodes, deep);
|
||||
this.set('collecting', false);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
this.idleTask.once('finish', () => {
|
||||
this.emit('collected', nodes, deep);
|
||||
this.set('collecting', false);
|
||||
});
|
||||
this.idleTask.once('hight-level-finish', () => {
|
||||
this.emit('ds-collected', nodes, deep);
|
||||
resolve();
|
||||
});
|
||||
// 没有命中任何 target,无需收集,直接完成
|
||||
if (batch.pending === 0) {
|
||||
this.emit('collected', nodes, deep);
|
||||
this.updateCollectingState();
|
||||
return true;
|
||||
}
|
||||
|
||||
this.activeBatches.add(batch);
|
||||
this.set('collecting', true);
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
batch.resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
public collectByWorker(dsl: MApp) {
|
||||
this.set('collecting', true);
|
||||
this.workerGeneration += 1;
|
||||
const generation = this.workerGeneration;
|
||||
|
||||
const { promise, resolve: waitingResolve } = Promise.withResolvers<void>();
|
||||
|
||||
this.waitingWorker = promise;
|
||||
this.resolveWaitingWorker = waitingResolve;
|
||||
|
||||
return new Promise<Record<string, Record<string, DepData>>>((resolve) => {
|
||||
const worker = new Work();
|
||||
@ -182,6 +209,11 @@ class Dep extends BaseService {
|
||||
resolve({});
|
||||
};
|
||||
}).then((depsData) => {
|
||||
if (generation !== this.workerGeneration) {
|
||||
waitingResolve();
|
||||
return depsData;
|
||||
}
|
||||
|
||||
traverseTarget(this.watcher.getTargetsList(), (target) => {
|
||||
if (depsData[target.type]?.[target.id]) {
|
||||
target.deps = reactive(depsData[target.type][target.id]);
|
||||
@ -200,6 +232,10 @@ class Dep extends BaseService {
|
||||
this.emit('collected', dsl.items, true);
|
||||
this.emit('ds-collected', dsl.items, true);
|
||||
waitingResolve();
|
||||
if (this.waitingWorker === promise) {
|
||||
this.waitingWorker = undefined;
|
||||
this.resolveWaitingWorker = undefined;
|
||||
}
|
||||
|
||||
return depsData;
|
||||
});
|
||||
@ -233,6 +269,7 @@ class Dep extends BaseService {
|
||||
}
|
||||
|
||||
public clearIdleTasks() {
|
||||
this.abortActiveBatches();
|
||||
this.idleTask.clearTasks();
|
||||
}
|
||||
|
||||
@ -251,6 +288,11 @@ class Dep extends BaseService {
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.abortActiveBatches();
|
||||
this.workerGeneration += 1;
|
||||
this.resolveWaitingWorker?.();
|
||||
this.resolveWaitingWorker = undefined;
|
||||
this.waitingWorker = undefined;
|
||||
this.idleTask.clearTasks();
|
||||
|
||||
for (const type of Object.keys(this.watcher.getTargetsList())) {
|
||||
@ -286,25 +328,99 @@ class Dep extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private enqueueTask(node: MNode, target: Target, depExtendedData: DepExtendedData, deep: boolean) {
|
||||
private enqueueTask(
|
||||
node: MNode,
|
||||
target: Target,
|
||||
depExtendedData: DepExtendedData,
|
||||
deep: boolean,
|
||||
batch: CollectBatch,
|
||||
) {
|
||||
const isDataSource = target.type === DepTargetType.DATA_SOURCE;
|
||||
|
||||
batch.pending += 1;
|
||||
if (isDataSource) {
|
||||
batch.dsPending += 1;
|
||||
}
|
||||
|
||||
this.idleTask.enqueueTask(
|
||||
({ node, deep, target }) => {
|
||||
this.collectNode(node, target, depExtendedData, deep);
|
||||
try {
|
||||
this.collectNode(node, target, depExtendedData, deep);
|
||||
} finally {
|
||||
this.onBatchTaskDone(batch, isDataSource);
|
||||
}
|
||||
},
|
||||
{
|
||||
node,
|
||||
deep: false,
|
||||
target,
|
||||
},
|
||||
target.type === DepTargetType.DATA_SOURCE,
|
||||
isDataSource,
|
||||
);
|
||||
|
||||
if (deep && Array.isArray(node.items) && node.items.length) {
|
||||
node.items.forEach((item) => {
|
||||
this.enqueueTask(item, target, depExtendedData, deep);
|
||||
this.enqueueTask(item, target, depExtendedData, deep, batch);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onBatchTaskDone(batch: CollectBatch, isDataSource: boolean) {
|
||||
if (isDataSource) {
|
||||
batch.dsPending -= 1;
|
||||
// 数据源依赖收集完先 resolve,让 stage 尽快更新,其余依赖继续在后台收集
|
||||
if (batch.dsPending === 0) {
|
||||
this.settleBatchDs(batch);
|
||||
}
|
||||
}
|
||||
|
||||
batch.pending -= 1;
|
||||
if (batch.pending === 0) {
|
||||
this.finishBatch(batch);
|
||||
}
|
||||
}
|
||||
|
||||
private settleBatchDs(batch: CollectBatch) {
|
||||
if (batch.dsSettled) return;
|
||||
batch.dsSettled = true;
|
||||
this.emit('ds-collected', batch.nodes, batch.deep);
|
||||
batch.resolve(true);
|
||||
}
|
||||
|
||||
private finishBatch(batch: CollectBatch) {
|
||||
if (!batch.collectedEmitted) {
|
||||
batch.collectedEmitted = true;
|
||||
this.emit('collected', batch.nodes, batch.deep);
|
||||
}
|
||||
// 没有数据源任务的批次在此结算 Promise
|
||||
this.settleBatchDs(batch);
|
||||
this.activeBatches.delete(batch);
|
||||
this.updateCollectingState();
|
||||
}
|
||||
|
||||
/**
|
||||
* idleTask 被清空时,在途批次的任务不会再执行,必须主动结算,
|
||||
* 否则对应的 collectIdle Promise 永远不会 resolve(collecting 卡在 true、once 监听器泄漏)。
|
||||
* 收集被中断(通常紧跟一次全量重新收集),因此不再 emit collected/ds-collected,仅结算 Promise。
|
||||
*/
|
||||
private abortActiveBatches() {
|
||||
if (!this.activeBatches.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const batches = [...this.activeBatches];
|
||||
this.activeBatches.clear();
|
||||
|
||||
for (const batch of batches) {
|
||||
batch.resolve(false);
|
||||
}
|
||||
|
||||
this.updateCollectingState();
|
||||
}
|
||||
|
||||
private updateCollectingState() {
|
||||
this.set('collecting', this.activeBatches.size > 0);
|
||||
}
|
||||
}
|
||||
|
||||
export type DepService = Dep;
|
||||
|
||||
@ -25,6 +25,12 @@ globalThis.requestIdleCallback =
|
||||
}, 1);
|
||||
};
|
||||
|
||||
globalThis.cancelIdleCallback =
|
||||
globalThis.cancelIdleCallback ||
|
||||
function (handle) {
|
||||
clearTimeout(handle as unknown as ReturnType<typeof setTimeout>);
|
||||
};
|
||||
|
||||
export class IdleTask<T = any> extends EventEmitter {
|
||||
private taskList: TaskList<T> = [];
|
||||
|
||||
|
||||
@ -91,7 +91,7 @@ const mkServices = () => {
|
||||
clear: vi.fn(),
|
||||
clearTargets: vi.fn(),
|
||||
clearIdleTasks: vi.fn(),
|
||||
collectIdle: vi.fn(async () => undefined),
|
||||
collectIdle: vi.fn(async () => true),
|
||||
collectByWorker: vi.fn(async () => undefined),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
@ -13,16 +13,17 @@ vi.mock('@editor/utils/dep/worker.ts?worker&inline', () => ({
|
||||
default: class FakeWorker {
|
||||
public static nextData: Record<string, any> = {};
|
||||
public static nextError = false;
|
||||
public static nextDelay = 0;
|
||||
public onmessage: ((e: any) => void) | null = null;
|
||||
public onerror: (() => void) | null = null;
|
||||
public postMessage() {
|
||||
setTimeout(() => {
|
||||
if (FakeWorker.nextError) {
|
||||
this.onerror?.(new Event('error'));
|
||||
this.onerror?.();
|
||||
return;
|
||||
}
|
||||
this.onmessage?.({ data: FakeWorker.nextData });
|
||||
}, 0);
|
||||
}, FakeWorker.nextDelay);
|
||||
}
|
||||
},
|
||||
}));
|
||||
@ -121,7 +122,7 @@ describe('Dep service', () => {
|
||||
test('collectIdle - 没有命中时立即 resolve 并 emit collected', async () => {
|
||||
const fn = vi.fn();
|
||||
depService.on('collected', fn);
|
||||
await depService.collectIdle([{ id: 'n1', type: 'text' }] as any);
|
||||
await expect(depService.collectIdle([{ id: 'n1', type: 'text' }] as any)).resolves.toBe(true);
|
||||
expect(fn).toHaveBeenCalled();
|
||||
depService.off('collected', fn);
|
||||
});
|
||||
@ -219,6 +220,83 @@ describe('Dep service', () => {
|
||||
fakeWorker.nextData = {};
|
||||
});
|
||||
|
||||
test('collectIdle 命中 target 时最终 resolve 并按批次 emit collected/ds-collected', async () => {
|
||||
depService.addTarget(makeTarget('ds1', DepTargetType.DATA_SOURCE));
|
||||
const collected = vi.fn();
|
||||
const dsCollected = vi.fn();
|
||||
depService.on('collected', collected);
|
||||
depService.on('ds-collected', dsCollected);
|
||||
|
||||
const nodes = [{ id: 'n1', type: 'text' }] as any;
|
||||
await expect(depService.collectIdle(nodes, {}, false, DepTargetType.DATA_SOURCE)).resolves.toBe(true);
|
||||
|
||||
expect(dsCollected).toHaveBeenCalledWith(nodes, false);
|
||||
expect(collected).toHaveBeenCalledWith(nodes, false);
|
||||
expect(depService.get('collecting')).toBe(false);
|
||||
|
||||
depService.off('collected', collected);
|
||||
depService.off('ds-collected', dsCollected);
|
||||
});
|
||||
|
||||
test('clearIdleTasks 会结算在途 collectIdle,避免 Promise 永久挂起且 collecting 复位', async () => {
|
||||
depService.addTarget(makeTarget('ds1', DepTargetType.DATA_SOURCE));
|
||||
|
||||
const promise = depService.collectIdle([{ id: 'n1', type: 'text' }] as any, {}, false, DepTargetType.DATA_SOURCE);
|
||||
expect(depService.get('collecting')).toBe(true);
|
||||
|
||||
// 快速触发:任务尚未执行就清空队列,批次应被主动结算而不是永久挂起
|
||||
depService.clearIdleTasks();
|
||||
|
||||
await expect(promise).resolves.toBe(false);
|
||||
expect(depService.get('collecting')).toBe(false);
|
||||
});
|
||||
|
||||
test('reset 会结算在途 collectIdle', async () => {
|
||||
depService.addTarget(makeTarget('ds1', DepTargetType.DATA_SOURCE));
|
||||
|
||||
const promise = depService.collectIdle([{ id: 'n1', type: 'text' }] as any, {}, false, DepTargetType.DATA_SOURCE);
|
||||
depService.reset();
|
||||
|
||||
await expect(promise).resolves.toBe(false);
|
||||
expect(depService.get('collecting')).toBe(false);
|
||||
});
|
||||
|
||||
test('reset 会忽略在途 worker 的过期结果,避免覆盖新依赖', async () => {
|
||||
const fakeWorker = (await import('@editor/utils/dep/worker.ts?worker&inline')).default as any;
|
||||
fakeWorker.nextDelay = 20;
|
||||
fakeWorker.nextData = {
|
||||
[DepTargetType.DATA_SOURCE]: { ds1: { n1: { data: {} } } },
|
||||
};
|
||||
|
||||
const workerPromise = depService.collectByWorker({ items: [], id: 'app', type: 'app' } as any);
|
||||
depService.reset();
|
||||
|
||||
const target = makeTarget('ds1', DepTargetType.DATA_SOURCE);
|
||||
depService.addTarget(target);
|
||||
const idlePromise = depService.collectIdle(
|
||||
[{ id: 'n1', type: 'text' }] as any,
|
||||
{},
|
||||
false,
|
||||
DepTargetType.DATA_SOURCE,
|
||||
);
|
||||
|
||||
await Promise.all([workerPromise, idlePromise]);
|
||||
expect(target.deps.n1).toBeUndefined();
|
||||
|
||||
fakeWorker.nextDelay = 0;
|
||||
fakeWorker.nextData = {};
|
||||
});
|
||||
|
||||
test('多个批次并发时各自独立 resolve,全部完成后 collecting 复位', async () => {
|
||||
depService.addTarget(makeTarget('ds1', DepTargetType.DATA_SOURCE));
|
||||
|
||||
const p1 = depService.collectIdle([{ id: 'n1', type: 'text' }] as any, {}, false, DepTargetType.DATA_SOURCE);
|
||||
const p2 = depService.collectIdle([{ id: 'n2', type: 'text' }] as any, {}, false, DepTargetType.DATA_SOURCE);
|
||||
|
||||
await Promise.all([p1, p2]);
|
||||
expect(depService.get('collecting')).toBe(false);
|
||||
});
|
||||
|
||||
test('destroy 会 reset 并移除监听', () => {
|
||||
depService.addTarget(makeTarget('destroy-me'));
|
||||
expect(() => depService.destroy()).not.toThrow();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user