@cjhyy/code-shell-chat 0.8.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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +416 -0
  3. package/dist/adapter-factory.d.ts +30 -0
  4. package/dist/adapter-factory.js +198 -0
  5. package/dist/attachments.d.ts +13 -0
  6. package/dist/attachments.js +74 -0
  7. package/dist/channel.d.ts +112 -0
  8. package/dist/channel.js +60 -0
  9. package/dist/chat-gateway.d.ts +58 -0
  10. package/dist/chat-gateway.js +232 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +259 -0
  13. package/dist/codeshell.d.ts +6 -0
  14. package/dist/codeshell.js +6 -0
  15. package/dist/config.d.ts +125 -0
  16. package/dist/config.js +469 -0
  17. package/dist/delivery-queue.d.ts +93 -0
  18. package/dist/delivery-queue.js +401 -0
  19. package/dist/desktop-control-client.d.ts +51 -0
  20. package/dist/desktop-control-client.js +309 -0
  21. package/dist/dingtalk.d.ts +19 -0
  22. package/dist/dingtalk.js +93 -0
  23. package/dist/discord.d.ts +23 -0
  24. package/dist/discord.js +134 -0
  25. package/dist/gateway.d.ts +34 -0
  26. package/dist/gateway.js +287 -0
  27. package/dist/index.d.ts +13 -0
  28. package/dist/index.js +13 -0
  29. package/dist/ingress.d.ts +8 -0
  30. package/dist/ingress.js +61 -0
  31. package/dist/instance-lock.d.ts +24 -0
  32. package/dist/instance-lock.js +100 -0
  33. package/dist/lark.d.ts +20 -0
  34. package/dist/lark.js +184 -0
  35. package/dist/lifecycle.d.ts +7 -0
  36. package/dist/lifecycle.js +16 -0
  37. package/dist/line.d.ts +17 -0
  38. package/dist/line.js +123 -0
  39. package/dist/matrix.d.ts +21 -0
  40. package/dist/matrix.js +208 -0
  41. package/dist/mattermost.d.ts +18 -0
  42. package/dist/mattermost.js +170 -0
  43. package/dist/media.d.ts +39 -0
  44. package/dist/media.js +223 -0
  45. package/dist/notification-progress.d.ts +46 -0
  46. package/dist/notification-progress.js +299 -0
  47. package/dist/notification-relay.d.ts +43 -0
  48. package/dist/notification-relay.js +285 -0
  49. package/dist/platform-canary.d.ts +22 -0
  50. package/dist/platform-canary.js +55 -0
  51. package/dist/protocol.d.ts +102 -0
  52. package/dist/protocol.js +1 -0
  53. package/dist/service-manager.d.ts +47 -0
  54. package/dist/service-manager.js +222 -0
  55. package/dist/slack.d.ts +17 -0
  56. package/dist/slack.js +136 -0
  57. package/dist/teams.d.ts +26 -0
  58. package/dist/teams.js +193 -0
  59. package/dist/telegram.d.ts +33 -0
  60. package/dist/telegram.js +263 -0
  61. package/dist/webhook.d.ts +26 -0
  62. package/dist/webhook.js +91 -0
  63. package/dist/wechat-login.d.ts +21 -0
  64. package/dist/wechat-login.js +97 -0
  65. package/dist/wechat-storage.d.ts +61 -0
  66. package/dist/wechat-storage.js +251 -0
  67. package/dist/wechat.d.ts +129 -0
  68. package/dist/wechat.js +1082 -0
  69. package/dist/wecom.d.ts +16 -0
  70. package/dist/wecom.js +149 -0
  71. package/dist/whatsapp.d.ts +27 -0
  72. package/dist/whatsapp.js +218 -0
  73. package/package.json +123 -0
