@cjhyy/code-shell-chat 0.9.46 → 0.9.47
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/delivery-queue.d.ts +9 -0
- package/dist/delivery-queue.js +49 -6
- package/package.json +1 -1
package/dist/delivery-queue.d.ts
CHANGED
|
@@ -7,6 +7,13 @@ export interface DeliveryQueueConfig {
|
|
|
7
7
|
retryBaseMs: number;
|
|
8
8
|
retryMaxMs: number;
|
|
9
9
|
completedTtlMs: number;
|
|
10
|
+
/**
|
|
11
|
+
* Failed attempts during which a retrying record keeps later same-target
|
|
12
|
+
* messages waiting. Past this, it keeps retrying without holding its target,
|
|
13
|
+
* so one message that always fails cannot block a conversation forever.
|
|
14
|
+
* Defaults to {@link DEFAULT_RETRY_HOLD_ATTEMPTS}.
|
|
15
|
+
*/
|
|
16
|
+
retryHoldAttempts?: number;
|
|
10
17
|
/**
|
|
11
18
|
* Directory for spooled attachment bytes. Defaults to `<path>.attachments`.
|
|
12
19
|
*
|
|
@@ -21,6 +28,8 @@ export interface DeliveryQueueConfig {
|
|
|
21
28
|
/** Largest single attachment to spool. Bigger ones fall back to memory-only. */
|
|
22
29
|
maxSpooledAttachmentBytes?: number;
|
|
23
30
|
}
|
|
31
|
+
/** With default backoff (1s doubling to 30s) this holds a target for about two minutes. */
|
|
32
|
+
export declare const DEFAULT_RETRY_HOLD_ATTEMPTS = 8;
|
|
24
33
|
export interface DeliveryQueueStatus {
|
|
25
34
|
pending: number;
|
|
26
35
|
inFlight: number;
|
package/dist/delivery-queue.js
CHANGED
|
@@ -2,6 +2,8 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { chmod, lstat, mkdir, open, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
|
+
/** With default backoff (1s doubling to 30s) this holds a target for about two minutes. */
|
|
6
|
+
export const DEFAULT_RETRY_HOLD_ATTEMPTS = 8;
|
|
5
7
|
const MAX_DELIVERY_STATE_BYTES = 64 * 1024 * 1024;
|
|
6
8
|
const MAX_COMPLETED_KEYS = 100_000;
|
|
7
9
|
const MAX_SPOOLED_ATTACHMENTS = 32;
|
|
@@ -69,6 +71,11 @@ export class DeliveryQueue {
|
|
|
69
71
|
*/
|
|
70
72
|
async stop() {
|
|
71
73
|
this.stopped = true;
|
|
74
|
+
// A duplicate must not be acknowledged while the original is still loading
|
|
75
|
+
// attachments. Wake those waiters without waiting on unbounded media I/O.
|
|
76
|
+
for (const preparation of this.preparing.values()) {
|
|
77
|
+
preparation.reject(new Error("Chat Gateway inbox is stopped"));
|
|
78
|
+
}
|
|
72
79
|
if (this.retryTimer)
|
|
73
80
|
clearTimeout(this.retryTimer);
|
|
74
81
|
this.retryTimer = undefined;
|
|
@@ -99,6 +106,8 @@ export class DeliveryQueue {
|
|
|
99
106
|
let previousPreparation;
|
|
100
107
|
let preparation;
|
|
101
108
|
let releasePreparation;
|
|
109
|
+
let acceptance;
|
|
110
|
+
let duplicateAcceptance;
|
|
102
111
|
const reserved = await this.withMutation(async () => {
|
|
103
112
|
if (this.stopped)
|
|
104
113
|
throw new Error("Chat Gateway inbox is stopped");
|
|
@@ -108,14 +117,29 @@ export class DeliveryQueue {
|
|
|
108
117
|
// Completed keys persisted before keys named the account cannot say
|
|
109
118
|
// which account accepted them; stay conservative until they expire.
|
|
110
119
|
(legacyDedupeKey !== undefined && this.completed.has(legacyDedupeKey)) ||
|
|
111
|
-
this.pending.some((entry) => entry.dedupeKey === dedupeKey)
|
|
112
|
-
[...this.preparing.values()].some((candidate) => candidate === dedupeKey))) {
|
|
120
|
+
this.pending.some((entry) => entry.dedupeKey === dedupeKey))) {
|
|
113
121
|
return false;
|
|
114
122
|
}
|
|
123
|
+
if (dedupeKey) {
|
|
124
|
+
const duplicate = [...this.preparing.values()].find((candidate) => candidate.dedupeKey === dedupeKey);
|
|
125
|
+
if (duplicate) {
|
|
126
|
+
duplicateAcceptance = duplicate.accepted;
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
115
130
|
if (this.pending.length + this.preparing.size >= this.config.maxPending) {
|
|
116
131
|
throw new DeliveryBackpressureError(this.config.maxPending);
|
|
117
132
|
}
|
|
118
|
-
|
|
133
|
+
let resolve;
|
|
134
|
+
let reject;
|
|
135
|
+
const accepted = new Promise((yes, no) => {
|
|
136
|
+
resolve = yes;
|
|
137
|
+
reject = no;
|
|
138
|
+
});
|
|
139
|
+
// The original enqueue may have no duplicate waiting on this promise.
|
|
140
|
+
void accepted.catch(() => undefined);
|
|
141
|
+
acceptance = { dedupeKey, accepted, resolve, reject };
|
|
142
|
+
this.preparing.set(id, acceptance);
|
|
119
143
|
previousPreparation = this.preparationByTarget.get(target);
|
|
120
144
|
preparation = new Promise((resolve) => {
|
|
121
145
|
releasePreparation = resolve;
|
|
@@ -123,8 +147,11 @@ export class DeliveryQueue {
|
|
|
123
147
|
this.preparationByTarget.set(target, preparation);
|
|
124
148
|
return true;
|
|
125
149
|
});
|
|
126
|
-
if (!reserved)
|
|
150
|
+
if (!reserved) {
|
|
151
|
+
// Never hold the mutation lock while waiting for the original to persist.
|
|
152
|
+
await duplicateAcceptance;
|
|
127
153
|
return "duplicate";
|
|
154
|
+
}
|
|
128
155
|
let spooled;
|
|
129
156
|
let record;
|
|
130
157
|
try {
|
|
@@ -154,6 +181,7 @@ export class DeliveryQueue {
|
|
|
154
181
|
this.pending.push(record);
|
|
155
182
|
if (persistent)
|
|
156
183
|
await this.persist();
|
|
184
|
+
acceptance.resolve();
|
|
157
185
|
}
|
|
158
186
|
catch (error) {
|
|
159
187
|
if (record) {
|
|
@@ -169,6 +197,7 @@ export class DeliveryQueue {
|
|
|
169
197
|
});
|
|
170
198
|
}
|
|
171
199
|
catch (error) {
|
|
200
|
+
acceptance.reject(error);
|
|
172
201
|
this.preparing.delete(id);
|
|
173
202
|
await this.discardSpool(record ?? {
|
|
174
203
|
id,
|
|
@@ -200,11 +229,25 @@ export class DeliveryQueue {
|
|
|
200
229
|
clearTimeout(this.retryTimer);
|
|
201
230
|
this.retryTimer = undefined;
|
|
202
231
|
const now = Date.now();
|
|
232
|
+
const holdAttempts = this.config.retryHoldAttempts ?? DEFAULT_RETRY_HOLD_ATTEMPTS;
|
|
203
233
|
while (this.inFlight.size < this.config.maxConcurrent) {
|
|
234
|
+
const delayedTargets = new Set();
|
|
204
235
|
const entry = this.pending.find((candidate) => {
|
|
205
|
-
if (this.inFlight.has(candidate.id)
|
|
236
|
+
if (this.inFlight.has(candidate.id))
|
|
237
|
+
return false;
|
|
238
|
+
const key = targetKey(candidate.message);
|
|
239
|
+
if (delayedTargets.has(key))
|
|
206
240
|
return false;
|
|
207
|
-
|
|
241
|
+
if (candidate.nextAttemptAt > now) {
|
|
242
|
+
// Bounded hold: a record that keeps failing stops blocking its
|
|
243
|
+
// conversation but stays queued for its own retries.
|
|
244
|
+
if (candidate.attempts < holdAttempts)
|
|
245
|
+
delayedTargets.add(key);
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
// Already-running peers may share a target when configured, but a
|
|
249
|
+
// queued retry must not be overtaken by later messages for that target.
|
|
250
|
+
return (this.targetInFlight.get(key) ?? 0) < this.config.maxPerTarget;
|
|
208
251
|
});
|
|
209
252
|
if (!entry)
|
|
210
253
|
break;
|