/** * `InMemoryApprovalAskStore` —— [ref] 车1,`ApprovalAskStore` 的语义真源(twin 对拆基准)。单进程内两个 * `Map`,方法体全同步(不含 `await` 中断点)—— * Node 单线程下这就是天然原子:`Promise.all` 发起的并发调用不会在方法体内部交错执行,与 * `approval-ask-store-sql.ts` 的 CAS UPDATE 语义逐字段一致(SQL twin 对拆套件的基准就是这份实现)。 * * 🔴 **S-384:本类同时是 `FileApprovalAskStore` 的内核**(local 车道的持久形不再是「第二份手抄」—— * 它把本类**原样**包起来,每次写前先把**调用本身**落 JSONL、重放时用同一只内核重放同一串调用)。 * 为此本类多了三件、且**只**多这三件(全是数据面,零转移判定): * · {@link InMemoryApprovalAskStore.constructor} 的可注入钟 `now` —— 五个动词(decideAsk / claimTerminal / * expireAsk / bindBatch / abortBatch)原先直读 `Date.now()`,那是重放路上唯一的不确定源:同一串调用 * 重放出来的 `updatedAtMs`/`decidedAtMs` 必须与当初**逐字节**相同,否则「重放等价」是假的。 * ⚠️ **钟只许在方法体的同步前缀里读**(第一个 `await` 之前)—— File 形据此把钟钉在一次调用的同步窗口 * 内,窗口外读钟会被它的钟函数**响亮拒**(不是静默回落 `Date.now()`)。今天五个动词全部满足; * 哪天有人把读钟挪到 `await` 之后,File 形的钟当场抛,而不是悄悄写出一条重放不出来的行。 * · {@link InMemoryApprovalAskStore.exportRows} / {@link InMemoryApprovalAskStore.importRows} —— 压实用的 * 整本快照口(纯搬行,不经任何谓词)。压实要把「一串调用」折成「一串终局行」,没有这一对就只能在 * File 形里另写一套「哪些行还算数」的判据 —— 那才是第二份手抄。 * * 行为口径与 `SqlApprovalAskStore` 逐条对齐: * - ensureAsk 幂等 upsert(ask_id 已在 ⇒ 返回既有行,不改写;batch 行若不存在则以 OPEN 补齐)。 * - transitionAsk 先过 machine 层 `canAskTransition`(非法转移 throw,不返回 false)。 * - decideAsk/expireAsk/bindBatch/abortBatch 的双/三步判定顺序与 SQL twin 的事务步骤一一对应 * (哪一步先判、判什么、失败时是否触碰另一半状态——照抄,不是「反正内存里原子就随便」)。 * - resolveProvisional 故意不经 canAskTransition(同 SQL twin 头注:这是版本化补偿的例外通道)。 */ import { type AskState, type BatchState } from "../approval-ask-machine.js"; import type { AskDecision, AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, EnsureAskResult, DecideResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js"; /** * [ref] 的 `updatedInput` **入店编码**——两 twin 共用的那一只(本函数是唯一真源)。 * * 语义:镜像 SQL twin 的「存 JSON 文本再解回来」(`dialectJsonEncoder` → `parseJsonColumn`)。BigInt / * 循环引用 ⇒ `JSON.stringify` 抛(两侧同抛);顶层函数 / `undefined` ⇒ `stringify` 交出 `undefined` * (「编出来什么都不是」)⇒ 本函数**响亮拒**,而不是让它在库里变成 NULL、在内存里变成活函数。 * * 🔴 为什么必须是一只**导出**函数而不是 `decideAsk` 里的内联段(S-384):`FileApprovalAskStore` 要在 * **落日志之前**把这一步做掉 —— 它的日志本身就是 JSON,而 `JSON.stringify` 对顶层函数是**静默丢键** * 不是抛。内联在 decideAsk 里的话,File 形只能在自己那侧再写一遍同样的判定(第二份手抄),或者 * 让「活路径抛 / 重放路径成功 DECIDE」这条真分岔活下来。 * * ⚠️ 它与 File 形对**整条调用**做的那次 JSON round-trip 是两件事,别合并:round-trip 保证的是 * 「内核看见的 == 重放看见的」(把静默丢键变成两边一致);本函数保证的是「编出来什么都不是 ⇒ 响亮拒」, * 而那条判据只在 `updatedInput` 这一格上成立 —— 它的**缺席**与**值为 null** 在 wire 上是两回事 * (`AskRow.updatedInput` 三态),所以「被 round-trip 静默丢成缺席」在这一格上不是无害的归一。 */ export declare function encodeStoreJsonPayload(what: string, value: unknown): unknown; /** [ref] `updatedInput` 的入店编码 = 上面那只的一次具名调用(词固定在一处,两 twin 与 File 形账本共用)。 */ export declare function encodeDecideUpdatedInput(updatedInput: unknown): unknown; /** {@link InMemoryApprovalAskStore.exportRows} 的产出 / {@link InMemoryApprovalAskStore.importRows} 的入参 * —— **纯行**,零判定。压实(S-384 File 形)与测试的状态对拆共用。 */ export interface ApprovalAskRowsSnapshot { asks: readonly AskRow[]; batches: readonly BatchRow[]; } export declare class InMemoryApprovalAskStore implements ApprovalAskStore { private readonly asks; private readonly batches; /** 店钟(S-384)。缺省 = 本进程墙钟,与本类落地以来逐字同值;File 形注入一只**钉住的**钟,让 * 「同一串调用重放出同一份状态」成立。读钟纪律见类顶注(只许在同步前缀里读)。 */ private readonly now; constructor(opts?: { now?: () => number; }); /** * S-384:整本行快照(压实用)。**深拷到行级**(与每个读口交出 `{ ...row }` 同一条纪律:交出去的行 * 不再是店内那一份,调用方改它碰不到店)。行内的 `cardJson`/`updatedInput`/`decisionActor` 仍是引用 —— * 与 `getAsk`/`listByState` 逐字同宽,不在这一口上单独加深拷(那会让快照口与读口对同一份 blob 给出 * 两种可变性)。 */ exportRows(): ApprovalAskRowsSnapshot; /** S-384:把快照里的行**原样**装回两张 Map(同键覆盖)。零谓词、零状态机 —— 它不是一条转移,是重放 * 的起手状态。压实产物与本口是一对:压实写出 {@link exportRows} 的行,启动时由本口装回。 */ importRows(snapshot: { asks?: readonly AskRow[]; batches?: readonly BatchRow[]; }): void; ensureAsk(row: NewAskRow): Promise; transitionAsk(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise; decideAsk(askId: string, batchId: string, decision: DecideAskInput): Promise; /** * [ref] 原子终局 claim(接口契约见 SQL 侧 {@link ApprovalAskStore.claimTerminal})。**委托形**:CAS 走 * 既有 verb(expire ⇒ `this.expireAsk`;cancel ⇒ `this.transitionAsk(STREAM_PENDING→VOID)`)——律 2 * 「一台机器」在本 twin 的落笔处是**字面同一个函数做写**(零第二套谓词);委托也让测试对 verb 的 * 实例级故障注入(`store.expireAsk = throwing` 等)对 claim 恒有效,注入面不因原语换名而漂移。 * 原子性:方法体的判定与读回之间唯一的 await 是对**同步体** verb 的调用(本店头注的单线程天然原子 * 论证);await 后读到的行 ≥ CAS 线性化点,而非 pending 态无回边 ⇒ 败方读数自足(与 SQL 侧同一条 * 单调性论证)。 */ claimTerminal(askId: string, batchId: string, intent: AskTerminalClaimIntent): Promise; expireAsk(askId: string, batchId: string): Promise; bindBatch(batchId: string, askId: string, gate: BindGateInput): Promise; deferReconcile(askId: string, expectedState: AskState, expectedRev: number, nowMs: number): Promise; abortBatch(batchId: string): Promise; listByState(state: AskState, limit: number): Promise; /** `signal` 的 twin 语义(车3 刀 3b):同步实现里没有「在飞」这回事,所以能做也只需做**入口复核** —— * 已 abort 就一条都不读、如实抛,与 SQL twin 的第①条语义逐字一致(第②条在这里天然平凡成立)。 */ listPendingByTask(taskId: string, signal?: AbortSignal): Promise; listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise; getAsk(askId: string): Promise; getByIdempotencyKey(taskId: string, key: string): Promise; resolveProvisional(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise; deleteByTask(taskId: string): Promise; getBatch(batchId: string): Promise; } export type { AskDecision, AskState, BatchState }; //# sourceMappingURL=approval-ask-store-memory.d.ts.map