@decentnetwork/lan 0.1.222 → 0.1.224

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.
@@ -1547,9 +1547,11 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
1547
1547
  if (!files || !files.length || !sender) return;
1548
1548
  const file = files[0];
1549
1549
  followScrollRef.current = true;
1550
- setSending({ name: file.name, via: mode });
1551
- Promise.resolve(sender(file)).then((r) => {
1550
+ setSending({ name: file.name, via: mode, pct: 0 });
1551
+ const onProgress = (p) => setSending((s) => s && s.name === file.name ? { ...s, pct: p && p.pct } : s);
1552
+ Promise.resolve(sender(file, onProgress)).then((r) => {
1552
1553
  if (r && r.ok === false) window.alert((lang === "zh" ? "\u53D1\u9001\u5931\u8D25: " : "Send failed: ") + (r.error || ""));
1554
+ else showFlash("ok", mode === "webrtc" ? lang === "zh" ? "WebRTC \u6587\u4EF6\u53D1\u9001\u5B8C\u6210" : "WebRTC file sent" : lang === "zh" ? "\u6587\u4EF6\u5DF2\u63D0\u4EA4\u53D1\u9001" : "File send started");
1553
1555
  }).finally(() => {
1554
1556
  setSending(null);
1555
1557
  requestAnimationFrame(scrollToBottom);
@@ -1633,7 +1635,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
1633
1635
  stroke: 2.2,
1634
1636
  color: flash.kind === "err" ? "var(--danger)" : "var(--accent)"
1635
1637
  }
1636
- ), flash.msg), sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto 8px", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "paperclip", size: 13, stroke: 2, color: "var(--accent)" }), (lang === "zh" ? "\u53D1\u9001\u4E2D: " : "Sending: ") + (sending.via === "webrtc" ? "WebRTC \xB7 " : "") + sending.name), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
1638
+ ), flash.msg), sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto 8px", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "paperclip", size: 13, stroke: 2, color: "var(--accent)" }), (lang === "zh" ? "\u53D1\u9001\u4E2D: " : "Sending: ") + (sending.via === "webrtc" ? "WebRTC \xB7 " : "") + sending.name + (sending.pct != null ? " \xB7 " + Math.min(100, Math.max(0, sending.pct)).toFixed(1) + "%" : "")), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
1637
1639
  "input",
