Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions docs/testing/unit/orchestration/orchestrator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,3 +735,72 @@ describe('整页开关入口(#325)', () => {
orch.stop();
});
});

describe('在飞互斥(#326)', () => {
test('在飞期间二次触发且页面尚无译文:假消息层只收到一轮请求,第二次返回忙碌', async () => {
let resolveSend!: (v: unknown) => void;
const send = vi.fn(() => new Promise((r) => (resolveSend = r)));
const orch = createOrchestrator({
send,
hasTranslated: () => false,
});
orch.start();

const first = orch.togglePage(items(1), 'en', 'zh-CN');
const second = await orch.togglePage(items(1), 'en', 'zh-CN');

expect(send).toHaveBeenCalledTimes(1);
expect(second.status).toBe('busy');

resolveSend({ ok: true, data: { translations: ['译'] } });
const firstResult = await first;
expect(firstResult.status).toBe('translated');
orch.stop();
});

test('在飞期间页面已有译文时再次触发:放行还原且在飞批次被中止', async () => {
let resolveSend!: (v: unknown) => void;
const send = vi.fn(() => new Promise((r) => (resolveSend = r)));
const restore = vi.fn();
let translated = false; // 首批渲染完成后置 true(模拟内容脚本渲染)
const orch = createOrchestrator({
send,
hasTranslated: () => translated,
restore,
});
orch.start();

const first = orch.togglePage(items(1), 'en', 'zh-CN');
// 首批在飞期间译文已落 DOM(渲染回调置位)→ 再次触发放行还原
translated = true;
const second = await orch.togglePage(items(1), 'en', 'zh-CN');

expect(second.status).toBe('restored');
expect(restore).toHaveBeenCalledTimes(1);

// 在飞批次被中止:首轮返回 aborted,不产生译文
resolveSend({ ok: true, data: { translations: ['译'] } });
const firstResult = await first;
expect(firstResult.status).toBe('aborted');
orch.stop();
});

test('忙碌状态不被调用方当作错误:正常返回而非抛错', async () => {
let resolveSend!: (v: unknown) => void;
const send = vi.fn(() => new Promise((r) => (resolveSend = r)));
const orch = createOrchestrator({ send, hasTranslated: () => false });
orch.start();

const first = orch.togglePage(items(1), 'en', 'zh-CN');
const second = await orch.togglePage(items(1), 'en', 'zh-CN');

expect(second.status).toBe('busy');
expect(second.summary).toBeUndefined();
// 忙碌结果不携带错误信息(区别于 error 状态)
expect(() => second).not.toThrow();

resolveSend({ ok: true, data: { translations: ['译'] } });
await first;
orch.stop();
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parallel-translation",
"version": "2.0.58",
"version": "2.0.59",
"description": "对照式网页翻译浏览器扩展",
"private": true,
"type": "module",
Expand Down
31 changes: 24 additions & 7 deletions src/orchestration/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export type PageToggleStatus =
| 'restored'
| 'disabled'
| 'blocked'
| 'busy'
| 'aborted'
| 'error'
| 'no-elements';
Expand Down Expand Up @@ -245,6 +246,8 @@ export function createOrchestrator(opts: OrchestratorOptions): TranslationOrches
// #262: 还原纪元在模块内 —— abort() 递增,在飞翻译据此放弃
// 尝试、重试与渲染;新翻译快照新纪元,不受旧批次干扰
let epoch = 0;
// #326: 整页开关入口在飞互斥 —— 在飞期间页面尚无译文时忽略新触发
let toggleInFlight = false;
// #265: 设置变更订阅(start 订阅 / stop 退订)
let unsubscribeSettings: (() => void) | null = null;

Expand Down Expand Up @@ -385,6 +388,13 @@ export function createOrchestrator(opts: OrchestratorOptions): TranslationOrches
async togglePage(items, from, to): Promise<PageToggleResult> {
if (!started) throw new Error('[PT] 编排未启动');

// #326: 在飞互斥 —— 在飞期间页面尚无译文时忽略新触发并返回
// 忙碌状态(忙碌不是错误,调用方不应当作失败);已有译文则
// 放行还原(下方还原分支中止在飞批次)
if (toggleInFlight && !opts.hasTranslated?.()) {
return { status: 'busy', admission: 'allowed' };
}

// 准入判定先行(#311):拦截时零请求、不执行任何动作
const admission = admissionFrom(opts);
if (admission !== 'allowed') {
Expand All @@ -396,6 +406,8 @@ export function createOrchestrator(opts: OrchestratorOptions): TranslationOrches

// #325: 翻译态查询经注入 —— 页面已有译文则还原,否则翻译
if (opts.hasTranslated?.()) {
// #326: 还原中止在飞批次(epoch 递增,在飞翻译放弃重试与渲染)
epoch++;
opts.restore?.();
return { status: 'restored', admission };
}
Expand All @@ -404,13 +416,18 @@ export function createOrchestrator(opts: OrchestratorOptions): TranslationOrches
return { status: 'no-elements', admission };
}

const summary = await translatePageImpl(items, from, to);
const status: PageToggleStatus = summary.aborted
? 'aborted'
: summary.allFailed
? 'error'
: 'translated';
return { status, admission, summary };
toggleInFlight = true;
try {
const summary = await translatePageImpl(items, from, to);
const status: PageToggleStatus = summary.aborted
? 'aborted'
: summary.allFailed
? 'error'
: 'translated';
return { status, admission, summary };
} finally {
toggleInFlight = false;
}
},

async translateText(text, from, to): Promise<SingleTextResult> {
Expand Down
Loading