@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 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(). Prefer @decentnetwork/peer getIceServers() when
5
- // the installed peer version exports it; otherwise fall back to
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 { dirname, join } from "node:path";
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
- const userid = carrierIdFromPublicKey(publicKey);
42
- const peerRoot = dirname(fileURLToPath(import.meta.resolve("@decentnetwork/peer")));
43
- const peerMod = (await import(pathToFileURL(join(peerRoot, "index.js")).href));
44
- if (typeof peerMod.getIceServers === "function") {
45
- return peerMod.getIceServers({
46
- bootstrapNodes: nodes,
47
- ourUserid: userid,
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
  }
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.6";
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" ? /* @__PURE__ */ React.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 2 }, title: "waiting for peer \u2014 will send when they're online" }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--faint)" }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, T.queued || "queued")) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
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",
@@ -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
- // Anything already queued for this peer goes firstthe new message
375
- // must not overtake older ones the moment the path recovers.
376
- await this.#flushOutbox(uid).catch(() => { });
377
- try {
378
- await this.#node.sendText(uid, text);
379
- this.#messages.setStatus(uid, msg.id, "sent");
380
- return {};
381
- }
382
- catch (e) {
383
- // Offline peer or failed delivery: same recovery either way — keep
384
- // the message "queued" (the UI shows the clock) and deliver it on
385
- // the friend's reconnect via #flushOutbox. This is a success from
386
- // the caller's side: the store owns the message; only its delivery
387
- // is pending.
388
- this.#logger.info(`send to ${uid.slice(0, 8)} queued: ${e.message}`);
389
- this.#messages.setStatus(uid, msg.id, "queued");
390
- return { queued: true };
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());
@@ -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. "queued" means the peer was offline when the
18
- * user hit send, so the daemon stored it and will deliver it (in order) the
19
- * moment the friend reconnects. Cleared once actually sent. */
20
- status?: "queued" | "sent" | "failed";
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. */
@@ -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.6",
4
- "description": "Beagle \u2014 P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
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": {