@decentnetwork/beagle 0.1.54 → 0.1.56
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 +54 -12
- 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.56";
|
|
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,19 @@ 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
|
+
/** Native (iOS/Android) peers get NO inline shortcut at any size — they
|
|
29
|
+
* stream via sendFile → DNFT1 (peer >= 0.1.141) like everyone else, because
|
|
30
|
+
* the inline envelope has no usable confirmation against a native peer:
|
|
31
|
+
* protoVersion is structurally 0 (no text-ack), the bare FileModel carries no
|
|
32
|
+
* deliveryId to ack, and the transport-ack signal (our send window draining)
|
|
33
|
+
* was FIELD-FALSIFIED at every scale — a 282-BYTE single-fragment PNG sat
|
|
34
|
+
* unconfirmed for 20s on the same session that streamed 100MB at 0.0% loss.
|
|
35
|
+
* Gates of 11MB and 2.5MB and 256KB all just manufactured failed chips for
|
|
36
|
+
* files that had actually arrived. DNFT1's contiguous ack is the only true
|
|
37
|
+
* delivery signal a native can give; the cost is one offer round trip. */
|
|
28
38
|
const MEDIA_RE = /\.(jpe?g|png|gif|webp|heic|bmp|svg|mp4|mov|m4v|webm|mp3|m4a|wav|aac|ogg|flac)$/i;
|
|
29
39
|
const isMediaFileName = (n) => MEDIA_RE.test(n);
|
|
30
40
|
/** Strip any directory component and characters that would let a peer-supplied
|
|
@@ -79,6 +89,27 @@ export class EmbeddedHost {
|
|
|
79
89
|
this.#messages = new MessageStore(resolve(opts.configDir, "messages.json"));
|
|
80
90
|
this.#meta = new FriendMetaStore(resolve(opts.configDir, "friends-meta.json"));
|
|
81
91
|
}
|
|
92
|
+
/** Friend-list order: pinned first, then most recent conversation, then
|
|
93
|
+
* never-messaged contacts by name. Returns a new array — `friends()`
|
|
94
|
+
* hands back the SDK's own list and sorting it in place would reorder
|
|
95
|
+
* the friend store as a side effect of merely rendering the sidebar. */
|
|
96
|
+
#sortForList(friends, last) {
|
|
97
|
+
const rank = (f) => [
|
|
98
|
+
this.#meta.get(f.carrierId)?.pinned ? 0 : 1,
|
|
99
|
+
// Negated so a LARGER timestamp sorts first. No conversation →
|
|
100
|
+
// MAX_SAFE_INTEGER, which parks those entries after every real one.
|
|
101
|
+
// Not Infinity: two never-messaged friends would then compare
|
|
102
|
+
// Infinity - Infinity = NaN, and the tie-break would only still work
|
|
103
|
+
// by accident of NaN being falsy.
|
|
104
|
+
last.has(f.carrierId) ? -last.get(f.carrierId).ts : Number.MAX_SAFE_INTEGER,
|
|
105
|
+
(f.name || f.carrierId).toLowerCase(),
|
|
106
|
+
];
|
|
107
|
+
return [...friends].sort((a, b) => {
|
|
108
|
+
const [ap, at, an] = rank(a);
|
|
109
|
+
const [bp, bt, bn] = rank(b);
|
|
110
|
+
return ap - bp || at - bt || an.localeCompare(bn);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
82
113
|
get downloadsDir() {
|
|
83
114
|
return resolve(this.#opts.configDir, "downloads");
|
|
84
115
|
}
|
|
@@ -338,7 +369,15 @@ export class EmbeddedHost {
|
|
|
338
369
|
case "friends-list": {
|
|
339
370
|
const last = this.#messages.lastMessages();
|
|
340
371
|
return {
|
|
341
|
-
|
|
372
|
+
// Most-recent conversation first, pinned above everything. Without
|
|
373
|
+
// this the list came out in friend-store insertion order — the
|
|
374
|
+
// order contacts were ADDED, which on restart is just the order
|
|
375
|
+
// they load from disk. lastMessages() has always been gathered
|
|
376
|
+
// "for the friend-list preview/sort"; only the preview half was
|
|
377
|
+
// ever wired up, so the sort silently degraded to arbitrary.
|
|
378
|
+
// Friends you have never exchanged a message with sort last, by
|
|
379
|
+
// name, instead of being scattered through the list.
|
|
380
|
+
friends: this.#sortForList(this.#node.friends(), last).map((f) => {
|
|
342
381
|
const meta = this.#meta.get(f.carrierId);
|
|
343
382
|
const lastMsg = last.get(f.carrierId);
|
|
344
383
|
return {
|
|
@@ -592,21 +631,23 @@ export class EmbeddedHost {
|
|
|
592
631
|
this.#events.emit("event", { type: "chat", userid, dir: "out" });
|
|
593
632
|
return { queued: true, name: safe, size: data.length };
|
|
594
633
|
}
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
// go through the normal sendFile below — peer >= 0.1.141 carries those to
|
|
598
|
-
// natives as DNFT1 frames inside friend messages (offset acks, FEC,
|
|
599
|
-
// resume; verified 100MB JS→iPad). Only route to inline what fits it.
|
|
634
|
+
// Everyone streams — a native friend's packets ride DNFT1 frames inside
|
|
635
|
+
// friend messages (see the note on the deleted native inline gate above).
|
|
600
636
|
const native = this.#node.isNativeFriend(userid);
|
|
601
|
-
const inlineNative = native && data.length <= INLINE_MAX_BYTES;
|
|
602
637
|
const msg = this.#messages.appendFile(userid, "out", { name: safe, size: data.length, status: "sending", sent: 0 });
|
|
603
638
|
if (!msg)
|
|
604
639
|
throw new Error("Could not create file message");
|
|
605
640
|
await mkdir(this.outboxDir, { recursive: true });
|
|
606
641
|
await writeFile(resolve(this.outboxDir, msg.id), data);
|
|
607
|
-
const fileId =
|
|
642
|
+
const fileId = this.#node.sendFile(userid, new Uint8Array(data), safe);
|
|
608
643
|
if (!fileId) {
|
|
609
|
-
//
|
|
644
|
+
// No transfer slot. The inline fallback only helps a JS friend (their
|
|
645
|
+
// text-ack makes it confirmable); to a native it is unconfirmable by
|
|
646
|
+
// construction, so fail honestly instead.
|
|
647
|
+
if (native) {
|
|
648
|
+
this.#messages.patchFile(userid, msg.id, { status: "failed" });
|
|
649
|
+
throw new Error("No free transfer slot — wait for a current transfer to finish and retry.");
|
|
650
|
+
}
|
|
610
651
|
if (data.length > INLINE_MAX_BYTES) {
|
|
611
652
|
this.#messages.patchFile(userid, msg.id, { status: "failed" });
|
|
612
653
|
throw new Error(`Could not start the transfer, and the file is too large (${(data.length / 1024 / 1024).toFixed(1)} MB) ` +
|
|
@@ -632,7 +673,8 @@ export class EmbeddedHost {
|
|
|
632
673
|
const status = delivery === "acked" ? "sent" : delivery === "offline" ? "queued" : "failed";
|
|
633
674
|
this.#messages.patchFile(userid, msg.id, { status, sent: delivery === "acked" ? data.length : 0 });
|
|
634
675
|
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}`
|
|
676
|
+
this.#logger.info(`inline file "${safe}" (${data.length}B) to ${userid.slice(0, 8)}: delivery=${delivery} → ${status}` +
|
|
677
|
+
(delivery === "accepted" ? " (unconfirmed — the bytes went out and may have arrived, but the peer never acknowledged)" : ""));
|
|
636
678
|
})();
|
|
637
679
|
return { inline: true, name: safe, size: data.length, pending: true };
|
|
638
680
|
}
|
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.56",
|
|
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.283",
|
|
30
30
|
"@decentnetwork/peer": "^0.1.141",
|
|
31
31
|
"@decentnetwork/peer-webrtc": "^0.2.16",
|
|
32
32
|
"js-yaml": "^4.1.0",
|