@decentnetwork/beagle 0.1.56 → 0.1.58
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/cli.js +3 -0
- package/dist/desktop/app.js +38 -5
- package/dist/embedded-host.js +80 -13
- package/dist/message-store.d.ts +3 -0
- package/dist/message-store.js +10 -0
- package/dist/peer-host.d.ts +2 -0
- package/dist/peer-host.js +29 -1
- package/dist/server.js +32 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ function parseArgs(argv) {
|
|
|
28
28
|
" --config-dir <p> identity/config dir (default ~/.agentnet)",
|
|
29
29
|
" --dora-dir <p> dora roster dir, if this machine runs a dora",
|
|
30
30
|
" --backend <k> force 'daemon' or 'embedded' (default: auto)",
|
|
31
|
+
" --wait-daemon <s> with --backend daemon, wait this long for it (default 60)",
|
|
31
32
|
" -h, --help this text",
|
|
32
33
|
].join("\n"));
|
|
33
34
|
process.exit(0);
|
|
@@ -40,6 +41,7 @@ function parseArgs(argv) {
|
|
|
40
41
|
configDir: get("--config-dir") ?? defaultConfigDir(),
|
|
41
42
|
doraDir: get("--dora-dir"),
|
|
42
43
|
backend: get("--backend"),
|
|
44
|
+
waitDaemon: get("--wait-daemon"),
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
const readJsonVer = (file) => {
|
|
@@ -109,6 +111,7 @@ async function main() {
|
|
|
109
111
|
onAutoAcceptChange: (enabled) => saveAutoAccept(args.configDir, enabled),
|
|
110
112
|
peerVersion: resolveVer("@decentnetwork/peer"),
|
|
111
113
|
force: args.backend,
|
|
114
|
+
waitForDaemonMs: args.waitDaemon ? Number(args.waitDaemon) * 1000 : undefined,
|
|
112
115
|
}));
|
|
113
116
|
}
|
|
114
117
|
catch (error) {
|
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.58";
|
|
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
|
@@ -62,6 +62,7 @@ export class EmbeddedHost {
|
|
|
62
62
|
* order, so a new message can never overtake an older one whose ACK is
|
|
63
63
|
* still pending. Entry removed when the chain drains. */
|
|
64
64
|
#sendChains = new Map();
|
|
65
|
+
#sweepTimer = null;
|
|
65
66
|
/** Friend requests held for manual accept when auto-accept is off. */
|
|
66
67
|
#pending = new Map();
|
|
67
68
|
/** Inbound call signals as a broadcast ring: every poller reads at its own
|
|
@@ -143,6 +144,50 @@ export class EmbeddedHost {
|
|
|
143
144
|
await this.#node.start();
|
|
144
145
|
await this.#node.join();
|
|
145
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;
|
|
146
191
|
}
|
|
147
192
|
#wire() {
|
|
148
193
|
this.#node.on("message", (pubkey, text, via) => {
|
|
@@ -152,6 +197,9 @@ export class EmbeddedHost {
|
|
|
152
197
|
this.#messages.append(pubkey, "in", text, Date.now(), undefined, via);
|
|
153
198
|
this.#meta.ensure(pubkey);
|
|
154
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);
|
|
155
203
|
});
|
|
156
204
|
this.#node.on("friend-request", (req) => {
|
|
157
205
|
const userid = req.userid ?? req.pubkey;
|
|
@@ -179,15 +227,8 @@ export class EmbeddedHost {
|
|
|
179
227
|
// sat visibly stuck despite the peer being online. One delayed retry
|
|
180
228
|
// covers that gap; the per-send chain and later reconnects cover the
|
|
181
229
|
// rest.
|
|
182
|
-
if (e.status === "connected")
|
|
183
|
-
void this.#
|
|
184
|
-
.catch(() => { })
|
|
185
|
-
.then(() => {
|
|
186
|
-
if (this.#messages.queuedOutgoing(e.pubkey).length === 0)
|
|
187
|
-
return;
|
|
188
|
-
return new Promise((r) => setTimeout(r, 4000)).then(() => this.#flushOutbox(e.pubkey).catch(() => { }));
|
|
189
|
-
});
|
|
190
|
-
}
|
|
230
|
+
if (e.status === "connected")
|
|
231
|
+
void this.#chainFlush(e.pubkey); // fast path
|
|
191
232
|
});
|
|
192
233
|
this.#node.on("call-signal", (evt) => {
|
|
193
234
|
this.#callLog.push({
|
|
@@ -322,6 +363,10 @@ export class EmbeddedHost {
|
|
|
322
363
|
return () => this.#events.off("event", fn);
|
|
323
364
|
}
|
|
324
365
|
async stop() {
|
|
366
|
+
if (this.#sweepTimer) {
|
|
367
|
+
clearInterval(this.#sweepTimer);
|
|
368
|
+
this.#sweepTimer = null;
|
|
369
|
+
}
|
|
325
370
|
// Stop the peer FIRST, then release the lock. The reverse order would open
|
|
326
371
|
// a window where a daemon could start while our peer is still live.
|
|
327
372
|
await this.#node.stop();
|
|
@@ -405,16 +450,38 @@ export class EmbeddedHost {
|
|
|
405
450
|
}),
|
|
406
451
|
};
|
|
407
452
|
}
|
|
408
|
-
case "friend-request":
|
|
409
|
-
|
|
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}`));
|
|
410
465
|
return {};
|
|
466
|
+
}
|
|
411
467
|
case "friends-pending":
|
|
412
468
|
return { pending: [...this.#pending.values()] };
|
|
413
469
|
case "friends-accept": {
|
|
414
470
|
const entry = this.#pending.get(uid);
|
|
415
|
-
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 {
|
|
416
480
|
await this.#node.acceptFriendRequest(entry.pubkey);
|
|
417
|
-
|
|
481
|
+
}
|
|
482
|
+
catch (e) {
|
|
483
|
+
this.#pending.set(uid, entry); // refused: leave state as it was
|
|
484
|
+
throw e;
|
|
418
485
|
}
|
|
419
486
|
return {};
|
|
420
487
|
}
|
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/peer-host.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface OpenPeerHostOptions {
|
|
|
46
46
|
/** Force a backend instead of auto-detecting. Mainly for testing the
|
|
47
47
|
* embedded path on a machine that also runs a daemon. */
|
|
48
48
|
force?: BackendKind;
|
|
49
|
+
/** How long `force: "daemon"` waits for the daemon socket. Default 60s. */
|
|
50
|
+
waitForDaemonMs?: number;
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* Pick a backend.
|
package/dist/peer-host.js
CHANGED
|
@@ -77,7 +77,35 @@ export async function openPeerHost(opts) {
|
|
|
77
77
|
return { host: new DaemonHost(dataDir), why: `decentlan daemon at ${dataDir}` };
|
|
78
78
|
}
|
|
79
79
|
if (opts.force === "daemon") {
|
|
80
|
-
|
|
80
|
+
// Wait rather than die.
|
|
81
|
+
//
|
|
82
|
+
// A supervised beagle (pm2, systemd) routinely starts BEFORE the daemon it
|
|
83
|
+
// is meant to attach to. Throwing there turned a boot-order race into a
|
|
84
|
+
// permanent problem: beagle fell back to embedded, took <dataDir>/daemon.pid,
|
|
85
|
+
// and from then on `agentnet service restart` refused to start the daemon
|
|
86
|
+
// because a live process already held the identity — which is why upgrading
|
|
87
|
+
// needed a pm2 stop / restart / pm2 start dance instead of one command.
|
|
88
|
+
//
|
|
89
|
+
// Explicit --backend daemon means "this box's identity belongs to the
|
|
90
|
+
// daemon". Honour that by waiting for it to appear.
|
|
91
|
+
const waitMs = Math.max(0, opts.waitForDaemonMs ?? 60_000);
|
|
92
|
+
const deadline = Date.now() + waitMs;
|
|
93
|
+
let waited = false;
|
|
94
|
+
while (!daemonIsRunning(dataDir)) {
|
|
95
|
+
if (Date.now() >= deadline) {
|
|
96
|
+
throw new Error(`--backend daemon was requested but no decentlan daemon appeared within ${Math.round(waitMs / 1000)}s ` +
|
|
97
|
+
`(looked for ${ipcSocketPath(dataDir)}).`);
|
|
98
|
+
}
|
|
99
|
+
if (!waited) {
|
|
100
|
+
waited = true;
|
|
101
|
+
console.log(`waiting up to ${Math.round(waitMs / 1000)}s for the decentlan daemon at ${ipcSocketPath(dataDir)}…`);
|
|
102
|
+
}
|
|
103
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
host: new DaemonHost(dataDir),
|
|
107
|
+
why: waited ? `decentlan daemon at ${dataDir} (after waiting)` : `decentlan daemon at ${dataDir}`,
|
|
108
|
+
};
|
|
81
109
|
}
|
|
82
110
|
// Reuse decentlan's identity when it exists so friends carry over — this is
|
|
83
111
|
// the same node, not a new one. The daemon is not running, so nothing else
|
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