aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/lightning.ts39
-rw-r--r--lib/ln.ts1
2 files changed, 40 insertions, 0 deletions
diff --git a/lib/lightning.ts b/lib/lightning.ts
index 4eb70e00..9e259ed5 100644
--- a/lib/lightning.ts
+++ b/lib/lightning.ts
@@ -9,8 +9,13 @@ import { getInvoice, getPayment, getUser } from "$lib/utils";
const LISTENER_RETRY_DELAY = 5000; // 5 seconds
const MAX_LISTENER_RETRIES = 10;
+// If the listener has been blocked in waitanyinvoice this long without returning,
+// AND cl is reachable, treat the listen socket as a zombie and recycle it.
+const LISTENER_STALL_MS = 5 * 60 * 1000; // 5 minutes
let listenerRetries = 0;
let listenerActive = false;
+let waitStartedAt = Date.now();
+let listenerEpoch = 0;
export async function listenForLightning() {
if (listenerActive) {
@@ -19,6 +24,8 @@ export async function listenForLightning() {
}
listenerActive = true;
+ waitStartedAt = Date.now();
+ const myEpoch = listenerEpoch;
try {
const payIndex = (await g("pay_index")) || 0;
@@ -36,6 +43,9 @@ export async function listenForLightning() {
payment_preimage: preimage,
} = inv;
+ // watchdog recycled us while blocked — a fresh listener owns the stream now.
+ if (myEpoch !== listenerEpoch) return;
+
await s("pay_index", pay_index);
// Reset retry counter on successful receive
@@ -84,6 +94,7 @@ export async function listenForLightning() {
}
} catch (e: any) {
listenerActive = false;
+ if (myEpoch !== listenerEpoch) return; // watchdog already re-armed a fresh listener
const errorCode = e?.code ?? e?.errno ?? "unknown";
const errorMsg = e?.message ?? String(e);
@@ -116,6 +127,34 @@ export async function listenForLightning() {
}
}
+export async function ensureListenerAlive() {
+ if (!listenerActive) {
+ warn("lightning listener: not active, restarting");
+ listenForLightning();
+ return;
+ }
+ if (Date.now() - waitStartedAt < LISTENER_STALL_MS) return;
+ let clAlive = false;
+ try {
+ const info = await ln.getinfo();
+ clAlive = !!info?.id;
+ } catch (_) {
+ clAlive = false;
+ }
+ if (!clAlive) return; // cl itself down — the retry/exit path handles that
+ err(
+ `lightning listener: stalled ${Math.round(
+ (Date.now() - waitStartedAt) / 1000,
+ )}s while cl is alive — recycling listen socket`,
+ );
+ listenerEpoch++;
+ try {
+ (lnListen as any).reset?.();
+ } catch (_) {}
+ listenerActive = false;
+ setTimeout(listenForLightning);
+}
+
export async function replay(index) {
const inv = await lnListen.waitanyinvoice(index - 1);
const {
diff --git a/lib/ln.ts b/lib/ln.ts
index 2cbd5120..c125eec4 100644
--- a/lib/ln.ts
+++ b/lib/ln.ts
@@ -98,6 +98,7 @@ function lightningProxy(rpcPath: string): any {
get(_t, prop: string | symbol) {
if (prop === "toString") return () => "[LightningProxy]";
if (prop === Symbol.toStringTag) return "LightningProxy";
+ if (prop === "reset") return () => { client = null; }; // watchdog: drop a zombie socket
return async (...args: any[]) => {
const c = ensure();