@decentnetwork/lan 0.1.151 → 0.1.154

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.
Binary file
Binary file
Binary file
Binary file
@@ -114,6 +114,7 @@ const STALL_MIN_OPEN_MS = 4000; // only a tunnel open at least this long can cou
114
114
  const STALL_MIN_BYTES = 16 * 1024; // delivered at least this = a healthy, real segment
115
115
  const STALL_DEAD_BYTES = 1024; // relayed LESS than this = the exit is genuinely dead (only THIS counts as a stall); a small-but-real request (playlist/key/keep-alive, 1-16 KB) is NEUTRAL, so it can't flap a healthy exit out of the pool
116
116
  const TRIP_AFTER_STALLS = 3; // consecutive DEAD tunnels before an exit is tripped out (hysteresis)
117
+ const TRIP_AFTER_UPSTREAM_FAILS = 5; // consecutive non-200 CONNECT responses (exit's proxy broken, e.g. 502s everything) before tripping
117
118
  const TRIP_COOLDOWN_MS = 30_000; // how long a tripped exit stays excluded before it's retried
118
119
  // Measures peak short-window throughput of a byte stream. Feed it every chunk
119
120
  // length via add(); read peakBps at the end. Peak (not average) shrugs off TCP
@@ -196,6 +197,7 @@ export function startMultiExitRouter(opts) {
196
197
  served: 0,
197
198
  lastRtt: null,
198
199
  lastSuccessMs: null,
200
+ consecUpstreamFails: 0,
199
201
  speedEwma: null,
200
202
  bytesDown: 0,
201
203
  stalls: 0,
@@ -458,6 +460,7 @@ export function startMultiExitRouter(opts) {
458
460
  // tunnels are flowing.
459
461
  exit.lastSuccessMs = Date.now();
460
462
  exit.fails = 0;
463
+ exit.consecUpstreamFails = 0;
461
464
  if (!exit.healthy) {
462
465
  exit.healthy = true;
463
466
  log(`✓ ${exit.name} [${exit.region}] UP (live traffic)`);
@@ -527,7 +530,20 @@ export function startMultiExitRouter(opts) {
527
530
  up.on("error", () => client.destroy());
528
531
  }
529
532
  else {
530
- // Exit answered but the ORIGIN errored (502/403/…) not an exit fault.
533
+ // Exit answered with a non-200 CONNECT status. Usually the ORIGIN's
534
+ // fault (a dead host 502s on EVERY exit — tripping would churn the pool),
535
+ // so we just fail over. BUT if THIS exit returns non-200 for many targets
536
+ // in a row (reset on any success), its own proxy is broken — e.g. it 502s
537
+ // everything (dead upstream / wrong version). Trip it so it stops eating
538
+ // the first pick on every request. A single dead origin can't reach the
539
+ // threshold: other hosts still succeed on this exit and reset the streak.
540
+ exit.consecUpstreamFails++;
541
+ if (exit.consecUpstreamFails >= TRIP_AFTER_UPSTREAM_FAILS && exit.trippedUntil <= Date.now()) {
542
+ exit.trippedUntil = Date.now() + TRIP_COOLDOWN_MS;
543
+ exit.healthy = false;
544
+ log(`✗ ${exit.name} [${exit.region}] TRIPPED for ${TRIP_COOLDOWN_MS / 1000}s (proxy returned ${status.trim()} for ${exit.consecUpstreamFails} targets in a row) — excluding from pool`);
545
+ printPool();
546
+ }
531
547
  fail(`upstream ${status}`, false);
532
548
  }
533
549
  };
package/dist/types.d.ts CHANGED
@@ -110,6 +110,10 @@ export interface BootstrapNode {
110
110
  port: number;
111
111
  pk: string;
112
112
  isTcp?: boolean;
113
+ /** Express relays only: set `false` to reach the relay over plain HTTP
114
+ * instead of HTTPS (the payload is already end-to-end encrypted). Lets a
115
+ * relay run on a raw IP with no TLS cert / domain / ICP filing. */
116
+ tls?: boolean;
113
117
  }
114
118
  export interface CarrierConfig {
115
119
  dataDir: string;
@@ -173,15 +173,24 @@ async function dkGet(path) {
173
173
  return r.json();
174
174
  }
175
175
  async function dkPost(path, body) {
176
- const r = await fetch(path, {
177
- method: "POST",
178
- headers: { "content-type": "application/json" },
179
- body: JSON.stringify(body || {})
180
- });
176
+ const ctrl = new AbortController();
177
+ const timer = setTimeout(() => ctrl.abort(), 15e3);
181
178
  try {
182
- return await r.json();
179
+ const r = await fetch(path, {
180
+ method: "POST",
181
+ headers: { "content-type": "application/json" },
182
+ body: JSON.stringify(body || {}),
183
+ signal: ctrl.signal
184
+ });
185
+ try {
186
+ return await r.json();
187
+ } catch (e) {
188
+ return { ok: r.ok };
189
+ }
183
190
  } catch (e) {
184
- return { ok: r.ok };
191
+ return { ok: false, error: e && e.name === "AbortError" ? "timed out" : e && e.message || "network error" };
192
+ } finally {
193
+ clearTimeout(timer);
185
194
  }
186
195
  }
187
196
  function dkCopy(text) {
@@ -1012,6 +1021,9 @@ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd }) {
1012
1021
  } else {
1013
1022
  setAddState({ kind: "err", msg: r && r.error ? `${T.addFailed}: ${r.error}` : T.addFailed });
1014
1023
  }
1024
+ }).catch((e) => {
1025
+ setAddState({ kind: "err", msg: `${T.addFailed}: ${e && e.message || e}` });
1026
+ }).finally(() => {
1015
1027
  setTimeout(() => setAddState((s) => s && s.kind !== "sending" ? null : s), 6e3);
1016
1028
  });
1017
1029
  };
package/dist/ui/server.js CHANGED
@@ -665,8 +665,27 @@ export function startFriendUi(opts) {
665
665
  }
666
666
  if (req.method === "POST" && url === "/api/add") {
667
667
  const { address } = await readBody(req);
668
- const r = await opts.call({ op: "friend-request", address });
669
- sendJson(res, r.ok ? 200 : 400, r);
668
+ // A friend-request is fire-and-forget: it's delivered when the peer is
669
+ // online and only becomes a friend once they accept. Don't let the UI
670
+ // hang on the network round-trip (DHT lookup / unreachable peer) — kick
671
+ // the send off, but respond within a short window. A fast failure (e.g.
672
+ // a malformed address) still surfaces its real error; a slow send
673
+ // returns optimistic success and continues in the background.
674
+ const sent = opts
675
+ .call({ op: "friend-request", address })
676
+ .then((r) => ({ done: true, r }))
677
+ .catch((e) => ({ done: true, r: { ok: false, error: e instanceof Error ? e.message : String(e) } }));
678
+ const settled = await Promise.race([
679
+ sent,
680
+ new Promise((r) => setTimeout(() => r({ done: false }), 2500)),
681
+ ]);
682
+ if (settled.done) {
683
+ sendJson(res, settled.r.ok ? 200 : 400, settled.r);
684
+ }
685
+ else {
686
+ void sent.catch(() => { }); // let it finish in the background, no unhandled rejection
687
+ sendJson(res, 200, { ok: true, queued: true });
688
+ }
670
689
  return;
671
690
  }
672
691
  res.writeHead(404);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.151",
3
+ "version": "0.1.154",
4
4
  "description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",