@@ -0,0 +1,170 @@
1
+ import WebSocket from "ws";
2
+ import { BUILTIN_CHANNEL_CAPABILITIES } from "./channel.js";
3
+ import { dispatchSafely, formatOutgoingMarkdown } from "./lifecycle.js";
4
+ import { mediaKind, outgoingAttachments, remoteAttachment, safeAttachmentName } from "./media.js";
5
+ export class MattermostAdapter {
6
+ config;
7
+ fetchFn;
8
+ channel = "mattermost";
9
+ capabilities = BUILTIN_CHANNEL_CAPABILITIES.mattermost;
10
+ combinesTextAndAttachmentsAtomically = true;
11
+ constructor(config, fetchFn = fetch) {
12
+ this.config = config;
13
+ this.fetchFn = fetchFn;
14
+ }
15
+ async run(handler, signal) {
16
+ let retryMs = 1_000;
17
+ while (!signal.aborted) {
18
+ try {
19
+ await this.connect(handler, signal);
20
+ retryMs = 1_000;
21
+ }
22
+ catch {
23
+ if (signal.aborted)
24
+ return;
25
+ await abortableDelay(retryMs, signal);
26
+ retryMs = Math.min(retryMs * 2, 30_000);
27
+ }
28
+ }
29
+ }
30
+ async send(target, message) {
31
+ const attachments = outgoingAttachments(message, this.capabilities.outbound.attachments);
32
+ const fileIds = [];
33
+ for (const attachment of attachments) {
34
+ const form = new FormData();
35
+ form.set("channel_id", target);
36
+ form.set("files", new Blob([new Uint8Array(attachment.data)], { type: attachment.mimeType }), safeAttachmentName(attachment.name));
37
+ const upload = await this.fetchFn(`${this.config.serverUrl}/api/v4/files`, {
38
+ method: "POST",
39
+ headers: { authorization: `Bearer ${this.config.botToken}` },
40
+ body: form,
41
+ signal: AbortSignal.timeout(30_000),
42
+ });
43
+ const uploadBody = (await upload.json().catch(() => ({})));
44
+ const id = uploadBody.file_infos?.[0]?.id;
45
+ if (!upload.ok || !id) {
46
+ throw new Error(uploadBody.message ?? `Mattermost 附件上传失败(HTTP ${upload.status})`);
47
+ }
48
+ fileIds.push(id);
49
+ }
50
+ const response = await this.fetchFn(`${this.config.serverUrl}/api/v4/posts`, {
51
+ method: "POST",
52
+ headers: {
53
+ authorization: `Bearer ${this.config.botToken}`,
54
+ "content-type": "application/json",
55
+ },
56
+ body: JSON.stringify({
57
+ channel_id: target,
58
+ message: formatOutgoingMarkdown(message.text, message.button),
59
+ ...(fileIds.length > 0 ? { file_ids: fileIds } : {}),
60
+ }),
61
+ signal: AbortSignal.timeout(15_000),
62
+ });
63
+ if (!response.ok) {
64
+ const body = (await response.json().catch(() => ({})));
65
+ throw new Error(body.message ?? `Mattermost 发送失败(HTTP ${response.status})`);
66
+ }
67
+ }
68
+ connect(handler, signal) {
69
+ return new Promise((resolve, reject) => {
70
+ const socket = new WebSocket(toWebSocketUrl(this.config.serverUrl));
71
+ let opened = false;
72
+ const abort = () => socket.close();
73
+ signal.addEventListener("abort", abort, { once: true });
74
+ socket.once("open", () => {
75
+ opened = true;
76
+ socket.send(JSON.stringify({
77
+ seq: 1,
78
+ action: "authentication_challenge",
79
+ data: { token: this.config.botToken },
80
+ }));
81
+ });
82
+ socket.on("message", (raw) => this.onMessage(raw, handler, socket));
83
+ socket.once("error", (error) => {
84
+ if (!opened)
85
+ reject(error);
86
+ });
87
+ socket.once("close", () => {
88
+ signal.removeEventListener("abort", abort);
89
+ resolve();
90
+ });
91
+ });
92
+ }
93
+ onMessage(raw, handler, socket) {
94
+ let event;
95
+ try {
96
+ event = JSON.parse(raw.toString());
97
+ }
98
+ catch {
99
+ return;
100
+ }
101
+ if (event.data?.error?.message) {
102
+ socket.close(1008, "authentication failed");
103
+ return;
104
+ }
105
+ if (event.event !== "posted" || !event.data?.post)
106
+ return;
107
+ let post;
108
+ try {
109
+ post =
110
+ typeof event.data.post === "string"
111
+ ? JSON.parse(event.data.post)
112
+ : event.data.post;
113
+ }
114
+ catch {
115
+ return;
116
+ }
117
+ if (!post.channel_id ||
118
+ !post.user_id ||
119
+ (!post.message && !post.file_ids?.length && !post.metadata?.files?.length) ||
120
+ post.user_id === this.config.botUserId) {
121
+ return;
122
+ }
123
+ const infos = post.metadata?.files ?? [];
124
+ const infoById = new Map(infos.flatMap((info) => (info.id ? [[info.id, info]] : [])));
125
+ const attachmentIds = post.file_ids?.length
126
+ ? post.file_ids
127
+ : infos.flatMap((info) => (info.id ? [info.id] : []));
128
+ const attachments = attachmentIds.map((id) => {
129
+ const info = infoById.get(id);
130
+ return remoteAttachment({
131
+ id,
132
+ kind: mediaKind(info?.mime_type, info?.name),
133
+ name: info?.name ?? `mattermost-${id}`,
134
+ mimeType: info?.mime_type,
135
+ size: info?.size,
136
+ url: `${this.config.serverUrl}/api/v4/files/${encodeURIComponent(id)}`,
137
+ headers: { authorization: `Bearer ${this.config.botToken}` },
138
+ fetch: this.fetchFn,
139
+ allowPrivateNetwork: true,
140
+ });
141
+ });
142
+ void dispatchSafely(handler, {
143
+ channel: this.channel,
144
+ target: post.channel_id,
145
+ senderId: post.user_id,
146
+ text: post.message ?? "",
147
+ ...(post.id ? { messageId: post.id } : {}),
148
+ ...(attachments.length > 0 ? { attachments } : {}),
149
+ });
150
+ }
151
+ }
152
+ function toWebSocketUrl(serverUrl) {
153
+ const url = new URL(serverUrl);
154
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
155
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/api/v4/websocket`;
156
+ return url.toString();
157
+ }
158
+ async function abortableDelay(ms, signal) {
159
+ if (signal.aborted)
160
+ return;
161
+ await new Promise((resolve) => {
162
+ const timer = setTimeout(done, ms);
163
+ signal.addEventListener("abort", done, { once: true });
164
+ function done() {
165
+ clearTimeout(timer);
166
+ signal.removeEventListener("abort", done);
167
+ resolve();
168
+ }
169
+ });
170
+ }
@@ -0,0 +1,39 @@
1
+ import type { ChatAttachment, ChatAttachmentKind, OutgoingAttachment, OutgoingMessage } from "./channel.js";
2
+ export declare const MAX_CHANNEL_ATTACHMENTS = 4;
3
+ export declare const MAX_CHANNEL_ATTACHMENT_BYTES: number;
4
+ export interface RemoteAttachmentOptions {
5
+ id: string;
6
+ kind?: ChatAttachmentKind;
7
+ name?: string;
8
+ mimeType?: string;
9
+ size?: number;
10
+ url: string;
11
+ headers?: Readonly<Record<string, string>>;
12
+ fetch?: typeof fetch;
13
+ /** Only for an operator-configured self-hosted platform origin. */
14
+ allowPrivateNetwork?: boolean;
15
+ }
16
+ /** Infer the Pet/Gateway media kind from a trusted platform MIME or filename. */
17
+ export declare function mediaKind(mimeType?: string, name?: string): ChatAttachmentKind;
18
+ /** Create a lazy platform attachment; no network request occurs before load(). */
19
+ export declare function remoteAttachment(options: RemoteAttachmentOptions): ChatAttachment;
20
+ interface DownloadRemoteAttachmentOptions extends Pick<RequestInit, "headers" | "signal"> {
21
+ allowPrivateNetwork?: boolean;
22
+ }
23
+ /** Bounded streaming download shared by authenticated platform media URLs. */
24
+ export declare function downloadRemoteAttachment(fetchFn: typeof fetch, url: string, init?: DownloadRemoteAttachmentOptions, maximum?: number): Promise<Uint8Array>;
25
+ /** Validate adapter-facing media before any platform request is made. */
26
+ export declare function outgoingAttachments(message: OutgoingMessage, supported: readonly ChatAttachmentKind[]): readonly OutgoingAttachment[];
27
+ export declare function safeAttachmentName(value: string | undefined, fallback?: string): string;
28
+ /**
29
+ * Resume a multi-request adapter send at the first unfinished visible step.
30
+ * DeliveryQueue retries the same cached OutgoingMessage object for Mimi turns,
31
+ * so this prevents a failed second attachment from duplicating earlier text.
32
+ * A failure while awaiting one platform call remains honestly at-least-once:
33
+ * the remote service may have accepted it without returning an acknowledgement.
34
+ */
35
+ export declare class OutgoingDeliveryTracker {
36
+ private readonly plans;
37
+ run(message: OutgoingMessage, createSteps: () => readonly (() => Promise<void>)[] | Promise<readonly (() => Promise<void>)[]>): Promise<void>;
38
+ }
39
+ export {};
package/dist/media.js ADDED
@@ -0,0 +1,223 @@
1
+ import { isIP } from "node:net";
2
+ export const MAX_CHANNEL_ATTACHMENTS = 4;
3
+ export const MAX_CHANNEL_ATTACHMENT_BYTES = 10 * 1024 * 1024;
4
+ /** Infer the Pet/Gateway media kind from a trusted platform MIME or filename. */
5
+ export function mediaKind(mimeType, name) {
6
+ const normalized = mimeType?.toLowerCase().split(";", 1)[0]?.trim() ?? "";
7
+ if (normalized.startsWith("image/"))
8
+ return "image";
9
+ if (normalized.startsWith("audio/"))
10
+ return "audio";
11
+ if (normalized.startsWith("video/"))
12
+ return "video";
13
+ const extension = name?.toLowerCase().match(/\.([a-z0-9]{1,10})$/)?.[1];
14
+ if (extension &&
15
+ ["png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff"].includes(extension)) {
16
+ return "image";
17
+ }
18
+ if (extension &&
19
+ ["mp3", "m4a", "aac", "wav", "ogg", "opus", "flac", "amr", "silk"].includes(extension)) {
20
+ return "audio";
21
+ }
22
+ if (extension && ["mp4", "mov", "webm", "mkv", "avi", "3gp"].includes(extension)) {
23
+ return "video";
24
+ }
25
+ return "file";
26
+ }
27
+ /** Create a lazy platform attachment; no network request occurs before load(). */
28
+ export function remoteAttachment(options) {
29
+ const name = safeAttachmentName(options.name, options.id);
30
+ return {
31
+ id: options.id,
32
+ kind: options.kind ?? mediaKind(options.mimeType, name),
33
+ name,
34
+ ...(options.mimeType?.trim() ? { mimeType: options.mimeType.trim() } : {}),
35
+ ...(options.size !== undefined ? { size: options.size } : {}),
36
+ load: (signal) => downloadRemoteAttachment(options.fetch ?? fetch, options.url, {
37
+ ...(options.headers ? { headers: options.headers } : {}),
38
+ ...(signal ? { signal } : {}),
39
+ ...(options.allowPrivateNetwork ? { allowPrivateNetwork: true } : {}),
40
+ }),
41
+ };
42
+ }
43
+ /** Bounded streaming download shared by authenticated platform media URLs. */
44
+ export async function downloadRemoteAttachment(fetchFn, url, init = {}, maximum = MAX_CHANNEL_ATTACHMENT_BYTES) {
45
+ const signal = init.signal
46
+ ? AbortSignal.any([init.signal, AbortSignal.timeout(30_000)])
47
+ : AbortSignal.timeout(30_000);
48
+ let currentUrl = url;
49
+ let currentHeaders = new Headers(init.headers);
50
+ let response;
51
+ for (let redirects = 0; redirects <= 5; redirects += 1) {
52
+ assertSafeMediaUrl(currentUrl, init.allowPrivateNetwork ?? false);
53
+ response = await fetchFn(currentUrl, {
54
+ method: "GET",
55
+ headers: currentHeaders,
56
+ signal,
57
+ redirect: "manual",
58
+ });
59
+ if (![301, 302, 303, 307, 308].includes(response.status))
60
+ break;
61
+ const location = response.headers.get("location");
62
+ if (!location || redirects === 5)
63
+ throw new Error("附件下载重定向无效");
64
+ const nextUrl = new URL(location, currentUrl);
65
+ if (nextUrl.origin !== new URL(currentUrl).origin) {
66
+ currentHeaders = new Headers(currentHeaders);
67
+ currentHeaders.delete("authorization");
68
+ currentHeaders.delete("cookie");
69
+ currentHeaders.delete("proxy-authorization");
70
+ }
71
+ currentUrl = nextUrl.toString();
72
+ }
73
+ if (!response)
74
+ throw new Error("附件下载失败");
75
+ if (!response.ok)
76
+ throw new Error(`附件下载失败(HTTP ${response.status})`);
77
+ const declared = Number(response.headers.get("content-length"));
78
+ if (Number.isFinite(declared) && declared > maximum)
79
+ throw new Error("附件超过大小限制");
80
+ if (!response.body) {
81
+ const bytes = new Uint8Array(await response.arrayBuffer());
82
+ if (bytes.byteLength > maximum)
83
+ throw new Error("附件超过大小限制");
84
+ return bytes;
85
+ }
86
+ const reader = response.body.getReader();
87
+ const chunks = [];
88
+ let total = 0;
89
+ try {
90
+ while (true) {
91
+ const { done, value } = await reader.read();
92
+ if (done)
93
+ break;
94
+ total += value.byteLength;
95
+ if (total > maximum) {
96
+ await reader.cancel();
97
+ throw new Error("附件超过大小限制");
98
+ }
99
+ chunks.push(value);
100
+ }
101
+ }
102
+ finally {
103
+ reader.releaseLock();
104
+ }
105
+ const output = new Uint8Array(total);
106
+ let offset = 0;
107
+ for (const chunk of chunks) {
108
+ output.set(chunk, offset);
109
+ offset += chunk.byteLength;
110
+ }
111
+ return output;
112
+ }
113
+ function assertSafeMediaUrl(value, allowPrivateNetwork) {
114
+ let url;
115
+ try {
116
+ url = new URL(value);
117
+ }
118
+ catch {
119
+ throw new Error("附件下载地址无效");
120
+ }
121
+ if (url.protocol !== "https:" && !(allowPrivateNetwork && url.protocol === "http:")) {
122
+ throw new Error("附件下载地址必须使用 HTTPS");
123
+ }
124
+ if (allowPrivateNetwork)
125
+ return;
126
+ const host = url.hostname.toLowerCase().replace(/^\[|\]$/gu, "");
127
+ if (host === "localhost" ||
128
+ host.endsWith(".localhost") ||
129
+ host.endsWith(".local") ||
130
+ host.endsWith(".internal") ||
131
+ isPrivateIpLiteral(host)) {
132
+ throw new Error("附件下载地址不能指向本机或私有网络");
133
+ }
134
+ }
135
+ function isPrivateIpLiteral(host) {
136
+ const family = isIP(host);
137
+ if (family === 4) {
138
+ const [a = 0, b = 0] = host.split(".").map(Number);
139
+ return (a === 0 ||
140
+ a === 10 ||
141
+ a === 127 ||
142
+ (a === 169 && b === 254) ||
143
+ (a === 172 && b >= 16 && b <= 31) ||
144
+ (a === 192 && b === 168) ||
145
+ a >= 224);
146
+ }
147
+ if (family === 6) {
148
+ const normalized = host.toLowerCase();
149
+ return (normalized === "::" ||
150
+ normalized === "::1" ||
151
+ normalized.startsWith("fc") ||
152
+ normalized.startsWith("fd") ||
153
+ normalized.startsWith("fe8") ||
154
+ normalized.startsWith("fe9") ||
155
+ normalized.startsWith("fea") ||
156
+ normalized.startsWith("feb") ||
157
+ normalized.startsWith("::ffff:127.") ||
158
+ normalized.startsWith("::ffff:10.") ||
159
+ normalized.startsWith("::ffff:192.168."));
160
+ }
161
+ return false;
162
+ }
163
+ /** Validate adapter-facing media before any platform request is made. */
164
+ export function outgoingAttachments(message, supported) {
165
+ const attachments = message.attachments ?? [];
166
+ if (attachments.length > MAX_CHANNEL_ATTACHMENTS) {
167
+ throw new Error(`单条消息最多发送 ${MAX_CHANNEL_ATTACHMENTS} 个附件`);
168
+ }
169
+ for (const attachment of attachments) {
170
+ if (!supported.includes(attachment.kind)) {
171
+ throw new Error(`当前渠道不支持发送 ${attachment.kind} 附件`);
172
+ }
173
+ if (!(attachment.data instanceof Uint8Array) ||
174
+ attachment.data.byteLength < 1 ||
175
+ attachment.data.byteLength > MAX_CHANNEL_ATTACHMENT_BYTES) {
176
+ throw new Error(`附件 ${attachment.name || "attachment"} 的大小不受支持`);
177
+ }
178
+ if (!attachment.mimeType.trim())
179
+ throw new Error("附件 MIME 类型不能为空");
180
+ if (attachment.kind === "image" && !attachment.mimeType.startsWith("image/")) {
181
+ throw new Error(`图片 ${attachment.name} 的 MIME 类型无效`);
182
+ }
183
+ }
184
+ return attachments;
185
+ }
186
+ export function safeAttachmentName(value, fallback = "attachment") {
187
+ const normalized = (value ?? "")
188
+ .replaceAll("\\", "/")
189
+ .split("/")
190
+ .at(-1)
191
+ ?.replace(/[\u0000-\u001f\u007f]/gu, "")
192
+ .trim()
193
+ .slice(0, 255);
194
+ return (normalized || fallback.replace(/[\u0000-\u001f\u007f]/gu, "").slice(0, 255) || "attachment");
195
+ }
196
+ /**
197
+ * Resume a multi-request adapter send at the first unfinished visible step.
198
+ * DeliveryQueue retries the same cached OutgoingMessage object for Mimi turns,
199
+ * so this prevents a failed second attachment from duplicating earlier text.
200
+ * A failure while awaiting one platform call remains honestly at-least-once:
201
+ * the remote service may have accepted it without returning an acknowledgement.
202
+ */
203
+ export class OutgoingDeliveryTracker {
204
+ plans = new WeakMap();
205
+ async run(message, createSteps) {
206
+ let plan = this.plans.get(message);
207
+ if (!plan) {
208
+ plan = { steps: Promise.resolve().then(createSteps), next: 0 };
209
+ this.plans.set(message, plan);
210
+ plan.steps.catch(() => {
211
+ if (this.plans.get(message) === plan)
212
+ this.plans.delete(message);
213
+ });
214
+ }
215
+ const steps = await plan.steps;
216
+ while (plan.next < steps.length) {
217
+ await steps[plan.next]();
218
+ plan.next += 1;
219
+ }
220
+ if (this.plans.get(message) === plan)
221
+ this.plans.delete(message);
222
+ }
223
+ }
@@ -0,0 +1,46 @@
1
+ export interface NotificationEventDeliveryProgressState {
2
+ chunks: Record<string, number>;
3
+ /** Number of attachment descriptors accepted for each opaque target. */
4
+ attachments: Record<string, number>;
5
+ }
6
+ export interface NotificationDeliveryProgressState {
7
+ version: 3;
8
+ events: Record<string, NotificationEventDeliveryProgressState>;
9
+ }
10
+ export interface NotificationDeliveryProgressStore {
11
+ load(): Promise<NotificationDeliveryProgressState | undefined>;
12
+ save(state: NotificationDeliveryProgressState): Promise<void>;
13
+ }
14
+ /** Keep transport progress next to, but independent from, the Desktop event cursor. */
15
+ export declare function notificationDeliveryProgressPath(eventCursorPath: string): string;
16
+ /** Opaque key: the progress file never needs to disclose a raw owner destination. */
17
+ export declare function notificationTargetProgressKey(channel: string, target: string): string;
18
+ /** Owner-only, atomic persistence for partially delivered notification events. */
19
+ export declare class FileNotificationDeliveryProgressStore implements NotificationDeliveryProgressStore {
20
+ readonly path: string;
21
+ constructor(path: string);
22
+ load(): Promise<NotificationDeliveryProgressState | undefined>;
23
+ save(state: NotificationDeliveryProgressState): Promise<void>;
24
+ }
25
+ /**
26
+ * Serializes progress mutations made by targets delivering in parallel. A
27
+ * successful transport call is recorded before the event handler returns, so
28
+ * a later target failure or ordinary process restart resumes at the owed part.
29
+ */
30
+ export declare class NotificationDeliveryProgress {
31
+ private readonly store?;
32
+ private state?;
33
+ private loadTask?;
34
+ private writeTail;
35
+ constructor(store?: NotificationDeliveryProgressStore | undefined);
36
+ begin(eventKey: string): Promise<void>;
37
+ chunkIndex(eventKey: string, targetKey: string): number;
38
+ attachmentIndex(eventKey: string, targetKey: string): number;
39
+ mark(eventKey: string, targetKey: string, chunkIndex: number, attachmentIndex?: number): Promise<void>;
40
+ /** Retry a previously failed state write before the outer event is acknowledged. */
41
+ flush(): Promise<void>;
42
+ private ensureLoaded;
43
+ private persistCurrent;
44
+ private requireState;
45
+ private requireEventState;
46
+ }