@decentnetwork/lan 0.1.269 → 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)
@@ -413,11 +413,21 @@ export class DoraIntegration {
413
413
  try {
414
414
  // One client per registry: this is the whole point — a merged call
415
415
  // would hide exactly the failure we are looking for.
416
+ // Same channel and event as the constructor above, deliberately:
417
+ // dora speaks on packet 162 via sendDora / "dora-message", NOT on
418
+ // chat's packet 64. Reaching for sendText/"message" here sent every
419
+ // request down a channel the registry never reads, and produced four
420
+ // identical timeouts that looked exactly like four dead doras.
421
+ //
422
+ // Timeout matches too. 8s was optimistic: the comment on the
423
+ // constructor records that China -> overseas bootstrap -> relay ->
424
+ // the dora's own relay and back consistently blows past 10s, which
425
+ // is why the working path uses 30.
416
426
  const one = new DoraClient({
417
427
  registryUserids: [id],
418
- sendText: (to, text) => this.opts.peerManager.sendText(to, text),
419
- onText: (h) => this.opts.peerManager.on("message", (pubkey, text) => h(pubkey, text)),
420
- timeoutMs: 8000,
428
+ sendText: (to, text) => this.opts.peerManager.sendDora(to, text),
429
+ onText: (h) => this.opts.peerManager.on("dora-message", (from, text) => h(from, text)),
430
+ timeoutMs: parseInt(process.env.AGENTNET_DORA_TIMEOUT_MS || "30000", 10),
421
431
  });
422
432
  const roster = await one.list();
423
433
  return { userid: id, ok: Array.isArray(roster) && roster.length > 0,
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.269";
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.269",
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",