@decentnetwork/lan 0.1.146 → 0.1.147

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
@@ -64,6 +64,8 @@ export declare class PeerManager extends EventEmitter {
64
64
  sendFile(userid: string, data: Uint8Array, name: string): string | null;
65
65
  /** Accept an incoming file offer. */
66
66
  acceptFile(userid: string, fileNumber: number): void;
67
+ /** Cancel an in-flight send by content fileId. Returns true if it was found. */
68
+ cancelSend(userid: string, fileId: string): boolean;
67
69
  /**
68
70
  * Send an outbound friend request to a Carrier address (NOT a bare
69
71
  * userid — sendFriendRequest needs the address form because it
@@ -125,6 +125,10 @@ export class PeerManager extends EventEmitter {
125
125
  acceptFile(userid, fileNumber) {
126
126
  this.peer?.acceptFile(userid, fileNumber);
127
127
  }
128
+ /** Cancel an in-flight send by content fileId. Returns true if it was found. */
129
+ cancelSend(userid, fileId) {
130
+ return this.peer?.cancelFileById(userid, fileId, true) ?? false;
131
+ }
128
132
  /**
129
133
  * Send an outbound friend request to a Carrier address (NOT a bare
130
134
  * userid — sendFriendRequest needs the address form because it
@@ -78,6 +78,8 @@ export interface IpcHandlers {
78
78
  fileSend: (userid: string, path: string) => Promise<Record<string, unknown>>;
79
79
  /** Delete file/chat messages by id (and their on-disk files). */
80
80
  fileDelete: (peer: string, ids: string[]) => Promise<Record<string, unknown>>;
81
+ /** Cancel in-flight sends by chat message id. */
82
+ fileCancel: (peer: string, ids: string[]) => Promise<Record<string, unknown>>;
81
83
  /** Mark a conversation read up to `ts` (defaults to now) — clears unread. */
82
84
  chatMarkRead: (userid: string, ts?: number) => Promise<void>;
83
85
  /** Re-read proxy allowlist from config and apply it to the running
@@ -102,7 +104,7 @@ export interface IpcHandlers {
102
104
  selfRestart: () => Promise<Record<string, unknown>>;
103
105
  }
104
106
  export interface IpcRequest {
105
- 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" | "chat-mark-read" | "subscribe" | "sign" | "proxy-reload" | "proxy-access" | "self-restart";
107
+ 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" | "proxy-reload" | "proxy-access" | "self-restart";
106
108
  address?: string;
107
109
  hello?: string;
108
110
  userid?: string;
@@ -225,6 +225,11 @@ export class IpcServer {
225
225
  throw new Error("userid is required");
226
226
  return await this.handlers.fileDelete(req.userid, req.ids ?? []);
227
227
  }
228
+ case "file-cancel": {
229
+ if (!req.userid)
230
+ throw new Error("userid is required");
231
+ return await this.handlers.fileCancel(req.userid, req.ids ?? []);
232
+ }
228
233
  case "chat-mark-read": {
229
234
  if (!req.userid)
230
235
  throw new Error("userid is required");
@@ -493,6 +493,22 @@ export class DaemonServer {
493
493
  this.logger.info(`Deleted ${removed.length} message(s), ${files} file(s) for ${userid.slice(0, 8)}`);
494
494
  return { deleted: removed.length, files };
495
495
  },
496
+ fileCancel: async (userid, ids) => {
497
+ let n = 0;
498
+ for (const id of ids) {
499
+ for (const [fid, v] of this.activeSends) {
500
+ if (v.peer !== userid || v.msgId !== id)
501
+ continue;
502
+ if (this.peerManager?.cancelSend(userid, fid))
503
+ n++;
504
+ this.activeSends.delete(fid);
505
+ this.messageStore?.patchFile(userid, id, { status: "failed", sent: 0, kbps: undefined });
506
+ }
507
+ }
508
+ this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
509
+ this.logger.info(`Cancelled ${n} send(s) for ${userid.slice(0, 8)}`);
510
+ return { cancelled: n };
511
+ },
496
512
  chatMarkRead: async (userid, ts) => {
497
513
  this.friendMeta?.markRead(userid, ts);
498
514
  },
@@ -216,6 +216,7 @@ const dkApi = {
216
216
  alias: (userid, alias) => dkPost("/api/friend-alias", { userid, alias }),
217
217
  markRead: (userid) => dkPost("/api/chat-mark-read", { userid }),
218
218
  delFiles: (userid, ids) => dkPost("/api/file-delete", { userid, ids }),
219
+ cancelSend: (userid, ids) => dkPost("/api/file-cancel", { userid, ids }),
219
220
  setProfile: (name, description) => dkPost("/api/set-profile", { name, description }),
220
221
  sendFile: async (userid, file) => {
221
222
  try {
@@ -1054,7 +1055,7 @@ const mediaBtnStyle = {
1054
1055
  justifyContent: "center",
1055
1056
  lineHeight: 0
1056
1057
  };
1057
- function Msg({ m, peer, T, onTheater, onDelete, selMode, selected, onToggleSel }) {
1058
+ function Msg({ m, peer, T, onTheater, onDelete, onCancel, selMode, selected, onToggleSel }) {
1058
1059
  const mine = m.from === "me";
1059
1060
  return /* @__PURE__ */ React.createElement(
1060
1061
  "div",
@@ -1153,7 +1154,18 @@ function Msg({ m, peer, T, onTheater, onDelete, selMode, selected, onToggleSel }
1153
1154
  const icon = /* @__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.media === "image" ? "image" : m.file.media === "video" ? "video" : m.file.media === "audio" ? "play" : "file", size: 18, stroke: 1.9 }));
1154
1155
  const meta = /* @__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 === "queued" ? `${m.file.size} \xB7 ${T.queued || "queued"}` : m.file.status === "sending" ? `${m.file.size} \xB7 ${dkPct(m.file.pct)}${m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : ""}` : m.file.status === "failed" ? `${m.file.size} \xB7 failed` : mine ? `${m.file.size} \xB7 sent` : `${m.file.size} \xB7 ${T.open || "open"}`));
1155
1156
  if (mine) {
1156
- return /* @__PURE__ */ React.createElement("div", { style: cardStyle }, icon, meta, m.file.status === "queued" ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : m.file.status === "sending" ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "rgba(255,255,255,0.9)", whiteSpace: "nowrap" } }, dkPct(m.file.pct), m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : "") : 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)" }));
1157
+ return /* @__PURE__ */ React.createElement("div", { style: cardStyle }, icon, meta, m.file.status === "queued" ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : m.file.status === "sending" ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "rgba(255,255,255,0.9)", whiteSpace: "nowrap" } }, dkPct(m.file.pct), m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : "") : 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)" }), (m.file.status === "sending" || m.file.status === "queued") && m.id && /* @__PURE__ */ React.createElement(
1158
+ "button",
1159
+ {
1160
+ title: T.cancel || "cancel",
1161
+ onClick: (e) => {
1162
+ e.stopPropagation();
1163
+ onCancel(m.id);
1164
+ },
1165
+ style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0 }
1166
+ },
1167
+ /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 13, stroke: 2.4, color: "#fff" })
1168
+ ));
1157
1169
  }
