@decentnetwork/lan 0.1.209 → 0.1.210
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/dist/daemon/ipc.d.ts +3 -1
- package/dist/daemon/ipc.js +5 -0
- package/dist/daemon/message-store.d.ts +2 -0
- package/dist/daemon/message-store.js +4 -0
- package/dist/daemon/server.js +53 -6
- package/dist/ui/desktop/app.js +24 -1
- package/dist/ui/server.js +13 -0
- package/package.json +2 -2
package/dist/daemon/ipc.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export interface IpcHandlers {
|
|
|
80
80
|
fileDelete: (peer: string, ids: string[]) => Promise<Record<string, unknown>>;
|
|
81
81
|
/** Cancel in-flight sends by chat message id. */
|
|
82
82
|
fileCancel: (peer: string, ids: string[]) => Promise<Record<string, unknown>>;
|
|
83
|
+
/** Restart incomplete sends from their retained outbox bytes. */
|
|
84
|
+
fileRetry: (peer: string, ids: string[]) => Promise<Record<string, unknown>>;
|
|
83
85
|
/** Mark a conversation read up to `ts` (defaults to now) — clears unread. */
|
|
84
86
|
chatMarkRead: (userid: string, ts?: number) => Promise<void>;
|
|
85
87
|
/** Send a WebRTC call-signaling payload (RtcSignal JSON) to a friend over the
|
|
@@ -113,7 +115,7 @@ export interface IpcHandlers {
|
|
|
113
115
|
selfRestart: () => Promise<Record<string, unknown>>;
|
|
114
116
|
}
|
|
115
117
|
export interface IpcRequest {
|
|
116
|
-
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "file-delete" | "file-cancel" | "chat-mark-read" | "subscribe" | "sign" | "call-signal" | "call-poll" | "proxy-reload" | "proxy-access" | "self-restart";
|
|
118
|
+
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "file-delete" | "file-cancel" | "file-retry" | "chat-mark-read" | "subscribe" | "sign" | "call-signal" | "call-poll" | "proxy-reload" | "proxy-access" | "self-restart";
|
|
117
119
|
address?: string;
|
|
118
120
|
hello?: string;
|
|
119
121
|
userid?: string;
|
package/dist/daemon/ipc.js
CHANGED
|
@@ -238,6 +238,11 @@ export class IpcServer {
|
|
|
238
238
|
throw new Error("userid is required");
|
|
239
239
|
return await this.handlers.fileCancel(req.userid, req.ids ?? []);
|
|
240
240
|
}
|
|
241
|
+
case "file-retry": {
|
|
242
|
+
if (!req.userid)
|
|
243
|
+
throw new Error("userid is required");
|
|
244
|
+
return await this.handlers.fileRetry(req.userid, req.ids ?? []);
|
|
245
|
+
}
|
|
241
246
|
case "chat-mark-read": {
|
|
242
247
|
if (!req.userid)
|
|
243
248
|
throw new Error("userid is required");
|
|
@@ -77,6 +77,8 @@ export declare class MessageStore {
|
|
|
77
77
|
avgKbps?: number;
|
|
78
78
|
kbps?: number;
|
|
79
79
|
}): boolean;
|
|
80
|
+
/** Look up one persisted message by its stable UI id. */
|
|
81
|
+
get(peer: string, id: string): ChatMessage | undefined;
|
|
80
82
|
/** Remove messages by id. Returns the removed ones so the caller can clean up
|
|
81
83
|
* any on-disk file the chip pointed at. */
|
|
82
84
|
deleteMessages(peer: string, ids: string[]): ChatMessage[];
|
|
@@ -91,6 +91,10 @@ export class MessageStore {
|
|
|
91
91
|
this.scheduleSave();
|
|
92
92
|
return true;
|
|
93
93
|
}
|
|
94
|
+
/** Look up one persisted message by its stable UI id. */
|
|
95
|
+
get(peer, id) {
|
|
96
|
+
return this.byPeer.get(peer)?.find((m) => m.id === id);
|
|
97
|
+
}
|
|
94
98
|
/** Remove messages by id. Returns the removed ones so the caller can clean up
|
|
95
99
|
* any on-disk file the chip pointed at. */
|
|
96
100
|
deleteMessages(peer, ids) {
|
package/dist/daemon/server.js
CHANGED
|
@@ -497,15 +497,22 @@ export class DaemonServer {
|
|
|
497
497
|
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
498
498
|
return { inline: true, name, size: data.length };
|
|
499
499
|
}
|
|
500
|
-
const fileId = this.peerManager?.sendFile(userid, new Uint8Array(data), name);
|
|
501
|
-
if (!fileId)
|
|
502
|
-
throw new Error("No free transfer slot (or friend unknown)");
|
|
503
|
-
this.logger.info(`Offering file "${name}" (${data.length}B) to ${userid.slice(0, 8)}`);
|
|
504
500
|
// Add the "out" chip immediately as STATUS=sending (not "sent" — the
|
|
505
501
|
// transfer is reliable+acked, so it only flips to "sent" once the
|
|
506
502
|
// receiver confirms every byte). Progress/complete/cancel events patch
|
|
507
|
-
// it.
|
|
503
|
+
// it. Retain the bytes until delivery so Cancel/Retry and content-hash
|
|
504
|
+
// resume work even though the UI upload temp file is removed.
|
|
508
505
|
const msg = this.messageStore?.appendFile(userid, "out", { name: sanitizeFileName(name), size: data.length, status: "sending", sent: 0 });
|
|
506
|
+
if (!msg)
|
|
507
|
+
throw new Error("Could not create file message");
|
|
508
|
+
await fs.mkdir(this.outboxDir, { recursive: true });
|
|
509
|
+
await fs.writeFile(resolve(this.outboxDir, msg.id), data);
|
|
510
|
+
const fileId = this.peerManager?.sendFile(userid, new Uint8Array(data), name);
|
|
511
|
+
if (!fileId) {
|
|
512
|
+
this.messageStore?.patchFile(userid, msg.id, { status: "failed" });
|
|
513
|
+
throw new Error("No free transfer slot (or friend unknown)");
|
|
514
|
+
}
|
|
515
|
+
this.logger.info(`Offering file "${name}" (${data.length}B) to ${userid.slice(0, 8)}`);
|
|
509
516
|
if (msg)
|
|
510
517
|
this.activeSends.set(fileId, { peer: userid, msgId: msg.id });
|
|
511
518
|
this.friendMeta?.ensure(userid);
|
|
@@ -551,6 +558,44 @@ export class DaemonServer {
|
|
|
551
558
|
this.logger.info(`Cancelled ${n} send(s) for ${userid.slice(0, 8)}`);
|
|
552
559
|
return { cancelled: n };
|
|
553
560
|
},
|
|
561
|
+
fileRetry: async (userid, ids) => {
|
|
562
|
+
const fs = await import("fs/promises");
|
|
563
|
+
if (!this.peerManager?.isFriendOnline(userid)) {
|
|
564
|
+
throw new Error("Peer is not online yet");
|
|
565
|
+
}
|
|
566
|
+
let retried = 0;
|
|
567
|
+
for (const id of ids) {
|
|
568
|
+
const msg = this.messageStore?.get(userid, id);
|
|
569
|
+
if (!msg?.file || msg.dir !== "out" || msg.file.status === "sent")
|
|
570
|
+
continue;
|
|
571
|
+
const bytesPath = resolve(this.outboxDir, id);
|
|
572
|
+
let data;
|
|
573
|
+
try {
|
|
574
|
+
data = await fs.readFile(bytesPath);
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
throw new Error(`Original bytes are no longer available for ${msg.file.name}`);
|
|
578
|
+
}
|
|
579
|
+
// Retry also acts as an immediate restart for a stuck 0%/partial
|
|
580
|
+
// transfer. Stop the old file-number first; the receiver retains a
|
|
581
|
+
// content-hash partial and answers the new offer with its resume ACK.
|
|
582
|
+
for (const [fid, active] of [...this.activeSends]) {
|
|
583
|
+
if (active.peer !== userid || active.msgId !== id)
|
|
584
|
+
continue;
|
|
585
|
+
this.peerManager.cancelSend(userid, fid);
|
|
586
|
+
this.activeSends.delete(fid);
|
|
587
|
+
}
|
|
588
|
+
const fileId = this.peerManager.sendFile(userid, new Uint8Array(data), msg.file.name);
|
|
589
|
+
if (!fileId)
|
|
590
|
+
throw new Error(`No free transfer slot for ${msg.file.name}`);
|
|
591
|
+
this.messageStore?.patchFile(userid, id, { status: "sending", sent: 0, kbps: undefined });
|
|
592
|
+
this.activeSends.set(fileId, { peer: userid, msgId: id });
|
|
593
|
+
retried++;
|
|
594
|
+
this.logger.info(`Retrying file "${msg.file.name}" (${data.length}B) to ${userid.slice(0, 8)}`);
|
|
595
|
+
}
|
|
596
|
+
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
597
|
+
return { retried };
|
|
598
|
+
},
|
|
554
599
|
chatMarkRead: async (userid, ts) => {
|
|
555
600
|
this.friendMeta?.markRead(userid, ts);
|
|
556
601
|
},
|
|
@@ -962,7 +1007,9 @@ export class DaemonServer {
|
|
|
962
1007
|
// If this was a queued (offline) send and its bytes are still in the
|
|
963
1008
|
// outbox, revert the chip to "queued" so it retries on the next
|
|
964
1009
|
// reconnect instead of dying as "failed". Otherwise mark failed.
|
|
965
|
-
const requeue =
|
|
1010
|
+
const requeue = p.reason !== "no-response" &&
|
|
1011
|
+
!this.peerManager?.isFriendOnline(t.peer) &&
|
|
1012
|
+
!!this.outboxDir && existsSync(resolve(this.outboxDir, t.msgId));
|
|
966
1013
|
this.messageStore?.patchFile(t.peer, t.msgId, { status: requeue ? "queued" : "failed", sent: 0 });
|
|
967
1014
|
this.activeSends.delete(p.fileId);
|
|
968
1015
|
this.ipcEvents.emit("event", { type: "chat", userid: t.peer, dir: "out" });
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -227,6 +227,7 @@ const dkApi = {
|
|
|
227
227
|
markRead: (userid) => dkPost("/api/chat-mark-read", { userid }),
|
|
228
228
|
delFiles: (userid, ids) => dkPost("/api/file-delete", { userid, ids }),
|
|
229
229
|
cancelSend: (userid, ids) => dkPost("/api/file-cancel", { userid, ids }),
|
|
230
|
+
retrySend: (userid, ids) => dkPost("/api/file-retry", { userid, ids }),
|
|
230
231
|
setProfile: (name, description) => dkPost("/api/set-profile", { name, description }),
|
|
231
232
|
sendFile: async (userid, file) => {
|
|
232
233
|
try {
|
|
@@ -1071,7 +1072,7 @@ const mediaBtnStyle = {
|
|
|
1071
1072
|
justifyContent: "center",
|
|
1072
1073
|
lineHeight: 0
|
|
1073
1074
|
};
|
|
1074
|
-
function Msg({ m, peer, T, onTheater, onDelete, onCancel, selMode, selected, onToggleSel }) {
|
|
1075
|
+
function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, selected, onToggleSel }) {
|
|
1075
1076
|
const mine = m.from === "me";
|
|
1076
1077
|
return /* @__PURE__ */ React.createElement(
|
|
1077
1078
|
"div",
|
|
@@ -1181,6 +1182,17 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, selMode, selected, onT
|
|
|
1181
1182
|
style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0 }
|
|
1182
1183
|
},
|
|
1183
1184
|
/* @__PURE__ */ React.createElement(Icon, { name: "x", size: 13, stroke: 2.4, color: "#fff" })
|
|
1185
|
+
), m.file.status !== "sent" && m.id && /* @__PURE__ */ React.createElement(
|
|
1186
|
+
"button",
|
|
1187
|
+
{
|
|
1188
|
+
title: T.retry || "retry",
|
|
1189
|
+
onClick: (e) => {
|
|
1190
|
+
e.stopPropagation();
|
|
1191
|
+
onRetry(m.id);
|
|
1192
|
+
},
|
|
1193
|
+
style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0 }
|
|
1194
|
+
},
|
|
1195
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "refresh", size: 13, stroke: 2.2, color: "#fff" })
|
|
1184
1196
|
));
|
|
1185
1197
|
}
|
|
1186
1198
|
return /* @__PURE__ */ React.createElement(
|
|
@@ -1267,6 +1279,12 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1267
1279
|
if (id) dkApi.cancelSend(peer.userId, [id]).catch(() => {
|
|
1268
1280
|
});
|
|
1269
1281
|
};
|
|
1282
|
+
const doRetry = (id) => {
|
|
1283
|
+
if (!id) return;
|
|
1284
|
+
dkApi.retrySend(peer.userId, [id]).then((r) => {
|
|
1285
|
+
if (r && r.ok === false) window.alert((lang === "zh" ? "\u91CD\u8BD5\u5931\u8D25: " : "Retry failed: ") + (r.error || ""));
|
|
1286
|
+
});
|
|
1287
|
+
};
|
|
1270
1288
|
const doDelete = (ids) => {
|
|
1271
1289
|
ids = ids.filter(Boolean);
|
|
1272
1290
|
if (!ids.length) return;
|
|
@@ -1352,6 +1370,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1352
1370
|
onTheater: setTheater,
|
|
1353
1371
|
onDelete: (id) => doDelete([id]),
|
|
1354
1372
|
onCancel: doCancel,
|
|
1373
|
+
onRetry: doRetry,
|
|
1355
1374
|
selMode,
|
|
1356
1375
|
selected: sel.has(m.id),
|
|
1357
1376
|
onToggleSel: toggleSel
|
|
@@ -1956,6 +1975,8 @@ const STR = {
|
|
|
1956
1975
|
pickPeer: "Select a peer to open the conversation",
|
|
1957
1976
|
queued: "queued",
|
|
1958
1977
|
open: "open",
|
|
1978
|
+
retry: "Retry",
|
|
1979
|
+
cancel: "Cancel",
|
|
1959
1980
|
myIp: "my ip",
|
|
1960
1981
|
copyAddr: "copy address",
|
|
1961
1982
|
peersOnline: "peers online",
|
|
@@ -2036,6 +2057,8 @@ const STR = {
|
|
|
2036
2057
|
pickPeer: "\u9009\u62E9\u4E00\u4F4D\u597D\u53CB\u5F00\u59CB\u4F1A\u8BDD",
|
|
2037
2058
|
queued: "\u5F85\u53D1\u9001",
|
|
2038
2059
|
open: "\u6253\u5F00",
|
|
2060
|
+
retry: "\u91CD\u8BD5",
|
|
2061
|
+
cancel: "\u53D6\u6D88",
|
|
2039
2062
|
myIp: "\u6211\u7684 IP",
|
|
2040
2063
|
copyAddr: "\u590D\u5236\u5730\u5740",
|
|
2041
2064
|
peersOnline: "\u5728\u7EBF\u597D\u53CB",
|
package/dist/ui/server.js
CHANGED
|
@@ -292,6 +292,19 @@ export function startFriendUi(opts) {
|
|
|
292
292
|
sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, ...(r.data ?? {}) } : { ok: false, error: r.error });
|
|
293
293
|
return;
|
|
294
294
|
}
|
|
295
|
+
// Retry/resume incomplete sends from the daemon's retained outbox copy.
|
|
296
|
+
if (req.method === "POST" && url === "/api/file-retry") {
|
|
297
|
+
const body = await readBody(req);
|
|
298
|
+
const userid = typeof body.userid === "string" ? body.userid : "";
|
|
299
|
+
const ids = Array.isArray(body.ids) ? body.ids.filter((x) => typeof x === "string") : [];
|
|
300
|
+
if (!userid || !ids.length) {
|
|
301
|
+
sendJson(res, 400, { ok: false, error: "userid and ids required" });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const r = await opts.call({ op: "file-retry", userid, ids });
|
|
305
|
+
sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, ...(r.data ?? {}) } : { ok: false, error: r.error });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
295
308
|
// ── File transfer ──────────────────────────────────────────────────
|
|
296
309
|
// Upload: the browser POSTs raw file bytes with ?userid=&name= in the
|
|
297
310
|
// query. We stream them to a temp file (no in-memory buffering of large
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.210",
|
|
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",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@decentnetwork/dora": "^0.1.14",
|
|
87
|
-
"@decentnetwork/peer": "^0.1.
|
|
87
|
+
"@decentnetwork/peer": "^0.1.104",
|
|
88
88
|
"@decentnetwork/peer-webrtc": "^0.2.10",
|
|
89
89
|
"ink": "^5.2.1",
|
|
90
90
|
"js-yaml": "^4.1.0",
|