update
This commit is contained in:
87
lib/echo-suppress.js
Normal file
87
lib/echo-suppress.js
Normal file
@@ -0,0 +1,87 @@
|
||||
'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 };
|
||||
Reference in New Issue
Block a user