1158
1170
  return /* @__PURE__ */ React.createElement(
1159
1171
  "div",
@@ -1225,6 +1237,10 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
1225
1237
  n.has(id) ? n.delete(id) : n.add(id);
1226
1238
  return n;
1227
1239
  });
1240
+ const doCancel = (id) => {
1241
+ if (id) dkApi.cancelSend(peer.userId, [id]).catch(() => {
1242
+ });
1243
+ };
1228
1244
  const doDelete = (ids) => {
1229
1245
  ids = ids.filter(Boolean);
1230
1246
  if (!ids.length) return;
@@ -1309,6 +1325,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
1309
1325
  T,
1310
1326
  onTheater: setTheater,
1311
1327
  onDelete: (id) => doDelete([id]),
1328
+ onCancel: doCancel,
1312
1329
  selMode,
1313
1330
  selected: sel.has(m.id),
1314
1331
  onToggleSel: toggleSel
package/dist/ui/server.js CHANGED
@@ -271,6 +271,19 @@ export function startFriendUi(opts) {
271
271
  sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, ...(r.data ?? {}) } : { ok: false, error: r.error });
272
272
  return;
273
273
  }
274
+ // Cancel in-flight sends by chat message id.
275
+ if (req.method === "POST" && url === "/api/file-cancel") {
276
+ const body = await readBody(req);
277
+ const userid = typeof body.userid === "string" ? body.userid : "";
278
+ const ids = Array.isArray(body.ids) ? body.ids.filter((x) => typeof x === "string") : [];
279
+ if (!userid || !ids.length) {
280
+ sendJson(res, 400, { ok: false, error: "userid and ids required" });
281
+ return;
282
+ }
283
+ const r = await opts.call({ op: "file-cancel", userid, ids });
284
+ sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, ...(r.data ?? {}) } : { ok: false, error: r.error });
285
+ return;
286
+ }
274
287
  // ── File transfer ──────────────────────────────────────────────────
275
288
  // Upload: the browser POSTs raw file bytes with ?userid=&name= in the
276
289
  // 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.146",
3
+ "version": "0.1.147",
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.78",
82
+ "@decentnetwork/peer": "^0.1.79",
83
83
  "ink": "^5.2.1",
84
84
  "js-yaml": "^4.1.0",
85
85
  "node-forge": "^1.4.0",