@decentnetwork/beagle 0.1.55 → 0.1.57
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 +38 -5
- package/dist/embedded-host.js +102 -44
- package/dist/message-store.d.ts +3 -0
- package/dist/message-store.js +10 -0
- package/dist/server.js +32 -2
- 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.57";
|
|
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"/>',
|
|
@@ -1275,9 +1275,39 @@ Object.assign(window, {
|
|
|
1275
1275
|
TweakButton
|
|
1276
1276
|
});
|
|
1277
1277
|
function RequestsBlock({ T, requests, onAct }) {
|
|
1278
|
-
|
|
1278
|
+
const [acted, setActed] = React.useState({});
|
|
1279
|
+
const [failed, setFailed] = React.useState({});
|
|
1280
|
+
const act = (r, kind) => {
|
|
1281
|
+
if (acted[r.id])
|
|
1282
|
+
return;
|
|
1283
|
+
setActed((a) => ({ ...a, [r.id]: kind }));
|
|
1284
|
+
setFailed((f) => {
|
|
1285
|
+
const n = { ...f };
|
|
1286
|
+
delete n[r.id];
|
|
1287
|
+
return n;
|
|
1288
|
+
});
|
|
1289
|
+
Promise.resolve(onAct(r.id, kind)).then((res) => {
|
|
1290
|
+
if (res && res.ok === false) {
|
|
1291
|
+
setActed((a) => {
|
|
1292
|
+
const n = { ...a };
|
|
1293
|
+
delete n[r.id];
|
|
1294
|
+
return n;
|
|
1295
|
+
});
|
|
1296
|
+
setFailed((f) => ({ ...f, [r.id]: res.error || "failed" }));
|
|
1297
|
+
}
|
|
1298
|
+
}).catch((e) => {
|
|
1299
|
+
setActed((a) => {
|
|
1300
|
+
const n = { ...a };
|
|
1301
|
+
delete n[r.id];
|
|
1302
|
+
return n;
|
|
1303
|
+
});
|
|
1304
|
+
setFailed((f) => ({ ...f, [r.id]: String(e && e.message || e) }));
|
|
1305
|
+
});
|
|
1306
|
+
};
|
|
1307
|
+
const visible = requests.filter((r) => !acted[r.id]);
|
|
1308
|
+
if (!visible.length)
|
|
1279
1309
|
return null;
|
|
1280
|
-
return /* @__PURE__ */ React.createElement("div", { style: { padding: "4px 0 8px" } }, /* @__PURE__ */ React.createElement(Section, { label: T.requests, count:
|
|
1310
|
+
return /* @__PURE__ */ React.createElement("div", { style: { padding: "4px 0 8px" } }, /* @__PURE__ */ React.createElement(Section, { label: T.requests, count: visible.length, style: { margin: "4px 4px 8px" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 6 } }, visible.map((r) => /* @__PURE__ */ React.createElement("div", { key: r.id, style: {
|
|
1281
1311
|
padding: "9px 10px",
|
|
1282
1312
|
borderRadius: 8,
|
|
1283
1313
|
background: "var(--panel-2)",
|
|
@@ -1285,7 +1315,7 @@ function RequestsBlock({ T, requests, onAct }) {
|
|
|
1285
1315
|
display: "flex",
|
|
1286
1316
|
flexDirection: "column",
|
|
1287
1317
|
gap: 8
|
|
1288
|
-
} }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed: r.carrier, size: 26, radius: 6 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement(Mono, { size: 12, copy: r.carrier, title: r.carrier }, shortKey(r.carrier, 8, 6)), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)", marginTop: 2 } }, "via ", r.via, " \xB7 ", r.time))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () =>
|
|
1318
|
+
} }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed: r.carrier, size: 26, radius: 6 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement(Mono, { size: 12, copy: r.carrier, title: r.carrier }, shortKey(r.carrier, 8, 6)), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)", marginTop: 2 } }, "via ", r.via, " \xB7 ", r.time))), failed[r.id] && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11, color: "var(--danger)" } }, failed[r.id]), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () => act(r, "accept"), style: { flex: 1 } }, T.accept), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "x", size: "sm", onClick: () => act(r, "reject"), style: { flex: 1 } }, T.reject))))));
|
|
1289
1319
|
}
|
|
1290
1320
|
function PeerRow({ peer, T, active, onClick }) {
|
|
1291
1321
|
const name = peer.alias || peer.userId;
|
|
@@ -5031,7 +5061,10 @@ function DkApp() {
|
|
|
5031
5061
|
const onAct = (id, kind) => {
|
|
5032
5062
|
const r = requests.find((x) => x.id === id);
|
|
5033
5063
|
const uid = r && r.userid || id;
|
|
5034
|
-
(kind === "accept" ? dkApi.accept(uid) : dkApi.reject(uid)).then(
|
|
5064
|
+
return (kind === "accept" ? dkApi.accept(uid) : dkApi.reject(uid)).then((res) => {
|
|
5065
|
+
data.refresh();
|
|
5066
|
+
return res;
|
|
5067
|
+
});
|
|
5035
5068
|
};
|
|
5036
5069
|
const onRemove = (peer) => {
|
|
5037
5070
|
dkApi.remove(peer.id).then(data.refresh);
|
package/dist/embedded-host.js
CHANGED
|
@@ -25,23 +25,16 @@ const OUTBOX_MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
|
25
25
|
/** Inline envelope ceiling for a JS peer: base64 (~1.34x) inside a 16MB
|
|
26
26
|
* bulkmsg cap. */
|
|
27
27
|
const INLINE_MAX_BYTES = 11 * 1024 * 1024;
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
+
/** 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. */
|
|
45
38
|
const MEDIA_RE = /\.(jpe?g|png|gif|webp|heic|bmp|svg|mp4|mov|m4v|webm|mp3|m4a|wav|aac|ogg|flac)$/i;
|
|
46
39
|
const isMediaFileName = (n) => MEDIA_RE.test(n);
|
|
47
40
|
/** Strip any directory component and characters that would let a peer-supplied
|
|
@@ -69,6 +62,7 @@ export class EmbeddedHost {
|
|
|
69
62
|
* order, so a new message can never overtake an older one whose ACK is
|
|
70
63
|
* still pending. Entry removed when the chain drains. */
|
|
71
64
|
#sendChains = new Map();
|
|
65
|
+
#sweepTimer = null;
|
|
72
66
|
/** Friend requests held for manual accept when auto-accept is off. */
|
|
73
67
|
#pending = new Map();
|
|
74
68
|
/** Inbound call signals as a broadcast ring: every poller reads at its own
|
|
@@ -150,6 +144,50 @@ export class EmbeddedHost {
|
|
|
150
144
|
await this.#node.start();
|
|
151
145
|
await this.#node.join();
|
|
152
146
|
await mkdir(this.downloadsDir, { recursive: true }).catch(() => undefined);
|
|
147
|
+
// The sweep is what actually makes "queued" resolve.
|
|
148
|
+
//
|
|
149
|
+
// Flushing only on the connected EDGE looked right and was not: if the
|
|
150
|
+
// peer was already connected when the message was queued, that edge had
|
|
151
|
+
// long since passed and nothing ever tried again — the message sat queued
|
|
152
|
+
// while the friend showed online. Ask it the other way round on a timer:
|
|
153
|
+
// who has mail waiting, and can we reach them now. Level-triggered, so a
|
|
154
|
+
// missed or early event costs one interval instead of the message.
|
|
155
|
+
this.#sweepTimer = setInterval(() => {
|
|
156
|
+
if (this.#sweeping)
|
|
157
|
+
return;
|
|
158
|
+
this.#sweeping = true;
|
|
159
|
+
void (async () => {
|
|
160
|
+
try {
|
|
161
|
+
for (const uid of this.#messages.queuedPeers()) {
|
|
162
|
+
if (this.#sessionUsable(uid))
|
|
163
|
+
await this.#chainFlush(uid);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
this.#sweeping = false;
|
|
168
|
+
}
|
|
169
|
+
})();
|
|
170
|
+
}, 8000);
|
|
171
|
+
this.#sweepTimer.unref?.();
|
|
172
|
+
}
|
|
173
|
+
#sweeping = false;
|
|
174
|
+
/** A session that can actually carry traffic right now. */
|
|
175
|
+
#sessionUsable(pubkey) {
|
|
176
|
+
try {
|
|
177
|
+
const st = this.#node.sessionStatus(pubkey);
|
|
178
|
+
return !!(st && st.established && (st.udpRemote || st.hasTcpRoute));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Queue a flush behind whatever that peer already has in flight, so a
|
|
185
|
+
* sweep and a send can never interleave and reorder the outbox. */
|
|
186
|
+
#chainFlush(pubkey) {
|
|
187
|
+
const prev = this.#sendChains.get(pubkey) ?? Promise.resolve();
|
|
188
|
+
const job = prev.then(() => this.#flushOutbox(pubkey).catch(() => { }));
|
|
189
|
+
this.#sendChains.set(pubkey, job.catch(() => { }));
|
|
190
|
+
return job;
|
|
153
191
|
}
|
|
154
192
|
#wire() {
|
|
155
193
|
this.#node.on("message", (pubkey, text, via) => {
|
|
@@ -159,6 +197,9 @@ export class EmbeddedHost {
|
|
|
159
197
|
this.#messages.append(pubkey, "in", text, Date.now(), undefined, via);
|
|
160
198
|
this.#meta.ensure(pubkey);
|
|
161
199
|
this.#events.emit("event", { type: "chat", userid: pubkey, dir: "in" });
|
|
200
|
+
// Hearing from them proves the session carries traffic — better
|
|
201
|
+
// evidence than any status flag, so use it.
|
|
202
|
+
void this.#chainFlush(pubkey);
|
|
162
203
|
});
|
|
163
204
|
this.#node.on("friend-request", (req) => {
|
|
164
205
|
const userid = req.userid ?? req.pubkey;
|
|
@@ -186,15 +227,8 @@ export class EmbeddedHost {
|
|
|
186
227
|
// sat visibly stuck despite the peer being online. One delayed retry
|
|
187
228
|
// covers that gap; the per-send chain and later reconnects cover the
|
|
188
229
|
// rest.
|
|
189
|
-
if (e.status === "connected")
|
|
190
|
-
void this.#
|
|
191
|
-
.catch(() => { })
|
|
192
|
-
.then(() => {
|
|
193
|
-
if (this.#messages.queuedOutgoing(e.pubkey).length === 0)
|
|
194
|
-
return;
|
|
195
|
-
return new Promise((r) => setTimeout(r, 4000)).then(() => this.#flushOutbox(e.pubkey).catch(() => { }));
|
|
196
|
-
});
|
|
197
|
-
}
|
|
230
|
+
if (e.status === "connected")
|
|
231
|
+
void this.#chainFlush(e.pubkey); // fast path
|
|
198
232
|
});
|
|
199
233
|
this.#node.on("call-signal", (evt) => {
|
|
200
234
|
this.#callLog.push({
|
|
@@ -329,6 +363,10 @@ export class EmbeddedHost {
|
|
|
329
363
|
return () => this.#events.off("event", fn);
|
|
330
364
|
}
|
|
331
365
|
async stop() {
|
|
366
|
+
if (this.#sweepTimer) {
|
|
367
|
+
clearInterval(this.#sweepTimer);
|
|
368
|
+
this.#sweepTimer = null;
|
|
369
|
+
}
|
|
332
370
|
// Stop the peer FIRST, then release the lock. The reverse order would open
|
|
333
371
|
// a window where a daemon could start while our peer is still live.
|
|
334
372
|
await this.#node.stop();
|
|
@@ -412,16 +450,38 @@ export class EmbeddedHost {
|
|
|
412
450
|
}),
|
|
413
451
|
};
|
|
414
452
|
}
|
|
415
|
-
case "friend-request":
|
|
416
|
-
|
|
453
|
+
case "friend-request": {
|
|
454
|
+
// sendFriendRequest waits for our own announce to store and then walks
|
|
455
|
+
// the DHT toward the target — tens of seconds is normal. server.ts
|
|
456
|
+
// already races this with a 2.5s timer so the HTTP layer cannot hang,
|
|
457
|
+
// but a validation error has to come back fast to be useful, so do the
|
|
458
|
+
// cheap check here and let the slow part run unattended.
|
|
459
|
+
const address = String(req.address ?? "");
|
|
460
|
+
if (!address)
|
|
461
|
+
throw new Error("friend-request requires an address");
|
|
462
|
+
void this.#node
|
|
463
|
+
.sendFriendRequest(address, req.hello)
|
|
464
|
+
.catch((e) => this.#logger.info(`friend-request to ${address.slice(0, 8)} failed: ${e.message}`));
|
|
417
465
|
return {};
|
|
466
|
+
}
|
|
418
467
|
case "friends-pending":
|
|
419
468
|
return { pending: [...this.#pending.values()] };
|
|
420
469
|
case "friends-accept": {
|
|
421
470
|
const entry = this.#pending.get(uid);
|
|
422
|
-
if (entry)
|
|
471
|
+
if (!entry)
|
|
472
|
+
return {};
|
|
473
|
+
// Drop it from pending BEFORE the node call, and answer without
|
|
474
|
+
// waiting on anything the network owns. The request card is rendered
|
|
475
|
+
// from a poll, so awaiting here left the user clicking Accept with
|
|
476
|
+
// nothing happening on screen at all — the same class of bug as
|
|
477
|
+
// chat-send's, which this file already fixed for messages.
|
|
478
|
+
this.#pending.delete(uid);
|
|
479
|
+
try {
|
|
423
480
|
await this.#node.acceptFriendRequest(entry.pubkey);
|
|
424
|
-
|
|
481
|
+
}
|
|
482
|
+
catch (e) {
|
|
483
|
+
this.#pending.set(uid, entry); // refused: leave state as it was
|
|
484
|
+
throw e;
|
|
425
485
|
}
|
|
426
486
|
return {};
|
|
427
487
|
}
|
|
@@ -638,29 +698,27 @@ export class EmbeddedHost {
|
|
|
638
698
|
this.#events.emit("event", { type: "chat", userid, dir: "out" });
|
|
639
699
|
return { queued: true, name: safe, size: data.length };
|
|
640
700
|
}
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
// go through the normal sendFile below — peer >= 0.1.141 carries those to
|
|
644
|
-
// natives as DNFT1 frames inside friend messages (offset acks, FEC,
|
|
645
|
-
// resume; verified 100MB JS→iPad). Only route to inline what fits it.
|
|
701
|
+
// Everyone streams — a native friend's packets ride DNFT1 frames inside
|
|
702
|
+
// friend messages (see the note on the deleted native inline gate above).
|
|
646
703
|
const native = this.#node.isNativeFriend(userid);
|
|
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;
|
|
652
704
|
const msg = this.#messages.appendFile(userid, "out", { name: safe, size: data.length, status: "sending", sent: 0 });
|
|
653
705
|
if (!msg)
|
|
654
706
|
throw new Error("Could not create file message");
|
|
655
707
|
await mkdir(this.outboxDir, { recursive: true });
|
|
656
708
|
await writeFile(resolve(this.outboxDir, msg.id), data);
|
|
657
|
-
const fileId =
|
|
709
|
+
const fileId = this.#node.sendFile(userid, new Uint8Array(data), safe);
|
|
658
710
|
if (!fileId) {
|
|
659
|
-
//
|
|
660
|
-
|
|
711
|
+
// No transfer slot. The inline fallback only helps a JS friend (their
|
|
712
|
+
// text-ack makes it confirmable); to a native it is unconfirmable by
|
|
713
|
+
// construction, so fail honestly instead.
|
|
714
|
+
if (native) {
|
|
715
|
+
this.#messages.patchFile(userid, msg.id, { status: "failed" });
|
|
716
|
+
throw new Error("No free transfer slot — wait for a current transfer to finish and retry.");
|
|
717
|
+
}
|
|
718
|
+
if (data.length > INLINE_MAX_BYTES) {
|
|
661
719
|
this.#messages.patchFile(userid, msg.id, { status: "failed" });
|
|
662
720
|
throw new Error(`Could not start the transfer, and the file is too large (${(data.length / 1024 / 1024).toFixed(1)} MB) ` +
|
|
663
|
-
`for the inline fallback (limit ${(
|
|
721
|
+
`for the inline fallback (limit ${(INLINE_MAX_BYTES / 1024 / 1024).toFixed(1)} MB).`);
|
|
664
722
|
}
|
|
665
723
|
// "sent" means the PEER CONFIRMED receipt — nothing less (the fake-sent
|
|
666
724
|
// of INBOX 2026-08-21). And sends are NON-BLOCKING (the 2026-08-05
|
package/dist/message-store.d.ts
CHANGED
|
@@ -66,6 +66,9 @@ export declare class MessageStore {
|
|
|
66
66
|
/** Outgoing messages still awaiting delivery (peer was offline), oldest
|
|
67
67
|
* first — both queued text and queued file chips. The daemon drains this
|
|
68
68
|
* on a friend's reconnect. */
|
|
69
|
+
/** Every peer with outgoing mail still waiting, so the outbox sweep does
|
|
70
|
+
* not have to ask per friend. */
|
|
71
|
+
queuedPeers(): string[];
|
|
69
72
|
queuedOutgoing(peer: string): ChatMessage[];
|
|
70
73
|
/** Append a file-transfer entry (shown as a file chip in the UI). */
|
|
71
74
|
appendFile(peer: string, dir: "in" | "out", file: {
|
package/dist/message-store.js
CHANGED
|
@@ -83,6 +83,16 @@ export class MessageStore {
|
|
|
83
83
|
/** Outgoing messages still awaiting delivery (peer was offline), oldest
|
|
84
84
|
* first — both queued text and queued file chips. The daemon drains this
|
|
85
85
|
* on a friend's reconnect. */
|
|
86
|
+
/** Every peer with outgoing mail still waiting, so the outbox sweep does
|
|
87
|
+
* not have to ask per friend. */
|
|
88
|
+
queuedPeers() {
|
|
89
|
+
const out = [];
|
|
90
|
+
for (const [peer, arr] of this.byPeer) {
|
|
91
|
+
if (arr.some((m) => m.dir === "out" && (m.status === "queued" || m.file?.status === "queued")))
|
|
92
|
+
out.push(peer);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
86
96
|
queuedOutgoing(peer) {
|
|
87
97
|
const arr = this.byPeer.get(peer) ?? [];
|
|
88
98
|
return arr.filter((m) => m.dir === "out" && (m.status === "queued" || m.file?.status === "queued"));
|
package/dist/server.js
CHANGED
|
@@ -120,13 +120,43 @@ async function ensSignAndSet(call, record) {
|
|
|
120
120
|
// nftid=<punk id> on the beagles.eth name. Proxied server-side (browser can't
|
|
121
121
|
// reach the origin's port cross-origin reliably); NOTE the upstream's TLS cert
|
|
122
122
|
// must be valid — renew it when these routes start failing.
|
|
123
|
-
|
|
123
|
+
// api.beagle.chat/punksapi, NOT app.beagle.chat:1337. That name was repointed
|
|
124
|
+
// to GitHub Pages when the browser client took it over, so this default aimed
|
|
125
|
+
// at a host that serves nothing on 1337 and every punk avatar in the desktop
|
|
126
|
+
// app silently fell back to an identicon. The service itself never moved — it
|
|
127
|
+
// is the same box, now reached through Caddy on 443 instead of its own TLS
|
|
128
|
+
// listener, so there is no second certificate to keep alive.
|
|
129
|
+
// Endpoint of last resort. The real one comes from bgservers.json (punksApi)
|
|
130
|
+
// via punksBase() below, so the host can move without shipping a new build —
|
|
131
|
+
// which is the failure this whole comment exists because of: app.beagle.chat
|
|
132
|
+
// :1337 was baked in here, the name was repointed at GitHub Pages, and every
|
|
133
|
+
// punk avatar in the app quietly became an identicon.
|
|
134
|
+
const PUNKS_API_URL = process.env.PUNKS_API_URL || "https://api.beagle.chat/punksapi";
|
|
135
|
+
const BGSERVERS_URL = process.env.BGSERVERS_URL || "https://beagle.chat/assets/bgservers.json";
|
|
136
|
+
let punksBaseCache = null;
|
|
137
|
+
function punksBase() {
|
|
138
|
+
if (process.env.PUNKS_API_URL)
|
|
139
|
+
return Promise.resolve(process.env.PUNKS_API_URL);
|
|
140
|
+
if (!punksBaseCache) {
|
|
141
|
+
punksBaseCache = (async () => {
|
|
142
|
+
try {
|
|
143
|
+
const cfg = (await fetchDiscoverJson(BGSERVERS_URL));
|
|
144
|
+
const u = cfg?.punksApi?.[0]?.url;
|
|
145
|
+
if (typeof u === "string" && u)
|
|
146
|
+
return u.replace(/\/+$/, "");
|
|
147
|
+
}
|
|
148
|
+
catch { /* fall through to the built-in default */ }
|
|
149
|
+
return PUNKS_API_URL;
|
|
150
|
+
})();
|
|
151
|
+
}
|
|
152
|
+
return punksBaseCache;
|
|
153
|
+
}
|
|
124
154
|
const punkCache = new Map(); // punk id → JSON; immutable set
|
|
125
155
|
async function punksFetch(path) {
|
|
126
156
|
const ctl = new AbortController();
|
|
127
157
|
const timer = setTimeout(() => ctl.abort(), 10_000);
|
|
128
158
|
try {
|
|
129
|
-
const r = await fetch(`${
|
|
159
|
+
const r = await fetch(`${await punksBase()}${path}`, { signal: ctl.signal });
|
|
130
160
|
if (!r.ok)
|
|
131
161
|
throw new Error(`punks upstream ${r.status}`);
|
|
132
162
|
return await r.json();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.57",
|
|
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",
|