@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.
- package/bin/tun-helper-darwin-amd64 +0 -0
- package/bin/tun-helper-darwin-arm64 +0 -0
- package/bin/tun-helper-linux-amd64 +0 -0
- package/bin/tun-helper-linux-arm64 +0 -0
- package/dist/console/console.js +6 -1
- package/dist/daemon/message-store.d.ts +14 -2
- package/dist/daemon/message-store.js +12 -1
- package/dist/daemon/server.d.ts +3 -0
- package/dist/daemon/server.js +42 -4
- package/dist/ui/desktop/app.js +9 -3
- package/package.json +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/console/console.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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) {
|
package/dist/daemon/server.d.ts
CHANGED
|
@@ -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?;
|
package/dist/daemon/server.js
CHANGED
|
@@ -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
|
-
//
|
|
329
|
-
|
|
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
|
-
|
|
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 () => {
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -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 ? {
|
|
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
|
|
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.
|
|
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.
|
|
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",
|