88 lines
4.6 KiB
JavaScript
88 lines
4.6 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* 剪贴板"回声抑制"状态机 —— 区分"应用自己写进剪贴板的内容"与"外部新复制"。
|
||
*
|
||
* 纯逻辑,不依赖 Electron:main.js 的 pollClipboard / writePayloadToClipboard /
|
||
* initializeClipboardState 都用它。抽出来之前这份逻辑内联在 main.js 里,
|
||
* 测试只能克隆一份状态机自己测自己(main.js 改坏了测试照样绿)—— 而这是
|
||
* 丢数据级的逻辑,必须有真保护。规则都从真实踩坑里固化,改动前先看
|
||
* test/echo-suppress.test.js。
|
||
*
|
||
* 规则:
|
||
* - 记**内容**而不是记 flag:记下我们写进去的内容,下一次轮询真正读到同样
|
||
* 内容时才吞。占位 flag 会被"两次 tick 之间夹的外部复制"错误消费,
|
||
* 把那次外部复制静默吞掉(丢数据)。
|
||
* - 读到不匹配的新内容时顺手清掉 pending,避免它误伤之后内容相同的外部
|
||
* 复制(点复制 A → 外部复制 B → 外部又复制回 A,第二个 A 必须入库)。
|
||
* - 文本被清空('')*不* 消化 pending:外部中途清空剪贴板不该让回声标记
|
||
* 失效,否则用户接着复制同内容会重复入库。
|
||
* - 基线(last)随每次"变化"更新;自己写入时同时预置 pending 和 last,
|
||
* 让下一个 tick 走"无变化"分支,连读图/编码的代价都不付。
|
||
* 为此 markOwnWrite 返回一个 undo 闭包:**写入抛错时必须调用它**——
|
||
* 失败时剪贴板上还是旧内容,基线却已预置成新值,不回滚的话下个 tick
|
||
* 会把真实的旧内容当成"新变化"再入一次库(旧内容早已在库里 → 重复条目)。
|
||
*/
|
||
function createEchoSuppress() {
|
||
let last = null; // 上次见到的内容(变化检测基线);null = 还没见过
|
||
let pending = null; // 我们自己写进去、等着"被读到"后吞掉的内容;null = 无
|
||
return {
|
||
/** 启动采样:记住当前剪贴板内容当基线,不当新内容、不标记回声。 */
|
||
seed(value) { last = value; },
|
||
|
||
/**
|
||
* 应用自己写剪贴板:预置基线(下一 tick 判"无变化")+ 记下回声内容。
|
||
* 返回 undo 闭包:实际写入(clipboard.writeText / writeImage)抛错时
|
||
* 调用它以回滚基线、清掉回声标记(见文件头最后一条规则)。
|
||
*/
|
||
markOwnWrite(value) {
|
||
const prevLast = last;
|
||
pending = value;
|
||
last = value;
|
||
return () => { pending = null; last = prevLast; };
|
||
},
|
||
|
||
/** 写入失败后调用:清回声标记,别吞掉用户接下来的真实复制。 */
|
||
clearPending() { pending = null; },
|
||
|
||
/** 1×1/空图出现时重置基线(poll 侧按"没东西"处理,下一次任何图都算新)。 */
|
||
resetBaseline() { last = null; },
|
||
|
||
/**
|
||
* pollClipboard 入库失败时调用:撤销上一次 observe 对基线/pending 的修改,
|
||
* 让下一次 tick 重新探测同一条内容。
|
||
*
|
||
* 背景:observe 一旦判定 'new',就已经把 last 设成新值;如果跟着的
|
||
* stmt.insertText / insertImage 抛错(SQLite 临时锁、磁盘满、字段非法……),
|
||
* 不回滚的话下一次 tick 读到同一条内容会判 'unchanged',**那条内容被
|
||
* 永久吞掉** —— 直到用户重启(initializeClipboardState 重新 seed 基线)
|
||
* 才能恢复。回滚后下次 tick 会再判 'new' 再试一次入库,符合预期。
|
||
*
|
||
* 一次 observe 只对应一次入库尝试:last 在入库失败之前已被 observe 改过,
|
||
* 但 pending 通常仍是 null(自己写入是 markOwnWrite 走的另一条路)——
|
||
* 不区分地统一置空足够,逻辑简单也避免回退到 observe 之前的精确快照。
|
||
*/
|
||
abortLastObserve() { last = null; pending = null; },
|
||
|
||
/**
|
||
* 喂入一次轮询读到的内容。
|
||
* @returns {'unchanged'|'empty'|'suppressed'|'new'}
|
||
* unchanged —— 与基线相同(文本:调用方继续图片分支;图片:直接 return)
|
||
* empty —— 空串,剪贴板被清空(仅文本;不消化 pending)
|
||
* suppressed —— 是我们自己写进去的回声,已吞
|
||
* new —— 外部新内容,调用方应入库
|
||
*/
|
||
observe(value) {
|
||
if (value === last) return 'unchanged';
|
||
last = value;
|
||
if (value === '') return 'empty';
|
||
if (pending !== null && value === pending) { pending = null; return 'suppressed'; }
|
||
// 不匹配的新内容顺手消化 pending(见文件头规则 2)
|
||
if (pending !== null) pending = null;
|
||
return 'new';
|
||
},
|
||
};
|
||
}
|
||
|
||
module.exports = { createEchoSuppress };
|