@decentnetwork/beagle 0.1.54 → 0.1.55
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 +1 -1
- package/dist/embedded-host.js +58 -7
- package/dist/server.js +17 -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.55";
|
|
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"/>',
|
package/dist/embedded-host.js
CHANGED
|
@@ -22,9 +22,26 @@ import { acquireIdentityLock } from "./identity-lock.js";
|
|
|
22
22
|
* on disk until they reconnect, and an unbounded queue is how you fill a
|
|
23
23
|
* user's disk by accident. Live transfers have no such limit. */
|
|
24
24
|
const OUTBOX_MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
25
|
+
/** Inline envelope ceiling for a JS peer: base64 (~1.34x) inside a 16MB
|
|
26
|
+
* bulkmsg cap. */
|
|
27
27
|
const INLINE_MAX_BYTES = 11 * 1024 * 1024;
|
|
28
|
+
/** Inline ceiling for a NATIVE (iOS/Android) peer — tiny files only.
|
|
29
|
+
*
|
|
30
|
+
* History: 11MB (the JS receiver's figure) created a dead band where
|
|
31
|
+
* multi-MB inline sends died silently in the native kernel (5.2MB failed,
|
|
32
|
+
* 52MB streamed fine); 2.5MB still sat inside the band where the envelope's
|
|
33
|
+
* only confirmation — the toxcore send window draining — is structurally
|
|
34
|
+
* unreliable: an 8.5MB envelope is ~11k bulkmsg fragments on a channel with
|
|
35
|
+
* no congestion control, and three live 8.5MB sends all timed out
|
|
36
|
+
* unconfirmed at 180s.
|
|
37
|
+
*
|
|
38
|
+
* Streaming (sendFile → DNFT1 for natives, peer >= 0.1.141) is the path
|
|
39
|
+
* built for size: offset acks, FEC, resume, and a real delivery verdict
|
|
40
|
+
* (verified 100MB JS→iPad, sent strictly after the receiver's disk write).
|
|
41
|
+
* Inline stays only as the one-round-trip shortcut for a thumbnail or a
|
|
42
|
+
* voice note: at 256KB (~350 fragments) the send window drains in seconds,
|
|
43
|
+
* so the transport ack actually means something. */
|
|
44
|
+
const INLINE_MAX_BYTES_NATIVE = 256 * 1024;
|
|
28
45
|
const MEDIA_RE = /\.(jpe?g|png|gif|webp|heic|bmp|svg|mp4|mov|m4v|webm|mp3|m4a|wav|aac|ogg|flac)$/i;
|
|
29
46
|
const isMediaFileName = (n) => MEDIA_RE.test(n);
|
|
30
47
|
/** Strip any directory component and characters that would let a peer-supplied
|
|
@@ -79,6 +96,27 @@ export class EmbeddedHost {
|
|
|
79
96
|
this.#messages = new MessageStore(resolve(opts.configDir, "messages.json"));
|
|
80
97
|
this.#meta = new FriendMetaStore(resolve(opts.configDir, "friends-meta.json"));
|
|
81
98
|
}
|
|
99
|
+
/** Friend-list order: pinned first, then most recent conversation, then
|
|
100
|
+
* never-messaged contacts by name. Returns a new array — `friends()`
|
|
101
|
+
* hands back the SDK's own list and sorting it in place would reorder
|
|
102
|
+
* the friend store as a side effect of merely rendering the sidebar. */
|
|
103
|
+
#sortForList(friends, last) {
|
|
104
|
+
const rank = (f) => [
|
|
105
|
+
this.#meta.get(f.carrierId)?.pinned ? 0 : 1,
|
|
106
|
+
// Negated so a LARGER timestamp sorts first. No conversation →
|
|
107
|
+
// MAX_SAFE_INTEGER, which parks those entries after every real one.
|
|
108
|
+
// Not Infinity: two never-messaged friends would then compare
|
|
109
|
+
// Infinity - Infinity = NaN, and the tie-break would only still work
|
|
110
|
+
// by accident of NaN being falsy.
|
|
111
|
+
last.has(f.carrierId) ? -last.get(f.carrierId).ts : Number.MAX_SAFE_INTEGER,
|
|
112
|
+
(f.name || f.carrierId).toLowerCase(),
|
|
113
|
+
];
|
|
114
|
+
return [...friends].sort((a, b) => {
|
|
115
|
+
const [ap, at, an] = rank(a);
|
|
116
|
+
const [bp, bt, bn] = rank(b);
|
|
117
|
+
return ap - bp || at - bt || an.localeCompare(bn);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
82
120
|
get downloadsDir() {
|
|
83
121
|
return resolve(this.#opts.configDir, "downloads");
|
|
84
122
|
}
|
|
@@ -338,7 +376,15 @@ export class EmbeddedHost {
|
|
|
338
376
|
case "friends-list": {
|
|
339
377
|
const last = this.#messages.lastMessages();
|
|
340
378
|
return {
|
|
341
|
-
|
|
379
|
+
// Most-recent conversation first, pinned above everything. Without
|
|
380
|
+
// this the list came out in friend-store insertion order — the
|
|
381
|
+
// order contacts were ADDED, which on restart is just the order
|
|
382
|
+
// they load from disk. lastMessages() has always been gathered
|
|
383
|
+
// "for the friend-list preview/sort"; only the preview half was
|
|
384
|
+
// ever wired up, so the sort silently degraded to arbitrary.
|
|
385
|
+
// Friends you have never exchanged a message with sort last, by
|
|
386
|
+
// name, instead of being scattered through the list.
|
|
387
|
+
friends: this.#sortForList(this.#node.friends(), last).map((f) => {
|
|
342
388
|
const meta = this.#meta.get(f.carrierId);
|
|
343
389
|
const lastMsg = last.get(f.carrierId);
|
|
344
390
|
return {
|
|
@@ -598,7 +644,11 @@ export class EmbeddedHost {
|
|
|
598
644
|
// natives as DNFT1 frames inside friend messages (offset acks, FEC,
|
|
599
645
|
// resume; verified 100MB JS→iPad). Only route to inline what fits it.
|
|
600
646
|
const native = this.#node.isNativeFriend(userid);
|
|
601
|
-
|
|
647
|
+
// Per-peer ceiling: a native receiver's inline limit is far lower than a
|
|
648
|
+
// JS one's, and using the JS number for both is what created the dead
|
|
649
|
+
// band (see INLINE_MAX_BYTES_NATIVE).
|
|
650
|
+
const inlineLimit = native ? INLINE_MAX_BYTES_NATIVE : INLINE_MAX_BYTES;
|
|
651
|
+
const inlineNative = native && data.length <= inlineLimit;
|
|
602
652
|
const msg = this.#messages.appendFile(userid, "out", { name: safe, size: data.length, status: "sending", sent: 0 });
|
|
603
653
|
if (!msg)
|
|
604
654
|
throw new Error("Could not create file message");
|
|
@@ -607,10 +657,10 @@ export class EmbeddedHost {
|
|
|
607
657
|
const fileId = inlineNative ? null : this.#node.sendFile(userid, new Uint8Array(data), safe);
|
|
608
658
|
if (!fileId) {
|
|
609
659
|
// Native friend, or no transfer slot — the inline envelope path.
|
|
610
|
-
if (data.length >
|
|
660
|
+
if (data.length > inlineLimit) {
|
|
611
661
|
this.#messages.patchFile(userid, msg.id, { status: "failed" });
|
|
612
662
|
throw new Error(`Could not start the transfer, and the file is too large (${(data.length / 1024 / 1024).toFixed(1)} MB) ` +
|
|
613
|
-
`for the inline fallback (limit ${(
|
|
663
|
+
`for the inline fallback (limit ${(inlineLimit / 1024 / 1024).toFixed(1)} MB).`);
|
|
614
664
|
}
|
|
615
665
|
// "sent" means the PEER CONFIRMED receipt — nothing less (the fake-sent
|
|
616
666
|
// of INBOX 2026-08-21). And sends are NON-BLOCKING (the 2026-08-05
|
|
@@ -632,7 +682,8 @@ export class EmbeddedHost {
|
|
|
632
682
|
const status = delivery === "acked" ? "sent" : delivery === "offline" ? "queued" : "failed";
|
|
633
683
|
this.#messages.patchFile(userid, msg.id, { status, sent: delivery === "acked" ? data.length : 0 });
|
|
634
684
|
this.#events.emit("event", { type: "chat", userid, dir: "out" });
|
|
635
|
-
this.#logger.info(`inline file "${safe}" (${data.length}B) to ${userid.slice(0, 8)}: delivery=${delivery} → ${status}`
|
|
685
|
+
this.#logger.info(`inline file "${safe}" (${data.length}B) to ${userid.slice(0, 8)}: delivery=${delivery} → ${status}` +
|
|
686
|
+
(delivery === "accepted" ? " (unconfirmed — the bytes went out and may have arrived, but the peer never acknowledged)" : ""));
|
|
636
687
|
})();
|
|
637
688
|
return { inline: true, name: safe, size: data.length, pending: true };
|
|
638
689
|
}
|
package/dist/server.js
CHANGED
|
@@ -1061,11 +1061,28 @@ export function startBeagleServer(opts) {
|
|
|
1061
1061
|
}
|
|
1062
1062
|
let destName = safe;
|
|
1063
1063
|
let destPath = join(destDir, destName);
|
|
1064
|
+
// Re-sending a file the user already has must not duplicate it.
|
|
1065
|
+
// The -N suffix exists so two DIFFERENT files sharing a name both
|
|
1066
|
+
// survive — but an identical re-send hit it too, so every retry of
|
|
1067
|
+
// the same 52 MB video wrote another full copy (observed: four
|
|
1068
|
+
// byte-identical copies of one file, and a 3.9 GB downloads dir).
|
|
1069
|
+
// Reuse the existing path when the size already matches, so the
|
|
1070
|
+
// upload overwrites that copy instead of growing a new one. Tradeoff:
|
|
1071
|
+
// a different file with the same name AND the same byte count
|
|
1072
|
+
// replaces the older copy rather than sitting beside it — the same
|
|
1073
|
+
// thing any "save to downloads" does, and far better than unbounded
|
|
1074
|
+
// duplication. Hashing instead would mean buffering the whole upload.
|
|
1075
|
+
const uploadSize = Number(req.headers["content-length"] ?? NaN);
|
|
1064
1076
|
if (keep) {
|
|
1065
1077
|
const dot = safe.lastIndexOf(".");
|
|
1066
1078
|
const base = dot > 0 ? safe.slice(0, dot) : safe;
|
|
1067
1079
|
const ext = dot > 0 ? safe.slice(dot) : "";
|
|
1068
1080
|
for (let i = 1; existsSync(destPath); i++) {
|
|
1081
|
+
// Same size is a cheap, reliable proxy here: these are the
|
|
1082
|
+
// sender's own files, not adversarial input, and the full
|
|
1083
|
+
// compare would mean buffering the upload to hash it.
|
|
1084
|
+
if (Number.isFinite(uploadSize) && statSync(destPath).size === uploadSize)
|
|
1085
|
+
break;
|
|
1069
1086
|
destName = `${base}-${i}${ext}`;
|
|
1070
1087
|
destPath = join(destDir, destName);
|
|
1071
1088
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.55",
|
|
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",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@decentnetwork/chat-components": "^0.1.3",
|
|
29
|
-
"@decentnetwork/lan": "^0.1.
|
|
29
|
+
"@decentnetwork/lan": "^0.1.282",
|
|
30
30
|
"@decentnetwork/peer": "^0.1.141",
|
|
31
31
|
"@decentnetwork/peer-webrtc": "^0.2.16",
|
|
32
32
|
"js-yaml": "^4.1.0",
|