@decentnetwork/beagle 0.1.6 → 0.1.7
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/call-ice.js +10 -31
- package/dist/desktop/app.js +12 -2
- package/dist/embedded-host.js +33 -19
- package/dist/message-store.d.ts +8 -6
- package/dist/message-store.js +5 -0
- package/package.json +2 -2
package/dist/call-ice.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
// Carrier bootstrap TURN credentials for browser WebRTC.
|
|
2
2
|
//
|
|
3
3
|
// Android online calls use CarrierExtension.getTurnServerInfo() →
|
|
4
|
-
// carrier_get_turn_server().
|
|
5
|
-
//
|
|
6
|
-
// deriveCarrierTurnCreds via the package dist path (older peer builds).
|
|
4
|
+
// carrier_get_turn_server(). We call @decentnetwork/peer getIceServers()
|
|
5
|
+
// (0.1.136+) with the same credential scheme.
|
|
7
6
|
import { readFileSync, existsSync } from "node:fs";
|
|
8
|
-
import {
|
|
9
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
-
import { base58ToBytes, carrierIdFromPublicKey } from "@decentnetwork/peer";
|
|
7
|
+
import { carrierIdFromPublicKey, getIceServers } from "@decentnetwork/peer";
|
|
11
8
|
function hexToBytes(hex) {
|
|
12
9
|
const clean = hex.trim().replace(/^0x/i, "");
|
|
13
10
|
if (clean.length % 2 !== 0)
|
|
@@ -38,29 +35,11 @@ export async function buildCarrierCallIceServers(opts) {
|
|
|
38
35
|
if (!nodes.length)
|
|
39
36
|
throw new Error("no bootstrap nodes for TURN");
|
|
40
37
|
const { publicKey, secretKey } = loadKeyPair(opts.keyFile);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
ourSecretKey: secretKey,
|
|
49
|
-
limit: opts.limit ?? 3,
|
|
50
|
-
includeTcpTurn: true,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
const { deriveCarrierTurnCreds } = (await import(pathToFileURL(join(peerRoot, "turn-creds.js")).href));
|
|
54
|
-
const iceServers = [];
|
|
55
|
-
for (const n of nodes) {
|
|
56
|
-
const creds = deriveCarrierTurnCreds({
|
|
57
|
-
bootnodeHost: n.host,
|
|
58
|
-
bootnodePublicKey: base58ToBytes(n.pk),
|
|
59
|
-
ourUserid: userid,
|
|
60
|
-
ourSecretKey: secretKey,
|
|
61
|
-
});
|
|
62
|
-
const base = `${creds.host}:${creds.port}`;
|
|
63
|
-
iceServers.push({ urls: `stun:${base}`, username: creds.username, credential: creds.password }, { urls: `turn:${base}`, username: creds.username, credential: creds.password }, { urls: `turn:${base}?transport=tcp`, username: creds.username, credential: creds.password });
|
|
64
|
-
}
|
|
65
|
-
return iceServers;
|
|
38
|
+
return getIceServers({
|
|
39
|
+
bootstrapNodes: nodes,
|
|
40
|
+
ourUserid: carrierIdFromPublicKey(publicKey),
|
|
41
|
+
ourSecretKey: secretKey,
|
|
42
|
+
limit: opts.limit ?? 3,
|
|
43
|
+
includeTcpTurn: true,
|
|
44
|
+
});
|
|
66
45
|
}
|
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.7";
|
|
2
2
|
const ICON_PATHS = {
|
|
3
3
|
// ---- tab bar (the four must feel like one set) ----
|
|
4
4
|
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
|
@@ -1964,7 +1964,15 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onC
|
|
|
1964
1964
|
style: { background: "none", border: "none", cursor: "pointer", padding: 0, marginLeft: 2, display: "inline-flex", opacity: 0.55 }
|
|
1965
1965
|
},
|
|
1966
1966
|
/* @__PURE__ */ React.createElement(Icon, { name: "trash", size: 11, stroke: 2, color: "var(--faint)" })
|
|
1967
|
-
), mine && m.status === "queued"
|
|
1967
|
+
), mine && (m.status === "queued" || m.status === "sending") ? /* @__PURE__ */ React.createElement(
|
|
1968
|
+
"span",
|
|
1969
|
+
{
|
|
1970
|
+
style: { display: "inline-flex", alignItems: "center", gap: 2 },
|
|
1971
|
+
title: m.status === "sending" ? "delivering\u2026" : "waiting for peer \u2014 will send when they're online"
|
|
1972
|
+
},
|
|
1973
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--faint)" }),
|
|
1974
|
+
/* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.status === "sending" ? T.sendingMsg || "sending" : T.queued || "queued")
|
|
1975
|
+
) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
|
|
1968
1976
|
);
|
|
1969
1977
|
}
|
|
1970
1978
|
function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
|
|
@@ -3466,6 +3474,7 @@ const STR = {
|
|
|
3466
3474
|
send: "Send",
|
|
3467
3475
|
pickPeer: "Select a peer to open the conversation",
|
|
3468
3476
|
queued: "queued",
|
|
3477
|
+
sendingMsg: "sending",
|
|
3469
3478
|
open: "open",
|
|
3470
3479
|
retry: "Retry",
|
|
3471
3480
|
cancel: "Cancel",
|
|
@@ -3560,6 +3569,7 @@ const STR = {
|
|
|
3560
3569
|
send: "\u53D1\u9001",
|
|
3561
3570
|
pickPeer: "\u9009\u62E9\u4E00\u4F4D\u597D\u53CB\u5F00\u59CB\u4F1A\u8BDD",
|
|
3562
3571
|
queued: "\u5F85\u53D1\u9001",
|
|
3572
|
+
sendingMsg: "\u53D1\u9001\u4E2D",
|
|
3563
3573
|
open: "\u6253\u5F00",
|
|
3564
3574
|
retry: "\u91CD\u8BD5",
|
|
3565
3575
|
cancel: "\u53D6\u6D88",
|
package/dist/embedded-host.js
CHANGED
|
@@ -47,6 +47,10 @@ export class EmbeddedHost {
|
|
|
47
47
|
#logger = new Logger({ prefix: "Beagle" });
|
|
48
48
|
#opts;
|
|
49
49
|
#events = new EventEmitter();
|
|
50
|
+
/** Per-peer delivery chains: background sends to one peer run strictly in
|
|
51
|
+
* order, so a new message can never overtake an older one whose ACK is
|
|
52
|
+
* still pending. Entry removed when the chain drains. */
|
|
53
|
+
#sendChains = new Map();
|
|
50
54
|
/** Friend requests held for manual accept when auto-accept is off. */
|
|
51
55
|
#pending = new Map();
|
|
52
56
|
/** Inbound call signals waiting for the UI's long-poll to collect them. */
|
|
@@ -368,27 +372,37 @@ export class EmbeddedHost {
|
|
|
368
372
|
// user's own words vanish from their own thread — they watched the
|
|
369
373
|
// peer answer a question that wasn't on screen (INBOX 2026-08-02).
|
|
370
374
|
// A send failure is a delivery state ON the message, never its absence.
|
|
371
|
-
const msg = this.#messages.append(uid, "out", text, Date.now());
|
|
375
|
+
const msg = this.#messages.append(uid, "out", text, Date.now(), "sending");
|
|
372
376
|
this.#meta.ensure(uid);
|
|
373
377
|
this.#events.emit("event", { type: "chat", userid: uid, dir: "out" });
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
378
|
+
// Return NOW. The delivery attempt runs in the background — an ACK
|
|
379
|
+
// that takes 15s per hop must cost the network 15s, never the user
|
|
380
|
+
// (INBOX 2026-08-05: a peer that never ACKed made every send block
|
|
381
|
+
// the UI for 30s, which reads as "stuck" and provokes re-sends).
|
|
382
|
+
// Per-peer chaining keeps FIFO: anything queued flushes first, and a
|
|
383
|
+
// second send waits for this one's attempt before its own.
|
|
384
|
+
const prev = this.#sendChains.get(uid) ?? Promise.resolve();
|
|
385
|
+
const job = prev.then(async () => {
|
|
386
|
+
await this.#flushOutbox(uid).catch(() => { });
|
|
387
|
+
try {
|
|
388
|
+
await this.#node.sendText(uid, text);
|
|
389
|
+
this.#messages.setStatus(uid, msg.id, "sent");
|
|
390
|
+
}
|
|
391
|
+
catch (e) {
|
|
392
|
+
// Offline peer or failed delivery: same recovery either way —
|
|
393
|
+
// "queued" (the UI shows the clock) and #flushOutbox delivers it
|
|
394
|
+
// on the friend's reconnect. A status update, not a failure.
|
|
395
|
+
this.#logger.info(`send to ${uid.slice(0, 8)} queued: ${e.message}`);
|
|
396
|
+
this.#messages.setStatus(uid, msg.id, "queued");
|
|
397
|
+
}
|
|
398
|
+
this.#events.emit("event", { type: "chat", userid: uid, dir: "out" });
|
|
399
|
+
});
|
|
400
|
+
this.#sendChains.set(uid, job);
|
|
401
|
+
void job.finally(() => {
|
|
402
|
+
if (this.#sendChains.get(uid) === job)
|
|
403
|
+
this.#sendChains.delete(uid);
|
|
404
|
+
});
|
|
405
|
+
return {};
|
|
392
406
|
}
|
|
393
407
|
case "chat-log-local":
|
|
394
408
|
this.#messages.append(uid, req.dir ?? "in", String(req.text ?? ""), Date.now());
|
package/dist/message-store.d.ts
CHANGED
|
@@ -14,10 +14,12 @@ export interface ChatMessage {
|
|
|
14
14
|
ts: number;
|
|
15
15
|
/** Stable per-message id (ts + per-process sequence) for UI keys / dedup. */
|
|
16
16
|
id: string;
|
|
17
|
-
/** Outgoing-text delivery state. "
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
17
|
+
/** Outgoing-text delivery state. "sending" = the background delivery attempt
|
|
18
|
+
* is still running (chat-send returns before the network is consulted, so
|
|
19
|
+
* the UI never blocks on a slow peer). "queued" = that attempt gave up; the
|
|
20
|
+
* daemon will deliver it (in order) the moment the friend reconnects.
|
|
21
|
+
* Cleared once actually sent. */
|
|
22
|
+
status?: "sending" | "queued" | "sent" | "failed";
|
|
21
23
|
/** How this message traversed the network, for the UI to distinguish at a
|
|
22
24
|
* glance (different colors). "online" = live net_crypto session (direct/relay);
|
|
23
25
|
* "offline" = express store-and-forward (the friend or we were offline). Lets
|
|
@@ -52,11 +54,11 @@ export declare class MessageStore {
|
|
|
52
54
|
/** Append a message and schedule a flush. Returns the stored message.
|
|
53
55
|
* Pass `status: "queued"` for an outgoing text the peer wasn't online to
|
|
54
56
|
* receive — the daemon flushes it on reconnect. */
|
|
55
|
-
append(peer: string, dir: "in" | "out", text: string, ts?: number, status?: "queued" | "sent" | "failed", via?: "online" | "offline"): ChatMessage;
|
|
57
|
+
append(peer: string, dir: "in" | "out", text: string, ts?: number, status?: "sending" | "queued" | "sent" | "failed", via?: "online" | "offline"): ChatMessage;
|
|
56
58
|
/** Set (or, with undefined, clear) the delivery status on a text message.
|
|
57
59
|
* Used to flip a "queued" message to delivered once it's actually sent.
|
|
58
60
|
* No-op if the id isn't found or is a file entry. Returns true if patched. */
|
|
59
|
-
setStatus(peer: string, id: string, status?: "queued" | "sent" | "failed"): boolean;
|
|
61
|
+
setStatus(peer: string, id: string, status?: "sending" | "queued" | "sent" | "failed"): boolean;
|
|
60
62
|
/** Outgoing messages still awaiting delivery (peer was offline), oldest
|
|
61
63
|
* first — both queued text and queued file chips. The daemon drains this
|
|
62
64
|
* on a friend's reconnect. */
|
package/dist/message-store.js
CHANGED
|
@@ -33,6 +33,11 @@ export class MessageStore {
|
|
|
33
33
|
let total = 0;
|
|
34
34
|
for (const [peer, msgs] of Object.entries(raw)) {
|
|
35
35
|
if (Array.isArray(msgs)) {
|
|
36
|
+
// A "sending" state cannot survive a restart — the attempt it
|
|
37
|
+
// described died with the process. Requeue so #flushOutbox owns it.
|
|
38
|
+
for (const m of msgs)
|
|
39
|
+
if (m.status === "sending")
|
|
40
|
+
m.status = "queued";
|
|
36
41
|
this.byPeer.set(peer, msgs);
|
|
37
42
|
total += msgs.length;
|
|
38
43
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Beagle
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
7
7
|
"bin": {
|