@zhushanwen/session-delivery 0.10.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/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @zhushanwen/session-delivery
2
+
3
+ Session 消息投递内核:零 pi 依赖的策略层,负责排队、合批、去重与 gated flush。
4
+
5
+ ## 设计原则
6
+
7
+ - **零 pi 依赖**:内核不 import pi 包任何符号。steer/followUp/triggerTurn/streamingBehavior 等 pi 词汇封闭在两侧适配器内。
8
+ - **端口注入**:`DeliveryPort` 是内核与外部世界的唯一接口(isIdle / hasPendingMessages / send / subscribeSettled),调用方负责注入运行时能力。
9
+ - **意图驱动**:调用方声明投递意图(`DeliveryIntent`),内核处理与目标 session 运行状态的冲突。intent → pi 参数的翻译在适配器内。
10
+
11
+ ## 接口
12
+
13
+ ```ts
14
+ import { createDelivery, type DeliveryHandle, type DeliveryPort, type DeliveryConfig } from '@zhushanwen/session-delivery'
15
+
16
+ const handle = createDelivery(port, config)
17
+
18
+ // 常规入口:入队 + 合批/去重/gated flush
19
+ handle.send({ payload: { kind: 'text', content: 'hello' } })
20
+
21
+ // 同步确认变体:入队 + 可达性确认
22
+ await handle.sendChecked({ payload: { kind: 'text', content: 'hello' } })
23
+
24
+ // 强制投递(shutdown / park 外部重触发 / settled 边沿内部复用)
25
+ handle.flush()
26
+
27
+ // 队列深度(诊断/测试)
28
+ handle.depth()
29
+
30
+ // 销毁(清空队列 + 清 timer)
31
+ handle.dispose()
32
+ ```
33
+
34
+ ## 策略默认值
35
+
36
+ | 策略 | 默认值 | 说明 |
37
+ |------|--------|------|
38
+ | `intent` | `'interrupt-at-turn-boundary'` | turn 边界抢占(F1 教训内化) |
39
+ | `busyPolicy` | `'retry-force'` | settled 边沿驱动 flush + watch-dog 30s 复核;无订阅装配退化退避 {100ms, 50}≈5s 后强制发送 |
40
+ | `mergeWindowMs` | `0` | 关;显式设值启用滑动窗口合批 |
41
+ | `backoff` | `{ ms: 100, max: 50 }` | 退避参数 |
42
+ | `watchdogMs` | `30_000` | watch-dog 复核间隔 |
43
+
44
+ ## 约束
45
+
46
+ - **同 session 单例 handle**:多 handle 并发投递竞态无保护。runtime 装配层必须以 sessionId 为键做单例注册表;extension 侧同理。
47
+ - **subscribeSettled 的退订语义由适配器负责兑现**。pi.on 返回 void 且无 off,适配器需用 disposed 标志包装。
48
+
49
+ ## 开发
50
+
51
+ ```bash
52
+ cd packages/session-delivery
53
+ npx vitest run # 跑测试
54
+ npx tsc --noEmit # 类型检查
55
+ ```
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @zhushanwen/session-delivery 类型定义。
3
+ *
4
+ * 设计约束:
5
+ * - 零 pi 依赖:不出现 steer/followUp/triggerTurn/streamingBehavior 等 pi 词汇
6
+ * - 意图驱动:调用方声明 intent,内核处理与 session 运行状态的冲突
7
+ * - 判别联合 payload:适配器声明 supportedPayloads 能力
8
+ */
9
+ /** 投递意图(D3):turn 边界抢占 / run 结束后注入,均含 idle 唤醒语义。 */
10
+ type DeliveryIntent = 'interrupt-at-turn-boundary' | 'after-run';
11
+ /** 文本 payload(runtime 通路一期唯一支持)。 */
12
+ interface TextPayload {
13
+ kind: 'text';
14
+ content: string;
15
+ }
16
+ /** custom message payload(extension 通路支持)。 */
17
+ interface CustomPayload {
18
+ kind: 'custom';
19
+ customType: string;
20
+ content: string;
21
+ display: boolean;
22
+ details?: unknown;
23
+ }
24
+ /** 判别联合(D9):envelope / payload 分离。 */
25
+ type DeliveryPayload = TextPayload | CustomPayload;
26
+ /** 投递消息 envelope。 */
27
+ interface DeliveryMessage {
28
+ payload: DeliveryPayload;
29
+ /** 缺省回落 config.intent。 */
30
+ intent?: DeliveryIntent;
31
+ /** 去重 key(开 dedupe 时必填)。 */
32
+ dedupeKey?: string;
33
+ /** 持久性预留(一期仅 'in-memory')。 */
34
+ durability?: 'in-memory';
35
+ }
36
+ /**
37
+ * port.send 的受理回执(U2 回执口径扩展位)。
38
+ *
39
+ * 受理 = 底层通道接受了消息(进入投递路径),**不等于送达事实**——送达判定由
40
+ * 调用方按自身回执锚点确认(如扩展侧扫描 custom_message entry)。旧 port 实现
41
+ * 返回 void 不受影响(void = 受理未知,按成功处理,与既有内核语义一致)。
42
+ */
43
+ interface SendReceipt {
44
+ /** 受理结果。false = 底层显式拒绝(消息未进入任何通道,走错误重试路径)。 */
45
+ accepted: boolean;
46
+ /** 拒绝原因(accepted:false 时携带,进 warn 日志与 reject 链)。 */
47
+ reason?: string;
48
+ }
49
+ /** 内核与外部世界的唯一接口(D2 端口注入)。 */
50
+ interface DeliveryPort {
51
+ /** 本通路支持的 payload kind(D9 fail-fast)。 */
52
+ supportedPayloads: readonly DeliveryPayload['kind'][];
53
+ /** 主 agent 是否空闲。 */
54
+ isIdle(): boolean;
55
+ /** 是否有排队中的消息。 */
56
+ hasPendingMessages(): boolean;
57
+ /**
58
+ * 投递消息(intent → pi 参数的翻译在适配器内部)。
59
+ * 返回受理回执(U2 扩展位):显式 `{accepted:false}` = 受理失败(内核按发送失败
60
+ * 处理);void / `{accepted:true}` = 受理成功。旧实现返回 void 兼容。
61
+ */
62
+ send(msg: DeliveryMessage, intent: DeliveryIntent): Promise<SendReceipt | void> | SendReceipt | void;
63
+ /** agent_settled 边沿订阅(D8)。缺省时内核退化退避轮询。返回退订函数。 */
64
+ subscribeSettled?(cb: () => void): () => void;
65
+ }
66
+ /** 内核配置(D4 策略默认值)。 */
67
+ interface DeliveryConfig {
68
+ /** 默认意图:'interrupt-at-turn-boundary'(D3)。 */
69
+ intent?: DeliveryIntent;
70
+ /** busy 策略:'retry-force'(默认)/ 'park'。 */
71
+ busyPolicy?: 'retry-force' | 'park';
72
+ /** 合批窗口(ms):0 = 关;>0 = 滑动窗口合批。 */
73
+ mergeWindowMs?: number;
74
+ /** 合批依赖谓词(D4 must-fix #1)。true 时 send() 走合批窗口,false/缺省时立即投。
75
+ * 禁止用 isIdle 代替。 */
76
+ mergeHoldActive?: () => boolean;
77
+ /** 退避参数。 */
78
+ backoff?: {
79
+ ms: number;
80
+ max: number;
81
+ };
82
+ /** watch-dog 复核间隔(ms)。默认 30_000。 */
83
+ watchdogMs?: number;
84
+ /** 去重配置(条数 LRU)。 */
85
+ dedupe?: {
86
+ maxKeys: number;
87
+ };
88
+ /**
89
+ * 投递终态信号(D4)。per-message 契约(ext-simplify-08 D1/B1):批次内每条消息
90
+ * 各获一次终态回调,msg 为该条原始消息(非合批 composed 消息);单消息批次行为
91
+ * 不变。回调循环边界:回调内 dispose() 后本批剩余条目不再回调(与 dispose「丢弃
92
+ * 队列不触发 onSettled」契约一致);回调内再 send() 的新消息走标准 flush 管线
93
+ * (空闲时可能在回调栈内立即投递)、不参与本批回调循环(契约完备性登记:当前
94
+ * 消费方两种形态均零可达)。
95
+ */
96
+ onSettled?: (msg: DeliveryMessage, outcome: 'delivered' | 'rejected') => void;
97
+ }
98
+ /** 投递句柄(createDelivery 返回)。 */
99
+ interface DeliveryHandle {
100
+ /** 唯一常规入口(D4 入口收敛);合批窗口 + 空闲零延迟立即投。 */
101
+ send(msg: DeliveryMessage, opts?: {
102
+ merge?: boolean;
103
+ }): void;
104
+ /** 入队 + 可达性同步确认。reject = 入队失败。 */
105
+ sendChecked(msg: DeliveryMessage): Promise<void>;
106
+ /** 强制投递尝试(shutdown / park 外部重触发 / settled 边沿内部复用)。 */
107
+ flush(): void;
108
+ /** 队列深度(诊断/测试)。 */
109
+ depth(): number;
110
+ /** 销毁(清空队列 + 清 timer + 退订 settled)。 */
111
+ dispose(): void;
112
+ }
113
+
114
+ /**
115
+ * 投递内核实现:createDelivery。
116
+ *
117
+ * 核心行为(design.md §3.3 D4/D8):
118
+ * - send() 入队 → dedupe → 合批窗口判定 → 立即投或窗口到期投
119
+ * - flush() → busy gate(isIdle + hasPendingMessages 双条件,G4)→ port.send
120
+ * - settled 边沿 → busy 复核通过 → flush;有订阅装配下 busy 只依赖边沿 + watch-dog(不退避强发)
121
+ * - watch-dog 低频复核(D8 兜底层①:settled 事件丢失的恢复路径)
122
+ * - port.send 失败 → 消息留在途、按 backoff 有限重试,达上限 settle rejected(D4 错误重试)
123
+ * - 终态上报 per-message(ext-simplify-08 D1/B1):onSettled 对批次内每条消息各回调
124
+ * 一次,msg 为该条原始消息(非 composed 合批消息);单消息批次行为不变
125
+ * - sendChecked() 统一经投递循环:resolve 挂钩 port.send 受理结果(busy 时经
126
+ * streaming 受理入 pi 队列即回,以此确认可达);首次受理失败即 reject(入口即拦)
127
+ * - in-flight 防重:单 handle 至多一个 port.send 在途
128
+ */
129
+
130
+ /**
131
+ * U4 warn 出口注入(设计 docs/architecture/pi-boundary-reliability.md 附录 D §5 U4):
132
+ * 装配方接 extensionLogger 使投递失败警告落 `<dataDir>/logs/`(console.warn 走
133
+ * stderr tee 不到日志盘——排查无痕)。通用包不硬依赖 taiji logger,故以可选注入
134
+ * 挂载;正式化并入 types.ts 的 DeliveryConfig 属领地外变更,暂以交叉类型承载。
135
+ */
136
+ interface DeliveryWarnSink {
137
+ /** 投递失败/异常警告出口。缺省 console.warn(前缀 `[session-delivery]`)。 */
138
+ warn?: (msg: string, err?: unknown) => void;
139
+ }
140
+ /** createDelivery 配置(DeliveryConfig + warn 出口注入)。 */
141
+ type DeliveryConfigWithWarn = DeliveryConfig & DeliveryWarnSink;
142
+ /**
143
+ * 创建投递句柄。
144
+ *
145
+ * 约束:同 session 必须单例 handle(多 handle 并发投递竞态无保护)。
146
+ * subscribeSettled 的退订语义由适配器负责兑现。
147
+ */
148
+ declare function createDelivery(port: DeliveryPort, config?: DeliveryConfigWithWarn): DeliveryHandle;
149
+
150
+ export { type CustomPayload, type DeliveryConfig, type DeliveryHandle, type DeliveryIntent, type DeliveryMessage, type DeliveryPayload, type DeliveryPort, type SendReceipt, type TextPayload, createDelivery };
package/dist/index.mjs ADDED
@@ -0,0 +1,395 @@
1
+ // src/lru.ts
2
+ var LruSet = class {
3
+ constructor(maxKeys) {
4
+ this.maxKeys = maxKeys;
5
+ this.map = /* @__PURE__ */ new Map();
6
+ }
7
+ maxKeys;
8
+ map;
9
+ /** 是否已包含 key。命中后刷新 LRU 位置。 */
10
+ has(key) {
11
+ if (!this.map.has(key)) return false;
12
+ this.map.delete(key);
13
+ this.map.set(key);
14
+ return true;
15
+ }
16
+ /** 插入 key。超容量时淘汰最旧。 */
17
+ add(key) {
18
+ if (this.map.has(key)) {
19
+ this.map.delete(key);
20
+ } else if (this.map.size >= this.maxKeys) {
21
+ const first = this.map.keys().next().value;
22
+ if (first !== void 0) this.map.delete(first);
23
+ }
24
+ this.map.set(key);
25
+ }
26
+ /** 清空。 */
27
+ clear() {
28
+ this.map.clear();
29
+ }
30
+ };
31
+
32
+ // src/delivery.ts
33
+ var DEFAULT_CONFIG = {
34
+ intent: "interrupt-at-turn-boundary",
35
+ busyPolicy: "retry-force",
36
+ mergeWindowMs: 0,
37
+ backoff: { ms: 100, max: 50 },
38
+ watchdogMs: 3e4
39
+ };
40
+ function isThenable(v) {
41
+ return !!v && typeof v.then === "function";
42
+ }
43
+ function buildBatchPayload(messages) {
44
+ if (messages.length === 1) return messages[0];
45
+ const contents = messages.map((m) => m.payload.content);
46
+ const content = contents.join("\n\n---\n\n");
47
+ const first = messages[0];
48
+ if (first.payload.kind === "custom") {
49
+ return {
50
+ ...first,
51
+ payload: {
52
+ kind: "custom",
53
+ customType: first.payload.customType,
54
+ content,
55
+ display: first.payload.display,
56
+ details: {
57
+ batch: true,
58
+ items: messages.map(
59
+ (m) => m.payload.kind === "custom" && m.payload.details !== void 0 ? m.payload.details : m.payload
60
+ )
61
+ }
62
+ }
63
+ };
64
+ }
65
+ return {
66
+ ...first,
67
+ payload: { kind: "text", content }
68
+ };
69
+ }
70
+ function settleChecked(batch, err, checkedPending) {
71
+ const rejected = /* @__PURE__ */ new Set();
72
+ if (checkedPending.length === 0) return rejected;
73
+ const batchSet = new Set(batch);
74
+ const kept = [];
75
+ for (const w of checkedPending) {
76
+ if (!batchSet.has(w.msg)) {
77
+ kept.push(w);
78
+ continue;
79
+ }
80
+ if (err === void 0) {
81
+ w.resolve();
82
+ } else {
83
+ w.reject(err);
84
+ rejected.add(w.msg);
85
+ }
86
+ }
87
+ checkedPending.length = 0;
88
+ checkedPending.push(...kept);
89
+ return rejected;
90
+ }
91
+ function safeIsIdle(port) {
92
+ try {
93
+ return port.isIdle();
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+ function isBusy(port) {
99
+ if (!safeIsIdle(port)) return true;
100
+ try {
101
+ return port.hasPendingMessages();
102
+ } catch {
103
+ return true;
104
+ }
105
+ }
106
+ function resolveWarnSink(config) {
107
+ return config?.warn ?? ((msg, err) => {
108
+ console.warn(`[session-delivery] ${msg}`, err ?? "");
109
+ });
110
+ }
111
+ function createDelivery(port, config) {
112
+ const cfg = {
113
+ ...DEFAULT_CONFIG,
114
+ ...config,
115
+ backoff: config?.backoff ?? DEFAULT_CONFIG.backoff
116
+ };
117
+ const queue = [];
118
+ const checkedPending = [];
119
+ let inflightBatch = [];
120
+ let inFlight = false;
121
+ let sendAttempts = 0;
122
+ let mergeTimer;
123
+ let backoffTimer;
124
+ let watchdogTimer;
125
+ let disposed = false;
126
+ let settledUnsub;
127
+ let missingKeyWarned = false;
128
+ const dedupSet = config?.dedupe ? new LruSet(config.dedupe.maxKeys) : null;
129
+ function clearMergeTimer() {
130
+ if (mergeTimer !== void 0) {
131
+ clearTimeout(mergeTimer);
132
+ mergeTimer = void 0;
133
+ }
134
+ }
135
+ function armMergeTimer() {
136
+ if (cfg.mergeWindowMs <= 0) return;
137
+ mergeTimer = setTimeout(() => {
138
+ mergeTimer = void 0;
139
+ flush();
140
+ }, cfg.mergeWindowMs);
141
+ }
142
+ function resetMergeTimer() {
143
+ clearMergeTimer();
144
+ armMergeTimer();
145
+ }
146
+ function ensureSettledSub() {
147
+ if (settledUnsub || !port.subscribeSettled) return;
148
+ settledUnsub = port.subscribeSettled(() => {
149
+ if (disposed) return;
150
+ if (!isBusy(port)) {
151
+ flush();
152
+ }
153
+ });
154
+ }
155
+ function teardownSettledSub() {
156
+ if (settledUnsub) {
157
+ settledUnsub();
158
+ settledUnsub = void 0;
159
+ }
160
+ }
161
+ function startWatchdog() {
162
+ if (watchdogTimer !== void 0) return;
163
+ if (!port.subscribeSettled) return;
164
+ watchdogTimer = setInterval(() => {
165
+ if (disposed || inFlight) return;
166
+ if (queue.length === 0) return;
167
+ if (!isBusy(port)) {
168
+ flush();
169
+ }
170
+ }, cfg.watchdogMs);
171
+ }
172
+ function stopWatchdog() {
173
+ if (watchdogTimer !== void 0) {
174
+ clearInterval(watchdogTimer);
175
+ watchdogTimer = void 0;
176
+ }
177
+ }
178
+ function attemptSend() {
179
+ const batch = inflightBatch;
180
+ const composed = buildBatchPayload(batch);
181
+ const intent = composed.intent ?? cfg.intent;
182
+ try {
183
+ const result = port.send(composed, intent);
184
+ if (isThenable(result)) {
185
+ result.then(
186
+ (receipt) => onSendReceipt(receipt),
187
+ (err) => onSendFail(err)
188
+ );
189
+ } else {
190
+ onSendReceipt(result);
191
+ }
192
+ } catch (err) {
193
+ onSendFail(err);
194
+ }
195
+ }
196
+ function onSendReceipt(receipt) {
197
+ if (receipt !== void 0 && receipt.accepted === false) {
198
+ onSendFail(new Error(receipt.reason ?? "port.send rejected (accepted:false)"));
199
+ return;
200
+ }
201
+ onSendOk();
202
+ }
203
+ function onSendOk() {
204
+ if (disposed) return;
205
+ const delivered = inflightBatch;
206
+ inFlight = false;
207
+ inflightBatch = [];
208
+ sendAttempts = 0;
209
+ settleChecked(delivered, void 0, checkedPending);
210
+ for (const m of delivered) {
211
+ if (disposed) break;
212
+ cfg.onSettled?.(m, "delivered");
213
+ }
214
+ pump();
215
+ }
216
+ function onSendFail(err) {
217
+ if (disposed) return;
218
+ sendAttempts++;
219
+ const rejected = settleChecked(inflightBatch, err, checkedPending);
220
+ if (rejected.size > 0) {
221
+ inflightBatch = inflightBatch.filter((m) => !rejected.has(m));
222
+ }
223
+ if (inflightBatch.length === 0) {
224
+ inFlight = false;
225
+ inflightBatch = [];
226
+ sendAttempts = 0;
227
+ warn("port.send failed", err);
228
+ pump();
229
+ return;
230
+ }
231
+ if (sendAttempts > cfg.backoff.max) {
232
+ const rejectedBatch = inflightBatch;
233
+ inFlight = false;
234
+ inflightBatch = [];
235
+ sendAttempts = 0;
236
+ warn("port.send failed after max retries", err);
237
+ for (const m of rejectedBatch) {
238
+ if (disposed) break;
239
+ cfg.onSettled?.(m, "rejected");
240
+ }
241
+ pump();
242
+ return;
243
+ }
244
+ if (sendAttempts === 1) warn("port.send failed, retrying with backoff", err);
245
+ backoffTimer = setTimeout(() => {
246
+ backoffTimer = void 0;
247
+ if (disposed || !inFlight) return;
248
+ attemptSend();
249
+ }, cfg.backoff.ms);
250
+ }
251
+ function pump() {
252
+ if (disposed || inFlight) return;
253
+ if (checkedPending.length > 0) {
254
+ const msgs = [];
255
+ for (const w of checkedPending) {
256
+ const idx = queue.indexOf(w.msg);
257
+ if (idx !== -1) {
258
+ queue.splice(idx, 1);
259
+ msgs.push(w.msg);
260
+ }
261
+ }
262
+ if (msgs.length > 0) {
263
+ inflightBatch = msgs;
264
+ inFlight = true;
265
+ sendAttempts = 0;
266
+ attemptSend();
267
+ return;
268
+ }
269
+ }
270
+ if (queue.length > 0) {
271
+ scheduleFlush(0);
272
+ return;
273
+ }
274
+ if (checkedPending.length === 0) stopWatchdog();
275
+ }
276
+ function doSend() {
277
+ if (disposed || queue.length === 0 || inFlight) return;
278
+ inFlight = true;
279
+ inflightBatch = queue.splice(0);
280
+ sendAttempts = 0;
281
+ attemptSend();
282
+ }
283
+ function scheduleFlush(attempt) {
284
+ if (disposed || queue.length === 0) return;
285
+ if (inFlight) return;
286
+ if (cfg.busyPolicy === "park" && attempt > 0) return;
287
+ if (backoffTimer !== void 0) {
288
+ clearTimeout(backoffTimer);
289
+ backoffTimer = void 0;
290
+ }
291
+ if (isBusy(port) && attempt < cfg.backoff.max) {
292
+ if (port.subscribeSettled) {
293
+ startWatchdog();
294
+ return;
295
+ }
296
+ backoffTimer = setTimeout(() => {
297
+ backoffTimer = void 0;
298
+ scheduleFlush(attempt + 1);
299
+ }, cfg.backoff.ms);
300
+ return;
301
+ }
302
+ doSend();
303
+ }
304
+ const warnSink = resolveWarnSink(config);
305
+ function warn(msg, err) {
306
+ warnSink(msg, err);
307
+ }
308
+ function passDedupe(msg) {
309
+ if (!dedupSet) return true;
310
+ if (!msg.dedupeKey) {
311
+ if (!missingKeyWarned) {
312
+ missingKeyWarned = true;
313
+ warn("dedupe enabled but message has no dedupeKey; delivering without dedupe");
314
+ }
315
+ return true;
316
+ }
317
+ if (dedupSet.has(msg.dedupeKey)) return false;
318
+ dedupSet.add(msg.dedupeKey);
319
+ return true;
320
+ }
321
+ function send(msg, opts) {
322
+ if (disposed) return;
323
+ if (!port.supportedPayloads.includes(msg.payload.kind)) {
324
+ warn(`unsupported payload kind: ${msg.payload.kind}`);
325
+ return;
326
+ }
327
+ if (!passDedupe(msg)) return;
328
+ queue.push(msg);
329
+ const useMerge = opts?.merge ?? (cfg.mergeWindowMs > 0 && cfg.mergeHoldActive != null && cfg.mergeHoldActive());
330
+ if (useMerge) {
331
+ resetMergeTimer();
332
+ ensureSettledSub();
333
+ return;
334
+ }
335
+ clearMergeTimer();
336
+ ensureSettledSub();
337
+ scheduleFlush(0);
338
+ }
339
+ async function sendChecked(msg) {
340
+ if (disposed) throw new Error("delivery handle disposed");
341
+ if (!port.supportedPayloads.includes(msg.payload.kind)) {
342
+ throw new Error(`unsupported payload kind: ${msg.payload.kind}`);
343
+ }
344
+ if (!passDedupe(msg)) return;
345
+ queue.push(msg);
346
+ ensureSettledSub();
347
+ return new Promise((resolve, reject) => {
348
+ checkedPending.push({ msg, resolve, reject });
349
+ if (!inFlight) {
350
+ const idx = queue.indexOf(msg);
351
+ if (idx !== -1) queue.splice(idx, 1);
352
+ inflightBatch = [msg];
353
+ inFlight = true;
354
+ sendAttempts = 0;
355
+ attemptSend();
356
+ }
357
+ });
358
+ }
359
+ function flush() {
360
+ if (disposed) return;
361
+ if (mergeTimer !== void 0) {
362
+ clearTimeout(mergeTimer);
363
+ mergeTimer = void 0;
364
+ }
365
+ scheduleFlush(0);
366
+ }
367
+ function depth() {
368
+ return queue.length + inflightBatch.length;
369
+ }
370
+ function dispose() {
371
+ disposed = true;
372
+ if (mergeTimer !== void 0) {
373
+ clearTimeout(mergeTimer);
374
+ mergeTimer = void 0;
375
+ }
376
+ if (backoffTimer !== void 0) {
377
+ clearTimeout(backoffTimer);
378
+ backoffTimer = void 0;
379
+ }
380
+ stopWatchdog();
381
+ teardownSettledSub();
382
+ queue.length = 0;
383
+ inflightBatch = [];
384
+ inFlight = false;
385
+ for (const w of checkedPending) {
386
+ w.reject(new Error("delivery handle disposed"));
387
+ }
388
+ checkedPending.length = 0;
389
+ dedupSet?.clear();
390
+ }
391
+ return { send, sendChecked, flush, depth, dispose };
392
+ }
393
+ export {
394
+ createDelivery
395
+ };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@zhushanwen/session-delivery",
3
+ "version": "0.10.0",
4
+ "description": "Session message delivery kernel: zero-pi-dependency strategy layer for queueing, batching, dedup, and gated flush of messages into pi sessions",
5
+ "main": "dist/index.mjs",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.mts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "import": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "license": "MIT",
19
+ "devDependencies": {
20
+ "@vitest/coverage-v8": "^4.1.9",
21
+ "tsup": "^8.5.1",
22
+ "typescript": "^5.7.0",
23
+ "vitest": "^4.1.6"
24
+ },
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "typecheck": "tsc --noEmit",
28
+ "test": "vitest run"
29
+ }
30
+ }