@decentnetwork/lan 0.1.113 → 0.1.115

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
@@ -1054,7 +1054,12 @@ var DaemonClient = class _DaemonClient {
1054
1054
  const arr = chats[userid] ?? [];
1055
1055
  return arr.map((m) => {
1056
1056
  const file = m.file;
1057
- const text = file ? `\u{1F4CE} ${file.name ?? "file"} (${fileSize(typeof file.size === "number" ? file.size : 0)})` : String(m.text ?? "");
1057
+ let suffix = "";
1058
+ if (file?.status === "sending") {
1059
+ const pct = file.size ? Math.min(100, Math.floor((file.sent ?? 0) / file.size * 100)) : 0;
1060
+ suffix = ` \u2014 sending ${pct}%`;
1061
+ } else if (file?.status === "failed") suffix = " \u2014 failed";
1062
+ const text = file ? `\u{1F4CE} ${file.name ?? "file"} (${fileSize(typeof file.size === "number" ? file.size : 0)})${suffix}` : String(m.text ?? "");
1058
1063
  return {
1059
1064
  t: hhmm(typeof m.ts === "number" ? m.ts : void 0),
1060
1065
  who: m.dir === "out" ? "me" : "them",
@@ -16,10 +16,14 @@ export interface ChatMessage {
16
16
  id: string;
17
17
  /** Present when this entry is a file transfer rather than a text message.
18
18
  * `name`/`size` describe the file; received files (dir:"in") are saved to
19
- * <configDir>/downloads/<name> and downloadable via the UI. */
19
+ * <configDir>/downloads/<name> and downloadable via the UI. For outgoing
20
+ * files, `status` tracks delivery and `sent` is the acked byte count (live
21
+ * progress) — the receiver confirms every byte before status becomes "sent". */
20
22
  file?: {
21
23
  name: string;
22
24
  size: number;
25
+ status?: "sending" | "sent" | "failed";
26
+ sent?: number;
23
27
  };
24
28
  }
25
29
  export declare class MessageStore {
@@ -33,11 +37,19 @@ export declare class MessageStore {
33
37
  private load;
34
38
  /** Append a message and schedule a flush. Returns the stored message. */
35
39
  append(peer: string, dir: "in" | "out", text: string, ts?: number): ChatMessage;
36
- /** Append a file-transfer entry (shown as a download chip in the UI). */
40
+ /** Append a file-transfer entry (shown as a file chip in the UI). */
37
41
  appendFile(peer: string, dir: "in" | "out", file: {
38
42
  name: string;
39
43
  size: number;
44
+ status?: "sending" | "sent" | "failed";
45
+ sent?: number;
40
46
  }, ts?: number): ChatMessage;
47
+ /** Patch an existing file message's transfer fields (status / sent bytes).
48
+ * No-op if the id isn't found. Returns true if it patched. */
49
+ patchFile(peer: string, id: string, patch: {
50
+ status?: "sending" | "sent" | "failed";
51
+ sent?: number;
52
+ }): boolean;
41
53
  private push;
42
54
  /**
43
55
  * Return history. With no peer, returns every peer's full thread (the legacy
@@ -47,10 +47,21 @@ export class MessageStore {
47
47
  append(peer, dir, text, ts = Date.now()) {
48
48
  return this.push(peer, { dir, text, ts, id: `${ts}-${this.seq++}` });
49
49
  }
50
- /** Append a file-transfer entry (shown as a download chip in the UI). */
50
+ /** Append a file-transfer entry (shown as a file chip in the UI). */
51
51
  appendFile(peer, dir, file, ts = Date.now()) {
52
52
  return this.push(peer, { dir, text: "", ts, id: `${ts}-${this.seq++}`, file });
53
53
  }
54
+ /** Patch an existing file message's transfer fields (status / sent bytes).
55
+ * No-op if the id isn't found. Returns true if it patched. */
56
+ patchFile(peer, id, patch) {
57
+ const arr = this.byPeer.get(peer);
58
+ const msg = arr?.find((m) => m.id === id);
59
+ if (!msg || !msg.file)
60
+ return false;
61
+ msg.file = { ...msg.file, ...patch };
62
+ this.scheduleSave();
63
+ return true;
64
+ }
54
65
  push(peer, msg) {
55
66
  let arr = this.byPeer.get(peer);
56
67
  if (!arr) {
@@ -41,6 +41,9 @@ export declare class DaemonServer {
41
41
  * chat, presence changes, and friend-requests so clients can refresh on
42
42
  * demand instead of polling on a tight interval. */
43
43
  private readonly ipcEvents;
44
+ /** Outgoing file transfers in flight: fileId → the chat message tracking it,
45
+ * so progress/complete/cancel events can patch its status + sent bytes. */
46
+ private readonly activeSends;
44
47
  private startedAt;
45
48
  private isRunning;
46
49
  private statusTimer?;
@@ -76,6 +76,9 @@ export class DaemonServer {
76
76
  * chat, presence changes, and friend-requests so clients can refresh on
77
77
  * demand instead of polling on a tight interval. */
78
78
  ipcEvents = new EventEmitter();
79
+ /** Outgoing file transfers in flight: fileId → the chat message tracking it,
80
+ * so progress/complete/cancel events can patch its status + sent bytes. */
81
+ activeSends = new Map();
79
82
  startedAt = 0;
80
83
  isRunning = false;
81
84
  statusTimer;
@@ -325,8 +328,13 @@ export class DaemonServer {
325
328
  if (!fileId)
326
329
  throw new Error("No free transfer slot (or friend unknown)");
327
330
  this.logger.info(`Offering file "${name}" (${data.length}B) to ${userid.slice(0, 8)}`);
328
- // Record in chat history (as an outgoing file chip) and push to the UI.
329
- this.messageStore?.appendFile(userid, "out", { name, size: data.length });
331
+ // Add the "out" chip immediately as STATUS=sending (not "sent" the
332
+ // transfer is reliable+acked, so it only flips to "sent" once the
333
+ // receiver confirms every byte). Progress/complete/cancel events patch
334
+ // it. The chip is tracked by fileId so those events can find it.
335
+ const msg = this.messageStore?.appendFile(userid, "out", { name: sanitizeFileName(name), size: data.length, status: "sending", sent: 0 });
336
+ if (msg)
337
+ this.activeSends.set(fileId, { peer: userid, msgId: msg.id });
330
338
  this.friendMeta?.ensure(userid);
331
339
  this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
332
340
  return { fileId, name, size: data.length };
@@ -603,10 +611,40 @@ export class DaemonServer {
603
611
  this.logger.info(`Incoming file "${o.name}" (${o.size}B) from ${o.friendId.slice(0, 8)} — accepting`);
604
612
  this.peerManager?.acceptFile(o.friendId, o.fileNumber);
605
613
  });
614
+ // Live send progress → patch the chip's sent bytes + push so the UI shows
615
+ // "sending …%". Throttled implicitly by the SDK's progress cadence.
616
+ this.peerManager.on("file-progress", (p) => {
617
+ if (!p.sending || !p.fileId)
618
+ return;
619
+ const t = this.activeSends.get(p.fileId);
620
+ if (!t)
621
+ return;
622
+ this.messageStore?.patchFile(t.peer, t.msgId, { sent: p.received });
623
+ this.ipcEvents.emit("event", { type: "chat", userid: t.peer, dir: "out" });
624
+ });
625
+ this.peerManager.on("file-cancel", (p) => {
626
+ if (!p.sending)
627
+ return;
628
+ this.logger.warn(`File send to ${p.friendId.slice(0, 8)} aborted (${p.reason ?? "cancelled"})`);
629
+ const t = p.fileId ? this.activeSends.get(p.fileId) : undefined;
630
+ if (t) {
631
+ this.messageStore?.patchFile(t.peer, t.msgId, { status: "failed" });
632
+ this.activeSends.delete(p.fileId);
633
+ this.ipcEvents.emit("event", { type: "chat", userid: t.peer, dir: "out" });
634
+ }
635
+ });
606
636
  this.peerManager.on("file-complete", (p) => {
607
637
  if (p.sending || !p.data) {
608
- if (p.sending)
609
- this.logger.info(`File "${p.name}" sent to ${p.friendId.slice(0, 8)}`);
638
+ if (p.sending) {
639
+ // The receiver has ACKed the whole file — now it's truly delivered.
640
+ this.logger.info(`File "${p.name}" delivered to ${p.friendId.slice(0, 8)}`);
641
+ const t = p.fileId ? this.activeSends.get(p.fileId) : undefined;
642
+ if (t) {
643
+ this.messageStore?.patchFile(t.peer, t.msgId, { status: "sent", sent: p.size });
644
+ this.activeSends.delete(p.fileId);
645
+ this.ipcEvents.emit("event", { type: "chat", userid: t.peer, dir: "out" });
646
+ }
647
+ }
610
648
  return;
611
649
  }
612
650
  void (async () => {
@@ -267,7 +267,13 @@ function useDaemonData() {
267
267
  from: m.dir === "out" ? "me" : "them",
268
268
  time: dkClock(m.ts),
269
269
  text: m.text,
270
- file: m.file ? { name: m.file.name, size: dkFileSize(m.file.size), dir: m.dir } : void 0,
270
+ file: m.file ? {
271
+ name: m.file.name,
272
+ size: dkFileSize(m.file.size),
273
+ dir: m.dir,
274
+ status: m.file.status,
275
+ pct: m.file.status === "sending" && m.file.size ? Math.min(100, Math.floor((m.file.sent || 0) / m.file.size * 100)) : void 0
276
+ } : void 0,
271
277
  status: m.dir === "out" ? "read" : void 0
272
278
  }));
273
279
  const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
@@ -1033,8 +1039,8 @@ function Msg({ m, peer, T }) {
1033
1039
  }
1034
1040
  },
1035
1041
  /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 7, flexShrink: 0, background: mine ? "rgba(255,255,255,0.16)" : "var(--chip)", display: "flex", alignItems: "center", justifyContent: "center", color: mine ? "#fff" : "var(--accent)" } }, /* @__PURE__ */ React.createElement(Icon, { name: m.file.kind || "file", size: 18, stroke: 1.9 })),
1036
- /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: mine ? "#fff" : "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: mine ? "rgba(255,255,255,0.7)" : "var(--faint)", marginTop: 1 } }, m.file.size, mine ? "" : " \xB7 download")),
1037
- mine ? /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 16, stroke: 2, color: "var(--accent)" })
1042
+ /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: mine ? "#fff" : "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: mine ? "rgba(255,255,255,0.7)" : "var(--faint)", marginTop: 1 } }, m.file.status === "sending" ? `${m.file.size} \xB7 sending ${m.file.pct != null ? m.file.pct + "%" : "\u2026"}` : m.file.status === "failed" ? `${m.file.size} \xB7 failed` : mine ? `${m.file.size} \xB7 sent` : `${m.file.size} \xB7 download`)),
1043
+ mine ? m.file.status === "sending" ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "rgba(255,255,255,0.9)" } }, m.file.pct != null ? m.file.pct + "%" : "\u2026") : m.file.status === "failed" ? /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 16, stroke: 2.4, color: "rgba(255,200,190,0.95)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 16, stroke: 2, color: "var(--accent)" })
1038
1044
  ) : /* @__PURE__ */ React.createElement("div", { style: {
1039
1045
  padding: "8px 12px",
1040
1046
  borderRadius: 12,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.113",
3
+ "version": "0.1.115",
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",
@@ -79,7 +79,7 @@
79
79
  },
80
80
  "dependencies": {
81
81
  "@decentnetwork/dora": "^0.1.11",
82
- "@decentnetwork/peer": "^0.1.49",
82
+ "@decentnetwork/peer": "^0.1.51",
83
83
  "ink": "^5.2.1",
84
84
  "js-yaml": "^4.1.0",
85
85
  "react": "^18.3.1",