'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 };