import type { ConfigDeps } from "../../deps.js"; import type { NotifyConfig, SettingsPatch } from "../model/type.js"; import type { SettingsView, WriteResult } from "./type.js"; /** 通知设置:本插件配置文件的唯一存取点。 */ declare class ConfigStore { /** 是否已装配;单例实例重复装配是编程错误,当场暴露。 */ private installed; /** 落盘路径:DSH home 由环境决定、进程内不变,故随实例一次性定下。 */ private readonly file; /** 装配入参(失败出口)。 */ private deps; /** 文件内容原样镜像:写回时以它为基底,才不会被一次保存抹掉不认识的键。 */ private stored; /** 用户层(净化后):写面做掩码还原、视图做回显都要它。 */ private user; /** 生效设置(**投递投影**):归一化补过默认值、收敛过越界,**不外发**——`current()` 拿它。 */ private effective; /** 视图投影(**外发**):键子集 + 原样 + 掩码,**不补默认值**。与投递投影是两份数据,见 view 模块的文件头。 */ private viewConfig; /** 用户层修订号(内容摘要):乐观并发的比较依据。 */ private revision; /** 写队列尾:新写挂在它后面,「读-改-写」不会交错。 */ private tail; /** 装配:读一次文件定下初值;此后只经 `write` 变更。 */ install(deps: ConfigDeps): void; /** 卸载:放开装配入参并丢掉用户层快照——它同时是「用户层」与「磁盘状态」的记忆。 */ release(): void; /** 当前生效设置(**投递投影**:补过默认值、收敛过越界;含明文凭据,**不外发**)。 * * pipeline / sdk / stores / dry-run 全靠它,故它一行不改地继续喂 `normalizeConfig` 的结果。 */ current(): NotifyConfig; /** 设置页视图:脱敏后的用户层与生效值 + 修订号 + 可写性,同一刻取齐。 */ view(): SettingsView; /** * 写:整段挂进写队列,队列内完成按字段合并(含掩码还原)→ 校验 → 落盘 → 刷新快照。 * * 为什么不把合并与校验留在队列外:两者都要读存量(合并沿用哪些键、判据据此决定重判哪些), * 队列外读到的存量会被并发的另一次写抽走。详见 `apply` 的注释。 * * `apply` 必须**直调 commit,不得再 enqueue**:enqueue 同步把 tail 设成 result.then(...), * task 内再 enqueue 就是等自己刚挂上去的 tail——那个 tail 排在自己后面,自己不返回它就不 resolve。 * 故 apply 与 commit 之间不经队列,是**结构上**写死的,不靠调用方记得。 */ write(patch: SettingsPatch, expectedRevision?: number): Promise; /** * 一次写的全过程:按字段合并(含掩码还原)→ 校验合并结果 → 落盘 → 刷新快照。 * * **整段在写队列内**,合并与校验都算在内(issue #1016 批次 B)。原来还原与校验在队列外, * 于是它们读到的存量是「入队那一刻」的:并发的两次写里,后一次会用前一次落盘**之前**的 * 用户层去还原掩码、判定「原样带回」,基线跟着另一次写漂走。把它们移进队列后,合并、判据 * 三者看到的是同一份存量快照。 * * 顺序不可换(#1016 S2):合并必须**先于**校验——判据审的是合并后的条目,而「删键之后必填键 * 此刻为空」这条判据只有合并之后才问得出;校验必须早于落盘,否则非法值会先写进文件。掩码在 * 合并里解(五态之一,见 merge 模块),不再有独立的还原步骤:两条路径各解一次会让「掩码在 * 哪一步变成原值」有两个答案,而判据看到的形态与落盘的形态就必须逐字一致。 * * 校验与合并之间不净化:磁盘上**存量**的陌生键不在提交里,合并原样沿用,一次保存抹不掉它们; * 而**本次提交**的陌生键由判据 400(#1016 S2 删掉了 extras 透传面)。0.2.3 的顶层渠道键已由 * upgrade 域在装配期搬走,写面收到它们会被校验直接拒(退役键清单),不在这里做二次翻译。 * 合并那一侧读哪份存量(磁盘原样,不是 effective)由 `mergePatchChannels` 的注释交代。 */ private apply; /** * 提交:版本比对 → 合并 → 原子落盘 → 采纳。 * * 整段在写队列内执行:比对与写入之间若能被另一次写插入,乐观并发就形同虚设 * ——两次写都读到同一旧版本、都判定通过,后写的把先写的悄悄覆盖。 * * `expectedRevision` 与合并结果的 `inherited` **语义不同,不许合并成一个参数**: * 前者是**内容摘要**比对,防的是「两个客户端同时改」的丢失更新(防的是覆盖); * 后者是**同 id 同 type 同名字段**的沿用清单,解的是「存量非法值不该因一次无关保存被拒」的 * 过度拒绝(防的是错杀)。两者一个按内容断版本、一个按字段认改动,混在一起会让任一条判据 * 悄悄变成另一条:把字段比对当版本号,用户改一个字段就凭空冲突;把版本号当字段比对, * 别的键变过就会让这次保存看起来「没改过」。 */ private commit; /** * 文件内容到达:镜像原样留下,**两份投影**由它各自派生(#1016 S3)。 * * 这一行是「读面拆两条通道」的落点:`effective` 走归一化(投递侧要完整形态),`viewConfig` 走 * 键子集投影(外发侧要磁盘原样)。两者曾共用一份数据,于是「补出来的默认值」被当成用户存过的值 * 交回磁盘——见 view 模块的文件头与 `view()` 的注释。 */ private adopt; /** 把一次写挂到队列尾;前一次无论成败,后一次都照常执行。 */ private enqueue; } /** 本域唯一的存取点实例:类不外放,外面 `new` 不出第二份设置状态。 */ export declare const configStore: ConfigStore; export {};