@decentnetwork/lan 0.1.270 → 0.1.272

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.
@@ -109,7 +109,7 @@ export interface IpcHandlers {
109
109
  * signals immediately, otherwise holds the connection until one arrives or a
110
110
  * ~20s timeout elapses (then resolves empty). The UI re-polls to get
111
111
  * near-instant offer/answer/candidate delivery without a WebSocket. */
112
- callPoll: () => Promise<Record<string, unknown>>;
112
+ callPoll: (since?: number) => Promise<Record<string, unknown>>;
113
113
  /** Re-read proxy allowlist from config and apply it to the running
114
114
  * proxy WITHOUT restarting the daemon. Lets `agentnet proxy
115
115
  * allow-host` take effect instantly instead of forcing a daemon
@@ -153,6 +153,8 @@ export interface IpcRequest {
153
153
  data?: string;
154
154
  /** Carrier custom-packet id for the "app-send" op (165-191). */
155
155
  packetId?: number;
156
+ /** Broadcast cursor for "call-poll": return signals with seq > since. */
157
+ since?: number;
156
158
  }
157
159
  export interface IpcResponseOk {
158
160
  ok: true;
@@ -297,7 +297,7 @@ export class IpcServer {
297
297
  case "dora-status":
298
298
  return await this.handlers.doraStatus();
299
299
  case "call-poll":
300
- return await this.handlers.callPoll();
300
+ return await this.handlers.callPoll(typeof req.since === "number" ? req.since : undefined);
301
301
  case "proxy-reload":
302
302
  return await this.handlers.proxyReload();
303
303
  case "proxy-access":
@@ -44,12 +44,25 @@ export declare class DaemonServer {
44
44
  /** Outgoing file transfers in flight: fileId → the chat message tracking it,
45
45
  * so progress/complete/cancel events can patch its status + sent bytes. */
46
46
  private readonly activeSends;
47
- /** Inbound WebRTC call-signaling payloads ({userid, data}) waiting to be
48
- * drained by the UI's /api/call-poll long-poll. Bounded so a UI that never
49
- * polls can't grow it unboundedly. */
50
- private readonly callSignalQueue;
47
+ /** Inbound WebRTC call-signaling payloads as a broadcast ring: every poller
48
+ * reads at its own cursor, entries expire by AGE (90s), never by being
49
+ * read. The previous shape — splice(0) on first poll — handed the whole
50
+ * batch to whichever poller arrived first, so with two tabs (or the
51
+ * desktop app + a browser) open, an incoming call rang a random one and
52
+ * the surface the user was looking at stayed silent (INBOX 2026-08-05).
53
+ * Bounded so a UI that never polls can't grow it unboundedly. */
54
+ private readonly callSignalLog;
55
+ /** Seeded with the epoch so a cursor from a PREVIOUS daemon incarnation is
56
+ * always smaller than any new signal's seq. Seeded at 0, a daemon restart
57
+ * made every open tab deaf: their old cursor exceeded the fresh counter,
58
+ * so `seq > since` never matched and incoming calls stopped ringing until
59
+ * a hard refresh. */
60
+ private callSignalSeq;
51
61
  /** Resolvers for in-flight call-poll long-polls, woken when a signal lands. */
52
62
  private callSignalWaiters;
63
+ /** Call signals older than this have no business ringing anything. */
64
+ private static readonly CALL_SIGNAL_TTL_MS;
65
+ private pruneCallSignals;
53
66
  /** Directory holding queued (offline) outgoing file bytes: <configDir>/outbox.
54
67
  * One file per queued message, named by its msgId. Set in start(). */
55
68
  private outboxDir;
@@ -90,12 +90,29 @@ export class DaemonServer {
90
90
  /** Outgoing file transfers in flight: fileId → the chat message tracking it,
91
91
  * so progress/complete/cancel events can patch its status + sent bytes. */
92
92
  activeSends = new Map();
93
- /** Inbound WebRTC call-signaling payloads ({userid, data}) waiting to be
94
- * drained by the UI's /api/call-poll long-poll. Bounded so a UI that never
95
- * polls can't grow it unboundedly. */
96
- callSignalQueue = [];
93
+ /** Inbound WebRTC call-signaling payloads as a broadcast ring: every poller
94
+ * reads at its own cursor, entries expire by AGE (90s), never by being
95
+ * read. The previous shape — splice(0) on first poll — handed the whole
96
+ * batch to whichever poller arrived first, so with two tabs (or the
97
+ * desktop app + a browser) open, an incoming call rang a random one and
98
+ * the surface the user was looking at stayed silent (INBOX 2026-08-05).
99
+ * Bounded so a UI that never polls can't grow it unboundedly. */
100
+ callSignalLog = [];
101
+ /** Seeded with the epoch so a cursor from a PREVIOUS daemon incarnation is
102
+ * always smaller than any new signal's seq. Seeded at 0, a daemon restart
103
+ * made every open tab deaf: their old cursor exceeded the fresh counter,
104
+ * so `seq > since` never matched and incoming calls stopped ringing until
105
+ * a hard refresh. */
106
+ callSignalSeq = Date.now();
97
107
  /** Resolvers for in-flight call-poll long-polls, woken when a signal lands. */
98
108
  callSignalWaiters = [];
109
+ /** Call signals older than this have no business ringing anything. */
110
+ static CALL_SIGNAL_TTL_MS = 90_000;
111
+ pruneCallSignals() {
112
+ const cutoff = Date.now() - DaemonServer.CALL_SIGNAL_TTL_MS;
113
+ while (this.callSignalLog.length && this.callSignalLog[0].ts < cutoff)
114
+ this.callSignalLog.shift();
115
+ }
99
116
  /** Directory holding queued (offline) outgoing file bytes: <configDir>/outbox.
100
117
  * One file per queued message, named by its msgId. Set in start(). */
101
118
  outboxDir = "";
@@ -708,11 +725,16 @@ export class DaemonServer {
708
725
  return { checkedAt: new Date().toISOString(), registries: [], note: "dora not enabled on this node" };
709
726
  return await this.doraIntegration.doraStatus();
710
727
  },
711
- callPoll: async () => {
712
- // Drain immediately if signals are queued; otherwise hold up to ~20s
713
- // for one to arrive (near-instant delivery without a WebSocket). The
714
- // IPC client's timeout is 30s, so a 20s hold is safe.
715
- if (this.callSignalQueue.length === 0) {
728
+ callPoll: async (since) => {
729
+ // Long-poll at a per-subscriber cursor: reading never consumes, so
730
+ // every open tab receives every signal. A caller without a cursor
731
+ // (older UI) starts at "now" and gets only future signals — the old
732
+ // semantics minus the stealing. Hold up to ~20s for a new signal
733
+ // (the IPC client's timeout is 30s, so a 20s hold is safe).
734
+ const from = since ?? this.callSignalSeq;
735
+ this.pruneCallSignals();
736
+ let avail = this.callSignalLog.filter((s) => s.seq > from);
737
+ if (avail.length === 0) {
716
738
  await new Promise((resolve) => {
717
739
  const timer = setTimeout(() => {
718
740
  this.callSignalWaiters = this.callSignalWaiters.filter((w) => w !== wake);
@@ -724,18 +746,19 @@ export class DaemonServer {
724
746
  };
725
747
  this.callSignalWaiters.push(wake);
726
748
  });
749
+ this.pruneCallSignals();
750
+ avail = this.callSignalLog.filter((s) => s.seq > from);
727
751
  }
728
- const signals = this.callSignalQueue.splice(0, this.callSignalQueue.length);
729
- if (signals.length) {
730
- const kinds = signals.map((s) => { try {
752
+ if (avail.length) {
753
+ const kinds = avail.map((s) => { try {
731
754
  return JSON.parse(s.data).type;
732
755
  }
733
756
  catch {
734
757
  return "?";
735
758
  } }).join(",");
736
- this.logger.info(`call-poll -> browser: ${signals.length} signal(s) [${kinds}]`);
759
+ this.logger.info(`call-poll -> browser: ${avail.length} signal(s) [${kinds}] (cursor ${from} -> ${this.callSignalSeq})`);
737
760
  }
738
- return { signals };
761
+ return { signals: avail.map(({ userid, data }) => ({ userid, data })), cursor: this.callSignalSeq };
739
762
  },
740
763
  subscribe: (emit) => {
741
764
  const onEvent = (event) => emit(event);
@@ -1198,9 +1221,10 @@ export class DaemonServer {
1198
1221
  if (!text)
1199
1222
  return;
1200
1223
  // Cap the backlog so a UI that stopped polling can't grow it forever.
1201
- if (this.callSignalQueue.length > 256)
1202
- this.callSignalQueue.shift();
1203
- this.callSignalQueue.push({ userid: evt.pubkey, data: text });
1224
+ if (this.callSignalLog.length > 256)
1225
+ this.callSignalLog.shift();
1226
+ this.callSignalLog.push({ seq: ++this.callSignalSeq, ts: Date.now(), userid: evt.pubkey, data: text });
1227
+ this.pruneCallSignals();
1204
1228
  const waiters = this.callSignalWaiters;
1205
1229
  this.callSignalWaiters = [];
1206
1230
  for (const wake of waiters)
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.270";
1
+ window.__DK_UI_VERSION="0.1.272";
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.270",
3
+ "version": "0.1.272",
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",