@cjhyy/code-shell-chat 0.8.13 → 0.8.20
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 +4 -0
- package/dist/delivery-queue.js +76 -53
- package/package.json +1 -1
package/dist/delivery-queue.d.ts
CHANGED
|
@@ -47,12 +47,16 @@ export declare class DeliveryQueue {
|
|
|
47
47
|
private readonly deliver;
|
|
48
48
|
private readonly onError;
|
|
49
49
|
private readonly pending;
|
|
50
|
+
/** Enqueues that reserved capacity while attachments are materialized outside the mutation lock. */
|
|
51
|
+
private readonly preparing;
|
|
50
52
|
private readonly completed;
|
|
51
53
|
private readonly inFlight;
|
|
52
54
|
private readonly targetInFlight;
|
|
53
55
|
private mutation;
|
|
54
56
|
private retryTimer?;
|
|
55
57
|
private persistRetryTimer?;
|
|
58
|
+
/** Terminal records whose spool may be deleted after a successful state retry. */
|
|
59
|
+
private readonly persistRetryCleanups;
|
|
56
60
|
private stopped;
|
|
57
61
|
constructor(config: DeliveryQueueConfig, deliver: (adapterId: string, message: ChannelMessage) => Promise<void>, onError: (error: unknown, message: ChannelMessage) => void);
|
|
58
62
|
start(): Promise<void>;
|
package/dist/delivery-queue.js
CHANGED
|
@@ -34,12 +34,16 @@ export class DeliveryQueue {
|
|
|
34
34
|
deliver;
|
|
35
35
|
onError;
|
|
36
36
|
pending = [];
|
|
37
|
+
/** Enqueues that reserved capacity while attachments are materialized outside the mutation lock. */
|
|
38
|
+
preparing = new Map();
|
|
37
39
|
completed = new Map();
|
|
38
40
|
inFlight = new Set();
|
|
39
41
|
targetInFlight = new Map();
|
|
40
42
|
mutation = Promise.resolve();
|
|
41
43
|
retryTimer;
|
|
42
44
|
persistRetryTimer;
|
|
45
|
+
/** Terminal records whose spool may be deleted after a successful state retry. */
|
|
46
|
+
persistRetryCleanups = new Map();
|
|
43
47
|
stopped = true;
|
|
44
48
|
constructor(config, deliver, onError) {
|
|
45
49
|
this.config = config;
|
|
@@ -76,65 +80,78 @@ export class DeliveryQueue {
|
|
|
76
80
|
if (this.stopped)
|
|
77
81
|
throw new Error("Chat Gateway inbox is stopped");
|
|
78
82
|
const dedupeKey = deliveryDedupeKey(message);
|
|
79
|
-
|
|
80
|
-
await this.withMutation(async () => {
|
|
83
|
+
const id = randomUUID();
|
|
84
|
+
const reserved = await this.withMutation(async () => {
|
|
85
|
+
if (this.stopped)
|
|
86
|
+
throw new Error("Chat Gateway inbox is stopped");
|
|
81
87
|
this.pruneCompleted();
|
|
82
88
|
if (dedupeKey &&
|
|
83
89
|
(this.completed.has(dedupeKey) ||
|
|
84
|
-
this.pending.some((entry) => entry.dedupeKey === dedupeKey)
|
|
85
|
-
|
|
86
|
-
return;
|
|
90
|
+
this.pending.some((entry) => entry.dedupeKey === dedupeKey) ||
|
|
91
|
+
[...this.preparing.values()].some((candidate) => candidate === dedupeKey))) {
|
|
92
|
+
return false;
|
|
87
93
|
}
|
|
88
|
-
if (this.pending.length >= this.config.maxPending) {
|
|
94
|
+
if (this.pending.length + this.preparing.size >= this.config.maxPending) {
|
|
89
95
|
throw new DeliveryBackpressureError(this.config.maxPending);
|
|
90
96
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
...(spooled ? { spooled } : {}),
|
|
110
|
-
};
|
|
111
|
-
this.pending.push(record);
|
|
112
|
-
if (persistent)
|
|
113
|
-
await this.persist();
|
|
114
|
-
}
|
|
115
|
-
catch (error) {
|
|
116
|
-
if (record) {
|
|
117
|
-
const index = this.pending.findIndex((candidate) => candidate.id === record?.id);
|
|
118
|
-
if (index >= 0)
|
|
119
|
-
this.pending.splice(index, 1);
|
|
120
|
-
await this.discardSpool(record);
|
|
121
|
-
}
|
|
122
|
-
else if (spooled) {
|
|
123
|
-
await this.discardSpool({
|
|
97
|
+
this.preparing.set(id, dedupeKey);
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
if (!reserved)
|
|
101
|
+
return "duplicate";
|
|
102
|
+
// attachment.load() may perform an unbounded network read. The reservation
|
|
103
|
+
// above preserves dedupe/backpressure, while doing the I/O here prevents one
|
|
104
|
+
// slow attachment from blocking every enqueue and terminal state write.
|
|
105
|
+
const spooled = await this.spoolAttachments(id, message);
|
|
106
|
+
let record;
|
|
107
|
+
try {
|
|
108
|
+
await this.withMutation(async () => {
|
|
109
|
+
try {
|
|
110
|
+
if (this.stopped)
|
|
111
|
+
throw new Error("Chat Gateway inbox is stopped");
|
|
112
|
+
const persistent = Boolean(this.config.path && (!message.attachments?.length || spooled !== undefined));
|
|
113
|
+
const safeMessage = persistent ? serializableMessage(message, spooled) : message;
|
|
114
|
+
record = {
|
|
124
115
|
id,
|
|
116
|
+
...(dedupeKey ? { dedupeKey } : {}),
|
|
125
117
|
adapterId,
|
|
126
|
-
message,
|
|
118
|
+
message: safeMessage,
|
|
127
119
|
attempts: 0,
|
|
128
|
-
nextAttemptAt:
|
|
129
|
-
persistent
|
|
130
|
-
spooled,
|
|
131
|
-
}
|
|
120
|
+
nextAttemptAt: Date.now(),
|
|
121
|
+
persistent,
|
|
122
|
+
...(spooled ? { spooled } : {}),
|
|
123
|
+
};
|
|
124
|
+
this.pending.push(record);
|
|
125
|
+
if (persistent)
|
|
126
|
+
await this.persist();
|
|
132
127
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (record) {
|
|
130
|
+
const index = this.pending.findIndex((candidate) => candidate.id === record?.id);
|
|
131
|
+
if (index >= 0)
|
|
132
|
+
this.pending.splice(index, 1);
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
this.preparing.delete(id);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
await this.discardSpool(record ?? {
|
|
143
|
+
id,
|
|
144
|
+
adapterId,
|
|
145
|
+
message,
|
|
146
|
+
attempts: 0,
|
|
147
|
+
nextAttemptAt: 0,
|
|
148
|
+
persistent: false,
|
|
149
|
+
...(spooled ? { spooled } : {}),
|
|
150
|
+
});
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
136
153
|
this.pump();
|
|
137
|
-
return
|
|
154
|
+
return "queued";
|
|
138
155
|
}
|
|
139
156
|
pump() {
|
|
140
157
|
if (this.stopped)
|
|
@@ -215,6 +232,8 @@ export class DeliveryQueue {
|
|
|
215
232
|
this.pump();
|
|
216
233
|
}
|
|
217
234
|
schedulePersistRetry(message, cleanup) {
|
|
235
|
+
if (cleanup)
|
|
236
|
+
this.persistRetryCleanups.set(cleanup.id, { entry: cleanup, message });
|
|
218
237
|
if (this.stopped || !this.config.path || this.persistRetryTimer)
|
|
219
238
|
return;
|
|
220
239
|
const delay = Math.max(10, Math.min(this.config.retryBaseMs, this.config.retryMaxMs));
|
|
@@ -224,11 +243,14 @@ export class DeliveryQueue {
|
|
|
224
243
|
return;
|
|
225
244
|
void this.withMutation(async () => {
|
|
226
245
|
await this.persist();
|
|
227
|
-
|
|
228
|
-
await this.discardSpool(
|
|
246
|
+
for (const [id, pendingCleanup] of this.persistRetryCleanups) {
|
|
247
|
+
await this.discardSpool(pendingCleanup.entry);
|
|
248
|
+
this.persistRetryCleanups.delete(id);
|
|
249
|
+
}
|
|
229
250
|
}).catch((error) => {
|
|
230
|
-
this.
|
|
231
|
-
this.
|
|
251
|
+
const relatedMessage = this.persistRetryCleanups.values().next().value?.message ?? message;
|
|
252
|
+
this.onError(error, relatedMessage);
|
|
253
|
+
this.schedulePersistRetry(relatedMessage);
|
|
232
254
|
});
|
|
233
255
|
}, delay);
|
|
234
256
|
this.persistRetryTimer.unref?.();
|
|
@@ -527,7 +549,8 @@ function validStoredEntry(entry, maxAttachmentBytes) {
|
|
|
527
549
|
value.message.attachments) {
|
|
528
550
|
return false;
|
|
529
551
|
}
|
|
530
|
-
if (value.dedupeKey !== undefined &&
|
|
552
|
+
if (value.dedupeKey !== undefined &&
|
|
553
|
+
(typeof value.dedupeKey !== "string" || value.dedupeKey.length > 16_384)) {
|
|
531
554
|
return false;
|
|
532
555
|
}
|
|
533
556
|
if (value.spooled === undefined)
|