@decentnetwork/lan 0.1.281 → 0.1.282

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.
@@ -169,6 +169,9 @@ export declare class IpcServer {
169
169
  private socketPath;
170
170
  private handlers;
171
171
  private server?;
172
+ /** Live client connections, so stop() can end them instead of waiting on
173
+ * a subscriber that never hangs up. */
174
+ private openSockets;
172
175
  private logger;
173
176
  constructor(socketPath: string, handlers: IpcHandlers);
174
177
  start(): Promise<void>;
@@ -61,6 +61,9 @@ export class IpcServer {
61
61
  socketPath;
62
62
  handlers;
63
63
  server;
64
+ /** Live client connections, so stop() can end them instead of waiting on
65
+ * a subscriber that never hangs up. */
66
+ openSockets = new Set();
64
67
  logger;
65
68
  constructor(socketPath, handlers) {
66
69
  this.socketPath = socketPath;
@@ -79,7 +82,14 @@ export class IpcServer {
79
82
  this.logger.warn(`Could not remove stale socket ${this.socketPath}: ${err}`);
80
83
  }
81
84
  }
82
- this.server = createServer((sock) => this.handleConnection(sock));
85
+ this.server = createServer((sock) => {
86
+ // Track every live connection so stop() can force them shut. Without
87
+ // this, server.close() waits for all of them and a single long-lived
88
+ // subscriber deadlocks shutdown (see stop()).
89
+ this.openSockets.add(sock);
90
+ sock.once("close", () => this.openSockets.delete(sock));
91
+ this.handleConnection(sock);
92
+ });
83
93
  await new Promise((resolve, reject) => {
84
94
  this.server.once("error", reject);
85
95
  this.server.listen(this.socketPath, () => {
@@ -103,6 +113,22 @@ export class IpcServer {
103
113
  async stop() {
104
114
  if (!this.server)
105
115
  return;
116
+ // server.close() only fires its callback once every EXISTING connection
117
+ // has closed — it stops new ones, it does not end open ones. Event
118
+ // subscribers ({op:"subscribe"}) hold their connection open by design and
119
+ // never close it, so waiting on that callback is an unbounded wait on a
120
+ // peer that is never going to hang up.
121
+ //
122
+ // That is not theoretical: a daemon logged "Stopping daemon" and then sat
123
+ // in teardown for 24h46m, finishing 300ms after an unrelated `pm2 restart`
124
+ // happened to drop the subscriber. The same bug is behind the SIGTERM
125
+ // hangs that ran out systemd's TimeoutStopSec — every journal entry with
126
+ // "Shutting down..." and no matching "Daemon stopped" is this.
127
+ //
128
+ // We are shutting down, so end them ourselves and let close() complete.
129
+ for (const sock of this.openSockets)
130
+ sock.destroy();
131
+ this.openSockets.clear();
106
132
  await new Promise((resolve) => {
107
133
  this.server.close(() => resolve());
108
134
  });
@@ -564,20 +564,22 @@ export class DaemonServer {
564
564
  this.logger.info(`Queued file "${name}" (${data.length}B) for offline ${userid.slice(0, 8)} (delivers on reconnect)`);
565
565
  return { queued: true, name: safe, size: data.length };
566
566
  }
567
- // NATIVE friends (iOS/Android/C Carrier): small files take the inline
568
- // FileModel-JSON-over-bulkmsg envelope (single blob, the Beagle apps'
569
- // own fast path); anything bigger falls through to the streaming
570
- // sendFile below, which peer >= 0.1.141 carries to natives as DNFT1
571
- // frames inside friend messages (offset acks, FEC, resume — verified
572
- // 100MB JS→iPad). This gate used to REJECT >11MB outright, which
573
- // blocked the very path built for large files.
574
- const INLINE_MAX = 11 * 1024 * 1024;
575
- if (this.peerManager?.isNativeFriend(userid) && data.length <= INLINE_MAX) {
576
- // The FileModel JSON envelope is base64 (inflates ~1.34x) + a little
577
- // JSON overhead, and the whole thing must fit the 16MB bulkmsg cap
578
- // (CARRIER_MAX_APP_BULKMSG_LEN, raised from 5MB alongside the receive
579
- // reorder buffer in peer >= 0.1.87). 11MB raw → ~14.7MB envelope,
580
- // safely under 16MB. Needs BOTH ends on the raised cap.
567
+ // NATIVE friends (iOS/Android/C Carrier): only TINY files take the
568
+ // inline FileModel envelope — a thumbnail/voice-note shortcut, one
569
+ // blob, one round trip. Everything else streams via sendFile, which
570
+ // peer >= 0.1.141 carries to natives as DNFT1 frames (offset acks,
571
+ // FEC, resume — verified 100MB JS→iPad).
572
+ //
573
+ // The gate was 11MB, then 2.5MB; both sat inside a band where the
574
+ // envelope's ONLY confirmation signal — the toxcore send window
575
+ // draining — is structurally unreliable: an 8.5MB envelope is ~11k
576
+ // bulkmsg fragments on a channel with no congestion control, and
577
+ // three live 8.5MB sends all timed out unconfirmed at 180s while a
578
+ // 5.2MB died silently in the native kernel. At 256KB (~350
579
+ // fragments) the window drains in seconds and the transport ack
580
+ // actually means something.
581
+ const INLINE_NATIVE_MAX = 256 * 1024;
582
+ if (this.peerManager?.isNativeFriend(userid) && data.length <= INLINE_NATIVE_MAX) {
581
583
  // Append the chip as "sending" FIRST, then let the SDK's delivery
582
584
  // verdict decide what it becomes. Marking "sent" the moment
583
585
  // sendInlineFile resolved was a lie — that only meant the bytes
@@ -601,14 +603,15 @@ export class DaemonServer {
601
603
  if (chip)
602
604
  this.messageStore?.patchFile(userid, chip.id, { status: "failed" });
603
605
  this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
604
- this.logger.warn(`Inline file "${name}" to native ${userid.slice(0, 8)} failed: ${e.message}`);
606
+ this.logger.warn(`Inline file "${name}" to native ${userid.slice(0, 8)} failed to send: ${e.message}`);
605
607
  return;
606
608
  }
607
609
  const status = delivery === "acked" ? "sent" : delivery === "offline" ? "queued" : "failed";
608
610
  if (chip)
609
611
  this.messageStore?.patchFile(userid, chip.id, { status, sent: delivery === "acked" ? data.length : 0 });
610
612
  this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
611
- this.logger.info(`Inline file "${name}" (${data.length}B) to native ${userid.slice(0, 8)}: delivery=${delivery} → ${status}`);
613
+ this.logger.info(`Inline file "${name}" (${data.length}B) to native ${userid.slice(0, 8)}: delivery=${delivery} → ${status}` +
614
+ (delivery === "accepted" ? " (unconfirmed — the bytes went out and may have arrived, but the peer never acknowledged)" : ""));
612
615
  })();
613
616
  return { inline: true, name, size: data.length, pending: true };
614
617
  }
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.281";
1
+ window.__DK_UI_VERSION="0.1.282";
2
2
  const ICON_PATHS = {
3
3
  // ---- tab bar (the four must feel like one set) ----
4
4
  users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.281",
3
+ "version": "0.1.282",
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",