mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-08-10 23:08:41 +00:00
将节点树遍历与 target 匹配下沉到与 collectByWorker 共用的常驻 worker, 避免大页面时主线程卡死;通过可序列化 descriptor 重建内置 target, 失败时回退主线程空闲队列。
580 lines
22 KiB
TypeScript
580 lines
22 KiB
TypeScript
/*
|
||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||
*
|
||
* Copyright (C) 2025 Tencent.
|
||
*/
|
||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||
import { reactive } from 'vue';
|
||
|
||
import { createDataSourceTarget, DepTargetType, Target } from '@tmagic/core';
|
||
|
||
import depService from '@editor/services/dep';
|
||
|
||
// 全量收集({ id, dsl })与增量收集({ id, payload })共用一个常驻 worker
|
||
vi.mock('@editor/utils/dep/worker.ts?worker&inline', () => ({
|
||
default: class FakeWorker {
|
||
public static instances: FakeWorker[] = [];
|
||
/** 全量收集返回的 deps */
|
||
public static nextData: Record<string, any> = {};
|
||
public static nextError = false;
|
||
public static nextDelay = 0;
|
||
/** 收到的全部请求(全量 + 增量),用于断言测例真正走到了 worker */
|
||
public static requests: any[] = [];
|
||
/** 增量收集返回的数据 */
|
||
public static response: any = { deps: {}, nodeIds: [] };
|
||
public static failed = false;
|
||
public static delay = 0;
|
||
|
||
public onmessage: ((e: any) => void) | null = null;
|
||
public onerror: (() => void) | null = null;
|
||
public onmessageerror: (() => void) | null = null;
|
||
|
||
constructor() {
|
||
FakeWorker.instances.push(this);
|
||
}
|
||
|
||
public postMessage(request: any) {
|
||
FakeWorker.requests.push(request);
|
||
|
||
if (!('dsl' in request)) {
|
||
setTimeout(() => {
|
||
this.onmessage?.({
|
||
data: { id: request.id, failed: FakeWorker.failed, ...FakeWorker.response },
|
||
});
|
||
}, FakeWorker.delay);
|
||
return;
|
||
}
|
||
|
||
setTimeout(() => {
|
||
if (FakeWorker.nextError) {
|
||
this.onerror?.();
|
||
return;
|
||
}
|
||
this.onmessage?.({ data: { id: request.id, deps: FakeWorker.nextData } });
|
||
}, FakeWorker.nextDelay);
|
||
}
|
||
|
||
public terminate() {}
|
||
},
|
||
}));
|
||
|
||
const makeTarget = (id = 't1', type: string = DepTargetType.DEFAULT) =>
|
||
new Target({
|
||
id,
|
||
type,
|
||
isTarget: () => false,
|
||
});
|
||
|
||
const getFakeWorker = async () => (await import('@editor/utils/dep/worker.ts?worker&inline')).default as any;
|
||
|
||
/** 环境不支持 Worker 时会走主线程 / 返回空结果,这里显式打开 worker 路径并重置 mock 状态 */
|
||
const enableCollectWorker = async () => {
|
||
const fakeCollectWorker = await getFakeWorker();
|
||
fakeCollectWorker.instances = [];
|
||
fakeCollectWorker.requests = [];
|
||
fakeCollectWorker.response = { deps: {}, nodeIds: [] };
|
||
fakeCollectWorker.failed = false;
|
||
fakeCollectWorker.delay = 0;
|
||
fakeCollectWorker.nextData = {};
|
||
fakeCollectWorker.nextError = false;
|
||
fakeCollectWorker.nextDelay = 0;
|
||
(globalThis as any).Worker = class {};
|
||
return fakeCollectWorker;
|
||
};
|
||
|
||
beforeEach(async () => {
|
||
depService.reset();
|
||
// collectByWorker / collectIdle 共用常驻 CollectWorkerClient,happy-dom 默认没有 Worker
|
||
await enableCollectWorker();
|
||
});
|
||
|
||
afterEach(() => {
|
||
delete (globalThis as any).Worker;
|
||
vi.clearAllMocks();
|
||
});
|
||
|
||
// Promise.withResolvers polyfill for older Node
|
||
if (typeof (Promise as any).withResolvers !== 'function') {
|
||
(Promise as any).withResolvers = function withResolvers<T>() {
|
||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||
let reject!: (reason?: unknown) => void;
|
||
const promise = new Promise<T>((res, rej) => {
|
||
resolve = res;
|
||
reject = rej;
|
||
});
|
||
return { promise, resolve, reject };
|
||
};
|
||
}
|
||
|
||
describe('Dep service', () => {
|
||
test('addTarget / getTarget / removeTarget', () => {
|
||
const t = makeTarget('t1');
|
||
depService.addTarget(t);
|
||
expect(depService.getTarget('t1')).toBeDefined();
|
||
expect(depService.hasTarget('t1')).toBe(true);
|
||
depService.removeTarget('t1');
|
||
expect(depService.getTarget('t1')).toBeUndefined();
|
||
});
|
||
|
||
test('addTarget 触发 add-target 事件', () => {
|
||
const fn = vi.fn();
|
||
depService.on('add-target', fn);
|
||
const t = makeTarget('t2');
|
||
depService.addTarget(t);
|
||
expect(fn).toHaveBeenCalledWith(t);
|
||
depService.off('add-target', fn);
|
||
});
|
||
|
||
test('removeTarget 触发 remove-target 事件', () => {
|
||
const fn = vi.fn();
|
||
const t = makeTarget('t3');
|
||
depService.addTarget(t);
|
||
depService.on('remove-target', fn);
|
||
depService.removeTarget('t3');
|
||
expect(fn).toHaveBeenCalledWith('t3', DepTargetType.DEFAULT);
|
||
depService.off('remove-target', fn);
|
||
});
|
||
|
||
test('removeTargets 不抛错并清空目标', () => {
|
||
depService.addTarget(makeTarget('a'));
|
||
depService.addTarget(makeTarget('b'));
|
||
expect(() => depService.removeTargets()).not.toThrow();
|
||
expect(depService.getTarget('a')).toBeUndefined();
|
||
});
|
||
|
||
test('removeTargets - 不存在的 type 直接返回', () => {
|
||
expect(() => depService.removeTargets('not-existing')).not.toThrow();
|
||
});
|
||
|
||
test('hasSpecifiedTypeTarget / clearTargets', () => {
|
||
depService.addTarget(makeTarget('x'));
|
||
expect(depService.hasSpecifiedTypeTarget()).toBe(true);
|
||
depService.clearTargets();
|
||
expect(depService.hasSpecifiedTypeTarget()).toBe(false);
|
||
});
|
||
|
||
test('set / get state', () => {
|
||
depService.set('collecting', true);
|
||
expect(depService.get('collecting')).toBe(true);
|
||
depService.set('taskLength', 5);
|
||
expect(depService.get('taskLength')).toBe(5);
|
||
});
|
||
|
||
test('collect 调用后触发 collected 事件', () => {
|
||
const fn = vi.fn();
|
||
depService.on('collected', fn);
|
||
depService.collect([{ id: 'n1', type: 'text' }] as any);
|
||
expect(fn).toHaveBeenCalled();
|
||
expect(depService.get('collecting')).toBe(false);
|
||
depService.off('collected', fn);
|
||
});
|
||
|
||
test('collectIdle - 没有命中时立即 resolve 并 emit collected', async () => {
|
||
const fn = vi.fn();
|
||
depService.on('collected', fn);
|
||
await expect(depService.collectIdle([{ id: 'n1', type: 'text' }] as any)).resolves.toBe(true);
|
||
expect(fn).toHaveBeenCalled();
|
||
depService.off('collected', fn);
|
||
});
|
||
|
||
test('collectByWorker 完成后触发 collected 与 ds-collected', async () => {
|
||
const fakeWorker = await enableCollectWorker();
|
||
const fn = vi.fn();
|
||
const dsFn = vi.fn();
|
||
depService.on('collected', fn);
|
||
depService.on('ds-collected', dsFn);
|
||
await depService.collectByWorker({ items: [], id: 'app', type: 'app' } as any);
|
||
// 必须真正 postMessage 到 worker,避免 isSupported=false 时假通过
|
||
expect(fakeWorker.requests.some((request: any) => 'dsl' in request)).toBe(true);
|
||
expect(fn).toHaveBeenCalled();
|
||
expect(dsFn).toHaveBeenCalled();
|
||
depService.off('collected', fn);
|
||
depService.off('ds-collected', dsFn);
|
||
});
|
||
|
||
test('clear 与 clearByType', () => {
|
||
expect(() => depService.clear()).not.toThrow();
|
||
expect(() => depService.clearByType(DepTargetType.DEFAULT)).not.toThrow();
|
||
});
|
||
|
||
test('clearIdleTasks 安全调用', () => {
|
||
expect(() => depService.clearIdleTasks()).not.toThrow();
|
||
});
|
||
|
||
test('reset 后 collecting=false 且 targets 清空', () => {
|
||
depService.addTarget(makeTarget('rs'));
|
||
depService.set('collecting', true);
|
||
depService.reset();
|
||
expect(depService.get('collecting')).toBe(false);
|
||
expect(depService.hasTarget('rs')).toBe(false);
|
||
});
|
||
|
||
test('collect 在有 collectable target 时会收集依赖并触发 collected / ds-collected', () => {
|
||
const collected = vi.fn();
|
||
const dsCollected = vi.fn();
|
||
depService.on('collected', collected);
|
||
depService.on('ds-collected', dsCollected);
|
||
depService.addTarget(makeTarget('t-collect'));
|
||
depService.collect([{ id: 'n1', type: 'text' }] as any);
|
||
expect(collected).toHaveBeenCalledWith([{ id: 'n1', type: 'text' }], false);
|
||
expect(dsCollected).toHaveBeenCalled();
|
||
depService.off('collected', collected);
|
||
depService.off('ds-collected', dsCollected);
|
||
});
|
||
|
||
test('collect 对 page 节点会清理 page 级旧依赖', () => {
|
||
depService.addTarget(makeTarget('page-target'));
|
||
expect(() => depService.collect([{ id: 'p1', type: 'page', items: [] }] as any, { pageId: 'p1' })).not.toThrow();
|
||
});
|
||
|
||
test('collectNode 支持 page 与普通节点两条路径', () => {
|
||
const target = makeTarget('node-target');
|
||
depService.addTarget(target);
|
||
depService.collectNode({ id: 'n1', type: 'text' } as any, target);
|
||
depService.collectNode({ id: 'p1', type: 'page', items: [] } as any, target, { pageId: 'p1' });
|
||
expect(depService.get('collecting')).toBe(false);
|
||
});
|
||
|
||
test('collectByWorker worker 报错时返回空对象并完成 collected', async () => {
|
||
const fakeWorker = await enableCollectWorker();
|
||
fakeWorker.nextError = true;
|
||
const collected = vi.fn();
|
||
depService.on('collected', collected);
|
||
const result = await depService.collectByWorker({ items: [], id: 'app', type: 'app' } as any);
|
||
expect(fakeWorker.requests.some((request: any) => 'dsl' in request)).toBe(true);
|
||
expect(result).toEqual({});
|
||
expect(collected).toHaveBeenCalled();
|
||
depService.off('collected', collected);
|
||
});
|
||
|
||
test('collectByWorker 会把 worker 返回的 deps 写回 target 与 dsl', async () => {
|
||
const fakeWorker = await enableCollectWorker();
|
||
depService.addTarget(makeTarget('ds1', DepTargetType.DATA_SOURCE));
|
||
depService.addTarget(makeTarget('cond1', DepTargetType.DATA_SOURCE_COND));
|
||
depService.addTarget(makeTarget('method1', DepTargetType.DATA_SOURCE_METHOD));
|
||
fakeWorker.nextData = {
|
||
[DepTargetType.DATA_SOURCE]: { ds1: { fieldA: { data: {} } } },
|
||
[DepTargetType.DATA_SOURCE_COND]: { cond1: { condA: { data: {} } } },
|
||
[DepTargetType.DATA_SOURCE_METHOD]: { method1: { methodA: { data: {} } } },
|
||
};
|
||
const dsl: any = {
|
||
items: [{ id: 'n1', type: 'text' }],
|
||
id: 'app',
|
||
type: 'app',
|
||
dataSourceDeps: {},
|
||
dataSourceCondDeps: {},
|
||
dataSourceMethodDeps: {},
|
||
};
|
||
await depService.collectByWorker(dsl);
|
||
expect(fakeWorker.requests.some((request: any) => 'dsl' in request)).toBe(true);
|
||
expect(dsl.dataSourceDeps.ds1).toBeDefined();
|
||
expect(dsl.dataSourceCondDeps.cond1).toBeDefined();
|
||
expect(dsl.dataSourceMethodDeps.method1).toBeDefined();
|
||
});
|
||
|
||
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('target 收集抛错时批次仍会结算,collecting 与 taskLength 复位', async () => {
|
||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||
depService.addTarget(
|
||
new Target({
|
||
id: 'throw-target',
|
||
type: DepTargetType.DATA_SOURCE,
|
||
isTarget: () => {
|
||
throw new Error('boom');
|
||
},
|
||
}),
|
||
);
|
||
|
||
const nodes = Array.from({ length: 50 }, (_, i) => ({ id: `n${i}`, type: 'text', text: 'x' })) as any;
|
||
await expect(depService.collectIdle(nodes, {}, false, DepTargetType.DATA_SOURCE)).resolves.toBe(true);
|
||
|
||
expect(depService.get('collecting')).toBe(false);
|
||
// taskLength 的更新做了 1s 节流,等节流窗口结束后才会同步到 0
|
||
await new Promise((resolve) => setTimeout(resolve, 1100));
|
||
expect(depService.get('taskLength')).toBe(0);
|
||
|
||
errorSpy.mockRestore();
|
||
});
|
||
|
||
test('collectIdle 优先用 worker 收集,主线程只把结果写回 target', async () => {
|
||
const fakeCollectWorker = await enableCollectWorker();
|
||
fakeCollectWorker.response = {
|
||
deps: { [DepTargetType.DATA_SOURCE]: { ds_1: { n1: { name: 'n1', keys: ['text'] } } } },
|
||
nodeIds: ['n1'],
|
||
};
|
||
|
||
const target = createDataSourceTarget({ id: 'ds_1', fields: [] }, reactive({}));
|
||
depService.addTarget(target);
|
||
// 旧依赖应被本次收集结果覆盖,而不是与之合并
|
||
target.deps.n1 = { name: 'n1', keys: ['stale'] };
|
||
|
||
const collected = vi.fn();
|
||
const dsCollected = vi.fn();
|
||
depService.on('collected', collected);
|
||
depService.on('ds-collected', dsCollected);
|
||
|
||
const nodes = [{ id: 'n1', type: 'text', text: '${ds_1.name}' }] as any;
|
||
await expect(depService.collectIdle(nodes, {}, true, DepTargetType.DATA_SOURCE)).resolves.toBe(true);
|
||
|
||
expect(fakeCollectWorker.requests).toHaveLength(1);
|
||
// isTarget 无法跨线程传递,worker 中用 descriptor 重建 target
|
||
expect(fakeCollectWorker.requests[0].payload).toContain('ds_1');
|
||
expect(target.deps.n1.keys).toEqual(['text']);
|
||
expect(dsCollected).toHaveBeenCalledWith(nodes, true);
|
||
expect(collected).toHaveBeenCalledWith(nodes, true);
|
||
expect(depService.get('collecting')).toBe(false);
|
||
|
||
depService.off('collected', collected);
|
||
depService.off('ds-collected', dsCollected);
|
||
});
|
||
|
||
test('collectIdle 会删除 worker 收集范围内节点的旧依赖', async () => {
|
||
const fakeCollectWorker = await enableCollectWorker();
|
||
fakeCollectWorker.response = { deps: {}, nodeIds: ['n1', 'n1_1'] };
|
||
|
||
const target = createDataSourceTarget({ id: 'ds_1', fields: [] }, reactive({}));
|
||
depService.addTarget(target);
|
||
target.deps.n1 = { name: 'n1', keys: ['text'] };
|
||
target.deps.n1_1 = { name: 'n1_1', keys: ['text'] };
|
||
target.deps.other = { name: 'other', keys: ['text'] };
|
||
|
||
await depService.collectIdle([{ id: 'n1', type: 'text' }] as any, {}, true, DepTargetType.DATA_SOURCE);
|
||
|
||
expect(target.deps.n1).toBeUndefined();
|
||
expect(target.deps.n1_1).toBeUndefined();
|
||
// 不在本次收集范围内的节点依赖不受影响
|
||
expect(target.deps.other).toBeDefined();
|
||
});
|
||
|
||
test('collectIdle 对 page 节点按 pageId 清理旧依赖', async () => {
|
||
const fakeCollectWorker = await enableCollectWorker();
|
||
fakeCollectWorker.response = { deps: {}, nodeIds: ['p1'] };
|
||
|
||
const target = createDataSourceTarget({ id: 'ds_1', fields: [] }, reactive({}));
|
||
depService.addTarget(target);
|
||
// 已被删除的节点残留依赖,只能按 pageId 匹配清理
|
||
target.deps.removed = { name: 'removed', keys: ['text'], data: { pageId: 'p1' } };
|
||
target.deps.otherPage = { name: 'otherPage', keys: ['text'], data: { pageId: 'p2' } };
|
||
|
||
await depService.collectIdle(
|
||
[{ id: 'p1', type: 'page', items: [] }] as any,
|
||
{ pageId: 'p1' },
|
||
true,
|
||
DepTargetType.DATA_SOURCE,
|
||
);
|
||
|
||
expect(target.deps.removed).toBeUndefined();
|
||
expect(target.deps.otherPage).toBeDefined();
|
||
});
|
||
|
||
test('worker 收集失败时回退到主线程收集,结果一致', async () => {
|
||
const fakeCollectWorker = await enableCollectWorker();
|
||
fakeCollectWorker.failed = true;
|
||
|
||
const target = createDataSourceTarget({ id: 'ds_1', fields: [{ name: 'name', type: 'string' }] }, reactive({}));
|
||
depService.addTarget(target);
|
||
|
||
const nodes = [{ id: 'n1', type: 'text', text: '${ds_1.name}' }] as any;
|
||
await expect(depService.collectIdle(nodes, {}, true, DepTargetType.DATA_SOURCE)).resolves.toBe(true);
|
||
|
||
expect(target.deps.n1.keys).toEqual(['text']);
|
||
});
|
||
|
||
test('没有 descriptor 的自定义 target 仍在主线程收集', async () => {
|
||
const fakeCollectWorker = await enableCollectWorker();
|
||
|
||
const target = new Target({
|
||
id: 'custom',
|
||
type: DepTargetType.DEFAULT,
|
||
isTarget: (key) => key === 'text',
|
||
});
|
||
depService.addTarget(target);
|
||
|
||
await depService.collectIdle([{ id: 'n1', type: 'text', text: 'abc' }] as any);
|
||
|
||
expect(fakeCollectWorker.requests).toHaveLength(0);
|
||
expect(target.deps.n1.keys).toEqual(['text']);
|
||
});
|
||
|
||
test('批次被中断后不再写回 worker 结果', async () => {
|
||
const fakeCollectWorker = await enableCollectWorker();
|
||
fakeCollectWorker.delay = 20;
|
||
fakeCollectWorker.response = {
|
||
deps: { [DepTargetType.DATA_SOURCE]: { ds_1: { n1: { name: 'n1', keys: ['text'] } } } },
|
||
nodeIds: ['n1'],
|
||
};
|
||
|
||
const target = createDataSourceTarget({ id: 'ds_1', fields: [] }, reactive({}));
|
||
depService.addTarget(target);
|
||
|
||
const promise = depService.collectIdle([{ id: 'n1', type: 'text' }] as any, {}, true, DepTargetType.DATA_SOURCE);
|
||
depService.clearIdleTasks();
|
||
|
||
await expect(promise).resolves.toBe(false);
|
||
await new Promise((resolve) => setTimeout(resolve, 40));
|
||
|
||
expect(target.deps.n1).toBeUndefined();
|
||
expect(depService.get('collecting')).toBe(false);
|
||
});
|
||
|
||
test('collectIdle deep=false 不会清掉未参与重收的子孙依赖', async () => {
|
||
const fakeCollectWorker = await enableCollectWorker();
|
||
// worker 按 deep=false 只返回容器自身 id
|
||
fakeCollectWorker.response = { deps: {}, nodeIds: ['container'] };
|
||
|
||
const target = createDataSourceTarget({ id: 'ds_1', fields: [] }, reactive({}));
|
||
depService.addTarget(target);
|
||
target.deps.container = { name: 'container', keys: ['text'] };
|
||
target.deps.child = { name: 'child', keys: ['text'] };
|
||
|
||
await depService.collectIdle(
|
||
[{ id: 'container', type: 'container', items: [{ id: 'child', type: 'text', text: '${ds_1.name}' }] }] as any,
|
||
{},
|
||
false,
|
||
DepTargetType.DATA_SOURCE,
|
||
);
|
||
|
||
expect(target.deps.container).toBeUndefined();
|
||
// 子孙未重收,旧依赖必须保留
|
||
expect(target.deps.child).toBeDefined();
|
||
});
|
||
|
||
test('target 在 worker 收集期间被重建时丢弃过期结果并主线程补收', async () => {
|
||
const fakeCollectWorker = await enableCollectWorker();
|
||
fakeCollectWorker.response = {
|
||
deps: { [DepTargetType.DATA_SOURCE]: { ds_1: { n1: { name: 'n1', keys: ['stale'] } } } },
|
||
nodeIds: ['n1'],
|
||
};
|
||
|
||
depService.addTarget(
|
||
createDataSourceTarget({ id: 'ds_1', fields: [{ name: 'name', type: 'string' }] }, reactive({})),
|
||
);
|
||
|
||
const promise = depService.collectIdle(
|
||
[{ id: 'n1', type: 'text', text: '${ds_1.name}' }] as any,
|
||
{},
|
||
true,
|
||
DepTargetType.DATA_SOURCE,
|
||
);
|
||
|
||
// 模拟修改数据源字段:target 被移除并重建
|
||
depService.removeTarget('ds_1', DepTargetType.DATA_SOURCE);
|
||
const newTarget = createDataSourceTarget({ id: 'ds_1', fields: [{ name: 'name', type: 'string' }] }, reactive({}));
|
||
depService.addTarget(newTarget);
|
||
|
||
await promise;
|
||
|
||
// 过期的 stale 结果未写入;对新 target 主线程补收得到正确依赖
|
||
expect(newTarget.deps.n1.keys).toEqual(['text']);
|
||
expect(depService.get('collecting')).toBe(false);
|
||
});
|
||
|
||
test('clearIdleTasks 会 abort 常驻 worker,避免在途任务继续占用', async () => {
|
||
const fakeCollectWorker = await enableCollectWorker();
|
||
fakeCollectWorker.delay = 50;
|
||
depService.addTarget(createDataSourceTarget({ id: 'ds_1', fields: [] }, reactive({})));
|
||
|
||
const promise = depService.collectIdle([{ id: 'n1', type: 'text' }] as any, {}, true, DepTargetType.DATA_SOURCE);
|
||
expect(fakeCollectWorker.instances).toHaveLength(1);
|
||
|
||
depService.clearIdleTasks();
|
||
await expect(promise).resolves.toBe(false);
|
||
|
||
// abort 后再次收集会新建 worker,而不是排队在已 abort 的旧任务后面
|
||
fakeCollectWorker.delay = 0;
|
||
await depService.collectIdle([{ id: 'n2', type: 'text' }] as any, {}, true, DepTargetType.DATA_SOURCE);
|
||
expect(fakeCollectWorker.instances.length).toBeGreaterThan(1);
|
||
});
|
||
|
||
test('collectIdle 节点为空时不会启动 worker', async () => {
|
||
const fakeCollectWorker = await enableCollectWorker();
|
||
depService.addTarget(createDataSourceTarget({ id: 'ds_1', fields: [] }, reactive({})));
|
||
|
||
await expect(depService.collectIdle([], {}, true, DepTargetType.DATA_SOURCE)).resolves.toBe(true);
|
||
|
||
expect(fakeCollectWorker.requests).toHaveLength(0);
|
||
});
|
||
|
||
test('destroy 会 reset 并移除监听', () => {
|
||
depService.addTarget(makeTarget('destroy-me'));
|
||
expect(() => depService.destroy()).not.toThrow();
|
||
expect(depService.hasTarget('destroy-me')).toBe(false);
|
||
});
|
||
});
|