@ghostlytools/cli 1.3.1 → 1.3.3
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/README.md +6 -6
- package/dist/assets/{api-Dq2QXQDk.js → api-B1fADtqd.js} +65 -20
- package/dist/assets/api-B1fADtqd.js.map +1 -0
- package/dist/assets/{api-TiB5QSG9.js → api-Bg7bh_6E.js} +2 -2
- package/dist/assets/{apiKit-CA2x5GJF.js → apiKit-BsUZPHLn.js} +44 -33
- package/dist/assets/apiKit-BsUZPHLn.js.map +1 -0
- package/dist/assets/{apps-BxV9i_Y4.js → apps-BaUNt-5c.js} +91 -24
- package/dist/assets/apps-BaUNt-5c.js.map +1 -0
- package/dist/assets/{apps-C-T8jeji.js → apps-DepomCiX.js} +1 -1
- package/dist/assets/{arkade-C_qDmHnm.js → arkade-CNnMaoEQ.js} +4 -4
- package/dist/assets/arkade-CNnMaoEQ.js.map +1 -0
- package/dist/assets/{backup-Bugnn3Ue.js → backup-B5Q1W-Ey.js} +1 -1
- package/dist/assets/{backup-CnMN2oUK.js → backup-Bbz3lyC5.js} +6 -6
- package/dist/assets/{backup-CnMN2oUK.js.map → backup-Bbz3lyC5.js.map} +1 -1
- package/dist/assets/buttons-CUd7j0ZA.js +2 -0
- package/dist/assets/{buttons-CvNBzh2S.js → buttons-DS1iURp8.js} +4 -4
- package/dist/assets/{buttons-CvNBzh2S.js.map → buttons-DS1iURp8.js.map} +1 -1
- package/dist/assets/{envelope-WjingYSc.js → envelope-fNC941z4.js} +2 -2
- package/dist/assets/{envelope-WjingYSc.js.map → envelope-fNC941z4.js.map} +1 -1
- package/dist/assets/{errors-DO5z6h_l.js → errors-B-zMxbcd.js} +6 -5
- package/dist/assets/{errors-DO5z6h_l.js.map → errors-B-zMxbcd.js.map} +1 -1
- package/dist/assets/{ghostly_iroh_web-Dvzh3Y6E.js → ghostly_iroh_web-BsAEFIjl.js} +30 -36
- package/dist/assets/ghostly_iroh_web-BsAEFIjl.js.map +1 -0
- package/dist/assets/ghostly_iroh_web_bg.wasm +0 -0
- package/dist/assets/{groupFiles-IEUW3DQz.js → groupFiles-6qnLCnOm.js} +82 -29
- package/dist/assets/groupFiles-6qnLCnOm.js.map +1 -0
- package/dist/assets/{host-CWt-jtqz.js → host-Dj3C1fn3.js} +305 -68
- package/dist/assets/host-Dj3C1fn3.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-CPYwSLLS.js → hyperdhtRelay-CrgYQN-g.js} +2 -2
- package/dist/assets/{hyperdhtRelay-CPYwSLLS.js.map → hyperdhtRelay-CrgYQN-g.js.map} +1 -1
- package/dist/assets/{irohWeb-4oChIOIp.js → irohWeb-DSyLp5Re.js} +3 -3
- package/dist/assets/{irohWeb-4oChIOIp.js.map → irohWeb-DSyLp5Re.js.map} +1 -1
- package/dist/assets/{main-DbqcdShN.js → main-BDd11Yd1.js} +159 -54
- package/dist/assets/main-BDd11Yd1.js.map +1 -0
- package/dist/assets/{mainline-ZkiOnevs.js → mainline-CLTo0nGa.js} +2 -2
- package/dist/assets/{mainline-ZkiOnevs.js.map → mainline-CLTo0nGa.js.map} +1 -1
- package/dist/assets/{networkReason-CetKkTPo.js → networkReason-DwcadZz5.js} +2 -2
- package/dist/assets/{networkReason-CetKkTPo.js.map → networkReason-DwcadZz5.js.map} +1 -1
- package/dist/assets/{nwc-CgkqyvsE.js → nwc-BNFyyECH.js} +3 -3
- package/dist/assets/{nwc-CgkqyvsE.js.map → nwc-BNFyyECH.js.map} +1 -1
- package/dist/assets/{nwcProvider-4eVz_8HP.js → nwcProvider-BbjJOfLD.js} +2 -2
- package/dist/assets/{nwcProvider-4eVz_8HP.js.map → nwcProvider-BbjJOfLD.js.map} +1 -1
- package/dist/assets/{profiles-BHc9pEYg.js → profiles-gPCjLOQq.js} +2 -2
- package/dist/assets/{profiles-BHc9pEYg.js.map → profiles-gPCjLOQq.js.map} +1 -1
- package/dist/assets/{registry-DlAKO0Fy.js → registry-CoLHpxkb.js} +17 -32
- package/dist/assets/registry-CoLHpxkb.js.map +1 -0
- package/dist/assets/{registry-OBJBnHVQ.js → registry-DoTb6uEr.js} +1 -1
- package/dist/assets/{resolve-CouHhCpJ.js → resolve-BIDJ-UUl.js} +18 -3
- package/dist/assets/resolve-BIDJ-UUl.js.map +1 -0
- package/dist/assets/{server-Ss0gJ-nw.js → server-Bx3RGkBC.js} +380 -116
- package/dist/assets/server-Bx3RGkBC.js.map +1 -0
- package/dist/assets/{src-DDzSERyc.js → src-D7gcJElT.js} +470 -242
- package/dist/assets/src-D7gcJElT.js.map +1 -0
- package/dist/assets/{turnRefs-B87dtrHM.js → turnRefs-PN644M9o.js} +2 -2
- package/dist/assets/{turnRefs-B87dtrHM.js.map → turnRefs-PN644M9o.js.map} +1 -1
- package/dist/assets/{usdt-Fj8CQO2z.js → usdt-B2TLRlYa.js} +3 -3
- package/dist/assets/{usdt-Fj8CQO2z.js.map → usdt-B2TLRlYa.js.map} +1 -1
- package/dist/ghostly.mjs +1 -1
- package/package.json +1 -1
- package/dist/assets/api-Dq2QXQDk.js.map +0 -1
- package/dist/assets/apiKit-CA2x5GJF.js.map +0 -1
- package/dist/assets/apps-BxV9i_Y4.js.map +0 -1
- package/dist/assets/arkade-C_qDmHnm.js.map +0 -1
- package/dist/assets/buttons-Bj7Vf7Pn.js +0 -2
- package/dist/assets/ghostly_iroh_web-Dvzh3Y6E.js.map +0 -1
- package/dist/assets/groupFiles-IEUW3DQz.js.map +0 -1
- package/dist/assets/host-CWt-jtqz.js.map +0 -1
- package/dist/assets/main-DbqcdShN.js.map +0 -1
- package/dist/assets/registry-DlAKO0Fy.js.map +0 -1
- package/dist/assets/resolve-CouHhCpJ.js.map +0 -1
- package/dist/assets/server-Ss0gJ-nw.js.map +0 -1
- package/dist/assets/src-DDzSERyc.js.map +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as fedimintSdkOn, r as irohRelayProblem } from "./irohWeb-
|
|
3
|
-
import { r as asCliError, t as CliError } from "./errors-
|
|
4
|
-
import { a as findChat } from "./apiKit-
|
|
5
|
-
import { _ as flushCardUpdates, c as paymentJson, d as nodeLocalFetch, f as chatJson, g as resumeHolds, h as groupMessageJson, l as reportAppFrames, m as groupJson, p as chatMessageJson, r as callApi, u as servedApps } from "./api-
|
|
6
|
-
import { n as RESTORED_MARK, r as acquireLock } from "./profiles-
|
|
1
|
+
import { $o as extractParamsFromSdp, Ai as isPublicAddress, Jo as RTC_CONFIG, Qo as callSignalHeardAt, Xo as buildSdpFromSignal, Yo as answersOffer, Zo as callRtcConfig, es as parseCallSignal, oi as checkPushEndpoint, qo as CALL_SIGNAL_MAX_AGE_MS, tl as setLinkTraceSink, ts as signalHasVideo, xl as fromBase64Url } from "./src-D7gcJElT.js";
|
|
2
|
+
import { a as fedimintSdkOn, r as irohRelayProblem } from "./irohWeb-DSyLp5Re.js";
|
|
3
|
+
import { r as asCliError, t as CliError } from "./errors-B-zMxbcd.js";
|
|
4
|
+
import { a as findChat } from "./apiKit-BsUZPHLn.js";
|
|
5
|
+
import { _ as flushCardUpdates, c as paymentJson, d as nodeLocalFetch, f as chatJson, g as resumeHolds, h as groupMessageJson, l as reportAppFrames, m as groupJson, p as chatMessageJson, r as callApi, u as servedApps } from "./api-B1fADtqd.js";
|
|
6
|
+
import { n as RESTORED_MARK, r as acquireLock } from "./profiles-gPCjLOQq.js";
|
|
7
7
|
import { r as privateFolder, t as ownFolder } from "./privateFolder-Bk9C9oFv.js";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
|
-
import { appendFileSync, chmodSync, closeSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
9
|
+
import { appendFileSync, chmodSync, closeSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
10
10
|
import { readFile } from "node:fs/promises";
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
12
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -418,11 +418,13 @@ var CallAudio = class {
|
|
|
418
418
|
this.schedule();
|
|
419
419
|
}
|
|
420
420
|
stop() {
|
|
421
|
-
this.stopped = true;
|
|
422
421
|
if (this.timer) clearTimeout(this.timer);
|
|
423
422
|
this.timer = null;
|
|
424
|
-
this.
|
|
425
|
-
this.
|
|
423
|
+
if (this.stopped) return;
|
|
424
|
+
this.stopped = true;
|
|
425
|
+
for (const codec of [this.options.encoder, this.options.decoder]) try {
|
|
426
|
+
codec.delete?.();
|
|
427
|
+
} catch {}
|
|
426
428
|
}
|
|
427
429
|
/** Sends the frame that is due now. Exposed for tests, which drive the clock themselves. */
|
|
428
430
|
tick() {
|
|
@@ -551,6 +553,57 @@ var CallAudio = class {
|
|
|
551
553
|
//#endregion
|
|
552
554
|
//#region src/calls/media.ts
|
|
553
555
|
var loading = null;
|
|
556
|
+
var OPUS_APPLICATION_VOIP = 2048;
|
|
557
|
+
var OPUS_SET_BITRATE = 4002;
|
|
558
|
+
/** The largest packet a codec reads or writes (opusscript's MAX_PACKET_SIZE). */
|
|
559
|
+
var MAX_PACKET_BYTES = 3828;
|
|
560
|
+
/**
|
|
561
|
+
* Room for a frame of PCM as the module reads and writes it, one byte of PCM in each 16-bit element: a concealment (an
|
|
562
|
+
* empty packet) writes 120 ms at 48 kHz, 5,760 samples, 23,040 bytes, at any rate.
|
|
563
|
+
*/
|
|
564
|
+
var PCM_ROOM_BYTES = 23040;
|
|
565
|
+
/** The most PCM a decode hands back, as opusscript's: its largest frame, 60 ms at 48 kHz (CallAudio's `conceal`). */
|
|
566
|
+
var MAX_PCM_OUT = 5760;
|
|
567
|
+
/**
|
|
568
|
+
* An Opus encoder or decoder on opusscript's libopus, with buffers that hold what libopus writes. opusscript's own
|
|
569
|
+
* wrapper (0.1.1) keeps 5,760 bytes for its PCM and reaches it through views at twice the buffer's address, past it: a
|
|
570
|
+
* 20 ms frame at 48 kHz takes 3,840 bytes there and a concealment 23,040. With three calls or more, as calls ended and
|
|
571
|
+
* others started, one codec's audio landed on another's state: noise, then "memory access out of bounds" in every
|
|
572
|
+
* call until the daemon restarted. The bytes in and out are the same as opusscript's.
|
|
573
|
+
*/
|
|
574
|
+
function opusCodec(native, rate) {
|
|
575
|
+
const handler = new native.OpusScriptHandler(rate, 1, OPUS_APPLICATION_VOIP);
|
|
576
|
+
const pcm = native._malloc(PCM_ROOM_BYTES), packet = native._malloc(MAX_PACKET_BYTES);
|
|
577
|
+
let deleted = false;
|
|
578
|
+
const codec = {
|
|
579
|
+
encode(frame, frameSize) {
|
|
580
|
+
if (deleted || frame.length * 2 > PCM_ROOM_BYTES) throw new Error("Encode error: Bad argument");
|
|
581
|
+
native.HEAPU16.set(frame, pcm / 2);
|
|
582
|
+
const length = handler._encode(pcm, frame.length, packet, frameSize);
|
|
583
|
+
if (length < 0) throw new Error(`Encode error: ${length}`);
|
|
584
|
+
return Buffer.from(native.HEAPU8.subarray(packet, packet + length));
|
|
585
|
+
},
|
|
586
|
+
decode(data) {
|
|
587
|
+
if (deleted || data.length > MAX_PACKET_BYTES) throw new Error("Decode error: Bad argument");
|
|
588
|
+
native.HEAPU8.set(data, packet);
|
|
589
|
+
const samples = handler._decode(packet, data.length, pcm);
|
|
590
|
+
if (samples < 0) throw new Error(`Decode error: ${samples}`);
|
|
591
|
+
return Buffer.from(native.HEAPU16.subarray(pcm / 2, pcm / 2 + Math.min(samples * 2, MAX_PCM_OUT)));
|
|
592
|
+
},
|
|
593
|
+
delete() {
|
|
594
|
+
if (deleted) return;
|
|
595
|
+
deleted = true;
|
|
596
|
+
native.OpusScriptHandler.destroy_handler(handler);
|
|
597
|
+
native._free(pcm);
|
|
598
|
+
native._free(packet);
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
if (handler._encoder_ctl(OPUS_SET_BITRATE, 32e3) < 0) {
|
|
602
|
+
codec.delete?.();
|
|
603
|
+
throw new Error("Encoder CTL error");
|
|
604
|
+
}
|
|
605
|
+
return codec;
|
|
606
|
+
}
|
|
554
607
|
/** The media stack, or why calls are not available here (the reason the call buttons of the contact's app show). */
|
|
555
608
|
function loadCallStack() {
|
|
556
609
|
return loading ??= (async () => {
|
|
@@ -558,12 +611,8 @@ function loadCallStack() {
|
|
|
558
611
|
try {
|
|
559
612
|
const ndc = await import("node-datachannel");
|
|
560
613
|
const lib = ndc.PeerConnection ? ndc : ndc.default;
|
|
561
|
-
const
|
|
562
|
-
const opus = (rate) =>
|
|
563
|
-
const codec = new OpusScript(rate, 1, OpusScript.Application.VOIP);
|
|
564
|
-
codec.setBitrate(32e3);
|
|
565
|
-
return codec;
|
|
566
|
-
};
|
|
614
|
+
const native = createRequire(import.meta.url)("opusscript/build/opusscript_native_wasm.js")();
|
|
615
|
+
const opus = (rate) => opusCodec(native, rate);
|
|
567
616
|
opus(48e3).delete?.();
|
|
568
617
|
return {
|
|
569
618
|
ndc: lib,
|
|
@@ -612,7 +661,8 @@ function describePair(pair) {
|
|
|
612
661
|
var refuseAnswers = Number(process.env.GHOSTLY_CALL_REFUSE_ANSWERS) || 0;
|
|
613
662
|
/** How long a description waits for its candidates: a server reflexive one and a little after it, or this long. */
|
|
614
663
|
var GATHER_MS = 5e3;
|
|
615
|
-
|
|
664
|
+
/** The signal keeps the first server reflexive candidate only: the apps' calls and the chats' data link settle as long. */
|
|
665
|
+
var SETTLE_MS = 150;
|
|
616
666
|
/**
|
|
617
667
|
* With a candidate in (a host one comes in milliseconds), the server reflexive one is waited for this long at most,
|
|
618
668
|
* not the whole of `GATHER_MS`: a STUN server that answers does in tens of milliseconds, and one that never does (UDP
|
|
@@ -629,8 +679,9 @@ var HOST_GATHER_MS = 2e3;
|
|
|
629
679
|
async function openConnection(stack, options, offer, held = false) {
|
|
630
680
|
const { ndc } = stack;
|
|
631
681
|
const bind = process.env.GHOSTLY_CALL_BIND;
|
|
682
|
+
const iceServers = callIceServers(options.iceServers);
|
|
632
683
|
const pc = new ndc.PeerConnection("ghostly-call", {
|
|
633
|
-
iceServers
|
|
684
|
+
iceServers,
|
|
634
685
|
...bind ? { bindAddress: bind } : {}
|
|
635
686
|
});
|
|
636
687
|
const born = Date.now();
|
|
@@ -674,7 +725,7 @@ async function openConnection(stack, options, offer, held = false) {
|
|
|
674
725
|
videoTrack = pc.addTrack(video);
|
|
675
726
|
videoTrack.onMessage(() => {});
|
|
676
727
|
}
|
|
677
|
-
const gathered = gathering(pc);
|
|
728
|
+
const gathered = gathering(pc, { turn: callUsesTurn(iceServers) });
|
|
678
729
|
if (offer) pc.setRemoteDescription(buildSdpFromSignal(held ? {
|
|
679
730
|
...offer,
|
|
680
731
|
c: []
|
|
@@ -864,8 +915,18 @@ function mdnsNote(signal, log) {
|
|
|
864
915
|
lookup(host, { all: true }).then((found) => log(`${host} resolves here to ${found.map((a) => a.address).join(", ")}`), () => log(`${host} does not resolve here: the call connects only if the contact's checks reach this side`));
|
|
865
916
|
}
|
|
866
917
|
}
|
|
867
|
-
/**
|
|
868
|
-
function
|
|
918
|
+
/** Whether a call's ICE servers (`callIceServers`) have a TURN server: its relay candidate is then the one waited for. */
|
|
919
|
+
function callUsesTurn(servers) {
|
|
920
|
+
return servers.some((server) => typeof server !== "string");
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Resolves once the local description has its candidates (see GATHER_MS and HOST_GATHER_MS). `turn`: the connection
|
|
924
|
+
* has a TURN server, and its relay candidate is waited for rather than the reflexive one, as the apps' calls do
|
|
925
|
+
* (`waitForIceGathering`): an allocation takes two round trips, and a TURN server 100 ms away gave its relay after the
|
|
926
|
+
* description had gone without it, on the networks where only the relay connects.
|
|
927
|
+
*/
|
|
928
|
+
function gathering(pc, { turn = false } = {}) {
|
|
929
|
+
const wanted = turn ? / typ relay/ : / typ srflx/;
|
|
869
930
|
return new Promise((resolve) => {
|
|
870
931
|
let settle = null;
|
|
871
932
|
let any = false;
|
|
@@ -884,7 +945,7 @@ function gathering(pc) {
|
|
|
884
945
|
});
|
|
885
946
|
pc.onLocalCandidate((candidate) => {
|
|
886
947
|
any = true;
|
|
887
|
-
if (!settle &&
|
|
948
|
+
if (!settle && wanted.test(candidate)) settle = setTimeout(done, SETTLE_MS);
|
|
888
949
|
});
|
|
889
950
|
});
|
|
890
951
|
}
|
|
@@ -915,6 +976,8 @@ var CallManager = class {
|
|
|
915
976
|
clearing = /* @__PURE__ */ new Map();
|
|
916
977
|
/** Per chat: when this side's last call there ended before it was answered (`CALL_PACE_MS`). */
|
|
917
978
|
unansweredAt = /* @__PURE__ */ new Map();
|
|
979
|
+
/** The early answers being made, those of ended calls too (MAX_EARLY_ANSWERS). */
|
|
980
|
+
makings = /* @__PURE__ */ new Set();
|
|
918
981
|
auto;
|
|
919
982
|
configPath;
|
|
920
983
|
stopping = false;
|
|
@@ -1087,11 +1150,15 @@ var CallManager = class {
|
|
|
1087
1150
|
*/
|
|
1088
1151
|
prepare(call) {
|
|
1089
1152
|
if (this.host.answers === false || call.ended || call.state !== "ringing") return;
|
|
1153
|
+
this.stopEarly(call);
|
|
1090
1154
|
call.earlyTimer = setTimeout(() => this.prepare(call), this.host.earlyAnswerMs ?? 3e4);
|
|
1091
1155
|
call.earlyTimer.unref?.();
|
|
1092
1156
|
if (call.making) return;
|
|
1157
|
+
const held = [...this.calls.values()].filter((c) => c !== call && c.early).length;
|
|
1158
|
+
if (this.makings.size + held >= 4 || [...this.makings].some((m) => m.chat === call.chat)) return;
|
|
1093
1159
|
const offer = call.offer;
|
|
1094
1160
|
const making = {
|
|
1161
|
+
chat: call.chat,
|
|
1095
1162
|
done: this.stack().then((stack) => EarlyAnswer.open(stack, offer, {
|
|
1096
1163
|
iceServers: this.host.engine.getState().settings?.iceServers ?? [],
|
|
1097
1164
|
log: (line) => process.stderr.write(`ghostly: call ${call.id}: ${line}\n`)
|
|
@@ -1103,15 +1170,19 @@ var CallManager = class {
|
|
|
1103
1170
|
claimed: false
|
|
1104
1171
|
};
|
|
1105
1172
|
call.making = making;
|
|
1173
|
+
this.makings.add(making);
|
|
1106
1174
|
making.done.then((early) => {
|
|
1175
|
+
this.makings.delete(making);
|
|
1107
1176
|
if (call.making === making) call.making = null;
|
|
1108
|
-
if (making.claimed
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1177
|
+
if (!making.claimed && early) {
|
|
1178
|
+
if (call.ended || call.state !== "ringing") early.close();
|
|
1179
|
+
else {
|
|
1180
|
+
call.early?.close();
|
|
1181
|
+
call.early = early;
|
|
1182
|
+
}
|
|
1112
1183
|
}
|
|
1113
|
-
|
|
1114
|
-
call.
|
|
1184
|
+
const next = this.byChat(making.chat);
|
|
1185
|
+
if (next && next !== call && next.direction === "in" && !next.making && !next.early) this.prepare(next);
|
|
1115
1186
|
});
|
|
1116
1187
|
}
|
|
1117
1188
|
/** The answered call's early answer: the one made, if still good, or the one being made; none from then on. */
|
|
@@ -1403,7 +1474,11 @@ var CallManager = class {
|
|
|
1403
1474
|
this.calls.delete(call.id);
|
|
1404
1475
|
if (call.direction === "out" && call.connectedAt === null && call.state === "ringing" && (reason === "hangup" || reason === "unanswered")) this.unansweredAt.set(call.chat, this.now);
|
|
1405
1476
|
if (report) process.stderr.write(`ghostly: call ${call.id}: ended (${reason})${call.media ? `, ice ${call.media.ice.state}` : ""}\n`);
|
|
1406
|
-
|
|
1477
|
+
try {
|
|
1478
|
+
call.media?.close();
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
process.stderr.write(`ghostly: call ${call.id}: closing its media failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1481
|
+
}
|
|
1407
1482
|
this.stopEarly(call);
|
|
1408
1483
|
call.early?.close();
|
|
1409
1484
|
call.early = null;
|
|
@@ -1562,6 +1637,8 @@ function readAuto(path) {
|
|
|
1562
1637
|
return off;
|
|
1563
1638
|
}
|
|
1564
1639
|
}
|
|
1640
|
+
/** A journal past this is trimmed, and only this much of its end is ever read. */
|
|
1641
|
+
var JOURNAL_MAX_BYTES = 33554432;
|
|
1565
1642
|
var SEEN_DB = "ghostly-cli";
|
|
1566
1643
|
var SEEN = "seen";
|
|
1567
1644
|
/**
|
|
@@ -1587,6 +1664,7 @@ var EventHub = class {
|
|
|
1587
1664
|
seenDb;
|
|
1588
1665
|
seq = 0;
|
|
1589
1666
|
journalLines = 0;
|
|
1667
|
+
journalBytes = 0;
|
|
1590
1668
|
listeners = /* @__PURE__ */ new Set();
|
|
1591
1669
|
stateListeners = /* @__PURE__ */ new Set();
|
|
1592
1670
|
callListeners = /* @__PURE__ */ new Set();
|
|
@@ -1623,13 +1701,24 @@ var EventHub = class {
|
|
|
1623
1701
|
}
|
|
1624
1702
|
/** Loads the journal's last seq and the seen set; call before `sink` is attached. */
|
|
1625
1703
|
async open() {
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
this.
|
|
1631
|
-
|
|
1704
|
+
try {
|
|
1705
|
+
if (existsSync(this.journalPath)) {
|
|
1706
|
+
const { lines, size } = journalTail(this.journalPath);
|
|
1707
|
+
this.journalLines = lines.length;
|
|
1708
|
+
this.journalBytes = size;
|
|
1709
|
+
for (let i = lines.length - 1; i >= 0; i--) try {
|
|
1710
|
+
this.seq = JSON.parse(lines[i]).seq;
|
|
1711
|
+
break;
|
|
1712
|
+
} catch {}
|
|
1713
|
+
if (size > 33554432 || lines.length > 2e4) this.trimJournal();
|
|
1714
|
+
}
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
process.stderr.write(`ghostly: the event journal could not be read, a new one begins: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1717
|
+
try {
|
|
1718
|
+
renameSync(this.journalPath, this.journalPath + ".unreadable");
|
|
1632
1719
|
} catch {}
|
|
1720
|
+
this.journalLines = 0;
|
|
1721
|
+
this.journalBytes = 0;
|
|
1633
1722
|
}
|
|
1634
1723
|
this.db = await new Promise((resolve, reject) => {
|
|
1635
1724
|
const request = indexedDB.open(this.seenDb, 1);
|
|
@@ -1754,13 +1843,10 @@ var EventHub = class {
|
|
|
1754
1843
|
replay(since) {
|
|
1755
1844
|
if (!existsSync(this.journalPath) || since >= this.seq) return [];
|
|
1756
1845
|
const out = [];
|
|
1757
|
-
for (const line of
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
if (event.seq > since) out.push(event);
|
|
1762
|
-
} catch {}
|
|
1763
|
-
}
|
|
1846
|
+
for (const line of journalTail(this.journalPath).lines) try {
|
|
1847
|
+
const event = JSON.parse(line);
|
|
1848
|
+
if (event.seq > since) out.push(event);
|
|
1849
|
+
} catch {}
|
|
1764
1850
|
if (out.length && out[0].seq > since + 1) out.unshift({
|
|
1765
1851
|
seq: since,
|
|
1766
1852
|
id: `events.gap:${since}:${out[0].seq}`,
|
|
@@ -1783,18 +1869,57 @@ var EventHub = class {
|
|
|
1783
1869
|
at: this.now(),
|
|
1784
1870
|
...fields
|
|
1785
1871
|
};
|
|
1786
|
-
|
|
1787
|
-
|
|
1872
|
+
try {
|
|
1873
|
+
const line = JSON.stringify(event) + "\n";
|
|
1874
|
+
appendFileSync(this.journalPath, line, { mode: 384 });
|
|
1875
|
+
this.journalBytes += Buffer.byteLength(line);
|
|
1876
|
+
if (++this.journalLines > 2e4 || this.journalBytes > 33554432) this.trimJournal();
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
process.stderr.write(`ghostly: event journal error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1879
|
+
}
|
|
1788
1880
|
for (const listener of this.listeners) try {
|
|
1789
1881
|
listener(event);
|
|
1790
1882
|
} catch {}
|
|
1791
1883
|
return event;
|
|
1792
1884
|
}
|
|
1885
|
+
/** Keeps the last `JOURNAL_KEEP` events, and of them no more than `JOURNAL_KEEP_BYTES`. */
|
|
1793
1886
|
trimJournal() {
|
|
1794
|
-
const lines =
|
|
1795
|
-
|
|
1887
|
+
const lines = journalTail(this.journalPath).lines.slice(-1e4);
|
|
1888
|
+
let start = lines.length, bytes = 0;
|
|
1889
|
+
while (start > 0 && bytes + Buffer.byteLength(lines[start - 1]) + 1 <= 16777216) bytes += Buffer.byteLength(lines[--start]) + 1;
|
|
1890
|
+
const kept = lines.slice(start);
|
|
1891
|
+
writeFileSync(this.journalPath + ".tmp", kept.length ? kept.join("\n") + "\n" : "", { mode: 384 });
|
|
1796
1892
|
renameSync(this.journalPath + ".tmp", this.journalPath);
|
|
1797
|
-
this.journalLines =
|
|
1893
|
+
this.journalLines = kept.length;
|
|
1894
|
+
this.journalBytes = bytes;
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* What was removed here leaves the journal too: the journal is written again without the events that name it (a
|
|
1898
|
+
* line that names it and cannot be read goes as well), and a journal set aside as unreadable, which cannot be sorted,
|
|
1899
|
+
* is deleted. `ids`: what was removed (a message's id, a chat's or a group's); `gone`: whether an event is of it.
|
|
1900
|
+
*/
|
|
1901
|
+
forget(ids, gone) {
|
|
1902
|
+
if (!ids.length) return;
|
|
1903
|
+
try {
|
|
1904
|
+
rmSync(this.journalPath + ".unreadable", { force: true });
|
|
1905
|
+
if (!existsSync(this.journalPath)) return;
|
|
1906
|
+
const lines = journalTail(this.journalPath).lines;
|
|
1907
|
+
const kept = lines.filter((line) => {
|
|
1908
|
+
if (!ids.some((id) => line.includes(JSON.stringify(id)))) return true;
|
|
1909
|
+
try {
|
|
1910
|
+
return !gone(JSON.parse(line));
|
|
1911
|
+
} catch {
|
|
1912
|
+
return false;
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
if (kept.length === lines.length) return;
|
|
1916
|
+
writeFileSync(this.journalPath + ".tmp", kept.length ? kept.join("\n") + "\n" : "", { mode: 384 });
|
|
1917
|
+
renameSync(this.journalPath + ".tmp", this.journalPath);
|
|
1918
|
+
this.journalLines = kept.length;
|
|
1919
|
+
this.journalBytes = kept.reduce((bytes, line) => bytes + Buffer.byteLength(line) + 1, 0);
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
process.stderr.write(`ghostly: event journal error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1922
|
+
}
|
|
1798
1923
|
}
|
|
1799
1924
|
handle(message) {
|
|
1800
1925
|
switch (message.kind) {
|
|
@@ -1871,11 +1996,12 @@ var EventHub = class {
|
|
|
1871
1996
|
...typingFields(shape)
|
|
1872
1997
|
});
|
|
1873
1998
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1999
|
+
const removedChats = [...this.chats.keys()].filter((id) => !links.has(id));
|
|
2000
|
+
for (const id of removedChats) {
|
|
1876
2001
|
this.chats.delete(id);
|
|
1877
2002
|
if (!quiet) this.emit("chat.removed", `chat.removed:${id}`, { chat: id });
|
|
1878
2003
|
}
|
|
2004
|
+
this.forget(removedChats, (event) => removedChats.includes(event.chat) && event.type !== "chat.removed");
|
|
1879
2005
|
const groups = new Map(state.groups.map((group) => [group.id, group]));
|
|
1880
2006
|
for (const [id, group] of groups) {
|
|
1881
2007
|
const shape = groupShape(group), before = this.groups.get(id);
|
|
@@ -1914,11 +2040,12 @@ var EventHub = class {
|
|
|
1914
2040
|
member
|
|
1915
2041
|
});
|
|
1916
2042
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
2043
|
+
const removedGroups = [...this.groups.keys()].filter((id) => !groups.has(id));
|
|
2044
|
+
for (const id of removedGroups) {
|
|
1919
2045
|
this.groups.delete(id);
|
|
1920
2046
|
if (!quiet) this.emit("group.removed", `group.removed:${id}`, { group: id });
|
|
1921
2047
|
}
|
|
2048
|
+
this.forget(removedGroups, (event) => removedGroups.includes(event.group) && event.type !== "group.removed");
|
|
1922
2049
|
for (const [id, payment] of Object.entries(state.payments ?? {})) {
|
|
1923
2050
|
const shape = `${payment.state}|${payment.error ?? ""}|${payment.closed ? 1 : 0}`, before = this.payments.get(id);
|
|
1924
2051
|
this.payments.set(id, shape);
|
|
@@ -2173,10 +2300,12 @@ var EventHub = class {
|
|
|
2173
2300
|
});
|
|
2174
2301
|
}
|
|
2175
2302
|
}
|
|
2303
|
+
const gone = [];
|
|
2176
2304
|
for (const id of deleted ?? [...known.keys()]) {
|
|
2177
2305
|
if (present.has(id) || !known.has(id)) continue;
|
|
2178
2306
|
known.delete(id);
|
|
2179
2307
|
changes.push([id, null]);
|
|
2308
|
+
gone.push(id);
|
|
2180
2309
|
if (!quiet) this.emit(group ? "group.deleted" : "message.deleted", `${group ? "group" : "message"}.deleted:${group ?? chat}:${id}`, group ? {
|
|
2181
2310
|
group,
|
|
2182
2311
|
messageId: id
|
|
@@ -2186,6 +2315,8 @@ var EventHub = class {
|
|
|
2186
2315
|
});
|
|
2187
2316
|
}
|
|
2188
2317
|
if (changes.length) this.persist(chat, changes);
|
|
2318
|
+
const whole = !!group && !deleted && messages.length === 0 && gone.length > 0;
|
|
2319
|
+
this.forget(whole ? [group] : gone, (event) => (group ? event.group === group : event.chat === chat) && !event.type.endsWith(".deleted") && (whole ? "message" in event || "messageId" in event || "replyId" in event : gone.includes(event.messageId) || gone.includes(event.message?.id) || gone.includes(event.replyId)));
|
|
2189
2320
|
for (const [fileId, messageId] of files) this.flushFile(fileId, messageId);
|
|
2190
2321
|
}
|
|
2191
2322
|
/**
|
|
@@ -2352,6 +2483,71 @@ function groupShape(group) {
|
|
|
2352
2483
|
pin: group.pin ?? null
|
|
2353
2484
|
};
|
|
2354
2485
|
}
|
|
2486
|
+
/**
|
|
2487
|
+
* The journal's whole lines within its last `JOURNAL_MAX_BYTES`, and its size: never the whole file as one string, so
|
|
2488
|
+
* a journal grown past what a string can hold (or a trim's bound) is still read, and in bounded time.
|
|
2489
|
+
*/
|
|
2490
|
+
function journalTail(path) {
|
|
2491
|
+
const fd = openSync(path, "r");
|
|
2492
|
+
try {
|
|
2493
|
+
const size = fstatSync(fd).size;
|
|
2494
|
+
const from = Math.max(0, size - JOURNAL_MAX_BYTES - 1);
|
|
2495
|
+
const buffer = Buffer.alloc(size - from);
|
|
2496
|
+
let read = 0;
|
|
2497
|
+
while (read < buffer.length) {
|
|
2498
|
+
const n = readSync(fd, buffer, read, buffer.length - read, from + read);
|
|
2499
|
+
if (!n) break;
|
|
2500
|
+
read += n;
|
|
2501
|
+
}
|
|
2502
|
+
const lines = buffer.toString("utf8", 0, read).split("\n");
|
|
2503
|
+
if (from > 0) lines.shift();
|
|
2504
|
+
return {
|
|
2505
|
+
lines: lines.filter(Boolean),
|
|
2506
|
+
size
|
|
2507
|
+
};
|
|
2508
|
+
} finally {
|
|
2509
|
+
closeSync(fd);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
//#endregion
|
|
2513
|
+
//#region src/joinLog.ts
|
|
2514
|
+
/**
|
|
2515
|
+
* A join through a group's link, as a daemon says it in its log (stderr): the steps of the knock and of the entry
|
|
2516
|
+
* session, one short line each, from the engine's join trace (packages/browser/src/engine/joinTrace.ts). Without them a
|
|
2517
|
+
* join that stalled left nothing in the admin's log to tell a knock never seen from a session that never opened
|
|
2518
|
+
* (2026-10-16). The reads of the knocks, every few seconds, and an edge's own steps are not said.
|
|
2519
|
+
*/
|
|
2520
|
+
var SAID = {
|
|
2521
|
+
"join.start": "joining through its link",
|
|
2522
|
+
"knock.published": "knock left",
|
|
2523
|
+
"knock.seen": "a knock seen: opening an entry session",
|
|
2524
|
+
"link.start": "entry session starting",
|
|
2525
|
+
"link.presence": "entry session: the other side's packet seen",
|
|
2526
|
+
"link.offering": "entry session: offering a connection",
|
|
2527
|
+
"link.answering": "entry session: answering a connection",
|
|
2528
|
+
"link.connecting": "entry session: connecting",
|
|
2529
|
+
"link.open": "entry session open",
|
|
2530
|
+
"link.idle": "entry session not open",
|
|
2531
|
+
"link.native": "entry session: WebRTC connected nothing, going on over a native transport",
|
|
2532
|
+
"invite.sent": "invitation sent on the entry session",
|
|
2533
|
+
"invite.received": "invitation received on the entry session: accepting",
|
|
2534
|
+
"welcome.sent": "welcome sent",
|
|
2535
|
+
"welcome.received": "welcome received: in the group"
|
|
2536
|
+
};
|
|
2537
|
+
/** The log line for one join-trace line, or null when it is not said. */
|
|
2538
|
+
function joinLogLine(traced) {
|
|
2539
|
+
let step;
|
|
2540
|
+
try {
|
|
2541
|
+
step = JSON.parse(traced);
|
|
2542
|
+
} catch {
|
|
2543
|
+
return null;
|
|
2544
|
+
}
|
|
2545
|
+
if (typeof step.step !== "string" || typeof step.g !== "string" || !Object.hasOwn(SAID, step.step)) return null;
|
|
2546
|
+
const role = step.role === "host" || step.role === "guest" ? step.role : void 0;
|
|
2547
|
+
if (step.step.startsWith("link.") && !role) return null;
|
|
2548
|
+
const at = typeof step.t === "number" ? ` at ${new Date(step.t).toISOString()}` : "";
|
|
2549
|
+
return `ghostly: group ${step.g}: ${SAID[step.step]}${role ? ` (${role === "host" ? "letting in" : "joining"})` : ""}${at}`;
|
|
2550
|
+
}
|
|
2355
2551
|
//#endregion
|
|
2356
2552
|
//#region src/runtime/fedimint.ts
|
|
2357
2553
|
/**
|
|
@@ -4354,10 +4550,12 @@ async function startRuntime(paths, options = {}) {
|
|
|
4354
4550
|
const pinned = pkarrRelays();
|
|
4355
4551
|
const pinnedIroh = irohRelays();
|
|
4356
4552
|
const trace = process.env.GHOSTLY_LINK_TRACE;
|
|
4357
|
-
if (trace) {
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4553
|
+
if (trace) setLinkTraceSink((line) => appendFileSync(trace, line + "\n", { mode: 384 }));
|
|
4554
|
+
if (trace || options.logJoins) (await import("./joinTrace-D-fnt2p3.js")).setJoinTraceSink((line) => {
|
|
4555
|
+
if (trace) appendFileSync(trace, line + "\n", { mode: 384 });
|
|
4556
|
+
const said = options.logJoins ? joinLogLine(line) : null;
|
|
4557
|
+
if (said) process.stderr.write(said + "\n");
|
|
4558
|
+
});
|
|
4361
4559
|
const store = await openPersistentIndexedDb(paths.db);
|
|
4362
4560
|
const restored = join(paths.dir, RESTORED_MARK);
|
|
4363
4561
|
if (existsSync(restored)) {
|
|
@@ -4380,17 +4578,17 @@ async function startRuntime(paths, options = {}) {
|
|
|
4380
4578
|
if (testMints) (await import("./mints-CqTdA4Gk.js")).declareTestMints(testMints.split(","));
|
|
4381
4579
|
await installEventSource();
|
|
4382
4580
|
await preloadWalletModules();
|
|
4383
|
-
(await import("./ghostly_iroh_web-
|
|
4581
|
+
(await import("./ghostly_iroh_web-BsAEFIjl.js")).initSync({ module: irohWasmBytes() });
|
|
4384
4582
|
const [{ registerFileBytes }, { NodeFileBytes }] = await Promise.all([import("./fileBytes-CgbibpbV.js"), import("./fileBytes-CfSXjTFa.js")]);
|
|
4385
4583
|
registerFileBytes("native", async () => new NodeFileBytes(paths.files), true);
|
|
4386
|
-
const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./server-
|
|
4584
|
+
const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./server-Bx3RGkBC.js"), import("./endpoint-BOTFMpfS.js")]);
|
|
4387
4585
|
const network = hyperdhtNetwork();
|
|
4388
4586
|
const dht = mainlineNetwork();
|
|
4389
|
-
const mainline = dht.off ? null : new (await (import("./mainline-
|
|
4587
|
+
const mainline = dht.off ? null : new (await (import("./mainline-CLTo0nGa.js"))).Mainline({
|
|
4390
4588
|
bootstrap: dht.bootstrap,
|
|
4391
4589
|
host: dht.host
|
|
4392
4590
|
});
|
|
4393
|
-
const transport = mainline || pinned ? new (await (import("./mainline-
|
|
4591
|
+
const transport = mainline || pinned ? new (await (import("./mainline-CLTo0nGa.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
|
|
4394
4592
|
const apps = servedApps(paths.dir).length > 0;
|
|
4395
4593
|
const server = new EngineServer({
|
|
4396
4594
|
...transport ? { transport } : {},
|
|
@@ -4438,6 +4636,13 @@ async function startRuntime(paths, options = {}) {
|
|
|
4438
4636
|
/** The longest request line the daemon reads (WISP 1100 § Framing). */
|
|
4439
4637
|
var MAX_LINE = 16777216;
|
|
4440
4638
|
/**
|
|
4639
|
+
* The most a connection may leave unread (answers and events the daemon wrote that its client has not taken): past
|
|
4640
|
+
* it, the connection ends. A `listen` stopped with Ctrl+Z, or a client that stops reading, kept every later event in
|
|
4641
|
+
* the daemon's memory until it read again (155 MB after 6,000 long texts); `listen` reconnects after the last seq it
|
|
4642
|
+
* took, and the journal still has the rest. A replay is the journal's size at most and does not count against it.
|
|
4643
|
+
*/
|
|
4644
|
+
var MAX_UNREAD = 8388608;
|
|
4645
|
+
/**
|
|
4441
4646
|
* How many of a chat's newest messages the start reads to see whether the events are in step with its history: the
|
|
4442
4647
|
* whole history of every chat and group, read at every start, took a daemon's start and its memory with every message
|
|
4443
4648
|
* the profile ever stored (1 s and 70 MB at 100,000).
|
|
@@ -4452,7 +4657,10 @@ async function openHost(paths, mode, version, options = {}) {
|
|
|
4452
4657
|
const release = acquireLock(paths);
|
|
4453
4658
|
let runtime;
|
|
4454
4659
|
try {
|
|
4455
|
-
runtime = await startRuntime(paths,
|
|
4660
|
+
runtime = await startRuntime(paths, mode === "daemon" ? {
|
|
4661
|
+
...options,
|
|
4662
|
+
logJoins: true
|
|
4663
|
+
} : options);
|
|
4456
4664
|
} catch (error) {
|
|
4457
4665
|
release();
|
|
4458
4666
|
throw error;
|
|
@@ -4596,12 +4804,23 @@ function ownerOnlyUmask() {
|
|
|
4596
4804
|
}
|
|
4597
4805
|
function connection(ctx, socket) {
|
|
4598
4806
|
const unsubscribes = [];
|
|
4599
|
-
|
|
4600
|
-
|
|
4807
|
+
let limit = MAX_UNREAD;
|
|
4808
|
+
const unsubscribe = () => {
|
|
4809
|
+
for (const off of unsubscribes.splice(0)) off();
|
|
4810
|
+
};
|
|
4811
|
+
const write = (value, replayed = false) => {
|
|
4812
|
+
if (socket.destroyed || socket.writableEnded) return;
|
|
4813
|
+
if (!replayed && socket.writableLength > limit) {
|
|
4814
|
+
unsubscribe();
|
|
4815
|
+
socket.end();
|
|
4816
|
+
return;
|
|
4817
|
+
}
|
|
4818
|
+
socket.write(JSON.stringify(value) + "\n");
|
|
4601
4819
|
};
|
|
4602
|
-
socket.on("
|
|
4603
|
-
|
|
4820
|
+
socket.on("drain", () => {
|
|
4821
|
+
limit = MAX_UNREAD;
|
|
4604
4822
|
});
|
|
4823
|
+
socket.on("close", unsubscribe);
|
|
4605
4824
|
socket.on("error", () => {});
|
|
4606
4825
|
const lines = createInterface({
|
|
4607
4826
|
input: socket,
|
|
@@ -4634,8 +4853,25 @@ function connection(ctx, socket) {
|
|
|
4634
4853
|
return;
|
|
4635
4854
|
}
|
|
4636
4855
|
if (request.method === "events.subscribe") {
|
|
4856
|
+
if (unsubscribes.length) {
|
|
4857
|
+
write({
|
|
4858
|
+
id,
|
|
4859
|
+
error: new CliError("bad_request", "This connection is subscribed already: open another one for a new stream").toJSON()
|
|
4860
|
+
});
|
|
4861
|
+
return;
|
|
4862
|
+
}
|
|
4637
4863
|
const params = request.params ?? {};
|
|
4638
4864
|
const since = typeof params.since === "number" ? params.since : ctx.hub.lastSeq;
|
|
4865
|
+
let replay;
|
|
4866
|
+
try {
|
|
4867
|
+
replay = ctx.hub.replay(since);
|
|
4868
|
+
} catch (error) {
|
|
4869
|
+
write({
|
|
4870
|
+
id,
|
|
4871
|
+
error: asCliError(error).toJSON()
|
|
4872
|
+
});
|
|
4873
|
+
return;
|
|
4874
|
+
}
|
|
4639
4875
|
const queued = [];
|
|
4640
4876
|
let replaying = true;
|
|
4641
4877
|
unsubscribes.push(ctx.hub.onEvent((event) => {
|
|
@@ -4650,10 +4886,11 @@ function connection(ctx, socket) {
|
|
|
4650
4886
|
}
|
|
4651
4887
|
});
|
|
4652
4888
|
let last = since;
|
|
4653
|
-
for (const event of
|
|
4654
|
-
write({ event });
|
|
4889
|
+
for (const event of replay) {
|
|
4890
|
+
write({ event }, true);
|
|
4655
4891
|
last = event.seq;
|
|
4656
4892
|
}
|
|
4893
|
+
limit = Math.max(limit, socket.writableLength + MAX_UNREAD);
|
|
4657
4894
|
replaying = false;
|
|
4658
4895
|
for (const event of queued) if (event.seq > last) write({ event });
|
|
4659
4896
|
return;
|
|
@@ -4668,6 +4905,6 @@ function connection(ctx, socket) {
|
|
|
4668
4905
|
});
|
|
4669
4906
|
}
|
|
4670
4907
|
//#endregion
|
|
4671
|
-
export { BASELINE_NEWEST, MAX_LINE, keepServing, openHost, serve };
|
|
4908
|
+
export { BASELINE_NEWEST, MAX_LINE, MAX_UNREAD, keepServing, openHost, serve };
|
|
4672
4909
|
|
|
4673
|
-
//# sourceMappingURL=host-
|
|
4910
|
+
//# sourceMappingURL=host-Dj3C1fn3.js.map
|