@zhushanwen/session-delivery 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +148 -95
- package/dist/index.mjs +108 -83
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -9,12 +9,11 @@
|
|
|
9
9
|
* v2 增量(投递所有权内核,v2 设计批次——设计文档未入 git,裁决随本仓 commit
|
|
10
10
|
* 历史可溯):
|
|
11
11
|
* 条目状态机/lane/条目双形态(活跃条目 + 终局记录)/entriesFull()+projection() 双视图;行为实现在
|
|
12
|
-
* 本包 src/delivery.ts。lane 字面量中的 'steer'
|
|
13
|
-
* /D1),非 pi API 词汇——intent →
|
|
12
|
+
* 本包 src/delivery.ts。lane 字面量中的 'steer' 是投递路径语义名(设计 §1.1 术语表
|
|
13
|
+
* /D1),非 pi API 词汇——intent → 底层参数的翻译在适配器内部,本包只记录投递路径,
|
|
14
14
|
* 零依赖纪律对 lane 不破例。
|
|
15
15
|
*
|
|
16
|
-
* v3 增量(统一消息投递内核,设计 .tmp/tech-design/unified-delivery.md §3.5
|
|
17
|
-
* 纯增量零行为变化——装配不传新参数 = v2 行为逐字节不变):
|
|
16
|
+
* v3 增量(统一消息投递内核,设计 .tmp/tech-design/unified-delivery.md §3.5):
|
|
18
17
|
* - DeliveryStore 持久化存储端口(persist / ack / discard / loadUndelivered /
|
|
19
18
|
* loadTerminalIds / hasTerminal):耐久承诺先于投递尝试;终局写持久终局;判重
|
|
20
19
|
* 第三级查持久记录。两个实现在两半区适配层(runtime 半区磁盘 outbox / port 半区
|
|
@@ -28,8 +27,8 @@
|
|
|
28
27
|
* 已完结 id 索引(loadTerminalIds 播种 + 终局即时登记)→ 持久记录(store.hasTerminal)。
|
|
29
28
|
* entriesFull() 的 tombstones 视图字段与结构保持(双视图消费方零改动),元素类型
|
|
30
29
|
* 由 DeliveryTerminalRecord 承载。
|
|
31
|
-
* -
|
|
32
|
-
*
|
|
30
|
+
* - 受理即出站(唯一出站形态,v2 队列持有分支已删):受理后立即 port.send,无
|
|
31
|
+
* 忙闲门禁、无合批窗口(合批归 port 提交侧承载)。
|
|
33
32
|
*/
|
|
34
33
|
/** 投递意图(D3):turn 边界抢占 / run 结束后注入,均含 idle 唤醒语义。 */
|
|
35
34
|
type DeliveryIntent = 'interrupt-at-turn-boundary' | 'after-run';
|
|
@@ -119,7 +118,7 @@ type SendReceipt = {
|
|
|
119
118
|
interface DeliveryPort {
|
|
120
119
|
/** 本通路支持的 payload kind(D9 fail-fast)。 */
|
|
121
120
|
supportedPayloads: readonly DeliveryPayload['kind'][];
|
|
122
|
-
/** 主 agent
|
|
121
|
+
/** 主 agent 是否空闲(受理即出站下内核不读——port 契约与诊断面保留)。 */
|
|
123
122
|
isIdle(): boolean;
|
|
124
123
|
/**
|
|
125
124
|
* 投递消息(intent → pi 参数的翻译在适配器内部)。
|
|
@@ -132,12 +131,15 @@ interface DeliveryPort {
|
|
|
132
131
|
* 处置 = 用户显式失败 + 重启应用。
|
|
133
132
|
*/
|
|
134
133
|
send(msg: DeliveryMessage, intent: DeliveryIntent): Promise<SendReceipt | void> | SendReceipt | void;
|
|
135
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* agent_settled 边沿订阅(D8):边沿即 flush 触发(retain 失败留守条目的重投时机)。
|
|
136
|
+
* 缺省时留守条目归外部 flush/send 触发。返回退订函数。
|
|
137
|
+
*/
|
|
136
138
|
subscribeSettled?(cb: () => void): () => void;
|
|
137
139
|
}
|
|
138
140
|
/**
|
|
139
141
|
* 送达凭据三档(v3,ADR-0074 词条扩展;提交方申报制,缺省 'marker'):
|
|
140
|
-
* - 'marker'
|
|
142
|
+
* - 'marker':出站文本尾附裸标记、送达以回执命中为准(文本投递路径主通路)。
|
|
141
143
|
* - 'acceptance':受理即送达——无回执锚点条目的「受理 = 送达」是唯一可判事实,
|
|
142
144
|
* port.send 受理成功即落 delivered 终态(v1 记账口径 / D9⑤ 显式例外)。
|
|
143
145
|
* - 'details-id'(v3 新增):通知条目送达判定 = 对话记录中出现 details 携带本条目
|
|
@@ -150,19 +152,25 @@ interface DeliveryPort {
|
|
|
150
152
|
type DeliveryReceiptAnchor = 'marker' | 'acceptance' | 'details-id';
|
|
151
153
|
/**
|
|
152
154
|
* 失败保留策略(v3):port.send 失败(首败,ADR-0122 无内核自动重试)时条目的处置。
|
|
153
|
-
* - 'forget':首败即停 +
|
|
155
|
+
* - 'forget':首败即停 + 显式上报后从内核移除(缺省 text 现状;重试决策归
|
|
154
156
|
* 消费方——人看页面通知可重发)。store 已 persist 的记录同步 discard(失败条目
|
|
155
157
|
* 无判重语义,防重启后被 loadUndelivered 复活成死条目)。
|
|
156
158
|
* - 'retain':失败条目留守——内存态转回 queued 等下一 settled 边沿 / 外部 flush
|
|
157
159
|
* 重入队(事件驱动,无定时器);store 记录保留(不 ack 不 discard),重启后经
|
|
158
|
-
* loadUndelivered
|
|
160
|
+
* loadUndelivered 重入队(通知投递路径无人值守语义,与账本现状同构)。
|
|
161
|
+
* - 'failed'(chat-flow-consistency 第 11 条):受理失败条目转 **failed 终态留守**——
|
|
162
|
+
* 不自动重投(无 retain 的边沿重投,等用户处置:resync 重试 / cancel 撤销)、
|
|
163
|
+
* 不 store.discard(重启后经 loadUndelivered 重放,重放带 transcript 判重防线),
|
|
164
|
+
* 与断连 failInFlight 的 failed 同态同出路(队列区一行 + 重试入口双呈现)。
|
|
159
165
|
*
|
|
160
166
|
* 缺省按载荷类型:custom = 'retain' / text = 'forget'(§3.4 提交选项缺省值表:
|
|
161
|
-
* 交互在席 vs 无人值守),提交方可显式覆写。sendChecked
|
|
162
|
-
*
|
|
163
|
-
*
|
|
167
|
+
* 交互在席 vs 无人值守),提交方可显式覆写。sendChecked 挂起条目缺省/显式 'retain'
|
|
168
|
+
* 按 'forget' 处置(同步失败回执契约与 retain 边沿重投矛盾——调用方已收到失败,
|
|
169
|
+
* 自动重投会产生「失败但送达」的矛盾观测;一次性 warn);显式 'failed' 放行——
|
|
170
|
+
* waiter reject 是受理口径通知,failed 留守是条目状态,两者不矛盾(failed 不自动
|
|
171
|
+
* 重投,「失败但送达」观测不存在)。
|
|
164
172
|
*/
|
|
165
|
-
type DeliveryFailurePolicy = 'forget' | 'retain';
|
|
173
|
+
type DeliveryFailurePolicy = 'forget' | 'retain' | 'failed';
|
|
166
174
|
/**
|
|
167
175
|
* 送达凭据检查函数(v3,'details-id' 凭据的判定承载形态):判定「对话记录中是否已
|
|
168
176
|
* 存在 details 携带该 id 的通知条目」。由装配方注入实现(实现读对话记录——pi 半区
|
|
@@ -178,7 +186,7 @@ type DeliveryProofCheck = (id: string) => boolean;
|
|
|
178
186
|
interface DeliveryStoreRecord {
|
|
179
187
|
/** 客户端幂等 id(= 条目 id)。 */
|
|
180
188
|
id: string;
|
|
181
|
-
/**
|
|
189
|
+
/** 投递路径(重入队时按原路径重报)。 */
|
|
182
190
|
lane: DeliveryLane;
|
|
183
191
|
/** 载荷(原样)。 */
|
|
184
192
|
payload: DeliveryPayload;
|
|
@@ -194,16 +202,20 @@ interface DeliveryStoreRecord {
|
|
|
194
202
|
meta?: DeliveryMeta;
|
|
195
203
|
}
|
|
196
204
|
/**
|
|
197
|
-
* 持久化存储端口(v3
|
|
205
|
+
* 持久化存储端口(v3,统一语言词条「持久化存储端口」):内核与耐久层的唯一接口。
|
|
198
206
|
* 两个实现在两半区适配层(本包零 fs 零 pi 知识):runtime 半区 = 磁盘 outbox 文件
|
|
199
|
-
*
|
|
207
|
+
* (文本投递路径,波 4 接线);port 半区 = session entry 读写(通知投递路径,波 2 接线)。
|
|
200
208
|
*
|
|
201
209
|
* 内核行为契约(设计 §3.5.1):
|
|
202
210
|
* - persist:send 受理前调用(耐久承诺先于投递尝试,与账本 record 先于 deliver 同
|
|
203
211
|
* 律)。persist 抛错 = 承诺未落 → 内核拒收该条目(send 返回 persist-failed /
|
|
204
212
|
* sendChecked 同步 throw),不进入投递流程。
|
|
205
|
-
* - ack:终局 delivered / cancelled
|
|
206
|
-
*
|
|
213
|
+
* - ack:终局 delivered / cancelled 时调用(写持久终局——已撤销消息的防复活凭据
|
|
214
|
+
* 同样在持久终局记录里)。磁盘权威(2026-10-30 用户裁决「内存终态必须以磁盘确认为
|
|
215
|
+
* 准」):ack 先于内存终态登记,抛错 = 持久终局未写入 → 内核不登记内存终态(条目
|
|
216
|
+
* 留守活跃集非终态)并显式上报分型错误 DeliveryAckPersistError;外部送达事实不
|
|
217
|
+
* 回滚,恢复后可能重投(at-least-once 显式接受)。实现方须遵守同序:自身内存派生
|
|
218
|
+
* 数据只在落盘成功后登记。
|
|
207
219
|
* - discard:forget 失败条目的记录无痕移除(无终局无判重语义——区别于 ack,不进
|
|
208
220
|
* 终局记录,防重启后 loadUndelivered 把已弃条目当未投递复活)。
|
|
209
221
|
* - loadUndelivered:供重启 / 重放时重入队(装配方启动时取回未终局记录重新提交)。
|
|
@@ -220,7 +232,7 @@ interface DeliveryStoreRecord {
|
|
|
220
232
|
interface DeliveryStore {
|
|
221
233
|
/** 受理前写入记录(耐久承诺先于投递尝试)。抛错 = 承诺未写入,内核拒收。 */
|
|
222
234
|
persist(record: DeliveryStoreRecord): void;
|
|
223
|
-
/** 终局(delivered / cancelled
|
|
235
|
+
/** 终局(delivered / cancelled)写持久终局(从待投差集移除、进终局记录)。抛错 = 持久终局未写入 → 内核不登记内存终态(磁盘权威,见上方契约)。 */
|
|
224
236
|
ack(id: string): void;
|
|
225
237
|
/** forget 失败条目的记录无痕移除(无终局形态,区别于 ack)。 */
|
|
226
238
|
discard(id: string): void;
|
|
@@ -235,11 +247,6 @@ interface DeliveryStore {
|
|
|
235
247
|
interface DeliveryConfig {
|
|
236
248
|
/** 默认意图:'interrupt-at-turn-boundary'(D3)。 */
|
|
237
249
|
intent?: DeliveryIntent;
|
|
238
|
-
/** 合批窗口(ms):0 = 关;>0 = 滑动窗口合批。 */
|
|
239
|
-
mergeWindowMs?: number;
|
|
240
|
-
/** 合批依赖谓词(D4 must-fix #1)。true 时 send() 走合批窗口,false/缺省时立即投。
|
|
241
|
-
* 禁止用 isIdle 代替。 */
|
|
242
|
-
mergeHoldActive?: () => boolean;
|
|
243
250
|
/** 去重配置(条数 LRU)。 */
|
|
244
251
|
dedupe?: {
|
|
245
252
|
maxKeys: number;
|
|
@@ -258,7 +265,7 @@ interface DeliveryConfig {
|
|
|
258
265
|
* (受理 ≠ 送达——SendReceipt 注释口径的机制化落地)。**显式例外:sendChecked 的
|
|
259
266
|
* 同步 settle 维持受理口径不变**——session_manager send 的「立即受理确认」契约
|
|
260
267
|
* 锚定在受理时点,后移到送达会让 agent 工具调用阻塞至目标 session 当前 turn 结束
|
|
261
|
-
* (steer
|
|
268
|
+
* (steer 类投递路径可达数十秒)。本注释即接口口径权威声明。
|
|
262
269
|
*
|
|
263
270
|
* outcome 'rejected' = 失败终局通知(ADR-0122 首败即停:send 失败一次即移除条目并
|
|
264
271
|
* 逐条回调;断连事件 failInFlight 转 failed 终态同样逐条回调)。重试决策归消费方,
|
|
@@ -269,7 +276,7 @@ interface DeliveryConfig {
|
|
|
269
276
|
*/
|
|
270
277
|
onSettled?: (msg: DeliveryMessage, outcome: 'delivered' | 'rejected') => void;
|
|
271
278
|
/**
|
|
272
|
-
* 持久化存储端口(v3)。缺省 undefined =
|
|
279
|
+
* 持久化存储端口(v3)。缺省 undefined = 无耐久层(v2 行为:纯内存条目集)——
|
|
273
280
|
* 既有 runtime 装配不传 = 逐字节不变(波 1 零行为变化锚)。提供时内核行为契约见
|
|
274
281
|
* DeliveryStore(受理前 persist / 终局 ack / forget 失败 discard / 构造期
|
|
275
282
|
* loadTerminalIds 播种判重索引)。
|
|
@@ -280,25 +287,11 @@ interface DeliveryConfig {
|
|
|
280
287
|
* 缺省 undefined = details-id 条目无法经 confirmDeliveredByProof 确认(留守 in-flight)。
|
|
281
288
|
*/
|
|
282
289
|
proofCheck?: DeliveryProofCheck;
|
|
283
|
-
/**
|
|
284
|
-
* 出站运行模式(v3,受理即出站 / 队列持有的模式参数;设计 §3.5.6):
|
|
285
|
-
* - 'queue-hold'(缺省)= v2 队列持有行为:busy gate(isIdle + 在途内查)排队 +
|
|
286
|
-
* settled 边沿驱动 flush + 合批窗口出站。
|
|
287
|
-
* - 'on-accept' = 受理即出站:受理后立即 port.send(忙闲门禁旁路——承重前提 =
|
|
288
|
-
* 底层唯一队列忙时不拒,适配层保证);合批窗口不用(队列唯一化后无组批时机,
|
|
289
|
-
* 合批归 port 提交侧承载),config.mergeWindowMs>0 与提交方 merge:true 被忽略;
|
|
290
|
-
* in-flight 防重保留(内核自身并发保护,非忙闲门禁);retain 失败留守条目仍经
|
|
291
|
-
* settled 边沿 / 外部 flush 重投。
|
|
292
|
-
* runtime 装配不传 = v2 队列持有行为不变(波 1 只实现不接线)。
|
|
293
|
-
*/
|
|
294
|
-
dispatchMode?: 'queue-hold' | 'on-accept';
|
|
295
290
|
}
|
|
296
291
|
/** 投递句柄(createDelivery 返回)。 */
|
|
297
292
|
interface DeliveryHandle {
|
|
298
|
-
/** 唯一常规入口(D4
|
|
299
|
-
send(msg: DeliveryMessage
|
|
300
|
-
merge?: boolean;
|
|
301
|
-
}): void;
|
|
293
|
+
/** 唯一常规入口(D4 入口收敛);受理即出站立即投。 */
|
|
294
|
+
send(msg: DeliveryMessage): void;
|
|
302
295
|
/** 入队 + 可达性同步确认。reject = 入队失败。 */
|
|
303
296
|
sendChecked(msg: DeliveryMessage): Promise<void>;
|
|
304
297
|
/** 强制投递尝试(shutdown flush / 外部重触发 / settled 边沿内部复用)。 */
|
|
@@ -309,8 +302,8 @@ interface DeliveryHandle {
|
|
|
309
302
|
dispose(): void;
|
|
310
303
|
}
|
|
311
304
|
/**
|
|
312
|
-
*
|
|
313
|
-
* 派生视图 +
|
|
305
|
+
* 投递路径(D1)。lane 在条目创建时由 runtime 内核适配层判定(读权威 occupancy
|
|
306
|
+
* 派生视图 + 内核自身队列态,单一判定源)后记录,此后不变。三档为投递路径语义名
|
|
314
307
|
* (设计 §1.1 术语表),非底层通道 API 词汇。
|
|
315
308
|
*/
|
|
316
309
|
type DeliveryLane = 'direct' | 'steer' | 'queued';
|
|
@@ -322,6 +315,11 @@ type DeliveryLane = 'direct' | 'steer' | 'queued';
|
|
|
322
315
|
* - in-flight/queued → cancelled:用户撤销(delivery.cancel)
|
|
323
316
|
* - in-flight → failed:断连事件驱动的未确认终局(failInFlight,ADR-0122——pi 连接
|
|
324
317
|
* 断开时结果未知,显式失败交用户处置)
|
|
318
|
+
* - queued → failed:出站投递受理失败(onFailure='failed',chat-flow-consistency
|
|
319
|
+
* 第 11 条——文本投递受理失败留守 failed 行,与断连未确认同出路,等用户处置)
|
|
320
|
+
* - failed → delivered:确定性送达事实(resync transcript 预检命中 / 迟到送达回执,
|
|
321
|
+
* 第 11 条歧义性终结——首败时文本可能已进 transcript,delivered 事实终结 failed
|
|
322
|
+
* 猜测,行随事实消失)
|
|
325
323
|
* - failed → queued:用户重试(resync 单条重报);failed → cancelled:用户移除
|
|
326
324
|
* 帧派生视图四态差异:cancelled 不进 session.delivery 帧(D5③),协议侧帧 state 无此值。
|
|
327
325
|
*/
|
|
@@ -335,7 +333,7 @@ interface DeliveryEntry {
|
|
|
335
333
|
/** 客户端幂等 id(= 协议层 clientUuid;裸标记身份源 D2,判重锚 D5②)。 */
|
|
336
334
|
id: string;
|
|
337
335
|
state: DeliveryEntryState;
|
|
338
|
-
/**
|
|
336
|
+
/** 投递路径(创建时判定后记录,不变,D1)。 */
|
|
339
337
|
lane: DeliveryLane;
|
|
340
338
|
/** 载荷(一期仅 text,DeliveryPayload 联合保留扩展位)。 */
|
|
341
339
|
payload: DeliveryPayload;
|
|
@@ -394,22 +392,25 @@ interface DeliveryEntriesProjection {
|
|
|
394
392
|
/**
|
|
395
393
|
* 投递所有权内核实现:createDelivery(v2)。
|
|
396
394
|
*
|
|
397
|
-
* 在 v1
|
|
398
|
-
* 驱动)之上升级为条目制所有权内核(v2 设计批次 D9①②③⑤ / D5②③ / D3,单元 u1——
|
|
395
|
+
* 在 v1 投递循环之上升级为条目制所有权内核(v2 设计批次 D9①②③⑤ / D5②③ / D3,单元 u1——
|
|
399
396
|
* 设计文档未入 git,裁决随本仓 commit 历史可溯)。零 pi 依赖不变:确认/回收由适配器(runtime
|
|
400
397
|
* registry)调 handle 方法驱动。
|
|
401
398
|
*
|
|
402
|
-
* 五态状态机(D9①,types.ts 迁移表)+
|
|
399
|
+
* 五态状态机(D9①,types.ts 迁移表)+ 本文件登记的实施扩展:
|
|
403
400
|
* - queued → in-flight:出站投递被受理(两阶段回执第一阶段,D2)
|
|
404
401
|
* - in-flight → delivered:confirmDelivered(送达回执,D2)
|
|
405
402
|
* - in-flight → queued:requeue(对账回收重投,D3)
|
|
406
403
|
* - queued/failed → cancelled:cancel(本地移除 + tombstone / 用户 × 移除)
|
|
407
404
|
* - in-flight → failed:断连事件驱动的未确认终局(failInFlight,ADR-0122)
|
|
405
|
+
* - queued → failed:出站投递受理失败留守(onFailure='failed',chat-flow-consistency
|
|
406
|
+
* 第 11 条——与断连未确认同出路,等用户处置)
|
|
408
407
|
* - failed → queued:requeue(用户重试/resync 单条重报)
|
|
409
|
-
* - [扩展①] queued → delivered:confirmDelivered 也接受 queued
|
|
410
|
-
* 场景 rebuild 直确认重建(D3②:适配器扫描 transcript
|
|
411
|
-
* + confirmDelivered
|
|
412
|
-
*
|
|
408
|
+
* - [扩展①] queued/failed → delivered:confirmDelivered 也接受 queued 与 failed
|
|
409
|
+
* 态——queued 服务 reattach 场景 rebuild 直确认重建(D3②:适配器扫描 transcript
|
|
410
|
+
* 判 delivered 后 send 重建 + confirmDelivered 一步落终态),以及出站批次在途时
|
|
411
|
+
* 送达回执先于受理回执到达的竞态形态(message_end 比 RPC resolve 快,事实优先);
|
|
412
|
+
* failed 服务第 11 条受理失败的歧义性终结——迟到的送达回执 / resync 的 transcript
|
|
413
|
+
* 预检命中即确定性送达事实,行随事实消失。
|
|
413
414
|
* - [扩展②] cancel 对 in-flight 两段式:首次调用只标记待收回(条目留守 in-flight,
|
|
414
415
|
* 返回 reclaim-requested,适配器驱动 clear_queue 收回);适配器收回确认后再次
|
|
415
416
|
* cancel(id) 终结为 cancelled + tombstone。收回失败不再调 → 条目留守原态
|
|
@@ -420,11 +421,14 @@ interface DeliveryEntriesProjection {
|
|
|
420
421
|
* 合批分段中途失败时已登记段留守 in-flight(等回执/对账),仅未受理段按失败处理。
|
|
421
422
|
*
|
|
422
423
|
* 失败语义(ADR-0122 首败即停,2026-10-05 用户裁决,backoff 自动重试链退役):
|
|
423
|
-
* port.send 失败(含 accepted:false
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
424
|
+
* port.send 失败(含 accepted:false)一次即收尾——处置按条目 onFailure 策略分流:
|
|
425
|
+
* forget 条目 checked reject + 从内核移除;非 checked forget 条目 onSettled('rejected')
|
|
426
|
+
* 逐条显式上报 + 从内核移除;failed 条目(chat-flow-consistency 第 11 条)转 failed
|
|
427
|
+
* 终态留守(不自动重投,等用户处置:resync 重试 / cancel 撤销),checked waiter
|
|
428
|
+
* reject 照常(受理口径通知与条目状态正交)。唯一例外:拒绝回执为 deferred 形态
|
|
429
|
+
* (设计内等待,DeliveryDeferReason——如 port 合批提交窗 hold)不计首败,条目留守
|
|
430
|
+
* 等投递端下一边沿重提(onSendDeferred)。重试决策归消费方(人看页面通知、agent
|
|
431
|
+
* 收失败回执),内核只做忠实投递与事实上报。
|
|
428
432
|
*
|
|
429
433
|
* onSettled 记账口径(D9⑤ 升级):
|
|
430
434
|
* - 'delivered' = 送达口径:仅 confirmDelivered 驱动回调(受理只转 in-flight,
|
|
@@ -456,39 +460,39 @@ interface DeliveryEntriesProjection {
|
|
|
456
460
|
* 终局(store.ack)。条目 id = opts.id ?? 内部生成;未传 id 的消息不参与幂等判重
|
|
457
461
|
* (v1「无 dedupe 配置不去重」语义保持)。
|
|
458
462
|
*
|
|
459
|
-
* v3
|
|
463
|
+
* v3 增量(统一消息投递内核,设计 unified-delivery §3.5):
|
|
460
464
|
* - DeliveryStore 端口(config.store):send/sendChecked 受理前先 persist(耐久承诺
|
|
461
465
|
* 先于投递尝试;persist 抛错 = 承诺未落 → 拒收 rejected('persist-failed'),条目
|
|
462
|
-
* 回滚不入队不投递);终局 delivered / cancelled
|
|
463
|
-
* forget 失败条目 discard(无终局语义的记录无痕移除,防重启复活)。ack
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
+
* 回滚不入队不投递);终局 delivered / cancelled 时 ack(finalizeEntry / drain);
|
|
467
|
+
* forget 失败条目 discard(无终局语义的记录无痕移除,防重启复活)。ack 先于内存
|
|
468
|
+
* 终态登记(磁盘权威,2026-10-30 用户裁决「内存终态必须以磁盘确认为准」):写盘
|
|
469
|
+
* 失败 → 内存不登记终态(条目留守活跃集非终态)+ 分型错误 DeliveryAckPersistError
|
|
470
|
+
* 显式上报——外部送达事实不回滚,恢复后可能重投(at-least-once 显式接受);判重
|
|
471
|
+
* 查询(hasTerminal)异常保守放行 + warn;persist 异常 fail-fast 拒收(承诺语义)。
|
|
466
472
|
* - 送达凭据三档(receiptAnchor):'details-id' 与 'marker' / 'acceptance' 并列
|
|
467
473
|
* (ADR-0074 词条扩展)。details-id 条目受理后维持 in-flight 等回执(同 marker);
|
|
468
474
|
* 判定逻辑以可注入凭据检查函数(config.proofCheck)承载,调用点 =
|
|
469
475
|
* handle.confirmDeliveredByProof(id)——装配方在回执证据到达时调用,内核按条目
|
|
470
476
|
* 凭据档位分派(非 details-id 条目一律拒绝,防误确认)。
|
|
471
|
-
* - 失败保留策略(opts.onFailure:'forget' | 'retain',缺省 custom=retain /
|
|
477
|
+
* - 失败保留策略(opts.onFailure:'forget' | 'retain' | 'failed',缺省 custom=retain /
|
|
472
478
|
* text=forget):retain 条目首败后内存态转回 queued 留守(下一 settled 边沿 /
|
|
473
479
|
* 外部 flush 重入队,事件驱动无定时器;失败轮不主动重投——防 idle 同步失败循环),
|
|
474
|
-
* store 记录保留(重启经 loadUndelivered 重入队);forget
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
* flush
|
|
483
|
-
*
|
|
484
|
-
* 清理波(登记:docs/todo/unified-delivery-residuals.md #1)。
|
|
480
|
+
* store 记录保留(重启经 loadUndelivered 重入队);forget 条目维持现状首败即停;
|
|
481
|
+
* failed 条目转 failed 终态留守(无自动重投,等用户 resync 重试 / cancel 撤销——
|
|
482
|
+
* chat-flow-consistency 第 11 条)。sendChecked 挂起条目缺省/显式 retain 按 forget
|
|
483
|
+
* 处置(同步失败回执契约与 retain 重投矛盾);显式 failed 放行(reject 是受理口径
|
|
484
|
+
* 通知,failed 留守是条目状态,不自动重投故无矛盾观测)。
|
|
485
|
+
* - 受理即出站(v3,唯一出站形态):受理后立即 port.send(忙闲门禁旁路——承重
|
|
486
|
+
* 前提 = 底层唯一队列忙时不拒,适配层保证);无合批窗口(队列唯一化后无组批
|
|
487
|
+
* 时机,合批归 port 提交侧承载);in-flight 防重保留(内核自身并发保护);
|
|
488
|
+
* retain 失败留守条目经 settled 边沿 / 外部 flush 重投(事件驱动,无定时器)。
|
|
489
|
+
* v2 队列持有形态(busy gate 排队 + 合批窗口)已随队列持有分支退役删除。
|
|
485
490
|
*
|
|
486
|
-
* 其余 v1/v2 机制逐条保持:
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
* reject)。[ADR-0122 退役登记] backoff 自动重试链、port.send settle 挂死兜底
|
|
491
|
+
* 其余 v1/v2 机制逐条保持:settled 边沿驱动(send / flush / 边沿统一出站触发)、
|
|
492
|
+
* dedupe LRU、dispose 语义(丢弃不触发 onSettled、checked 挂起 reject)。
|
|
493
|
+
* [ADR-0122 退役登记] backoff 自动重试链、port.send settle 挂死兜底
|
|
490
494
|
* (60s)、watchdog 30s 定时复核、无订阅装配 busy 退避轮询——时间平抑/补偿类机制
|
|
491
|
-
*
|
|
495
|
+
* 全部删除。depth() 口径保持 v1 = 尚未受理的
|
|
492
496
|
* 消息数(受理转 in-flight 后不计;「在途未确认」数经 entriesFull() 全量视图消费)。
|
|
493
497
|
*/
|
|
494
498
|
|
|
@@ -504,17 +508,15 @@ interface DeliveryWarnSink {
|
|
|
504
508
|
}
|
|
505
509
|
/** createDelivery 配置(DeliveryConfig + warn 出口注入)。 */
|
|
506
510
|
type DeliveryConfigWithWarn = DeliveryConfig & DeliveryWarnSink;
|
|
507
|
-
/** send/sendChecked
|
|
511
|
+
/** send/sendChecked 提交选项。 */
|
|
508
512
|
interface DeliverySubmitOptions {
|
|
509
|
-
/** 合批窗口判定覆盖(v1 既有)。 */
|
|
510
|
-
merge?: boolean;
|
|
511
513
|
/**
|
|
512
514
|
* 客户端幂等 id(= 条目 id = 协议层 clientUuid,D2 裸标记身份源/D5② 判重锚)。
|
|
513
515
|
* 显式提供时参与幂等判重:命中活跃条目或 tombstone 即吞(resync 重报/重放去重)。
|
|
514
516
|
* 缺省内部生成、不参与判重。
|
|
515
517
|
*/
|
|
516
518
|
id?: string;
|
|
517
|
-
/**
|
|
519
|
+
/** 投递路径(D1):适配器判定后传入记录;缺省 'direct'(既有调用方语义)。 */
|
|
518
520
|
lane?: DeliveryLane;
|
|
519
521
|
/**
|
|
520
522
|
* 送达回执锚申报(msg-pipeline-debloat D1 申报制;v3 三档,ADR-0074 词条扩展):
|
|
@@ -529,8 +531,8 @@ interface DeliverySubmitOptions {
|
|
|
529
531
|
* 后维持 in-flight 等回执(同 'marker'),终局由装配方调 handle.confirmDeliveredByProof
|
|
530
532
|
* 驱动(判定逻辑经 config.proofCheck 注入,内核零 pi 知识)。
|
|
531
533
|
*
|
|
532
|
-
|
|
533
|
-
|
|
534
|
+
* 缺省 'marker' 是刻意的保守取向:无标记提交点漏申报的代价 = 复发已知死锁形态
|
|
535
|
+
* (in-flight 永挂——条目无人确认终态,有全链诊断可查,可见);误申报 'acceptance' 的代价
|
|
534
536
|
* = 静默丢送达确认语义(更隐蔽)。两害取可见者。新增无标记投递来源必须显式申报
|
|
535
537
|
* 'acceptance' 或 'details-id' 并登记来源清单(ADR-0074 词条)。
|
|
536
538
|
*/
|
|
@@ -538,13 +540,16 @@ interface DeliverySubmitOptions {
|
|
|
538
540
|
/**
|
|
539
541
|
* 失败保留策略(v3):port.send 首败时本条目的处置(ADR-0122 无内核自动重试不变
|
|
540
542
|
* ——retain 不是重试机制,是失败条目的留存策略)。
|
|
541
|
-
* - 'forget' = 首败即停 +
|
|
542
|
-
* 的记录同步 discard)。
|
|
543
|
+
* - 'forget' = 首败即停 + 显式上报后移除;store 已 persist 的记录同步 discard。
|
|
543
544
|
* - 'retain' = 失败条目留守:内存态转回 queued 等下一 settled 边沿 / 外部 flush
|
|
544
545
|
* 重入队;store 记录保留(重启经 loadUndelivered 重入队)。
|
|
546
|
+
* - 'failed' = 受理失败转 failed 终态留守(chat-flow-consistency 第 11 条):不自动
|
|
547
|
+
* 重投、不 discard,等用户处置(resync 重试 / cancel 撤销),与断连 failInFlight
|
|
548
|
+
* 的 failed 同态同出路。
|
|
545
549
|
* 缺省按载荷类型:custom = 'retain' / text = 'forget'(§3.4 缺省值表:交互在席 vs
|
|
546
|
-
* 无人值守)。sendChecked
|
|
547
|
-
*
|
|
550
|
+
* 无人值守)。sendChecked 挂起条目缺省/显式 retain 按 forget 处置(同步失败回执
|
|
551
|
+
* 契约与 retain 重投矛盾;一次性 warn);显式 failed 放行——waiter reject 是受理
|
|
552
|
+
* 口径通知,failed 留守是条目状态,两者不矛盾(failed 不自动重投)。
|
|
548
553
|
*/
|
|
549
554
|
onFailure?: DeliveryFailurePolicy;
|
|
550
555
|
/**
|
|
@@ -610,7 +615,7 @@ interface DeliveryHandleV2 extends DeliveryHandle {
|
|
|
610
615
|
* 的条目生效——调 config.proofCheck(id)(对话记录中出现 details 携带本条目 id 的
|
|
611
616
|
* 通知条目的判定,由装配方注入实现),true → confirmDelivered(终局化 + onSettled
|
|
612
617
|
* 回调),false → 留守。非 details-id 条目(marker / acceptance 有各自判定通路)、
|
|
613
|
-
* 未注入 proofCheck、proofCheck 抛错(降级 warn
|
|
618
|
+
* 未注入 proofCheck、proofCheck 抛错(降级 warn 视为未送达,保守放行型降级)时
|
|
614
619
|
* 一律返回 false 不转移。装配方在回执证据到达(message_end 等)或重放扫描时对本
|
|
615
620
|
* 批候选条目调用,内核保证凭据档位分派(误确认在结构上不可达)。
|
|
616
621
|
* 幂等:未知/已终态 no-op。@returns 是否发生了状态转移。
|
|
@@ -642,7 +647,21 @@ interface DeliveryHandleV2 extends DeliveryHandle {
|
|
|
642
647
|
* 待收回(扩展②两段式);已终态幂等返回既有 tombstone。
|
|
643
648
|
*/
|
|
644
649
|
cancel(id: string): DeliveryCancelResult;
|
|
645
|
-
/**
|
|
650
|
+
/**
|
|
651
|
+
* 原位替换条目内容(chat-flow-consistency D6 edit 原语):语义 = 替换指定条目的 payload
|
|
652
|
+
* 且**条目 id 与排队位置均不变**——上层只见「替换内容,位置保持」,收回-全部-保序重投是
|
|
653
|
+
* registry 编排层组合 requeue 的内部细节,本原语不感知 pi。
|
|
654
|
+
* 接受 queued(未触达底层通道,替换后出站即新文本;持有期排队的常态)/ failed(留守行
|
|
655
|
+
* 编辑,替换后由调用方 requeue 重投)/ in-flight(registry 收回路径:clear_queue 已把
|
|
656
|
+
* 文本收回后、requeue 重投前的窗口内替换)。拒绝:终态/未知 id(false)、queued 但已在
|
|
657
|
+
* 出站批内(受理登记窗口——文本可能已进底层通道,替换会双投,调用方按悬空窗口处置)。
|
|
658
|
+
* msg.payload 同步替换引用(onSettled per-message 契约的消费方拿到替换后内容)。
|
|
659
|
+
*/
|
|
660
|
+
edit(id: string, payload: DeliveryPayload): boolean;
|
|
661
|
+
/**
|
|
662
|
+
* 全量取回文本(forceQuit,D9③):条目清空 + 全部记 cancelled tombstone(ack 写盘
|
|
663
|
+
* 失败的条目除外——文本照常返回,条目转 failed 留守,见 drain 实现的磁盘权威序注释)。
|
|
664
|
+
*/
|
|
646
665
|
drain(): DrainResult;
|
|
647
666
|
}
|
|
648
667
|
/** cancel(id) 结果(D-3 偏差:cancelled 形态携带条目快照含 payload 全文供草稿恢复)。 */
|
|
@@ -676,7 +695,7 @@ declare function createDelivery(port: DeliveryPort, config?: DeliveryConfigWithW
|
|
|
676
695
|
*
|
|
677
696
|
* 判别形态依据(设计 §3.3 D4 尾注):本包与消费方(runtime registry)是进程内同包
|
|
678
697
|
* 调用边界——编译期类型可得,instanceof 是最简判别且免词表;与跨进程 WS 边界的
|
|
679
|
-
*
|
|
698
|
+
* error envelope 分类码形态不构成双轨。
|
|
680
699
|
*/
|
|
681
700
|
/**
|
|
682
701
|
* 「用户主动回收」对挂起 sendChecked waiter 的 reject 错误类。
|
|
@@ -698,6 +717,28 @@ declare class DeliveryReclaimError extends Error {
|
|
|
698
717
|
readonly entryId: string | undefined;
|
|
699
718
|
constructor(kind: 'cancelled' | 'drained', entryId?: string);
|
|
700
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* 「终局事实已发生,但持久终局(store.ack)写入失败」的分型错误(2026-10-30 用户裁决
|
|
722
|
+
* 「磁盘为准」:内存终态必须以磁盘确认为准)。
|
|
723
|
+
*
|
|
724
|
+
* 产生点(delivery.ts ackStore):confirmDelivered / cancel / drain 终局化时先写持久
|
|
725
|
+
* 终局,store.ack 抛错即包装为本类——内核不登记内存终态(条目留守活跃集非终态,判重
|
|
726
|
+
* 与恢复以磁盘为唯一权威),由各终局入口捕获留痕并显式上报。
|
|
727
|
+
*
|
|
728
|
+
* 语义边界:外部送达事实不可回滚(消息已实际发出)——写盘失败期间该条不视为已终态,
|
|
729
|
+
* 恢复(重启重放 / 下一回执边沿重驱动)后可能重投,at-least-once 语义被显式接受
|
|
730
|
+
* (宁可可能重投,不做内存单方终态)。
|
|
731
|
+
*
|
|
732
|
+
* 结构化字段:
|
|
733
|
+
* - entryId:终局写入失败的条目 id。
|
|
734
|
+
* - state:本应落定的终态('delivered' | 'cancelled')——文案按它分叉:delivered 强调
|
|
735
|
+
* 送达事实已成立;cancelled 强调本地撤销意图已发生但持久撤销凭据未落。
|
|
736
|
+
*/
|
|
737
|
+
declare class DeliveryAckPersistError extends Error {
|
|
738
|
+
readonly entryId: string;
|
|
739
|
+
readonly state: 'delivered' | 'cancelled';
|
|
740
|
+
constructor(entryId: string, state: 'delivered' | 'cancelled', cause?: unknown);
|
|
741
|
+
}
|
|
701
742
|
|
|
702
743
|
/**
|
|
703
744
|
* 磁盘 outbox —— DeliveryStore 的 runtime 半区实现(统一消息投递设计 §3.5.1 / §3.8
|
|
@@ -721,6 +762,8 @@ declare class DeliveryReclaimError extends Error {
|
|
|
721
762
|
* 设计 §3.5.1「两个实现在两半区适配层」);目录与文件路径由装配方(runtime 侧)动态
|
|
722
763
|
* 推导后以字符串传入。崩溃写入的中途半行由读取容错覆盖(JSON.parse 失败 warn 跳过)
|
|
723
764
|
* ——最坏丢一行 = 最坏多投一次,与「必达优先于去重」语义一致,不需要原子替换协议。
|
|
765
|
+
* 读失败(文件存在但不可读)显式分型上抛 DiskStoreReadError,不折叠成空结果(见
|
|
766
|
+
* ensureLoaded 注释);文件不存在 = 合法空态。
|
|
724
767
|
*/
|
|
725
768
|
|
|
726
769
|
/** fs 窄接口(注入点;生产 = node:fs 适配,测试 = 内存替身)。 */
|
|
@@ -752,6 +795,16 @@ interface DiskDeliveryStore extends DeliveryStore {
|
|
|
752
795
|
*/
|
|
753
796
|
attachSegments(id: string, segments: readonly unknown[]): void;
|
|
754
797
|
}
|
|
798
|
+
/**
|
|
799
|
+
* outbox 读失败错误(显式分型:读失败 ≠ 空文件)。文件存在但不可读(权限 / 瞬时 fs
|
|
800
|
+
* 故障)时由 ensureLoaded 抛出——判别形态与 DeliveryReclaimError 同理(进程内同包
|
|
801
|
+
* 调用边界,instanceof 最简判别)。
|
|
802
|
+
*/
|
|
803
|
+
declare class DiskStoreReadError extends Error {
|
|
804
|
+
/** outbox 文件路径(诊断定位)。 */
|
|
805
|
+
readonly file: string;
|
|
806
|
+
constructor(file: string, cause?: unknown);
|
|
807
|
+
}
|
|
755
808
|
declare function createDiskDeliveryStore(deps: DiskDeliveryStoreDeps): DiskDeliveryStore;
|
|
756
809
|
|
|
757
|
-
export { type CustomPayload, type DeliveryCancelResult, type DeliveryConfig, type DeliveryConfigWithWarn, type DeliveryDeferReason, type DeliveryEntriesFull, type DeliveryEntriesProjection, type DeliveryEntry, type DeliveryEntryState, type DeliveryFailurePolicy, type DeliveryHandle, type DeliveryHandleV2, type DeliveryIntent, type DeliveryLane, type DeliveryMessage, type DeliveryMeta, type DeliveryPayload, type DeliveryPort, type DeliveryProofCheck, type DeliveryReceiptAnchor, DeliveryReclaimError, type DeliverySendResult, type DeliveryStore, type DeliveryStoreRecord, type DeliverySubmitOptions, type DeliveryTerminalRecord, type DeliveryWarnSink, type DiskDeliveryStore, type DiskDeliveryStoreDeps, type DiskStoreFs, type DrainResult, type SendReceipt, type TextPayload, createDelivery, createDiskDeliveryStore };
|
|
810
|
+
export { type CustomPayload, DeliveryAckPersistError, type DeliveryCancelResult, type DeliveryConfig, type DeliveryConfigWithWarn, type DeliveryDeferReason, type DeliveryEntriesFull, type DeliveryEntriesProjection, type DeliveryEntry, type DeliveryEntryState, type DeliveryFailurePolicy, type DeliveryHandle, type DeliveryHandleV2, type DeliveryIntent, type DeliveryLane, type DeliveryMessage, type DeliveryMeta, type DeliveryPayload, type DeliveryPort, type DeliveryProofCheck, type DeliveryReceiptAnchor, DeliveryReclaimError, type DeliverySendResult, type DeliveryStore, type DeliveryStoreRecord, type DeliverySubmitOptions, type DeliveryTerminalRecord, type DeliveryWarnSink, type DiskDeliveryStore, type DiskDeliveryStoreDeps, type DiskStoreFs, DiskStoreReadError, type DrainResult, type SendReceipt, type TextPayload, createDelivery, createDiskDeliveryStore };
|
package/dist/index.mjs
CHANGED
|
@@ -9,6 +9,19 @@ var DeliveryReclaimError = class extends Error {
|
|
|
9
9
|
this.entryId = entryId;
|
|
10
10
|
}
|
|
11
11
|
};
|
|
12
|
+
var DeliveryAckPersistError = class extends Error {
|
|
13
|
+
entryId;
|
|
14
|
+
state;
|
|
15
|
+
constructor(entryId, state, cause) {
|
|
16
|
+
super(
|
|
17
|
+
state === "delivered" ? `delivery delivered but terminal record not persisted; recovery may redeliver (id=${entryId})` : `entry cancelled locally but terminal record not persisted; recovery may redeliver (id=${entryId})`,
|
|
18
|
+
{ cause }
|
|
19
|
+
);
|
|
20
|
+
this.name = "DeliveryAckPersistError";
|
|
21
|
+
this.entryId = entryId;
|
|
22
|
+
this.state = state;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
12
25
|
|
|
13
26
|
// src/lru.ts
|
|
14
27
|
var LruSet = class {
|
|
@@ -43,8 +56,7 @@ var LruSet = class {
|
|
|
43
56
|
|
|
44
57
|
// src/delivery.ts
|
|
45
58
|
var DEFAULT_CONFIG = {
|
|
46
|
-
intent: "interrupt-at-turn-boundary"
|
|
47
|
-
mergeWindowMs: 0
|
|
59
|
+
intent: "interrupt-at-turn-boundary"
|
|
48
60
|
};
|
|
49
61
|
var DEFAULT_DELIVERED_WINDOW = 50;
|
|
50
62
|
function isThenable(v) {
|
|
@@ -103,14 +115,6 @@ function settleChecked(batch, err, checkedPending) {
|
|
|
103
115
|
checkedPending.push(...kept);
|
|
104
116
|
return rejected;
|
|
105
117
|
}
|
|
106
|
-
function safeIsIdle(port, onFault) {
|
|
107
|
-
try {
|
|
108
|
-
return port.isIdle();
|
|
109
|
-
} catch (err) {
|
|
110
|
-
onFault?.(err);
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
118
|
function resolveWarnSink(config) {
|
|
115
119
|
return config?.warn ?? ((msg, err) => {
|
|
116
120
|
console.warn(`[session-delivery] ${msg}`, err ?? "");
|
|
@@ -126,16 +130,13 @@ function createDelivery(port, config) {
|
|
|
126
130
|
const terminalIndex = /* @__PURE__ */ new Set();
|
|
127
131
|
const terminalRecords = /* @__PURE__ */ new Map();
|
|
128
132
|
let checkedRetainWarned = false;
|
|
129
|
-
let mergeIgnoredWarned = false;
|
|
130
133
|
const deliveredLog = [];
|
|
131
134
|
const checkedPending = [];
|
|
132
135
|
let inflightBatch = [];
|
|
133
136
|
let inFlight = false;
|
|
134
|
-
let mergeTimer;
|
|
135
137
|
let disposed = false;
|
|
136
138
|
let settledUnsub;
|
|
137
139
|
let missingKeyWarned = false;
|
|
138
|
-
let probeFaultWarned = false;
|
|
139
140
|
let idSeq = 0;
|
|
140
141
|
const dedupSet = config?.dedupe ? new LruSet(config.dedupe.maxKeys) : null;
|
|
141
142
|
const changeSubs = /* @__PURE__ */ new Set();
|
|
@@ -156,12 +157,12 @@ function createDelivery(port, config) {
|
|
|
156
157
|
meta: e.msg.meta
|
|
157
158
|
});
|
|
158
159
|
}
|
|
159
|
-
function ackStore(id) {
|
|
160
|
+
function ackStore(id, state) {
|
|
160
161
|
if (!store) return;
|
|
161
162
|
try {
|
|
162
163
|
store.ack(id);
|
|
163
164
|
} catch (err) {
|
|
164
|
-
|
|
165
|
+
throw new DeliveryAckPersistError(id, state, err);
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
function safeHasTerminal(id) {
|
|
@@ -207,19 +208,6 @@ function createDelivery(port, config) {
|
|
|
207
208
|
warn(`onSettled callback threw (outcome=${outcome}); isolated from delivery state machine`, err);
|
|
208
209
|
}
|
|
209
210
|
}
|
|
210
|
-
function warnProbeFault(err) {
|
|
211
|
-
if (probeFaultWarned) return;
|
|
212
|
-
probeFaultWarned = true;
|
|
213
|
-
warn("port busy probe (isIdle) threw; conservatively treating as busy", err);
|
|
214
|
-
}
|
|
215
|
-
function isBusySafe() {
|
|
216
|
-
if (!safeIsIdle(port, warnProbeFault)) return true;
|
|
217
|
-
return hasInFlightEntries();
|
|
218
|
-
}
|
|
219
|
-
function hasInFlightEntries() {
|
|
220
|
-
for (const e of active) if (e.state === "in-flight") return true;
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
211
|
function inRegistry(e) {
|
|
224
212
|
return activeIndex.get(e.id) === e;
|
|
225
213
|
}
|
|
@@ -228,6 +216,7 @@ function createDelivery(port, config) {
|
|
|
228
216
|
return false;
|
|
229
217
|
}
|
|
230
218
|
function finalizeEntry(e, state) {
|
|
219
|
+
ackStore(e.id, state);
|
|
231
220
|
const batchIdx = inflightBatch.indexOf(e);
|
|
232
221
|
if (batchIdx !== -1) inflightBatch.splice(batchIdx, 1);
|
|
233
222
|
const idx = active.indexOf(e);
|
|
@@ -239,7 +228,6 @@ function createDelivery(port, config) {
|
|
|
239
228
|
e.settledAt = ts;
|
|
240
229
|
terminalIndex.add(e.id);
|
|
241
230
|
terminalRecords.set(e.id, { id: e.id, state, lane: e.lane, settledAt: ts });
|
|
242
|
-
ackStore(e.id);
|
|
243
231
|
if (state === "delivered") {
|
|
244
232
|
resolveWaitersOf(e);
|
|
245
233
|
deliveredLog.push(e);
|
|
@@ -247,6 +235,25 @@ function createDelivery(port, config) {
|
|
|
247
235
|
}
|
|
248
236
|
notifyChange();
|
|
249
237
|
}
|
|
238
|
+
function parkAsFailed(e) {
|
|
239
|
+
const batchIdx = inflightBatch.indexOf(e);
|
|
240
|
+
if (batchIdx !== -1) inflightBatch.splice(batchIdx, 1);
|
|
241
|
+
const ts = now();
|
|
242
|
+
e.state = "failed";
|
|
243
|
+
e.updatedAt = ts;
|
|
244
|
+
e.settledAt = ts;
|
|
245
|
+
notifyChange();
|
|
246
|
+
}
|
|
247
|
+
function cancelFinalize(e) {
|
|
248
|
+
try {
|
|
249
|
+
finalizeEntry(e, "cancelled");
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (!(err instanceof DeliveryAckPersistError)) throw err;
|
|
252
|
+
warn(`store.ack failed; cancel not terminalized, entry parked failed (id=${e.id})`, err);
|
|
253
|
+
parkAsFailed(e);
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
250
257
|
function removeActive(e) {
|
|
251
258
|
const idx = active.indexOf(e);
|
|
252
259
|
if (idx !== -1) active.splice(idx, 1);
|
|
@@ -268,30 +275,11 @@ function createDelivery(port, config) {
|
|
|
268
275
|
}
|
|
269
276
|
}
|
|
270
277
|
}
|
|
271
|
-
function clearMergeTimer() {
|
|
272
|
-
if (mergeTimer !== void 0) {
|
|
273
|
-
clearTimeout(mergeTimer);
|
|
274
|
-
mergeTimer = void 0;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
function armMergeTimer() {
|
|
278
|
-
if (cfg.mergeWindowMs <= 0) return;
|
|
279
|
-
mergeTimer = setTimeout(() => {
|
|
280
|
-
mergeTimer = void 0;
|
|
281
|
-
flush();
|
|
282
|
-
}, cfg.mergeWindowMs);
|
|
283
|
-
}
|
|
284
|
-
function resetMergeTimer() {
|
|
285
|
-
clearMergeTimer();
|
|
286
|
-
armMergeTimer();
|
|
287
|
-
}
|
|
288
278
|
function ensureSettledSub() {
|
|
289
279
|
if (settledUnsub || !port.subscribeSettled) return;
|
|
290
280
|
settledUnsub = port.subscribeSettled(() => {
|
|
291
281
|
if (disposed) return;
|
|
292
|
-
|
|
293
|
-
flush();
|
|
294
|
-
}
|
|
282
|
+
flush();
|
|
295
283
|
});
|
|
296
284
|
}
|
|
297
285
|
function teardownSettledSub() {
|
|
@@ -374,6 +362,7 @@ function createDelivery(port, config) {
|
|
|
374
362
|
if (e.onFailure === "retain") continue;
|
|
375
363
|
callOnSettled(e.msg, "rejected");
|
|
376
364
|
}
|
|
365
|
+
const ts = now();
|
|
377
366
|
for (const e of batch) {
|
|
378
367
|
if (!inRegistry(e)) continue;
|
|
379
368
|
if (e.onFailure === "retain") {
|
|
@@ -384,6 +373,14 @@ function createDelivery(port, config) {
|
|
|
384
373
|
warn(`entry retained after failure (id=${e.id}); re-dispatch waits for next settled edge or external flush`);
|
|
385
374
|
continue;
|
|
386
375
|
}
|
|
376
|
+
if (e.onFailure === "failed") {
|
|
377
|
+
e.state = "failed";
|
|
378
|
+
e.updatedAt = ts;
|
|
379
|
+
e.settledAt = ts;
|
|
380
|
+
changed = true;
|
|
381
|
+
warn(`entry kept as failed after rejection (id=${e.id}); awaiting user retry (resync) or cancel`);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
387
384
|
store?.discard(e.id);
|
|
388
385
|
removeActive(e);
|
|
389
386
|
changed = true;
|
|
@@ -430,17 +427,12 @@ function createDelivery(port, config) {
|
|
|
430
427
|
function scheduleFlush() {
|
|
431
428
|
if (disposed || !hasQueuedEntries()) return;
|
|
432
429
|
if (inFlight) return;
|
|
433
|
-
if (cfg.dispatchMode !== "on-accept" && isBusySafe()) return;
|
|
434
430
|
doSend();
|
|
435
431
|
}
|
|
436
432
|
const warnSink = resolveWarnSink(config);
|
|
437
433
|
function warn(msg, err) {
|
|
438
434
|
warnSink(msg, err);
|
|
439
435
|
}
|
|
440
|
-
if (config?.dispatchMode === "on-accept" && (config.mergeWindowMs ?? 0) > 0 && !mergeIgnoredWarned) {
|
|
441
|
-
mergeIgnoredWarned = true;
|
|
442
|
-
warn("dispatchMode=on-accept ignores mergeWindowMs (batching moves to submit side)");
|
|
443
|
-
}
|
|
444
436
|
function passDedupe(msg) {
|
|
445
437
|
if (!dedupSet) return true;
|
|
446
438
|
if (!msg.dedupeKey) {
|
|
@@ -511,13 +503,6 @@ function createDelivery(port, config) {
|
|
|
511
503
|
if (!persistBeforeAccept(entry)) {
|
|
512
504
|
return { kind: "rejected", reason: "persist-failed" };
|
|
513
505
|
}
|
|
514
|
-
const useMerge = cfg.dispatchMode !== "on-accept" && (opts?.merge ?? (cfg.mergeWindowMs > 0 && cfg.mergeHoldActive != null && cfg.mergeHoldActive()));
|
|
515
|
-
if (useMerge) {
|
|
516
|
-
resetMergeTimer();
|
|
517
|
-
ensureSettledSub();
|
|
518
|
-
return { kind: "accepted", id: entry.id };
|
|
519
|
-
}
|
|
520
|
-
clearMergeTimer();
|
|
521
506
|
ensureSettledSub();
|
|
522
507
|
scheduleFlush();
|
|
523
508
|
return { kind: "accepted", id: entry.id };
|
|
@@ -535,7 +520,7 @@ function createDelivery(port, config) {
|
|
|
535
520
|
if (opts?.onFailure === "retain") {
|
|
536
521
|
if (!checkedRetainWarned) {
|
|
537
522
|
checkedRetainWarned = true;
|
|
538
|
-
warn("sendChecked ignores onFailure=retain (checked entries
|
|
523
|
+
warn("sendChecked ignores onFailure=retain (checked entries default to forget)");
|
|
539
524
|
}
|
|
540
525
|
opts = { ...opts, onFailure: "forget" };
|
|
541
526
|
}
|
|
@@ -555,7 +540,6 @@ function createDelivery(port, config) {
|
|
|
555
540
|
}
|
|
556
541
|
function flush() {
|
|
557
542
|
if (disposed) return;
|
|
558
|
-
clearMergeTimer();
|
|
559
543
|
scheduleFlush();
|
|
560
544
|
}
|
|
561
545
|
function depth() {
|
|
@@ -585,8 +569,14 @@ function createDelivery(port, config) {
|
|
|
585
569
|
}
|
|
586
570
|
const e = activeIndex.get(id);
|
|
587
571
|
if (!e) return false;
|
|
588
|
-
if (e.state !== "queued" && e.state !== "in-flight") return false;
|
|
589
|
-
|
|
572
|
+
if (e.state !== "queued" && e.state !== "in-flight" && e.state !== "failed") return false;
|
|
573
|
+
try {
|
|
574
|
+
finalizeEntry(e, "delivered");
|
|
575
|
+
} catch (err) {
|
|
576
|
+
if (!(err instanceof DeliveryAckPersistError)) throw err;
|
|
577
|
+
warn(`store.ack failed; terminal not registered, entry stays non-terminal (id=${id})`, err);
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
590
580
|
if (!disposed) callOnSettled(e.msg, "delivered");
|
|
591
581
|
return true;
|
|
592
582
|
}
|
|
@@ -673,13 +663,25 @@ function createDelivery(port, config) {
|
|
|
673
663
|
return { kind: "reclaim-requested", entry: snapshot(e) };
|
|
674
664
|
}
|
|
675
665
|
rejectWaitersOf(e, new DeliveryReclaimError("cancelled", id));
|
|
676
|
-
|
|
666
|
+
cancelFinalize(e);
|
|
677
667
|
return { kind: "cancelled", entry: snapshot(e) };
|
|
678
668
|
}
|
|
679
669
|
rejectWaitersOf(e, new DeliveryReclaimError("cancelled", id));
|
|
680
|
-
|
|
670
|
+
cancelFinalize(e);
|
|
681
671
|
return { kind: "cancelled", entry: snapshot(e) };
|
|
682
672
|
}
|
|
673
|
+
function edit(id, payload) {
|
|
674
|
+
if (disposed) return false;
|
|
675
|
+
const e = activeIndex.get(id);
|
|
676
|
+
if (!e) return false;
|
|
677
|
+
if (e.state !== "queued" && e.state !== "in-flight" && e.state !== "failed") return false;
|
|
678
|
+
if (e.state === "queued" && inflightBatch.includes(e)) return false;
|
|
679
|
+
e.payload = payload;
|
|
680
|
+
e.msg.payload = payload;
|
|
681
|
+
e.updatedAt = now();
|
|
682
|
+
notifyChange();
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
683
685
|
function failInFlight(reason) {
|
|
684
686
|
if (disposed) return 0;
|
|
685
687
|
const ts = now();
|
|
@@ -702,19 +704,29 @@ function createDelivery(port, config) {
|
|
|
702
704
|
}
|
|
703
705
|
function drain() {
|
|
704
706
|
if (disposed) return [];
|
|
705
|
-
clearMergeTimer();
|
|
706
707
|
const drained = active.slice();
|
|
707
|
-
|
|
708
|
-
|
|
708
|
+
const ts = now();
|
|
709
|
+
const result = [];
|
|
710
|
+
for (const e of drained) {
|
|
711
|
+
try {
|
|
712
|
+
ackStore(e.id, "cancelled");
|
|
713
|
+
const idx = active.indexOf(e);
|
|
714
|
+
if (idx !== -1) active.splice(idx, 1);
|
|
715
|
+
activeIndex.delete(e.id);
|
|
716
|
+
e.state = "cancelled";
|
|
717
|
+
e.updatedAt = ts;
|
|
718
|
+
e.settledAt = ts;
|
|
719
|
+
terminalIndex.add(e.id);
|
|
720
|
+
terminalRecords.set(e.id, { id: e.id, state: "cancelled", lane: e.lane, settledAt: ts });
|
|
721
|
+
} catch (err) {
|
|
722
|
+
if (!(err instanceof DeliveryAckPersistError)) throw err;
|
|
723
|
+
warn(`store.ack failed in drain; entry not terminalized, parked failed (id=${e.id})`, err);
|
|
724
|
+
parkAsFailed(e);
|
|
725
|
+
}
|
|
726
|
+
result.push({ id: e.id, payload: e.payload });
|
|
727
|
+
}
|
|
709
728
|
inflightBatch = [];
|
|
710
729
|
inFlight = false;
|
|
711
|
-
const ts = now();
|
|
712
|
-
const result = drained.map((e) => {
|
|
713
|
-
terminalIndex.add(e.id);
|
|
714
|
-
terminalRecords.set(e.id, { id: e.id, state: "cancelled", lane: e.lane, settledAt: ts });
|
|
715
|
-
ackStore(e.id);
|
|
716
|
-
return { id: e.id, payload: e.payload };
|
|
717
|
-
});
|
|
718
730
|
for (const w of checkedPending) {
|
|
719
731
|
w.reject(new DeliveryReclaimError("drained"));
|
|
720
732
|
}
|
|
@@ -724,7 +736,6 @@ function createDelivery(port, config) {
|
|
|
724
736
|
}
|
|
725
737
|
function dispose() {
|
|
726
738
|
disposed = true;
|
|
727
|
-
clearMergeTimer();
|
|
728
739
|
teardownSettledSub();
|
|
729
740
|
active.length = 0;
|
|
730
741
|
activeIndex.clear();
|
|
@@ -740,7 +751,7 @@ function createDelivery(port, config) {
|
|
|
740
751
|
checkedPending.length = 0;
|
|
741
752
|
dedupSet?.clear();
|
|
742
753
|
}
|
|
743
|
-
return { send, sendChecked, flush, depth, entriesFull, projection, onChange, confirmDelivered, confirmDeliveredByProof, confirmAccepted, requeue, cancel, failInFlight, drain, dispose };
|
|
754
|
+
return { send, sendChecked, flush, depth, entriesFull, projection, onChange, confirmDelivered, confirmDeliveredByProof, confirmAccepted, requeue, cancel, edit, failInFlight, drain, dispose };
|
|
744
755
|
}
|
|
745
756
|
|
|
746
757
|
// src/disk-store.ts
|
|
@@ -748,6 +759,15 @@ function serializeLine(line) {
|
|
|
748
759
|
return `${JSON.stringify(line)}
|
|
749
760
|
`;
|
|
750
761
|
}
|
|
762
|
+
var DiskStoreReadError = class extends Error {
|
|
763
|
+
/** outbox 文件路径(诊断定位)。 */
|
|
764
|
+
file;
|
|
765
|
+
constructor(file, cause) {
|
|
766
|
+
super(`disk delivery store read failed: ${file}`, { cause });
|
|
767
|
+
this.name = "DiskStoreReadError";
|
|
768
|
+
this.file = file;
|
|
769
|
+
}
|
|
770
|
+
};
|
|
751
771
|
function createDiskDeliveryStore(deps) {
|
|
752
772
|
const { fs, dir, file } = deps;
|
|
753
773
|
const log = deps.log ?? ((message, err) => {
|
|
@@ -780,15 +800,18 @@ function createDiskDeliveryStore(deps) {
|
|
|
780
800
|
}
|
|
781
801
|
function ensureLoaded() {
|
|
782
802
|
if (loaded) return;
|
|
783
|
-
|
|
784
|
-
|
|
803
|
+
if (!fs.existsSync(file)) {
|
|
804
|
+
loaded = true;
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
785
807
|
let raw;
|
|
786
808
|
try {
|
|
787
809
|
raw = fs.readFileSync(file, "utf8");
|
|
788
810
|
} catch (err) {
|
|
789
|
-
log(`[disk-delivery-store] read failed (treated as empty), file=${file}`, err);
|
|
790
|
-
|
|
811
|
+
log(`[disk-delivery-store] read failed (propagating; not treated as empty), file=${file}`, err);
|
|
812
|
+
throw new DiskStoreReadError(file, err);
|
|
791
813
|
}
|
|
814
|
+
loaded = true;
|
|
792
815
|
let lineNumber = 0;
|
|
793
816
|
for (const lineText of raw.split("\n")) {
|
|
794
817
|
lineNumber += 1;
|
|
@@ -856,7 +879,9 @@ function createDiskDeliveryStore(deps) {
|
|
|
856
879
|
};
|
|
857
880
|
}
|
|
858
881
|
export {
|
|
882
|
+
DeliveryAckPersistError,
|
|
859
883
|
DeliveryReclaimError,
|
|
884
|
+
DiskStoreReadError,
|
|
860
885
|
createDelivery,
|
|
861
886
|
createDiskDeliveryStore
|
|
862
887
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/session-delivery",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Session message delivery kernel: zero-pi-dependency strategy layer for
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Session message delivery kernel: zero-pi-dependency strategy layer for idempotent, accept-time dispatch of messages into pi sessions",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.mts",
|
|
@@ -25,6 +25,6 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsup",
|
|
27
27
|
"typecheck": "tsc --noEmit",
|
|
28
|
-
"test": "vitest run"
|
|
28
|
+
"test": "vitest run --pool=threads"
|
|
29
29
|
}
|
|
30
30
|
}
|