@decentnetwork/beagle 0.1.102 → 0.1.104
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/desktop/app.js +40 -14
- package/dist/embedded-host.js +3 -0
- package/dist/lan-handoff.d.ts +5 -0
- package/dist/lan-handoff.js +43 -0
- package/dist/message-store.d.ts +3 -0
- package/dist/message-store.js +10 -0
- package/package.json +2 -2
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.104";
|
|
2
2
|
"use strict";
|
|
3
3
|
(() => {
|
|
4
4
|
// packages/beagle-ui/dist/beagle-ui.js
|
|
@@ -118,12 +118,14 @@ window.__DK_UI_VERSION="0.1.102";
|
|
|
118
118
|
var hostPrefersRtcFile = false;
|
|
119
119
|
var hostHasKeyBackup = false;
|
|
120
120
|
var hostEnsWrite = true;
|
|
121
|
+
var hostBrandHref = "";
|
|
121
122
|
function setUiHost(host2) {
|
|
122
123
|
H = host2 || {};
|
|
123
124
|
hostNeedsWelcome = !!H.hostNeedsWelcome;
|
|
124
125
|
hostPrefersRtcFile = !!H.hostPrefersRtcFile;
|
|
125
126
|
hostHasKeyBackup = !!H.hostHasKeyBackup;
|
|
126
127
|
hostEnsWrite = H.hostEnsWrite !== void 0 ? !!H.hostEnsWrite : !H.hostNeedsWelcome;
|
|
128
|
+
hostBrandHref = typeof H.hostBrandHref === "string" ? H.hostBrandHref : "";
|
|
127
129
|
}
|
|
128
130
|
function fileUrl(name) {
|
|
129
131
|
return H.fileUrl ? H.fileUrl(name) : "/api/file-download?name=" + encodeURIComponent(name);
|
|
@@ -409,10 +411,13 @@ window.__DK_UI_VERSION="0.1.102";
|
|
|
409
411
|
// Pass the real state through. Collapsing everything that was not
|
|
410
412
|
// 'queued' to 'read' meant a message still in flight — or one that
|
|
411
413
|
// never went out at all — rendered as delivered.
|
|
412
|
-
status: m.dir === "out" ? m.status === "queued" || m.status === "sending" || m.status === "failed" ? m.status : "read" : void 0,
|
|
414
|
+
status: m.dir === "out" ? m.status === "queued" || m.status === "sending" || m.status === "failed" || m.status === "sent" ? m.status : "read" : void 0,
|
|
413
415
|
error: m.error,
|
|
416
|
+
// false = went to a live session but nobody acknowledged it (one tick).
|
|
417
|
+
// Dropped here before, so that state could never render.
|
|
418
|
+
confirmed: m.confirmed,
|
|
414
419
|
// Delivery path: "online" = live session, "offline" = express relay.
|
|
415
|
-
//
|
|
420
|
+
// Both directions carry it now; the bubble labels them the same way.
|
|
416
421
|
via: m.via,
|
|
417
422
|
// Brief 30: a validated component form, already whitelisted host-side.
|
|
418
423
|
// Passed through untouched — the renderer treats every string as text,
|
|
@@ -1566,14 +1571,14 @@ window.__DK_UI_VERSION="0.1.102";
|
|
|
1566
1571
|
letterSpacing: -0.1,
|
|
1567
1572
|
wordBreak: "break-word"
|
|
1568
1573
|
} }, /* @__PURE__ */ React.createElement(MarkdownText, { text: read.text, onNameCard })));
|
|
1569
|
-
})(), read.form && /* @__PURE__ */ React.createElement("div", { style: { marginTop: 6 } }, /* @__PURE__ */ React.createElement(DkChatForm, { form: read.form, submitted, peer, onSubmit: onFormSubmit })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time),
|
|
1570
|
-
"
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
), !selMode && m.id && /* @__PURE__ */ React.createElement(
|
|
1574
|
+
})(), read.form && /* @__PURE__ */ React.createElement("div", { style: { marginTop: 6 } }, /* @__PURE__ */ React.createElement(DkChatForm, { form: read.form, submitted, peer, onSubmit: onFormSubmit })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), (() => {
|
|
1575
|
+
const sentDone = mine && m.status && m.status !== "queued" && m.status !== "sending" && m.status !== "failed";
|
|
1576
|
+
const via = !mine ? m.via : sentDone ? m.via === "offline" ? "offline" : "online" : null;
|
|
1577
|
+
if (!via)
|
|
1578
|
+
return null;
|
|
1579
|
+
const title = mine ? via === "offline" ? T.viaOutOffline || "posted to the offline relay \u2014 they get it when they come online" : T.viaOutOnline || "sent over a live session (online)" : via === "offline" ? "delivered via express relay (offline)" : "delivered over a live session (online)";
|
|
1580
|
+
return /* @__PURE__ */ React.createElement("span", { title, style: { fontFamily: "var(--mono)", fontSize: 9, color: via === "offline" ? "#f59e0b" : "var(--faint)" } }, via === "offline" ? "\u79BB\u7EBF" : "\u5728\u7EBF");
|
|
1581
|
+
})(), !selMode && m.id && /* @__PURE__ */ React.createElement(
|
|
1577
1582
|
"button",
|
|
1578
1583
|
{
|
|
1579
1584
|
title: T.delete || "delete",
|
|
@@ -1608,7 +1613,7 @@ window.__DK_UI_VERSION="0.1.102";
|
|
|
1608
1613
|
},
|
|
1609
1614
|
/* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--faint)" }),
|
|
1610
1615
|
/* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.status === "sending" ? T.sendingMsg || "sending" : T.queued || "queued")
|
|
1611
|
-
) : mine && m.status === "sent" && m.confirmed === false ? /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 2.2, color: "var(--faint)", title: T.sentUnconfirmed || "sent \u2014 not confirmed by the peer" }) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
|
|
1616
|
+
) : mine && m.via === "offline" && m.status !== "queued" && m.status !== "sending" ? /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 2.2, color: "#f59e0b", title: T.viaOutOffline || "posted to the offline relay \u2014 they get it when they come online" }) : mine && m.status === "sent" && m.confirmed === false ? /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 2.2, color: "var(--faint)", title: T.sentUnconfirmed || "sent \u2014 not confirmed by the peer" }) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
|
|
1612
1617
|
);
|
|
1613
1618
|
}
|
|
1614
1619
|
function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted, onToggleMute, phone, onBack }) {
|
|
@@ -4957,6 +4962,9 @@ ${peer.address}`
|
|
|
4957
4962
|
pubRecSent: "sent \u2713",
|
|
4958
4963
|
updTitle: "Update available",
|
|
4959
4964
|
updLater: "Later",
|
|
4965
|
+
brandAbout: "What is Beagle?",
|
|
4966
|
+
viaOutOffline: "posted to the offline relay \u2014 they get it when they come online",
|
|
4967
|
+
viaOutOnline: "sent over a live session (online)",
|
|
4960
4968
|
updNow: "Update now",
|
|
4961
4969
|
updRestart: "Restart",
|
|
4962
4970
|
updBusy: "Updating\u2026 (about a minute)",
|
|
@@ -5146,6 +5154,9 @@ ${peer.address}`
|
|
|
5146
5154
|
pubRecSent: "\u5DF2\u53D1\u9001 \u2713",
|
|
5147
5155
|
updTitle: "\u53D1\u73B0\u65B0\u7248\u672C",
|
|
5148
5156
|
updLater: "\u7A0D\u540E",
|
|
5157
|
+
brandAbout: "Beagle \u662F\u4EC0\u4E48?",
|
|
5158
|
+
viaOutOffline: "\u5DF2\u5B58\u5230\u79BB\u7EBF\u4E2D\u8F6C\u2014\u2014\u5BF9\u65B9\u4E0A\u7EBF\u540E\u6536\u5230",
|
|
5159
|
+
viaOutOnline: "\u5728\u7EBF\u76F4\u63A5\u9001\u8FBE",
|
|
5149
5160
|
updNow: "\u7ACB\u5373\u66F4\u65B0",
|
|
5150
5161
|
updRestart: "\u91CD\u542F",
|
|
5151
5162
|
updBusy: "\u66F4\u65B0\u4E2D\u2026(\u7EA6\u4E00\u5206\u949F)",
|
|
@@ -5235,6 +5246,21 @@ ${peer.address}`
|
|
|
5235
5246
|
} }, /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 22, stroke: active ? 2.1 : 1.8, color: active ? "var(--accent)" : "var(--dim)" }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 10, fontWeight: 600, color: active ? "var(--accent)" : "var(--faint)", whiteSpace: "nowrap" } }, label), badge ? /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", top: 4, left: "50%", marginLeft: 6 } }, /* @__PURE__ */ React.createElement(Unread, { n: badge })) : null);
|
|
5236
5247
|
}
|
|
5237
5248
|
var DK_PEOPLE_TABS = ["here", "recommended", "registered"];
|
|
5249
|
+
function Brand({ T, children, style }) {
|
|
5250
|
+
if (!hostBrandHref)
|
|
5251
|
+
return /* @__PURE__ */ React.createElement("span", { style }, children);
|
|
5252
|
+
return /* @__PURE__ */ React.createElement(
|
|
5253
|
+
"a",
|
|
5254
|
+
{
|
|
5255
|
+
href: hostBrandHref,
|
|
5256
|
+
target: "_blank",
|
|
5257
|
+
rel: "noopener noreferrer",
|
|
5258
|
+
title: T.brandAbout || "What is Beagle?",
|
|
5259
|
+
style: { textDecoration: "none", color: "inherit", display: "flex", alignItems: "center", ...style }
|
|
5260
|
+
},
|
|
5261
|
+
children
|
|
5262
|
+
);
|
|
5263
|
+
}
|
|
5238
5264
|
function DkUpdateModal({ T, info, onClose }) {
|
|
5239
5265
|
const [phase, setPhase] = React.useState("idle");
|
|
5240
5266
|
const [err, setErr] = React.useState(null);
|
|
@@ -5650,14 +5676,14 @@ ${peer.address}`
|
|
|
5650
5676
|
{ id: "profile", icon: "userRound", label: T.profile, active: tab === "profile", go: () => setTab("profile") }
|
|
5651
5677
|
];
|
|
5652
5678
|
const inConversation = phone && tab === "chat" && !!activeId && (peers || []).some((p) => p.id === activeId);
|
|
5653
|
-
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", flexDirection: phone ? "column" : "row", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement(DkPhoneStyles, null), !phone && /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement(
|
|
5679
|
+
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", flexDirection: phone ? "column" : "row", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement(DkPhoneStyles, null), !phone && /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement(Brand, { T, style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8, flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: {
|
|
5654
5680
|
padding: 4,
|
|
5655
5681
|
borderRadius: 12,
|
|
5656
5682
|
cursor: "pointer",
|
|
5657
5683
|
display: "flex",
|
|
5658
5684
|
border: "1px solid " + (tab === "profile" ? "var(--line)" : "transparent"),
|
|
5659
5685
|
background: tab === "profile" ? "var(--panel-2)" : "transparent"
|
|
5660
|
-
} }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, !inConversation && /* @__PURE__ */ React.createElement(DkBrowserNotice, { lang: t.lang }), !inConversation && /* @__PURE__ */ React.createElement("div", { style: { height: 46, boxSizing: "content-box", paddingTop: phone ? "env(safe-area-inset-top, 0px)" : 0, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: phone ? 8 : 12, paddingLeft: phone ? 12 : 16, paddingRight: phone ? 12 : 16 } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)", whiteSpace: "nowrap" } }, "\xB7 ", (nav.find((n) => n.id === tab) || { label: T.profile }).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), phone ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), shareBtn, /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: { padding: 0, border: "none", background: "transparent", cursor: "pointer", display: "flex" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 28, radius: 7, dot: false }))) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), shareBtn, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name)))), phone && peopleTab && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", gap: 6, padding: "8px 12px", borderBottom: "1px solid var(--line)", background: "var(--panel)", overflowX: "auto" } }, nav.filter((n) => DK_PEOPLE_TABS.includes(n.id)).map((n) => /* @__PURE__ */ React.createElement("button", { key: n.id, onClick: () => setTab(n.id), style: {
|
|
5686
|
+
} }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, !inConversation && /* @__PURE__ */ React.createElement(DkBrowserNotice, { lang: t.lang }), !inConversation && /* @__PURE__ */ React.createElement("div", { style: { height: 46, boxSizing: "content-box", paddingTop: phone ? "env(safe-area-inset-top, 0px)" : 0, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: phone ? 8 : 12, paddingLeft: phone ? 12 : 16, paddingRight: phone ? 12 : 16 } }, /* @__PURE__ */ React.createElement(Brand, { T, style: { gap: 12, flexShrink: 0 } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle")), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)", whiteSpace: "nowrap" } }, "\xB7 ", (nav.find((n) => n.id === tab) || { label: T.profile }).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), phone ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), shareBtn, /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: { padding: 0, border: "none", background: "transparent", cursor: "pointer", display: "flex" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 28, radius: 7, dot: false }))) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), shareBtn, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name)))), phone && peopleTab && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", gap: 6, padding: "8px 12px", borderBottom: "1px solid var(--line)", background: "var(--panel)", overflowX: "auto" } }, nav.filter((n) => DK_PEOPLE_TABS.includes(n.id)).map((n) => /* @__PURE__ */ React.createElement("button", { key: n.id, onClick: () => setTab(n.id), style: {
|
|
5661
5687
|
display: "inline-flex",
|
|
5662
5688
|
alignItems: "center",
|
|
5663
5689
|
gap: 6,
|
package/dist/embedded-host.js
CHANGED
|
@@ -597,6 +597,9 @@ export class EmbeddedHost {
|
|
|
597
597
|
// unconfirmed message is how a phone ends up with two copies.
|
|
598
598
|
const r = await this.#node.sendText(uid, text);
|
|
599
599
|
this.#messages.setStatus(uid, msg.id, "sent", !r || r.delivery === "acked");
|
|
600
|
+
// Same field incoming messages carry, so the bubble can say which
|
|
601
|
+
// road it took: a relay post is waiting for them, not with them.
|
|
602
|
+
this.#messages.setVia(uid, msg.id, r && r.delivery === "offline" ? "offline" : "online");
|
|
600
603
|
}
|
|
601
604
|
catch (e) {
|
|
602
605
|
// Offline peer or failed delivery: same recovery either way —
|
package/dist/lan-handoff.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ export declare const DAEMON_GRACE_MS: number;
|
|
|
13
13
|
* Only a streak of misses that also spans real time is believed. */
|
|
14
14
|
export declare const DAEMON_MISS_STREAK = 3;
|
|
15
15
|
export declare const DAEMON_MISS_WINDOW_MS = 5000;
|
|
16
|
+
/** Is the process named in <dataDir>/daemon.pid still there? A root daemon
|
|
17
|
+
* answers signal 0 with EPERM, which still proves it exists. */
|
|
18
|
+
export declare function daemonPidAlive(dataDir: string): boolean;
|
|
16
19
|
export interface SwitchableHostOptions {
|
|
17
20
|
dataDir: string;
|
|
18
21
|
/** Build a fresh embedded host. Called on first start and on every fallback,
|
|
@@ -30,6 +33,8 @@ export interface SwitchableHostOptions {
|
|
|
30
33
|
graceMs?: number;
|
|
31
34
|
/** Liveness probe, injectable for tests. Defaults to a real connect. */
|
|
32
35
|
probe?: (dataDir: string) => Promise<boolean>;
|
|
36
|
+
/** Test seam: does the daemon's process exist? Default reads <dataDir>/daemon.pid. */
|
|
37
|
+
pidAlive?: (dataDir: string) => boolean;
|
|
33
38
|
}
|
|
34
39
|
/**
|
|
35
40
|
* A PeerHost whose backend can change under it.
|
package/dist/lan-handoff.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// `releasing` is the honest name for the middle state: our peer is down and the
|
|
14
14
|
// daemon is not up yet, so messaging is offline and the UI must say so. Hiding
|
|
15
15
|
// that gap would make dropped messages look like a network fault.
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
16
18
|
import { probeDaemon } from "./ipc.js";
|
|
17
19
|
import { Logger } from "./logger.js";
|
|
18
20
|
/** How often we look for the daemon socket appearing or vanishing. */
|
|
@@ -30,6 +32,26 @@ export const DAEMON_GRACE_MS = 60 * 1000;
|
|
|
30
32
|
* Only a streak of misses that also spans real time is believed. */
|
|
31
33
|
export const DAEMON_MISS_STREAK = 3;
|
|
32
34
|
export const DAEMON_MISS_WINDOW_MS = 5000;
|
|
35
|
+
/** Is the process named in <dataDir>/daemon.pid still there? A root daemon
|
|
36
|
+
* answers signal 0 with EPERM, which still proves it exists. */
|
|
37
|
+
export function daemonPidAlive(dataDir) {
|
|
38
|
+
let pid = 0;
|
|
39
|
+
try {
|
|
40
|
+
pid = Number(readFileSync(join(dataDir, "daemon.pid"), "utf-8").trim());
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (!Number.isInteger(pid) || pid <= 1)
|
|
46
|
+
return false;
|
|
47
|
+
try {
|
|
48
|
+
process.kill(pid, 0);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return err.code === "EPERM";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
33
55
|
/**
|
|
34
56
|
* A PeerHost whose backend can change under it.
|
|
35
57
|
*
|
|
@@ -49,12 +71,14 @@ export class SwitchablePeerHost {
|
|
|
49
71
|
* grace period; reset the moment it answers again. */
|
|
50
72
|
#daemonMissingSince = 0;
|
|
51
73
|
#probe;
|
|
74
|
+
#pidAlive;
|
|
52
75
|
constructor(initial, opts) {
|
|
53
76
|
this.#inner = initial;
|
|
54
77
|
this.#state = initial.kind === "daemon" ? "daemon" : "embedded";
|
|
55
78
|
this.#opts = opts;
|
|
56
79
|
this.#log = opts.log ?? new Logger({ prefix: "LAN" });
|
|
57
80
|
this.#probe = opts.probe ?? ((dir) => probeDaemon(dir));
|
|
81
|
+
this.#pidAlive = opts.pidAlive ?? daemonPidAlive;
|
|
58
82
|
this.#watch();
|
|
59
83
|
}
|
|
60
84
|
get kind() {
|
|
@@ -144,6 +168,8 @@ export class SwitchablePeerHost {
|
|
|
144
168
|
}
|
|
145
169
|
/** Consecutive missed probes; 0 the moment the daemon answers. */
|
|
146
170
|
#missStreak = 0;
|
|
171
|
+
/** One "not answering but alive" line per stall, not one per tick. */
|
|
172
|
+
#stallLogged = false;
|
|
147
173
|
/** A probe can take its full 2 s timeout against a 1.5 s watch interval.
|
|
148
174
|
* Overlapping ticks would each act on the other's half-made decision. */
|
|
149
175
|
#ticking = false;
|
|
@@ -161,6 +187,10 @@ export class SwitchablePeerHost {
|
|
|
161
187
|
async #step() {
|
|
162
188
|
const daemonUp = await this.#probe(this.#opts.dataDir);
|
|
163
189
|
if (daemonUp) {
|
|
190
|
+
if (this.#stallLogged && this.#daemonMissingSince) {
|
|
191
|
+
this.#log.info(`the daemon answers again after ${((Date.now() - this.#daemonMissingSince) / 1000).toFixed(1)} s`);
|
|
192
|
+
}
|
|
193
|
+
this.#stallLogged = false;
|
|
164
194
|
this.#daemonMissingSince = 0;
|
|
165
195
|
this.#missStreak = 0;
|
|
166
196
|
}
|
|
@@ -189,6 +219,19 @@ export class SwitchablePeerHost {
|
|
|
189
219
|
const missedMs = Date.now() - this.#daemonMissingSince;
|
|
190
220
|
if (this.#missStreak < DAEMON_MISS_STREAK || missedMs < DAEMON_MISS_WINDOW_MS)
|
|
191
221
|
return;
|
|
222
|
+
// Not answering is not gone. On 2026-09-18 mac-dev's daemon (pid
|
|
223
|
+
// unchanged for 19 h) refused every connect for ~20 s at a time while
|
|
224
|
+
// it was busy, and each stretch emptied the friend list. While its
|
|
225
|
+
// process exists it is stalled, and it will answer again; only a
|
|
226
|
+
// dead pid is an absence. Say so once per stall, then keep serving.
|
|
227
|
+
if (this.#pidAlive(this.#opts.dataDir)) {
|
|
228
|
+
if (!this.#stallLogged) {
|
|
229
|
+
this.#stallLogged = true;
|
|
230
|
+
this.#log.info(`the daemon is not answering (${this.#missStreak} probes over ${(missedMs / 1000).toFixed(1)} s) ` +
|
|
231
|
+
`but its process is alive — treating it as busy, not gone`);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
192
235
|
this.#log.info(`the daemon went away — waiting for it to come back (forced daemon backend; ` +
|
|
193
236
|
`${this.#missStreak} probes missed over ${(missedMs / 1000).toFixed(1)} s)`);
|
|
194
237
|
const old = this.#inner;
|
package/dist/message-store.d.ts
CHANGED
|
@@ -70,6 +70,9 @@ export declare class MessageStore {
|
|
|
70
70
|
* resent; the flag is what the UI reads to stop over-claiming.
|
|
71
71
|
*/
|
|
72
72
|
setStatus(peer: string, id: string, status?: "sending" | "queued" | "sent" | "failed", confirmed?: boolean): boolean;
|
|
73
|
+
/** Record how an outgoing text travelled: "online" over a live session,
|
|
74
|
+
* "offline" posted to the express relay for the peer to collect. */
|
|
75
|
+
setVia(peer: string, id: string, via: "online" | "offline"): boolean;
|
|
73
76
|
/** Has this peer ever sent us anything? Used as proof-of-friendship for
|
|
74
77
|
* records that predate the SDK's acceptedAt bookkeeping — a message can
|
|
75
78
|
* only arrive over an established session. */
|
package/dist/message-store.js
CHANGED
|
@@ -86,6 +86,16 @@ export class MessageStore {
|
|
|
86
86
|
this.scheduleSave();
|
|
87
87
|
return true;
|
|
88
88
|
}
|
|
89
|
+
/** Record how an outgoing text travelled: "online" over a live session,
|
|
90
|
+
* "offline" posted to the express relay for the peer to collect. */
|
|
91
|
+
setVia(peer, id, via) {
|
|
92
|
+
const msg = this.byPeer.get(peer)?.find((m) => m.id === id);
|
|
93
|
+
if (!msg || msg.file)
|
|
94
|
+
return false;
|
|
95
|
+
msg.via = via;
|
|
96
|
+
this.scheduleSave();
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
89
99
|
/** Has this peer ever sent us anything? Used as proof-of-friendship for
|
|
90
100
|
* records that predate the SDK's acceptedAt bookkeeping — a message can
|
|
91
101
|
* only arrive over an established session. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.104",
|
|
4
4
|
"description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"yargs": "^17.7.2"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@decentnetwork/beagle-ui": "^0.2.
|
|
38
|
+
"@decentnetwork/beagle-ui": "^0.2.19",
|
|
39
39
|
"@types/js-yaml": "^4.0.9",
|
|
40
40
|
"@types/node": "^20.11.0",
|
|
41
41
|
"@types/yargs": "^17.0.32",
|