@zhushanwen/session-delivery 1.0.0 → 1.1.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 +59 -27
- package/dist/index.mjs +64 -140
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -88,13 +88,12 @@ interface DeliveryPort {
|
|
|
88
88
|
* 处理);void / `{accepted:true}` = 受理成功。旧实现返回 void 兼容。
|
|
89
89
|
*
|
|
90
90
|
* settle 契约:返回 promise 时实现必须 settle(resolve 或 reject)——悬挂的
|
|
91
|
-
* promise 会让内核 in-flight
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* 投递;通道内判重由适配器/对端按裸标记(clientUuid)负责,内核不建第二套判重。
|
|
91
|
+
* promise 会让内核 in-flight 防重永久占位、通路停摆。信任边界内不设防
|
|
92
|
+
* (ADR-0122 范围纪律):本机操作无响应 = 底层已挂,内核不建墙钟兜底,
|
|
93
|
+
* 处置 = 用户显式失败 + 重启应用。
|
|
95
94
|
*/
|
|
96
95
|
send(msg: DeliveryMessage, intent: DeliveryIntent): Promise<SendReceipt | void> | SendReceipt | void;
|
|
97
|
-
/** agent_settled 边沿订阅(D8
|
|
96
|
+
/** agent_settled 边沿订阅(D8)。缺省时 busy 条目留守至外部 flush/send 触发。返回退订函数。 */
|
|
98
97
|
subscribeSettled?(cb: () => void): () => void;
|
|
99
98
|
}
|
|
100
99
|
/** 内核配置(D4 策略默认值)。 */
|
|
@@ -106,13 +105,6 @@ interface DeliveryConfig {
|
|
|
106
105
|
/** 合批依赖谓词(D4 must-fix #1)。true 时 send() 走合批窗口,false/缺省时立即投。
|
|
107
106
|
* 禁止用 isIdle 代替。 */
|
|
108
107
|
mergeHoldActive?: () => boolean;
|
|
109
|
-
/** 退避参数。 */
|
|
110
|
-
backoff?: {
|
|
111
|
-
ms: number;
|
|
112
|
-
max: number;
|
|
113
|
-
};
|
|
114
|
-
/** watch-dog 复核间隔(ms)。默认 30_000。 */
|
|
115
|
-
watchdogMs?: number;
|
|
116
108
|
/** 去重配置(条数 LRU)。 */
|
|
117
109
|
dedupe?: {
|
|
118
110
|
maxKeys: number;
|
|
@@ -133,8 +125,12 @@ interface DeliveryConfig {
|
|
|
133
125
|
* 锚定在受理时点,后移到送达会让 agent 工具调用阻塞至目标 session 当前 turn 结束
|
|
134
126
|
* (steer 类车道可达数十秒)。本注释即接口口径权威声明。
|
|
135
127
|
*
|
|
128
|
+
* outcome 'rejected' = 失败终局通知(ADR-0122 首败即停:send 失败一次即移除条目并
|
|
129
|
+
* 逐条回调;断连事件 failInFlight 转 failed 终态同样逐条回调)。重试决策归消费方,
|
|
130
|
+
* 内核不建自动重试。
|
|
131
|
+
*
|
|
136
132
|
* 异常契约:实现不应抛——回调异常由内核捕获并 warn 留痕,不影响条目状态机与
|
|
137
|
-
* 其余条目的回调(回调异常 ≠
|
|
133
|
+
* 其余条目的回调(回调异常 ≠ 投递失败,不进失败重试链)。
|
|
138
134
|
*/
|
|
139
135
|
onSettled?: (msg: DeliveryMessage, outcome: 'delivered' | 'rejected') => void;
|
|
140
136
|
}
|
|
@@ -165,7 +161,8 @@ type DeliveryLane = 'direct' | 'steer' | 'queued';
|
|
|
165
161
|
* - in-flight → delivered:送达回执到达(message_end 标记命中 / 适配器确认,D2)
|
|
166
162
|
* - in-flight → queued:对账回收重投(滞留收回,D3;cancel 部分收回的其余条目同路径)
|
|
167
163
|
* - in-flight/queued → cancelled:用户撤销(delivery.cancel)
|
|
168
|
-
* - in-flight → failed
|
|
164
|
+
* - in-flight → failed:断连事件驱动的未确认终局(failInFlight,ADR-0122——pi 连接
|
|
165
|
+
* 断开时结果未知,显式失败交用户处置)
|
|
169
166
|
* - failed → queued:用户重试(resync 单条重报);failed → cancelled:用户移除
|
|
170
167
|
* 帧投影四态差异:cancelled 不进 session.delivery 帧(D5③),协议侧帧 state 无此值。
|
|
171
168
|
*/
|
|
@@ -187,8 +184,6 @@ interface DeliveryEntry {
|
|
|
187
184
|
createdAt: number;
|
|
188
185
|
/** 最近一次状态迁移时间(epoch ms)。 */
|
|
189
186
|
updatedAt: number;
|
|
190
|
-
/** 已尝试投递次数(重试耗尽判定源:sendAttempts > backoff.max → failed,§3.4)。 */
|
|
191
|
-
sendAttempts: number;
|
|
192
187
|
/** 终态落定时间(epoch ms;delivered/failed/cancelled 时有值,tombstone 提取源)。 */
|
|
193
188
|
settledAt?: number;
|
|
194
189
|
}
|
|
@@ -198,6 +193,11 @@ interface DeliveryEntry {
|
|
|
198
193
|
* cancelled tombstone 防「cancel 确认帧断连窗口丢失 → 已撤销消息被 resync 复活」。
|
|
199
194
|
* 栖身 runtime 进程内存、不跨 runtime 重启;reattach 场景(判重表已清空)判重锚
|
|
200
195
|
* 回落 transcript 全量标记扫描(D5②/§3.4)。
|
|
196
|
+
*
|
|
197
|
+
* 语义边界(pi1-disposition-chat-flow D1②):tombstone 语义从「送达事实」扩为
|
|
198
|
+
* 「离开系统的事实」——handled 条目(pi 接管输入等受理即终局形态)复用送达确认
|
|
199
|
+
* 原语写入本记录,终态固定为 delivered(复用既有送达终态,不新造终态形态;对账器
|
|
200
|
+
* 见 tombstone 不重投,继承断线重连 resync 判重防线)。state 联合仅两值不变。
|
|
201
201
|
*/
|
|
202
202
|
interface DeliveryTombstone {
|
|
203
203
|
id: string;
|
|
@@ -230,17 +230,17 @@ interface DeliveryEntriesProjection {
|
|
|
230
230
|
/**
|
|
231
231
|
* 投递所有权内核实现:createDelivery(v2)。
|
|
232
232
|
*
|
|
233
|
-
* 在 v1 投递循环(入队 → dedupe → 合批窗口 → busy gate → port.send →
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
233
|
+
* 在 v1 投递循环(入队 → dedupe → 合批窗口 → busy gate → port.send → settled 边沿
|
|
234
|
+
* 驱动)之上升级为条目制所有权内核(设计 .tmp/tech-design/delivery-ownership-kernel.md
|
|
235
|
+
* §3 D9①②③⑤ / D5②③ / D3,单元 u1)。零 pi 依赖不变:确认/回收由适配器(runtime
|
|
236
|
+
* registry)调 handle 方法驱动。
|
|
237
237
|
*
|
|
238
238
|
* 五态状态机(D9①,types.ts 迁移表)+ 本文件登记的两处实施扩展:
|
|
239
239
|
* - queued → in-flight:出站投递被受理(两阶段回执第一阶段,D2)
|
|
240
240
|
* - in-flight → delivered:confirmDelivered(送达回执,D2)
|
|
241
241
|
* - in-flight → queued:requeue(对账回收重投,D3)
|
|
242
242
|
* - queued/failed → cancelled:cancel(本地移除 + tombstone / 用户 × 移除)
|
|
243
|
-
* - in-flight → failed
|
|
243
|
+
* - in-flight → failed:断连事件驱动的未确认终局(failInFlight,ADR-0122)
|
|
244
244
|
* - failed → queued:requeue(用户重试/resync 单条重报)
|
|
245
245
|
* - [扩展①] queued → delivered:confirmDelivered 也接受 queued 态——服务 reattach
|
|
246
246
|
* 场景 rebuild 直确认重建(D3②:适配器扫描 transcript 判 delivered 后 send 重建
|
|
@@ -255,16 +255,26 @@ interface DeliveryEntriesProjection {
|
|
|
255
255
|
* 已由底层通道受理——queued → in-flight + 出批 + checked waiter 受理口径 resolve。
|
|
256
256
|
* 合批分段中途失败时已登记段留守 in-flight(等回执/对账),仅未受理段走失败面。
|
|
257
257
|
*
|
|
258
|
+
* 失败语义(ADR-0122 首败即停,2026-10-05 用户裁决,backoff 自动重试链退役):
|
|
259
|
+
* port.send 失败(含 accepted:false)一次即收口——checked 条目 reject + 从内核移除;
|
|
260
|
+
* 非 checked 条目 onSettled('rejected') 逐条显式上报 + 从内核移除。重试决策归消费方
|
|
261
|
+
* (人看页面通知、agent 收失败回执),内核只做忠实投递与事实上报。
|
|
262
|
+
*
|
|
258
263
|
* onSettled 记账口径(D9⑤ 升级):
|
|
259
264
|
* - 'delivered' = 送达口径:仅 confirmDelivered 驱动回调(受理只转 in-flight,
|
|
260
265
|
* 不回调——受理 ≠ 送达的机制化落地);per-message 契约(ext-simplify-08)在
|
|
261
266
|
* confirmDelivered 路径同样成立——对每条消息各回调一次,msg 为该条原始消息
|
|
262
267
|
* (非 composed 合批消息),additive meta(notifyId 等)随原消息引用原样
|
|
263
268
|
* 透传到回调,内核不读不改。
|
|
264
|
-
* - 'rejected' =
|
|
269
|
+
* - 'rejected' = 失败终局通知(send 首败移除 / failInFlight 断连未确认,仅通知)。
|
|
265
270
|
* - 显式例外:sendChecked 的同步 settle 维持受理口径不变——promise 在 port.send
|
|
266
271
|
* 受理成功时点 resolve(session_manager send 的 {queued:true} 契约锚,D9⑤ 锁定),
|
|
267
|
-
* 与 onSettled
|
|
272
|
+
* 与 onSettled 送达口径正交。补充受理口径的第二 settle 路径(F1-11):条目在
|
|
273
|
+
* port.send promise settle 之前已终局 delivered 时(handled 终局路径——适配器在
|
|
274
|
+
* port.send 实现内部先 confirmDelivered 再让 promise settle,finalizeEntry 同步
|
|
275
|
+
* 摘批后 onSendOk 的 settleChecked 只 settle 当前批成员),受理确认随 confirmDelivered
|
|
276
|
+
* 同步了结(resolveWaitersOf)——契约「受理成功即 resolve」对命令条目成立,
|
|
277
|
+
* waiter 不因 settle 时序倒挂滞留。
|
|
268
278
|
*
|
|
269
279
|
* 双视图(D9②/D5③):entriesFull() = 全量视图(活跃条目 + 全量 tombstone,对账/
|
|
270
280
|
* 判重消费);projection() = 投影视图(活跃全量 + delivered 最近 50 条完整条目,
|
|
@@ -277,10 +287,12 @@ interface DeliveryEntriesProjection {
|
|
|
277
287
|
* id 的消息不参与幂等判重(v1「无 dedupe 配置不去重」语义保持)。
|
|
278
288
|
*
|
|
279
289
|
* 其余 v1 机制逐条保持:busy gate(isIdle + 内核在途内查双条件——hasPendingMessages
|
|
280
|
-
* 自镜像四件套已按 msg-pipeline-debloat D2 拆除,busy 判定内查 active
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
290
|
+
* 自镜像四件套已按 msg-pipeline-debloat D2 拆除,busy 判定内查 active 表)、合批窗口、
|
|
291
|
+
* settled 边沿驱动、dedupe LRU、dispose 语义(丢弃不触发 onSettled、checked 挂账
|
|
292
|
+
* reject)。[ADR-0122 退役登记] backoff 自动重试链、port.send settle 挂死兜底
|
|
293
|
+
* (60s)、watchdog 30s 定时复核、无订阅装配 busy 退避轮询——时间平抑/补偿类机制
|
|
294
|
+
* 全部删除,busy 等待归 settled 边沿与外部触发。depth() 口径保持 v1 = 尚未受理的
|
|
295
|
+
* 消息数(受理转 in-flight 后不计;「在途未确认」数经 entriesFull() 全量视图消费)。
|
|
284
296
|
*/
|
|
285
297
|
|
|
286
298
|
/**
|
|
@@ -321,6 +333,19 @@ interface DeliverySubmitOptions {
|
|
|
321
333
|
* 'acceptance' 并登记来源清单(ADR-0074 词条)。
|
|
322
334
|
*/
|
|
323
335
|
receiptAnchor?: 'marker' | 'acceptance';
|
|
336
|
+
/**
|
|
337
|
+
* 无标记条目标志(pi1-disposition-chat-flow D14⑥ 起源;技能/芯片通路并入后为
|
|
338
|
+
* 「出站不注标条目」统一标志——命令 / 手打 skill 与 prompt 模板 / `<taiji-skill>`
|
|
339
|
+
* 芯片注入文本)。true = 条目出站文本不带投递标记、终局凭据不走 message_end
|
|
340
|
+
* 标记回执(pi disposition 响应 / 断连事件驱动,适配器职责)。内核两处行为:
|
|
341
|
+
* - 组批隔离:与普通条目合批会被 buildBatchPayload 以 BATCH_SEP 拼接,pi 命令解析
|
|
342
|
+
* 与适配器全文身份匹配对拼接文本必然失效——批内含无标记条目时只取队首一条单独
|
|
343
|
+
* 成批(isolateUnmarkedEntry)。
|
|
344
|
+
* - 首败即停(ADR-0122 统一语义后不再差异化,全条目同形态)。
|
|
345
|
+
* 'acceptance' 锚条目(agent 通路)出站同样无标记但走合批、以受理即终态,不置
|
|
346
|
+
* 本标志。
|
|
347
|
+
*/
|
|
348
|
+
unmarked?: boolean;
|
|
324
349
|
}
|
|
325
350
|
/**
|
|
326
351
|
* send() 受理结果(b31-D6:失败双表面窄口收口)。调用方忽略返回值不受影响
|
|
@@ -373,6 +398,13 @@ interface DeliveryHandleV2 extends DeliveryHandle {
|
|
|
373
398
|
* 仅未受理段走失败面——已进底层通道的文本不再误报「投递失败」。
|
|
374
399
|
*/
|
|
375
400
|
confirmAccepted(id: string): boolean;
|
|
401
|
+
/**
|
|
402
|
+
* 断连事件驱动的显式失败终局(ADR-0122 命令终局事件化的断连腿):把全部 in-flight
|
|
403
|
+
* 条目批量转 failed 终态(留守活跃集等用户处置:resync 重试 / cancel 移除),
|
|
404
|
+
* 逐条 onSettled('rejected') 显式上报。queued 条目不触碰(未触达底层通道,随队列
|
|
405
|
+
* 存活,重连后照常投递)。幂等:无 in-flight 条目返回 0。@returns 终态化条数。
|
|
406
|
+
*/
|
|
407
|
+
failInFlight(reason: string): number;
|
|
376
408
|
/**
|
|
377
409
|
* 回收重置 queued 至队首(保持 ids 相对序,D3 own 处置/failed 重试)。
|
|
378
410
|
* 只接受 in-flight/failed;queued 幂等跳过(已在队列);cancelled/delivered
|
package/dist/index.mjs
CHANGED
|
@@ -44,12 +44,9 @@ var LruSet = class {
|
|
|
44
44
|
// src/delivery.ts
|
|
45
45
|
var DEFAULT_CONFIG = {
|
|
46
46
|
intent: "interrupt-at-turn-boundary",
|
|
47
|
-
mergeWindowMs: 0
|
|
48
|
-
backoff: { ms: 100, max: 50 },
|
|
49
|
-
watchdogMs: 3e4
|
|
47
|
+
mergeWindowMs: 0
|
|
50
48
|
};
|
|
51
49
|
var DEFAULT_DELIVERED_WINDOW = 50;
|
|
52
|
-
var PORT_SEND_SETTLE_TIMEOUT_MS = 6e4;
|
|
53
50
|
function isThenable(v) {
|
|
54
51
|
return !!v && typeof v.then === "function";
|
|
55
52
|
}
|
|
@@ -81,6 +78,10 @@ function buildBatchPayload(messages) {
|
|
|
81
78
|
payload: images.length > 0 ? { kind: "text", content, images } : { kind: "text", content }
|
|
82
79
|
};
|
|
83
80
|
}
|
|
81
|
+
function isolateUnmarkedEntry(batch) {
|
|
82
|
+
const firstUnmarked = batch.find((e) => e.unmarked);
|
|
83
|
+
return firstUnmarked !== void 0 ? [firstUnmarked] : batch;
|
|
84
|
+
}
|
|
84
85
|
function settleChecked(batch, err, checkedPending) {
|
|
85
86
|
const rejected = /* @__PURE__ */ new Set();
|
|
86
87
|
if (checkedPending.length === 0) return rejected;
|
|
@@ -118,8 +119,7 @@ function resolveWarnSink(config) {
|
|
|
118
119
|
function createDelivery(port, config) {
|
|
119
120
|
const cfg = {
|
|
120
121
|
...DEFAULT_CONFIG,
|
|
121
|
-
...config
|
|
122
|
-
backoff: config?.backoff ?? DEFAULT_CONFIG.backoff
|
|
122
|
+
...config
|
|
123
123
|
};
|
|
124
124
|
const active = [];
|
|
125
125
|
const activeIndex = /* @__PURE__ */ new Map();
|
|
@@ -128,11 +128,7 @@ function createDelivery(port, config) {
|
|
|
128
128
|
const checkedPending = [];
|
|
129
129
|
let inflightBatch = [];
|
|
130
130
|
let inFlight = false;
|
|
131
|
-
let sendAttempts = 0;
|
|
132
131
|
let mergeTimer;
|
|
133
|
-
let backoffTimer;
|
|
134
|
-
let hangTimer;
|
|
135
|
-
let watchdogTimer;
|
|
136
132
|
let disposed = false;
|
|
137
133
|
let settledUnsub;
|
|
138
134
|
let missingKeyWarned = false;
|
|
@@ -155,7 +151,6 @@ function createDelivery(port, config) {
|
|
|
155
151
|
payload: e.payload,
|
|
156
152
|
createdAt: e.createdAt,
|
|
157
153
|
updatedAt: e.updatedAt,
|
|
158
|
-
sendAttempts: e.sendAttempts,
|
|
159
154
|
...e.settledAt !== void 0 ? { settledAt: e.settledAt } : {}
|
|
160
155
|
};
|
|
161
156
|
}
|
|
@@ -207,6 +202,7 @@ function createDelivery(port, config) {
|
|
|
207
202
|
e.settledAt = ts;
|
|
208
203
|
tombstones.set(e.id, { id: e.id, state, lane: e.lane, settledAt: ts });
|
|
209
204
|
if (state === "delivered") {
|
|
205
|
+
resolveWaitersOf(e);
|
|
210
206
|
deliveredLog.push(e);
|
|
211
207
|
if (deliveredLog.length > DEFAULT_DELIVERED_WINDOW) deliveredLog.shift();
|
|
212
208
|
}
|
|
@@ -225,6 +221,14 @@ function createDelivery(port, config) {
|
|
|
225
221
|
}
|
|
226
222
|
}
|
|
227
223
|
}
|
|
224
|
+
function resolveWaitersOf(e) {
|
|
225
|
+
for (let i = checkedPending.length - 1; i >= 0; i--) {
|
|
226
|
+
if (checkedPending[i].entry === e) {
|
|
227
|
+
checkedPending[i].resolve();
|
|
228
|
+
checkedPending.splice(i, 1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
228
232
|
function clearMergeTimer() {
|
|
229
233
|
if (mergeTimer !== void 0) {
|
|
230
234
|
clearTimeout(mergeTimer);
|
|
@@ -242,18 +246,6 @@ function createDelivery(port, config) {
|
|
|
242
246
|
clearMergeTimer();
|
|
243
247
|
armMergeTimer();
|
|
244
248
|
}
|
|
245
|
-
function clearBackoffTimer() {
|
|
246
|
-
if (backoffTimer !== void 0) {
|
|
247
|
-
clearTimeout(backoffTimer);
|
|
248
|
-
backoffTimer = void 0;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
function clearHangTimer() {
|
|
252
|
-
if (hangTimer !== void 0) {
|
|
253
|
-
clearTimeout(hangTimer);
|
|
254
|
-
hangTimer = void 0;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
249
|
function ensureSettledSub() {
|
|
258
250
|
if (settledUnsub || !port.subscribeSettled) return;
|
|
259
251
|
settledUnsub = port.subscribeSettled(() => {
|
|
@@ -269,30 +261,12 @@ function createDelivery(port, config) {
|
|
|
269
261
|
settledUnsub = void 0;
|
|
270
262
|
}
|
|
271
263
|
}
|
|
272
|
-
function startWatchdog() {
|
|
273
|
-
if (watchdogTimer !== void 0) return;
|
|
274
|
-
if (!port.subscribeSettled) return;
|
|
275
|
-
watchdogTimer = setInterval(() => {
|
|
276
|
-
if (disposed || inFlight) return;
|
|
277
|
-
if (!hasQueuedEntries()) return;
|
|
278
|
-
if (!isBusySafe()) {
|
|
279
|
-
flush();
|
|
280
|
-
}
|
|
281
|
-
}, cfg.watchdogMs);
|
|
282
|
-
}
|
|
283
|
-
function stopWatchdog() {
|
|
284
|
-
if (watchdogTimer !== void 0) {
|
|
285
|
-
clearInterval(watchdogTimer);
|
|
286
|
-
watchdogTimer = void 0;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
264
|
function attemptSend() {
|
|
290
265
|
if (inflightBatch.some((e) => !inRegistry(e))) {
|
|
291
266
|
inflightBatch = inflightBatch.filter((e) => inRegistry(e));
|
|
292
267
|
}
|
|
293
268
|
if (inflightBatch.length === 0) {
|
|
294
269
|
inFlight = false;
|
|
295
|
-
sendAttempts = 0;
|
|
296
270
|
pump();
|
|
297
271
|
return;
|
|
298
272
|
}
|
|
@@ -302,30 +276,9 @@ function createDelivery(port, config) {
|
|
|
302
276
|
try {
|
|
303
277
|
const result = port.send(composed, intent);
|
|
304
278
|
if (isThenable(result)) {
|
|
305
|
-
let settled = false;
|
|
306
|
-
clearHangTimer();
|
|
307
|
-
hangTimer = setTimeout(() => {
|
|
308
|
-
if (settled || disposed) return;
|
|
309
|
-
settled = true;
|
|
310
|
-
hangTimer = void 0;
|
|
311
|
-
warn(
|
|
312
|
-
`port.send hung; force-failed after ${PORT_SEND_SETTLE_TIMEOUT_MS}ms (retries may duplicate-deliver; see DeliveryPort.send settle contract)`
|
|
313
|
-
);
|
|
314
|
-
onSendFail(new Error(`port.send promise did not settle within ${PORT_SEND_SETTLE_TIMEOUT_MS}ms`));
|
|
315
|
-
}, PORT_SEND_SETTLE_TIMEOUT_MS);
|
|
316
279
|
result.then(
|
|
317
|
-
(receipt) =>
|
|
318
|
-
|
|
319
|
-
settled = true;
|
|
320
|
-
clearHangTimer();
|
|
321
|
-
onSendReceipt(receipt);
|
|
322
|
-
},
|
|
323
|
-
(err) => {
|
|
324
|
-
if (settled) return;
|
|
325
|
-
settled = true;
|
|
326
|
-
clearHangTimer();
|
|
327
|
-
onSendFail(err);
|
|
328
|
-
}
|
|
280
|
+
(receipt) => onSendReceipt(receipt),
|
|
281
|
+
(err) => onSendFail(err)
|
|
329
282
|
);
|
|
330
283
|
} else {
|
|
331
284
|
onSendReceipt(result);
|
|
@@ -346,7 +299,6 @@ function createDelivery(port, config) {
|
|
|
346
299
|
const batch = inflightBatch;
|
|
347
300
|
inFlight = false;
|
|
348
301
|
inflightBatch = [];
|
|
349
|
-
sendAttempts = 0;
|
|
350
302
|
settleChecked(batch, void 0, checkedPending);
|
|
351
303
|
let changed = false;
|
|
352
304
|
for (const e of batch) {
|
|
@@ -366,49 +318,24 @@ function createDelivery(port, config) {
|
|
|
366
318
|
}
|
|
367
319
|
function onSendFail(err) {
|
|
368
320
|
if (disposed) return;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
inflightBatch = [];
|
|
380
|
-
sendAttempts = 0;
|
|
381
|
-
warn("port.send failed", err);
|
|
382
|
-
pump();
|
|
383
|
-
return;
|
|
321
|
+
const batch = inflightBatch;
|
|
322
|
+
inFlight = false;
|
|
323
|
+
inflightBatch = [];
|
|
324
|
+
warn("port.send failed (first-failure stop, no kernel retry)", err);
|
|
325
|
+
const rejected = settleChecked(batch, err, checkedPending);
|
|
326
|
+
let changed = false;
|
|
327
|
+
for (const e of batch) {
|
|
328
|
+
if (!inRegistry(e)) continue;
|
|
329
|
+
if (rejected.has(e)) continue;
|
|
330
|
+
callOnSettled(e.msg, "rejected");
|
|
384
331
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
sendAttempts = 0;
|
|
390
|
-
warn("port.send failed after max retries", err);
|
|
391
|
-
let changed = false;
|
|
392
|
-
for (const e of failedBatch) {
|
|
393
|
-
if (disposed) break;
|
|
394
|
-
if (!inRegistry(e)) continue;
|
|
395
|
-
e.state = "failed";
|
|
396
|
-
const ts = now();
|
|
397
|
-
e.updatedAt = ts;
|
|
398
|
-
e.settledAt = ts;
|
|
399
|
-
changed = true;
|
|
400
|
-
callOnSettled(e.msg, "rejected");
|
|
401
|
-
}
|
|
402
|
-
if (changed) notifyChange();
|
|
403
|
-
pump();
|
|
404
|
-
return;
|
|
332
|
+
for (const e of batch) {
|
|
333
|
+
if (!inRegistry(e)) continue;
|
|
334
|
+
removeActive(e);
|
|
335
|
+
changed = true;
|
|
405
336
|
}
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
backoffTimer = void 0;
|
|
409
|
-
if (disposed || !inFlight) return;
|
|
410
|
-
attemptSend();
|
|
411
|
-
}, cfg.backoff.ms);
|
|
337
|
+
if (changed) notifyChange();
|
|
338
|
+
pump();
|
|
412
339
|
}
|
|
413
340
|
function pump() {
|
|
414
341
|
if (disposed || inFlight) return;
|
|
@@ -418,43 +345,29 @@ function createDelivery(port, config) {
|
|
|
418
345
|
if (inRegistry(w.entry) && w.entry.state === "queued") batch.push(w.entry);
|
|
419
346
|
}
|
|
420
347
|
if (batch.length > 0) {
|
|
421
|
-
inflightBatch = batch;
|
|
348
|
+
inflightBatch = isolateUnmarkedEntry(batch);
|
|
422
349
|
inFlight = true;
|
|
423
|
-
sendAttempts = 0;
|
|
424
350
|
attemptSend();
|
|
425
351
|
return;
|
|
426
352
|
}
|
|
427
353
|
}
|
|
428
354
|
if (hasQueuedEntries()) {
|
|
429
|
-
scheduleFlush(
|
|
355
|
+
scheduleFlush();
|
|
430
356
|
return;
|
|
431
357
|
}
|
|
432
|
-
if (checkedPending.length === 0) stopWatchdog();
|
|
433
358
|
}
|
|
434
359
|
function doSend() {
|
|
435
360
|
if (disposed || inFlight) return;
|
|
436
|
-
const batch = active.filter((e) => e.state === "queued");
|
|
361
|
+
const batch = isolateUnmarkedEntry(active.filter((e) => e.state === "queued"));
|
|
437
362
|
if (batch.length === 0) return;
|
|
438
363
|
inFlight = true;
|
|
439
364
|
inflightBatch = batch;
|
|
440
|
-
sendAttempts = 0;
|
|
441
365
|
attemptSend();
|
|
442
366
|
}
|
|
443
|
-
function scheduleFlush(
|
|
367
|
+
function scheduleFlush() {
|
|
444
368
|
if (disposed || !hasQueuedEntries()) return;
|
|
445
369
|
if (inFlight) return;
|
|
446
|
-
|
|
447
|
-
if (isBusySafe() && attempt < cfg.backoff.max) {
|
|
448
|
-
if (port.subscribeSettled) {
|
|
449
|
-
startWatchdog();
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
backoffTimer = setTimeout(() => {
|
|
453
|
-
backoffTimer = void 0;
|
|
454
|
-
scheduleFlush(attempt + 1);
|
|
455
|
-
}, cfg.backoff.ms);
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
370
|
+
if (isBusySafe()) return;
|
|
458
371
|
doSend();
|
|
459
372
|
}
|
|
460
373
|
const warnSink = resolveWarnSink(config);
|
|
@@ -487,10 +400,10 @@ function createDelivery(port, config) {
|
|
|
487
400
|
payload: msg.payload,
|
|
488
401
|
createdAt: ts,
|
|
489
402
|
updatedAt: ts,
|
|
490
|
-
sendAttempts: 0,
|
|
491
403
|
msg,
|
|
492
404
|
cancelRequested: false,
|
|
493
|
-
receiptAnchor: opts?.receiptAnchor ?? "marker"
|
|
405
|
+
receiptAnchor: opts?.receiptAnchor ?? "marker",
|
|
406
|
+
unmarked: opts?.unmarked ?? false
|
|
494
407
|
};
|
|
495
408
|
active.push(entry);
|
|
496
409
|
activeIndex.set(entry.id, entry);
|
|
@@ -520,7 +433,7 @@ function createDelivery(port, config) {
|
|
|
520
433
|
}
|
|
521
434
|
clearMergeTimer();
|
|
522
435
|
ensureSettledSub();
|
|
523
|
-
scheduleFlush(
|
|
436
|
+
scheduleFlush();
|
|
524
437
|
return { kind: "accepted", id: entry.id };
|
|
525
438
|
}
|
|
526
439
|
async function sendChecked(msg, opts) {
|
|
@@ -540,7 +453,6 @@ function createDelivery(port, config) {
|
|
|
540
453
|
if (!inFlight) {
|
|
541
454
|
inflightBatch = [entry];
|
|
542
455
|
inFlight = true;
|
|
543
|
-
sendAttempts = 0;
|
|
544
456
|
attemptSend();
|
|
545
457
|
}
|
|
546
458
|
});
|
|
@@ -548,7 +460,7 @@ function createDelivery(port, config) {
|
|
|
548
460
|
function flush() {
|
|
549
461
|
if (disposed) return;
|
|
550
462
|
clearMergeTimer();
|
|
551
|
-
scheduleFlush(
|
|
463
|
+
scheduleFlush();
|
|
552
464
|
}
|
|
553
465
|
function depth() {
|
|
554
466
|
let n = 0;
|
|
@@ -616,13 +528,12 @@ function createDelivery(port, config) {
|
|
|
616
528
|
const idx = active.indexOf(e);
|
|
617
529
|
if (idx !== -1) active.splice(idx, 1);
|
|
618
530
|
e.state = "queued";
|
|
619
|
-
e.sendAttempts = 0;
|
|
620
531
|
e.updatedAt = now();
|
|
621
532
|
e.settledAt = void 0;
|
|
622
533
|
}
|
|
623
534
|
active.unshift(...found);
|
|
624
535
|
notifyChange();
|
|
625
|
-
scheduleFlush(
|
|
536
|
+
scheduleFlush();
|
|
626
537
|
return found.length;
|
|
627
538
|
}
|
|
628
539
|
function cancel(id) {
|
|
@@ -651,17 +562,34 @@ function createDelivery(port, config) {
|
|
|
651
562
|
finalizeEntry(e, "cancelled");
|
|
652
563
|
return { kind: "cancelled", entry: snapshot(e) };
|
|
653
564
|
}
|
|
565
|
+
function failInFlight(reason) {
|
|
566
|
+
if (disposed) return 0;
|
|
567
|
+
const ts = now();
|
|
568
|
+
let count = 0;
|
|
569
|
+
for (const e of active) {
|
|
570
|
+
if (e.state !== "in-flight") continue;
|
|
571
|
+
const batchIdx = inflightBatch.indexOf(e);
|
|
572
|
+
if (batchIdx !== -1) inflightBatch.splice(batchIdx, 1);
|
|
573
|
+
e.state = "failed";
|
|
574
|
+
e.updatedAt = ts;
|
|
575
|
+
e.settledAt = ts;
|
|
576
|
+
count++;
|
|
577
|
+
callOnSettled(e.msg, "rejected");
|
|
578
|
+
}
|
|
579
|
+
if (count > 0) {
|
|
580
|
+
warn(`failInFlight: ${count} in-flight entry(ies) marked failed (${reason})`);
|
|
581
|
+
notifyChange();
|
|
582
|
+
}
|
|
583
|
+
return count;
|
|
584
|
+
}
|
|
654
585
|
function drain() {
|
|
655
586
|
if (disposed) return [];
|
|
656
587
|
clearMergeTimer();
|
|
657
|
-
clearBackoffTimer();
|
|
658
|
-
clearHangTimer();
|
|
659
588
|
const drained = active.slice();
|
|
660
589
|
active.length = 0;
|
|
661
590
|
activeIndex.clear();
|
|
662
591
|
inflightBatch = [];
|
|
663
592
|
inFlight = false;
|
|
664
|
-
sendAttempts = 0;
|
|
665
593
|
const ts = now();
|
|
666
594
|
const result = drained.map((e) => {
|
|
667
595
|
tombstones.set(e.id, { id: e.id, state: "cancelled", lane: e.lane, settledAt: ts });
|
|
@@ -671,16 +599,12 @@ function createDelivery(port, config) {
|
|
|
671
599
|
w.reject(new DeliveryReclaimError("drained"));
|
|
672
600
|
}
|
|
673
601
|
checkedPending.length = 0;
|
|
674
|
-
stopWatchdog();
|
|
675
602
|
if (drained.length > 0) notifyChange();
|
|
676
603
|
return result;
|
|
677
604
|
}
|
|
678
605
|
function dispose() {
|
|
679
606
|
disposed = true;
|
|
680
607
|
clearMergeTimer();
|
|
681
|
-
clearBackoffTimer();
|
|
682
|
-
clearHangTimer();
|
|
683
|
-
stopWatchdog();
|
|
684
608
|
teardownSettledSub();
|
|
685
609
|
active.length = 0;
|
|
686
610
|
activeIndex.clear();
|
|
@@ -695,7 +619,7 @@ function createDelivery(port, config) {
|
|
|
695
619
|
checkedPending.length = 0;
|
|
696
620
|
dedupSet?.clear();
|
|
697
621
|
}
|
|
698
|
-
return { send, sendChecked, flush, depth, entriesFull, projection, onChange, confirmDelivered, confirmAccepted, requeue, cancel, drain, dispose };
|
|
622
|
+
return { send, sendChecked, flush, depth, entriesFull, projection, onChange, confirmDelivered, confirmAccepted, requeue, cancel, failInFlight, drain, dispose };
|
|
699
623
|
}
|
|
700
624
|
export {
|
|
701
625
|
DeliveryReclaimError,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/session-delivery",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Session message delivery kernel: zero-pi-dependency strategy layer for queueing, batching, dedup, and gated flush of messages into pi sessions",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"module": "dist/index.mjs",
|