aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2025-12-16 13:42:42 -0800
committerAdam Soltys <[email protected]>2025-12-16 13:42:42 -0800
commit52fb26dbaf6a169cc63c50d480d3e7a9a999854c (patch)
treee6415cfd593004d2d1512597a07b258ff5471167
parent2dc93f8fa0cb1b57d74ff25f99f944d6d0bac789 (diff)
downloadcoinos-server-52fb26dbaf6a169cc63c50d480d3e7a9a999854c.tar.gz
coinos-server-52fb26dbaf6a169cc63c50d480d3e7a9a999854c.zip
more robust lightning client
-rw-r--r--lib/ln.ts88
1 files changed, 85 insertions, 3 deletions
diff --git a/lib/ln.ts b/lib/ln.ts
index c4ab40fd..1e190c31 100644
--- a/lib/ln.ts
+++ b/lib/ln.ts
@@ -1,5 +1,87 @@
import config from "$config";
-const { LightningClient } = (await import("@asoltys/clightning-client")).default;
-export default new LightningClient(config.lightning);
-export const lnb = new LightningClient(config.lightningb);
+const mod = (await import("@asoltys/clightning-client")).default;
+const { LightningClient } = mod as { LightningClient: any };
+
+class LightningUnavailableError extends Error {
+ code = "LIGHTNING_UNAVAILABLE" as const;
+ constructor(msg: string) {
+ super(msg);
+ this.name = "LightningUnavailableError";
+ }
+}
+
+function isUnavailable(e: any) {
+ const code = e?.code ?? e?.errno;
+ return code === "ENOENT" || code === 2 || code === "ECONNREFUSED";
+}
+
+function isSocketDied(e: any) {
+ const code = e?.code ?? e?.errno;
+ return code === "EPIPE" || code === "ECONNRESET" || code === "ENOENT" || code === 2;
+}
+
+function lightningProxy(rpcPath: string) {
+ let client: any = null;
+
+ let nextTryAt = 0;
+ let backoff = 250;
+ const maxBackoff = 5000;
+
+ function ensure() {
+ if (client) return client;
+
+ const now = Date.now();
+ if (now < nextTryAt) {
+ throw new LightningUnavailableError(`Lightning not ready at ${rpcPath}`);
+ }
+
+ try {
+ client = new LightningClient(rpcPath);
+ backoff = 250;
+ nextTryAt = 0;
+ return client;
+ } catch (e: any) {
+ nextTryAt = Date.now() + backoff;
+ backoff = Math.min(maxBackoff, Math.floor(backoff * 1.8));
+
+ if (isUnavailable(e)) {
+ throw new LightningUnavailableError(
+ `Lightning RPC unavailable at ${rpcPath} (${e?.code ?? e?.errno ?? "error"})`
+ );
+ }
+ throw e;
+ }
+ }
+
+ return new Proxy(
+ {},
+ {
+ get(_t, prop: string | symbol) {
+ if (prop === "toString") return () => "[LightningProxy]";
+ if (prop === Symbol.toStringTag) return "LightningProxy";
+
+ return async (...args: any[]) => {
+ const c = ensure();
+ const v = c[prop as any];
+
+ if (typeof v !== "function") return v;
+
+ try {
+ return await v.apply(c, args);
+ } catch (e: any) {
+ if (isSocketDied(e)) client = null;
+ throw e;
+ }
+ };
+ },
+ }
+ );
+}
+
+// exports preserved
+const ln = lightningProxy(config.lightning);
+export default ln;
+
+export const lnb = lightningProxy(config.lightningb);
+export { LightningUnavailableError };