@decentnetwork/lan 0.1.270 → 0.1.271

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,20 @@ 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
+ private callSignalSeq;
51
56
  /** Resolvers for in-flight call-poll long-polls, woken when a signal lands. */
52
57
  private callSignalWaiters;
58
+ /** Call signals older than this have no business ringing anything. */
59
+ private static readonly CALL_SIGNAL_TTL_MS;
60
+ private pruneCallSignals;
53
61
  /** Directory holding queued (offline) outgoing file bytes: <configDir>/outbox.
54
62
  * One file per queued message, named by its msgId. Set in start(). */
55
63
  private outboxDir;
@@ -90,12 +90,24 @@ 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
+ callSignalSeq = 0;
97
102
  /** Resolvers for in-flight call-poll long-polls, woken when a signal lands. */
98
103
  callSignalWaiters = [];
104
+ /** Call signals older than this have no business ringing anything. */
105
+ static CALL_SIGNAL_TTL_MS = 90_000;
106
+ pruneCallSignals() {
107
+ const cutoff = Date.now() - DaemonServer.CALL_SIGNAL_TTL_MS;
108
+ while (this.callSignalLog.length && this.callSignalLog[0].ts < cutoff)
109
+ this.callSignalLog.shift();
110
+ }
99
111
  /** Directory holding queued (offline) outgoing file bytes: <configDir>/outbox.
100
112
  * One file per queued message, named by its msgId. Set in start(). */
101
113
  outboxDir = "";
@@ -708,11 +720,16 @@ export class DaemonServer {
708
720
  return { checkedAt: new Date().toISOString(), registries: [], note: "dora not enabled on this node" };
709
721
  return await this.doraIntegration.doraStatus();
710
722
  },
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) {
723
+ callPoll: async (since) => {
724
+ // Long-poll at a per-subscriber cursor: reading never consumes, so
725
+ // every open tab receives every signal. A caller without a cursor
726
+ // (older UI) starts at "now" and gets only future signals — the old
727
+ // semantics minus the stealing. Hold up to ~20s for a new signal
728
+ // (the IPC client's timeout is 30s, so a 20s hold is safe).
729
+ const from = since ?? this.callSignalSeq;
730
+ this.pruneCallSignals();
731
+ let avail = this.callSignalLog.filter((s) => s.seq > from);
732
+ if (avail.length === 0) {
716
733
  await new Promise((resolve) => {
717
734
  const timer = setTimeout(() => {
718
735
  this.callSignalWaiters = this.callSignalWaiters.filter((w) => w !== wake);
@@ -724,18 +741,19 @@ export class DaemonServer {
724
741
  };
725
742
  this.callSignalWaiters.push(wake);
726
743
  });
744
+ this.pruneCallSignals();
745
+ avail = this.callSignalLog.filter((s) => s.seq > from);
727
746
  }
728
- const signals = this.callSignalQueue.splice(0, this.callSignalQueue.length);
729
- if (signals.length) {
730
- const kinds = signals.map((s) => { try {
747
+ if (avail.length) {
748
+ const kinds = avail.map((s) => { try {
731
749
  return JSON.parse(s.data).type;
732
750
  }
733
751
  catch {
734
752
  return "?";
735
753
  } }).join(",");
736
- this.logger.info(`call-poll -> browser: ${signals.length} signal(s) [${kinds}]`);
754
+ this.logger.info(`call-poll -> browser: ${avail.length} signal(s) [${kinds}] (cursor ${from} -> ${this.callSignalSeq})`);
737
755
  }
738
- return { signals };
756
+ return { signals: avail.map(({ userid, data }) => ({ userid, data })), cursor: this.callSignalSeq };
739
757
  },
740
758
  subscribe: (emit) => {
741
759
  const onEvent = (event) => emit(event);
@@ -1198,9 +1216,10 @@ export class DaemonServer {
1198
1216
  if (!text)
1199
1217
  return;
1200
1218
  // 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 });
1219
+ if (this.callSignalLog.length > 256)
1220
+ this.callSignalLog.shift();
1221
+ this.callSignalLog.push({ seq: ++this.callSignalSeq, ts: Date.now(), userid: evt.pubkey, data: text });
1222
+ this.pruneCallSignals();
1204
1223
  const waiters = this.callSignalWaiters;
1205
1224
  this.callSignalWaiters = [];
1206
1225
  for (const wake of waiters)
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.270";
1
+ window.__DK_UI_VERSION="0.1.271";
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.271",
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",