1638
1640
  {
1639
1641
  ref: fileRef,
@@ -1897,15 +1899,43 @@ function dkWaitForOpen(dc, timeoutMs, describeState) {
1897
1899
  }
1898
1900
  function dkWaitBufferedLow(dc, highWater) {
1899
1901
  if (dc.bufferedAmount < highWater) return Promise.resolve();
1900
- return new Promise((resolve) => {
1902
+ return new Promise((resolve, reject) => {
1901
1903
  const prev = dc.bufferedAmountLowThreshold;
1902
1904
  dc.bufferedAmountLowThreshold = Math.floor(highWater / 2);
1905
+ let poll = null;
1906
+ let timer = null;
1903
1907
  const done = () => {
1908
+ cleanup();
1909
+ resolve();
1910
+ };
1911
+ const cleanup = () => {
1912
+ if (poll) clearInterval(poll);
1913
+ if (timer) clearTimeout(timer);
1904
1914
  dc.removeEventListener("bufferedamountlow", done);
1915
+ dc.removeEventListener("close", onClose);
1916
+ dc.removeEventListener("error", onError);
1905
1917
  dc.bufferedAmountLowThreshold = prev;
1906
- resolve();
1918
+ };
1919
+ const check = () => {
1920
+ if (dc.readyState !== "open") return onClose();
1921
+ if (dc.bufferedAmount < highWater) done();
1922
+ };
1923
+ const onClose = () => {
1924
+ cleanup();
1925
+ reject(new Error("WebRTC data channel closed while draining"));
1926
+ };
1927
+ const onError = () => {
1928
+ cleanup();
1929
+ reject(new Error("WebRTC data channel failed while draining"));
1907
1930
  };
1908
1931
  dc.addEventListener("bufferedamountlow", done);
1932
+ dc.addEventListener("close", onClose);
1933
+ dc.addEventListener("error", onError);
1934
+ poll = setInterval(check, 250);
1935
+ timer = setTimeout(() => {
1936
+ cleanup();
1937
+ reject(new Error("WebRTC data channel drain timed out buffered=" + dc.bufferedAmount));
1938
+ }, 3e4);
1909
1939
  });
1910
1940
  }
1911
1941
  function dkFileDownload(blob, name) {
@@ -1978,7 +2008,10 @@ function useRtcFileController(selfId, onReceivedText) {
1978
2008
  let sess = peersRef.current.get(fileId);
1979
2009
  try {
1980
2010
  if (env.type === "offer") {
1981
- if (sess) cleanup(fileId);
2011
+ if (sess && sess.pc && sess.pc.signalingState !== "closed") {
2012
+ console.warn("[file-rtc] duplicate offer ignored for " + fileId);
2013
+ return;
2014
+ }
1982
2015
  const pc = new RTCPeerConnection({ iceServers: CALL_ICE_SERVERS });
1983
2016
  pc.oniceconnectionstatechange = () => console.warn("[file-rtc] receiver ice=" + pc.iceConnectionState + " conn=" + pc.connectionState);
1984
2017
  pc.onconnectionstatechange = () => console.warn("[file-rtc] receiver conn=" + pc.connectionState + " ice=" + pc.iceConnectionState);
@@ -2002,6 +2035,10 @@ function useRtcFileController(selfId, onReceivedText) {
2002
2035
  await pc.setLocalDescription(answer);
2003
2036
  await sendSignal(peerId, fileId, "answer", { sdp: answer.sdp });
2004
2037
  } else if (env.type === "answer" && sess) {
2038
+ if (sess.pc.signalingState !== "have-local-offer") {
2039
+ console.warn("[file-rtc] stale answer ignored for " + fileId + " state=" + sess.pc.signalingState);
2040
+ return;
2041
+ }
2005
2042
  await sess.pc.setRemoteDescription({ type: "answer", sdp: env.sdp });
2006
2043
  if (sess.pendingCandidates) {
2007
2044
  for (const c of sess.pendingCandidates.splice(0)) {
@@ -2106,10 +2143,12 @@ function useRtcFileController(selfId, onReceivedText) {
2106
2143
  function RtcFileInbox({ T, peers, ctl }) {
2107
2144
  const items = ctl && ctl.incoming || [];
2108
2145
  if (!items.length) return null;
2109
- return /* @__PURE__ */ React.createElement("div", { style: { position: "fixed", right: 18, bottom: 18, zIndex: 90, width: 360, display: "flex", flexDirection: "column", gap: 10 } }, items.map((it) => {
2146
+ const closeLabel = T && T.receivedClose || "Close";
2147
+ const downloadLabel = T && T.downloadFile || "Download";
2148
+ return /* @__PURE__ */ React.createElement("div", { style: { position: "fixed", right: 72, bottom: 18, zIndex: 90, width: 360, maxWidth: "calc(100vw - 96px)", display: "flex", flexDirection: "column", gap: 10 } }, items.map((it) => {
2110
2149
  const peer = (peers || []).find((p) => p.id === it.peerId || p.userId === it.peerId) || {};
2111
2150
  const who = peer.alias || peer.name || shortKey(it.peerId, 8, 5);
2112
- return /* @__PURE__ */ React.createElement("div", { key: it.id, style: { border: "1px solid var(--line)", background: "var(--panel)", borderRadius: 12, padding: 12, boxShadow: "0 14px 40px rgba(0,0,0,0.35)" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)", marginBottom: 6 } }, T && T.receivedFile || "Received file", " \xB7 ", who), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 13, fontWeight: 700, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, it.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", marginTop: 3 } }, dkFileSize(it.size)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 10 } }, /* @__PURE__ */ React.createElement("button", { onClick: () => ctl.dismissIncoming(it.id), style: { height: 30, borderRadius: 8, border: "1px solid var(--line)", background: "transparent", color: "var(--dim)", cursor: "pointer" } }, T && T.cancel || "Close"), /* @__PURE__ */ React.createElement("a", { href: it.url, download: it.name, onClick: () => setTimeout(() => ctl.dismissIncoming(it.id), 500), style: { height: 30, borderRadius: 8, padding: "6px 10px", background: "var(--accent)", color: "#fff", textDecoration: "none", fontFamily: "var(--mono)", fontSize: 12 } }, T && T.open || "Download")));
2151
+ return /* @__PURE__ */ React.createElement("div", { key: it.id, style: { border: "1px solid var(--line)", background: "var(--panel)", borderRadius: 12, padding: 12, boxShadow: "0 14px 40px rgba(0,0,0,0.35)" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)", marginBottom: 6 } }, T && T.receivedFile || "Received file", " \xB7 ", who), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 13, fontWeight: 700, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, it.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", marginTop: 3 } }, dkFileSize(it.size)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 10, paddingRight: 2 } }, /* @__PURE__ */ React.createElement("button", { onClick: () => ctl.dismissIncoming(it.id), style: { height: 30, minWidth: 58, borderRadius: 8, border: "1px solid var(--line)", background: "transparent", color: "var(--dim)", cursor: "pointer" } }, closeLabel), /* @__PURE__ */ React.createElement("a", { href: it.url, download: it.name, onClick: () => setTimeout(() => ctl.dismissIncoming(it.id), 500), style: { height: 30, minWidth: 76, textAlign: "center", borderRadius: 8, padding: "6px 10px", background: "var(--accent)", color: "#fff", textDecoration: "none", fontFamily: "var(--mono)", fontSize: 12 } }, downloadLabel)));
2113
2152
  }));
2114
2153
  }
2115
2154
  Object.assign(window, { useRtcFileController, RtcFileInbox });
@@ -2444,6 +2483,8 @@ const STR = {
2444
2483
  block: "Block peer",
2445
2484
  remove: "Remove friend",
2446
2485
  receivedFile: "Received file",
2486
+ receivedClose: "Close",
2487
+ downloadFile: "Download",
2447
2488
  attach: "Attach file",
2448
2489
  message: "Message",
2449
2490
  send: "Send",
@@ -2536,6 +2577,8 @@ const STR = {
2536
2577
  block: "\u62C9\u9ED1",
2537
2578
  remove: "\u5220\u9664\u597D\u53CB",
2538
2579
  receivedFile: "\u6536\u5230\u6587\u4EF6",
2580
+ receivedClose: "\u5173\u95ED",
2581
+ downloadFile: "\u4E0B\u8F7D",
2539
2582
  attach: "\u9644\u4EF6",
2540
2583
  message: "\u6D88\u606F",
2541
2584
  send: "\u53D1\u9001",
@@ -2675,9 +2718,9 @@ function DkApp() {
2675
2718
  if (!activeId || !text || !text.trim()) return;
2676
2719
  dkApi.send(activeId, text.trim()).then(() => data.loadThread(activeId)).then(data.refresh);
2677
2720
  };
2678
- const onSendFile = (file) => {
2721
+ const onSendFile = (file, onProgress) => {
2679
2722
  if (!activeId || !file) return Promise.resolve({ ok: false });
2680
- return rtcFileCtl.sendFile(activeId, file).then((r) => {
2723
+ return rtcFileCtl.sendFile(activeId, file, onProgress).then((r) => {
2681
2724
  if (r && r.ok && r.via === "webrtc") {
2682
2725
  return dkApi.send(activeId, (t.lang === "zh" ? "\u5DF2\u901A\u8FC7 WebRTC \u53D1\u9001\u6587\u4EF6: " : "sent file via WebRTC: ") + file.name).then(() => ({ ok: true, via: "webrtc" }));
2683
2726
  }
@@ -2688,9 +2731,9 @@ function DkApp() {
2688
2731
  return r;
2689
2732
  });
2690
2733
  };
2691
- const onSendRtcFile = (file) => {
2734
+ const onSendRtcFile = (file, onProgress) => {
2692
2735
  if (!activeId || !file) return Promise.resolve({ ok: false, error: "no active peer or file" });
2693
- return rtcFileCtl.sendFile(activeId, file).then((r) => {
2736
+ return rtcFileCtl.sendFile(activeId, file, onProgress).then((r) => {
2694
2737
  if (r && r.ok && r.via === "webrtc") {
2695
2738
  return dkApi.send(activeId, (t.lang === "zh" ? "\u5DF2\u901A\u8FC7 WebRTC \u53D1\u9001\u6587\u4EF6: " : "sent file via WebRTC: ") + file.name).then(() => ({ ok: true, via: "webrtc" }));
2696
2739
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.222",
3
+ "version": "0.1.224",
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",