@diegoaltoworks/chatter 0.64.0 → 0.66.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/dist/channels/whatsapp/channel.d.ts +43 -1
- package/dist/channels/whatsapp/channel.d.ts.map +1 -1
- package/dist/channels/whatsapp/index.d.ts +1 -1
- package/dist/channels/whatsapp/index.d.ts.map +1 -1
- package/dist/channels/whatsapp/index.js +98 -7
- package/dist/channels/whatsapp/index.mjs +98 -7
- package/package.json +1 -1
|
@@ -30,6 +30,39 @@ export interface WhatsAppMessageEvent {
|
|
|
30
30
|
sock: WASocket;
|
|
31
31
|
message: WAMessage;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* A session's connection state, derived from the same `connection.update`
|
|
35
|
+
* and lease transitions the channel already reacts to - not a new event
|
|
36
|
+
* source. `connecting` covers both the first connect after acquiring the
|
|
37
|
+
* lease and every reconnect attempt; `waiting_for_lease` covers not
|
|
38
|
+
* currently holding this session's lease, whether another instance holds it
|
|
39
|
+
* or acquiring/renewing it is failing.
|
|
40
|
+
*/
|
|
41
|
+
export type WaConnectionState = "connected" | "connecting" | "waiting_for_lease" | "disconnected";
|
|
42
|
+
export interface WaSessionStatus {
|
|
43
|
+
sessionId: string;
|
|
44
|
+
state: WaConnectionState;
|
|
45
|
+
/** `Date.now()`-style timestamp of the last state transition (not of the last read). */
|
|
46
|
+
since: number;
|
|
47
|
+
/** The most recent failure description; cleared once the session reconnects. */
|
|
48
|
+
lastError?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Optional liveness escalation: `onUnhealthy` fires once a session has been
|
|
52
|
+
* anything but `connected` for at least `unhealthyAfterMs`, and rearms the
|
|
53
|
+
* moment it reconnects. That includes `waiting_for_lease` - legitimate
|
|
54
|
+
* during a brief multi-instance deploy overlap, so size `unhealthyAfterMs`
|
|
55
|
+
* comfortably above how long that overlap is expected to last, or an
|
|
56
|
+
* instance that never wins the lease (a stuck rollout) will trip the
|
|
57
|
+
* watchdog repeatedly. Off by default - an unconfigured host still gets a
|
|
58
|
+
* bounded, grep-able error log while unhealthy (see `createWhatsAppChannel`'s
|
|
59
|
+
* status checker), just no callback. A host can use `onUnhealthy` to alert,
|
|
60
|
+
* or to `process.exit` so the platform restarts the container.
|
|
61
|
+
*/
|
|
62
|
+
export interface WhatsAppWatchdogConfig {
|
|
63
|
+
unhealthyAfterMs: number;
|
|
64
|
+
onUnhealthy?: (status: WaSessionStatus) => void;
|
|
65
|
+
}
|
|
33
66
|
export interface WhatsAppChannelConfig {
|
|
34
67
|
/** Passphrase that encrypts auth state at rest (AES-256-GCM key material). Keep it stable across restarts and deploys — losing it means re-pairing. */
|
|
35
68
|
sessionSecret: string;
|
|
@@ -60,6 +93,8 @@ export interface WhatsAppChannelConfig {
|
|
|
60
93
|
leaseStore?: WaLeaseStore;
|
|
61
94
|
/** Baileys auth-state persistence, encrypted above this seam. Defaults to a `wa_auth` table in `deps.db`; inject your own to run this channel without a libsql database. */
|
|
62
95
|
authStore?: WaAuthKV;
|
|
96
|
+
/** Liveness escalation for a session stuck unhealthy. Off by default (see {@link WhatsAppWatchdogConfig}). */
|
|
97
|
+
watchdog?: WhatsAppWatchdogConfig;
|
|
63
98
|
}
|
|
64
99
|
/** A live, leased session: what `stop()` (or losing the lease) must tear down. */
|
|
65
100
|
export interface WaSessionHandle {
|
|
@@ -99,6 +134,8 @@ export interface LeaseGatedConnectDeps {
|
|
|
99
134
|
* no live socket and nothing pending never recovers on its own.
|
|
100
135
|
*/
|
|
101
136
|
onConnectFailed?: (sessionId: string, error: unknown) => void;
|
|
137
|
+
/** Reports a connection-state transition, for the channel's status/watchdog seam. Optional - direct callers of `acquireSessionLease` in tests don't need it. */
|
|
138
|
+
onStatusChange?: (sessionId: string, state: WaConnectionState, lastError?: string) => void;
|
|
102
139
|
}
|
|
103
140
|
/**
|
|
104
141
|
* Acquire `sessionId`'s lease before connecting — two instances must never
|
|
@@ -130,5 +167,10 @@ export interface WhatsAppMediaPayload {
|
|
|
130
167
|
* `false` rather than sending Baileys a `{ image: { url: undefined } }`.
|
|
131
168
|
*/
|
|
132
169
|
export declare function normalizeWaMediaPayload(payload: unknown): WhatsAppMediaPayload;
|
|
133
|
-
|
|
170
|
+
/** `createWhatsAppChannel`'s return type: the {@link Channel} SPI plus a way to poll per-session connection health without log-scraping. */
|
|
171
|
+
export interface WhatsAppChannel extends Channel {
|
|
172
|
+
/** Per-session status, in the order `sessionIds` was configured. */
|
|
173
|
+
getStatus(): WaSessionStatus[];
|
|
174
|
+
}
|
|
175
|
+
export declare function createWhatsAppChannel(config: WhatsAppChannelConfig): WhatsAppChannel;
|
|
134
176
|
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAgB,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEjF,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,OAAO,KAAK,EAAoB,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,KAAK,OAAO,EAAmD,MAAM,WAAW,CAAC;AAC1F,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/channel.ts"],"names":[],"mappings":"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"}
|
|
@@ -27,7 +27,7 @@ export type { AuthStateRuntime, TursoAuthStateResult, WaAuthKV } from "./authSta
|
|
|
27
27
|
export { createTursoWaAuthKV, useAuthState, useTursoAuthState } from "./authState";
|
|
28
28
|
export type { Baileys } from "./baileys";
|
|
29
29
|
export { loadBaileys } from "./baileys";
|
|
30
|
-
export { acquireSessionLease, createWhatsAppChannel, type LeaseGatedConnectDeps, normalizeWaMediaPayload, reconnectDelayMs, senderNameFor, shutdownWaSessions, type WaSessionHandle, type WhatsAppChannelConfig, type WhatsAppMediaPayload, type WhatsAppMessageEvent, } from "./channel";
|
|
30
|
+
export { acquireSessionLease, createWhatsAppChannel, type LeaseGatedConnectDeps, normalizeWaMediaPayload, reconnectDelayMs, senderNameFor, shutdownWaSessions, type WaConnectionState, type WaSessionHandle, type WaSessionStatus, type WhatsAppChannel, type WhatsAppChannelConfig, type WhatsAppMediaPayload, type WhatsAppMessageEvent, type WhatsAppWatchdogConfig, } from "./channel";
|
|
31
31
|
export { decrypt, encrypt } from "./crypto";
|
|
32
32
|
export { createWhatsAppImageHandler, type DrawRequestDetectorConfig, drawSubject, isDrawRequest, type WhatsAppImageHandler, type WhatsAppImageHandlerDeps, type WhatsAppImageRequestContext, type WhatsAppImageStrings, } from "./images";
|
|
33
33
|
export { createWhatsAppInboundHandler, extractText, isGroupJid, jidsMatch, jidToPhoneNumber, messageContext, type ResolvedWaMessage, resolveWaMessage, senderPhoneFor, stripOwnMentions, type WaMessageContext, type WhatsAppInboundConfig, } from "./inbound";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnF,YAAY,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,qBAAqB,EAC1B,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnF,YAAY,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,qBAAqB,EAC1B,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,GAC5B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EACL,0BAA0B,EAC1B,KAAK,yBAAyB,EAC9B,WAAW,EACX,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,2BAA2B,EAChC,KAAK,oBAAoB,GAC1B,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,4BAA4B,EAC5B,WAAW,EACX,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,cAAc,EACd,KAAK,iBAAiB,EACtB,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,KAAK,UAAU,EACf,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,2BAA2B,EAC3B,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,2BAA2B,GACjC,MAAM,UAAU,CAAC"}
|
|
@@ -348,9 +348,13 @@ function createTursoWaLeaseStore(client) {
|
|
|
348
348
|
// src/channels/whatsapp/channel.ts
|
|
349
349
|
var RECONNECT_DELAY_MS = 5e3;
|
|
350
350
|
var RECONNECT_MAX_DELAY_MS = 10 * 60 * 1e3;
|
|
351
|
+
var UNHEALTHY_LOG_INTERVAL_MS = 5 * 60 * 1e3;
|
|
351
352
|
function reconnectDelayMs(failures) {
|
|
352
353
|
return exponentialBackoffMs(RECONNECT_DELAY_MS, RECONNECT_MAX_DELAY_MS, failures);
|
|
353
354
|
}
|
|
355
|
+
function describeError(error) {
|
|
356
|
+
return error instanceof Error ? error.message : String(error);
|
|
357
|
+
}
|
|
354
358
|
async function shutdownWaSessions(sessions) {
|
|
355
359
|
await Promise.all(
|
|
356
360
|
[...sessions.values()].map(async (handle) => {
|
|
@@ -381,6 +385,7 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
381
385
|
const schedule = deps.schedule ?? ((fn, ms) => void setTimeout(fn, ms));
|
|
382
386
|
const logger = deps.logger ?? createConsoleLogger();
|
|
383
387
|
if (isStopped()) return;
|
|
388
|
+
deps.onStatusChange?.(sessionId, "waiting_for_lease");
|
|
384
389
|
const retry = () => {
|
|
385
390
|
schedule(
|
|
386
391
|
() => void acquireSessionLease(sessionId, deps).catch(
|
|
@@ -394,6 +399,7 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
394
399
|
acquired = await leaseStore.tryAcquire(sessionId, instanceId, now(), staleMs);
|
|
395
400
|
} catch (error) {
|
|
396
401
|
logger.warn(`WhatsApp[${sessionId}]: lease acquire failed, retrying:`, error);
|
|
402
|
+
deps.onStatusChange?.(sessionId, "waiting_for_lease", describeError(error));
|
|
397
403
|
retry();
|
|
398
404
|
return;
|
|
399
405
|
}
|
|
@@ -402,13 +408,37 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
402
408
|
retry();
|
|
403
409
|
return;
|
|
404
410
|
}
|
|
411
|
+
const demoteSelf = () => {
|
|
412
|
+
void loseSession(sessionId, sessions).then((lost) => {
|
|
413
|
+
if (lost) retry();
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
let lastHeartbeatSuccessAt = now();
|
|
405
417
|
const heartbeat = setInterval(() => {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
418
|
+
if (isStopped()) return;
|
|
419
|
+
const attemptAt = now();
|
|
420
|
+
const failingForMs = attemptAt - lastHeartbeatSuccessAt;
|
|
421
|
+
if (failingForMs >= staleMs) {
|
|
422
|
+
logger.error(
|
|
423
|
+
`WhatsApp[${sessionId}]: lease heartbeats have been failing for ${failingForMs}ms, treating the lease as lost`
|
|
424
|
+
);
|
|
425
|
+
deps.onStatusChange?.(
|
|
426
|
+
sessionId,
|
|
427
|
+
"disconnected",
|
|
428
|
+
`lease heartbeats failing for ${failingForMs}ms`
|
|
429
|
+
);
|
|
430
|
+
demoteSelf();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
void leaseStore.tryAcquire(sessionId, instanceId, attemptAt, staleMs).then((stillHeld) => {
|
|
434
|
+
if (isStopped()) return;
|
|
435
|
+
if (!stillHeld) {
|
|
436
|
+
logger.error(`WhatsApp[${sessionId}]: lost the lease to another instance, disconnecting`);
|
|
437
|
+
deps.onStatusChange?.(sessionId, "disconnected", "lost the lease to another instance");
|
|
438
|
+
demoteSelf();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
lastHeartbeatSuccessAt = Math.max(lastHeartbeatSuccessAt, attemptAt);
|
|
412
442
|
}).catch((error) => logger.warn(`WhatsApp[${sessionId}]: lease heartbeat failed:`, error));
|
|
413
443
|
}, heartbeatMs);
|
|
414
444
|
if (typeof heartbeat === "object" && "unref" in heartbeat) heartbeat.unref();
|
|
@@ -457,6 +487,26 @@ function createWhatsAppChannel(config) {
|
|
|
457
487
|
const failureCounts = /* @__PURE__ */ new Map();
|
|
458
488
|
const registeredSenderNames = /* @__PURE__ */ new Map();
|
|
459
489
|
let senders;
|
|
490
|
+
const statuses = new Map(
|
|
491
|
+
sessionIds.map((id) => [id, { sessionId: id, state: "waiting_for_lease", since: now() }])
|
|
492
|
+
);
|
|
493
|
+
const unhealthySince = /* @__PURE__ */ new Map();
|
|
494
|
+
const watchdogFired = /* @__PURE__ */ new Set();
|
|
495
|
+
const lastUnhealthyLogAt = /* @__PURE__ */ new Map();
|
|
496
|
+
let healthCheckInterval;
|
|
497
|
+
function setStatus(sessionId, state, lastError) {
|
|
498
|
+
const prev = statuses.get(sessionId);
|
|
499
|
+
const since = prev && prev.state === state ? prev.since : now();
|
|
500
|
+
const resolvedLastError = lastError !== void 0 ? lastError : state === "connected" ? void 0 : prev?.lastError;
|
|
501
|
+
statuses.set(sessionId, { sessionId, state, since, lastError: resolvedLastError });
|
|
502
|
+
if (state === "connected") {
|
|
503
|
+
unhealthySince.delete(sessionId);
|
|
504
|
+
watchdogFired.delete(sessionId);
|
|
505
|
+
lastUnhealthyLogAt.delete(sessionId);
|
|
506
|
+
} else if (!unhealthySince.has(sessionId)) {
|
|
507
|
+
unhealthySince.set(sessionId, since);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
460
510
|
return {
|
|
461
511
|
name: channelName,
|
|
462
512
|
async start(deps) {
|
|
@@ -482,7 +532,8 @@ function createWhatsAppChannel(config) {
|
|
|
482
532
|
waitMs,
|
|
483
533
|
schedule,
|
|
484
534
|
logger: log,
|
|
485
|
-
onConnectFailed: recoverFromFailedConnect
|
|
535
|
+
onConnectFailed: recoverFromFailedConnect,
|
|
536
|
+
onStatusChange: setStatus
|
|
486
537
|
};
|
|
487
538
|
function nextRetryDelayMs(sessionId) {
|
|
488
539
|
const failures = failureCounts.get(sessionId) ?? 0;
|
|
@@ -497,6 +548,7 @@ function createWhatsAppChannel(config) {
|
|
|
497
548
|
const handle = claimSession(sessionId, sessions);
|
|
498
549
|
if (!handle) return;
|
|
499
550
|
const delay = nextRetryDelayMs(sessionId);
|
|
551
|
+
setStatus(sessionId, "disconnected", describeError(error));
|
|
500
552
|
log.error(
|
|
501
553
|
`WhatsApp[${sessionId}]: connect attempt failed, releasing the lease and retrying in ${Math.round(delay / 1e3)}s:`,
|
|
502
554
|
error
|
|
@@ -516,6 +568,7 @@ function createWhatsAppChannel(config) {
|
|
|
516
568
|
}
|
|
517
569
|
async function connect(sessionId) {
|
|
518
570
|
if (stopped || !sessions.has(sessionId)) return;
|
|
571
|
+
setStatus(sessionId, "connecting");
|
|
519
572
|
const baileys = await loadBaileys2();
|
|
520
573
|
const runtime = {
|
|
521
574
|
bufferJSON: baileys.BufferJSON,
|
|
@@ -532,6 +585,7 @@ function createWhatsAppChannel(config) {
|
|
|
532
585
|
log.warn(
|
|
533
586
|
`WhatsApp[${sessionId}]: no paired session found \u2014 pair it first (see docs/channels.md). Re-checking in 60s.`
|
|
534
587
|
);
|
|
588
|
+
setStatus(sessionId, "disconnected", "no paired session - pair it first");
|
|
535
589
|
schedule(() => attemptConnect(sessionId), 6e4);
|
|
536
590
|
return;
|
|
537
591
|
}
|
|
@@ -561,6 +615,7 @@ function createWhatsAppChannel(config) {
|
|
|
561
615
|
const { connection, lastDisconnect } = update;
|
|
562
616
|
if (connection === "open") {
|
|
563
617
|
failureCounts.set(sessionId, 0);
|
|
618
|
+
setStatus(sessionId, "connected");
|
|
564
619
|
log.info(`WhatsApp[${sessionId}]: connected as ${sock.user?.id ?? "unknown"}`);
|
|
565
620
|
const senderName = senderNameFor(channelName, sessionId);
|
|
566
621
|
const sender = {
|
|
@@ -591,6 +646,7 @@ function createWhatsAppChannel(config) {
|
|
|
591
646
|
}
|
|
592
647
|
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
593
648
|
if (statusCode === baileys.DisconnectReason.loggedOut) {
|
|
649
|
+
setStatus(sessionId, "disconnected", "session logged out - re-pair required");
|
|
594
650
|
log.error(
|
|
595
651
|
`WhatsApp[${sessionId}]: session logged out \u2014 re-pair to reconnect (see docs/channels.md).`
|
|
596
652
|
);
|
|
@@ -599,6 +655,7 @@ function createWhatsAppChannel(config) {
|
|
|
599
655
|
}
|
|
600
656
|
if (!stopped && sessions.has(sessionId)) {
|
|
601
657
|
const delay = nextRetryDelayMs(sessionId);
|
|
658
|
+
setStatus(sessionId, "disconnected", `connection closed (${statusCode ?? "?"})`);
|
|
602
659
|
log.warn(
|
|
603
660
|
`WhatsApp[${sessionId}]: connection closed (${statusCode ?? "?"}), reconnecting in ${Math.round(delay / 1e3)}s...`
|
|
604
661
|
);
|
|
@@ -619,6 +676,33 @@ function createWhatsAppChannel(config) {
|
|
|
619
676
|
});
|
|
620
677
|
}
|
|
621
678
|
}
|
|
679
|
+
function checkHealth() {
|
|
680
|
+
const nowMs = now();
|
|
681
|
+
for (const sessionId of sessionIds) {
|
|
682
|
+
const unhealthySinceMs = unhealthySince.get(sessionId);
|
|
683
|
+
if (unhealthySinceMs === void 0) continue;
|
|
684
|
+
const status = statuses.get(sessionId);
|
|
685
|
+
if (!status) continue;
|
|
686
|
+
const watchdog = config.watchdog;
|
|
687
|
+
if (watchdog && nowMs - unhealthySinceMs >= watchdog.unhealthyAfterMs && !watchdogFired.has(sessionId)) {
|
|
688
|
+
watchdogFired.add(sessionId);
|
|
689
|
+
watchdog.onUnhealthy?.(status);
|
|
690
|
+
}
|
|
691
|
+
if (!watchdog?.onUnhealthy) {
|
|
692
|
+
const lastLogged = lastUnhealthyLogAt.get(sessionId) ?? unhealthySinceMs;
|
|
693
|
+
if (nowMs - lastLogged >= UNHEALTHY_LOG_INTERVAL_MS) {
|
|
694
|
+
lastUnhealthyLogAt.set(sessionId, nowMs);
|
|
695
|
+
log.error(
|
|
696
|
+
`WhatsApp[${sessionId}]: unhealthy (${status.state}) since ${new Date(status.since).toISOString()}` + (status.lastError ? ` - ${status.lastError}` : "")
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
healthCheckInterval = setInterval(checkHealth, heartbeatMs);
|
|
703
|
+
if (typeof healthCheckInterval === "object" && "unref" in healthCheckInterval) {
|
|
704
|
+
healthCheckInterval.unref();
|
|
705
|
+
}
|
|
622
706
|
for (const sessionId of sessionIds) {
|
|
623
707
|
void acquireSessionLease(sessionId, leaseDeps).catch(
|
|
624
708
|
(error) => log.error(`WhatsApp[${sessionId}]: failed to start:`, error)
|
|
@@ -627,11 +711,18 @@ function createWhatsAppChannel(config) {
|
|
|
627
711
|
},
|
|
628
712
|
async stop() {
|
|
629
713
|
stopped = true;
|
|
714
|
+
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
|
715
|
+
for (const sessionId of sessionIds) setStatus(sessionId, "disconnected", "channel stopped");
|
|
630
716
|
for (const senderName of registeredSenderNames.values()) {
|
|
631
717
|
senders?.unregister(senderName);
|
|
632
718
|
}
|
|
633
719
|
registeredSenderNames.clear();
|
|
634
720
|
await shutdownWaSessions(sessions);
|
|
721
|
+
},
|
|
722
|
+
getStatus() {
|
|
723
|
+
return sessionIds.map(
|
|
724
|
+
(id) => statuses.get(id) ?? { sessionId: id, state: "waiting_for_lease", since: now() }
|
|
725
|
+
);
|
|
635
726
|
}
|
|
636
727
|
};
|
|
637
728
|
}
|
|
@@ -283,9 +283,13 @@ function createTursoWaLeaseStore(client) {
|
|
|
283
283
|
// src/channels/whatsapp/channel.ts
|
|
284
284
|
var RECONNECT_DELAY_MS = 5e3;
|
|
285
285
|
var RECONNECT_MAX_DELAY_MS = 10 * 60 * 1e3;
|
|
286
|
+
var UNHEALTHY_LOG_INTERVAL_MS = 5 * 60 * 1e3;
|
|
286
287
|
function reconnectDelayMs(failures) {
|
|
287
288
|
return exponentialBackoffMs(RECONNECT_DELAY_MS, RECONNECT_MAX_DELAY_MS, failures);
|
|
288
289
|
}
|
|
290
|
+
function describeError(error) {
|
|
291
|
+
return error instanceof Error ? error.message : String(error);
|
|
292
|
+
}
|
|
289
293
|
async function shutdownWaSessions(sessions) {
|
|
290
294
|
await Promise.all(
|
|
291
295
|
[...sessions.values()].map(async (handle) => {
|
|
@@ -316,6 +320,7 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
316
320
|
const schedule = deps.schedule ?? ((fn, ms) => void setTimeout(fn, ms));
|
|
317
321
|
const logger = deps.logger ?? createConsoleLogger();
|
|
318
322
|
if (isStopped()) return;
|
|
323
|
+
deps.onStatusChange?.(sessionId, "waiting_for_lease");
|
|
319
324
|
const retry = () => {
|
|
320
325
|
schedule(
|
|
321
326
|
() => void acquireSessionLease(sessionId, deps).catch(
|
|
@@ -329,6 +334,7 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
329
334
|
acquired = await leaseStore.tryAcquire(sessionId, instanceId, now(), staleMs);
|
|
330
335
|
} catch (error) {
|
|
331
336
|
logger.warn(`WhatsApp[${sessionId}]: lease acquire failed, retrying:`, error);
|
|
337
|
+
deps.onStatusChange?.(sessionId, "waiting_for_lease", describeError(error));
|
|
332
338
|
retry();
|
|
333
339
|
return;
|
|
334
340
|
}
|
|
@@ -337,13 +343,37 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
337
343
|
retry();
|
|
338
344
|
return;
|
|
339
345
|
}
|
|
346
|
+
const demoteSelf = () => {
|
|
347
|
+
void loseSession(sessionId, sessions).then((lost) => {
|
|
348
|
+
if (lost) retry();
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
let lastHeartbeatSuccessAt = now();
|
|
340
352
|
const heartbeat = setInterval(() => {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
353
|
+
if (isStopped()) return;
|
|
354
|
+
const attemptAt = now();
|
|
355
|
+
const failingForMs = attemptAt - lastHeartbeatSuccessAt;
|
|
356
|
+
if (failingForMs >= staleMs) {
|
|
357
|
+
logger.error(
|
|
358
|
+
`WhatsApp[${sessionId}]: lease heartbeats have been failing for ${failingForMs}ms, treating the lease as lost`
|
|
359
|
+
);
|
|
360
|
+
deps.onStatusChange?.(
|
|
361
|
+
sessionId,
|
|
362
|
+
"disconnected",
|
|
363
|
+
`lease heartbeats failing for ${failingForMs}ms`
|
|
364
|
+
);
|
|
365
|
+
demoteSelf();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
void leaseStore.tryAcquire(sessionId, instanceId, attemptAt, staleMs).then((stillHeld) => {
|
|
369
|
+
if (isStopped()) return;
|
|
370
|
+
if (!stillHeld) {
|
|
371
|
+
logger.error(`WhatsApp[${sessionId}]: lost the lease to another instance, disconnecting`);
|
|
372
|
+
deps.onStatusChange?.(sessionId, "disconnected", "lost the lease to another instance");
|
|
373
|
+
demoteSelf();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
lastHeartbeatSuccessAt = Math.max(lastHeartbeatSuccessAt, attemptAt);
|
|
347
377
|
}).catch((error) => logger.warn(`WhatsApp[${sessionId}]: lease heartbeat failed:`, error));
|
|
348
378
|
}, heartbeatMs);
|
|
349
379
|
if (typeof heartbeat === "object" && "unref" in heartbeat) heartbeat.unref();
|
|
@@ -392,6 +422,26 @@ function createWhatsAppChannel(config) {
|
|
|
392
422
|
const failureCounts = /* @__PURE__ */ new Map();
|
|
393
423
|
const registeredSenderNames = /* @__PURE__ */ new Map();
|
|
394
424
|
let senders;
|
|
425
|
+
const statuses = new Map(
|
|
426
|
+
sessionIds.map((id) => [id, { sessionId: id, state: "waiting_for_lease", since: now() }])
|
|
427
|
+
);
|
|
428
|
+
const unhealthySince = /* @__PURE__ */ new Map();
|
|
429
|
+
const watchdogFired = /* @__PURE__ */ new Set();
|
|
430
|
+
const lastUnhealthyLogAt = /* @__PURE__ */ new Map();
|
|
431
|
+
let healthCheckInterval;
|
|
432
|
+
function setStatus(sessionId, state, lastError) {
|
|
433
|
+
const prev = statuses.get(sessionId);
|
|
434
|
+
const since = prev && prev.state === state ? prev.since : now();
|
|
435
|
+
const resolvedLastError = lastError !== void 0 ? lastError : state === "connected" ? void 0 : prev?.lastError;
|
|
436
|
+
statuses.set(sessionId, { sessionId, state, since, lastError: resolvedLastError });
|
|
437
|
+
if (state === "connected") {
|
|
438
|
+
unhealthySince.delete(sessionId);
|
|
439
|
+
watchdogFired.delete(sessionId);
|
|
440
|
+
lastUnhealthyLogAt.delete(sessionId);
|
|
441
|
+
} else if (!unhealthySince.has(sessionId)) {
|
|
442
|
+
unhealthySince.set(sessionId, since);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
395
445
|
return {
|
|
396
446
|
name: channelName,
|
|
397
447
|
async start(deps) {
|
|
@@ -417,7 +467,8 @@ function createWhatsAppChannel(config) {
|
|
|
417
467
|
waitMs,
|
|
418
468
|
schedule,
|
|
419
469
|
logger: log,
|
|
420
|
-
onConnectFailed: recoverFromFailedConnect
|
|
470
|
+
onConnectFailed: recoverFromFailedConnect,
|
|
471
|
+
onStatusChange: setStatus
|
|
421
472
|
};
|
|
422
473
|
function nextRetryDelayMs(sessionId) {
|
|
423
474
|
const failures = failureCounts.get(sessionId) ?? 0;
|
|
@@ -432,6 +483,7 @@ function createWhatsAppChannel(config) {
|
|
|
432
483
|
const handle = claimSession(sessionId, sessions);
|
|
433
484
|
if (!handle) return;
|
|
434
485
|
const delay = nextRetryDelayMs(sessionId);
|
|
486
|
+
setStatus(sessionId, "disconnected", describeError(error));
|
|
435
487
|
log.error(
|
|
436
488
|
`WhatsApp[${sessionId}]: connect attempt failed, releasing the lease and retrying in ${Math.round(delay / 1e3)}s:`,
|
|
437
489
|
error
|
|
@@ -451,6 +503,7 @@ function createWhatsAppChannel(config) {
|
|
|
451
503
|
}
|
|
452
504
|
async function connect(sessionId) {
|
|
453
505
|
if (stopped || !sessions.has(sessionId)) return;
|
|
506
|
+
setStatus(sessionId, "connecting");
|
|
454
507
|
const baileys = await loadBaileys2();
|
|
455
508
|
const runtime = {
|
|
456
509
|
bufferJSON: baileys.BufferJSON,
|
|
@@ -467,6 +520,7 @@ function createWhatsAppChannel(config) {
|
|
|
467
520
|
log.warn(
|
|
468
521
|
`WhatsApp[${sessionId}]: no paired session found \u2014 pair it first (see docs/channels.md). Re-checking in 60s.`
|
|
469
522
|
);
|
|
523
|
+
setStatus(sessionId, "disconnected", "no paired session - pair it first");
|
|
470
524
|
schedule(() => attemptConnect(sessionId), 6e4);
|
|
471
525
|
return;
|
|
472
526
|
}
|
|
@@ -496,6 +550,7 @@ function createWhatsAppChannel(config) {
|
|
|
496
550
|
const { connection, lastDisconnect } = update;
|
|
497
551
|
if (connection === "open") {
|
|
498
552
|
failureCounts.set(sessionId, 0);
|
|
553
|
+
setStatus(sessionId, "connected");
|
|
499
554
|
log.info(`WhatsApp[${sessionId}]: connected as ${sock.user?.id ?? "unknown"}`);
|
|
500
555
|
const senderName = senderNameFor(channelName, sessionId);
|
|
501
556
|
const sender = {
|
|
@@ -526,6 +581,7 @@ function createWhatsAppChannel(config) {
|
|
|
526
581
|
}
|
|
527
582
|
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
528
583
|
if (statusCode === baileys.DisconnectReason.loggedOut) {
|
|
584
|
+
setStatus(sessionId, "disconnected", "session logged out - re-pair required");
|
|
529
585
|
log.error(
|
|
530
586
|
`WhatsApp[${sessionId}]: session logged out \u2014 re-pair to reconnect (see docs/channels.md).`
|
|
531
587
|
);
|
|
@@ -534,6 +590,7 @@ function createWhatsAppChannel(config) {
|
|
|
534
590
|
}
|
|
535
591
|
if (!stopped && sessions.has(sessionId)) {
|
|
536
592
|
const delay = nextRetryDelayMs(sessionId);
|
|
593
|
+
setStatus(sessionId, "disconnected", `connection closed (${statusCode ?? "?"})`);
|
|
537
594
|
log.warn(
|
|
538
595
|
`WhatsApp[${sessionId}]: connection closed (${statusCode ?? "?"}), reconnecting in ${Math.round(delay / 1e3)}s...`
|
|
539
596
|
);
|
|
@@ -554,6 +611,33 @@ function createWhatsAppChannel(config) {
|
|
|
554
611
|
});
|
|
555
612
|
}
|
|
556
613
|
}
|
|
614
|
+
function checkHealth() {
|
|
615
|
+
const nowMs = now();
|
|
616
|
+
for (const sessionId of sessionIds) {
|
|
617
|
+
const unhealthySinceMs = unhealthySince.get(sessionId);
|
|
618
|
+
if (unhealthySinceMs === void 0) continue;
|
|
619
|
+
const status = statuses.get(sessionId);
|
|
620
|
+
if (!status) continue;
|
|
621
|
+
const watchdog = config.watchdog;
|
|
622
|
+
if (watchdog && nowMs - unhealthySinceMs >= watchdog.unhealthyAfterMs && !watchdogFired.has(sessionId)) {
|
|
623
|
+
watchdogFired.add(sessionId);
|
|
624
|
+
watchdog.onUnhealthy?.(status);
|
|
625
|
+
}
|
|
626
|
+
if (!watchdog?.onUnhealthy) {
|
|
627
|
+
const lastLogged = lastUnhealthyLogAt.get(sessionId) ?? unhealthySinceMs;
|
|
628
|
+
if (nowMs - lastLogged >= UNHEALTHY_LOG_INTERVAL_MS) {
|
|
629
|
+
lastUnhealthyLogAt.set(sessionId, nowMs);
|
|
630
|
+
log.error(
|
|
631
|
+
`WhatsApp[${sessionId}]: unhealthy (${status.state}) since ${new Date(status.since).toISOString()}` + (status.lastError ? ` - ${status.lastError}` : "")
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
healthCheckInterval = setInterval(checkHealth, heartbeatMs);
|
|
638
|
+
if (typeof healthCheckInterval === "object" && "unref" in healthCheckInterval) {
|
|
639
|
+
healthCheckInterval.unref();
|
|
640
|
+
}
|
|
557
641
|
for (const sessionId of sessionIds) {
|
|
558
642
|
void acquireSessionLease(sessionId, leaseDeps).catch(
|
|
559
643
|
(error) => log.error(`WhatsApp[${sessionId}]: failed to start:`, error)
|
|
@@ -562,11 +646,18 @@ function createWhatsAppChannel(config) {
|
|
|
562
646
|
},
|
|
563
647
|
async stop() {
|
|
564
648
|
stopped = true;
|
|
649
|
+
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
|
650
|
+
for (const sessionId of sessionIds) setStatus(sessionId, "disconnected", "channel stopped");
|
|
565
651
|
for (const senderName of registeredSenderNames.values()) {
|
|
566
652
|
senders?.unregister(senderName);
|
|
567
653
|
}
|
|
568
654
|
registeredSenderNames.clear();
|
|
569
655
|
await shutdownWaSessions(sessions);
|
|
656
|
+
},
|
|
657
|
+
getStatus() {
|
|
658
|
+
return sessionIds.map(
|
|
659
|
+
(id) => statuses.get(id) ?? { sessionId: id, state: "waiting_for_lease", since: now() }
|
|
660
|
+
);
|
|
570
661
|
}
|
|
571
662
|
};
|
|
572
663
|
}
|
package/package.json
CHANGED