@ghostlytools/cli 1.0.1 → 1.0.2
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 +8 -5
- package/dist/assets/{hyperdhtRelay-_rJsllcP.js → hyperdhtRelay-cMlRWrmD.js} +3 -3
- package/dist/assets/{hyperdhtRelay-_rJsllcP.js.map → hyperdhtRelay-cMlRWrmD.js.map} +1 -1
- package/dist/assets/{mainline-B0mUxYPH.js → mainline-_roUnpDA.js} +2 -2
- package/dist/assets/{mainline-B0mUxYPH.js.map → mainline-_roUnpDA.js.map} +1 -1
- package/dist/assets/{server-C-8wbsAX.js → server-Cajp8VLY.js} +626 -167
- package/dist/assets/server-Cajp8VLY.js.map +1 -0
- package/dist/ghostly.mjs +436 -45
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/assets/server-C-8wbsAX.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as boundedIdentityFetch, $n as LIVENESS_PING_MS, $r as IDENTITY_REVOCATION_LABEL, $t as GroupWakeLimiter, A as TEST_MINT, Ai as
|
|
1
|
+
import { $ as boundedIdentityFetch, $i as GROUP_MEMBER_CAP, $n as LIVENESS_PING_MS, $r as IDENTITY_REVOCATION_LABEL, $t as GroupWakeLimiter, A as TEST_MINT, Aa as normalizeRelayUrl, Ai as GROUP_REACTION_FRAME, An as assertTokenUnits, Ar as nextPinNumber, At as transact, B as registeredLightningProviders, Ba as identityFromSeedB64, Bi as pairedReplyAuthor, Bn as isPubkyKey, Br as vapidKeysMatch, Bt as communityHasFrame, C as networkLabel, Ca as createChatInvite, Ci as EDIT_RATE_WINDOW_MS, Cn as ONCHAIN_PROVIDER, Cr as transferEnded, Ct as STORES, D as WALLET_TYPES, Da as DEFAULT_RELAYS, Di as forwardedAgain, Dn as TEST_USDT_FAUCET_AMOUNT, Dt as fileStore, Ea as inviteOwnership, Ei as parseLinkPreview, En as SEPOLIA_TEST_USDT_FAUCET, Er as typingActivity, Et as deleteDatabase, F as paysItsOwnInvoices, Fi as reactionEmoji, Fn as SPARK_NETWORKS, Fr as WakeLimiter, Ft as openGroupTyping, G as SourceConfigError, Ga as randomBytes, Gi as checkStatusCard, Gn as cashuRequestPayload, Gr as HoldKeys, Gt as decodeGroupEntryLink, H as reserveAdapterIds, Ha as sanitizeAvatar, Hi as replySnippet, Hn as engineText, Hr as EDIT_CAPABILITY, Ht as decodeCommunityLink, I as BACKUP_MEDIA_TYPE, Ii as reactionIsNewer, In as SPARK_PROVIDER, Ir as newWakeToken, It as sealGroupTyping, J as networkMode, Ja as utf8Decode, Ji as arrivalKey, Jn as isFederationId, Jr as readManifest, Jt as knockIdentity, K as SourceUnreachableError, Ka as toBase64Url, Ki as sameButtonText, Kn as fedimintRequestPayload, Kr as HoldRefusedError, Kt as encodeGroupEntryLink, L as MAX_BACKUP_BYTES, Li as readReaction, Ln as sparkAddressKind, Lr as relayRequest, Lt as COMMUNITY_LIMITS, M as isTestMint, Ma as isDiscoveryBudgetError, Mi as ReactionWindow, Mn as formatPaymentAmount, Mr as pinNumberHolds, N as isWorthlessMint, Na as withRequestOptions, Ni as nextReactionNumber, Nn as validatePaymentTarget, Nr as readPin, Nt as GroupTypingBoard, O as BITCOIN_INVOICE_ON_TESTNET, Oa as RelayTransport, Oi as readForwarded, On as USDT_PUBLIC_RPC, Or as PIN_LIMITS, Ot as openDb, P as mintNetwork, Pa as PacketTooLargeError, Pi as queueReaction, Pn as walletNetworkOf, Pr as WAKE_CALL_INTERVAL_MS, Pt as groupTypingEpoch, Q as setOwnDidSource, Qi as shownTime, Qn as INVITE_TAKEN, Qr as dialDescriptors, R as identityProviders, Ra as createIdentity, Ri as validReactionNumber, Rn as sparkInvoiceDetails, Rr as wakeRequest, Rt as CommunitySession, S as WrongNetworkError, Sa as OwnInviteError, Si as EDIT_BUFFER_MS, Sn as EVM_TEST_CHAINS, Sr as FILE_LIMITS, T as cashuMint, Ta as encodeInviteCode, Ti as dhtEditId, Tn as SEPOLIA_TEST_USDT, Tr as TypingSender, Tt as databaseName, U as NothingSpentError, Ua as bytesEqual, Ui as BUTTON_ID, Un as decodeBolt11, Ur as HOLD_CAPABILITY, Ut as encodeCommunityLink, V as registeredOnchainProviders, Va as publicKeyFromZ32, Vi as readReply, Vn as engineError, Vt as communityMessageId, W as PROVIDER_PLATFORMS, Wa as fromBase64Url, Wi as STATUS_CARD_LIMITS, Wn as ENDPOINT, Wr as HOLD_LIMITS, X as assertPublicServer, Xa as __exportAll, Xi as heardTime, Xn as parseFedimintRequestPayload, Xr as DHT_TEXT_CAPABILITY, Xt as mergeKnocks, Y as redact, Ya as utf8Encode, Yi as claimedTime, Yn as parseCashuRequestPayload, Yr as CapsExchange, Yt as knockRecords, Z as resolveAtprotoDid, Zi as receivedTimestamp, Zn as GhostLink, Zr as capsDescriptors, Zt as readKnocks, _ as newDeviceKey, _a as parseCallSignal, _i as fileMessageText, _n as didDhtFromPublicKey, _r as readBeacon, _t as fileBytes, a as createFailure, aa as DirectPathWatch, ai as receivedIdentityStatus, an as GroupSession, ar as COMMUNITY_TOPOLOGY, at as replyRef, b as ModeChanged, ba as LEGACY_SERVICES, bi as DHT_MESSAGE_TTL, bn as signDidDhtPacket, br as shouldBeHub, bt as removeFileBytes, c as paymentNetwork, ca as traceLink, ci as PeerProofs, cr as doorHubs, ct as paymentWireId, d as networkReason, da as parseLocalTarget, di as TRANSPORTS, dn as parseCommunityEdit, dr as lobbyRecords, dt as removeStored, ei as IdentityExchange, en as groupWakeFrame, er as pairedMessageFrame, et as verifyIdentity, f as rpcHost, fa as webLocalFetch, fi as automaticTransport, fn as mentionsMember, fr as mergeBeacon, ft as storedSize, g as intentRepository, ga as RTC_CONFIG, gi as safeBlobType, gn as didDhtDocument, gr as rankHubs, h as ProviderSources, ha as encodeControl, hi as parseImageMeta, hn as normalizeAtprotoHandle, hr as pickHubs, ht as blobDigest, i as assertConfirmedReal, ia as edgeParams, ii as newIdentityBinding, in as GROUP_READ_NOTE, ir as pickMeshHubs, it as receivedPairedReply, j as fakesLightning, ja as budgetRetryMs, ji as REACTION_LIMITS, jn as assertWholeSats, jr as pinIsNewer, jt as wrap, k as DEFAULT_MINTS, ka as currentRelays, ki as COMMUNITY_REACTION_FRAME, kn as WALLET_NETWORKS, kr as mayPin, kt as store, l as paymentNetworksOf, la as GhostlyHttpError, li as emptyProofLedger, ln as GROUP_EDIT_TEXT_BYTES, lr as freshHubs, lt as FileAppender, m as LightningCards, ma as decodeControl, mi as irohRelayUrl, mn as isAtprotoDid, mr as newerHead, n as loadFedimintSdk, na as rosterHas, ni as identityRevocationValue, nn as parseGroupWakeFrame, nr as mayBeHub, nt as groupReply, o as createTiming, oi as revokesIdentity, on as groupMessageId, or as beaconKeys, ot as replyTo, p as CASHU_CARD, pa as LIMITS, pi as irohDescriptor, pn as groupName, pr as mergeLobby, pt as streamStored, q as isNothingSpentError, qa as toZ32, qi as statusCardText, qn as findEndpoint, qr as newMailbox, qt as entryParams, r as WALLET_NAMES, ra as verifyCommitSignature, ri as identityStatement, rr as meshHubs, rt as pairedWireReply, s as crossNetwork, sa as RELAY_POLL_INTERVALS, si as lastSharedWithMe, sr as beaconRecords, st as paymentAlias, t as fedimintDatabase, ta as MEMBER_KEY, ti as emptyIdentityLedger, tn as groupWakes, tr as MESH_HUBS, tt as chosenResolver, u as walletInstances, ua as formatLocalTarget, ui as validExternalKey, un as carryMentions, ur as lobbyKeys, ut as readStored, v as sealSeed, va as emptyLinkRecords, vi as parseVoiceMeta, vn as encodeDidDhtDeactivated, vr as readBeaconHead, vt as fileBytesOf, w as CASHU_MINT_SOURCE, wa as decodeInviteCode, wi as RateWindow, wn as PaymentPreflightError, wr as transferStalled, wt as clearProfileStores, x as ModeGate, xa as serviceIdFromName, xi as peekDhtMailbox, xn as ETHEREUM_USDT, xr as ChatFiles, y as unsealSeed, ya as HTTP_SERVICE_PROTO, yi as parseVideoMeta, yn as encodeDidDhtPacket, yr as readLobby, z as onAdaptersChanged, za as identityFromSeed, zi as wireReaction, zn as isBitcoinAddress, zr as checkPushEndpoint, zt as GROUP_READ_NOTE_COMMUNITY } from "../ghostly.mjs";
|
|
2
2
|
import { base64, base64url, base64urlnopad, bech32, hex } from "@scure/base";
|
|
3
3
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
4
4
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
@@ -11407,6 +11407,7 @@ function composeDetails(message, around) {
|
|
|
11407
11407
|
linkId: message.linkId,
|
|
11408
11408
|
sender: message.sender,
|
|
11409
11409
|
timestamp: message.timestamp,
|
|
11410
|
+
...message.sentAt !== void 0 && { sentAt: message.sentAt },
|
|
11410
11411
|
via: message.via,
|
|
11411
11412
|
delivery: message.delivery,
|
|
11412
11413
|
deliveryError: message.deliveryError,
|
|
@@ -11579,6 +11580,11 @@ var FileDesk = class {
|
|
|
11579
11580
|
peerRoom: files?.peerRoom ?? null
|
|
11580
11581
|
};
|
|
11581
11582
|
}
|
|
11583
|
+
/** A files/3 transfer of this chat's under way on its session: offered, moving, or being checked. */
|
|
11584
|
+
moving(linkId) {
|
|
11585
|
+
const files = this.chats.get(linkId)?.files;
|
|
11586
|
+
return !!files?.live && files.records().some((r) => r.state === "offered" || r.state === "active" || r.state === "verifying");
|
|
11587
|
+
}
|
|
11582
11588
|
/**
|
|
11583
11589
|
* Offers a file whose record and bytes are stored under its local id (`outgoingFileId`). Offered before (Retry, a
|
|
11584
11590
|
* restart): an unfinished transfer is taken over and offered again, a failed one sent again; never a second one.
|
|
@@ -12234,7 +12240,7 @@ function takesPeerEdit(message, text) {
|
|
|
12234
12240
|
function withEdit(message, edit) {
|
|
12235
12241
|
const before = message.edit?.history ?? [];
|
|
12236
12242
|
const history = edit.text === message.text || edit.card && edit.card.kind !== "buttons" ? before : trimHistory([...before, {
|
|
12237
|
-
at: message.edit?.at ?? message
|
|
12243
|
+
at: message.edit?.at ?? shownTime(message),
|
|
12238
12244
|
text: message.text
|
|
12239
12245
|
}]);
|
|
12240
12246
|
const next = {
|
|
@@ -13381,6 +13387,32 @@ var db = {
|
|
|
13381
13387
|
}
|
|
13382
13388
|
};
|
|
13383
13389
|
//#endregion
|
|
13390
|
+
//#region ../browser/src/engine/arrival.ts
|
|
13391
|
+
/**
|
|
13392
|
+
* A history stored before a received row was placed where it arrived (WISP 400, requirement 10; until 2026-10): a
|
|
13393
|
+
* contact's row sits under the time the contact's clock said, up to five minutes ahead of this device's. Almost all of
|
|
13394
|
+
* it stays as it is: its stored time is its place, so nothing moves when the app updates. Only the rows whose stored
|
|
13395
|
+
* time has not come yet are settled, once, at the start: they are the last rows of their chat, and left there a
|
|
13396
|
+
* message written in the next minutes would sort above them. Each takes a place just before now, in the order they
|
|
13397
|
+
* were in and after everything before them, and keeps the time its sender said beside it (`sentAt`).
|
|
13398
|
+
*
|
|
13399
|
+
* Returns the rows to write, as they are now; none when nothing is ahead. Rows of mine are never touched: their time
|
|
13400
|
+
* is the one both sides know them by.
|
|
13401
|
+
*/
|
|
13402
|
+
function settleAhead(history, now) {
|
|
13403
|
+
const ahead = history.filter((m) => m.sender === "peer" && m.sentAt === void 0 && m.timestamp > now);
|
|
13404
|
+
if (!ahead.length) return [];
|
|
13405
|
+
let last = history.reduce((max, m) => m.timestamp > now ? max : Math.max(max, m.timestamp), 0);
|
|
13406
|
+
return ahead.map((m, i) => {
|
|
13407
|
+
last = Math.max(last + 1, now - (ahead.length - i));
|
|
13408
|
+
return {
|
|
13409
|
+
...m,
|
|
13410
|
+
timestamp: last,
|
|
13411
|
+
sentAt: m.timestamp
|
|
13412
|
+
};
|
|
13413
|
+
});
|
|
13414
|
+
}
|
|
13415
|
+
//#endregion
|
|
13384
13416
|
//#region ../browser/src/engine/reactions.ts
|
|
13385
13417
|
var isGroup$1 = (chat) => chat.startsWith("group:");
|
|
13386
13418
|
/** The row a reaction names in a chat: the id both sides know it by (`replyRef`). */
|
|
@@ -13721,6 +13753,26 @@ var HUB_GRACE_MS = 18e4;
|
|
|
13721
13753
|
var REFUSED_FOR_MS$1 = 6e5;
|
|
13722
13754
|
var ENTRY_LINGER_MS$1 = 2e4;
|
|
13723
13755
|
var RELAYED_KEPT = 4096;
|
|
13756
|
+
/**
|
|
13757
|
+
* Someone the chain took out while its app was closed comes back believing it is a member: it asks in a hub's lobby,
|
|
13758
|
+
* or lists itself as a hub. A hub then keeps an edge to it for a moment, only to hand it the commit that took it out
|
|
13759
|
+
* (`farewell`): at most `FAREWELL_EDGES` at a time, each awaited `FAREWELL_WAIT_MS` and closed `FAREWELL_LINGER_MS`
|
|
13760
|
+
* after the commit went; the same key is not waited for again for a while (longer once it was told).
|
|
13761
|
+
*/
|
|
13762
|
+
var FAREWELL_EDGES = 4;
|
|
13763
|
+
var FAREWELL_WAIT_MS = 12e4;
|
|
13764
|
+
var FAREWELL_LINGER_MS = 1e4;
|
|
13765
|
+
var FAREWELL_RETRY_MS = 6e4;
|
|
13766
|
+
var FAREWELL_TOLD_MS = 6e5;
|
|
13767
|
+
var FAREWELL_SAY_MS = 1e4;
|
|
13768
|
+
/**
|
|
13769
|
+
* A key asking from before the commit that took it out was there and was sent that commit: not waited for. The two
|
|
13770
|
+
* times are two devices' clocks (the asker's, the commit's signer's), so "before" allows this much between them:
|
|
13771
|
+
* a returning member whose clock runs behind the admin's is told all the same.
|
|
13772
|
+
*/
|
|
13773
|
+
var FAREWELL_SKEW_MS = 3e5;
|
|
13774
|
+
/** A member that opens the group's link again knocks to check it is still in, this long at most (`joinByLink`). */
|
|
13775
|
+
var CHECK_MS = 18e4;
|
|
13724
13776
|
var MESSAGE_LINK$1 = (groupId) => `group:${groupId}`;
|
|
13725
13777
|
/**
|
|
13726
13778
|
* A member key for a joiner whose entry session the member's side dials. Of a paired session's two
|
|
@@ -13752,6 +13804,8 @@ var Communities = class {
|
|
|
13752
13804
|
/** Joiners: groups whose knock is out (published), and the knock record it is in. */
|
|
13753
13805
|
knocked = /* @__PURE__ */ new Set();
|
|
13754
13806
|
knockAt = /* @__PURE__ */ new Map();
|
|
13807
|
+
/** Joiners: how many others had a knock still being refreshed in the record I last read (they wait with me). */
|
|
13808
|
+
knockingWith = /* @__PURE__ */ new Map();
|
|
13755
13809
|
/** My frames an edge took, and which of them carry an edit (`frame id → editKey`). */
|
|
13756
13810
|
frames = new FramesTaken();
|
|
13757
13811
|
carriers = /* @__PURE__ */ new Map();
|
|
@@ -13796,9 +13850,11 @@ var Communities = class {
|
|
|
13796
13850
|
const history = await this.store.getMessages(MESSAGE_LINK$1(group.id)), said = history.filter((m) => !m.event && !m.groupPay);
|
|
13797
13851
|
const last = said[said.length - 1], lastPeer = [...said].reverse().find((m) => m.sender !== "me");
|
|
13798
13852
|
if (last) this.lastMessageAt.set(group.id, last.timestamp);
|
|
13799
|
-
|
|
13853
|
+
const peerAt = cameOrWritten(lastPeer?.timestamp, lastPeer ? group.came?.peer : void 0);
|
|
13854
|
+
if (peerAt) this.lastPeerMessageAt.set(group.id, peerAt);
|
|
13800
13855
|
const mention = [...history].reverse().find((m) => m.mentioned);
|
|
13801
|
-
|
|
13856
|
+
const mentionAt = cameOrWritten(mention?.timestamp, mention ? group.came?.mention : void 0);
|
|
13857
|
+
if (mentionAt) this.lastMentionAt.set(group.id, mentionAt);
|
|
13802
13858
|
for (const [, linkId] of this.host.entries(group.id)) if (group.joining?.linkId !== linkId) await this.host.closeEdge(linkId);
|
|
13803
13859
|
const live = this.live.get(group.id);
|
|
13804
13860
|
if (live) for (const key of this.host.edges(group.id).keys()) live.restored.set(key, 0);
|
|
@@ -13821,7 +13877,7 @@ var Communities = class {
|
|
|
13821
13877
|
invited: [],
|
|
13822
13878
|
memberLinks: {}
|
|
13823
13879
|
};
|
|
13824
|
-
if (
|
|
13880
|
+
if (this.knocking(group, live)) return [{
|
|
13825
13881
|
...base,
|
|
13826
13882
|
name: groupName(group.joining.name) ?? live?.session.name ?? "",
|
|
13827
13883
|
isAdmin: false,
|
|
@@ -13834,7 +13890,8 @@ var Communities = class {
|
|
|
13834
13890
|
members: 0,
|
|
13835
13891
|
accepted: true,
|
|
13836
13892
|
viaLink: true,
|
|
13837
|
-
stage: this.joinStage(group)
|
|
13893
|
+
stage: this.joinStage(group),
|
|
13894
|
+
...this.knockingWith.get(group.id) ? { waiting: this.knockingWith.get(group.id) } : {}
|
|
13838
13895
|
}
|
|
13839
13896
|
}];
|
|
13840
13897
|
if (!live) return [];
|
|
@@ -13843,7 +13900,7 @@ var Communities = class {
|
|
|
13843
13900
|
const id = edges.get(key);
|
|
13844
13901
|
return !!id && this.host.linkReady(id, 2);
|
|
13845
13902
|
};
|
|
13846
|
-
const hubs = freshHubs(live
|
|
13903
|
+
const hubs = freshHubs(this.hubs(live), this.now()).map((h) => h.key);
|
|
13847
13904
|
return [{
|
|
13848
13905
|
...base,
|
|
13849
13906
|
name: s.name,
|
|
@@ -13904,17 +13961,36 @@ var Communities = class {
|
|
|
13904
13961
|
if (!link) throw new Error("This is not a link to a group");
|
|
13905
13962
|
const existing = this.stored.get(link.g);
|
|
13906
13963
|
const live = this.live.get(link.g);
|
|
13907
|
-
if (live && live.session.status === "active") return link.g;
|
|
13908
13964
|
if (existing?.joining && existing.joining.host === link.host) return link.g;
|
|
13965
|
+
if (live) {
|
|
13966
|
+
const s = live.session;
|
|
13967
|
+
if (s.status === "active" && s.roster.length === 1) return link.g;
|
|
13968
|
+
if (s.status === "active") {
|
|
13969
|
+
await this.startJoining(link, s.state.seedB64, this.now(), true);
|
|
13970
|
+
return link.g;
|
|
13971
|
+
}
|
|
13972
|
+
if (s.status === "removed" || s.status === "lost") {
|
|
13973
|
+
await this.startJoining(link, s.state.seedB64, this.now());
|
|
13974
|
+
return link.g;
|
|
13975
|
+
}
|
|
13976
|
+
}
|
|
13909
13977
|
if (existing) await this.forget(link.g);
|
|
13910
13978
|
await this.startJoining(link, dialedKey(link), this.now());
|
|
13911
13979
|
return link.g;
|
|
13912
13980
|
}
|
|
13913
|
-
|
|
13981
|
+
/** Knocking to get in: a joiner, or someone who was in (removed, or its admission lost) asking again. Not a member checking it still is. */
|
|
13982
|
+
knocking(group, live) {
|
|
13983
|
+
return !!group.joining && (!live || live.session.status !== "active");
|
|
13984
|
+
}
|
|
13985
|
+
/** `check`: a member, as far as this device knows, asking the door whether it still is (`joinByLink`). */
|
|
13986
|
+
async startJoining(link, seedB64, since, check = false) {
|
|
13914
13987
|
this.lastKnock.delete(link.g);
|
|
13915
13988
|
this.knocked.delete(link.g);
|
|
13916
13989
|
this.knockAt.delete(link.g);
|
|
13990
|
+
this.knockingWith.delete(link.g);
|
|
13991
|
+
const before = this.stored.get(link.g)?.joining;
|
|
13917
13992
|
const linkId = await this.host.openEntry(link, "guest", seedB64, link.host);
|
|
13993
|
+
if (before && before.linkId !== linkId) await this.host.closeEdge(before.linkId);
|
|
13918
13994
|
const group = this.stored.get(link.g) ?? {
|
|
13919
13995
|
id: link.g,
|
|
13920
13996
|
createdAt: since
|
|
@@ -13927,7 +14003,8 @@ var Communities = class {
|
|
|
13927
14003
|
name: group.community?.name ?? "",
|
|
13928
14004
|
inviter: "",
|
|
13929
14005
|
pieces: [],
|
|
13930
|
-
since
|
|
14006
|
+
since,
|
|
14007
|
+
...check && { check: true }
|
|
13931
14008
|
};
|
|
13932
14009
|
this.stored.set(link.g, group);
|
|
13933
14010
|
await this.store.putGroup(group);
|
|
@@ -13935,9 +14012,29 @@ var Communities = class {
|
|
|
13935
14012
|
traceJoin(link.g, "join.start");
|
|
13936
14013
|
this.knock(group, since).catch(() => {});
|
|
13937
14014
|
}
|
|
14015
|
+
/** The check is over (answered, or nobody was at the door): the group stays as it is here. */
|
|
14016
|
+
async endCheck(group) {
|
|
14017
|
+
const joining = group.joining;
|
|
14018
|
+
if (!joining) return;
|
|
14019
|
+
delete group.joining;
|
|
14020
|
+
this.lastKnock.delete(group.id);
|
|
14021
|
+
this.knocked.delete(group.id);
|
|
14022
|
+
this.knockAt.delete(group.id);
|
|
14023
|
+
this.knockingWith.delete(group.id);
|
|
14024
|
+
await this.host.closeEdge(joining.linkId);
|
|
14025
|
+
await this.store.putGroup(group);
|
|
14026
|
+
this.host.emit();
|
|
14027
|
+
}
|
|
13938
14028
|
async send(groupId, text, mentions = [], reply, forwarded, card) {
|
|
13939
14029
|
const live = this.live.get(groupId);
|
|
13940
|
-
if (!live) return {
|
|
14030
|
+
if (!live) return {
|
|
14031
|
+
error: "You are not in this group yet",
|
|
14032
|
+
refused: true
|
|
14033
|
+
};
|
|
14034
|
+
if (!live.session.isMember) return {
|
|
14035
|
+
error: live.session.state.statusReason ?? "You are not in this group",
|
|
14036
|
+
refused: true
|
|
14037
|
+
};
|
|
13941
14038
|
const result = card ? await live.session.sendText(text, this.host.myNick?.(), Date.now(), mentions, reply, forwarded, card) : await live.session.sendText(text, this.host.myNick?.(), Date.now(), mentions, reply, forwarded);
|
|
13942
14039
|
return "error" in result ? { error: result.error } : {
|
|
13943
14040
|
error: null,
|
|
@@ -14050,7 +14147,7 @@ var Communities = class {
|
|
|
14050
14147
|
*/
|
|
14051
14148
|
async decline(groupId) {
|
|
14052
14149
|
const group = this.stored.get(groupId), live = this.live.get(groupId);
|
|
14053
|
-
if (!group
|
|
14150
|
+
if (!group || !this.knocking(group, live)) throw new Error("No invitation to decline");
|
|
14054
14151
|
if (live) throw new Error("You were in this group: leave it to stop trying to get back in");
|
|
14055
14152
|
await this.forget(groupId);
|
|
14056
14153
|
}
|
|
@@ -14062,6 +14159,7 @@ var Communities = class {
|
|
|
14062
14159
|
this.lastMessageAt.delete(groupId);
|
|
14063
14160
|
this.lastPeerMessageAt.delete(groupId);
|
|
14064
14161
|
this.lastMentionAt.delete(groupId);
|
|
14162
|
+
this.knockingWith.delete(groupId);
|
|
14065
14163
|
for (const linkId of [...this.host.edges(groupId).values(), ...this.host.entries(groupId).values()]) await this.host.closeEdge(linkId);
|
|
14066
14164
|
await this.store.deleteGroup(groupId);
|
|
14067
14165
|
this.host.historyGone?.(groupId);
|
|
@@ -14077,9 +14175,10 @@ var Communities = class {
|
|
|
14077
14175
|
this.time = now;
|
|
14078
14176
|
for (const group of [...this.stored.values()]) {
|
|
14079
14177
|
const live = this.live.get(group.id);
|
|
14080
|
-
if (group.joining && (
|
|
14081
|
-
const
|
|
14082
|
-
if (!this.
|
|
14178
|
+
if (group.joining && (this.knocking(group, live) || group.joining.check)) {
|
|
14179
|
+
const waited = now - group.joining.since, every = waited > this.timings.patienceMs ? this.timings.slowKnockMs : this.timings.knockMs;
|
|
14180
|
+
if (!this.knocking(group, live) && waited > CHECK_MS) await this.endCheck(group);
|
|
14181
|
+
else if (!this.host.linkReady(group.joining.linkId, 2) && !this.host.linkSeen?.(group.joining.linkId) && now - (this.lastKnock.get(group.id) ?? 0) >= every) await this.knock(group, now).catch(() => {});
|
|
14083
14182
|
if (!live) continue;
|
|
14084
14183
|
}
|
|
14085
14184
|
if (!live) continue;
|
|
@@ -14096,6 +14195,12 @@ var Communities = class {
|
|
|
14096
14195
|
continue;
|
|
14097
14196
|
}
|
|
14098
14197
|
if (live.session.status !== "active") {
|
|
14198
|
+
if (live.hub) {
|
|
14199
|
+
live.hub = false;
|
|
14200
|
+
await this.closeEntries(group.id);
|
|
14201
|
+
await this.publishBeacon(group.id, live, now, false).catch(() => {});
|
|
14202
|
+
}
|
|
14203
|
+
live.farewells.clear();
|
|
14099
14204
|
await this.reconcile(group.id, live, now);
|
|
14100
14205
|
continue;
|
|
14101
14206
|
}
|
|
@@ -14111,14 +14216,19 @@ var Communities = class {
|
|
|
14111
14216
|
live.lingering.delete(linkId);
|
|
14112
14217
|
if ([...this.host.entries(groupId).values()].includes(linkId)) await this.host.closeEdge(linkId);
|
|
14113
14218
|
}
|
|
14114
|
-
const alone = live.hub && !freshHubs(live
|
|
14115
|
-
if (now - live.lastBeaconRead >= (alone ? COMMUNITY_TOPOLOGY.beaconEveryMs : this.timings.beaconReadMs) || live.lastBeaconRead === 0) await this.readBeacon(groupId, live, now);
|
|
14116
|
-
|
|
14219
|
+
const alone = live.hub && !freshHubs(this.hubs(live), now).some((h) => h.key !== me);
|
|
14220
|
+
if ((live.beaconFailedAt ? now - live.beaconFailedAt >= BEACON_RETRY_MS : now - live.lastBeaconRead >= (alone ? COMMUNITY_TOPOLOGY.beaconEveryMs : this.timings.beaconReadMs)) || live.lastBeaconRead === 0) await this.readBeacon(groupId, live, now);
|
|
14221
|
+
if (!live.beaconKnown) {
|
|
14222
|
+
await this.keepLooking(groupId, live, now);
|
|
14223
|
+
await this.reconcile(groupId, live, now);
|
|
14224
|
+
return;
|
|
14225
|
+
}
|
|
14226
|
+
const others = freshHubs(this.hubs(live), now).filter((h) => h.key !== me);
|
|
14117
14227
|
if (!live.hub) {
|
|
14118
|
-
const settled = () => now - live.joinedAt >= this.timings.newcomerMs || !freshHubs(live
|
|
14119
|
-
const want = () => settled() && (shouldBeHub(me, live
|
|
14228
|
+
const settled = () => now - live.joinedAt >= this.timings.newcomerMs || !freshHubs(this.hubs(live), now).some((h) => h.key !== me);
|
|
14229
|
+
const want = () => settled() && (shouldBeHub(me, this.hubs(live), now) || !!live.forceHub && freshHubs(this.hubs(live), now).length < COMMUNITY_TOPOLOGY.maxHubs) && this.capacity(groupId, live, now) > 0;
|
|
14120
14230
|
if (want()) {
|
|
14121
|
-
if (!live.hubCandidateAt) live.hubCandidateAt = now + (freshHubs(live
|
|
14231
|
+
if (!live.hubCandidateAt) live.hubCandidateAt = now + (freshHubs(this.hubs(live), now).length ? Math.floor(this.random() * this.timings.hubJitterMs) : 0);
|
|
14122
14232
|
if (now >= live.hubCandidateAt) {
|
|
14123
14233
|
live.hubCandidateAt = 0;
|
|
14124
14234
|
if (live.lastBeaconRead !== now) await this.readBeacon(groupId, live, now);
|
|
@@ -14144,6 +14254,10 @@ var Communities = class {
|
|
|
14144
14254
|
await this.publishBeacon(groupId, live, now, false);
|
|
14145
14255
|
} else if (now - live.lastBeaconTry < BEACON_RETRY_MS) {} else if (now - live.lastBeaconWrite >= COMMUNITY_TOPOLOGY.beaconEveryMs || !live.beacon.some((h) => h.key === me) || now - live.lastBeaconWrite >= 5e3 && Math.abs(this.beaconLoad(groupId, live, now) - (live.beacon.find((h) => h.key === me)?.load ?? load)) >= 8) await this.publishBeacon(groupId, live, now, true).catch(() => {});
|
|
14146
14256
|
}
|
|
14257
|
+
if (live.hub) {
|
|
14258
|
+
for (const h of freshHubs(live.beacon, now)) if (h.ts + FAREWELL_SKEW_MS > (s.outAt(h.key) ?? Infinity)) this.wantFarewell(groupId, live, h.key, now);
|
|
14259
|
+
}
|
|
14260
|
+
this.endFarewells(live, now);
|
|
14147
14261
|
if (live.hub) {
|
|
14148
14262
|
const busy = this.admitting(groupId, live, now);
|
|
14149
14263
|
const lobbyEvery = now < live.lobbyBusyUntil && !busy ? this.timings.lobbyPollMs : this.timings.lobbyIdlePollMs;
|
|
@@ -14200,7 +14314,7 @@ var Communities = class {
|
|
|
14200
14314
|
}
|
|
14201
14315
|
if (s.needsCatchUp && now - (live.lastCatchUp ?? 0) >= 5e3 || now - (live.lastCatchUp ?? 0) >= 3e4) {
|
|
14202
14316
|
live.lastCatchUp = now;
|
|
14203
|
-
const ready = [...this.host.edges(groupId)].filter(([, id]) => this.host.linkReady(id, 2)).map(([key]) => key);
|
|
14317
|
+
const ready = [...this.host.edges(groupId)].filter(([key, id]) => this.host.linkReady(id, 2) && !s.wasRemoved(key)).map(([key]) => key);
|
|
14204
14318
|
for (const key of ready.slice(0, 3)) s.catchUp(key);
|
|
14205
14319
|
}
|
|
14206
14320
|
await this.reconcile(groupId, live, now);
|
|
@@ -14258,7 +14372,7 @@ var Communities = class {
|
|
|
14258
14372
|
capacity(groupId, live, now, entries = true) {
|
|
14259
14373
|
const room = this.host.peerRoom?.(groupId);
|
|
14260
14374
|
if (room === void 0) return COMMUNITY_TOPOLOGY.hubCapacity;
|
|
14261
|
-
const hubs = new Set(freshHubs(live
|
|
14375
|
+
const hubs = new Set(freshHubs(this.hubs(live), now).map((h) => h.key));
|
|
14262
14376
|
for (const key of this.host.edges(groupId).keys()) if (!live.members.has(key)) hubs.add(key);
|
|
14263
14377
|
hubs.delete(live.session.myKey);
|
|
14264
14378
|
const held = hubs.size + (entries ? this.host.entries(groupId).size : 0);
|
|
@@ -14272,7 +14386,15 @@ var Communities = class {
|
|
|
14272
14386
|
async readBeacon(groupId, live, now) {
|
|
14273
14387
|
live.lastBeaconRead = now;
|
|
14274
14388
|
const keys = beaconKeys(live.session.state.rv, groupId);
|
|
14275
|
-
const records = await this.host.resolve(keys.identity.pubKeyZ32, true).catch(() =>
|
|
14389
|
+
const records = await this.host.resolve(keys.identity.pubKeyZ32, true).catch(() => void 0);
|
|
14390
|
+
if (records === void 0) {
|
|
14391
|
+
live.beaconFailedAt = now;
|
|
14392
|
+
live.beaconAskedAt ||= now;
|
|
14393
|
+
if (live.session.roster.length === 1 || now - live.beaconAskedAt >= COMMUNITY_TOPOLOGY.beaconFreshMs) live.beaconKnown = true;
|
|
14394
|
+
return;
|
|
14395
|
+
}
|
|
14396
|
+
live.beaconFailedAt = 0;
|
|
14397
|
+
live.beaconKnown = true;
|
|
14276
14398
|
live.beacon = readBeacon(keys, records ?? []);
|
|
14277
14399
|
live.head = readBeaconHead(keys, records ?? []);
|
|
14278
14400
|
this.noteHubs(live, now);
|
|
@@ -14296,8 +14418,10 @@ var Communities = class {
|
|
|
14296
14418
|
live.lastBeaconTry = now;
|
|
14297
14419
|
const keys = beaconKeys(live.session.state.rv, groupId);
|
|
14298
14420
|
let existing = live.beacon, head = live.head;
|
|
14299
|
-
if (live.lastBeaconRead !== now) {
|
|
14300
|
-
const
|
|
14421
|
+
if (live.lastBeaconRead !== now || live.beaconFailedAt === now) {
|
|
14422
|
+
const read = await this.host.resolve(keys.identity.pubKeyZ32, true).catch(() => void 0);
|
|
14423
|
+
if (read === void 0 && live.session.roster.length > 1) throw new Error("The beacon could not be read; not published");
|
|
14424
|
+
const records = read ?? [];
|
|
14301
14425
|
existing = readBeacon(keys, records);
|
|
14302
14426
|
head = readBeaconHead(keys, records);
|
|
14303
14427
|
}
|
|
@@ -14314,9 +14438,74 @@ var Communities = class {
|
|
|
14314
14438
|
this.noteHubs(live, now);
|
|
14315
14439
|
live.lastBeaconWrite = now;
|
|
14316
14440
|
live.lastBeaconRead = now;
|
|
14441
|
+
live.beaconFailedAt = 0;
|
|
14442
|
+
live.beaconKnown = true;
|
|
14443
|
+
}
|
|
14444
|
+
/**
|
|
14445
|
+
* The beacon's hubs, less those the chain took out: hubs are members. Someone removed while its app was closed lists
|
|
14446
|
+
* itself there when it comes back, believing it is a member, until it is told (`wantFarewell`).
|
|
14447
|
+
*/
|
|
14448
|
+
hubs(live) {
|
|
14449
|
+
const top = live.session.top, memo = live.hubsMemo;
|
|
14450
|
+
if (memo && memo.beacon === live.beacon && memo.top === top) return memo.hubs;
|
|
14451
|
+
const hubs = live.beacon.filter((h) => !live.session.wasRemoved(h.key));
|
|
14452
|
+
live.hubsMemo = {
|
|
14453
|
+
beacon: live.beacon,
|
|
14454
|
+
top,
|
|
14455
|
+
hubs
|
|
14456
|
+
};
|
|
14457
|
+
return hubs;
|
|
14458
|
+
}
|
|
14459
|
+
/** A frame goes on an edge to a member; to someone the chain took out, only the commit that took it out. */
|
|
14460
|
+
mayHear(live, key, frame) {
|
|
14461
|
+
if (!live?.session.wasRemoved(key)) return true;
|
|
14462
|
+
const f = frame;
|
|
14463
|
+
return f.t === "group-commit" && (f.commit?.k === "remove" || f.commit?.k === "leave") && f.commit.s === key;
|
|
14464
|
+
}
|
|
14465
|
+
/** The edges of the group that may hear `frame`. */
|
|
14466
|
+
hearers(groupId, frame) {
|
|
14467
|
+
const live = this.live.get(groupId);
|
|
14468
|
+
return [...this.host.edges(groupId)].filter(([key]) => this.mayHear(live, key, frame));
|
|
14469
|
+
}
|
|
14470
|
+
/** As a hub: someone the chain took out is asking (in my lobby, or listing itself as a hub). An edge to tell it, within bounds. */
|
|
14471
|
+
wantFarewell(groupId, live, key, now) {
|
|
14472
|
+
if (key === live.session.myKey || live.farewells.has(key) || now < (live.farewellQuiet.get(key) ?? 0)) return;
|
|
14473
|
+
if (live.farewells.size >= FAREWELL_EDGES || this.capacity(groupId, live, now) <= 0) return;
|
|
14474
|
+
live.farewells.set(key, { since: now });
|
|
14475
|
+
if (!this.host.edges(groupId).has(key)) live.expect.add(key);
|
|
14476
|
+
}
|
|
14477
|
+
/** Farewell edges end: told a moment ago, never came, or back in the group; and a hub no more keeps none. */
|
|
14478
|
+
endFarewells(live, now) {
|
|
14479
|
+
for (const [key, f] of live.farewells) {
|
|
14480
|
+
const back = !live.session.wasRemoved(key);
|
|
14481
|
+
if (!back && live.hub && (f.told ? now - f.told < FAREWELL_LINGER_MS : now - f.since < FAREWELL_WAIT_MS)) continue;
|
|
14482
|
+
live.farewells.delete(key);
|
|
14483
|
+
if (!back) live.farewellQuiet.set(key, now + (f.told ? FAREWELL_TOLD_MS : FAREWELL_RETRY_MS));
|
|
14484
|
+
}
|
|
14485
|
+
for (const [key, until] of live.farewellQuiet) if (until <= now) live.farewellQuiet.delete(key);
|
|
14486
|
+
for (const [key, at] of live.farewellSaid) if (now - at >= FAREWELL_SAY_MS) live.farewellSaid.delete(key);
|
|
14487
|
+
}
|
|
14488
|
+
/**
|
|
14489
|
+
* Someone the chain took out says where it is (`group-sync`): it is handed the commits up to the one that took it
|
|
14490
|
+
* out, and nothing else (`CommunitySession.farewell`). Nothing else it says is heard.
|
|
14491
|
+
*/
|
|
14492
|
+
sayFarewell(groupId, live, key, sync) {
|
|
14493
|
+
const now = this.now(), linkId = this.host.edges(groupId).get(key);
|
|
14494
|
+
if (!linkId || now - (live.farewellSaid.get(key) ?? -Infinity) < FAREWELL_SAY_MS) return;
|
|
14495
|
+
const frames = live.session.farewell(key, sync);
|
|
14496
|
+
if (!frames.length) return;
|
|
14497
|
+
live.farewellSaid.set(key, now);
|
|
14498
|
+
try {
|
|
14499
|
+
for (const frame of frames) this.host.sendOnLink(linkId, frame);
|
|
14500
|
+
} catch {
|
|
14501
|
+
return;
|
|
14502
|
+
}
|
|
14503
|
+
const waited = live.farewells.get(key);
|
|
14504
|
+
if (waited) waited.told = now;
|
|
14505
|
+
traceJoin(groupId, "farewell.sent");
|
|
14317
14506
|
}
|
|
14318
14507
|
noteHubs(live, now) {
|
|
14319
|
-
for (const h of freshHubs(live
|
|
14508
|
+
for (const h of freshHubs(this.hubs(live), now)) live.seenHubs.set(h.key, now);
|
|
14320
14509
|
for (const [key, at] of live.seenHubs) if (now - at > HUB_GRACE_MS) live.seenHubs.delete(key);
|
|
14321
14510
|
}
|
|
14322
14511
|
recentHub(live, key) {
|
|
@@ -14336,7 +14525,11 @@ var Communities = class {
|
|
|
14336
14525
|
const keys = lobbyKeys(live.session.state.rv, groupId, live.session.myKey);
|
|
14337
14526
|
const entries = readLobby(keys, await this.host.resolve(keys.identity.pubKeyZ32, true).catch(() => null) ?? []);
|
|
14338
14527
|
for (const { key, ts } of entries) {
|
|
14339
|
-
if (now - ts > COMMUNITY_TOPOLOGY.lobbyFreshMs || key === live.session.myKey
|
|
14528
|
+
if (now - ts > COMMUNITY_TOPOLOGY.lobbyFreshMs || key === live.session.myKey) continue;
|
|
14529
|
+
if (live.session.wasRemoved(key)) {
|
|
14530
|
+
if (ts + FAREWELL_SKEW_MS > (live.session.outAt(key) ?? Infinity)) this.wantFarewell(groupId, live, key, now);
|
|
14531
|
+
continue;
|
|
14532
|
+
}
|
|
14340
14533
|
live.emptySince = now;
|
|
14341
14534
|
if (!live.members.has(key) && live.members.size >= this.capacity(groupId, live, now)) continue;
|
|
14342
14535
|
if (!live.members.has(key)) {
|
|
@@ -14353,10 +14546,11 @@ var Communities = class {
|
|
|
14353
14546
|
if (s.status === "active") {
|
|
14354
14547
|
if (live.hub) {
|
|
14355
14548
|
this.shed(groupId, live, now);
|
|
14356
|
-
for (const h of freshHubs(live
|
|
14549
|
+
for (const h of freshHubs(this.hubs(live), now)) if (h.key !== s.myKey) wanted.add(h.key);
|
|
14357
14550
|
for (const key of live.members.keys()) wanted.add(key);
|
|
14551
|
+
for (const key of live.farewells.keys()) wanted.add(key);
|
|
14358
14552
|
} else for (const key of live.myHubs) wanted.add(key);
|
|
14359
|
-
for (const [key, id] of existing) if (this.host.linkReady(id, 2) && (live.members.has(key) || this.recentHub(live, key))) wanted.add(key);
|
|
14553
|
+
for (const [key, id] of existing) if (this.host.linkReady(id, 2) && (live.members.has(key) || this.recentHub(live, key)) && !s.wasRemoved(key)) wanted.add(key);
|
|
14360
14554
|
}
|
|
14361
14555
|
for (const [key, since] of live.restored) {
|
|
14362
14556
|
if (!since) live.restored.set(key, now);
|
|
@@ -14411,7 +14605,7 @@ var Communities = class {
|
|
|
14411
14605
|
g: groupId,
|
|
14412
14606
|
host: s.entryKey
|
|
14413
14607
|
};
|
|
14414
|
-
const hubs = doorHubs(live
|
|
14608
|
+
const hubs = doorHubs(this.hubs(live), now, s.myKey);
|
|
14415
14609
|
if (!hubs.includes(s.myKey)) return;
|
|
14416
14610
|
const door = [...hubs].sort()[0], doorSig = [...hubs].sort().join(",");
|
|
14417
14611
|
if (live.doors !== doorSig) {
|
|
@@ -14433,7 +14627,7 @@ var Communities = class {
|
|
|
14433
14627
|
ts
|
|
14434
14628
|
}));
|
|
14435
14629
|
for (const { key, ts } of knocks) {
|
|
14436
|
-
if (now - ts > 18e4 || this.refused.has(key) || live.pendingEntries.has(key) || key === link.host) continue;
|
|
14630
|
+
if (now - ts > 18e4 || this.refused.has(key) || live.pendingEntries.has(key) || key === link.host || key === s.myKey) continue;
|
|
14437
14631
|
const stillKnocking = now - ts < 2 * this.timings.slowKnockMs;
|
|
14438
14632
|
if (rosterHas(s.roster, key)) {
|
|
14439
14633
|
const added = [...s.state.chain].reverse().find((c) => c.k === "add" && c.s === key)?.ts ?? 0;
|
|
@@ -14510,7 +14704,10 @@ var Communities = class {
|
|
|
14510
14704
|
ms: Date.now() - started,
|
|
14511
14705
|
record: n
|
|
14512
14706
|
});
|
|
14513
|
-
|
|
14707
|
+
const others = knocks.filter((k) => k.key !== me && now - k.ts < 2 * this.timings.slowKnockMs).length;
|
|
14708
|
+
const moved = (this.knockingWith.get(group.id) ?? 0) !== others;
|
|
14709
|
+
this.knockingWith.set(group.id, others);
|
|
14710
|
+
if (!this.knocked.has(group.id) || moved) {
|
|
14514
14711
|
this.knocked.add(group.id);
|
|
14515
14712
|
this.host.emit();
|
|
14516
14713
|
}
|
|
@@ -14602,7 +14799,16 @@ var Communities = class {
|
|
|
14602
14799
|
return;
|
|
14603
14800
|
}
|
|
14604
14801
|
traceJoin(g, "welcome.received");
|
|
14605
|
-
const
|
|
14802
|
+
const before = this.live.get(g)?.session, again = !!before;
|
|
14803
|
+
if (before?.status === "active" && before.isMember) {
|
|
14804
|
+
const mine = (chain) => [...chain].reverse().find((c) => c.k === "add" && c.s === before.myKey)?.sig;
|
|
14805
|
+
if (mine(joined.state.chain) === mine(before.state.chain)) {
|
|
14806
|
+
await this.endCheck(group);
|
|
14807
|
+
return;
|
|
14808
|
+
}
|
|
14809
|
+
await this.event(g, "removed", "You were removed from this group", this.now(), joined.state.chain.length - 1);
|
|
14810
|
+
}
|
|
14811
|
+
const back = !!before && before.status !== "lost";
|
|
14606
14812
|
for (const [, id] of this.host.entries(g)) if (id !== linkId) await this.host.closeEdge(id);
|
|
14607
14813
|
const member = {
|
|
14608
14814
|
id: g,
|
|
@@ -14620,9 +14826,11 @@ var Communities = class {
|
|
|
14620
14826
|
live.joinedAt = now;
|
|
14621
14827
|
await this.store.putGroup(member);
|
|
14622
14828
|
if (!again) await this.event(g, "joined", `You joined. ${GROUP_READ_NOTE_COMMUNITY}`, now, joined.state.chain.length - 1);
|
|
14829
|
+
else if (back) await this.event(g, "joined", "You joined again", now, joined.state.chain.length - 1);
|
|
14623
14830
|
this.lastKnock.delete(g);
|
|
14624
14831
|
this.knocked.delete(g);
|
|
14625
14832
|
this.knockAt.delete(g);
|
|
14833
|
+
this.knockingWith.delete(g);
|
|
14626
14834
|
await this.host.closeEdge(linkId);
|
|
14627
14835
|
await this.reconcile(g, live, now);
|
|
14628
14836
|
this.host.emit();
|
|
@@ -14642,12 +14850,17 @@ var Communities = class {
|
|
|
14642
14850
|
/** A frame on an edge, from the member it is pinned to: handled, then relayed on by a hub. */
|
|
14643
14851
|
async handleEdgeFrame(groupId, peerKey, frame) {
|
|
14644
14852
|
const live = this.live.get(groupId);
|
|
14645
|
-
if (!live || !frame || typeof frame !== "object"
|
|
14853
|
+
if (!live || !frame || typeof frame !== "object") return;
|
|
14646
14854
|
const f = frame;
|
|
14855
|
+
if (live.session.wasRemoved(peerKey)) {
|
|
14856
|
+
if (f.t === "group-sync" && live.session.isMember) this.sayFarewell(groupId, live, peerKey, f);
|
|
14857
|
+
return;
|
|
14858
|
+
}
|
|
14647
14859
|
if ((f.t === "group-secret" || f.t === "group-entry") && typeof f.to === "string" && f.to !== live.session.myKey) {
|
|
14648
14860
|
if (!live.hub) return;
|
|
14649
14861
|
const id = `${String(f.t)}:${f.to}:${String(f.h ?? f.x)}`;
|
|
14650
14862
|
if (!this.remember(live, id)) return;
|
|
14863
|
+
if (live.session.wasRemoved(f.to)) return;
|
|
14651
14864
|
const edges = this.host.edges(groupId), direct = edges.get(f.to);
|
|
14652
14865
|
if (direct && this.host.linkReady(direct, 2)) {
|
|
14653
14866
|
this.sendTo(direct, f);
|
|
@@ -14657,10 +14870,10 @@ var Communities = class {
|
|
|
14657
14870
|
return;
|
|
14658
14871
|
}
|
|
14659
14872
|
if (!await live.session.handle(peerKey, frame) || !live.hub) return;
|
|
14660
|
-
for (const [key, id] of this.
|
|
14873
|
+
for (const [key, id] of this.hearers(groupId, f)) if (key !== peerKey) this.sendTo(id, f);
|
|
14661
14874
|
}
|
|
14662
14875
|
peerIsHub(live, key) {
|
|
14663
|
-
return this.recentHub(live, key) && !live.members.has(key);
|
|
14876
|
+
return this.recentHub(live, key) && !live.members.has(key) && !live.session.wasRemoved(key);
|
|
14664
14877
|
}
|
|
14665
14878
|
remember(live, id) {
|
|
14666
14879
|
if (live.relayed.has(id)) return false;
|
|
@@ -14711,21 +14924,24 @@ var Communities = class {
|
|
|
14711
14924
|
await this.store.putGroup(group);
|
|
14712
14925
|
},
|
|
14713
14926
|
broadcast: (frame) => {
|
|
14714
|
-
for (const linkId of this.
|
|
14927
|
+
for (const [, linkId] of this.hearers(id, frame)) this.sendTo(linkId, frame);
|
|
14928
|
+
},
|
|
14929
|
+
direct: (to, frame) => {
|
|
14930
|
+
if (this.mayHear(this.live.get(id), to, frame)) this.sendTo(this.host.edges(id).get(to), frame);
|
|
14715
14931
|
},
|
|
14716
|
-
direct: (to, frame) => this.sendTo(this.host.edges(id).get(to), frame),
|
|
14717
14932
|
addressed: (to, frame) => {
|
|
14718
14933
|
const edges = this.host.edges(id), live = this.live.get(id);
|
|
14719
14934
|
const direct = edges.get(to);
|
|
14935
|
+
if (live?.session.wasRemoved(to)) return;
|
|
14720
14936
|
if (direct && this.host.linkReady(direct, 2)) {
|
|
14721
14937
|
this.sendTo(direct, frame);
|
|
14722
14938
|
return;
|
|
14723
14939
|
}
|
|
14724
14940
|
const hubs = new Set(live ? [...live.seenHubs.keys()].filter((key) => this.recentHub(live, key)) : []);
|
|
14725
|
-
for (const [key, linkId] of edges) if (hubs.has(key) || live?.myHubs.includes(key)) this.sendTo(linkId, frame);
|
|
14941
|
+
for (const [key, linkId] of edges) if ((hubs.has(key) || live?.myHubs.includes(key)) && !live?.session.wasRemoved(key)) this.sendTo(linkId, frame);
|
|
14726
14942
|
},
|
|
14727
14943
|
message: async (m) => {
|
|
14728
|
-
const timestamp =
|
|
14944
|
+
const timestamp = m.timestamp;
|
|
14729
14945
|
const mentioned = m.sender !== session.myKey && mentionsMember(m.mentions, session.myKey);
|
|
14730
14946
|
const stored = await this.host.storeMessage({
|
|
14731
14947
|
linkId: MESSAGE_LINK$1(id),
|
|
@@ -14740,26 +14956,28 @@ var Communities = class {
|
|
|
14740
14956
|
...m.forwarded && { forwarded: m.forwarded },
|
|
14741
14957
|
...m.card && { card: m.card }
|
|
14742
14958
|
});
|
|
14743
|
-
|
|
14744
|
-
|
|
14959
|
+
const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, this.now());
|
|
14960
|
+
this.lastMessageAt.set(id, Math.max(this.lastMessageAt.get(id) ?? 0, came));
|
|
14745
14961
|
if (mentioned) this.lastMentionAt.set(id, Math.max(this.lastMentionAt.get(id) ?? 0, came));
|
|
14746
14962
|
if (m.sender !== session.myKey) this.lastPeerMessageAt.set(id, Math.max(this.lastPeerMessageAt.get(id) ?? 0, came));
|
|
14963
|
+
noteCame(this.stored.get(id), came, m.sender !== session.myKey, mentioned);
|
|
14747
14964
|
},
|
|
14748
14965
|
app: (m) => this.deliver(id, () => this.host.communityApp?.(id, m.sender, m.frame)),
|
|
14749
14966
|
pair: (m) => this.deliver(id, () => this.host.communityPair?.(id, m.sender, m.payload)),
|
|
14750
14967
|
changed: () => {
|
|
14751
14968
|
this.membershipChanged(id);
|
|
14752
14969
|
},
|
|
14753
|
-
metaChanged: (by, change) => {
|
|
14970
|
+
metaChanged: (by, change, at) => {
|
|
14754
14971
|
const name = by === session.myKey ? "You" : session.state.nicks[by] ?? `Member ${by.slice(0, 8)}`;
|
|
14972
|
+
const when = receivedTimestamp(at, this.now());
|
|
14755
14973
|
(async () => {
|
|
14756
|
-
for (const line of metaLines(name, change, session.name)) await this.event(id, line.event, line.text,
|
|
14974
|
+
for (const line of metaLines(name, change, session.name)) await this.event(id, line.event, line.text, when, session.epoch, by);
|
|
14757
14975
|
this.host.emit();
|
|
14758
14976
|
})();
|
|
14759
14977
|
},
|
|
14760
14978
|
clock: () => this.now(),
|
|
14761
14979
|
relay: (frame) => {
|
|
14762
|
-
if (this.live.get(id)?.hub) for (const linkId of this.
|
|
14980
|
+
if (this.live.get(id)?.hub) for (const [, linkId] of this.hearers(id, frame)) this.sendTo(linkId, frame);
|
|
14763
14981
|
}
|
|
14764
14982
|
});
|
|
14765
14983
|
this.live.set(id, {
|
|
@@ -14769,6 +14987,8 @@ var Communities = class {
|
|
|
14769
14987
|
beacon: [],
|
|
14770
14988
|
head: null,
|
|
14771
14989
|
lastBeaconRead: 0,
|
|
14990
|
+
beaconKnown: false,
|
|
14991
|
+
beaconFailedAt: 0,
|
|
14772
14992
|
lastBeaconWrite: 0,
|
|
14773
14993
|
lastBeaconTry: 0,
|
|
14774
14994
|
hubCandidateAt: 0,
|
|
@@ -14786,6 +15006,9 @@ var Communities = class {
|
|
|
14786
15006
|
lingering: /* @__PURE__ */ new Map(),
|
|
14787
15007
|
seenHubs: /* @__PURE__ */ new Map(),
|
|
14788
15008
|
restored: /* @__PURE__ */ new Map(),
|
|
15009
|
+
farewells: /* @__PURE__ */ new Map(),
|
|
15010
|
+
farewellQuiet: /* @__PURE__ */ new Map(),
|
|
15011
|
+
farewellSaid: /* @__PURE__ */ new Map(),
|
|
14789
15012
|
knocksScanned: false,
|
|
14790
15013
|
knockCursor: 0,
|
|
14791
15014
|
lastShardPoll: 0,
|
|
@@ -14814,22 +15037,17 @@ var Communities = class {
|
|
|
14814
15037
|
if (statusChanged && s.status === "removed") await this.event(groupId, "removed", s.state.statusReason ?? "You were removed from this group", when, s.epoch);
|
|
14815
15038
|
else if (statusChanged && s.status === "forked") await this.event(groupId, "forked", s.state.statusReason ?? "The membership history forked", when, s.epoch);
|
|
14816
15039
|
else if (s.status === "active" && before !== after) {
|
|
14817
|
-
|
|
14818
|
-
for (const [key] of
|
|
14819
|
-
|
|
14820
|
-
if (top.k === "
|
|
15040
|
+
const at = receivedTimestamp(top.ts, when);
|
|
15041
|
+
for (const [key] of after) if (!rosterHas(before, key) && key !== s.myKey) await this.event(groupId, "joined", `${name(key)} joined`, at, top.e, key);
|
|
15042
|
+
for (const [key] of before) if (!rosterHas(after, key)) await this.event(groupId, "gone", `${name(key)} is no longer a member`, at, top.e, key);
|
|
15043
|
+
if (top.k === "role" && before.find(([k]) => k === top.s)?.[1] !== "admin") await this.event(groupId, "admin", `${name(top.s)} ${top.s === s.myKey ? "are" : "is"} now the admin`, at, top.e, top.s);
|
|
15044
|
+
if (top.k === "rotate") await this.event(groupId, "rotated", "Keys rotated: a fresh epoch", at, top.e);
|
|
14821
15045
|
}
|
|
14822
15046
|
this.host.emit();
|
|
14823
15047
|
}
|
|
14824
|
-
/**
|
|
14825
|
-
* A line in the group's history. Its id carries its time, and the engine's clock moves once a tick: two
|
|
14826
|
-
* lines of one kind within a tick (a picture changed, then removed) get distinct times, a millisecond
|
|
14827
|
-
* apart, or the store would keep the first and drop the second as already there.
|
|
14828
|
-
*/
|
|
15048
|
+
/** A line in the group's history. Its id carries its time: two lines at the same moment get distinct ones (`eventTime`). */
|
|
14829
15049
|
async event(groupId, event, text, timestamp, epoch, member) {
|
|
14830
|
-
|
|
14831
|
-
if (timestamp <= last) timestamp = last + 1;
|
|
14832
|
-
this.lastEventAt.set(groupId, Math.max(last, timestamp));
|
|
15050
|
+
timestamp = eventTime(this.eventTimes, groupId, timestamp);
|
|
14833
15051
|
await this.host.storeMessage({
|
|
14834
15052
|
linkId: MESSAGE_LINK$1(groupId),
|
|
14835
15053
|
id: `event:${epoch}:${event}:${member ?? ""}:${timestamp}`,
|
|
@@ -14841,7 +15059,7 @@ var Communities = class {
|
|
|
14841
15059
|
via: "datalink"
|
|
14842
15060
|
});
|
|
14843
15061
|
}
|
|
14844
|
-
|
|
15062
|
+
eventTimes = /* @__PURE__ */ new Map();
|
|
14845
15063
|
};
|
|
14846
15064
|
//#endregion
|
|
14847
15065
|
//#region ../browser/src/engine/meshHubs.ts
|
|
@@ -15481,12 +15699,46 @@ var mentionAt = (map, groupId) => {
|
|
|
15481
15699
|
return at ? { lastMentionAt: at } : {};
|
|
15482
15700
|
};
|
|
15483
15701
|
/**
|
|
15484
|
-
* When a
|
|
15485
|
-
*
|
|
15486
|
-
*
|
|
15487
|
-
*
|
|
15702
|
+
* When a message counts as having come, for the list of groups, unread and the "@": when it reached me, never when
|
|
15703
|
+
* its sender says it was written (a member's clock ahead would keep the group unread and on top; written before I
|
|
15704
|
+
* last looked but handed to me since, a catch-up is news here). A copy I had already (`stored` false) or one
|
|
15705
|
+
* completed in place moves nothing forward: its own time, now at the latest.
|
|
15488
15706
|
*/
|
|
15489
|
-
var cameAt = (timestamp, stored, now) => stored === false ?
|
|
15707
|
+
var cameAt = (timestamp, stored, now) => stored === false ? Math.min(timestamp, now) : now;
|
|
15708
|
+
/**
|
|
15709
|
+
* The time a line of a group's history is kept under: its own, or the next millisecond no other line of this run took.
|
|
15710
|
+
* A line's id carries its time, and two lines can share a moment (two members gone in one change, a picture changed
|
|
15711
|
+
* then removed within a tick): the store would keep the first and drop the second as already there. Only a time
|
|
15712
|
+
* already taken moves: a line older than the last one written (a rename read after a later commit, in a catch-up)
|
|
15713
|
+
* keeps its place.
|
|
15714
|
+
*/
|
|
15715
|
+
function eventTime(taken, groupId, timestamp) {
|
|
15716
|
+
let times = taken.get(groupId);
|
|
15717
|
+
if (!times) taken.set(groupId, times = /* @__PURE__ */ new Set());
|
|
15718
|
+
while (times.has(timestamp)) timestamp++;
|
|
15719
|
+
times.add(timestamp);
|
|
15720
|
+
if (times.size > 1024) times.delete(times.values().next().value);
|
|
15721
|
+
return timestamp;
|
|
15722
|
+
}
|
|
15723
|
+
/**
|
|
15724
|
+
* Keeps on the group's record when a member's message came, where that is later than what it holds: the record is
|
|
15725
|
+
* saved with the session right after (a message taken moves its state), so a message handed to me late still makes
|
|
15726
|
+
* the group unread after the app starts again. Read from the history alone, by its own time, it sorted among what I
|
|
15727
|
+
* had read and the mark was gone on the next start.
|
|
15728
|
+
*/
|
|
15729
|
+
function noteCame(group, came, peer, mentioned) {
|
|
15730
|
+
if (!group) return;
|
|
15731
|
+
if (peer && came > (group.came?.peer ?? 0)) group.came = {
|
|
15732
|
+
...group.came,
|
|
15733
|
+
peer: came
|
|
15734
|
+
};
|
|
15735
|
+
if (mentioned && came > (group.came?.mention ?? 0)) group.came = {
|
|
15736
|
+
...group.came,
|
|
15737
|
+
mention: came
|
|
15738
|
+
};
|
|
15739
|
+
}
|
|
15740
|
+
/** What `load` starts the unread marks from: the history's own times, or when its latest came if that was later. */
|
|
15741
|
+
var cameOrWritten = (written, came) => written === void 0 && came === void 0 ? void 0 : Math.max(written ?? 0, came ?? 0);
|
|
15490
15742
|
/** The view's `lastPeerMessageAt`, when there is one. */
|
|
15491
15743
|
var peerMessageAt = (map, groupId) => {
|
|
15492
15744
|
const at = map.get(groupId);
|
|
@@ -15500,6 +15752,7 @@ var ENTRY_TIMINGS = {
|
|
|
15500
15752
|
slowKnockMs: 2e4,
|
|
15501
15753
|
patienceMs: 12e4
|
|
15502
15754
|
};
|
|
15755
|
+
var KNOCK_VERIFY_SPREAD_MS = 700;
|
|
15503
15756
|
/** Entry sessions the admin runs at once; a joiner who does not finish in time is not answered again for a while. */
|
|
15504
15757
|
var MAX_PENDING_ENTRIES = 4;
|
|
15505
15758
|
var ENTRY_TIMEOUT_MS = 18e4;
|
|
@@ -15594,6 +15847,10 @@ var Groups = class {
|
|
|
15594
15847
|
/** Admin side: until when a group's link is looked at the warm pace. */
|
|
15595
15848
|
warmUntil = /* @__PURE__ */ new Map();
|
|
15596
15849
|
lastKnock = /* @__PURE__ */ new Map();
|
|
15850
|
+
/** Joiner side: when a knock just written is read back (`KNOCK_VERIFY_MS`); none once it was found there. */
|
|
15851
|
+
knockCheckAt = /* @__PURE__ */ new Map();
|
|
15852
|
+
/** Joiner side: knocks written in a row without one found in the record since (`KNOCK_VERIFIES`). */
|
|
15853
|
+
knockRewrites = /* @__PURE__ */ new Map();
|
|
15597
15854
|
/** Per group, members met over their admission a moment ago: their edge is opened expecting them. */
|
|
15598
15855
|
justMet = /* @__PURE__ */ new Map();
|
|
15599
15856
|
/** Joiner side: groups whose knock is published, for the stage the joiner is shown. */
|
|
@@ -15651,9 +15908,11 @@ var Groups = class {
|
|
|
15651
15908
|
const history = await this.store.getMessages(MESSAGE_LINK(group.id)), said = history.filter((m) => !m.event && !m.groupPay);
|
|
15652
15909
|
const last = said[said.length - 1], lastPeer = [...said].reverse().find((m) => m.sender !== "me");
|
|
15653
15910
|
if (last) this.lastMessageAt.set(group.id, last.timestamp);
|
|
15654
|
-
|
|
15911
|
+
const peerAt = cameOrWritten(lastPeer?.timestamp, lastPeer ? group.came?.peer : void 0);
|
|
15912
|
+
if (peerAt) this.lastPeerMessageAt.set(group.id, peerAt);
|
|
15655
15913
|
const mention = [...history].reverse().find((m) => m.mentioned);
|
|
15656
|
-
|
|
15914
|
+
const mentionAt = cameOrWritten(mention?.timestamp, mention ? group.came?.mention : void 0);
|
|
15915
|
+
if (mentionAt) this.lastMentionAt.set(group.id, mentionAt);
|
|
15657
15916
|
}
|
|
15658
15917
|
for (const id of this.sessions.keys()) this.reconcileEdges(id);
|
|
15659
15918
|
for (const group of this.stored.values()) for (const [, linkId] of this.host.entries(group.id)) if (!group.invitation?.entry || group.invitation.linkId !== linkId) await this.host.closeEdge(linkId);
|
|
@@ -16035,6 +16294,8 @@ var Groups = class {
|
|
|
16035
16294
|
this.lastTold.delete(groupId);
|
|
16036
16295
|
this.pendingEntries.delete(groupId);
|
|
16037
16296
|
this.knocked.delete(groupId);
|
|
16297
|
+
this.knockCheckAt.delete(groupId);
|
|
16298
|
+
this.knockRewrites.delete(groupId);
|
|
16038
16299
|
this.justMet.delete(groupId);
|
|
16039
16300
|
this.relayAsked.delete(groupId);
|
|
16040
16301
|
this.hubs.forget(groupId);
|
|
@@ -16103,7 +16364,7 @@ var Groups = class {
|
|
|
16103
16364
|
if (existing?.state?.status === "active" || existing?.invitation?.entry === link.host) return link.g;
|
|
16104
16365
|
if (existing?.invitation?.seedB64 && !existing.invitation.entry) throw new Error("You are already joining this group");
|
|
16105
16366
|
if (existing) await this.forget(link.g);
|
|
16106
|
-
const seedB64 =
|
|
16367
|
+
const seedB64 = dialedKey(link);
|
|
16107
16368
|
const linkId = await this.host.openEntry(link, "guest", seedB64, link.host);
|
|
16108
16369
|
const group = {
|
|
16109
16370
|
id: link.g,
|
|
@@ -16138,7 +16399,7 @@ var Groups = class {
|
|
|
16138
16399
|
}
|
|
16139
16400
|
if (group.invitation?.entry && !group.state) {
|
|
16140
16401
|
const every = now - group.createdAt > this.timings.patienceMs ? this.timings.slowKnockMs : this.timings.knockMs;
|
|
16141
|
-
if (!(this.host.linkReady(group.invitation.linkId) || !!this.host.linkSeen?.(group.invitation.linkId)) && now - (this.lastKnock.get(group.id) ?? 0) >= every) await this.knock(group, now).catch(() => {});
|
|
16402
|
+
if (!(this.host.linkReady(group.invitation.linkId) || !!this.host.linkSeen?.(group.invitation.linkId)) && (now - (this.lastKnock.get(group.id) ?? 0) >= every || now >= (this.knockCheckAt.get(group.id) ?? Infinity))) await this.knock(group, now).catch(() => {});
|
|
16142
16403
|
continue;
|
|
16143
16404
|
}
|
|
16144
16405
|
const session = this.sessions.get(group.id);
|
|
@@ -16251,15 +16512,29 @@ var Groups = class {
|
|
|
16251
16512
|
host: invitation.entry
|
|
16252
16513
|
}, identity = knockIdentity(link);
|
|
16253
16514
|
const started = Date.now();
|
|
16515
|
+
this.knockCheckAt.delete(group.id);
|
|
16254
16516
|
const existing = readKnocks(link, await this.host.resolve(identity.pubKeyZ32) ?? []);
|
|
16517
|
+
const key = identityFromSeedB64(invitation.seedB64).pubKeyZ32, mine = existing.find((k) => k.key === key);
|
|
16255
16518
|
traceJoin(group.id, "knock.read", {
|
|
16256
16519
|
ms: Date.now() - started,
|
|
16257
|
-
others: existing.length
|
|
16520
|
+
others: existing.length,
|
|
16521
|
+
...mine ? { mine: now - mine.ts } : {}
|
|
16258
16522
|
});
|
|
16523
|
+
if (mine && now - mine.ts >= 0 && now - mine.ts < 3e4) {
|
|
16524
|
+
this.knockRewrites.delete(group.id);
|
|
16525
|
+
if (!this.knocked.has(group.id)) {
|
|
16526
|
+
this.knocked.add(group.id);
|
|
16527
|
+
this.host.emit();
|
|
16528
|
+
}
|
|
16529
|
+
return;
|
|
16530
|
+
}
|
|
16259
16531
|
await this.host.publish(identity, knockRecords(link, mergeKnocks(existing, {
|
|
16260
|
-
key
|
|
16532
|
+
key,
|
|
16261
16533
|
ts: now
|
|
16262
16534
|
}, now)));
|
|
16535
|
+
const rewrites = (this.knockRewrites.get(group.id) ?? 0) + 1;
|
|
16536
|
+
this.knockRewrites.set(group.id, rewrites);
|
|
16537
|
+
if (rewrites <= 2) this.knockCheckAt.set(group.id, now + 800 + gossipStart(key) % KNOCK_VERIFY_SPREAD_MS);
|
|
16263
16538
|
traceJoin(group.id, "knock.published", { ms: Date.now() - started });
|
|
16264
16539
|
if (!this.knocked.has(group.id)) {
|
|
16265
16540
|
this.knocked.add(group.id);
|
|
@@ -16745,7 +17020,7 @@ var Groups = class {
|
|
|
16745
17020
|
}
|
|
16746
17021
|
},
|
|
16747
17022
|
message: async (m) => {
|
|
16748
|
-
const timestamp =
|
|
17023
|
+
const timestamp = m.timestamp;
|
|
16749
17024
|
const mentioned = m.sender !== session.myKey && mentionsMember(m.mentions, session.myKey);
|
|
16750
17025
|
const message = {
|
|
16751
17026
|
linkId: MESSAGE_LINK(state.id),
|
|
@@ -16761,10 +17036,11 @@ var Groups = class {
|
|
|
16761
17036
|
...m.card && { card: m.card }
|
|
16762
17037
|
};
|
|
16763
17038
|
const stored = m.completes && this.host.completeMessage ? (await this.host.completeMessage(message), false) : await this.host.storeMessage(message);
|
|
16764
|
-
|
|
16765
|
-
|
|
17039
|
+
const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, this.now());
|
|
17040
|
+
this.lastMessageAt.set(state.id, Math.max(this.lastMessageAt.get(state.id) ?? 0, came));
|
|
16766
17041
|
if (mentioned) this.lastMentionAt.set(state.id, Math.max(this.lastMentionAt.get(state.id) ?? 0, came));
|
|
16767
17042
|
if (m.sender !== session.myKey) this.lastPeerMessageAt.set(state.id, Math.max(this.lastPeerMessageAt.get(state.id) ?? 0, came));
|
|
17043
|
+
noteCame(this.stored.get(state.id), came, m.sender !== session.myKey, mentioned);
|
|
16768
17044
|
this.typings.messageFrom(state.id, m.sender);
|
|
16769
17045
|
},
|
|
16770
17046
|
edit: (e) => this.host.groupEdit?.(state.id, e),
|
|
@@ -16772,8 +17048,8 @@ var Groups = class {
|
|
|
16772
17048
|
if (session.status === "removed" && this.hubs.isHub(state.id) && !this.removedAt.has(state.id)) this.removedAt.set(state.id, this.now());
|
|
16773
17049
|
this.membershipChanged(state.id);
|
|
16774
17050
|
},
|
|
16775
|
-
metaChanged: (by, change) => {
|
|
16776
|
-
this.metaChanged(state.id, session, by, change);
|
|
17051
|
+
metaChanged: (by, change, at) => {
|
|
17052
|
+
this.metaChanged(state.id, session, by, change, at);
|
|
16777
17053
|
}
|
|
16778
17054
|
});
|
|
16779
17055
|
this.sessions.set(state.id, session);
|
|
@@ -16795,7 +17071,7 @@ var Groups = class {
|
|
|
16795
17071
|
top
|
|
16796
17072
|
});
|
|
16797
17073
|
const name = (key) => key === session.myKey ? "You" : session.state.nicks[key] ?? `Member ${key.slice(0, 8)}`;
|
|
16798
|
-
const when = Date.now();
|
|
17074
|
+
const when = moved ? receivedTimestamp(top.ts) : Date.now();
|
|
16799
17075
|
if (session.status === "removed") {
|
|
16800
17076
|
if (statusChanged) await this.event(groupId, "removed", session.state.statusReason ?? "You were removed from this group", when, session.epoch);
|
|
16801
17077
|
} else if (session.status === "forked") {
|
|
@@ -16817,9 +17093,11 @@ var Groups = class {
|
|
|
16817
17093
|
this.host.membersChanged?.(groupId);
|
|
16818
17094
|
this.host.emit();
|
|
16819
17095
|
}
|
|
16820
|
-
|
|
17096
|
+
/** `at`: when the admin signed the statement (the line's time, as a commit's is its line's). */
|
|
17097
|
+
async metaChanged(groupId, session, by, change, at) {
|
|
16821
17098
|
const name = by === session.myKey ? "You" : session.state.nicks[by] ?? `Member ${by.slice(0, 8)}`;
|
|
16822
|
-
|
|
17099
|
+
const when = at === void 0 ? Date.now() : receivedTimestamp(at);
|
|
17100
|
+
for (const line of metaLines(name, change, session.name)) await this.event(groupId, line.event, line.text, when, session.epoch, by);
|
|
16823
17101
|
this.host.emit();
|
|
16824
17102
|
}
|
|
16825
17103
|
/**
|
|
@@ -16854,11 +17132,9 @@ var Groups = class {
|
|
|
16854
17132
|
this.hubs.met(groupId, key, this.now());
|
|
16855
17133
|
}
|
|
16856
17134
|
/** `member`: whom it is about, so the apps can name them as they are known now, not as they were then. */
|
|
16857
|
-
/** A line in the group's history; lines at the same time (two members gone in one change) get distinct ones, see `
|
|
17135
|
+
/** A line in the group's history; lines at the same time (two members gone in one change) get distinct ones, see `eventTime`. */
|
|
16858
17136
|
async event(groupId, event, text, timestamp, epoch, member) {
|
|
16859
|
-
|
|
16860
|
-
if (timestamp <= last) timestamp = last + 1;
|
|
16861
|
-
this.lastEventAt.set(groupId, Math.max(last, timestamp));
|
|
17137
|
+
timestamp = eventTime(this.eventTimes, groupId, timestamp);
|
|
16862
17138
|
await this.host.storeMessage({
|
|
16863
17139
|
linkId: MESSAGE_LINK(groupId),
|
|
16864
17140
|
id: `event:${epoch}:${event}:${timestamp}`,
|
|
@@ -16870,7 +17146,7 @@ var Groups = class {
|
|
|
16870
17146
|
via: "datalink"
|
|
16871
17147
|
});
|
|
16872
17148
|
}
|
|
16873
|
-
|
|
17149
|
+
eventTimes = /* @__PURE__ */ new Map();
|
|
16874
17150
|
};
|
|
16875
17151
|
//#endregion
|
|
16876
17152
|
//#region ../browser/src/engine/groupEdges.ts
|
|
@@ -17379,7 +17655,7 @@ var GroupEdits = class {
|
|
|
17379
17655
|
if (!takes(current) || (current.edit?.seq ?? 0) >= edit.e) return null;
|
|
17380
17656
|
const next = withEdit(current, {
|
|
17381
17657
|
seq: edit.e,
|
|
17382
|
-
at:
|
|
17658
|
+
at: heardTime(edit.ts),
|
|
17383
17659
|
text: edit.m,
|
|
17384
17660
|
card: edit.sc
|
|
17385
17661
|
});
|
|
@@ -17876,6 +18152,14 @@ var POLL_IDLE_MS = 3e5;
|
|
|
17876
18152
|
var RENEW_AFTER_MS = 3456e5;
|
|
17877
18153
|
/** My pointer is published again this often while something is held, in case a relay forgot it. */
|
|
17878
18154
|
var REPUBLISH_MS = 36e5;
|
|
18155
|
+
/**
|
|
18156
|
+
* A held item's address has this long to answer and be read whole, plus the time its declared size takes at the
|
|
18157
|
+
* slowest pace worth waiting for (64 KiB a second): about 21 s for a manifest, two and a half minutes for the largest
|
|
18158
|
+
* item. Past it the read is given up as any failed one is, and the next poll starts there again.
|
|
18159
|
+
*/
|
|
18160
|
+
var HOLD_FETCH_MS = 2e4;
|
|
18161
|
+
var HOLD_FETCH_BYTES_PER_S = 65536;
|
|
18162
|
+
var holdFetchTimeoutMs = (max) => HOLD_FETCH_MS + Math.ceil(max / HOLD_FETCH_BYTES_PER_S) * 1e3;
|
|
17879
18163
|
var hex$2 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
17880
18164
|
/**
|
|
17881
18165
|
* Store-and-forward for away contacts (WISP 4xx, `hold/1`), on both ends of every chat that turned it
|
|
@@ -18409,7 +18693,7 @@ var HoldEngine = class {
|
|
|
18409
18693
|
});
|
|
18410
18694
|
if (manifest.header.kind !== "manifest") throw new HoldRefusedError("format", "Not a manifest");
|
|
18411
18695
|
mailbox = manifest.header.mailbox;
|
|
18412
|
-
entries = readManifest(manifest.header.meta).filter(([seq]) => seq > this.state(linkId).inSeq);
|
|
18696
|
+
entries = readManifest(manifest.header.meta).filter(([seq]) => seq > this.state(linkId).inSeq).sort((a, b) => a[0] - b[0]);
|
|
18413
18697
|
} catch (error) {
|
|
18414
18698
|
if (error instanceof HoldRefusedError) {
|
|
18415
18699
|
await this.save(linkId, {
|
|
@@ -18527,11 +18811,33 @@ var HoldEngine = class {
|
|
|
18527
18811
|
}
|
|
18528
18812
|
this.host.changed();
|
|
18529
18813
|
}
|
|
18530
|
-
/**
|
|
18814
|
+
/**
|
|
18815
|
+
* A presigned address read whole, but never past `max` bytes: what the manifest declared, not what a server sends.
|
|
18816
|
+
* And never past `holdFetchTimeoutMs(max)`: a request that hangs (no answer, or a body that stops) is aborted and
|
|
18817
|
+
* fails as a network problem does. Before, one hung request held the pickup, and every chat's after it, for good.
|
|
18818
|
+
*/
|
|
18531
18819
|
async fetchBytes(url, max) {
|
|
18820
|
+
const abort = new AbortController();
|
|
18821
|
+
let timer;
|
|
18822
|
+
const late = new Promise((_, reject) => {
|
|
18823
|
+
timer = setTimeout(() => {
|
|
18824
|
+
reject(/* @__PURE__ */ new Error("The storage did not answer in time"));
|
|
18825
|
+
abort.abort();
|
|
18826
|
+
}, holdFetchTimeoutMs(max));
|
|
18827
|
+
});
|
|
18828
|
+
const read = this.readBytes(url, max, abort.signal);
|
|
18829
|
+
read.catch(() => {});
|
|
18830
|
+
try {
|
|
18831
|
+
return await Promise.race([read, late]);
|
|
18832
|
+
} finally {
|
|
18833
|
+
clearTimeout(timer);
|
|
18834
|
+
}
|
|
18835
|
+
}
|
|
18836
|
+
async readBytes(url, max, signal) {
|
|
18532
18837
|
const response = await (this.host.fetch ?? ((...args) => fetch(...args)))(url, {
|
|
18533
18838
|
cache: "no-store",
|
|
18534
|
-
credentials: "omit"
|
|
18839
|
+
credentials: "omit",
|
|
18840
|
+
signal
|
|
18535
18841
|
});
|
|
18536
18842
|
if (!response.ok) throw new Error(`The storage answered ${response.status}`);
|
|
18537
18843
|
if (Number(response.headers.get("content-length")) > max) throw new HoldRefusedError("size", "The held item is larger than the manifest said");
|
|
@@ -18541,6 +18847,9 @@ var HoldEngine = class {
|
|
|
18541
18847
|
return bytes;
|
|
18542
18848
|
}
|
|
18543
18849
|
const reader = response.body.getReader();
|
|
18850
|
+
signal.addEventListener("abort", () => {
|
|
18851
|
+
reader.cancel().catch(() => {});
|
|
18852
|
+
}, { once: true });
|
|
18544
18853
|
const chunks = [];
|
|
18545
18854
|
let size = 0;
|
|
18546
18855
|
for (let next = await reader.read(); !next.done; next = await reader.read()) {
|
|
@@ -23205,7 +23514,11 @@ var WalletSetup = class {
|
|
|
23205
23514
|
* The wasm (about 1 MB gzip) loads on the first chat that starts an endpoint,
|
|
23206
23515
|
* never on the app's first load.
|
|
23207
23516
|
*/
|
|
23208
|
-
/**
|
|
23517
|
+
/**
|
|
23518
|
+
* n0's public relays, the same ones the Desktop's Iroh homes on by default. Iroh itself names them with the trailing
|
|
23519
|
+
* dot of a full domain name (`use1-1.relay.n0.iroh.link.`), and compares relay URLs as text; here they have none,
|
|
23520
|
+
* because WebKit opens no host name that ends in a dot. `BROWSER_SPELLING` is what keeps the two one relay.
|
|
23521
|
+
*/
|
|
23209
23522
|
var DEFAULT_IROH_RELAYS = [
|
|
23210
23523
|
"https://use1-1.relay.n0.iroh.link/",
|
|
23211
23524
|
"https://euc1-1.relay.n0.iroh.link/",
|
|
@@ -23213,6 +23526,12 @@ var DEFAULT_IROH_RELAYS = [
|
|
|
23213
23526
|
"https://usw1-1.relay.n0.iroh.link/"
|
|
23214
23527
|
];
|
|
23215
23528
|
/**
|
|
23529
|
+
* The spelling of every relay URL handed to the wasm Iroh (`irohRelayUrl`): the relays it homes on and a contact's
|
|
23530
|
+
* relay before a dial. One spelling, so a contact on this endpoint's relay is reached over the connection already
|
|
23531
|
+
* open to it; without the dot, so Safari can open it (a Desktop's record names its relay with the dot).
|
|
23532
|
+
*/
|
|
23533
|
+
var BROWSER_SPELLING = "plain";
|
|
23534
|
+
/**
|
|
23216
23535
|
* Why a relay URL cannot be used, or null. TLS, except on this machine (a local `iroh-relay --dev`): a page
|
|
23217
23536
|
* may not open plain WebSockets elsewhere, and the relay's operator would see traffic metadata in the clear.
|
|
23218
23537
|
*/
|
|
@@ -23310,7 +23629,7 @@ var IrohWebChannel = class {
|
|
|
23310
23629
|
async function createIrohWebEndpoint(seedB64, options = {}) {
|
|
23311
23630
|
const seed = fromBase64Url(seedB64);
|
|
23312
23631
|
if (seed.length !== 32) throw new Error("Invalid transport seed");
|
|
23313
|
-
const relays = [...options.relays?.length ? options.relays : DEFAULT_IROH_RELAYS];
|
|
23632
|
+
const relays = [...new Set((options.relays?.length ? options.relays : DEFAULT_IROH_RELAYS).map((relay) => irohRelayUrl(relay, BROWSER_SPELLING)))];
|
|
23314
23633
|
const node = await (await (options.load ?? loadIrohWasm)()).IrohNode.start(seed, relays, options.onlineMs ?? 1e4);
|
|
23315
23634
|
const channels = /* @__PURE__ */ new Set();
|
|
23316
23635
|
let stopped = false;
|
|
@@ -23340,7 +23659,7 @@ async function createIrohWebEndpoint(seedB64, options = {}) {
|
|
|
23340
23659
|
},
|
|
23341
23660
|
async connect(descriptor) {
|
|
23342
23661
|
if (stopped) throw new Error("Iroh endpoint is stopped");
|
|
23343
|
-
const conn = await node.connect(descriptor, options.connectMs ?? 2e4);
|
|
23662
|
+
const conn = await node.connect(irohDescriptor(descriptor, BROWSER_SPELLING), options.connectMs ?? 2e4);
|
|
23344
23663
|
if (stopped) {
|
|
23345
23664
|
conn.close();
|
|
23346
23665
|
throw new Error("Iroh endpoint is stopped");
|
|
@@ -23520,12 +23839,29 @@ var GhostlyNode = class GhostlyNode {
|
|
|
23520
23839
|
relays;
|
|
23521
23840
|
/** Other profiles of this device, looked at for new messages (WISP 04 § Checking other profiles). */
|
|
23522
23841
|
profilePeek;
|
|
23842
|
+
/** Whether this device's WebRTC attempts say direct connections are blocked on its network (`directPath.ts`). */
|
|
23843
|
+
directPath = new DirectPathWatch(() => {
|
|
23844
|
+
if (!this.shuttingDown) this.emitState();
|
|
23845
|
+
});
|
|
23846
|
+
/**
|
|
23847
|
+
* Edges of groups whose WebRTC did not connect in this run of the app (`edgeWithoutRtc`): they go on as an app with
|
|
23848
|
+
* no WebRTC does, over a native transport. Not kept: the next start tries WebRTC first again.
|
|
23849
|
+
*/
|
|
23850
|
+
edgeRtcOff = /* @__PURE__ */ new Set();
|
|
23523
23851
|
pollIntervals;
|
|
23524
23852
|
localFetch;
|
|
23525
23853
|
links = /* @__PURE__ */ new Map();
|
|
23526
23854
|
services = [];
|
|
23527
23855
|
activeLinkId = null;
|
|
23528
|
-
|
|
23856
|
+
/**
|
|
23857
|
+
* Native listeners start one at a time per transport (`ensureNativeEndpoints`): their slots are counted per transport.
|
|
23858
|
+
* Not one queue for all: a transport slow to start held every other chat's listeners of every kind behind it.
|
|
23859
|
+
*/
|
|
23860
|
+
nativeQueues = /* @__PURE__ */ new Map();
|
|
23861
|
+
/** Every listener start asked for so far has ended. */
|
|
23862
|
+
get nativeQueue() {
|
|
23863
|
+
return Promise.all(this.nativeQueues.values()).then(() => {});
|
|
23864
|
+
}
|
|
23529
23865
|
shuttingDown = false;
|
|
23530
23866
|
/** `start` has put the kept transfers back (EngineState.transfersRestored). */
|
|
23531
23867
|
transfersRestored = false;
|
|
@@ -23551,11 +23887,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
23551
23887
|
cueFeedback({ cue, key }, linkId) {
|
|
23552
23888
|
this.feedback("cue", cue + ":" + key, linkId, false, cue);
|
|
23553
23889
|
}
|
|
23554
|
-
/** The newest time
|
|
23890
|
+
/** The newest time written in each chat while this engine runs: a message written well before it is a catch-up. */
|
|
23555
23891
|
newestAt = /* @__PURE__ */ new Map();
|
|
23556
|
-
/**
|
|
23892
|
+
/** The last place given in each chat while this engine runs (`arrivalKey`). */
|
|
23893
|
+
placedAt = /* @__PURE__ */ new Map();
|
|
23894
|
+
/** Only a new message, or mine going out, says anything (engine/attention.ts): judged by when it was written. */
|
|
23557
23895
|
messageFeedback(type, message, newest = this.newestAt.get(message.linkId)) {
|
|
23558
|
-
const attention = messageAttention(type,
|
|
23896
|
+
const attention = messageAttention(type, {
|
|
23897
|
+
...message,
|
|
23898
|
+
timestamp: shownTime(message)
|
|
23899
|
+
}, {
|
|
23559
23900
|
startedAt: this.feedbackStartedAt,
|
|
23560
23901
|
newest,
|
|
23561
23902
|
now: Date.now()
|
|
@@ -24409,7 +24750,12 @@ var GhostlyNode = class GhostlyNode {
|
|
|
24409
24750
|
},
|
|
24410
24751
|
messages: (groupId) => db.getMessages(`group:${groupId}`),
|
|
24411
24752
|
putMessage: async (message) => {
|
|
24412
|
-
await db.
|
|
24753
|
+
const kept = await db.getMessage(message.linkId, message.id);
|
|
24754
|
+
await db.putMessage(kept ? {
|
|
24755
|
+
...message,
|
|
24756
|
+
timestamp: kept.timestamp,
|
|
24757
|
+
...kept.sentAt !== void 0 && { sentAt: kept.sentAt }
|
|
24758
|
+
} : this.placed(message));
|
|
24413
24759
|
await this.messagesChanged(message.linkId, [message.id]);
|
|
24414
24760
|
this.emitState();
|
|
24415
24761
|
}
|
|
@@ -24659,6 +25005,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
24659
25005
|
});
|
|
24660
25006
|
const history = /* @__PURE__ */ new Map();
|
|
24661
25007
|
for (const stored of await db.getLinks()) {
|
|
25008
|
+
if (!stored.group) await this.settleHistory(stored.id);
|
|
24662
25009
|
const messages = stored.group ? [] : await db.getMessages(stored.id);
|
|
24663
25010
|
const storedFiles = stored.group ? [] : await fileStore.listForLink(stored.id);
|
|
24664
25011
|
const waiting = new Set(messages.flatMap((m) => m.sender === "me" && m.delivery === "waiting" && m.file ? [m.file.id] : []));
|
|
@@ -24676,6 +25023,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
24676
25023
|
if (stored.profile && !stored.group) await this.outboxFor(stored.id).recover();
|
|
24677
25024
|
}
|
|
24678
25025
|
this.transfersRestored = true;
|
|
25026
|
+
for (const group of await db.getGroups()) await this.settleHistory(`group:${group.id}`);
|
|
24679
25027
|
await this.groups.load();
|
|
24680
25028
|
for (const group of this.groups.views()) {
|
|
24681
25029
|
const note = latestReaction(await db.getMessages(`group:${group.id}`));
|
|
@@ -24738,6 +25086,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
24738
25086
|
async shutdown() {
|
|
24739
25087
|
this.shuttingDown = true;
|
|
24740
25088
|
this.depart();
|
|
25089
|
+
this.directPath.close();
|
|
24741
25090
|
if (this.relayRetry) clearTimeout(this.relayRetry);
|
|
24742
25091
|
if (this.activeSlotRetry) clearTimeout(this.activeSlotRetry);
|
|
24743
25092
|
for (const timer of this.resumeTimers.values()) clearTimeout(timer);
|
|
@@ -24806,6 +25155,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
24806
25155
|
...this.transport.configure && { direct: true },
|
|
24807
25156
|
...this.transport.discovery && { discovery: this.transport.discovery() },
|
|
24808
25157
|
...typeof RTCPeerConnection === "undefined" && { webrtc: false },
|
|
25158
|
+
...this.directPath.blocked && { directBlocked: true },
|
|
24809
25159
|
...typeof RTCPeerConnection === "undefined" && !Object.keys(this.nativeFactories).length && { groupLinks: false }
|
|
24810
25160
|
},
|
|
24811
25161
|
links: [...this.links.values()].filter((live) => !live.stored.group).map((live) => this.viewOf(live)).sort((a, b) => b.createdAt - a.createdAt),
|
|
@@ -25255,6 +25605,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
25255
25605
|
groupHistoryGone(groupId) {
|
|
25256
25606
|
const linkId = `group:${groupId}`;
|
|
25257
25607
|
this.newestAt.delete(linkId);
|
|
25608
|
+
this.placedAt.delete(linkId);
|
|
25258
25609
|
this.reactionNotes.delete(linkId);
|
|
25259
25610
|
this.events.onMessages(linkId, []);
|
|
25260
25611
|
}
|
|
@@ -25474,7 +25825,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
25474
25825
|
* (`network`): relays left alone for failing on the old network are asked again at once.
|
|
25475
25826
|
*/
|
|
25476
25827
|
wake(params = {}) {
|
|
25477
|
-
if (params.network)
|
|
25828
|
+
if (params.network) {
|
|
25829
|
+
this.transport.networkChanged?.();
|
|
25830
|
+
this.directPath.reset();
|
|
25831
|
+
}
|
|
25478
25832
|
for (const live of this.links.values()) live.link?.wake();
|
|
25479
25833
|
this.hold.wake();
|
|
25480
25834
|
for (const network of WALLET_NETWORKS) {
|
|
@@ -25519,7 +25873,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
25519
25873
|
};
|
|
25520
25874
|
if (!live?.link) return { error: "You are offline" };
|
|
25521
25875
|
if (!trimmed) return { error: null };
|
|
25522
|
-
const timestamp = params.timestamp ?? Date.now();
|
|
25876
|
+
const timestamp = params.timestamp ?? arrivalKey(Date.now(), this.placedAt.get(linkId));
|
|
25523
25877
|
if (live.stored.profile) live.link.setTyping(false);
|
|
25524
25878
|
if (live.stored.profile) {
|
|
25525
25879
|
const found = params.replyTo === void 0 ? void 0 : await this.replyFor(linkId, params.replyTo);
|
|
@@ -25590,6 +25944,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
25590
25944
|
async sendChatText(live, trimmed, timestamp, preview, reply, forwarded, card) {
|
|
25591
25945
|
const { link } = live, linkId = live.stored.id;
|
|
25592
25946
|
if (!link) return { error: "You are offline" };
|
|
25947
|
+
this.chatInUse(live);
|
|
25593
25948
|
const bytes = new TextEncoder().encode(trimmed).length;
|
|
25594
25949
|
if (bytes > LIMITS.maxChatMessageBytes) return {
|
|
25595
25950
|
error: `Message exceeds ${LIMITS.maxChatMessageBytes} UTF-8 bytes.`,
|
|
@@ -25841,7 +26196,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
25841
26196
|
if (!takesPeerEdit(current, edit.m) || (current.edit?.seq ?? 0) >= edit.e) return null;
|
|
25842
26197
|
const next = withEdit(current, {
|
|
25843
26198
|
seq: edit.e,
|
|
25844
|
-
at:
|
|
26199
|
+
at: heardTime(edit.ts),
|
|
25845
26200
|
text: edit.m,
|
|
25846
26201
|
preview: edit.pv,
|
|
25847
26202
|
card: edit.sc
|
|
@@ -26833,7 +27188,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
26833
27188
|
}
|
|
26834
27189
|
async createGroup({ name, profile }) {
|
|
26835
27190
|
if (typeof name !== "string" || !name.trim()) throw new Error("Give the group a name");
|
|
26836
|
-
|
|
27191
|
+
const clean = groupName(name);
|
|
27192
|
+
if (!clean) throw new Error(`A group's name is 1 to 64 characters on one line`);
|
|
27193
|
+
return { groupId: await this.groups.create(clean, profile === "mesh" ? "mesh" : "community") };
|
|
26837
27194
|
}
|
|
26838
27195
|
inviteToGroup({ groupId, linkId }) {
|
|
26839
27196
|
if (!this.links.get(linkId)?.stored.profile || this.links.get(linkId)?.stored.group) throw new Error("Invite a paired contact");
|
|
@@ -27044,7 +27401,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
27044
27401
|
if (typeof linkId !== "string" || !linkId) return { error: "No chat to react in" };
|
|
27045
27402
|
if (linkId.startsWith("group:")) {
|
|
27046
27403
|
const groupId = linkId.slice(6);
|
|
27047
|
-
if (!this.membership(groupId)) return {
|
|
27404
|
+
if (!this.membership(groupId)) return {
|
|
27405
|
+
error: this.groups.views().find((g) => g.id === groupId)?.statusReason ?? "You are not in this group",
|
|
27406
|
+
refused: true
|
|
27407
|
+
};
|
|
27048
27408
|
const result = await this.reactions.mine(linkId, messageId, emoji);
|
|
27049
27409
|
return "error" in result ? result : this.sendGroupReaction(groupId, result.reaction);
|
|
27050
27410
|
}
|
|
@@ -28460,6 +28820,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
28460
28820
|
const live = this.links.get(linkId);
|
|
28461
28821
|
if (!live?.stored.group) return;
|
|
28462
28822
|
this.links.delete(linkId);
|
|
28823
|
+
this.edgeRtcOff.delete(linkId);
|
|
28463
28824
|
this.groupWakeReceived.delete(linkId);
|
|
28464
28825
|
this.groupNativeWaiting.delete(linkId);
|
|
28465
28826
|
this.groupNativeRetryAt = 0;
|
|
@@ -28601,7 +28962,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
28601
28962
|
usdtPaymentsSupport: !entry,
|
|
28602
28963
|
barkPaymentsSupport: !entry,
|
|
28603
28964
|
params: stored,
|
|
28604
|
-
rtcAvailable: typeof RTCPeerConnection !== "undefined",
|
|
28965
|
+
rtcAvailable: typeof RTCPeerConnection !== "undefined" && !this.edgeRtcOff.has(linkId),
|
|
28605
28966
|
resume: !entry && stored.edgeLive ? resumeOn : void 0,
|
|
28606
28967
|
native: {
|
|
28607
28968
|
peerDescriptors: stored.peerDescriptors,
|
|
@@ -28693,6 +29054,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
28693
29054
|
live.pairing = state;
|
|
28694
29055
|
this.emitState();
|
|
28695
29056
|
},
|
|
29057
|
+
onDirectEvidence: (evidence) => {
|
|
29058
|
+
this.directPath.note(peer, evidence);
|
|
29059
|
+
if (!entry && evidence !== "open" && evidence !== "closed") this.edgeWithoutRtc(linkId);
|
|
29060
|
+
},
|
|
28696
29061
|
onDataLinkState: (state) => {
|
|
28697
29062
|
traceJoin(group, `link.${state}`, { role });
|
|
28698
29063
|
if (state === "open" || live.dataLink === "open") live.lastSyncAt = Date.now();
|
|
@@ -28723,7 +29088,27 @@ var GhostlyNode = class GhostlyNode {
|
|
|
28723
29088
|
*/
|
|
28724
29089
|
keepsGroupNative(stored) {
|
|
28725
29090
|
if (this.options.webrtcGroupLinks) return false;
|
|
28726
|
-
return typeof RTCPeerConnection === "undefined" || !!stored.peerTransports && !stored.peerTransports.includes("webrtc/1");
|
|
29091
|
+
return typeof RTCPeerConnection === "undefined" || this.edgeRtcOff.has(stored.id) || !!stored.peerTransports && !stored.peerTransports.includes("webrtc/1");
|
|
29092
|
+
}
|
|
29093
|
+
/**
|
|
29094
|
+
* An edge's WebRTC attempt exchanged both descriptions and connected nothing, or found no candidate at all
|
|
29095
|
+
* (`DirectEvidence`): a network that lets no direct connection through (a VPN, a firewall, on either side). WebRTC is
|
|
29096
|
+
* all an edge between two apps that have it runs, and there is no TURN server unless someone set one, so the edge
|
|
29097
|
+
* never went live, and the member stayed unreachable for as long as that network lasted. The edge starts again as on an
|
|
29098
|
+
* app with no WebRTC (WISP 9xx § Transports): it runs its native endpoints and says so in its packet (`_tr`, with no
|
|
29099
|
+
* `webrtc/1`), the member's app starts its own for it as it does for a Linux Desktop, and the two meet over Iroh
|
|
29100
|
+
* through its relay. For this run of the app only.
|
|
29101
|
+
*/
|
|
29102
|
+
async edgeWithoutRtc(linkId) {
|
|
29103
|
+
const live = this.links.get(linkId), link = live?.link;
|
|
29104
|
+
if (!live || !link || !live.stored.group || live.stored.groupEntry || this.shuttingDown || this.options.webrtcGroupLinks) return;
|
|
29105
|
+
if (this.edgeRtcOff.has(linkId) || this.keepsGroupNative(live.stored) || !Object.keys(this.nativeFactories).length) return;
|
|
29106
|
+
this.edgeRtcOff.add(linkId);
|
|
29107
|
+
traceLink(live.myPubKeyZ32, "edge-rtc-off", {});
|
|
29108
|
+
live.link = null;
|
|
29109
|
+
await link.stop(false).catch(() => {});
|
|
29110
|
+
if (this.links.get(linkId) !== live || live.link || this.shuttingDown || this.settings.online === false) return;
|
|
29111
|
+
this.startEdge(linkId);
|
|
28727
29112
|
}
|
|
28728
29113
|
startLink(linkId, messages) {
|
|
28729
29114
|
const live = this.links.get(linkId);
|
|
@@ -28731,7 +29116,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
28731
29116
|
const { stored } = live;
|
|
28732
29117
|
if (stored.group) return this.startEdge(linkId);
|
|
28733
29118
|
if (stored.profile) live.pairing = { status: "connecting" };
|
|
28734
|
-
const lastSeenTimestamp = messages.reduce((max, m) => m.sender === "peer" && m.via === "pkarr" ? Math.max(max, m.timestamp) : max, 0);
|
|
29119
|
+
const lastSeenTimestamp = messages.reduce((max, m) => m.sender === "peer" && m.via === "pkarr" ? Math.max(max, m.sentAt ?? m.timestamp) : max, 0);
|
|
28735
29120
|
const credentials = stored.profile && stored.participationSeed ? {
|
|
28736
29121
|
seedB64: stored.participationSeed,
|
|
28737
29122
|
peerKey: stored.pairedPeerKey,
|
|
@@ -28964,6 +29349,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
28964
29349
|
live.peerAck = ack;
|
|
28965
29350
|
this.emitState();
|
|
28966
29351
|
},
|
|
29352
|
+
onDirectEvidence: (evidence) => this.directPath.note(stored.peerPubKeyZ32, evidence),
|
|
28967
29353
|
onDataLinkState: (state) => {
|
|
28968
29354
|
const was = live.dataLink;
|
|
28969
29355
|
live.dataLink = state;
|
|
@@ -29023,6 +29409,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
29023
29409
|
};
|
|
29024
29410
|
db.patchLink(linkId, { inviteCode: void 0 });
|
|
29025
29411
|
}
|
|
29412
|
+
if (message.via !== "datalink") this.chatInUse(live);
|
|
29026
29413
|
return this.storeMessage({
|
|
29027
29414
|
linkId,
|
|
29028
29415
|
id: `peer_${message.id ?? message.timestamp}`,
|
|
@@ -29191,7 +29578,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
29191
29578
|
get nativeFactories() {
|
|
29192
29579
|
const relay = this.relaysHyperdht ? this.hyperdhtRelay : "";
|
|
29193
29580
|
return {
|
|
29194
|
-
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-
|
|
29581
|
+
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-cMlRWrmD.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
|
|
29195
29582
|
...this.options.nativeTransports,
|
|
29196
29583
|
...this.options.irohWeb ? { "iroh/1": (seedB64) => createIrohWebEndpoint(seedB64, { relays: this.irohRelays }) } : {}
|
|
29197
29584
|
};
|
|
@@ -29254,70 +29641,110 @@ var GhostlyNode = class GhostlyNode {
|
|
|
29254
29641
|
}
|
|
29255
29642
|
this.emitState();
|
|
29256
29643
|
}
|
|
29257
|
-
|
|
29644
|
+
/**
|
|
29645
|
+
* A text going or coming in a 1:1 chat with no live session: the chat is in use, and takes the native listeners it
|
|
29646
|
+
* lacks as the chat on screen would (a bot answering its ninth contact has no chat on screen).
|
|
29647
|
+
*/
|
|
29648
|
+
chatInUse(live) {
|
|
29649
|
+
const link = live.link;
|
|
29650
|
+
if (live.stored.group || !link || link.isDataLinkOpen || !this.keepsNativeEndpoints(live.stored.id, live.stored)) return;
|
|
29651
|
+
if (Object.keys(this.nativeFactories).every((t) => link.availableTransports.includes(t))) return;
|
|
29652
|
+
this.ensureNativeEndpoints(live.stored.id, true);
|
|
29653
|
+
}
|
|
29654
|
+
/**
|
|
29655
|
+
* Whether the chat goes live only over a native transport: this app has no WebRTC, or its contact has none (its record
|
|
29656
|
+
* lists no `webrtc/1`). Not known yet (no record read) counts as only natively, as a chat before its first record must.
|
|
29657
|
+
*/
|
|
29658
|
+
reachedOnlyNatively(live) {
|
|
29659
|
+
const theirs = live.stored.capsState?.peer?.transports ?? live.stored.peerTransports;
|
|
29660
|
+
return typeof RTCPeerConnection === "undefined" || !theirs || !theirs.includes("webrtc/1");
|
|
29661
|
+
}
|
|
29662
|
+
/** `inUse`: a text is going or coming in the chat now, which counts as the chat on screen does for taking a listener. */
|
|
29663
|
+
ensureNativeEndpoints(linkId, inUse = false) {
|
|
29258
29664
|
const expected = this.links.get(linkId)?.link;
|
|
29259
|
-
const
|
|
29665
|
+
const starts = Object.entries(this.nativeFactories).map(([transport, factory]) => {
|
|
29666
|
+
const key = transport;
|
|
29667
|
+
const operation = (this.nativeQueues.get(key) ?? Promise.resolve()).then(() => factory ? this.startNativeEndpoint(linkId, expected, key, factory, inUse) : false);
|
|
29668
|
+
this.nativeQueues.set(key, operation.then(() => {}, () => {}));
|
|
29669
|
+
return operation;
|
|
29670
|
+
});
|
|
29671
|
+
return Promise.all(starts).then((full) => {
|
|
29260
29672
|
const live = this.links.get(linkId), link = live?.link;
|
|
29261
|
-
if (this.shuttingDown || !live
|
|
29262
|
-
|
|
29263
|
-
|
|
29264
|
-
|
|
29265
|
-
|
|
29266
|
-
|
|
29267
|
-
|
|
29268
|
-
|
|
29269
|
-
|
|
29270
|
-
|
|
29271
|
-
|
|
29272
|
-
|
|
29273
|
-
|
|
29274
|
-
|
|
29275
|
-
|
|
29276
|
-
|
|
29277
|
-
|
|
29278
|
-
|
|
29279
|
-
|
|
29280
|
-
|
|
29281
|
-
|
|
29282
|
-
|
|
29283
|
-
|
|
29284
|
-
|
|
29285
|
-
|
|
29286
|
-
|
|
29287
|
-
|
|
29288
|
-
|
|
29289
|
-
|
|
29290
|
-
|
|
29291
|
-
|
|
29292
|
-
|
|
29293
|
-
...live.
|
|
29294
|
-
[key]:
|
|
29295
|
-
};
|
|
29296
|
-
await db.patchLink(linkId, { transportSeeds });
|
|
29297
|
-
live.stored = {
|
|
29298
|
-
...live.stored,
|
|
29299
|
-
transportSeeds
|
|
29673
|
+
if (this.shuttingDown || !live || !link || link !== expected) return;
|
|
29674
|
+
if (live.stored.group && this.keepsGroupNative(live.stored)) {
|
|
29675
|
+
if (full.some(Boolean) && !link.availableTransports.some((t) => t !== "webrtc/1")) this.groupNativeWaiting.add(linkId);
|
|
29676
|
+
else this.groupNativeWaiting.delete(linkId);
|
|
29677
|
+
}
|
|
29678
|
+
this.emitState();
|
|
29679
|
+
});
|
|
29680
|
+
}
|
|
29681
|
+
/** One transport's listener for one chat, in that transport's queue. True when a group's link found no slot free. */
|
|
29682
|
+
async startNativeEndpoint(linkId, expected, key, factory, inUse) {
|
|
29683
|
+
const live = this.links.get(linkId), link = live?.link;
|
|
29684
|
+
if (this.shuttingDown || !live?.stored.profile || live.stored.deliveryMode === "dht" || !link || link !== expected) return false;
|
|
29685
|
+
const group = !!live.stored.group;
|
|
29686
|
+
if (group && !this.keepsGroupNative(live.stored)) return false;
|
|
29687
|
+
if (link.availableTransports.includes(key)) return false;
|
|
29688
|
+
live.transportErrors ??= {};
|
|
29689
|
+
try {
|
|
29690
|
+
const owners = [...this.links.values()].filter((other) => other.link?.availableTransports.includes(key));
|
|
29691
|
+
if (group && (owners.length >= NATIVE_SLOTS || owners.filter((other) => other.stored.group).length >= 4)) return true;
|
|
29692
|
+
if (owners.length >= NATIVE_SLOTS) {
|
|
29693
|
+
const now = Date.now();
|
|
29694
|
+
const taking = !group && (this.activeLinkId === linkId || inUse);
|
|
29695
|
+
const victim = owners.filter((other) => other.stored.group && other.link?.canReleaseEndpoint(key)).sort((a, b) => (a.link?.isDataLinkOpen ? 1 : 0) - (b.link?.isDataLinkOpen ? 1 : 0))[0] ?? (taking ? owners.filter((other) => other !== live && other.stored.id !== this.activeLinkId && other.link?.canReleaseEndpoint(key) && now - (other.nativeTakenAt?.[key] ?? 0) >= 12e4).sort((a, b) => a.lastMessageAt - b.lastMessageAt)[0] : void 0);
|
|
29696
|
+
const idle = victim || !taking || !this.reachedOnlyNatively(live) ? void 0 : owners.filter((other) => other !== live && !other.stored.group && other.stored.id !== this.activeLinkId && !this.fileDesk.moving(other.stored.id) && other.link?.canYieldEndpoint(key, 12e4)).sort((a, b) => a.link.lastActivityAt - b.link.lastActivityAt)[0];
|
|
29697
|
+
const idleFor = idle ? now - idle.link.lastActivityAt : 0;
|
|
29698
|
+
if (idle && await idle.link.yieldEndpoint(key, 12e4)) {
|
|
29699
|
+
traceLink(live.myPubKeyZ32, "native-take", {
|
|
29700
|
+
transport: key,
|
|
29701
|
+
idle: idleFor,
|
|
29702
|
+
active: this.activeLinkId === linkId
|
|
29703
|
+
});
|
|
29704
|
+
live.nativeTakenAt = {
|
|
29705
|
+
...live.nativeTakenAt,
|
|
29706
|
+
[key]: Date.now()
|
|
29300
29707
|
};
|
|
29301
|
-
|
|
29302
|
-
|
|
29303
|
-
|
|
29304
|
-
|
|
29305
|
-
|
|
29306
|
-
|
|
29307
|
-
|
|
29308
|
-
this.
|
|
29309
|
-
|
|
29310
|
-
|
|
29708
|
+
idle.transportErrors ??= {};
|
|
29709
|
+
idle.transportErrors[key] = "Listener given to a chat in use: this one was quiet. Open this chat to take one back; your messages and transport identity are saved.";
|
|
29710
|
+
} else if (!victim) {
|
|
29711
|
+
traceLink(live.myPubKeyZ32, "native-no-slot", {
|
|
29712
|
+
transport: key,
|
|
29713
|
+
active: this.activeLinkId === linkId
|
|
29714
|
+
});
|
|
29715
|
+
if (this.activeLinkId === linkId && !group) this.retryActiveSlot(linkId);
|
|
29716
|
+
throw new Error("All eight native connection slots are in use. This chat takes one once a chat live over one has been quiet for 2 minutes. Disconnect a native connection in another chat to free one now.");
|
|
29717
|
+
} else {
|
|
29718
|
+
await victim.link.releaseEndpoint(key);
|
|
29719
|
+
victim.transportErrors ??= {};
|
|
29720
|
+
victim.transportErrors[key] = "Listener released for another chat. Open this chat to restore it; your messages and transport identity are saved.";
|
|
29721
|
+
if (victim.stored.group) this.groupNativeWaiting.add(victim.stored.id);
|
|
29311
29722
|
}
|
|
29312
29723
|
}
|
|
29313
|
-
if (
|
|
29314
|
-
|
|
29315
|
-
|
|
29724
|
+
if (this.shuttingDown || live.link !== link) return false;
|
|
29725
|
+
const seed = live.stored.transportSeeds?.[key] ?? createIdentity().seedB64;
|
|
29726
|
+
const transportSeeds = {
|
|
29727
|
+
...live.stored.transportSeeds,
|
|
29728
|
+
[key]: seed
|
|
29729
|
+
};
|
|
29730
|
+
live.stored = {
|
|
29731
|
+
...live.stored,
|
|
29732
|
+
transportSeeds
|
|
29733
|
+
};
|
|
29734
|
+
await db.patchLink(linkId, { transportSeeds });
|
|
29735
|
+
const endpoint = await factory(seed);
|
|
29736
|
+
if (this.shuttingDown || live.stored.deliveryMode === "dht" || live.link !== link || !this.links.has(linkId)) {
|
|
29737
|
+
await endpoint.close();
|
|
29738
|
+
return false;
|
|
29316
29739
|
}
|
|
29740
|
+
link.registerEndpoint(endpoint);
|
|
29741
|
+
delete live.transportErrors[key];
|
|
29742
|
+
this.capsChanged(linkId);
|
|
29317
29743
|
this.emitState();
|
|
29318
|
-
})
|
|
29319
|
-
|
|
29320
|
-
|
|
29744
|
+
} catch (error) {
|
|
29745
|
+
live.transportErrors[key] = error instanceof Error ? error.message : "Native adapter could not start. Reopen this chat to retry.";
|
|
29746
|
+
}
|
|
29747
|
+
return false;
|
|
29321
29748
|
}
|
|
29322
29749
|
/**
|
|
29323
29750
|
* The order saved chats start in, which is the order they take native listeners in (`NATIVE_SLOTS` per transport):
|
|
@@ -29446,15 +29873,46 @@ var GhostlyNode = class GhostlyNode {
|
|
|
29446
29873
|
deleted: unique.filter((_, i) => !rows[i])
|
|
29447
29874
|
});
|
|
29448
29875
|
}
|
|
29876
|
+
/**
|
|
29877
|
+
* At the start, before anything reads a history: received rows an earlier version stored under a time that has not
|
|
29878
|
+
* come yet take a place before now (`settleAhead`). One row is read when nothing is ahead, which is every start but
|
|
29879
|
+
* the first after the update.
|
|
29880
|
+
*/
|
|
29881
|
+
async settleHistory(linkId) {
|
|
29882
|
+
const now = Date.now();
|
|
29883
|
+
const newest = (await db.getMessagePage(linkId, { limit: 1 })).messages[0];
|
|
29884
|
+
if (!newest || newest.timestamp <= now) return;
|
|
29885
|
+
for (const row of settleAhead(await db.getMessages(linkId), now)) await db.putMessage(row);
|
|
29886
|
+
}
|
|
29887
|
+
/**
|
|
29888
|
+
* A new row with its place in its history. A received row takes it here and now, past the last place given in its
|
|
29889
|
+
* chat: the time its sender says is kept beside it (`sentAt`) and shown, and orders nothing (WISP 400, requirement
|
|
29890
|
+
* 10). A sender's clock ahead or behind then moves no message past another, and what I send next goes below what I
|
|
29891
|
+
* just received. A row of mine keeps its own time; one placed already (it has `sentAt`) is left as it is. A line
|
|
29892
|
+
* of a group's history (a membership change, a rename) is not a message: it stays at its commit's time, as before.
|
|
29893
|
+
*/
|
|
29894
|
+
placed(message) {
|
|
29895
|
+
const now = Date.now(), last = this.placedAt.get(message.linkId) ?? 0;
|
|
29896
|
+
if (message.event) return message.sender === "peer" ? {
|
|
29897
|
+
...message,
|
|
29898
|
+
timestamp: receivedTimestamp(message.timestamp)
|
|
29899
|
+
} : message;
|
|
29900
|
+
if (message.sender === "peer" && message.sentAt === void 0) {
|
|
29901
|
+
const sentAt = claimedTime(message.timestamp);
|
|
29902
|
+
message = {
|
|
29903
|
+
...message,
|
|
29904
|
+
timestamp: arrivalKey(now, last),
|
|
29905
|
+
...sentAt !== void 0 && { sentAt }
|
|
29906
|
+
};
|
|
29907
|
+
this.placedAt.set(message.linkId, message.timestamp);
|
|
29908
|
+
} else this.placedAt.set(message.linkId, Math.max(last, Math.min(message.timestamp, now)));
|
|
29909
|
+
return message;
|
|
29910
|
+
}
|
|
29449
29911
|
async storeMessage(message) {
|
|
29450
29912
|
await this.storeNewMessage(message);
|
|
29451
29913
|
}
|
|
29452
29914
|
/** Stores a message; false when it was there already (or deleted here): nothing new came. */
|
|
29453
29915
|
async storeNewMessage(message) {
|
|
29454
|
-
if (message.sender === "peer") message = {
|
|
29455
|
-
...message,
|
|
29456
|
-
timestamp: receivedTimestamp(message.timestamp)
|
|
29457
|
-
};
|
|
29458
29916
|
const edge = this.links.get(message.linkId)?.stored;
|
|
29459
29917
|
if (edge?.group && edge.groupPeer && !edge.groupEntry) message = {
|
|
29460
29918
|
...message,
|
|
@@ -29469,6 +29927,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
29469
29927
|
id: `${message.linkId}:${message.id}`,
|
|
29470
29928
|
...message.sender === "peer" ? { member: pay.member } : {}
|
|
29471
29929
|
};
|
|
29930
|
+
message = this.placed(message);
|
|
29472
29931
|
const live = this.links.get(message.linkId);
|
|
29473
29932
|
if (live?.stored.deletedIds?.includes(message.id)) return false;
|
|
29474
29933
|
message = await this.resolveReply(message);
|
|
@@ -29498,7 +29957,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
29498
29957
|
}
|
|
29499
29958
|
if (!await db.addMessage(message)) return false;
|
|
29500
29959
|
const newest = this.newestAt.get(message.linkId);
|
|
29501
|
-
if (!message.event) this.newestAt.set(message.linkId, Math.max(newest ?? 0, message
|
|
29960
|
+
if (!message.event) this.newestAt.set(message.linkId, Math.max(newest ?? 0, shownTime(message)));
|
|
29502
29961
|
if (message.sender === "peer") this.messageFeedback("message", message, newest);
|
|
29503
29962
|
if (live) live.lastMessageAt = Math.max(live.lastMessageAt, message.timestamp);
|
|
29504
29963
|
const early = message.sender === "peer" && message.id.startsWith("peer_") ? this.editBuffer.take(message.linkId, message.id.slice(5)) : void 0;
|
|
@@ -29804,4 +30263,4 @@ function signalKind(signal) {
|
|
|
29804
30263
|
//#endregion
|
|
29805
30264
|
export { EngineServer, hyperdhtRelayProblem as n, server_exports as t };
|
|
29806
30265
|
|
|
29807
|
-
//# sourceMappingURL=server-
|
|
30266
|
+
//# sourceMappingURL=server-Cajp8VLY.js.map
|