@decentnetwork/beagle 0.1.9 → 0.1.11
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 +24 -17
- package/dist/embedded-host.d.ts +2 -0
- package/dist/embedded-host.js +59 -26
- package/dist/server.js +10 -1
- package/package.json +1 -1
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.11";
|
|
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"/>',
|
|
@@ -509,12 +509,16 @@ function dkRtcSignalBus() {
|
|
|
509
509
|
}
|
|
510
510
|
}
|
|
511
511
|
async function pollLoop() {
|
|
512
|
+
let cursor = null;
|
|
512
513
|
while (!stopped) {
|
|
513
514
|
let signals = [];
|
|
514
515
|
try {
|
|
515
|
-
const
|
|
516
|
+
const u = cursor == null ? "/api/call-poll" : "/api/call-poll?since=" + cursor;
|
|
517
|
+
const r = await fetch(u, { headers: { "cache-control": "no-cache" } });
|
|
516
518
|
const d = await r.json();
|
|
517
519
|
signals = d && d.signals || [];
|
|
520
|
+
if (d && typeof d.cursor === "number")
|
|
521
|
+
cursor = d.cursor;
|
|
518
522
|
} catch (e) {
|
|
519
523
|
await new Promise((res) => setTimeout(res, 1e3));
|
|
520
524
|
continue;
|
|
@@ -3145,14 +3149,19 @@ function useCallController(selfId, onCallLog) {
|
|
|
3145
3149
|
alert("Could not start call: " + (e && e.message || e));
|
|
3146
3150
|
});
|
|
3147
3151
|
}, [waitEngine]);
|
|
3152
|
+
const incomingRef = React.useRef(null);
|
|
3153
|
+
React.useEffect(() => {
|
|
3154
|
+
incomingRef.current = incoming;
|
|
3155
|
+
}, [incoming]);
|
|
3156
|
+
const activeRef = React.useRef(null);
|
|
3157
|
+
React.useEffect(() => {
|
|
3158
|
+
activeRef.current = active;
|
|
3159
|
+
}, [active]);
|
|
3148
3160
|
const accept = React.useCallback(() => {
|
|
3149
|
-
|
|
3150
|
-
setIncoming((inc) => {
|
|
3151
|
-
pending = inc;
|
|
3152
|
-
return null;
|
|
3153
|
-
});
|
|
3161
|
+
const pending = incomingRef.current;
|
|
3154
3162
|
if (!pending)
|
|
3155
3163
|
return;
|
|
3164
|
+
setIncoming(null);
|
|
3156
3165
|
setActive({ callId: pending.callId, peerId: pending.peerId, video: pending.video, direction: "incoming", state: "connecting" });
|
|
3157
3166
|
waitEngine().then((eng) => {
|
|
3158
3167
|
if (!eng)
|
|
@@ -3162,19 +3171,17 @@ function useCallController(selfId, onCallLog) {
|
|
|
3162
3171
|
}, [waitEngine]);
|
|
3163
3172
|
const reject = React.useCallback(() => {
|
|
3164
3173
|
const eng = engineRef.current;
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
});
|
|
3174
|
+
const inc = incomingRef.current;
|
|
3175
|
+
if (eng && inc)
|
|
3176
|
+
eng.reject(inc.callId);
|
|
3177
|
+
setIncoming(null);
|
|
3170
3178
|
}, []);
|
|
3171
3179
|
const hangup = React.useCallback(() => {
|
|
3172
3180
|
const eng = engineRef.current;
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
});
|
|
3181
|
+
const a = activeRef.current;
|
|
3182
|
+
if (eng && a && a.callId)
|
|
3183
|
+
eng.hangup(a.callId);
|
|
3184
|
+
setActive(null);
|
|
3178
3185
|
setLocalStream(null);
|
|
3179
3186
|
setRemoteStream(null);
|
|
3180
3187
|
}, []);
|
package/dist/embedded-host.d.ts
CHANGED
|
@@ -36,4 +36,6 @@ export declare class EmbeddedHost implements PeerHost {
|
|
|
36
36
|
subscribe(emit: (event: Record<string, unknown>) => void): () => void;
|
|
37
37
|
stop(): Promise<void>;
|
|
38
38
|
call(req: IpcRequest): Promise<IpcResponse>;
|
|
39
|
+
/** Signals older than this have no business ringing a phone. */
|
|
40
|
+
static readonly CALL_SIGNAL_TTL_MS = 90000;
|
|
39
41
|
}
|
package/dist/embedded-host.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// What is deliberately NOT here, versus the daemon: virtual IPs, exits, dora,
|
|
9
9
|
// ACL, packet routing. Those are decentlan's job and need privilege beagle
|
|
10
10
|
// refuses to ask for.
|
|
11
|
+
var _a;
|
|
11
12
|
import { EventEmitter } from "node:events";
|
|
12
13
|
import { existsSync } from "node:fs";
|
|
13
14
|
import { mkdir, readFile, writeFile, unlink } from "node:fs/promises";
|
|
@@ -53,8 +54,19 @@ export class EmbeddedHost {
|
|
|
53
54
|
#sendChains = new Map();
|
|
54
55
|
/** Friend requests held for manual accept when auto-accept is off. */
|
|
55
56
|
#pending = new Map();
|
|
56
|
-
/** Inbound call signals
|
|
57
|
-
|
|
57
|
+
/** Inbound call signals as a broadcast ring: every poller reads at its own
|
|
58
|
+
* cursor, entries expire by AGE, never by being read. The previous shape —
|
|
59
|
+
* a queue drained by splice(0) — handed the whole batch to whichever
|
|
60
|
+
* poller arrived first, so with two tabs open an incoming call rang a
|
|
61
|
+
* random one and the tab the user was looking at stayed silent
|
|
62
|
+
* (INBOX 2026-08-05: "能打出去接不进来"). */
|
|
63
|
+
#callLog = [];
|
|
64
|
+
/** Seeded with the epoch so a cursor from a PREVIOUS process incarnation is
|
|
65
|
+
* always smaller than any new signal's seq. Seeded at 0, a restart made
|
|
66
|
+
* every open tab deaf: their old cursor (say 27) exceeded the fresh
|
|
67
|
+
* counter, so `seq > since` never matched and calls stopped ringing until
|
|
68
|
+
* a hard refresh. */
|
|
69
|
+
#callSeq = Date.now();
|
|
58
70
|
#callWaiters = [];
|
|
59
71
|
/** fileId -> which chat message it belongs to, so progress can patch it. */
|
|
60
72
|
#activeSends = new Map();
|
|
@@ -147,8 +159,14 @@ export class EmbeddedHost {
|
|
|
147
159
|
}
|
|
148
160
|
});
|
|
149
161
|
this.#node.on("call-signal", (evt) => {
|
|
150
|
-
this.#
|
|
151
|
-
|
|
162
|
+
this.#callLog.push({
|
|
163
|
+
seq: ++this.#callSeq,
|
|
164
|
+
ts: Date.now(),
|
|
165
|
+
userid: evt.pubkey,
|
|
166
|
+
data: Buffer.from(evt.data).toString("utf-8"),
|
|
167
|
+
});
|
|
168
|
+
this.#pruneCallLog();
|
|
169
|
+
// Wake every long-poll; each reads from its own cursor, nobody consumes.
|
|
152
170
|
const waiters = this.#callWaiters.splice(0);
|
|
153
171
|
for (const w of waiters)
|
|
154
172
|
w();
|
|
@@ -495,32 +513,46 @@ export class EmbeddedHost {
|
|
|
495
513
|
await this.#node.sendCallSignal(uid, String(req.text ?? req.data ?? ""));
|
|
496
514
|
return {};
|
|
497
515
|
case "call-poll":
|
|
498
|
-
return
|
|
516
|
+
return this.#pollCalls(typeof req.since === "number" ? req.since : undefined);
|
|
499
517
|
default:
|
|
500
518
|
throw new Error(`Unsupported op in embedded backend: ${req.op}`);
|
|
501
519
|
}
|
|
502
520
|
}
|
|
503
|
-
/**
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
521
|
+
/** Signals older than this have no business ringing a phone. */
|
|
522
|
+
static CALL_SIGNAL_TTL_MS = 90_000;
|
|
523
|
+
#pruneCallLog() {
|
|
524
|
+
const cutoff = Date.now() - _a.CALL_SIGNAL_TTL_MS;
|
|
525
|
+
while (this.#callLog.length && this.#callLog[0].ts < cutoff)
|
|
526
|
+
this.#callLog.shift();
|
|
527
|
+
}
|
|
528
|
+
/** Long-poll at a cursor: every subscriber sees every signal (broadcast),
|
|
529
|
+
* reading never consumes. `since` is the cursor from the previous
|
|
530
|
+
* response; a caller without one (older UI) starts at "now" and gets only
|
|
531
|
+
* future signals — exactly the old semantics minus the stealing. Returns
|
|
532
|
+
* immediately when the log has anything past the cursor, else holds ~20s. */
|
|
533
|
+
async #pollCalls(since) {
|
|
534
|
+
const from = since ?? this.#callSeq;
|
|
535
|
+
this.#pruneCallLog();
|
|
536
|
+
let avail = this.#callLog.filter((s) => s.seq > from);
|
|
537
|
+
if (!avail.length) {
|
|
538
|
+
await new Promise((res) => {
|
|
539
|
+
const done = () => {
|
|
540
|
+
clearTimeout(timer);
|
|
541
|
+
res();
|
|
542
|
+
};
|
|
543
|
+
const timer = setTimeout(() => {
|
|
544
|
+
const i = this.#callWaiters.indexOf(done);
|
|
545
|
+
if (i >= 0)
|
|
546
|
+
this.#callWaiters.splice(i, 1);
|
|
547
|
+
res();
|
|
548
|
+
}, 20_000);
|
|
549
|
+
timer.unref?.();
|
|
550
|
+
this.#callWaiters.push(done);
|
|
551
|
+
});
|
|
552
|
+
this.#pruneCallLog();
|
|
553
|
+
avail = this.#callLog.filter((s) => s.seq > from);
|
|
554
|
+
}
|
|
555
|
+
return { signals: avail.map(({ userid, data }) => ({ userid, data })), cursor: this.#callSeq };
|
|
524
556
|
}
|
|
525
557
|
async #fileSend(userid, path) {
|
|
526
558
|
const data = await readFile(path);
|
|
@@ -578,3 +610,4 @@ export class EmbeddedHost {
|
|
|
578
610
|
return { fileId, name: safe, size: data.length };
|
|
579
611
|
}
|
|
580
612
|
}
|
|
613
|
+
_a = EmbeddedHost;
|
package/dist/server.js
CHANGED
|
@@ -900,8 +900,17 @@ export function startBeagleServer(opts) {
|
|
|
900
900
|
// The daemon holds this up to ~20s (long-poll). opts.call's own IPC
|
|
901
901
|
// timeout is 30s, so it returns before that; on any error, resolve
|
|
902
902
|
// empty so the browser simply re-polls.
|
|
903
|
+
//
|
|
904
|
+
// `since` is the caller's broadcast cursor: signal delivery is
|
|
905
|
+
// per-subscriber, so two open tabs BOTH ring on an incoming call.
|
|
906
|
+
// Without it (older UI) the backend serves only future signals.
|
|
907
|
+
const sinceRaw = new URL(req.url || "/", "http://x").searchParams.get("since");
|
|
908
|
+
const since = sinceRaw !== null && sinceRaw !== "" ? Number(sinceRaw) : undefined;
|
|
903
909
|
try {
|
|
904
|
-
const r = await opts.call({
|
|
910
|
+
const r = await opts.call({
|
|
911
|
+
op: "call-poll",
|
|
912
|
+
...(Number.isFinite(since) ? { since } : {}),
|
|
913
|
+
});
|
|
905
914
|
sendJson(res, 200, r.ok ? r.data ?? { signals: [] } : { signals: [] });
|
|
906
915
|
}
|
|
907
916
|
catch {
|
package/package.json
CHANGED