Files
Clipboard/lib/echo-suppress.js
2026-09-12 13:59:12 +08:00

88 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* 剪贴板"回声抑制"状态机 —— 区分"应用自己写进剪贴板的内容"与"外部新复制"。
*
* 纯逻辑,不依赖 Electronmain.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 };