修复:编辑重复日程缺范围选择;同步并发不再误报失败
问题1(用户反馈):删除重复日程有"仅这一次/此次及以后/全部"三选项,编辑却没有。
根因:save() 把 validate() 与 chosenBook() 检查放在 isSeriesOccurrence() **之前**,
任一不满足即提前 return,永远到不了范围选择弹层;delete 路径则是先判范围。
修法:
- save() 把"重复日程先选范围"提到最前(与删除路径一致),校验推迟到 doSave() 内
- chosenBook() 增加兜底:展开编辑时若该日程所属日历本不在"可写清单"里
(被标记只读/写权限探测失败),用该日程自己的 calKey/href 构造目标;
initPage 里还会用 CalendarDataBridge.lookupBook 把它补进选择列表(带真实名称/颜色)
- 新增 original 原始行快照:doSave 会就地改写 this.event 的 calKey/startTime,
而 EXDATE/UNTIL/拆系列必须依据原值 —— 否则范围计算用错系列
- applySeriesEdit 的 one/future 分支在改写当前行后,用 EventDb.restoreRowById 还原原行,
避免原系列残留幽灵行
- 加保存判定日志(脱敏)便于诊断
问题2(用户反馈):同步进行中再点手动同步/账号管理确定,提示"同步失败"造成误解。
根因:互斥锁抛的是普通 Error,三个调用点统一按"失败"提示。
修法:
- SyncEngine 新增 SyncBusyError 专用类型 + isSyncing/isAnySyncing 查询;
锁命中时抛 SyncBusyError,且 catch 不再把它包成普通 Error
- Index.syncSingle / AccountsPage.syncSingleAccount:命中→提示
"有同步任务正在进行中,请耐心等待",不显示失败
- Index.syncAll:命中记为 busySkipped(不算失败),全部被跳过时也只提示等待
This commit is contained in:
@@ -10,12 +10,49 @@ import { LogUtil } from './LogUtil';
|
||||
import { AppSettings } from './AppSettings';
|
||||
import { SystemCalendarMirror } from './SystemCalendarMirror';
|
||||
|
||||
/**
|
||||
* ⭐ "已有同步任务正在进行"专用错误类型。
|
||||
*
|
||||
* 背景(用户反馈):后台同步进行中时,用户再点「手动同步」或在账号管理点「保存并同步」,
|
||||
* 会并发触发第二轮同步;互斥锁把它拦下来,但那时抛的是普通 Error → 界面统一显示
|
||||
* "同步失败:该账号正在同步中,请稍后再试"。用户看到"失败"会误以为出了问题。
|
||||
*
|
||||
* 正确做法:并发被拦下时**不是失败**,而应提示"有同步任务正在进行中,请耐心等待"。
|
||||
* 因此用这个独立类型与真正的同步故障区分开,界面据此改提示语(而非报错)。
|
||||
*/
|
||||
export class SyncBusyError extends Error {
|
||||
constructor() {
|
||||
super('SYNC_BUSY');
|
||||
}
|
||||
}
|
||||
|
||||
export class SyncEngine {
|
||||
/** 正在同步中的账号 id → 开始时间:防止超时后的"僵尸同步"与新一轮同步并发写库 */
|
||||
private static activeSyncs: Map<string, number> = new Map();
|
||||
/** 互斥锁最长持有时间:超过视为异常残留(如进程挂起后网络停摆),允许抢占 */
|
||||
private static readonly LOCK_STALE_MS: number = 15 * 60 * 1000;
|
||||
|
||||
/** 某账号当前是否正在同步(供界面判断,不触发任何操作) */
|
||||
static isSyncing(accId: string): boolean {
|
||||
const heldSince: number | undefined = SyncEngine.activeSyncs.get(accId);
|
||||
if (heldSince === undefined) {
|
||||
return false;
|
||||
}
|
||||
return Date.now() - heldSince < SyncEngine.LOCK_STALE_MS;
|
||||
}
|
||||
|
||||
/** 当前是否有任意账号正在同步 */
|
||||
static isAnySyncing(): boolean {
|
||||
const now: number = Date.now();
|
||||
for (const id of SyncEngine.activeSyncs.keys()) {
|
||||
const since: number | undefined = SyncEngine.activeSyncs.get(id);
|
||||
if (since !== undefined && now - since < SyncEngine.LOCK_STALE_MS) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给异步操作加超时保护,防止网络挂起导致界面一直转圈
|
||||
*/
|
||||
@@ -38,7 +75,8 @@ export class SyncEngine {
|
||||
const heldSince: number | undefined = SyncEngine.activeSyncs.get(acc.id);
|
||||
if (heldSince !== undefined) {
|
||||
if (Date.now() - heldSince < SyncEngine.LOCK_STALE_MS) {
|
||||
throw new Error('该账号正在同步中,请稍后再试');
|
||||
// ⭐ 抛专用类型:界面据此提示"同步进行中,请耐心等待"而非"失败"
|
||||
throw new SyncBusyError();
|
||||
}
|
||||
LogUtil.write(`同步互斥锁超时残留(账号 id=${acc.id}),强制释放并重新同步`);
|
||||
}
|
||||
@@ -71,6 +109,10 @@ export class SyncEngine {
|
||||
LogUtil.write(`同步账号 id=${acc.id} 完成:拉取 ${r} 条日程,耗时 ${Math.round((Date.now() - t0) / 1000)} 秒`);
|
||||
return r;
|
||||
} catch (err) {
|
||||
// ⭐ "正在同步中"原样抛出(界面据此提示"请耐心等待",不当成失败)
|
||||
if (err instanceof SyncBusyError) {
|
||||
throw err;
|
||||
}
|
||||
const e = err as BusinessError;
|
||||
LogUtil.write(`同步账号 id=${acc.id} 失败:${e.message}(耗时 ${Math.round((Date.now() - t0) / 1000)} 秒)`);
|
||||
throw new Error(e.message !== '' ? e.message : `错误码 ${e.code}`);
|
||||
|
||||
Reference in New Issue
Block a user