Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions adapters/whatsapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,14 @@ while signed in as the intended GSV user. The code expires after ten minutes.
The message that requests the code is consumed by the linking flow, so send a
new message after linking to start a conversation with an agent.

While connected, the outbound WhatsApp WebSocket prevents the account Durable
Object from being evicted. Cloudflare limits that keepalive effect to 15 minutes
per outbound connection; the WebSocket itself may continue after that point.
GSV therefore schedules an alarm ten minutes after each successful connection.
When the alarm fires, the account opens a replacement with its saved
linked-device credentials while the current transport keeps handling messages.
Only after the replacement authenticates does it become active and close the
old transport, establishing a fresh outbound-connection lease before the
15-minute keepalive cap. The alarm is the scheduled trigger, not the mechanism
that keeps the object alive, and this reconnect is not a WhatsApp logout or a
new pairing.
While connected, the outbound WhatsApp WebSocket initially prevents the account
Durable Object from being evicted. Cloudflare limits that keepalive effect to 15
minutes per outbound connection; the WebSocket itself may continue after that
point. GSV keeps the same provider session and schedules a Durable Object alarm
every 30 seconds. Each alarm is an incoming event inside Cloudflare's minimum
70-second idle-eviction window, so routine residency maintenance never opens a
second WhatsApp session. Baileys separately pings WhatsApp every 30 seconds and
the ordinary reconnect path replaces a transport only when it is unhealthy.

This design targets the free Workers and Durable Objects plan; it does not
require Containers. The account alarm also arbitrates pairing expiry,
Expand Down
32 changes: 1 addition & 31 deletions adapters/whatsapp/src/lifecycle.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DisconnectReason } from "@whiskeysockets/baileys";

export const SOCKET_LEASE_REFRESH_INTERVAL_MS = 10 * 60 * 1000;
export const SOCKET_RESIDENCY_ALARM_INTERVAL_MS = 30_000;
export const PAIRING_WINDOW_MS = 2 * 60 * 1000;
export const INBOUND_RETRY_DELAY_MS = 10_000;
export const INBOUND_RETRY_BATCH_SIZE = 25;
Expand Down Expand Up @@ -78,36 +78,6 @@ export function nextAccountAlarmDeadline(
);
}

export type SocketLeaseHealth = {
hasSocket: boolean;
stateConnected: boolean;
socketAuthenticated: boolean;
webSocketOpen: boolean;
};

export type SocketLeaseAction = "wait" | "refresh" | "recover";

/**
* An established socket is checked on every account alarm, but a healthy
* socket is replaced only when its absolute Cloudflare keepalive lease is due.
*/
export function socketLeaseAction(
refreshAt: number | undefined,
health: SocketLeaseHealth,
now = Date.now(),
): SocketLeaseAction {
if (refreshAt === undefined || !Number.isFinite(refreshAt)) return "wait";
if (
!health.hasSocket
|| !health.stateConnected
|| !health.socketAuthenticated
|| !health.webSocketOpen
) {
return "recover";
}
return refreshAt <= now ? "refresh" : "wait";
}

export function pairingSessionExpired(
authenticated: boolean,
expiresAt: number | undefined,
Expand Down
4 changes: 3 additions & 1 deletion adapters/whatsapp/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type WhatsAppAccountState = {
lastActivity?: number;
lastError?: string;
disconnectReason?: string;
leaseRefreshAt?: number;
residencyAlarmAt?: number;
reconnectAt?: number;
connectionDeadlineAt?: number;
pairingExpiresAt?: number;
Expand Down Expand Up @@ -63,10 +63,12 @@ export function restoreWhatsAppAccountState(
if (stored?.version === 2) {
const {
rotationAt: _obsoleteRotationAt,
leaseRefreshAt: _obsoleteLeaseRefreshAt,
lastMessageAt: _obsoleteLastMessageAt,
...current
} = stored as WhatsAppAccountState & {
rotationAt?: number;
leaseRefreshAt?: number;
lastMessageAt?: number;
};
return { ...defaultWhatsAppAccountState(), ...current };
Expand Down
Loading
Loading