@diegoaltoworks/chatter 0.65.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 +74 -1
- package/dist/channels/whatsapp/index.mjs +74 -1
- 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
|
}
|
|
@@ -416,6 +422,11 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
416
422
|
logger.error(
|
|
417
423
|
`WhatsApp[${sessionId}]: lease heartbeats have been failing for ${failingForMs}ms, treating the lease as lost`
|
|
418
424
|
);
|
|
425
|
+
deps.onStatusChange?.(
|
|
426
|
+
sessionId,
|
|
427
|
+
"disconnected",
|
|
428
|
+
`lease heartbeats failing for ${failingForMs}ms`
|
|
429
|
+
);
|
|
419
430
|
demoteSelf();
|
|
420
431
|
return;
|
|
421
432
|
}
|
|
@@ -423,6 +434,7 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
423
434
|
if (isStopped()) return;
|
|
424
435
|
if (!stillHeld) {
|
|
425
436
|
logger.error(`WhatsApp[${sessionId}]: lost the lease to another instance, disconnecting`);
|
|
437
|
+
deps.onStatusChange?.(sessionId, "disconnected", "lost the lease to another instance");
|
|
426
438
|
demoteSelf();
|
|
427
439
|
return;
|
|
428
440
|
}
|
|
@@ -475,6 +487,26 @@ function createWhatsAppChannel(config) {
|
|
|
475
487
|
const failureCounts = /* @__PURE__ */ new Map();
|
|
476
488
|
const registeredSenderNames = /* @__PURE__ */ new Map();
|
|
477
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
|
+
}
|
|
478
510
|
return {
|
|
479
511
|
name: channelName,
|
|
480
512
|
async start(deps) {
|
|
@@ -500,7 +532,8 @@ function createWhatsAppChannel(config) {
|
|
|
500
532
|
waitMs,
|
|
501
533
|
schedule,
|
|
502
534
|
logger: log,
|
|
503
|
-
onConnectFailed: recoverFromFailedConnect
|
|
535
|
+
onConnectFailed: recoverFromFailedConnect,
|
|
536
|
+
onStatusChange: setStatus
|
|
504
537
|
};
|
|
505
538
|
function nextRetryDelayMs(sessionId) {
|
|
506
539
|
const failures = failureCounts.get(sessionId) ?? 0;
|
|
@@ -515,6 +548,7 @@ function createWhatsAppChannel(config) {
|
|
|
515
548
|
const handle = claimSession(sessionId, sessions);
|
|
516
549
|
if (!handle) return;
|
|
517
550
|
const delay = nextRetryDelayMs(sessionId);
|
|
551
|
+
setStatus(sessionId, "disconnected", describeError(error));
|
|
518
552
|
log.error(
|
|
519
553
|
`WhatsApp[${sessionId}]: connect attempt failed, releasing the lease and retrying in ${Math.round(delay / 1e3)}s:`,
|
|
520
554
|
error
|
|
@@ -534,6 +568,7 @@ function createWhatsAppChannel(config) {
|
|
|
534
568
|
}
|
|
535
569
|
async function connect(sessionId) {
|
|
536
570
|
if (stopped || !sessions.has(sessionId)) return;
|
|
571
|
+
setStatus(sessionId, "connecting");
|
|
537
572
|
const baileys = await loadBaileys2();
|
|
538
573
|
const runtime = {
|
|
539
574
|
bufferJSON: baileys.BufferJSON,
|
|
@@ -550,6 +585,7 @@ function createWhatsAppChannel(config) {
|
|
|
550
585
|
log.warn(
|
|
551
586
|
`WhatsApp[${sessionId}]: no paired session found \u2014 pair it first (see docs/channels.md). Re-checking in 60s.`
|
|
552
587
|
);
|
|
588
|
+
setStatus(sessionId, "disconnected", "no paired session - pair it first");
|
|
553
589
|
schedule(() => attemptConnect(sessionId), 6e4);
|
|
554
590
|
return;
|
|
555
591
|
}
|
|
@@ -579,6 +615,7 @@ function createWhatsAppChannel(config) {
|
|
|
579
615
|
const { connection, lastDisconnect } = update;
|
|
580
616
|
if (connection === "open") {
|
|
581
617
|
failureCounts.set(sessionId, 0);
|
|
618
|
+
setStatus(sessionId, "connected");
|
|
582
619
|
log.info(`WhatsApp[${sessionId}]: connected as ${sock.user?.id ?? "unknown"}`);
|
|
583
620
|
const senderName = senderNameFor(channelName, sessionId);
|
|
584
621
|
const sender = {
|
|
@@ -609,6 +646,7 @@ function createWhatsAppChannel(config) {
|
|
|
609
646
|
}
|
|
610
647
|
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
611
648
|
if (statusCode === baileys.DisconnectReason.loggedOut) {
|
|
649
|
+
setStatus(sessionId, "disconnected", "session logged out - re-pair required");
|
|
612
650
|
log.error(
|
|
613
651
|
`WhatsApp[${sessionId}]: session logged out \u2014 re-pair to reconnect (see docs/channels.md).`
|
|
614
652
|
);
|
|
@@ -617,6 +655,7 @@ function createWhatsAppChannel(config) {
|
|
|
617
655
|
}
|
|
618
656
|
if (!stopped && sessions.has(sessionId)) {
|
|
619
657
|
const delay = nextRetryDelayMs(sessionId);
|
|
658
|
+
setStatus(sessionId, "disconnected", `connection closed (${statusCode ?? "?"})`);
|
|
620
659
|
log.warn(
|
|
621
660
|
`WhatsApp[${sessionId}]: connection closed (${statusCode ?? "?"}), reconnecting in ${Math.round(delay / 1e3)}s...`
|
|
622
661
|
);
|
|
@@ -637,6 +676,33 @@ function createWhatsAppChannel(config) {
|
|
|
637
676
|
});
|
|
638
677
|
}
|
|
639
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
|
+
}
|
|
640
706
|
for (const sessionId of sessionIds) {
|
|
641
707
|
void acquireSessionLease(sessionId, leaseDeps).catch(
|
|
642
708
|
(error) => log.error(`WhatsApp[${sessionId}]: failed to start:`, error)
|
|
@@ -645,11 +711,18 @@ function createWhatsAppChannel(config) {
|
|
|
645
711
|
},
|
|
646
712
|
async stop() {
|
|
647
713
|
stopped = true;
|
|
714
|
+
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
|
715
|
+
for (const sessionId of sessionIds) setStatus(sessionId, "disconnected", "channel stopped");
|
|
648
716
|
for (const senderName of registeredSenderNames.values()) {
|
|
649
717
|
senders?.unregister(senderName);
|
|
650
718
|
}
|
|
651
719
|
registeredSenderNames.clear();
|
|
652
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
|
+
);
|
|
653
726
|
}
|
|
654
727
|
};
|
|
655
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
|
}
|
|
@@ -351,6 +357,11 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
351
357
|
logger.error(
|
|
352
358
|
`WhatsApp[${sessionId}]: lease heartbeats have been failing for ${failingForMs}ms, treating the lease as lost`
|
|
353
359
|
);
|
|
360
|
+
deps.onStatusChange?.(
|
|
361
|
+
sessionId,
|
|
362
|
+
"disconnected",
|
|
363
|
+
`lease heartbeats failing for ${failingForMs}ms`
|
|
364
|
+
);
|
|
354
365
|
demoteSelf();
|
|
355
366
|
return;
|
|
356
367
|
}
|
|
@@ -358,6 +369,7 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
358
369
|
if (isStopped()) return;
|
|
359
370
|
if (!stillHeld) {
|
|
360
371
|
logger.error(`WhatsApp[${sessionId}]: lost the lease to another instance, disconnecting`);
|
|
372
|
+
deps.onStatusChange?.(sessionId, "disconnected", "lost the lease to another instance");
|
|
361
373
|
demoteSelf();
|
|
362
374
|
return;
|
|
363
375
|
}
|
|
@@ -410,6 +422,26 @@ function createWhatsAppChannel(config) {
|
|
|
410
422
|
const failureCounts = /* @__PURE__ */ new Map();
|
|
411
423
|
const registeredSenderNames = /* @__PURE__ */ new Map();
|
|
412
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
|
+
}
|
|
413
445
|
return {
|
|
414
446
|
name: channelName,
|
|
415
447
|
async start(deps) {
|
|
@@ -435,7 +467,8 @@ function createWhatsAppChannel(config) {
|
|
|
435
467
|
waitMs,
|
|
436
468
|
schedule,
|
|
437
469
|
logger: log,
|
|
438
|
-
onConnectFailed: recoverFromFailedConnect
|
|
470
|
+
onConnectFailed: recoverFromFailedConnect,
|
|
471
|
+
onStatusChange: setStatus
|
|
439
472
|
};
|
|
440
473
|
function nextRetryDelayMs(sessionId) {
|
|
441
474
|
const failures = failureCounts.get(sessionId) ?? 0;
|
|
@@ -450,6 +483,7 @@ function createWhatsAppChannel(config) {
|
|
|
450
483
|
const handle = claimSession(sessionId, sessions);
|
|
451
484
|
if (!handle) return;
|
|
452
485
|
const delay = nextRetryDelayMs(sessionId);
|
|
486
|
+
setStatus(sessionId, "disconnected", describeError(error));
|
|
453
487
|
log.error(
|
|
454
488
|
`WhatsApp[${sessionId}]: connect attempt failed, releasing the lease and retrying in ${Math.round(delay / 1e3)}s:`,
|
|
455
489
|
error
|
|
@@ -469,6 +503,7 @@ function createWhatsAppChannel(config) {
|
|
|
469
503
|
}
|
|
470
504
|
async function connect(sessionId) {
|
|
471
505
|
if (stopped || !sessions.has(sessionId)) return;
|
|
506
|
+
setStatus(sessionId, "connecting");
|
|
472
507
|
const baileys = await loadBaileys2();
|
|
473
508
|
const runtime = {
|
|
474
509
|
bufferJSON: baileys.BufferJSON,
|
|
@@ -485,6 +520,7 @@ function createWhatsAppChannel(config) {
|
|
|
485
520
|
log.warn(
|
|
486
521
|
`WhatsApp[${sessionId}]: no paired session found \u2014 pair it first (see docs/channels.md). Re-checking in 60s.`
|
|
487
522
|
);
|
|
523
|
+
setStatus(sessionId, "disconnected", "no paired session - pair it first");
|
|
488
524
|
schedule(() => attemptConnect(sessionId), 6e4);
|
|
489
525
|
return;
|
|
490
526
|
}
|
|
@@ -514,6 +550,7 @@ function createWhatsAppChannel(config) {
|
|
|
514
550
|
const { connection, lastDisconnect } = update;
|
|
515
551
|
if (connection === "open") {
|
|
516
552
|
failureCounts.set(sessionId, 0);
|
|
553
|
+
setStatus(sessionId, "connected");
|
|
517
554
|
log.info(`WhatsApp[${sessionId}]: connected as ${sock.user?.id ?? "unknown"}`);
|
|
518
555
|
const senderName = senderNameFor(channelName, sessionId);
|
|
519
556
|
const sender = {
|
|
@@ -544,6 +581,7 @@ function createWhatsAppChannel(config) {
|
|
|
544
581
|
}
|
|
545
582
|
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
546
583
|
if (statusCode === baileys.DisconnectReason.loggedOut) {
|
|
584
|
+
setStatus(sessionId, "disconnected", "session logged out - re-pair required");
|
|
547
585
|
log.error(
|
|
548
586
|
`WhatsApp[${sessionId}]: session logged out \u2014 re-pair to reconnect (see docs/channels.md).`
|
|
549
587
|
);
|
|
@@ -552,6 +590,7 @@ function createWhatsAppChannel(config) {
|
|
|
552
590
|
}
|
|
553
591
|
if (!stopped && sessions.has(sessionId)) {
|
|
554
592
|
const delay = nextRetryDelayMs(sessionId);
|
|
593
|
+
setStatus(sessionId, "disconnected", `connection closed (${statusCode ?? "?"})`);
|
|
555
594
|
log.warn(
|
|
556
595
|
`WhatsApp[${sessionId}]: connection closed (${statusCode ?? "?"}), reconnecting in ${Math.round(delay / 1e3)}s...`
|
|
557
596
|
);
|
|
@@ -572,6 +611,33 @@ function createWhatsAppChannel(config) {
|
|
|
572
611
|
});
|
|
573
612
|
}
|
|
574
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
|
+
}
|
|
575
641
|
for (const sessionId of sessionIds) {
|
|
576
642
|
void acquireSessionLease(sessionId, leaseDeps).catch(
|
|
577
643
|
(error) => log.error(`WhatsApp[${sessionId}]: failed to start:`, error)
|
|
@@ -580,11 +646,18 @@ function createWhatsAppChannel(config) {
|
|
|
580
646
|
},
|
|
581
647
|
async stop() {
|
|
582
648
|
stopped = true;
|
|
649
|
+
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
|
650
|
+
for (const sessionId of sessionIds) setStatus(sessionId, "disconnected", "channel stopped");
|
|
583
651
|
for (const senderName of registeredSenderNames.values()) {
|
|
584
652
|
senders?.unregister(senderName);
|
|
585
653
|
}
|
|
586
654
|
registeredSenderNames.clear();
|
|
587
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
|
+
);
|
|
588
661
|
}
|
|
589
662
|
};
|
|
590
663
|
}
|
package/package.json
CHANGED