mirror of https://github.com/openclaw/openclaw.git
fix(web): handle 515 Stream Error during WhatsApp QR pairing (#27910)
* fix(web): handle 515 Stream Error during WhatsApp QR pairing getStatusCode() never unwrapped the lastDisconnect wrapper object, so login.errorStatus was always undefined and the 515 restart path in restartLoginSocket was dead code. - Add err.error?.output?.statusCode fallback to getStatusCode() - Export waitForCredsSaveQueue() so callers can await pending creds - Await creds flush in restartLoginSocket before creating new socket Fixes #3942 * test: update session mock for getStatusCode unwrap + waitForCredsSaveQueue Mirror the getStatusCode fix (err.error?.output?.statusCode fallback) in the test mock and export waitForCredsSaveQueue so restartLoginSocket tests work correctly. * fix(web): scope creds save queue per-authDir to avoid cross-account blocking The credential save queue was a single global promise chain shared by all WhatsApp accounts. In multi-account setups, a slow save on one account blocked credential writes and 515 restart recovery for unrelated accounts. Replace the global queue with a per-authDir Map so each account's creds serialize independently. waitForCredsSaveQueue() now accepts an optional authDir to wait on a single account's queue, or waits on all when omitted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: use real Baileys v7 error shape in 515 restart test The test was using { output: { statusCode: 515 } } which was already handled before the fix. Updated to use the actual Baileys v7 shape { error: { output: { statusCode: 515 } } } to cover the new fallback path in getStatusCode. Co-Authored-By: Claude Code (Opus 4.6) <noreply@anthropic.com> * fix(web): bound credential-queue wait during 515 restart Prevents restartLoginSocket from blocking indefinitely if a queued saveCreds() promise stalls (e.g. hung filesystem write). Co-Authored-By: Claude <noreply@anthropic.com> * fix: clear flush timeout handle and assert creds queue in test Co-Authored-By: Claude <noreply@anthropic.com> * fix: evict settled credsSaveQueues entries to prevent unbounded growth Co-Authored-By: Claude <noreply@anthropic.com> * fix: share WhatsApp 515 creds flush handling (#27910) (thanks @asyncjason) --------- Co-authored-by: Jason Separovic <jason@wilma.dog> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
parent
843e3c1efb
commit
9d3e653ec9
|
|
@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
||||||
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
|
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
|
||||||
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
|
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
|
||||||
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) thanks @MonkeyLeeT.
|
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) thanks @MonkeyLeeT.
|
||||||
|
- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { startWebLoginWithQr, waitForWebLogin } from "./login-qr.js";
|
import { startWebLoginWithQr, waitForWebLogin } from "./login-qr.js";
|
||||||
import { createWaSocket, logoutWeb, waitForWaConnection } from "./session.js";
|
import {
|
||||||
|
createWaSocket,
|
||||||
|
logoutWeb,
|
||||||
|
waitForCredsSaveQueueWithTimeout,
|
||||||
|
waitForWaConnection,
|
||||||
|
} from "./session.js";
|
||||||
|
|
||||||
vi.mock("./session.js", () => {
|
vi.mock("./session.js", () => {
|
||||||
const createWaSocket = vi.fn(
|
const createWaSocket = vi.fn(
|
||||||
|
|
@ -17,11 +22,13 @@ vi.mock("./session.js", () => {
|
||||||
const getStatusCode = vi.fn(
|
const getStatusCode = vi.fn(
|
||||||
(err: unknown) =>
|
(err: unknown) =>
|
||||||
(err as { output?: { statusCode?: number } })?.output?.statusCode ??
|
(err as { output?: { statusCode?: number } })?.output?.statusCode ??
|
||||||
(err as { status?: number })?.status,
|
(err as { status?: number })?.status ??
|
||||||
|
(err as { error?: { output?: { statusCode?: number } } })?.error?.output?.statusCode,
|
||||||
);
|
);
|
||||||
const webAuthExists = vi.fn(async () => false);
|
const webAuthExists = vi.fn(async () => false);
|
||||||
const readWebSelfId = vi.fn(() => ({ e164: null, jid: null }));
|
const readWebSelfId = vi.fn(() => ({ e164: null, jid: null }));
|
||||||
const logoutWeb = vi.fn(async () => true);
|
const logoutWeb = vi.fn(async () => true);
|
||||||
|
const waitForCredsSaveQueueWithTimeout = vi.fn(async () => {});
|
||||||
return {
|
return {
|
||||||
createWaSocket,
|
createWaSocket,
|
||||||
waitForWaConnection,
|
waitForWaConnection,
|
||||||
|
|
@ -30,6 +37,7 @@ vi.mock("./session.js", () => {
|
||||||
webAuthExists,
|
webAuthExists,
|
||||||
readWebSelfId,
|
readWebSelfId,
|
||||||
logoutWeb,
|
logoutWeb,
|
||||||
|
waitForCredsSaveQueueWithTimeout,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -39,22 +47,43 @@ vi.mock("./qr-image.js", () => ({
|
||||||
|
|
||||||
const createWaSocketMock = vi.mocked(createWaSocket);
|
const createWaSocketMock = vi.mocked(createWaSocket);
|
||||||
const waitForWaConnectionMock = vi.mocked(waitForWaConnection);
|
const waitForWaConnectionMock = vi.mocked(waitForWaConnection);
|
||||||
|
const waitForCredsSaveQueueWithTimeoutMock = vi.mocked(waitForCredsSaveQueueWithTimeout);
|
||||||
const logoutWebMock = vi.mocked(logoutWeb);
|
const logoutWebMock = vi.mocked(logoutWeb);
|
||||||
|
|
||||||
|
async function flushTasks() {
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
describe("login-qr", () => {
|
describe("login-qr", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("restarts login once on status 515 and completes", async () => {
|
it("restarts login once on status 515 and completes", async () => {
|
||||||
|
let releaseCredsFlush: (() => void) | undefined;
|
||||||
|
const credsFlushGate = new Promise<void>((resolve) => {
|
||||||
|
releaseCredsFlush = resolve;
|
||||||
|
});
|
||||||
waitForWaConnectionMock
|
waitForWaConnectionMock
|
||||||
.mockRejectedValueOnce({ output: { statusCode: 515 } })
|
// Baileys v7 wraps the error: { error: BoomError(515) }
|
||||||
|
.mockRejectedValueOnce({ error: { output: { statusCode: 515 } } })
|
||||||
.mockResolvedValueOnce(undefined);
|
.mockResolvedValueOnce(undefined);
|
||||||
|
waitForCredsSaveQueueWithTimeoutMock.mockReturnValueOnce(credsFlushGate);
|
||||||
|
|
||||||
const start = await startWebLoginWithQr({ timeoutMs: 5000 });
|
const start = await startWebLoginWithQr({ timeoutMs: 5000 });
|
||||||
expect(start.qrDataUrl).toBe("data:image/png;base64,base64");
|
expect(start.qrDataUrl).toBe("data:image/png;base64,base64");
|
||||||
|
|
||||||
const result = await waitForWebLogin({ timeoutMs: 5000 });
|
const resultPromise = waitForWebLogin({ timeoutMs: 5000 });
|
||||||
|
await flushTasks();
|
||||||
|
await flushTasks();
|
||||||
|
|
||||||
|
expect(createWaSocketMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(waitForCredsSaveQueueWithTimeoutMock).toHaveBeenCalledOnce();
|
||||||
|
expect(waitForCredsSaveQueueWithTimeoutMock).toHaveBeenCalledWith(expect.any(String));
|
||||||
|
|
||||||
|
releaseCredsFlush?.();
|
||||||
|
const result = await resultPromise;
|
||||||
|
|
||||||
expect(result.connected).toBe(true);
|
expect(result.connected).toBe(true);
|
||||||
expect(createWaSocketMock).toHaveBeenCalledTimes(2);
|
expect(createWaSocketMock).toHaveBeenCalledTimes(2);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
getStatusCode,
|
getStatusCode,
|
||||||
logoutWeb,
|
logoutWeb,
|
||||||
readWebSelfId,
|
readWebSelfId,
|
||||||
|
waitForCredsSaveQueueWithTimeout,
|
||||||
waitForWaConnection,
|
waitForWaConnection,
|
||||||
webAuthExists,
|
webAuthExists,
|
||||||
} from "./session.js";
|
} from "./session.js";
|
||||||
|
|
@ -85,9 +86,10 @@ async function restartLoginSocket(login: ActiveLogin, runtime: RuntimeEnv) {
|
||||||
}
|
}
|
||||||
login.restartAttempted = true;
|
login.restartAttempted = true;
|
||||||
runtime.log(
|
runtime.log(
|
||||||
info("WhatsApp asked for a restart after pairing (code 515); retrying connection once…"),
|
info("WhatsApp asked for a restart after pairing (code 515); waiting for creds to save…"),
|
||||||
);
|
);
|
||||||
closeSocket(login.sock);
|
closeSocket(login.sock);
|
||||||
|
await waitForCredsSaveQueueWithTimeout(login.authDir);
|
||||||
try {
|
try {
|
||||||
const sock = await createWaSocket(false, login.verbose, {
|
const sock = await createWaSocket(false, login.verbose, {
|
||||||
authDir: login.authDir,
|
authDir: login.authDir,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,12 @@ import path from "node:path";
|
||||||
import { DisconnectReason } from "@whiskeysockets/baileys";
|
import { DisconnectReason } from "@whiskeysockets/baileys";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { loginWeb } from "./login.js";
|
import { loginWeb } from "./login.js";
|
||||||
import { createWaSocket, formatError, waitForWaConnection } from "./session.js";
|
import {
|
||||||
|
createWaSocket,
|
||||||
|
formatError,
|
||||||
|
waitForCredsSaveQueueWithTimeout,
|
||||||
|
waitForWaConnection,
|
||||||
|
} from "./session.js";
|
||||||
|
|
||||||
const rmMock = vi.spyOn(fs, "rm");
|
const rmMock = vi.spyOn(fs, "rm");
|
||||||
|
|
||||||
|
|
@ -35,10 +40,19 @@ vi.mock("./session.js", () => {
|
||||||
const createWaSocket = vi.fn(async () => (call++ === 0 ? sockA : sockB));
|
const createWaSocket = vi.fn(async () => (call++ === 0 ? sockA : sockB));
|
||||||
const waitForWaConnection = vi.fn();
|
const waitForWaConnection = vi.fn();
|
||||||
const formatError = vi.fn((err: unknown) => `formatted:${String(err)}`);
|
const formatError = vi.fn((err: unknown) => `formatted:${String(err)}`);
|
||||||
|
const getStatusCode = vi.fn(
|
||||||
|
(err: unknown) =>
|
||||||
|
(err as { output?: { statusCode?: number } })?.output?.statusCode ??
|
||||||
|
(err as { status?: number })?.status ??
|
||||||
|
(err as { error?: { output?: { statusCode?: number } } })?.error?.output?.statusCode,
|
||||||
|
);
|
||||||
|
const waitForCredsSaveQueueWithTimeout = vi.fn(async () => {});
|
||||||
return {
|
return {
|
||||||
createWaSocket,
|
createWaSocket,
|
||||||
waitForWaConnection,
|
waitForWaConnection,
|
||||||
formatError,
|
formatError,
|
||||||
|
getStatusCode,
|
||||||
|
waitForCredsSaveQueueWithTimeout,
|
||||||
WA_WEB_AUTH_DIR: authDir,
|
WA_WEB_AUTH_DIR: authDir,
|
||||||
logoutWeb: vi.fn(async (params: { authDir?: string }) => {
|
logoutWeb: vi.fn(async (params: { authDir?: string }) => {
|
||||||
await fs.rm(params.authDir ?? authDir, {
|
await fs.rm(params.authDir ?? authDir, {
|
||||||
|
|
@ -52,8 +66,14 @@ vi.mock("./session.js", () => {
|
||||||
|
|
||||||
const createWaSocketMock = vi.mocked(createWaSocket);
|
const createWaSocketMock = vi.mocked(createWaSocket);
|
||||||
const waitForWaConnectionMock = vi.mocked(waitForWaConnection);
|
const waitForWaConnectionMock = vi.mocked(waitForWaConnection);
|
||||||
|
const waitForCredsSaveQueueWithTimeoutMock = vi.mocked(waitForCredsSaveQueueWithTimeout);
|
||||||
const formatErrorMock = vi.mocked(formatError);
|
const formatErrorMock = vi.mocked(formatError);
|
||||||
|
|
||||||
|
async function flushTasks() {
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
describe("loginWeb coverage", () => {
|
describe("loginWeb coverage", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
|
@ -65,12 +85,25 @@ describe("loginWeb coverage", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("restarts once when WhatsApp requests code 515", async () => {
|
it("restarts once when WhatsApp requests code 515", async () => {
|
||||||
|
let releaseCredsFlush: (() => void) | undefined;
|
||||||
|
const credsFlushGate = new Promise<void>((resolve) => {
|
||||||
|
releaseCredsFlush = resolve;
|
||||||
|
});
|
||||||
waitForWaConnectionMock
|
waitForWaConnectionMock
|
||||||
.mockRejectedValueOnce({ output: { statusCode: 515 } })
|
.mockRejectedValueOnce({ error: { output: { statusCode: 515 } } })
|
||||||
.mockResolvedValueOnce(undefined);
|
.mockResolvedValueOnce(undefined);
|
||||||
|
waitForCredsSaveQueueWithTimeoutMock.mockReturnValueOnce(credsFlushGate);
|
||||||
|
|
||||||
const runtime = { log: vi.fn(), error: vi.fn() } as never;
|
const runtime = { log: vi.fn(), error: vi.fn() } as never;
|
||||||
await loginWeb(false, waitForWaConnectionMock as never, runtime);
|
const pendingLogin = loginWeb(false, waitForWaConnectionMock as never, runtime);
|
||||||
|
await flushTasks();
|
||||||
|
|
||||||
|
expect(createWaSocketMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(waitForCredsSaveQueueWithTimeoutMock).toHaveBeenCalledOnce();
|
||||||
|
expect(waitForCredsSaveQueueWithTimeoutMock).toHaveBeenCalledWith(authDir);
|
||||||
|
|
||||||
|
releaseCredsFlush?.();
|
||||||
|
await pendingLogin;
|
||||||
|
|
||||||
expect(createWaSocketMock).toHaveBeenCalledTimes(2);
|
expect(createWaSocketMock).toHaveBeenCalledTimes(2);
|
||||||
const firstSock = await createWaSocketMock.mock.results[0]?.value;
|
const firstSock = await createWaSocketMock.mock.results[0]?.value;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,14 @@ import { danger, info, success } from "../../../src/globals.js";
|
||||||
import { logInfo } from "../../../src/logger.js";
|
import { logInfo } from "../../../src/logger.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../../../src/runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../../../src/runtime.js";
|
||||||
import { resolveWhatsAppAccount } from "./accounts.js";
|
import { resolveWhatsAppAccount } from "./accounts.js";
|
||||||
import { createWaSocket, formatError, logoutWeb, waitForWaConnection } from "./session.js";
|
import {
|
||||||
|
createWaSocket,
|
||||||
|
formatError,
|
||||||
|
getStatusCode,
|
||||||
|
logoutWeb,
|
||||||
|
waitForCredsSaveQueueWithTimeout,
|
||||||
|
waitForWaConnection,
|
||||||
|
} from "./session.js";
|
||||||
|
|
||||||
export async function loginWeb(
|
export async function loginWeb(
|
||||||
verbose: boolean,
|
verbose: boolean,
|
||||||
|
|
@ -24,20 +31,17 @@ export async function loginWeb(
|
||||||
await wait(sock);
|
await wait(sock);
|
||||||
console.log(success("✅ Linked! Credentials saved for future sends."));
|
console.log(success("✅ Linked! Credentials saved for future sends."));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const code =
|
const code = getStatusCode(err);
|
||||||
(err as { error?: { output?: { statusCode?: number } } })?.error?.output?.statusCode ??
|
|
||||||
(err as { output?: { statusCode?: number } })?.output?.statusCode;
|
|
||||||
if (code === 515) {
|
if (code === 515) {
|
||||||
console.log(
|
console.log(
|
||||||
info(
|
info("WhatsApp asked for a restart after pairing (code 515); waiting for creds to save…"),
|
||||||
"WhatsApp asked for a restart after pairing (code 515); creds are saved. Restarting connection once…",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
sock.ws?.close();
|
sock.ws?.close();
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
await waitForCredsSaveQueueWithTimeout(account.authDir);
|
||||||
const retry = await createWaSocket(false, verbose, {
|
const retry = await createWaSocket(false, verbose, {
|
||||||
authDir: account.authDir,
|
authDir: account.authDir,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,62 @@ describe("web session", () => {
|
||||||
expect(inFlight).toBe(0);
|
expect(inFlight).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("lets different authDir queues flush independently", async () => {
|
||||||
|
let inFlightA = 0;
|
||||||
|
let inFlightB = 0;
|
||||||
|
let releaseA: (() => void) | null = null;
|
||||||
|
let releaseB: (() => void) | null = null;
|
||||||
|
const gateA = new Promise<void>((resolve) => {
|
||||||
|
releaseA = resolve;
|
||||||
|
});
|
||||||
|
const gateB = new Promise<void>((resolve) => {
|
||||||
|
releaseB = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveCredsA = vi.fn(async () => {
|
||||||
|
inFlightA += 1;
|
||||||
|
await gateA;
|
||||||
|
inFlightA -= 1;
|
||||||
|
});
|
||||||
|
const saveCredsB = vi.fn(async () => {
|
||||||
|
inFlightB += 1;
|
||||||
|
await gateB;
|
||||||
|
inFlightB -= 1;
|
||||||
|
});
|
||||||
|
useMultiFileAuthStateMock
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
state: { creds: {} as never, keys: {} as never },
|
||||||
|
saveCreds: saveCredsA,
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
state: { creds: {} as never, keys: {} as never },
|
||||||
|
saveCreds: saveCredsB,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createWaSocket(false, false, { authDir: "/tmp/wa-a" });
|
||||||
|
const sockA = getLastSocket();
|
||||||
|
await createWaSocket(false, false, { authDir: "/tmp/wa-b" });
|
||||||
|
const sockB = getLastSocket();
|
||||||
|
|
||||||
|
sockA.ev.emit("creds.update", {});
|
||||||
|
sockB.ev.emit("creds.update", {});
|
||||||
|
|
||||||
|
await flushCredsUpdate();
|
||||||
|
|
||||||
|
expect(saveCredsA).toHaveBeenCalledTimes(1);
|
||||||
|
expect(saveCredsB).toHaveBeenCalledTimes(1);
|
||||||
|
expect(inFlightA).toBe(1);
|
||||||
|
expect(inFlightB).toBe(1);
|
||||||
|
|
||||||
|
(releaseA as (() => void) | null)?.();
|
||||||
|
(releaseB as (() => void) | null)?.();
|
||||||
|
await flushCredsUpdate();
|
||||||
|
await flushCredsUpdate();
|
||||||
|
|
||||||
|
expect(inFlightA).toBe(0);
|
||||||
|
expect(inFlightB).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("rotates creds backup when creds.json is valid JSON", async () => {
|
it("rotates creds backup when creds.json is valid JSON", async () => {
|
||||||
const creds = mockCredsJsonSpies("{}");
|
const creds = mockCredsJsonSpies("{}");
|
||||||
const backupSuffix = path.join(
|
const backupSuffix = path.join(
|
||||||
|
|
|
||||||
|
|
@ -31,17 +31,24 @@ export {
|
||||||
webAuthExists,
|
webAuthExists,
|
||||||
} from "./auth-store.js";
|
} from "./auth-store.js";
|
||||||
|
|
||||||
let credsSaveQueue: Promise<void> = Promise.resolve();
|
// Per-authDir queues so multi-account creds saves don't block each other.
|
||||||
|
const credsSaveQueues = new Map<string, Promise<void>>();
|
||||||
|
const CREDS_SAVE_FLUSH_TIMEOUT_MS = 15_000;
|
||||||
function enqueueSaveCreds(
|
function enqueueSaveCreds(
|
||||||
authDir: string,
|
authDir: string,
|
||||||
saveCreds: () => Promise<void> | void,
|
saveCreds: () => Promise<void> | void,
|
||||||
logger: ReturnType<typeof getChildLogger>,
|
logger: ReturnType<typeof getChildLogger>,
|
||||||
): void {
|
): void {
|
||||||
credsSaveQueue = credsSaveQueue
|
const prev = credsSaveQueues.get(authDir) ?? Promise.resolve();
|
||||||
|
const next = prev
|
||||||
.then(() => safeSaveCreds(authDir, saveCreds, logger))
|
.then(() => safeSaveCreds(authDir, saveCreds, logger))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.warn({ error: String(err) }, "WhatsApp creds save queue error");
|
logger.warn({ error: String(err) }, "WhatsApp creds save queue error");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (credsSaveQueues.get(authDir) === next) credsSaveQueues.delete(authDir);
|
||||||
});
|
});
|
||||||
|
credsSaveQueues.set(authDir, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function safeSaveCreds(
|
async function safeSaveCreds(
|
||||||
|
|
@ -186,10 +193,37 @@ export async function waitForWaConnection(sock: ReturnType<typeof makeWASocket>)
|
||||||
export function getStatusCode(err: unknown) {
|
export function getStatusCode(err: unknown) {
|
||||||
return (
|
return (
|
||||||
(err as { output?: { statusCode?: number } })?.output?.statusCode ??
|
(err as { output?: { statusCode?: number } })?.output?.statusCode ??
|
||||||
(err as { status?: number })?.status
|
(err as { status?: number })?.status ??
|
||||||
|
(err as { error?: { output?: { statusCode?: number } } })?.error?.output?.statusCode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Await pending credential saves — scoped to one authDir, or all if omitted. */
|
||||||
|
export function waitForCredsSaveQueue(authDir?: string): Promise<void> {
|
||||||
|
if (authDir) {
|
||||||
|
return credsSaveQueues.get(authDir) ?? Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.all(credsSaveQueues.values()).then(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Await pending credential saves, but don't hang forever on stalled I/O. */
|
||||||
|
export async function waitForCredsSaveQueueWithTimeout(
|
||||||
|
authDir: string,
|
||||||
|
timeoutMs = CREDS_SAVE_FLUSH_TIMEOUT_MS,
|
||||||
|
): Promise<void> {
|
||||||
|
let flushTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
await Promise.race([
|
||||||
|
waitForCredsSaveQueue(authDir),
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
flushTimeout = setTimeout(resolve, timeoutMs);
|
||||||
|
}),
|
||||||
|
]).finally(() => {
|
||||||
|
if (flushTimeout) {
|
||||||
|
clearTimeout(flushTimeout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function safeStringify(value: unknown, limit = 800): string {
|
function safeStringify(value: unknown, limit = 800): string {
|
||||||
try {
|
try {
|
||||||
const seen = new WeakSet();
|
const seen = new WeakSet();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue