@ghostlytools/cli 1.2.3 → 1.3.0
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 +18 -7
- package/dist/assets/api-BiaX6l6f.js +3 -0
- package/dist/assets/api-CrxVxtY1.js +4071 -0
- package/dist/assets/api-CrxVxtY1.js.map +1 -0
- package/dist/assets/apiKit-CajiDxdK.js +196 -0
- package/dist/assets/apiKit-CajiDxdK.js.map +1 -0
- package/dist/assets/apps-CbfiCSIw.js +787 -0
- package/dist/assets/apps-CbfiCSIw.js.map +1 -0
- package/dist/assets/apps-t23gkc28.js +2 -0
- package/dist/assets/{arkade-B-ALutmU.js → arkade-D4BVJFEQ.js} +2 -2
- package/dist/assets/{arkade-B-ALutmU.js.map → arkade-D4BVJFEQ.js.map} +1 -1
- package/dist/assets/backup-BAMi2lAq.js +731 -0
- package/dist/assets/backup-BAMi2lAq.js.map +1 -0
- package/dist/assets/backup-Cg52WeQi.js +2 -0
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/buttons-CWl2yNJu.js +2 -0
- package/dist/assets/buttons-CX1pKB0j.js +497 -0
- package/dist/assets/buttons-CX1pKB0j.js.map +1 -0
- package/dist/assets/{endpoint-pgFkR2AS.js → endpoint-Dn7H8qE1.js} +14 -8
- package/dist/assets/endpoint-Dn7H8qE1.js.map +1 -0
- package/dist/assets/envelope-BvU0fLEt.js +90 -0
- package/dist/assets/envelope-BvU0fLEt.js.map +1 -0
- package/dist/assets/errors-my6VJR_0.js +77 -0
- package/dist/assets/errors-my6VJR_0.js.map +1 -0
- package/dist/assets/{fileBytes-Dbifu5I5.js → fileBytes-CfSXjTFa.js} +2 -2
- package/dist/assets/{fileBytes-Dbifu5I5.js.map → fileBytes-CfSXjTFa.js.map} +1 -1
- package/dist/assets/fileBytes-CgbibpbV.js +2 -0
- package/dist/assets/{fileBytes-BcYZhNgF.js → fileBytes-Di7VZtmU.js} +6 -19
- package/dist/assets/fileBytes-Di7VZtmU.js.map +1 -0
- package/dist/assets/fileBytesIdb-CFP-DSkZ.js +2 -0
- package/dist/assets/{fileBytesIdb-DzSaqI0A.js → fileBytesIdb-D5u6H8X_.js} +4 -4
- package/dist/assets/{fileBytesIdb-DzSaqI0A.js.map → fileBytesIdb-D5u6H8X_.js.map} +1 -1
- package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js → fileBytesOpfs-Dd1N4ERH.js} +3 -3
- package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js.map → fileBytesOpfs-Dd1N4ERH.js.map} +1 -1
- package/dist/assets/groupFiles-BNTSDaoq.js +7685 -0
- package/dist/assets/groupFiles-BNTSDaoq.js.map +1 -0
- package/dist/assets/groupFiles-DBbjOLuD.js +2088 -0
- package/dist/assets/groupFiles-DBbjOLuD.js.map +1 -0
- package/dist/assets/host-Cmd205Dc.js +4648 -0
- package/dist/assets/host-Cmd205Dc.js.map +1 -0
- package/dist/assets/hyperdhtRelay-BT_wRxab.js +21 -0
- package/dist/assets/hyperdhtRelay-BT_wRxab.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-hKC9DpIH.js → hyperdhtRelay-CEmBpT0Q.js} +26 -10
- package/dist/assets/hyperdhtRelay-CEmBpT0Q.js.map +1 -0
- package/dist/assets/{idb-CTF6LF5B.js → idb-B5L8wuOm.js} +7 -5
- package/dist/assets/idb-B5L8wuOm.js.map +1 -0
- package/dist/assets/irohWeb--YyEA-hX.js +545 -0
- package/dist/assets/irohWeb--YyEA-hX.js.map +1 -0
- package/dist/assets/{joinTrace-CO8mlhNO.js → joinTrace-CjRDFy85.js} +2 -7
- package/dist/assets/{joinTrace-CO8mlhNO.js.map → joinTrace-CjRDFy85.js.map} +1 -1
- package/dist/assets/joinTrace-D-fnt2p3.js +2 -0
- package/dist/assets/main-BzNN6bkx.js +4302 -0
- package/dist/assets/main-BzNN6bkx.js.map +1 -0
- package/dist/assets/{mainline-DriRu_hb.js → mainline-D5D5C-jT.js} +14 -6
- package/dist/assets/mainline-D5D5C-jT.js.map +1 -0
- package/dist/assets/mints-BrlY773j.js +74 -0
- package/dist/assets/mints-BrlY773j.js.map +1 -0
- package/dist/assets/mints-CqTdA4Gk.js +2 -0
- package/dist/assets/networkReason-DxZWHw6v.js +154 -0
- package/dist/assets/networkReason-DxZWHw6v.js.map +1 -0
- package/dist/assets/privateFolder-Bk9C9oFv.js +38 -0
- package/dist/assets/privateFolder-Bk9C9oFv.js.map +1 -0
- package/dist/assets/profiles-B6F4oa9K.js +139 -0
- package/dist/assets/profiles-B6F4oa9K.js.map +1 -0
- package/dist/assets/{restoredRows-C_84q0Pd.js → restoredRows-Db3oUTko.js} +2 -2
- package/dist/assets/{restoredRows-C_84q0Pd.js.map → restoredRows-Db3oUTko.js.map} +1 -1
- package/dist/assets/{server-DHMXMwNn.js → server-CBCEYKMN.js} +1290 -277
- package/dist/assets/server-CBCEYKMN.js.map +1 -0
- package/dist/assets/{src-_FzJkeAJ.js → src-DhfGgAUu.js} +1068 -2266
- package/dist/assets/src-DhfGgAUu.js.map +1 -0
- package/dist/assets/turnRefs-ChEfgbSX.js +323 -0
- package/dist/assets/turnRefs-ChEfgbSX.js.map +1 -0
- package/dist/assets/{usdt-D0lO2pRR.js → usdt-C1Xao2xH.js} +3 -3
- package/dist/assets/{usdt-D0lO2pRR.js.map → usdt-C1Xao2xH.js.map} +1 -1
- package/dist/ghostly.mjs +4 -21992
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/endpoint-pgFkR2AS.js.map +0 -1
- package/dist/assets/fileBytes-BcYZhNgF.js.map +0 -1
- package/dist/assets/hyperdhtRelay-hKC9DpIH.js.map +0 -1
- package/dist/assets/idb-CTF6LF5B.js.map +0 -1
- package/dist/assets/mainline-DriRu_hb.js.map +0 -1
- package/dist/assets/rolldown-runtime-8H4AJuhK.js +0 -14
- package/dist/assets/server-DHMXMwNn.js.map +0 -1
- package/dist/assets/src-_FzJkeAJ.js.map +0 -1
|
@@ -1,23 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { $ as
|
|
3
|
-
import { a as
|
|
4
|
-
import { a as
|
|
5
|
-
import {
|
|
6
|
-
import { n as
|
|
1
|
+
import { n as traceJoin } from "./joinTrace-CjRDFy85.js";
|
|
2
|
+
import { $ as appRef, D as parseVideoMeta, Ft as toBase64Url, G as claimedTime, It as toZ32, J as shownTime, K as heardTime, Lt as utf8Decode, N as STATUS_CARD_LIMITS, Nt as randomBytes, O as BUTTON_ID, Ot as bytesEqual, Q as appFingerprint, Rt as utf8Encode, T as parseVoiceMeta, U as statusCardText, V as sameButtonText, W as arrivalKey, Z as APP_STATEMENT_LIMITS, _ as safeBlobType, _t as LIMITS, at as readAppRevocation, bt as encodeControl, c as readGroupFileMeta, g as formatFileSize, ht as readCanonicalJson, it as isAppRef, jt as fromBase64Url, nt as isAppHash, o as groupFileFallback, p as parseImageMeta, pt as canonicalJson, q as receivedTimestamp, rt as isAppKey, t as GROUP_FILE_LIMITS, v as sanitizeFileName, w as fileMessageText, y as sanitizeMime, yt as decodeControl, z as checkStatusCard } from "./groupFiles-DBbjOLuD.js";
|
|
3
|
+
import { $a as HANDOFF_HELLO, $i as EDIT_RATE_WINDOW_MS, $o as readEnrollGrant, $r as STATUS_CARD_CAPABILITY, $t as didDhtFromPublicKey, A as CommunitySession, Aa as RELAY_POLL_INTERVALS, Ao as readHandoffHello, Ar as typingActivity, As as nextTurnPosition, B as knockRecords, Bi as PeerProofs, Bn as isFederationId, Bo as signTombstone, Br as deviceWakeRequest, Bs as RelayTransport, C as sha256Paced, Ca as rosterHas, Ci as CapsExchange, Co as handoffStreamKey, Cr as FILE_LIMITS, Cs as TURN_RAISE_READ_TIMEOUT_MS, D as openGroupTyping, Do as readHandoffCancel, Ds as TURN_LAST_SEQUENCE, E as groupTypingEpoch, Ea as DirectPathWatch, Ei as dialDescriptors, Eo as readHandoffAck, Es as turnPutSummary, Et as normalizeAtprotoHandle, Fa as webLocalFetch, Fi as identityStatement, Fn as decodeBolt11, Fr as pinIsNewer, Fs as turnKeys, G as groupWakeFrame, Ga as emptyLinkRecords, Gn as LIVENESS_PING_MS, Gr as checkPushEndpoint, Gs as budgetRetryMs, H as readKnocks, Hi as validExternalKey, Hn as parseFedimintRequestPayload, Hr as parseWakeFrame, Hs as normalizeRelayUrl, I as decodeGroupEntryLink, Ii as newIdentityBinding, In as ENDPOINT, Ir as pinNumberHolds, Is as turnName, J as GROUP_LIMITS, Ja as serviceIdFromName, Ji as DHT_MESSAGE_TTL, Jn as engineText, Jo as enrollDigits, Jr as chatAppId, Js as PacketTooLargeError, K as groupWakes, Ka as HTTP_SERVICE_PROTO, Kn as pairedMessageFrame, Ko as enrollCancelFrame, Kr as vapidKeysMatch, Ks as isDiscoveryBudgetError, L as encodeGroupEntryLink, La as RTC_CONFIG, Li as receivedIdentityStatus, Ln as cashuRequestPayload, Lo as readSetUpdate, Lr as readPin, Ls as turnStatedVerifier, M as communityHasFrame, Ma as GhostlyHttpError, Mo as readHandoffTurnFrame, Mr as PIN_LIMITS, Ms as signTurnPacket, N as decodeCommunityLink, Na as formatLocalTarget, Ni as emptyIdentityLedger, No as readHandoffVerified, Nr as mayPin, Ns as signTurnRelease, O as sealGroupTyping, Oa as DHT_POLL_INTERVALS, Oo as readHandoffDone, Or as TypingSender, Os as TURN_MAX, P as encodeCommunityLink, Pa as parseLocalTarget, Pi as identityRevocationValue, Po as sealHandoffPiece, Pr as nextPinNumber, Ps as turnInstance, Q as frameBytes, Qa as HANDOFF_HAVE, Qi as EDIT_BUFFER_MS, Qo as enrollProofFrame, Qr as EDIT_CAPABILITY, Qt as didDhtDocument, R as entryParams, Ri as revokesIdentity, Rn as fedimintRequestPayload, Ro as setAckFrame, Rr as WAKE_CALL_INTERVAL_MS, Rs as turnVerifierMark, S as readAppBundleAsync, Sa as commitHash, Sn as isBitcoinAddress, So as handoffRetryAfter, Sr as ChatFiles, T as GroupTypingBoard, Ta as edgeParams, Ti as capsDescriptors, To as newHandoffId, Tr as transferStalled, Ts as classifyTurnRead, Tt as isAtprotoDid, Ua as parseCallSignal, Ui as TRANSPORTS, Un as GhostLink, Ur as relayRequest, V as mergeKnocks, Vi as emptyProofLedger, Vn as parseCashuRequestPayload, Vo as verifySetUpdate, Vr as newWakeToken, Vs as currentRelays, W as GroupWakeLimiter, Wi as automaticTransport, Wn as INVITE_TAKEN, Wr as wakeRequest, Ws as traceLink, X as GROUP_READ_NOTE, Xa as HANDOFF_CANCEL, Xi as DHT_FILE_CAPABILITY, Xo as enrollGrantFrame, Xr as APPS_CAPABILITY, Xs as createRelayPayload, Ya as HANDOFF_ACK, Yi as peekDhtMailbox, Yn as CarriedTransport, Yo as enrollDoneFrame, Yr as isAppVersion, Z as GroupSession, Za as HANDOFF_DONE, Zi as dhtFileId, Zo as enrollHelloFrame, Zr as BUTTONS_CAPABILITY, _a as readReply, _n as walletNetworkOf, _o as handoffHelloFrame, _r as rankHubs, _s as deviceLinkPairing, a as planAppUpdate, aa as COMMUNITY_REACTION_FRAME, ai as HoldRefusedError, an as EVM_TEST_CHAINS, ao as HANDOFF_VERIFIED, ar as pickMeshHubs, as as OwnInviteError, at as groupName, b as pacer, bn as sparkAddressKind, bo as handoffPakeFrame, br as readLobby, bs as seedSigner, ca as ReactionWindow, ci as readManifest, cn as SEPOLIA_TEST_USDT, co as handoffAttemptFailed, cr as beaconRecords, cs as encodeInviteCode, d as APP_BUNDLE_LIMITS, da as reactionEmoji, dn as USDT_PUBLIC_RPC, do as handoffCancelFrame, dr as lobbyKeys, ds as DEVICES_CAPABILITY, ea as RateWindow, ec as createIdentity, ei as UPGRADE_CAPABILITY, en as encodeDidDhtDeactivated, eo as HANDOFF_OFFER, er as groupSignalFrame, es as readEnrollHello, f as APP_BUNDLE_MAGIC, fa as reactionIsNewer, fn as WALLET_NETWORKS, fo as handoffContext, fr as lobbyRecords, fs as ENROLL_CAPABILITY, g as appViewOf, ga as pairedReplyAuthor, gn as validatePaymentTarget, go as handoffEphemeral, gr as pickHubs, gs as deviceKeyZ32, h as appDigest, ha as wireReaction, hn as formatPaymentAmount, ho as handoffDigest, hr as newerHead, hs as deviceEchoNonce, i as checkAppBeforeRun, ia as readForwarded, ic as verify, ii as HoldKeys, in as ETHEREUM_USDT, io as HANDOFF_TIMINGS, ir as meshHubs, is as readDeviceInvite, it as mentionsMember, j as GROUP_READ_NOTE_COMMUNITY, ja as presenceSeenAt, ji as IdentityExchange, jo as readHandoffPake, js as readTurnPacket, k as COMMUNITY_LIMITS, ki as IDENTITY_REVOCATION_LABEL, ko as readHandoffHave, ks as firstDeviceSetSecret, l as readAppStoreSteps, la as nextReactionNumber, lo as handoffAttemptSucceeded, lr as doorHubs, ma as validReactionNumber, mn as assertWholeSats, mo as handoffDataFrame, mr as mergeLobby, ms as deviceEchoFrame, n as appStoreDecision, na as parseLinkPreview, nc as identityFromSeedB64, ni as HOLD_DATED_BACK_MS, no as HANDOFF_PIECE_BYTES, nr as MESH_HUBS, ns as createDeviceInvite, nt as carryMentions, o as APP_STORE_LIMITS, oa as GROUP_REACTION_FRAME, oc as sanitizeAvatar, oi as isHoldUrl, on as ONCHAIN_PROVIDER, oo as filePartName, or as COMMUNITY_TOPOLOGY, os as createChatInvite, p as APP_ICON_PATH, pa as readReaction, pn as assertTokenUnits, po as handoffContextProof, pr as mergeBeacon, ps as HANDOFF_CAPABILITY, q as parseGroupWakeFrame, qa as LEGACY_SERVICES, qn as engineError, qo as enrollCancelReason, qs as withRequestOptions, r as appUpdateDecision, ra as forwardedAgain, rc as publicKeyFromZ32, ri as HOLD_LIMITS, rn as signDidDhtPacket, ro as HANDOFF_REQUEST, rr as mayBeHub, rs as deviceInviteJoinerParams, rt as parseCommunityEdit, sa as REACTION_LIMITS, si as newMailbox, sn as PaymentPreflightError, so as handoffAttemptAllowed, sr as beaconKeys, ss as decodeInviteCode, t as appRevocationFor, ta as dhtEditId, tc as identityFromSeed, ti as HOLD_CAPABILITY, tn as encodeDidDhtPacket, to as HANDOFF_PAKE, tr as readGroupSignal, ts as verifyEnrollProof, tt as GROUP_EDIT_TEXT_BYTES, ua as queueReaction, un as TEST_USDT_FAUCET_AMOUNT, uo as handoffBusyFrame, ur as freshHubs, us as inviteOwnership, va as replySnippet, vn as SPARK_NETWORKS, vo as handoffManifestFrames, vr as readBeacon, vs as devicePingFrame, wa as verifyCommitSignature, wi as DHT_TEXT_CAPABILITY, wn as isPubkyKey, wo as handoffVersions, wr as transferEnded, ws as TURN_SETTLE_MS, xa as MEMBER_KEY, xn as sparkInvoiceDetails, xo as handoffReleaseFrame, xr as shouldBeHub, xs as webCryptoSigner, y as isAppUrl, ya as GROUP_MEMBER_CAP, yn as SPARK_PROVIDER, yo as handoffOfferFrame, yr as readBeaconHead, ys as newDeviceSetSecret, z as knockIdentity, zi as lastSharedWithMe, zn as findEndpoint, zo as setUpdateFrame, zr as WakeLimiter, zs as DEFAULT_RELAYS } from "./src-DhfGgAUu.js";
|
|
4
|
+
import { a as clearProfileStores, c as fileStore, d as profileOpenFailure, f as store, i as STORES$1, l as newestWalletTx, m as wrap, n as APP_STORAGE_SCOPE_INDEX, o as databaseName, p as transact, r as CARD_INDEX, s as deleteDatabase, t as APP_STORAGE_CHAT_INDEX, u as openDb } from "./idb-B5L8wuOm.js";
|
|
5
|
+
import { a as sealSeed, i as newDeviceKey, o as unsealSeed, r as intentRepository } from "./networkReason-DxZWHw6v.js";
|
|
6
|
+
import { $ as SourceConfigError, A as identityProviders, B as verifyIdentity, C as FileAppender, D as streamStored, E as storedSize, F as onAdaptersChanged, G as normalizePhrase, H as LightningCards, I as registeredLightningProviders, J as sourceKey, K as lightningPaying, L as registeredOnchainProviders, M as resolveAtprotoDid, N as setOwnDidSource, P as chosenResolver, Q as PROVIDER_PLATFORMS, R as reserveAdapterIds, S as WALLET_TYPES, T as removeStored, U as cardsKey, V as CASHU_CARD, W as isRecoveryPhrase, X as cashuMint, Y as CASHU_MINT_SOURCE, Z as NothingSpentError, _ as crossNetwork, _t as setAwayWallets, a as storedGroupFiles, at as breezDatabaseInUse, b as walletInstances, c as receivedPairedReply, ct as iceServerProblem, d as paymentAlias, dt as ModeGate, et as SourceUnreachableError, f as paymentWireId, ft as WrongNetworkError, g as createTiming, gt as requireTurn, h as createFailure, ht as refuseAway, i as groupOfFile, it as breezDatabase, j as assertPublicServer, l as replyRef, lt as iceServerSetting, m as assertConfirmedReal, mt as awayFrom, n as groupFileId, nt as networkMode, o as groupReply, ot as breezDatabasesFor, p as WALLET_NAMES, pt as networkLabel, q as ProviderSources, r as groupFileText, rt as redact, s as pairedWireReply, st as dropBreezDatabase, t as GroupFileDesk, tt as isNothingSpentError, u as replyTo, ut as ModeChanged, v as paymentNetwork, vt as setSingleWriterGate, w as readStored$1, y as paymentNetworksOf, z as boundedIdentityFetch } from "./groupFiles-BNTSDaoq.js";
|
|
7
|
+
import { i as fedimintDatabase, n as createIrohWebEndpoint, o as loadFedimintSdk, r as irohRelayProblem, t as DEFAULT_IROH_RELAYS } from "./irohWeb--YyEA-hX.js";
|
|
8
|
+
import { c as isTestMint, d as paysItsOwnInvoices, l as isWorthlessMint, n as DEFAULT_MINTS, o as fakesLightning, r as TEST_MINT, t as BITCOIN_INVOICE_ON_TESTNET, u as mintNetwork } from "./mints-BrlY773j.js";
|
|
9
|
+
import { c as fileSpace, f as FILE_BYTES_STEP, o as fileBytes, r as blobDigest, s as fileBytesOf, u as removeFileBytes } from "./fileBytes-Di7VZtmU.js";
|
|
10
|
+
import { n as MAX_BACKUP_BYTES, t as BACKUP_MEDIA_TYPE } from "./envelope-BvU0fLEt.js";
|
|
11
|
+
import { t as hyperdhtRelayProblem } from "./hyperdhtRelay-BT_wRxab.js";
|
|
7
12
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
8
13
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
9
14
|
import { base64, base64url, base64urlnopad, bech32, hex } from "@scure/base";
|
|
10
15
|
import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
11
16
|
import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
12
17
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
13
|
-
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
14
|
-
import { decode, noteEncode, npubEncode } from "nostr-tools/nip19";
|
|
15
|
-
import { finalizeEvent, getPublicKey, verifyEvent } from "nostr-tools/pure";
|
|
16
18
|
import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from "@scure/bip39";
|
|
19
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
17
20
|
import { Amount, MeltChangeError, OutputData, Wallet, decodePaymentRequest, deserializeSwapPreview, getEncodedToken, getTokenMetadata, isMintOperationError, serializeSwapPreview } from "@cashu/cashu-ts";
|
|
18
21
|
import { Address, OutScript, Transaction } from "@scure/btc-signer";
|
|
19
22
|
import * as nip04 from "nostr-tools/nip04";
|
|
20
23
|
import * as nip44 from "nostr-tools/nip44";
|
|
24
|
+
import { finalizeEvent, getPublicKey, verifyEvent } from "nostr-tools/pure";
|
|
25
|
+
import { decode, noteEncode, npubEncode } from "nostr-tools/nip19";
|
|
21
26
|
/** Evidence older than this says nothing about the clock now. */
|
|
22
27
|
var CLOCK_EVIDENCE_MS = 18e5;
|
|
23
28
|
/** Sources kept of each kind: a profile has a few relays, and may have hundreds of contacts. */
|
|
@@ -180,7 +185,7 @@ async function migrateWalletNetworks(now = Date.now()) {
|
|
|
180
185
|
//#endregion
|
|
181
186
|
//#region ../browser/src/engine/paymentAdapters/usdtWallet.ts
|
|
182
187
|
/** The Ethereum libraries, a large part of the app: loaded once a USDT wallet is made, opened or restored, not with the engine. */
|
|
183
|
-
var loadUsdt = () => import("./usdt-
|
|
188
|
+
var loadUsdt = () => import("./usdt-C1Xao2xH.js");
|
|
184
189
|
/** Every new profile starts with this wallet: an address to receive on, nothing to set up. */
|
|
185
190
|
var DEFAULT_USDT = {
|
|
186
191
|
network: "ethereum",
|
|
@@ -529,21 +534,6 @@ var UsdtWallet = class {
|
|
|
529
534
|
}
|
|
530
535
|
};
|
|
531
536
|
//#endregion
|
|
532
|
-
//#region ../browser/src/shared/ice.ts
|
|
533
|
-
/**
|
|
534
|
-
* What is wrong with a TURN/STUN server, or null. Browsers refuse to create any WebRTC connection when
|
|
535
|
-
* one server in the list is malformed, so a bad entry here would silently cut off calls and files.
|
|
536
|
-
*/
|
|
537
|
-
function iceServerProblem(server) {
|
|
538
|
-
const urls = server.urls.split(/[\s,]+/).filter(Boolean);
|
|
539
|
-
if (!urls.length) return "Enter the TURN server address";
|
|
540
|
-
for (const url of urls) {
|
|
541
|
-
if (!/^(stun|turns?):[^\s/?#]+(\?transport=(udp|tcp))?$/i.test(url)) return `“${url}” is not a TURN or STUN address (turn:host:port)`;
|
|
542
|
-
if (/^turns?:/i.test(url) && (!server.username?.trim() || !server.credential?.trim())) return "A TURN server needs its username and credential";
|
|
543
|
-
}
|
|
544
|
-
return null;
|
|
545
|
-
}
|
|
546
|
-
//#endregion
|
|
547
537
|
//#region ../browser/src/engine/paymentAdapters/backup.ts
|
|
548
538
|
/**
|
|
549
539
|
* Wallet database versions a backup may carry. 3 is what SDK 0.4.74-0.4.76 made (Ghostly 1.1.3 and earlier);
|
|
@@ -642,7 +632,7 @@ async function restoreArkDatabase(walletId, snapshot) {
|
|
|
642
632
|
//#endregion
|
|
643
633
|
//#region ../browser/src/engine/paymentAdapters/arkWallet.ts
|
|
644
634
|
/** The Ark SDK, a large part of the app: loaded once an Ark wallet is made, opened or restored, not with the engine. */
|
|
645
|
-
var loadArkade = () => import("./arkade-
|
|
635
|
+
var loadArkade = () => import("./arkade-D4BVJFEQ.js");
|
|
646
636
|
/** How long a Testnet wallet about to make way for another waits for its server to say what it holds. */
|
|
647
637
|
var RETIRE_READ_MS = 1e4;
|
|
648
638
|
/** Every new profile starts with this wallet: nothing to set up before receiving. */
|
|
@@ -11154,18 +11144,39 @@ var DEFAULT_APP_STORES = [{
|
|
|
11154
11144
|
url: "https://raw.githubusercontent.com/MiguelMedeiros/ghostly-store/HEAD/ghostly-store.json",
|
|
11155
11145
|
key: "y379ia3t1urwudj8o4w1qwmuwp1mcxyf956b5r7pqup7stce6diy"
|
|
11156
11146
|
}];
|
|
11147
|
+
/**
|
|
11148
|
+
* The apps that come with Ghostly. A short list kept here on purpose, never "everything in the official store": an
|
|
11149
|
+
* entry is added with the pin of its bundle in apps/ui/src/assets/apps/built-in.json, which the build reads from the
|
|
11150
|
+
* app's own repository (apps/ui/scripts/built-in-apps.mjs); no bundle is kept in this repository.
|
|
11151
|
+
*
|
|
11152
|
+
* Chess: Ghostly's own publisher key for it (fingerprint odcg w6wj w8dy nqop), version 2.3.0 (`sequence` 4), at the
|
|
11153
|
+
* commit of its repository that the official store lists.
|
|
11154
|
+
*/
|
|
11155
|
+
var BUILT_IN_APPS = [{
|
|
11156
|
+
publisher: "odcgw6wjw8dynqop84r47jbjcdqossbgrfiejd367e14hgxmjcho",
|
|
11157
|
+
name: "chess",
|
|
11158
|
+
digest: "EEnFmP0grTkTe_9qgapQsEEZzWB9FcpoimWkHnfecVs",
|
|
11159
|
+
url: "https://cdn.jsdelivr.net/gh/MiguelMedeiros/ghostly-chess@c665ab194478145fd9b9a4777e6c0d1893969649/app.ghostlyapp",
|
|
11160
|
+
title: "Chess",
|
|
11161
|
+
tagline: "Play chess with a contact, live in your chat"
|
|
11162
|
+
}];
|
|
11157
11163
|
var APP_STORAGE_LIMITS = {
|
|
11158
11164
|
/** A key, in UTF-8 bytes. */
|
|
11159
11165
|
keyBytes: 256,
|
|
11160
11166
|
/** A value, as compact JSON in UTF-8 bytes. */
|
|
11161
11167
|
valueBytes: 65536,
|
|
11162
11168
|
/**
|
|
11163
|
-
* Counted for each key beside its own bytes and its value's: what the row costs on the device (the app's reference
|
|
11164
|
-
* the scope
|
|
11165
|
-
* held 20 times its 5 MiB.
|
|
11169
|
+
* Counted for each key beside its own bytes and its value's: what the row costs on the device (the app's reference and
|
|
11170
|
+
* the scope, in the row's own key and in its two indexes). Without it a scope of one-byte values held 20 times its 5 MiB.
|
|
11166
11171
|
*/
|
|
11167
11172
|
rowBytes: 128,
|
|
11168
|
-
/**
|
|
11173
|
+
/**
|
|
11174
|
+
* How many times a key's bytes count. The device keeps a key four times (the row's own key, its value and both
|
|
11175
|
+
* indexes), three of them as UTF-16, about 10 bytes for each byte of the key: counted once, a scope of 256-byte keys
|
|
11176
|
+
* took 40 to 55 MB for its 5 MiB.
|
|
11177
|
+
*/
|
|
11178
|
+
keyTimes: 8,
|
|
11179
|
+
/** Every key and value of one app in one scope, with `rowBytes` for each key and its bytes `keyTimes` over. */
|
|
11169
11180
|
scopeBytes: 5242880
|
|
11170
11181
|
};
|
|
11171
11182
|
var APP_CHECK_TIMINGS = {
|
|
@@ -11178,22 +11189,74 @@ var APP_CHECK_TIMINGS = {
|
|
|
11178
11189
|
/** Revocations are checked this long at a time before the page's thread is let go. */
|
|
11179
11190
|
var REVOCATIONS_SLICE_MS = 10;
|
|
11180
11191
|
/**
|
|
11192
|
+
* `ghostly-revoke.json` files whose result is kept, so an unchanged one is not checked again (the oldest dropped first):
|
|
11193
|
+
* at most this many, and of the ones whose list is held at most one file's limit of bytes in all. A publisher's file is
|
|
11194
|
+
* small; nine large ones beside one app's URLs, or one that changes at every check, would stay for the engine's life.
|
|
11195
|
+
*/
|
|
11196
|
+
var REVOCATION_FILES_KEPT = {
|
|
11197
|
+
max: 64,
|
|
11198
|
+
bytes: APP_FETCH_LIMITS.revocationsBytes
|
|
11199
|
+
};
|
|
11200
|
+
/**
|
|
11181
11201
|
* Revocations of store indexes whose signature was checked, kept by `appRevocationId` so an index that changes around
|
|
11182
11202
|
* them does not have each checked again (the one read longest ago dropped first): four indexes' worth, about 1 MiB.
|
|
11183
11203
|
*/
|
|
11184
11204
|
var STORE_REVOCATIONS_KEPT = 4 * APP_STORE_LIMITS.revoked;
|
|
11185
|
-
/** `ghostly-revoke.json` files whose result is kept, so an unchanged one is not checked again (the oldest dropped first). */
|
|
11186
|
-
var REVOCATION_FILES_KEPT = 64;
|
|
11187
11205
|
/** A fetched bundle kept for its install screen: at most this many, for this long. */
|
|
11188
11206
|
var STAGED = {
|
|
11189
11207
|
max: 4,
|
|
11190
11208
|
ms: 6e5
|
|
11191
11209
|
};
|
|
11210
|
+
/**
|
|
11211
|
+
* Checked bundles kept in memory: the most recently used `min`, and beyond them each one a running app used (`entry`,
|
|
11212
|
+
* `file`, `fileAt`) within `ms`. The engine is not told when an app closes; one not used for that long goes, and is read
|
|
11213
|
+
* and checked again if it still runs.
|
|
11214
|
+
*/
|
|
11215
|
+
var BUNDLES_KEPT = {
|
|
11216
|
+
min: 2,
|
|
11217
|
+
ms: 6e4
|
|
11218
|
+
};
|
|
11192
11219
|
/** The first bytes of a bundle: magic, the manifest's length and the largest manifest. */
|
|
11193
11220
|
var PEEK_BYTES = APP_BUNDLE_MAGIC.length + 4 + APP_BUNDLE_LIMITS.manifestBytes;
|
|
11194
11221
|
var bundleId = (digest) => `app-${digest}`;
|
|
11195
|
-
/**
|
|
11196
|
-
|
|
11222
|
+
/**
|
|
11223
|
+
* The key of a scope's counter row (`AppScopeCount` as JSON in `value`, `size` 0): an app's key is never empty
|
|
11224
|
+
* (`keyOf`), and the app's own rows of a scope are `keysOf`, which leaves it out.
|
|
11225
|
+
*/
|
|
11226
|
+
var COUNT_KEY = "";
|
|
11227
|
+
var NO_ROWS = {
|
|
11228
|
+
rows: 0,
|
|
11229
|
+
keyBytes: 0,
|
|
11230
|
+
size: 0
|
|
11231
|
+
};
|
|
11232
|
+
/**
|
|
11233
|
+
* What rows take of their scope's 5 MiB: `rowBytes` each, their keys `keyTimes` over and their values. Not kept, so a
|
|
11234
|
+
* count from before counts alike.
|
|
11235
|
+
*/
|
|
11236
|
+
var costOf = ({ rows, keyBytes, size }) => rows * APP_STORAGE_LIMITS.rowBytes + size + (APP_STORAGE_LIMITS.keyTimes - 1) * keyBytes;
|
|
11237
|
+
/** One row as a count, `sign` -1 to take it away. */
|
|
11238
|
+
var countOf = ({ key, size }, sign = 1) => ({
|
|
11239
|
+
rows: sign,
|
|
11240
|
+
keyBytes: sign * byteLength(key),
|
|
11241
|
+
size: sign * size
|
|
11242
|
+
});
|
|
11243
|
+
var plus = (a, b) => ({
|
|
11244
|
+
rows: a.rows + b.rows,
|
|
11245
|
+
keyBytes: a.keyBytes + b.keyBytes,
|
|
11246
|
+
size: a.size + b.size
|
|
11247
|
+
});
|
|
11248
|
+
function readCount(value) {
|
|
11249
|
+
try {
|
|
11250
|
+
const c = JSON.parse(value);
|
|
11251
|
+
return [
|
|
11252
|
+
c.rows,
|
|
11253
|
+
c.keyBytes,
|
|
11254
|
+
c.size
|
|
11255
|
+
].every((n) => Number.isSafeInteger(n) && n >= 0) ? c : null;
|
|
11256
|
+
} catch {
|
|
11257
|
+
return null;
|
|
11258
|
+
}
|
|
11259
|
+
}
|
|
11197
11260
|
/**
|
|
11198
11261
|
* A listing's URLs this client reads, in order, a jsDelivr copy first: one this client reads names a full commit, so it
|
|
11199
11262
|
* holds exactly the version the store reviewed, where a raw `HEAD` may have moved on to one it has not (WISP 1200 §
|
|
@@ -11209,6 +11272,46 @@ function fail(code, words) {
|
|
|
11209
11272
|
throw new Error(`${code}: ${words}`);
|
|
11210
11273
|
}
|
|
11211
11274
|
/**
|
|
11275
|
+
* The key, in `STORES.settings`, of what the person did about the apps that come with Ghostly (one of
|
|
11276
|
+
* `PROFILE_SETTINGS_KEYS`: "Clear all data" drops it, and the profile is offered them as a new one is).
|
|
11277
|
+
*/
|
|
11278
|
+
var BUILT_IN_APPS_KEY = "appBuiltIns";
|
|
11279
|
+
/** The marks as kept, read with nothing taken on trust: a record that is not one is no mark. */
|
|
11280
|
+
function builtInMarks(kept) {
|
|
11281
|
+
const removed = {};
|
|
11282
|
+
const held = kept?.removed;
|
|
11283
|
+
if (held && typeof held === "object") {
|
|
11284
|
+
for (const [ref, at] of Object.entries(held)) if (isAppRef(ref) && typeof at === "number") removed[ref] = at;
|
|
11285
|
+
}
|
|
11286
|
+
return { removed };
|
|
11287
|
+
}
|
|
11288
|
+
/** Queues, on the settings store of an open transaction, a change of the built-in apps' marks. */
|
|
11289
|
+
function markBuiltIns(settings, change) {
|
|
11290
|
+
const read = settings.get(BUILT_IN_APPS_KEY);
|
|
11291
|
+
read.onsuccess = () => {
|
|
11292
|
+
const marks = builtInMarks(read.result);
|
|
11293
|
+
change(marks);
|
|
11294
|
+
settings.put(marks, BUILT_IN_APPS_KEY);
|
|
11295
|
+
};
|
|
11296
|
+
}
|
|
11297
|
+
var builtInRef = (app) => appRef(app.publisher, app.name);
|
|
11298
|
+
/**
|
|
11299
|
+
* The entries of the built-in list this profile is offered (WISP 1200 § Built-in apps): each reference once, not
|
|
11300
|
+
* installed (`installed`, by reference) and not removed by the person. An entry that is not a whole one (its key, its
|
|
11301
|
+
* name, its digest, a URL off the list of `appFetch.ts`, no title or tagline) is skipped, as a build with no bundle
|
|
11302
|
+
* for it would be.
|
|
11303
|
+
*/
|
|
11304
|
+
function builtInsDue(list, installed, marks) {
|
|
11305
|
+
const due = /* @__PURE__ */ new Map();
|
|
11306
|
+
for (const app of list) {
|
|
11307
|
+
if (!isAppKey(app.publisher) || !isAppHash(app.digest) || !isAppFetchUrl(app.url) || !app.title || !app.tagline) continue;
|
|
11308
|
+
const ref = builtInRef(app);
|
|
11309
|
+
if (!isAppRef(ref) || due.has(ref) || installed.has(ref) || marks.removed[ref] !== void 0) continue;
|
|
11310
|
+
due.set(ref, app);
|
|
11311
|
+
}
|
|
11312
|
+
return [...due.values()];
|
|
11313
|
+
}
|
|
11314
|
+
/**
|
|
11212
11315
|
* Why a pasted link is not read: a github.com page that is neither a repository nor a file of one (`github-link`), so
|
|
11213
11316
|
* the person is not told GitHub is off the list; any other host (`host`).
|
|
11214
11317
|
*/
|
|
@@ -11231,8 +11334,26 @@ function withRevocations(app, read) {
|
|
|
11231
11334
|
revocations: [...merged.values()]
|
|
11232
11335
|
} : app;
|
|
11233
11336
|
}
|
|
11234
|
-
/** Rows of one app (`[ref]`), or of one app in one scope (`[ref, scope]`), in `STORES.appStorage
|
|
11337
|
+
/** Rows of one app (`[ref]`), or of one app in one scope (`[ref, scope]`), in `STORES.appStorage`, counter rows included. */
|
|
11235
11338
|
var rowsOf = (...prefix) => IDBKeyRange.bound(prefix, [...prefix, []]);
|
|
11339
|
+
/** The app's own rows of one scope: its counter row left out. */
|
|
11340
|
+
var keysOf = (ref, scope) => IDBKeyRange.bound([
|
|
11341
|
+
ref,
|
|
11342
|
+
scope,
|
|
11343
|
+
COUNT_KEY
|
|
11344
|
+
], [
|
|
11345
|
+
ref,
|
|
11346
|
+
scope,
|
|
11347
|
+
[]
|
|
11348
|
+
], true);
|
|
11349
|
+
/** A scope's counter row (`COUNT_KEY`). */
|
|
11350
|
+
var counterRow = (ref, scope, count) => ({
|
|
11351
|
+
ref,
|
|
11352
|
+
scope,
|
|
11353
|
+
key: COUNT_KEY,
|
|
11354
|
+
value: JSON.stringify(count),
|
|
11355
|
+
size: 0
|
|
11356
|
+
});
|
|
11236
11357
|
function done(tx) {
|
|
11237
11358
|
return new Promise((resolve, reject) => {
|
|
11238
11359
|
tx.oncomplete = () => resolve();
|
|
@@ -11265,8 +11386,10 @@ function peekAppManifest(bytes) {
|
|
|
11265
11386
|
var Apps = class {
|
|
11266
11387
|
host;
|
|
11267
11388
|
staged = /* @__PURE__ */ new Map();
|
|
11268
|
-
/** Bundles read back and checked, by digest: the
|
|
11389
|
+
/** Bundles read back and checked, by digest, least recently used first (`BUNDLES_KEPT`): the ones running are read once, not on every `file`. */
|
|
11269
11390
|
verified = /* @__PURE__ */ new Map();
|
|
11391
|
+
/** When a running app last used its bundle, by digest (`BUNDLES_KEPT`): an icon drawn is not a use. */
|
|
11392
|
+
runUsed = /* @__PURE__ */ new Map();
|
|
11270
11393
|
/** Reads of a bundle under way, by digest: icons drawn at once share one read. */
|
|
11271
11394
|
verifying = /* @__PURE__ */ new Map();
|
|
11272
11395
|
/**
|
|
@@ -11274,7 +11397,7 @@ var Apps = class {
|
|
|
11274
11397
|
* page, in a chat's cards and in the composer's Apps is read without the whole bundle again.
|
|
11275
11398
|
*/
|
|
11276
11399
|
icons = /* @__PURE__ */ new Map();
|
|
11277
|
-
/**
|
|
11400
|
+
/** What each `ref\0scope` holds, read once from its counter row and kept in step by the writes here. */
|
|
11278
11401
|
usage = /* @__PURE__ */ new Map();
|
|
11279
11402
|
queues = /* @__PURE__ */ new Map();
|
|
11280
11403
|
timer = null;
|
|
@@ -11298,7 +11421,7 @@ var Apps = class {
|
|
|
11298
11421
|
* index listing the same is not peeked again at every check.
|
|
11299
11422
|
*/
|
|
11300
11423
|
listingMisses = /* @__PURE__ */ new Map();
|
|
11301
|
-
/** `ghostly-revoke.json` files checked, by the SHA-256 of their bytes: their revocations, or null when refused. */
|
|
11424
|
+
/** `ghostly-revoke.json` files checked, by the SHA-256 of their bytes: their revocations, or null when refused, and the bytes the held list counts for. */
|
|
11302
11425
|
revocationFiles = /* @__PURE__ */ new Map();
|
|
11303
11426
|
/** Revocations read in a store's index and verified, by `appRevocationId`: at most `STORE_REVOCATIONS_KEPT`. */
|
|
11304
11427
|
storeRevocations = /* @__PURE__ */ new Set();
|
|
@@ -11391,6 +11514,59 @@ var Apps = class {
|
|
|
11391
11514
|
if (!isAppRef(ref)) fail("bad-ref", "Not an app reference");
|
|
11392
11515
|
return await this.app(ref) ?? fail("not-installed", "This app is not installed");
|
|
11393
11516
|
}
|
|
11517
|
+
/** The built-in list's entry of this reference, if it has one. */
|
|
11518
|
+
builtIn(ref) {
|
|
11519
|
+
return (this.host.builtIns ?? BUILT_IN_APPS).find((app) => builtInRef(app) === ref);
|
|
11520
|
+
}
|
|
11521
|
+
async builtInMarks() {
|
|
11522
|
+
return builtInMarks(await wrap((await store(STORES$1.settings, "readonly")).get(BUILT_IN_APPS_KEY)));
|
|
11523
|
+
}
|
|
11524
|
+
/**
|
|
11525
|
+
* The built-in apps this profile is offered (WISP 1200 § Built-in apps): shown with the installed ones, with nothing
|
|
11526
|
+
* stored and nothing granted. Opening one is an install like any other, from the bundle the build carries.
|
|
11527
|
+
*/
|
|
11528
|
+
async builtIns() {
|
|
11529
|
+
const list = this.host.builtIns ?? BUILT_IN_APPS;
|
|
11530
|
+
if (!list.length || !this.host.readBuiltIn) return [];
|
|
11531
|
+
const [installed, marks] = await Promise.all([this.installed(), this.builtInMarks()]);
|
|
11532
|
+
return builtInsDue(list, new Set(installed.map((a) => a.ref)), marks).map((app) => ({
|
|
11533
|
+
ref: builtInRef(app),
|
|
11534
|
+
publisher: app.publisher,
|
|
11535
|
+
name: app.name,
|
|
11536
|
+
fingerprint: appFingerprint(app.publisher),
|
|
11537
|
+
digest: app.digest,
|
|
11538
|
+
url: app.url,
|
|
11539
|
+
title: app.title,
|
|
11540
|
+
tagline: app.tagline
|
|
11541
|
+
}));
|
|
11542
|
+
}
|
|
11543
|
+
/**
|
|
11544
|
+
* The bundle the build carries for a built-in app, checked as a fetched one is, and against its entry: the digest the
|
|
11545
|
+
* list pins and the entry's reference. Anything else in that file is not this app, and nothing of it is kept.
|
|
11546
|
+
*/
|
|
11547
|
+
async bundled(app) {
|
|
11548
|
+
if (!this.host.readBuiltIn) fail("not-carried", "This build does not carry this app");
|
|
11549
|
+
let bytes;
|
|
11550
|
+
try {
|
|
11551
|
+
bytes = await this.host.readBuiltIn(app);
|
|
11552
|
+
} catch {
|
|
11553
|
+
fail("not-carried", "This build does not carry this app");
|
|
11554
|
+
}
|
|
11555
|
+
if (!(bytes instanceof Uint8Array) || bytes.length > APP_BUNDLE_LIMITS.bundleBytes) fail("not-carried", "This build does not carry this app");
|
|
11556
|
+
const read = await readAppBundleAsync(bytes);
|
|
11557
|
+
if (!read.ok) fail(read.reason, `This is not an app Ghostly can install${read.detail ? ` (${read.detail})` : ""}`);
|
|
11558
|
+
if (read.bundle.digest !== app.digest || versionOf(read.bundle.manifest, read.bundle.digest).ref !== builtInRef(app)) fail("unlisted", "The app this build carries is not the one it names");
|
|
11559
|
+
return {
|
|
11560
|
+
bundle: read.bundle,
|
|
11561
|
+
bytes
|
|
11562
|
+
};
|
|
11563
|
+
}
|
|
11564
|
+
/** The built-in entry of this reference while the profile is offered it (`builtIns`), else undefined. */
|
|
11565
|
+
async offered(ref) {
|
|
11566
|
+
const app = this.builtIn(ref);
|
|
11567
|
+
if (!app || !this.host.readBuiltIn) return void 0;
|
|
11568
|
+
return builtInsDue([app], await this.app(ref) ? /* @__PURE__ */ new Set([ref]) : /* @__PURE__ */ new Set(), await this.builtInMarks())[0];
|
|
11569
|
+
}
|
|
11394
11570
|
/**
|
|
11395
11571
|
* An installed app that may still run (WISP 1200 § Takedowns): a running app whose version its publisher revoked, or
|
|
11396
11572
|
* a store of the person's removed (unless this run was started with Run anyway), is cut off with `stopped`, whether
|
|
@@ -11543,7 +11719,7 @@ var Apps = class {
|
|
|
11543
11719
|
*/
|
|
11544
11720
|
async writeBundle(digest, bytes) {
|
|
11545
11721
|
const id = bundleId(digest);
|
|
11546
|
-
const expected = toBase64Url(
|
|
11722
|
+
const expected = toBase64Url(await sha256Paced(bytes));
|
|
11547
11723
|
let files;
|
|
11548
11724
|
let stored;
|
|
11549
11725
|
try {
|
|
@@ -11588,6 +11764,7 @@ var Apps = class {
|
|
|
11588
11764
|
}
|
|
11589
11765
|
async removeBundle(kind, digest) {
|
|
11590
11766
|
this.verified.delete(digest);
|
|
11767
|
+
this.runUsed.delete(digest);
|
|
11591
11768
|
this.icons.delete(digest);
|
|
11592
11769
|
await (await fileBytesOf(kind))?.remove(bundleId(digest)).catch(() => {});
|
|
11593
11770
|
}
|
|
@@ -11597,18 +11774,24 @@ var Apps = class {
|
|
|
11597
11774
|
if (app?.digest !== digest && app?.pending?.digest !== digest) await this.removeBundle(kind, digest);
|
|
11598
11775
|
}
|
|
11599
11776
|
/** The installed bundle read back and checked again before it runs (WISP 1200 § Signatures: "again before it runs one"). */
|
|
11600
|
-
verifiedBundle(app) {
|
|
11777
|
+
verifiedBundle(app, running) {
|
|
11778
|
+
if (running) this.runUsed.set(app.digest, this.now());
|
|
11601
11779
|
const kept = this.verified.get(app.digest);
|
|
11602
|
-
if (kept)
|
|
11780
|
+
if (kept) {
|
|
11781
|
+
this.verified.delete(app.digest);
|
|
11782
|
+
this.verified.set(app.digest, kept);
|
|
11783
|
+
this.dropUnusedBundles();
|
|
11784
|
+
return Promise.resolve(kept);
|
|
11785
|
+
}
|
|
11603
11786
|
const under = this.verifying.get(app.digest);
|
|
11604
11787
|
if (under) return under;
|
|
11605
11788
|
const reading = (async () => {
|
|
11606
11789
|
const bytes = await this.readBundleBytes(app.bytes, app.digest);
|
|
11607
11790
|
if (!bytes) fail("needs-files", "This app's files are not on this device yet");
|
|
11608
|
-
const read =
|
|
11791
|
+
const read = await readAppBundleAsync(bytes);
|
|
11609
11792
|
if (!read.ok || read.bundle.digest !== app.digest) fail("damaged", "This app's stored files do not match what was installed");
|
|
11610
|
-
if (this.verified.size >= 2) this.verified.delete(this.verified.keys().next().value);
|
|
11611
11793
|
this.verified.set(app.digest, read.bundle);
|
|
11794
|
+
this.dropUnusedBundles();
|
|
11612
11795
|
this.icons.set(app.digest, read.bundle.files.get("icon.png")?.slice() ?? null);
|
|
11613
11796
|
return read.bundle;
|
|
11614
11797
|
})().finally(() => {
|
|
@@ -11617,9 +11800,21 @@ var Apps = class {
|
|
|
11617
11800
|
this.verifying.set(app.digest, reading);
|
|
11618
11801
|
return reading;
|
|
11619
11802
|
}
|
|
11803
|
+
/**
|
|
11804
|
+
* Apps running at once do not push each other's bundle out, which every `file` read would then read and check again
|
|
11805
|
+
* whole; one closed (or only drawn as an icon) goes once two newer ones are kept, at the next bundle read or hit.
|
|
11806
|
+
*/
|
|
11807
|
+
dropUnusedBundles() {
|
|
11808
|
+
for (const digest of this.verified.keys()) {
|
|
11809
|
+
if (this.verified.size <= BUNDLES_KEPT.min) break;
|
|
11810
|
+
if (this.now() - (this.runUsed.get(digest) ?? -Infinity) < BUNDLES_KEPT.ms) continue;
|
|
11811
|
+
this.verified.delete(digest);
|
|
11812
|
+
this.runUsed.delete(digest);
|
|
11813
|
+
}
|
|
11814
|
+
}
|
|
11620
11815
|
async fetchBundle(url) {
|
|
11621
11816
|
const bytes = await this.host.fetch(url, { maxBytes: APP_BUNDLE_LIMITS.bundleBytes });
|
|
11622
|
-
const read =
|
|
11817
|
+
const read = await readAppBundleAsync(bytes);
|
|
11623
11818
|
if (!read.ok) fail(read.reason, `This is not an app Ghostly can install${read.detail ? ` (${read.detail})` : ""}`);
|
|
11624
11819
|
return {
|
|
11625
11820
|
bundle: read.bundle,
|
|
@@ -11635,7 +11830,7 @@ var Apps = class {
|
|
|
11635
11830
|
*/
|
|
11636
11831
|
async readStoreAt(url, heldKey, held) {
|
|
11637
11832
|
const [indexBytes, sigBytes] = await Promise.all([this.host.fetch(url, { maxBytes: APP_FETCH_LIMITS.storeIndexBytes }), this.host.fetch(besideUrl(url, "ghostly-store.sig"), { maxBytes: APP_FETCH_LIMITS.sigBytes })]);
|
|
11638
|
-
if (held?.index && held.digest !== void 0 && toBase64Url(
|
|
11833
|
+
if (held?.index && held.digest !== void 0 && toBase64Url(await sha256Paced(indexBytes)) === held.digest) return {
|
|
11639
11834
|
ok: true,
|
|
11640
11835
|
store: {
|
|
11641
11836
|
index: held.index,
|
|
@@ -11644,14 +11839,11 @@ var Apps = class {
|
|
|
11644
11839
|
}
|
|
11645
11840
|
};
|
|
11646
11841
|
const steps = readAppStoreSteps(indexBytes, sigBytes, this.nowS(), heldKey, this.checkedRevocations);
|
|
11647
|
-
|
|
11842
|
+
const pause = pacer(REVOCATIONS_SLICE_MS);
|
|
11648
11843
|
for (;;) {
|
|
11649
11844
|
const step = steps.next();
|
|
11650
11845
|
if (step.done) return step.value;
|
|
11651
|
-
|
|
11652
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
11653
|
-
since = performance.now();
|
|
11654
|
-
}
|
|
11846
|
+
await pause();
|
|
11655
11847
|
}
|
|
11656
11848
|
}
|
|
11657
11849
|
/**
|
|
@@ -11675,19 +11867,16 @@ var Apps = class {
|
|
|
11675
11867
|
* engine shares the page's thread, and each entry is a signature check. The same bytes are not checked again.
|
|
11676
11868
|
*/
|
|
11677
11869
|
async readRevocationsFile(bytes) {
|
|
11678
|
-
const id = toBase64Url(
|
|
11870
|
+
const id = toBase64Url(await sha256Paced(bytes));
|
|
11679
11871
|
const known = this.revocationFiles.get(id);
|
|
11680
|
-
if (known !== void 0) return known;
|
|
11872
|
+
if (known !== void 0) return known.revocations;
|
|
11681
11873
|
const read = readCanonicalJson(bytes);
|
|
11682
11874
|
let out = null;
|
|
11683
11875
|
if (read.ok && Array.isArray(read.value) && read.value.length <= APP_STATEMENT_LIMITS.revocations) {
|
|
11684
11876
|
out = [];
|
|
11685
|
-
|
|
11877
|
+
const pause = pacer(REVOCATIONS_SLICE_MS);
|
|
11686
11878
|
for (const entry of read.value) {
|
|
11687
|
-
|
|
11688
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
11689
|
-
since = performance.now();
|
|
11690
|
-
}
|
|
11879
|
+
await pause();
|
|
11691
11880
|
const one = readAppRevocation(entry);
|
|
11692
11881
|
if (!one.ok) {
|
|
11693
11882
|
out = null;
|
|
@@ -11697,8 +11886,17 @@ var Apps = class {
|
|
|
11697
11886
|
}
|
|
11698
11887
|
}
|
|
11699
11888
|
this.revocationFiles.delete(id);
|
|
11700
|
-
this.revocationFiles.set(id,
|
|
11701
|
-
|
|
11889
|
+
this.revocationFiles.set(id, {
|
|
11890
|
+
revocations: out,
|
|
11891
|
+
bytes: out ? bytes.length : 0
|
|
11892
|
+
});
|
|
11893
|
+
let held = 0;
|
|
11894
|
+
for (const file of this.revocationFiles.values()) held += file.bytes;
|
|
11895
|
+
for (const [old, file] of this.revocationFiles) {
|
|
11896
|
+
if (this.revocationFiles.size <= REVOCATION_FILES_KEPT.max && held <= REVOCATION_FILES_KEPT.bytes) break;
|
|
11897
|
+
this.revocationFiles.delete(old);
|
|
11898
|
+
held -= file.bytes;
|
|
11899
|
+
}
|
|
11702
11900
|
return out;
|
|
11703
11901
|
}
|
|
11704
11902
|
summary(s) {
|
|
@@ -11876,6 +12074,12 @@ var Apps = class {
|
|
|
11876
12074
|
let urls;
|
|
11877
12075
|
let expect = null;
|
|
11878
12076
|
let fromStore;
|
|
12077
|
+
if ("builtIn" in source) {
|
|
12078
|
+
if (!isAppRef(source.builtIn)) fail("bad-ref", "Not an app reference");
|
|
12079
|
+
const app = await this.offered(source.builtIn) ?? fail("not-offered", "This app does not come with Ghostly here");
|
|
12080
|
+
const { bundle, bytes } = await this.bundled(app);
|
|
12081
|
+
return this.stage(bundle, bytes, app.url, void 0, true);
|
|
12082
|
+
}
|
|
11879
12083
|
if ("url" in source) urls = [typeof source.url === "string" && appPasteUrl(source.url, "app.ghostlyapp") || notReadable(source.url, "Apps are read only from raw.githubusercontent.com, or from cdn.jsdelivr.net at a commit, over HTTPS")];
|
|
11880
12084
|
else if ("store" in source) {
|
|
11881
12085
|
const s = await this.storeRecord(source.store);
|
|
@@ -11893,6 +12097,11 @@ var Apps = class {
|
|
|
11893
12097
|
const { ref, url, sequence, digest } = source.card;
|
|
11894
12098
|
if (!isAppRef(ref)) fail("bad-ref", "Not an app reference");
|
|
11895
12099
|
if (!isAppFetchUrl(url)) fail("host", "A card's app is read only from raw.githubusercontent.com, or from cdn.jsdelivr.net at a commit");
|
|
12100
|
+
const carried = typeof digest === "string" ? await this.offered(ref) : void 0;
|
|
12101
|
+
if (carried && carried.digest === digest) {
|
|
12102
|
+
const held = await this.bundled(carried).catch(() => null);
|
|
12103
|
+
if (held) return this.stage(held.bundle, held.bytes, carried.url, void 0, true);
|
|
12104
|
+
}
|
|
11896
12105
|
urls = [url];
|
|
11897
12106
|
expect = {
|
|
11898
12107
|
ref,
|
|
@@ -11924,7 +12133,7 @@ var Apps = class {
|
|
|
11924
12133
|
const oldest = this.staged.values().next().value;
|
|
11925
12134
|
if (oldest) this.sweep = setTimeout(() => this.sweepStaged(), STAGED.ms - (this.now() - oldest.at));
|
|
11926
12135
|
}
|
|
11927
|
-
async stage(bundle, bytes, from, fromStore) {
|
|
12136
|
+
async stage(bundle, bytes, from, fromStore, carried = false) {
|
|
11928
12137
|
for (const [digest, s] of this.staged) if (this.now() - s.at > STAGED.ms) this.staged.delete(digest);
|
|
11929
12138
|
this.staged.delete(bundle.digest);
|
|
11930
12139
|
while (this.staged.size >= STAGED.max) this.staged.delete(this.staged.keys().next().value);
|
|
@@ -11955,7 +12164,8 @@ var Apps = class {
|
|
|
11955
12164
|
asks,
|
|
11956
12165
|
run: this.runStatus(version, this.storeViews(stores), installed?.revocations),
|
|
11957
12166
|
listedBy,
|
|
11958
|
-
unknownPublisher: !listedBy.some((s) => s.kind === "curated")
|
|
12167
|
+
unknownPublisher: !listedBy.some((s) => s.kind === "curated"),
|
|
12168
|
+
...carried && { carried: true }
|
|
11959
12169
|
};
|
|
11960
12170
|
}
|
|
11961
12171
|
/**
|
|
@@ -12009,6 +12219,7 @@ var Apps = class {
|
|
|
12009
12219
|
const kind = await this.writeBundle(digest, bytes);
|
|
12010
12220
|
const next = this.versionRecord(installed, bundle, kind, from, fromStore);
|
|
12011
12221
|
await this.putApp(next);
|
|
12222
|
+
if (!installed && this.builtIn(version.ref)) await this.keepBuiltIn(version.ref);
|
|
12012
12223
|
if (installed && installed.digest !== digest) await this.removeBundle(installed.bytes, installed.digest);
|
|
12013
12224
|
if (installed?.pending && next.pending?.digest !== installed.pending.digest && installed.pending.digest !== digest) await this.removeBundle(installed.pending.bytes, installed.pending.digest);
|
|
12014
12225
|
this.staged.delete(digest);
|
|
@@ -12047,7 +12258,7 @@ var Apps = class {
|
|
|
12047
12258
|
const installed = await this.installedOrFail(ref);
|
|
12048
12259
|
const pending = installed.pending ?? fail("no-update", "No update waits for this app");
|
|
12049
12260
|
const bytes = await this.readBundleBytes(pending.bytes, pending.digest);
|
|
12050
|
-
const read = bytes ?
|
|
12261
|
+
const read = bytes ? await readAppBundleAsync(bytes) : null;
|
|
12051
12262
|
if (!read?.ok || read.bundle.digest !== pending.digest) {
|
|
12052
12263
|
const { pending: _p, ...rest } = installed;
|
|
12053
12264
|
await this.putApp(rest);
|
|
@@ -12282,6 +12493,26 @@ var Apps = class {
|
|
|
12282
12493
|
const app = await this.installedOrFail(ref);
|
|
12283
12494
|
const stores = await this.stores();
|
|
12284
12495
|
if (await this.hasBundle(app.bytes, app.digest)) return this.runCheckOf(app, stores);
|
|
12496
|
+
const carried = this.builtIn(app.ref);
|
|
12497
|
+
if (carried && carried.digest === app.digest) {
|
|
12498
|
+
const held = await this.bundled(carried).catch(() => null);
|
|
12499
|
+
if (held) {
|
|
12500
|
+
const kind = await this.writeBundle(app.digest, held.bytes);
|
|
12501
|
+
const now = await this.app(app.ref);
|
|
12502
|
+
if (!now || now.digest !== app.digest) {
|
|
12503
|
+
await this.removeBundle(kind, app.digest);
|
|
12504
|
+
fail("not-installed", "This app is not installed");
|
|
12505
|
+
}
|
|
12506
|
+
if (now.bytes !== kind) await this.putApp({
|
|
12507
|
+
...now,
|
|
12508
|
+
bytes: kind
|
|
12509
|
+
});
|
|
12510
|
+
return this.runCheckOf({
|
|
12511
|
+
...now,
|
|
12512
|
+
bytes: kind
|
|
12513
|
+
}, stores);
|
|
12514
|
+
}
|
|
12515
|
+
}
|
|
12285
12516
|
const urls = [app.from];
|
|
12286
12517
|
for (const s of stores) {
|
|
12287
12518
|
if (app.store !== void 0 && s.key !== app.store) continue;
|
|
@@ -12289,6 +12520,14 @@ var Apps = class {
|
|
|
12289
12520
|
}
|
|
12290
12521
|
if (app.store === void 0) urls.push(...app.manifest.sources ?? []);
|
|
12291
12522
|
for (const url of new Set(urls.filter(isAppFetchUrl))) {
|
|
12523
|
+
try {
|
|
12524
|
+
if (peekAppManifest(await this.host.fetch(url, {
|
|
12525
|
+
maxBytes: PEEK_BYTES,
|
|
12526
|
+
peek: true
|
|
12527
|
+
}))?.digest !== app.digest) continue;
|
|
12528
|
+
} catch {
|
|
12529
|
+
continue;
|
|
12530
|
+
}
|
|
12292
12531
|
let fetched;
|
|
12293
12532
|
try {
|
|
12294
12533
|
fetched = await this.fetchBundle(url);
|
|
@@ -12336,7 +12575,7 @@ var Apps = class {
|
|
|
12336
12575
|
if (run.status === "removed" && runAnyway !== true) fail("removed", `Removed by ${run.by[0].name}: ${run.by[0].reason}`);
|
|
12337
12576
|
if (run.status === "needs-files") fail("needs-files", "This app's files are not on this device yet");
|
|
12338
12577
|
if (run.status === "removed") this.anyway.add(`${app.ref} ${app.digest}`);
|
|
12339
|
-
const bundle = await this.verifiedBundle(app);
|
|
12578
|
+
const bundle = await this.verifiedBundle(app, true);
|
|
12340
12579
|
const entry = bundle.files.get(bundle.manifest.entry) ?? fail("damaged", "The entry is missing");
|
|
12341
12580
|
this.ran.add(app.ref);
|
|
12342
12581
|
const versions = this.started.get(app.ref) ?? /* @__PURE__ */ new Map();
|
|
@@ -12351,14 +12590,40 @@ var Apps = class {
|
|
|
12351
12590
|
entry: utf8Decode(entry)
|
|
12352
12591
|
};
|
|
12353
12592
|
}
|
|
12354
|
-
/**
|
|
12593
|
+
/**
|
|
12594
|
+
* Where a file of the installed bundle lies in this device's file storage, for a client that hands the bytes to the
|
|
12595
|
+
* app itself (Desktop: Rust reads them, checks their SHA-256 and gives them to the app's window, so they never cross
|
|
12596
|
+
* the Ghostly window). The same checks as `file`; null where the bundle is not in such storage, and for the icon,
|
|
12597
|
+
* which `file` answers from memory.
|
|
12598
|
+
*/
|
|
12599
|
+
async fileAt({ ref, path, digest }) {
|
|
12600
|
+
if (path === "icon.png") return null;
|
|
12601
|
+
const app = await this.runningOrFail(ref, digest);
|
|
12602
|
+
if (digest !== void 0 && digest !== app.digest) fail("no-file", "No such file in this app");
|
|
12603
|
+
if (app.bytes !== "native") return null;
|
|
12604
|
+
const bundle = await this.verifiedBundle(app, true);
|
|
12605
|
+
const file = typeof path === "string" ? bundle.files.get(path) : void 0;
|
|
12606
|
+
const listed = bundle.manifest.files.find((f) => f.path === path);
|
|
12607
|
+
if (!file || !listed) fail("no-file", "No such file in this app");
|
|
12608
|
+
return {
|
|
12609
|
+
space: fileSpace(),
|
|
12610
|
+
id: bundleId(app.digest),
|
|
12611
|
+
offset: file.byteOffset,
|
|
12612
|
+
length: file.length,
|
|
12613
|
+
sha256: listed.sha256
|
|
12614
|
+
};
|
|
12615
|
+
}
|
|
12616
|
+
/**
|
|
12617
|
+
* A file of the installed bundle, for the broker's `file` (`ghostly.file(path)`): a copy, which the broker transfers to
|
|
12618
|
+
* the app as it is. `digest`: the version the frame runs.
|
|
12619
|
+
*/
|
|
12355
12620
|
async file({ ref, path, digest }) {
|
|
12356
12621
|
const app = path === "icon.png" ? await this.installedOrFail(ref) : await this.runningOrFail(ref, digest);
|
|
12357
12622
|
if (path !== "icon.png" && digest !== void 0 && digest !== app.digest) fail("no-file", "No such file in this app");
|
|
12358
12623
|
const icon = path === "icon.png" ? this.icons.get(app.digest) : void 0;
|
|
12359
12624
|
if (icon) return icon.slice();
|
|
12360
12625
|
if (icon === null) fail("no-file", "No such file in this app");
|
|
12361
|
-
const bundle = await this.verifiedBundle(app);
|
|
12626
|
+
const bundle = await this.verifiedBundle(app, path !== APP_ICON_PATH);
|
|
12362
12627
|
if (typeof path !== "string" || !bundle.files.has(path)) fail("no-file", "No such file in this app");
|
|
12363
12628
|
return bundle.files.get(path).slice();
|
|
12364
12629
|
}
|
|
@@ -12389,6 +12654,7 @@ var Apps = class {
|
|
|
12389
12654
|
run: await this.runCheckOf(app, stores),
|
|
12390
12655
|
listedBy,
|
|
12391
12656
|
unknownPublisher: !listedBy.some((s) => s.kind === "curated"),
|
|
12657
|
+
...this.builtIn(app.ref)?.digest === app.digest && { builtIn: true },
|
|
12392
12658
|
...app.pending && { pending: {
|
|
12393
12659
|
sequence: app.pending.sequence,
|
|
12394
12660
|
version: app.pending.manifest.version,
|
|
@@ -12408,23 +12674,44 @@ var Apps = class {
|
|
|
12408
12674
|
}
|
|
12409
12675
|
/**
|
|
12410
12676
|
* Uninstalls an app: its bundle (and a waiting update's), its record and its storage in every scope. The person chose
|
|
12411
|
-
* it, after the offer of an export (`exportData`).
|
|
12677
|
+
* it, after the offer of an export (`exportData`). A built-in app is marked removed in the same transaction, and one
|
|
12678
|
+
* the person never opened is only marked: no start and no update offers it again (WISP 1200 § Built-in apps).
|
|
12412
12679
|
*/
|
|
12413
12680
|
async uninstall({ ref }) {
|
|
12414
|
-
|
|
12415
|
-
const
|
|
12416
|
-
|
|
12417
|
-
|
|
12681
|
+
if (!isAppRef(ref)) fail("bad-ref", "Not an app reference");
|
|
12682
|
+
const app = await this.app(ref);
|
|
12683
|
+
const builtIn = this.builtIn(ref) !== void 0;
|
|
12684
|
+
if (!app && !builtIn) fail("not-installed", "This app is not installed");
|
|
12685
|
+
const tx = (await openDb()).transaction([
|
|
12686
|
+
STORES$1.apps,
|
|
12687
|
+
STORES$1.appStorage,
|
|
12688
|
+
...builtIn ? [STORES$1.settings] : []
|
|
12689
|
+
], "readwrite");
|
|
12690
|
+
tx.objectStore(STORES$1.apps).delete(ref);
|
|
12691
|
+
tx.objectStore(STORES$1.appStorage).delete(rowsOf(ref));
|
|
12692
|
+
if (builtIn) markBuiltIns(tx.objectStore(STORES$1.settings), (marks) => {
|
|
12693
|
+
marks.removed[ref] ??= this.now();
|
|
12694
|
+
});
|
|
12418
12695
|
try {
|
|
12419
12696
|
await done(tx);
|
|
12420
12697
|
} finally {
|
|
12421
|
-
this.held.delete(
|
|
12698
|
+
this.held.delete(ref);
|
|
12422
12699
|
}
|
|
12423
|
-
for (const key of [...this.usage.keys()]) if (key.startsWith(`${
|
|
12424
|
-
this.started.delete(
|
|
12700
|
+
for (const key of [...this.usage.keys()]) if (key.startsWith(`${ref}\0`)) this.usage.delete(key);
|
|
12701
|
+
this.started.delete(ref);
|
|
12702
|
+
if (!app) return;
|
|
12425
12703
|
await this.removeBundle(app.bytes, app.digest);
|
|
12426
12704
|
if (app.pending) await this.removeBundle(app.pending.bytes, app.pending.digest);
|
|
12427
12705
|
}
|
|
12706
|
+
/** Takes the "removed by the person" mark off a built-in app, if it has one. */
|
|
12707
|
+
async keepBuiltIn(ref) {
|
|
12708
|
+
if ((await this.builtInMarks()).removed[ref] === void 0) return;
|
|
12709
|
+
const tx = (await openDb()).transaction(STORES$1.settings, "readwrite");
|
|
12710
|
+
markBuiltIns(tx.objectStore(STORES$1.settings), (marks) => {
|
|
12711
|
+
delete marks.removed[ref];
|
|
12712
|
+
});
|
|
12713
|
+
await done(tx);
|
|
12714
|
+
}
|
|
12428
12715
|
scopeOf(scope) {
|
|
12429
12716
|
if (scope === "alone" || typeof scope === "string" && this.host.isChat(scope)) return scope;
|
|
12430
12717
|
fail("bad-scope", "Not a chat of this profile");
|
|
@@ -12443,13 +12730,24 @@ var Apps = class {
|
|
|
12443
12730
|
});
|
|
12444
12731
|
return run;
|
|
12445
12732
|
}
|
|
12733
|
+
/**
|
|
12734
|
+
* What a scope holds: its counter row, when the number of rows agrees with it (one key count, no value read). A
|
|
12735
|
+
* scope without one (written before it was kept) or where it disagrees (rows written by an older client) is
|
|
12736
|
+
* counted from its rows once.
|
|
12737
|
+
*/
|
|
12446
12738
|
async usageOf(ref, scope) {
|
|
12447
12739
|
const id = `${ref}\0${scope}`;
|
|
12448
|
-
|
|
12449
|
-
if (
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
|
|
12740
|
+
const cached = this.usage.get(id);
|
|
12741
|
+
if (cached) return cached;
|
|
12742
|
+
const rows = await store(STORES$1.appStorage, "readonly");
|
|
12743
|
+
const [counter, n] = await Promise.all([wrap(rows.get([
|
|
12744
|
+
ref,
|
|
12745
|
+
scope,
|
|
12746
|
+
COUNT_KEY
|
|
12747
|
+
])), wrap(rows.count(keysOf(ref, scope)))]);
|
|
12748
|
+
let used = counter ? readCount(counter.value) : n ? null : NO_ROWS;
|
|
12749
|
+
if (!used || used.rows !== n) used = (await wrap((await store(STORES$1.appStorage, "readonly")).getAll(keysOf(ref, scope)))).reduce((sum, r) => plus(sum, countOf(r)), NO_ROWS);
|
|
12750
|
+
this.usage.set(id, used);
|
|
12453
12751
|
return used;
|
|
12454
12752
|
}
|
|
12455
12753
|
async storageGet({ ref, scope, key, digest }) {
|
|
@@ -12463,8 +12761,8 @@ var Apps = class {
|
|
|
12463
12761
|
}
|
|
12464
12762
|
/**
|
|
12465
12763
|
* Keeps a JSON value under a key: at most 64 KiB, and 5 MiB for every key of this app in this scope, each counted
|
|
12466
|
-
*
|
|
12467
|
-
*
|
|
12764
|
+
* as `costOf` says. A scope already past that (filled under an older count) still takes a write that does not grow
|
|
12765
|
+
* it, so the app can make room.
|
|
12468
12766
|
*/
|
|
12469
12767
|
async storageSet({ ref, scope, key, value, digest }) {
|
|
12470
12768
|
const app = await this.runningOrFail(ref, digest);
|
|
@@ -12487,20 +12785,24 @@ var Apps = class {
|
|
|
12487
12785
|
where,
|
|
12488
12786
|
name
|
|
12489
12787
|
]));
|
|
12490
|
-
const next =
|
|
12491
|
-
|
|
12788
|
+
const next = plus(old ? plus(used, countOf(old, -1)) : used, countOf({
|
|
12789
|
+
key: name,
|
|
12790
|
+
size
|
|
12791
|
+
}));
|
|
12792
|
+
if (costOf(next) > APP_STORAGE_LIMITS.scopeBytes && costOf(next) > costOf(used)) fail("full", "This app's storage in this chat is full (5 MiB)");
|
|
12492
12793
|
const writing = await store(STORES$1.appStorage, "readwrite");
|
|
12493
12794
|
if (where !== "alone" && !this.host.isChat(where)) {
|
|
12494
12795
|
this.usage.delete(`${app.ref}\0${where}`);
|
|
12495
12796
|
fail("bad-scope", "Not a chat of this profile");
|
|
12496
12797
|
}
|
|
12497
|
-
|
|
12798
|
+
writing.put({
|
|
12498
12799
|
ref: app.ref,
|
|
12499
12800
|
scope: where,
|
|
12500
12801
|
key: name,
|
|
12501
12802
|
value: text,
|
|
12502
12803
|
size
|
|
12503
|
-
})
|
|
12804
|
+
});
|
|
12805
|
+
await wrap(writing.put(counterRow(app.ref, where, next)));
|
|
12504
12806
|
this.usage.set(`${app.ref}\0${where}`, next);
|
|
12505
12807
|
});
|
|
12506
12808
|
}
|
|
@@ -12515,39 +12817,73 @@ var Apps = class {
|
|
|
12515
12817
|
name
|
|
12516
12818
|
]));
|
|
12517
12819
|
if (!old) return;
|
|
12518
|
-
|
|
12820
|
+
const next = plus(used, countOf(old, -1));
|
|
12821
|
+
const writing = await store(STORES$1.appStorage, "readwrite");
|
|
12822
|
+
writing.delete([
|
|
12519
12823
|
app.ref,
|
|
12520
12824
|
where,
|
|
12521
12825
|
name
|
|
12522
|
-
])
|
|
12523
|
-
|
|
12826
|
+
]);
|
|
12827
|
+
await (next.rows ? wrap(writing.put(counterRow(app.ref, where, next))) : wrap(writing.delete([
|
|
12828
|
+
app.ref,
|
|
12829
|
+
where,
|
|
12830
|
+
COUNT_KEY
|
|
12831
|
+
])));
|
|
12832
|
+
this.usage.set(`${app.ref}\0${where}`, next);
|
|
12524
12833
|
});
|
|
12525
12834
|
}
|
|
12526
12835
|
async storageKeys({ ref, scope, digest }) {
|
|
12527
12836
|
const app = await this.runningOrFail(ref, digest);
|
|
12528
|
-
return (await wrap((await store(STORES$1.appStorage, "readonly")).getAllKeys(
|
|
12837
|
+
return (await wrap((await store(STORES$1.appStorage, "readonly")).getAllKeys(keysOf(app.ref, this.scopeOf(scope))))).map((k) => k[2]);
|
|
12529
12838
|
}
|
|
12530
12839
|
/** An app's storage in every scope, as export files (WISP 1200 § Where installed apps live), offered before an uninstall. */
|
|
12531
12840
|
async exportData({ ref }) {
|
|
12532
|
-
|
|
12533
|
-
|
|
12534
|
-
const scopes = /* @__PURE__ */ new Map();
|
|
12535
|
-
for (const row of rows) {
|
|
12536
|
-
const out = scopes.get(row.scope) ?? {
|
|
12841
|
+
return (await this.exportDataText({ ref })).map(({ app, scope, entries }) => {
|
|
12842
|
+
const out = {
|
|
12537
12843
|
ghostlyAppData: 1,
|
|
12538
|
-
app
|
|
12539
|
-
scope
|
|
12844
|
+
app,
|
|
12845
|
+
scope,
|
|
12540
12846
|
entries: {}
|
|
12541
12847
|
};
|
|
12542
|
-
Object.defineProperty(out.entries,
|
|
12543
|
-
value: JSON.parse(
|
|
12848
|
+
for (const [key, json] of entries) Object.defineProperty(out.entries, key, {
|
|
12849
|
+
value: JSON.parse(json),
|
|
12544
12850
|
enumerable: true,
|
|
12545
12851
|
writable: true,
|
|
12546
12852
|
configurable: true
|
|
12547
12853
|
});
|
|
12548
|
-
|
|
12854
|
+
return out;
|
|
12855
|
+
});
|
|
12856
|
+
}
|
|
12857
|
+
/**
|
|
12858
|
+
* `exportData` with each value as the JSON text it is kept as, for a file written without parsing and writing every
|
|
12859
|
+
* value again. Read a scope at a time, the thread let go between them: ten chats of 5 MiB read at once held it for
|
|
12860
|
+
* the whole read.
|
|
12861
|
+
*/
|
|
12862
|
+
async exportDataText({ ref }) {
|
|
12863
|
+
const app = await this.installedOrFail(ref);
|
|
12864
|
+
const scopes = [];
|
|
12865
|
+
const index = (await store(STORES$1.appStorage, "readonly")).index(APP_STORAGE_SCOPE_INDEX);
|
|
12866
|
+
await new Promise((resolve, reject) => {
|
|
12867
|
+
const request = index.openKeyCursor(rowsOf(app.ref), "nextunique");
|
|
12868
|
+
request.onerror = () => reject(request.error);
|
|
12869
|
+
request.onsuccess = () => {
|
|
12870
|
+
const cursor = request.result;
|
|
12871
|
+
if (!cursor) return resolve();
|
|
12872
|
+
scopes.push(cursor.key[1]);
|
|
12873
|
+
cursor.continue();
|
|
12874
|
+
};
|
|
12875
|
+
});
|
|
12876
|
+
const out = [];
|
|
12877
|
+
for (const [i, scope] of scopes.entries()) {
|
|
12878
|
+
if (i) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
12879
|
+
const rows = await wrap((await store(STORES$1.appStorage, "readonly")).getAll(keysOf(app.ref, scope)));
|
|
12880
|
+
if (rows.length) out.push({
|
|
12881
|
+
app: app.ref,
|
|
12882
|
+
scope,
|
|
12883
|
+
entries: rows.map((row) => [row.key, row.value])
|
|
12884
|
+
});
|
|
12549
12885
|
}
|
|
12550
|
-
return
|
|
12886
|
+
return out;
|
|
12551
12887
|
}
|
|
12552
12888
|
};
|
|
12553
12889
|
//#endregion
|
|
@@ -16672,7 +17008,7 @@ function handoffNotices(was, handoff, session) {
|
|
|
16672
17008
|
* device link must offer every transport both devices run: a Linux Desktop has no WebRTC, and an Iroh relay that
|
|
16673
17009
|
* one side cannot reach leaves HyperDHT as the only way through.
|
|
16674
17010
|
*/
|
|
16675
|
-
var NATIVE_RETRY_MS = 1e4;
|
|
17011
|
+
var NATIVE_RETRY_MS$1 = 1e4;
|
|
16676
17012
|
var NATIVE_RETRY_MAX_MS = 12e4;
|
|
16677
17013
|
/** How often a standby reads the turn while its screen shows (WISP 06 § When a device checks), and the jitter on it. */
|
|
16678
17014
|
var STANDBY_TURN_EVERY_MS = 6e5;
|
|
@@ -16981,7 +17317,7 @@ var DeviceLinks = class {
|
|
|
16981
17317
|
}
|
|
16982
17318
|
retryNative(running) {
|
|
16983
17319
|
if (running.nativeRetry || this.stopped || this.running.get(running.id) !== running) return;
|
|
16984
|
-
const wait = Math.min(NATIVE_RETRY_MS * 2 ** running.nativeMisses, NATIVE_RETRY_MAX_MS);
|
|
17320
|
+
const wait = Math.min(NATIVE_RETRY_MS$1 * 2 ** running.nativeMisses, NATIVE_RETRY_MAX_MS);
|
|
16985
17321
|
running.nativeMisses++;
|
|
16986
17322
|
running.nativeRetry = setTimeout(() => {
|
|
16987
17323
|
running.nativeRetry = void 0;
|
|
@@ -19355,6 +19691,7 @@ var FileDesk = class {
|
|
|
19355
19691
|
openSource: (record) => this.openSource(linkId, record),
|
|
19356
19692
|
changed: (record, transferred, progress) => this.changed(linkId, record, transferred, progress),
|
|
19357
19693
|
room: async () => (await fileBytes()).room(),
|
|
19694
|
+
...this.deps.sendData && { sendData: (id, offset, data) => this.deps.sendData(linkId, id, offset, data) },
|
|
19358
19695
|
writable: () => this.deps.writable?.(linkId)
|
|
19359
19696
|
});
|
|
19360
19697
|
this.chats.set(linkId, made);
|
|
@@ -19374,6 +19711,7 @@ var FileDesk = class {
|
|
|
19374
19711
|
records.push(file.wire3);
|
|
19375
19712
|
if (file.wire3.direction === "in") chat.local.set(file.wire3.id, file.id);
|
|
19376
19713
|
if (!transferEnded(file.wire3)) this.restored.add(file.id);
|
|
19714
|
+
if (file.wire3.state === "done") continue;
|
|
19377
19715
|
this.deps.transfers.set(file.id, this.view(chat, file.wire3, file.wire3.confirmed, transferStalled(file.wire3, Date.now(), this.restored.has(file.id), Date.now())));
|
|
19378
19716
|
}
|
|
19379
19717
|
chat.files.restore(records);
|
|
@@ -19814,7 +20152,7 @@ var FileDesk = class {
|
|
|
19814
20152
|
view.room = this.deps.transfers.get(id)?.room;
|
|
19815
20153
|
this.room(id);
|
|
19816
20154
|
}
|
|
19817
|
-
this.deps.transfers.set(id, view);
|
|
20155
|
+
if (record.state !== "done" || this.underway.has(id) || this.deps.transfers.has(id)) this.deps.transfers.set(id, view);
|
|
19818
20156
|
if (!transferEnded(record)) {
|
|
19819
20157
|
this.underway.add(id);
|
|
19820
20158
|
this.ended.delete(id);
|
|
@@ -20342,6 +20680,10 @@ var Outbox = class {
|
|
|
20342
20680
|
get sameLink() {
|
|
20343
20681
|
return !this.stopped && this.closed === this.began;
|
|
20344
20682
|
}
|
|
20683
|
+
/** One message as stored now: by its id when the store can, else picked out of the chat's history. */
|
|
20684
|
+
async one(id) {
|
|
20685
|
+
return this.store.get ? this.store.get(id) : (await this.store.read()).find((m) => m.id === id);
|
|
20686
|
+
}
|
|
20345
20687
|
/** A new message a turn could not send (the link closed under it, or the ones before it still wait): it waits too. */
|
|
20346
20688
|
async waitsItsTurn(message) {
|
|
20347
20689
|
await this.store.update(message.id, "waiting", "Waits for the messages before it to go.", {
|
|
@@ -20446,7 +20788,7 @@ var Outbox = class {
|
|
|
20446
20788
|
this.busy.add(id);
|
|
20447
20789
|
let again;
|
|
20448
20790
|
try {
|
|
20449
|
-
let message = read ??
|
|
20791
|
+
let message = read ?? await this.one(id);
|
|
20450
20792
|
if (!message?.wireId || !message.delivery || message.delivery === "delivered") return;
|
|
20451
20793
|
const wait = this.waiting.get(id);
|
|
20452
20794
|
if (wait?.timer) {
|
|
@@ -20487,7 +20829,7 @@ var Outbox = class {
|
|
|
20487
20829
|
again = this.reopenedWhileBusy.delete(id);
|
|
20488
20830
|
}
|
|
20489
20831
|
if (again) {
|
|
20490
|
-
const message =
|
|
20832
|
+
const message = await this.one(id);
|
|
20491
20833
|
if (message) await this.again(message);
|
|
20492
20834
|
}
|
|
20493
20835
|
}
|
|
@@ -20497,7 +20839,7 @@ var Outbox = class {
|
|
|
20497
20839
|
* nothing about it is unconfirmed; it counts no attempt until it goes.
|
|
20498
20840
|
*/
|
|
20499
20841
|
async wait(id, reason) {
|
|
20500
|
-
const message =
|
|
20842
|
+
const message = await this.one(id);
|
|
20501
20843
|
if (!message || this.stopped) return;
|
|
20502
20844
|
await this.store.update(id, "waiting", reason, { resendUntil: message.resendUntil ?? this.now() + this.policy.windowMs });
|
|
20503
20845
|
this.schedule(id);
|
|
@@ -20587,7 +20929,7 @@ var Outbox = class {
|
|
|
20587
20929
|
*/
|
|
20588
20930
|
async attempt(id, passed) {
|
|
20589
20931
|
if (this.stopped || this.busy.has(id)) return true;
|
|
20590
|
-
const message =
|
|
20932
|
+
const message = await this.one(id);
|
|
20591
20933
|
if (!message || message.delivery !== "queued" && message.delivery !== "waiting" || message.via === "hold") {
|
|
20592
20934
|
this.forget(id);
|
|
20593
20935
|
return true;
|
|
@@ -20614,7 +20956,7 @@ var Outbox = class {
|
|
|
20614
20956
|
}
|
|
20615
20957
|
/** After a failed attempt: queued again, unless its window or its attempts are spent. */
|
|
20616
20958
|
async requeue(id, reason) {
|
|
20617
|
-
const message =
|
|
20959
|
+
const message = await this.one(id);
|
|
20618
20960
|
if (message && message.delivery !== "delivered") await this.queue(message, reason);
|
|
20619
20961
|
}
|
|
20620
20962
|
async queue(message, reason) {
|
|
@@ -20662,7 +21004,7 @@ var Outbox = class {
|
|
|
20662
21004
|
this.turn(async () => {
|
|
20663
21005
|
if (this.receipts.get(id) !== timer) return;
|
|
20664
21006
|
this.receipts.delete(id);
|
|
20665
|
-
const message =
|
|
21007
|
+
const message = await this.one(id);
|
|
20666
21008
|
if (message?.via === "pkarr") await this.expired(message);
|
|
20667
21009
|
else await this.requeue(id, "No receipt received yet.");
|
|
20668
21010
|
}).catch(() => {});
|
|
@@ -21349,8 +21691,9 @@ var db = {
|
|
|
21349
21691
|
tx.onerror = tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("Link storage failed"));
|
|
21350
21692
|
});
|
|
21351
21693
|
},
|
|
21352
|
-
|
|
21353
|
-
|
|
21694
|
+
/** `relaxed`: for what a power cut may cost (a round trip, a note made again later), no disk sync of its own. */
|
|
21695
|
+
async patchLink(linkId, patch, relaxed = false) {
|
|
21696
|
+
const tx = (await openDb()).transaction(STORES$1.links, "readwrite", relaxed ? RELAXED : void 0);
|
|
21354
21697
|
const links = tx.objectStore(STORES$1.links);
|
|
21355
21698
|
const request = links.get(linkId);
|
|
21356
21699
|
request.onsuccess = () => {
|
|
@@ -21611,6 +21954,32 @@ var db = {
|
|
|
21611
21954
|
});
|
|
21612
21955
|
return result;
|
|
21613
21956
|
},
|
|
21957
|
+
/**
|
|
21958
|
+
* `patchMessage` on these rows of one history, all in one transaction: one disk sync for all of them. Resolves to
|
|
21959
|
+
* the rows changed, in the order of `ids`.
|
|
21960
|
+
*/
|
|
21961
|
+
async patchMessages(linkId, ids, change) {
|
|
21962
|
+
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite");
|
|
21963
|
+
const messages = tx.objectStore(STORES$1.messages);
|
|
21964
|
+
const results = [];
|
|
21965
|
+
ids.forEach((id, i) => {
|
|
21966
|
+
const request = messages.get([linkId, id]);
|
|
21967
|
+
request.onsuccess = () => {
|
|
21968
|
+
const message = request.result;
|
|
21969
|
+
const patch = message && change(message);
|
|
21970
|
+
if (!patch) return;
|
|
21971
|
+
messages.put(results[i] = {
|
|
21972
|
+
...message,
|
|
21973
|
+
...patch
|
|
21974
|
+
});
|
|
21975
|
+
};
|
|
21976
|
+
});
|
|
21977
|
+
await new Promise((resolve, reject) => {
|
|
21978
|
+
tx.oncomplete = () => resolve();
|
|
21979
|
+
tx.onerror = tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("Message storage failed"));
|
|
21980
|
+
});
|
|
21981
|
+
return results.filter((row) => !!row);
|
|
21982
|
+
},
|
|
21614
21983
|
async deleteMessage(linkId, messageId) {
|
|
21615
21984
|
await wrap((await store(STORES$1.messages, "readwrite")).delete([linkId, messageId]));
|
|
21616
21985
|
},
|
|
@@ -22013,6 +22382,202 @@ async function myPin(host, chat, messageId, remove, current, now = Date.now()) {
|
|
|
22013
22382
|
};
|
|
22014
22383
|
}
|
|
22015
22384
|
//#endregion
|
|
22385
|
+
//#region ../browser/src/engine/groupAuthorOrder.ts
|
|
22386
|
+
/**
|
|
22387
|
+
* How long a member's message may still be placed again below that member's older ones (WISP 400, requirement 10):
|
|
22388
|
+
* about a catch-up, whose answers end within the three minutes the app waits for the first.
|
|
22389
|
+
*/
|
|
22390
|
+
var AUTHOR_ORDER_WINDOW_MS = 18e4;
|
|
22391
|
+
AUTHOR_ORDER_WINDOW_MS / 2;
|
|
22392
|
+
/** Whether `a` was written after `b` by the same member: a later epoch, or a higher number in the same one. */
|
|
22393
|
+
function after(a, b) {
|
|
22394
|
+
return a.epoch > b.epoch || a.epoch === b.epoch && a.seq > b.seq;
|
|
22395
|
+
}
|
|
22396
|
+
/**
|
|
22397
|
+
* A member's messages in a private group in the order that member wrote them (WISP 400, requirement 10, and WISP 902
|
|
22398
|
+
* § Catch-up). A message is placed where it arrives. When one comes of a member whose later messages were placed here
|
|
22399
|
+
* moments ago (two catch-up answers raced: the author's own newest 32 and another member's hand-on of the older ones),
|
|
22400
|
+
* those later ones are placed again below it, in the order they were written, and the lines that came after them before
|
|
22401
|
+
* the older one (another member's, mine) go along below them, in their order: a reply to the newest stays below it. A
|
|
22402
|
+
* row only ever moves down, so nobody else's lines change order. While messages of that member are still missing
|
|
22403
|
+
* (`missing`), the rows wait for them, so each row moves about once in a whole catch-up.
|
|
22404
|
+
*/
|
|
22405
|
+
var GroupAuthorOrder = class {
|
|
22406
|
+
host;
|
|
22407
|
+
linkOf;
|
|
22408
|
+
missing;
|
|
22409
|
+
/** Per group: the members' rows followed, in the order they are placed in the history. */
|
|
22410
|
+
rows = /* @__PURE__ */ new Map();
|
|
22411
|
+
/** Per group: the members with a row placed above an older one of theirs, since when, and when their last row came. */
|
|
22412
|
+
due = /* @__PURE__ */ new Map();
|
|
22413
|
+
running = /* @__PURE__ */ new Set();
|
|
22414
|
+
constructor(host, linkOf, missing = () => false) {
|
|
22415
|
+
this.host = host;
|
|
22416
|
+
this.linkOf = linkOf;
|
|
22417
|
+
this.missing = missing;
|
|
22418
|
+
}
|
|
22419
|
+
/** A member's message was stored in the group's history, new: `at` is when, on the engine's clock. */
|
|
22420
|
+
placed(groupId, row) {
|
|
22421
|
+
if (!this.host.placeAgain) return;
|
|
22422
|
+
const rows = (this.rows.get(groupId) ?? []).filter((r) => r.at > row.at - AUTHOR_ORDER_WINDOW_MS).slice(-575);
|
|
22423
|
+
rows.push(row);
|
|
22424
|
+
this.rows.set(groupId, rows);
|
|
22425
|
+
let due = this.due.get(groupId);
|
|
22426
|
+
if (!due?.has(row.sender) && !rows.some((r) => r.sender === row.sender && after(r, row))) return;
|
|
22427
|
+
if (!due) this.due.set(groupId, due = /* @__PURE__ */ new Map());
|
|
22428
|
+
due.set(row.sender, {
|
|
22429
|
+
since: due.get(row.sender)?.since ?? row.at,
|
|
22430
|
+
last: row.at
|
|
22431
|
+
});
|
|
22432
|
+
if (!this.missing(groupId, row.sender)) this.run(groupId, row.at);
|
|
22433
|
+
}
|
|
22434
|
+
/** Time passed: rows nothing more is on its way for, or that waited long enough, are placed again. */
|
|
22435
|
+
tick(now) {
|
|
22436
|
+
for (const groupId of this.due.keys()) this.run(groupId, now);
|
|
22437
|
+
}
|
|
22438
|
+
/** The group's history is gone (left or forgotten). */
|
|
22439
|
+
forget(groupId) {
|
|
22440
|
+
this.rows.delete(groupId);
|
|
22441
|
+
this.due.delete(groupId);
|
|
22442
|
+
}
|
|
22443
|
+
/**
|
|
22444
|
+
* One group's rows put in order, a member at a time; what comes meanwhile is put in order by the next turn, so a
|
|
22445
|
+
* catch-up's frames that come together move each row once, not once per frame.
|
|
22446
|
+
*/
|
|
22447
|
+
async run(groupId, now) {
|
|
22448
|
+
if (this.running.has(groupId)) return;
|
|
22449
|
+
this.running.add(groupId);
|
|
22450
|
+
try {
|
|
22451
|
+
for (;;) {
|
|
22452
|
+
const due = this.due.get(groupId);
|
|
22453
|
+
const ready = [...due ?? []].filter(([sender, { since, last }]) => now - last >= 15e3 || now - since >= 9e4 || !this.missing(groupId, sender)).map(([sender]) => sender);
|
|
22454
|
+
if (!ready.length) break;
|
|
22455
|
+
for (const sender of ready) due.delete(sender);
|
|
22456
|
+
if (!due.size) this.due.delete(groupId);
|
|
22457
|
+
for (const sender of ready) {
|
|
22458
|
+
const rows = this.rows.get(groupId) ?? [];
|
|
22459
|
+
const mine = rows.filter((r) => r.sender === sender);
|
|
22460
|
+
let from, below;
|
|
22461
|
+
for (const r of [...mine].reverse()) {
|
|
22462
|
+
if (below && after(r, below) && (!from || after(from, r))) from = r;
|
|
22463
|
+
if (!below || after(below, r)) below = r;
|
|
22464
|
+
}
|
|
22465
|
+
if (!from) continue;
|
|
22466
|
+
const newer = mine.filter((r) => !after(from, r)).sort((a, b) => after(a, b) ? 1 : after(b, a) ? -1 : 0);
|
|
22467
|
+
const ids = new Set(newer.map((r) => r.id));
|
|
22468
|
+
const first = rows.findIndex((r) => ids.has(r.id));
|
|
22469
|
+
let end = rows.length;
|
|
22470
|
+
for (let i = first + 1, top = rows[first]; i < rows.length; i++) {
|
|
22471
|
+
if (rows[i].sender !== sender) continue;
|
|
22472
|
+
if (after(top, rows[i])) {
|
|
22473
|
+
end = i;
|
|
22474
|
+
break;
|
|
22475
|
+
}
|
|
22476
|
+
top = rows[i];
|
|
22477
|
+
}
|
|
22478
|
+
const followers = rows.slice(first, end).filter((r) => r.sender !== sender);
|
|
22479
|
+
for (const r of followers) ids.add(r.id);
|
|
22480
|
+
const moved = [...newer, ...followers];
|
|
22481
|
+
this.rows.set(groupId, [...rows.filter((r) => !ids.has(r.id)), ...moved.map((r) => ({
|
|
22482
|
+
...r,
|
|
22483
|
+
at: Math.max(r.at, now)
|
|
22484
|
+
}))]);
|
|
22485
|
+
await this.host.placeAgain(this.linkOf(groupId), moved.map((r) => r.id)).catch(() => {});
|
|
22486
|
+
}
|
|
22487
|
+
}
|
|
22488
|
+
} finally {
|
|
22489
|
+
this.running.delete(groupId);
|
|
22490
|
+
}
|
|
22491
|
+
}
|
|
22492
|
+
};
|
|
22493
|
+
//#endregion
|
|
22494
|
+
//#region ../browser/src/engine/groupLineOrder.ts
|
|
22495
|
+
/**
|
|
22496
|
+
* The lines of a group's history ("… joined", "… is no longer a member") among the messages of their moment, for a
|
|
22497
|
+
* member back from away (WISP 902 § Catch-up). A message sealed under an epoch was written before its author knew of
|
|
22498
|
+
* the next commit, so the line of that commit goes below it; a message under the commit's epoch or a later one was
|
|
22499
|
+
* written knowing it, and stays below the line. A catch-up brings the commits first (a commit opens the messages of
|
|
22500
|
+
* its epoch) and, in a private group, the answers of several members race, so the messages of the epoch before can come
|
|
22501
|
+
* after the line. Then the line, and the rows after it written knowing it, are placed again below them, in their order;
|
|
22502
|
+
* no clock is read. A row only ever moves down, and only rows placed here in the last few minutes, so a live
|
|
22503
|
+
* conversation read long ago never moves. Moves made meanwhile go in one turn: a row moves about once a catch-up page.
|
|
22504
|
+
*/
|
|
22505
|
+
var GroupLineOrder = class {
|
|
22506
|
+
host;
|
|
22507
|
+
/** Per history: the rows followed, in the order they are placed in it. */
|
|
22508
|
+
rows = /* @__PURE__ */ new Map();
|
|
22509
|
+
due = /* @__PURE__ */ new Set();
|
|
22510
|
+
running = /* @__PURE__ */ new Set();
|
|
22511
|
+
constructor(host) {
|
|
22512
|
+
this.host = host;
|
|
22513
|
+
}
|
|
22514
|
+
/**
|
|
22515
|
+
* A row is being stored, a line of the history or a message sealed under this epoch, and takes its place now: `at` is
|
|
22516
|
+
* when, on the engine's clock. Call what this returns with whether it was new: a message below a line of a later
|
|
22517
|
+
* epoch takes the line (and the rows after it written knowing it) below it.
|
|
22518
|
+
*/
|
|
22519
|
+
placing(linkId, row, at) {
|
|
22520
|
+
if (!this.host.placeAgain) return () => {};
|
|
22521
|
+
const followed = {
|
|
22522
|
+
...row,
|
|
22523
|
+
at
|
|
22524
|
+
};
|
|
22525
|
+
const rows = (this.rows.get(linkId) ?? []).filter((r) => r.at > at - AUTHOR_ORDER_WINDOW_MS).slice(-575);
|
|
22526
|
+
rows.push(followed);
|
|
22527
|
+
this.rows.set(linkId, rows);
|
|
22528
|
+
return (stored) => {
|
|
22529
|
+
const now = this.rows.get(linkId);
|
|
22530
|
+
if (!stored) {
|
|
22531
|
+
if (now) this.rows.set(linkId, now.filter((r) => r !== followed));
|
|
22532
|
+
return;
|
|
22533
|
+
}
|
|
22534
|
+
if (!followed.line && now?.some((r) => r.line && r.epoch > followed.epoch && now.indexOf(r) < now.indexOf(followed))) this.run(linkId);
|
|
22535
|
+
};
|
|
22536
|
+
}
|
|
22537
|
+
/** These rows were placed again below everything (`GroupAuthorOrder`), in this order. */
|
|
22538
|
+
moved(linkId, ids) {
|
|
22539
|
+
const rows = this.rows.get(linkId);
|
|
22540
|
+
if (!rows) return;
|
|
22541
|
+
const moved = ids.map((id) => rows.find((r) => r.id === id)).filter((r) => !!r);
|
|
22542
|
+
if (!moved.length) return;
|
|
22543
|
+
this.rows.set(linkId, [...rows.filter((r) => !moved.includes(r)), ...moved]);
|
|
22544
|
+
if (moved.some((r) => !r.line)) this.run(linkId);
|
|
22545
|
+
}
|
|
22546
|
+
/** The group's history is gone (left or forgotten). */
|
|
22547
|
+
forget(linkId) {
|
|
22548
|
+
this.rows.delete(linkId);
|
|
22549
|
+
this.due.delete(linkId);
|
|
22550
|
+
}
|
|
22551
|
+
/** The history's rows put in order; what comes while a move is written goes in the next turn. */
|
|
22552
|
+
run(linkId) {
|
|
22553
|
+
this.due.add(linkId);
|
|
22554
|
+
if (this.running.has(linkId)) return;
|
|
22555
|
+
this.running.add(linkId);
|
|
22556
|
+
(async () => {
|
|
22557
|
+
try {
|
|
22558
|
+
while (this.due.delete(linkId)) {
|
|
22559
|
+
const rows = this.rows.get(linkId) ?? [];
|
|
22560
|
+
const order = [];
|
|
22561
|
+
for (const row of rows) {
|
|
22562
|
+
const above = row.line ? -1 : order.findIndex((r) => r.line && r.epoch > row.epoch);
|
|
22563
|
+
if (above < 0) order.push(row);
|
|
22564
|
+
else order.splice(above, 0, row);
|
|
22565
|
+
}
|
|
22566
|
+
let from = 0;
|
|
22567
|
+
while (from < rows.length && rows[from] === order[from]) from++;
|
|
22568
|
+
if (from === rows.length) continue;
|
|
22569
|
+
const last = rows[rows.length - 1].at;
|
|
22570
|
+
for (const r of order.slice(from)) r.at = Math.max(r.at, last);
|
|
22571
|
+
this.rows.set(linkId, order);
|
|
22572
|
+
await this.host.placeAgain(linkId, order.slice(from).map((r) => r.id)).catch(() => {});
|
|
22573
|
+
}
|
|
22574
|
+
} finally {
|
|
22575
|
+
this.running.delete(linkId);
|
|
22576
|
+
}
|
|
22577
|
+
})();
|
|
22578
|
+
}
|
|
22579
|
+
};
|
|
22580
|
+
//#endregion
|
|
22016
22581
|
//#region ../browser/src/engine/hubClocks.ts
|
|
22017
22582
|
/**
|
|
22018
22583
|
* A hub dates its beacon entry by its own clock, and a reader judges it by its own. A hub's time closer than this to
|
|
@@ -22266,11 +22831,14 @@ var Communities = class {
|
|
|
22266
22831
|
/** My frames an edge took, and which of them carry an edit (`frame id → editKey`). */
|
|
22267
22832
|
frames = new FramesTaken();
|
|
22268
22833
|
carriers = /* @__PURE__ */ new Map();
|
|
22834
|
+
/** The lines of each history among the messages of their moment (`GroupLineOrder`). */
|
|
22835
|
+
lineOrder;
|
|
22269
22836
|
constructor(host, store, timings = COMMUNITY_TIMINGS, random = Math.random) {
|
|
22270
22837
|
this.host = host;
|
|
22271
22838
|
this.store = store;
|
|
22272
22839
|
this.timings = timings;
|
|
22273
22840
|
this.random = random;
|
|
22841
|
+
this.lineOrder = new GroupLineOrder(host);
|
|
22274
22842
|
}
|
|
22275
22843
|
/** Application frames and pair payloads handed to the engine, one after the other per group. */
|
|
22276
22844
|
deliveries = /* @__PURE__ */ new Map();
|
|
@@ -22287,6 +22855,16 @@ var Communities = class {
|
|
|
22287
22855
|
}
|
|
22288
22856
|
/** The clock: the last tick's time (the engine ticks every second), so every decision reads one clock. */
|
|
22289
22857
|
time = 0;
|
|
22858
|
+
/**
|
|
22859
|
+
* The communities this app is in, and those it is a hub of, as of the last tick: their idle looks share one
|
|
22860
|
+
* community's pace (`beaconEvery`, `knockRecordsToRead`). Each community a hub held cost it about 10 requests a minute
|
|
22861
|
+
* on each relay with nobody coming in: 4 of them on one relay, or 8 on two, spent the relays' budget on those looks,
|
|
22862
|
+
* and the edges of some of them stayed down 10 minutes and more (CLI daemons, 2026-10-13).
|
|
22863
|
+
*/
|
|
22864
|
+
sharing = {
|
|
22865
|
+
groups: 1,
|
|
22866
|
+
hubs: 1
|
|
22867
|
+
};
|
|
22290
22868
|
now() {
|
|
22291
22869
|
return this.time || Date.now();
|
|
22292
22870
|
}
|
|
@@ -22359,6 +22937,7 @@ var Communities = class {
|
|
|
22359
22937
|
accepted: true,
|
|
22360
22938
|
viaLink: true,
|
|
22361
22939
|
stage: this.joinStage(group),
|
|
22940
|
+
since: group.joining.since,
|
|
22362
22941
|
...this.knockingWith.get(group.id) ? { waiting: this.knockingWith.get(group.id) } : {}
|
|
22363
22942
|
} : void 0;
|
|
22364
22943
|
if (invitation && !this.keptWhileKnocking(live)) return [{
|
|
@@ -22589,6 +23168,21 @@ var Communities = class {
|
|
|
22589
23168
|
await s.leave();
|
|
22590
23169
|
await this.forget(groupId);
|
|
22591
23170
|
}
|
|
23171
|
+
/**
|
|
23172
|
+
* Delete from this device: left first, as Leave does. With nothing connected to carry the request, a member's leave
|
|
23173
|
+
* goes unsaid and the group goes anyway, as a private group's failed goodbye does: the others still list the device,
|
|
23174
|
+
* but members who never open the app again kept it here for good. An admin with members is still refused: gone with
|
|
23175
|
+
* the role, the group would have no admin.
|
|
23176
|
+
*/
|
|
23177
|
+
async deleteHere(groupId) {
|
|
23178
|
+
try {
|
|
23179
|
+
await this.leave(groupId);
|
|
23180
|
+
} catch (error) {
|
|
23181
|
+
const s = this.live.get(groupId)?.session;
|
|
23182
|
+
if (!s || s.isAdmin && s.others.length) throw error;
|
|
23183
|
+
await this.forget(groupId);
|
|
23184
|
+
}
|
|
23185
|
+
}
|
|
22592
23186
|
successor(groupId) {
|
|
22593
23187
|
const s = this.live.get(groupId)?.session;
|
|
22594
23188
|
if (!s) return void 0;
|
|
@@ -22659,6 +23253,7 @@ var Communities = class {
|
|
|
22659
23253
|
this.lastMentionAt.delete(groupId);
|
|
22660
23254
|
this.knockingWith.delete(groupId);
|
|
22661
23255
|
this.knockRead.delete(groupId);
|
|
23256
|
+
this.lineOrder.forget(MESSAGE_LINK$1(groupId));
|
|
22662
23257
|
for (const linkId of [...this.host.edges(groupId).values(), ...this.host.entries(groupId).values()]) await this.host.closeEdge(linkId);
|
|
22663
23258
|
await this.store.deleteGroup(groupId);
|
|
22664
23259
|
await this.files?.drop(groupId);
|
|
@@ -22673,6 +23268,11 @@ var Communities = class {
|
|
|
22673
23268
|
/** Everything timed: beacon, hub election, lobbies, knocks, leaves, edges. The engine calls it every second or so. */
|
|
22674
23269
|
async tick(now = Date.now()) {
|
|
22675
23270
|
this.time = now;
|
|
23271
|
+
const active = [...this.live.values()].filter((l) => l.session.status === "active");
|
|
23272
|
+
this.sharing = {
|
|
23273
|
+
groups: Math.max(1, active.length),
|
|
23274
|
+
hubs: Math.max(1, active.filter((l) => l.hub).length)
|
|
23275
|
+
};
|
|
22676
23276
|
for (const group of [...this.stored.values()]) {
|
|
22677
23277
|
const live = this.live.get(group.id);
|
|
22678
23278
|
if (group.joining && (this.knocking(group, live) || group.joining.check)) {
|
|
@@ -22719,7 +23319,7 @@ var Communities = class {
|
|
|
22719
23319
|
if ([...this.host.entries(groupId).values()].includes(linkId)) await this.host.closeEdge(linkId);
|
|
22720
23320
|
}
|
|
22721
23321
|
const alone = live.hub && !freshHubs(this.hubs(live), now).some((h) => h.key !== me);
|
|
22722
|
-
if ((live.beaconFailedAt ? now - live.beaconFailedAt >= BEACON_RETRY_MS : now - live.lastBeaconRead >= (
|
|
23322
|
+
if ((live.beaconFailedAt ? now - live.beaconFailedAt >= BEACON_RETRY_MS : now - live.lastBeaconRead >= this.beaconEvery(groupId, live, alone)) || live.lastBeaconRead === 0) await this.readBeacon(groupId, live, now);
|
|
22723
23323
|
if (!live.beaconKnown) {
|
|
22724
23324
|
await this.keepLooking(groupId, live, now);
|
|
22725
23325
|
await this.reconcile(groupId, live, now);
|
|
@@ -22981,11 +23581,22 @@ var Communities = class {
|
|
|
22981
23581
|
const n = live.members.size;
|
|
22982
23582
|
return n >= this.capacity(groupId, live, now, false) ? Math.max(n, COMMUNITY_TOPOLOGY.hubCapacity) : n;
|
|
22983
23583
|
}
|
|
23584
|
+
/**
|
|
23585
|
+
* How often the beacon is read: alone, as often as I write my entry. A member with an edge up shares one community's
|
|
23586
|
+
* pace with the others it is in (`sharing`), up to that of a lone hub: an entry stays fresh three such readings. A
|
|
23587
|
+
* hub does not: the door is the hubs whose entries it read lately (`doorHubs`, an entry 45 s old at most), and read
|
|
23588
|
+
* slower, the door kept changing and read every knock record again each time. Nor a member with no edge up: who the
|
|
23589
|
+
* hubs are is what it needs to connect.
|
|
23590
|
+
*/
|
|
23591
|
+
beaconEvery(groupId, live, alone) {
|
|
23592
|
+
if (alone) return COMMUNITY_TOPOLOGY.beaconEveryMs;
|
|
23593
|
+
return !live.hub && [...this.host.edges(groupId).values()].some((id) => this.host.linkReady(id, 2)) ? Math.min(this.timings.beaconReadMs * this.sharing.groups, COMMUNITY_TOPOLOGY.beaconEveryMs) : this.timings.beaconReadMs;
|
|
23594
|
+
}
|
|
22984
23595
|
/** Reads the beacon; false when the reading did not happen (the relays held it back, or none answered). */
|
|
22985
23596
|
async readBeacon(groupId, live, now) {
|
|
22986
23597
|
live.lastBeaconRead = now;
|
|
22987
23598
|
const keys = beaconKeys(live.session.state.rv, groupId);
|
|
22988
|
-
const records = await this.host.resolve(keys.identity.pubKeyZ32, true, live
|
|
23599
|
+
const records = await this.host.resolve(keys.identity.pubKeyZ32, true, this.doorBusy(groupId, live, now)).catch(() => void 0);
|
|
22989
23600
|
if (records === void 0) {
|
|
22990
23601
|
live.beaconFailedAt = now;
|
|
22991
23602
|
live.beaconAskedAt ||= now;
|
|
@@ -23193,7 +23804,7 @@ var Communities = class {
|
|
|
23193
23804
|
}
|
|
23194
23805
|
async pollLobby(groupId, live, now) {
|
|
23195
23806
|
const keys = lobbyKeys(live.session.state.rv, groupId, live.session.myKey);
|
|
23196
|
-
const entries = readLobby(keys, await this.host.resolve(keys.identity.pubKeyZ32, true,
|
|
23807
|
+
const entries = readLobby(keys, await this.host.resolve(keys.identity.pubKeyZ32, true, this.doorBusy(groupId, live, now)).catch(() => null) ?? []);
|
|
23197
23808
|
for (const { key, ts } of entries) {
|
|
23198
23809
|
if (now - ts > COMMUNITY_TOPOLOGY.lobbyFreshMs || key === live.session.myKey) continue;
|
|
23199
23810
|
if (live.session.wasRemoved(key)) {
|
|
@@ -23283,6 +23894,17 @@ var Communities = class {
|
|
|
23283
23894
|
return now - live.admittedAt < ADMITTED_QUIET_MS || [...live.pendingEntries.keys()].some((key) => down(entries.get(key))) || [...live.expect].some((key) => down(edges.get(key)));
|
|
23284
23895
|
}
|
|
23285
23896
|
/**
|
|
23897
|
+
* As a hub: someone is coming in, or about to. I am letting someone in, the link was just shown, a crowd fills the
|
|
23898
|
+
* bell, or members ask in my lobby (a hub just left, say). Then my reads of the bell, the beacon and my lobby go as
|
|
23899
|
+
* the door's (`PkarrRequestOptions.door`): not held back while links signal. Otherwise they give way to the links
|
|
23900
|
+
* that signal, my edges in my other communities first: an app that was a hub of 4 communities on one relay (8 on
|
|
23901
|
+
* two) spent its relays' background share on reads that each said nobody was there, and its edges, whose signaling
|
|
23902
|
+
* had what was left, stayed down 10 minutes and more in some of them (CLI daemons, 2026-10-13).
|
|
23903
|
+
*/
|
|
23904
|
+
doorBusy(groupId, live, now) {
|
|
23905
|
+
return live.hub && (now < live.warmUntil || now < live.crowdUntil || now < live.lobbyBusyUntil || this.admitting(groupId, live, now));
|
|
23906
|
+
}
|
|
23907
|
+
/**
|
|
23286
23908
|
* Which knock records to read now (none, most of the time): every one at once the first time
|
|
23287
23909
|
* (a hub just elected, an app back, a change of door), since a knock left while nobody was at the
|
|
23288
23910
|
* door may be in any; then the door the bell, and the others in turn; the other hubs one in turn, slowly.
|
|
@@ -23295,7 +23917,7 @@ var Communities = class {
|
|
|
23295
23917
|
return Array.from({ length: 4 }, (_, n) => n);
|
|
23296
23918
|
}
|
|
23297
23919
|
const crowd = now < live.crowdUntil;
|
|
23298
|
-
const every = !door ? this.timings.otherHubKnockPollMs : busy && !crowd ? this.timings.knockBusyPollMs : now < live.warmUntil ? this.timings.knockWarmPollMs : this.timings.knockPollMs;
|
|
23920
|
+
const every = !door ? this.timings.otherHubKnockPollMs : busy && !crowd ? this.timings.knockBusyPollMs : now < live.warmUntil ? this.timings.knockWarmPollMs : Math.min(this.timings.knockPollMs * this.sharing.hubs, this.timings.otherHubKnockPollMs);
|
|
23299
23921
|
if (now - live.lastKnockPoll < every) return [];
|
|
23300
23922
|
live.lastKnockPoll = now;
|
|
23301
23923
|
if (!door) return [live.knockCursor = (live.knockCursor + 1) % 4];
|
|
@@ -23321,9 +23943,10 @@ var Communities = class {
|
|
|
23321
23943
|
const records = this.knockRecordsToRead(live, door === s.myKey, busy, now);
|
|
23322
23944
|
if (!records.length) return;
|
|
23323
23945
|
let failed = 0;
|
|
23946
|
+
const open = door === s.myKey && this.doorBusy(groupId, live, now);
|
|
23324
23947
|
const read = await Promise.all(records.map(async (n) => {
|
|
23325
23948
|
const record = knockRecord(link, n);
|
|
23326
|
-
return readKnocks(record, await this.host.resolve(knockIdentity(record).pubKeyZ32, true,
|
|
23949
|
+
return readKnocks(record, await this.host.resolve(knockIdentity(record).pubKeyZ32, true, open && n === KNOCK_BELL).catch(() => {
|
|
23327
23950
|
failed++;
|
|
23328
23951
|
return null;
|
|
23329
23952
|
}) ?? []);
|
|
@@ -23717,6 +24340,11 @@ var Communities = class {
|
|
|
23717
24340
|
sender: m.sender,
|
|
23718
24341
|
file: m.file
|
|
23719
24342
|
}, session.myKey) : void 0;
|
|
24343
|
+
const placing = this.lineOrder.placing(MESSAGE_LINK$1(id), {
|
|
24344
|
+
id: m.id,
|
|
24345
|
+
epoch: m.epoch,
|
|
24346
|
+
line: false
|
|
24347
|
+
}, this.now());
|
|
23720
24348
|
const stored = await this.host.storeMessage({
|
|
23721
24349
|
linkId: MESSAGE_LINK$1(id),
|
|
23722
24350
|
id: m.id,
|
|
@@ -23731,6 +24359,7 @@ var Communities = class {
|
|
|
23731
24359
|
...m.card && { card: m.card },
|
|
23732
24360
|
...file && { file }
|
|
23733
24361
|
});
|
|
24362
|
+
placing(stored !== false);
|
|
23734
24363
|
const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, arrivalNow(this.now()));
|
|
23735
24364
|
this.lastMessageAt.set(id, Math.max(this.lastMessageAt.get(id) ?? 0, came));
|
|
23736
24365
|
if (mentioned) this.lastMentionAt.set(id, Math.max(this.lastMentionAt.get(id) ?? 0, came));
|
|
@@ -23827,7 +24456,16 @@ var Communities = class {
|
|
|
23827
24456
|
else if (s.status === "active" && before !== after) {
|
|
23828
24457
|
const at = receivedTimestamp(top.ts, when);
|
|
23829
24458
|
for (const [key] of after) if (!rosterHas(before, key) && key !== s.myKey) await this.event(groupId, "joined", `${name(key)} joined`, at, top.e, key);
|
|
23830
|
-
|
|
24459
|
+
const left = (key) => {
|
|
24460
|
+
const chain = s.state.chain;
|
|
24461
|
+
for (let i = chain.length - 1; i >= 0; i--) if ((chain[i].k === "remove" || chain[i].k === "leave") && chain[i].s === key) return chain[i].k === "leave";
|
|
24462
|
+
return false;
|
|
24463
|
+
};
|
|
24464
|
+
for (const [key] of before) {
|
|
24465
|
+
if (rosterHas(after, key)) continue;
|
|
24466
|
+
if (left(key)) await this.event(groupId, "left", `${name(key)} left`, at, top.e, key);
|
|
24467
|
+
else await this.event(groupId, "gone", `${name(key)} is no longer a member`, at, top.e, key);
|
|
24468
|
+
}
|
|
23831
24469
|
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);
|
|
23832
24470
|
if (top.k === "rotate") await this.event(groupId, "rotated", "Keys rotated: a fresh epoch", at, top.e);
|
|
23833
24471
|
}
|
|
@@ -23836,16 +24474,21 @@ var Communities = class {
|
|
|
23836
24474
|
/** A line in the group's history. Its id carries its time: two lines at the same moment get distinct ones (`eventTime`). */
|
|
23837
24475
|
async event(groupId, event, text, timestamp, epoch, member) {
|
|
23838
24476
|
timestamp = eventTime(this.eventTimes, groupId, timestamp);
|
|
23839
|
-
|
|
24477
|
+
const id = `event:${epoch}:${event}:${member ?? ""}:${timestamp}`;
|
|
24478
|
+
this.lineOrder.placing(MESSAGE_LINK$1(groupId), {
|
|
24479
|
+
id,
|
|
24480
|
+
epoch,
|
|
24481
|
+
line: true
|
|
24482
|
+
}, this.now())(await this.host.storeMessage({
|
|
23840
24483
|
linkId: MESSAGE_LINK$1(groupId),
|
|
23841
|
-
id
|
|
24484
|
+
id,
|
|
23842
24485
|
text,
|
|
23843
24486
|
sender: "peer",
|
|
23844
24487
|
event,
|
|
23845
24488
|
member,
|
|
23846
24489
|
timestamp,
|
|
23847
24490
|
via: "datalink"
|
|
23848
|
-
});
|
|
24491
|
+
}) !== false);
|
|
23849
24492
|
}
|
|
23850
24493
|
eventTimes = /* @__PURE__ */ new Map();
|
|
23851
24494
|
};
|
|
@@ -24853,6 +25496,11 @@ var INVITED_KEPT_MS = LEFT_KEPT_MS;
|
|
|
24853
25496
|
*/
|
|
24854
25497
|
var ACCEPT_AGAIN_MS = 6e4;
|
|
24855
25498
|
var ACCEPT_AGAINS = 5;
|
|
25499
|
+
/**
|
|
25500
|
+
* Admin side: a member let in over its chat, its edge not up, is sent its welcome again at most this often. The welcome
|
|
25501
|
+
* is the whole chain and the sealed secrets (megabytes, tens of ms to build); its app says its accept once a minute.
|
|
25502
|
+
*/
|
|
25503
|
+
var REWELCOME_MS = ACCEPT_AGAIN_MS / 2;
|
|
24856
25504
|
/** Invitee side: the invitations one contact chat may have waiting here at a time. More are answered as declined. */
|
|
24857
25505
|
var MAX_INVITATIONS_PER_CHAT = 4;
|
|
24858
25506
|
/** What an invitation says of its group, kept as a claim within what a group can be: an epoch from 0, a roster of 1 to the most a group holds. */
|
|
@@ -24869,9 +25517,13 @@ var FAREWELL_ANSWERS = 3;
|
|
|
24869
25517
|
var FAREWELL_COMMITS = 64;
|
|
24870
25518
|
/**
|
|
24871
25519
|
* A member whose messages were just asked of another (`Groups.askOf`) is told where I am only once that one's answer
|
|
24872
|
-
* has come: no message
|
|
25520
|
+
* has come: no message of it for this long, or `MESH_SYNC_HOLD_MAX_MS` after it was asked. Told at once, its own
|
|
24873
25521
|
* answer raced the other's, which holds the same messages among everyone's in the order they were written, and a
|
|
24874
25522
|
* member back in a group of five read each member's lines apart, the newest above older ones (WISP 902 § Catch-up).
|
|
25523
|
+
* A message of the answer is one written before the asking: what the member writes meanwhile is no reason to wait, so
|
|
25524
|
+
* the longest wait only has to cover a whole answer (the 256 messages a member keeps for the others and its own last
|
|
25525
|
+
* 32) at the pace of a phone with a long history, 600 ms a frame. At ten seconds, an answer of 200 messages at four a
|
|
25526
|
+
* second was cut at the 40th, and the others' newest lines landed above what was still coming.
|
|
24875
25527
|
*/
|
|
24876
25528
|
var MESH_SYNC_HOLD_MS = 1500;
|
|
24877
25529
|
/** Where a member's gossip turns start among the members it is connected to: a number of its own, from its key. */
|
|
@@ -24985,6 +25637,8 @@ var Groups = class {
|
|
|
24985
25637
|
syncHeld = /* @__PURE__ */ new Map();
|
|
24986
25638
|
/** Per mesh group and member, when a message last came over its edge (its own, or one it handed on). */
|
|
24987
25639
|
lastHanded = /* @__PURE__ */ new Map();
|
|
25640
|
+
/** Admin side, per group and member let in: when its welcome was last sent again, over its chat or the link (`REWELCOME_MS`). */
|
|
25641
|
+
rewelcomed = /* @__PURE__ */ new Map();
|
|
24988
25642
|
lastGossip = /* @__PURE__ */ new Map();
|
|
24989
25643
|
/**
|
|
24990
25644
|
* Per mesh group, each reachable member some of whose messages never reached me (`GroupSession.lacking`): what was
|
|
@@ -25016,6 +25670,10 @@ var Groups = class {
|
|
|
25016
25670
|
communities;
|
|
25017
25671
|
/** My messages and edits an edge of a private group took. */
|
|
25018
25672
|
frames = new FramesTaken();
|
|
25673
|
+
/** Each member's messages in the order that member wrote them, though catch-up answers race (WISP 400, requirement 10). */
|
|
25674
|
+
authorOrder;
|
|
25675
|
+
/** The lines of each history among the messages of their moment (`GroupLineOrder`). */
|
|
25676
|
+
lineOrder;
|
|
25019
25677
|
/** Hubs of the private groups past 16 members (WISP 902 · Group Mesh § Hubs). */
|
|
25020
25678
|
hubs;
|
|
25021
25679
|
/** Groups whose edges are being reconciled because their hubs changed: one at a time. */
|
|
@@ -25042,6 +25700,16 @@ var Groups = class {
|
|
|
25042
25700
|
}
|
|
25043
25701
|
}, hubTimings);
|
|
25044
25702
|
this.typings = new GroupTypings(host);
|
|
25703
|
+
this.lineOrder = new GroupLineOrder(host);
|
|
25704
|
+
const placeAgain = host.placeAgain && (async (linkId, ids) => {
|
|
25705
|
+
const done = host.placeAgain(linkId, ids);
|
|
25706
|
+
this.lineOrder.moved(linkId, ids);
|
|
25707
|
+
await done;
|
|
25708
|
+
});
|
|
25709
|
+
this.authorOrder = new GroupAuthorOrder({ placeAgain }, MESSAGE_LINK, (groupId, sender) => {
|
|
25710
|
+
const session = this.sessions.get(groupId);
|
|
25711
|
+
return !!session && session.missing(sender, this.gapsGone.get(groupId)?.get(sender)?.[session.epoch]) > 0;
|
|
25712
|
+
});
|
|
25045
25713
|
this.signals = new MeshSignals(host, {
|
|
25046
25714
|
session: (id) => this.sessions.get(id),
|
|
25047
25715
|
stored: (id) => this.stored.get(id),
|
|
@@ -25079,13 +25747,14 @@ var Groups = class {
|
|
|
25079
25747
|
await this.communities.load(all.filter((g) => g.community || g.joining));
|
|
25080
25748
|
for (const group of all.filter((g) => !g.community && !g.joining)) {
|
|
25081
25749
|
this.stored.set(group.id, group);
|
|
25082
|
-
if (group.state) this.attach(group.state);
|
|
25083
|
-
if (group.invited) this.invited.set(group.id, new Set(Object.keys(group.invited)));
|
|
25084
25750
|
const history = await this.store.getMessages(MESSAGE_LINK(group.id)), said = history.filter((m) => !m.event && !m.groupPay);
|
|
25085
|
-
|
|
25751
|
+
const framed = history.filter((m) => m.frame);
|
|
25752
|
+
this.framesKept.set(group.id, framed.map((m) => ({
|
|
25086
25753
|
id: m.id,
|
|
25087
25754
|
bytes: frameBytes(m.frame)
|
|
25088
25755
|
})));
|
|
25756
|
+
if (group.state) this.attach(group.state, framed.map((m) => m.frame));
|
|
25757
|
+
if (group.invited) this.invited.set(group.id, new Set(Object.keys(group.invited)));
|
|
25089
25758
|
const last = said[said.length - 1], lastPeer = [...said].reverse().find((m) => m.sender !== "me");
|
|
25090
25759
|
if (last) this.lastMessageAt.set(group.id, last.timestamp);
|
|
25091
25760
|
const peerAt = cameOrWritten(lastPeer?.timestamp, lastPeer ? group.came?.peer : void 0);
|
|
@@ -25162,6 +25831,7 @@ var Groups = class {
|
|
|
25162
25831
|
members: Math.min(Math.max(invitation.n, 0), GROUP_MEMBER_CAP.max),
|
|
25163
25832
|
accepted: !!invitation.seedB64,
|
|
25164
25833
|
...this.acceptSaid.get(group.id)?.unanswered ? { unanswered: true } : {},
|
|
25834
|
+
...invitation.takenBack ? { takenBack: true } : {},
|
|
25165
25835
|
...invitation.entry ? {
|
|
25166
25836
|
viaLink: true,
|
|
25167
25837
|
stage: this.joinStage(group)
|
|
@@ -25343,6 +26013,7 @@ var Groups = class {
|
|
|
25343
26013
|
async accept(groupId) {
|
|
25344
26014
|
const group = this.stored.get(groupId);
|
|
25345
26015
|
if (!group?.invitation) throw new Error("No invitation to accept");
|
|
26016
|
+
if (group.invitation.takenBack) throw new Error("This invitation was taken back");
|
|
25346
26017
|
if (!this.host.linkReady(group.invitation.linkId)) throw new Error("The contact who invited you is not connected. Try again when they are.");
|
|
25347
26018
|
const seedB64 = group.invitation.seedB64 ?? createIdentity().seedB64;
|
|
25348
26019
|
group.invitation = {
|
|
@@ -25366,6 +26037,7 @@ var Groups = class {
|
|
|
25366
26037
|
if (this.isCommunity(groupId)) return this.communities.decline(groupId);
|
|
25367
26038
|
const group = this.stored.get(groupId);
|
|
25368
26039
|
if (!group?.invitation) throw new Error("No invitation to decline");
|
|
26040
|
+
if (group.invitation.takenBack) return this.forget(groupId, false);
|
|
25369
26041
|
if (!group.previous) return this.forget(groupId);
|
|
25370
26042
|
await this.dropInvitation(group, this.sayDecline(groupId, group.invitation.linkId));
|
|
25371
26043
|
}
|
|
@@ -25381,6 +26053,25 @@ var Groups = class {
|
|
|
25381
26053
|
await this.store.putGroup(previous);
|
|
25382
26054
|
this.host.emit();
|
|
25383
26055
|
}
|
|
26056
|
+
/**
|
|
26057
|
+
* Invitee side: who sent the invitation took it back. A group I kept under its id comes back as it was; otherwise the
|
|
26058
|
+
* invitation stays a week, marked, so the list says it was taken back where it was (an accept said for it stops).
|
|
26059
|
+
*/
|
|
26060
|
+
async takeBackFrom(group) {
|
|
26061
|
+
if (group.previous) return this.dropInvitation(group);
|
|
26062
|
+
if (group.invitation.takenBack) return;
|
|
26063
|
+
const { seedB64: _unused, ...invitation } = group.invitation;
|
|
26064
|
+
group.invitation = {
|
|
26065
|
+
...invitation,
|
|
26066
|
+
pieces: [],
|
|
26067
|
+
takenBack: this.now()
|
|
26068
|
+
};
|
|
26069
|
+
this.acceptSaid.delete(group.id);
|
|
26070
|
+
await this.store.putGroup(group);
|
|
26071
|
+
const kept = [...this.stored.values()].filter((x) => x.invitation?.takenBack && !x.state && x.invitation.linkId === invitation.linkId).reverse().sort((a, b) => b.invitation.takenBack - a.invitation.takenBack);
|
|
26072
|
+
for (const old of kept.slice(MAX_INVITATIONS_PER_CHAT)) await this.forget(old.id, false);
|
|
26073
|
+
this.host.emit();
|
|
26074
|
+
}
|
|
25384
26075
|
/** Invitee side: says no to an invitation. With the inviter's chat down (its app is closed, say), returns the answer owed, said when that chat is next up. */
|
|
25385
26076
|
sayDecline(groupId, linkId) {
|
|
25386
26077
|
try {
|
|
@@ -25573,6 +26264,8 @@ var Groups = class {
|
|
|
25573
26264
|
this.lastMessageAt.delete(groupId);
|
|
25574
26265
|
this.lastPeerMessageAt.delete(groupId);
|
|
25575
26266
|
this.lastMentionAt.delete(groupId);
|
|
26267
|
+
this.authorOrder.forget(groupId);
|
|
26268
|
+
this.lineOrder.forget(MESSAGE_LINK(groupId));
|
|
25576
26269
|
await this.store.deleteGroup(groupId);
|
|
25577
26270
|
await this.store.putGroup(group);
|
|
25578
26271
|
await this.files?.drop(groupId);
|
|
@@ -25766,19 +26459,20 @@ var Groups = class {
|
|
|
25766
26459
|
}
|
|
25767
26460
|
/** `answer`: false when an invitation goes because its inviter took it back, so no decline is said for it. */
|
|
25768
26461
|
async forget(groupId, answer = true) {
|
|
25769
|
-
if (this.isCommunity(groupId)) return this.communities.
|
|
26462
|
+
if (this.isCommunity(groupId)) return this.communities.deleteHere(groupId);
|
|
25770
26463
|
const session = this.sessions.get(groupId);
|
|
25771
26464
|
if (session?.status === "active") try {
|
|
25772
26465
|
await this.leave(groupId);
|
|
25773
26466
|
} catch (e) {
|
|
25774
26467
|
if (session.isAdmin && session.others.length) throw e;
|
|
25775
26468
|
}
|
|
25776
|
-
const gone = this.stored.get(groupId), invitation = answer && gone?.invitation && !gone.invitation.entry ? gone.invitation : void 0;
|
|
26469
|
+
const gone = this.stored.get(groupId), invitation = answer && gone?.invitation && !gone.invitation.entry && !gone.invitation.takenBack ? gone.invitation : void 0;
|
|
25777
26470
|
const owed = invitation && this.sayDecline(groupId, invitation.linkId);
|
|
25778
26471
|
this.sessions.delete(groupId);
|
|
25779
26472
|
this.stored.delete(groupId);
|
|
25780
26473
|
this.takeBack(groupId);
|
|
25781
26474
|
this.acceptSaid.delete(groupId);
|
|
26475
|
+
this.entryHosts.delete(groupId);
|
|
25782
26476
|
this.lastRoster.delete(groupId);
|
|
25783
26477
|
this.lastTold.delete(groupId);
|
|
25784
26478
|
this.pendingEntries.delete(groupId);
|
|
@@ -25792,6 +26486,7 @@ var Groups = class {
|
|
|
25792
26486
|
this.relayAsked.delete(groupId);
|
|
25793
26487
|
this.syncHeld.delete(groupId);
|
|
25794
26488
|
this.lastHanded.delete(groupId);
|
|
26489
|
+
this.rewelcomed.delete(groupId);
|
|
25795
26490
|
this.hubs.forget(groupId);
|
|
25796
26491
|
this.removedAt.delete(groupId);
|
|
25797
26492
|
this.lastGossip.delete(groupId);
|
|
@@ -25804,6 +26499,8 @@ var Groups = class {
|
|
|
25804
26499
|
this.hereHeard.delete(groupId);
|
|
25805
26500
|
this.hereActed.delete(groupId);
|
|
25806
26501
|
this.signals.forget(groupId);
|
|
26502
|
+
this.authorOrder.forget(groupId);
|
|
26503
|
+
this.lineOrder.forget(MESSAGE_LINK(groupId));
|
|
25807
26504
|
for (const linkId of [...this.host.edges(groupId).values(), ...this.host.entries(groupId).values()]) await this.host.closeEdge(linkId);
|
|
25808
26505
|
await this.store.deleteGroup(groupId);
|
|
25809
26506
|
if (owed && !this.stored.has(groupId)) {
|
|
@@ -25902,6 +26599,7 @@ var Groups = class {
|
|
|
25902
26599
|
if (this.ticking) return;
|
|
25903
26600
|
this.ticking = true;
|
|
25904
26601
|
try {
|
|
26602
|
+
this.authorOrder.tick(now);
|
|
25905
26603
|
for (const group of [...this.stored.values()]) {
|
|
25906
26604
|
if (group.left) {
|
|
25907
26605
|
if (now - group.left.at > LEFT_KEPT_MS) await this.forget(group.id);
|
|
@@ -25911,6 +26609,10 @@ var Groups = class {
|
|
|
25911
26609
|
await this.declineOwed(group, now);
|
|
25912
26610
|
if (!group.state) continue;
|
|
25913
26611
|
}
|
|
26612
|
+
if (group.invitation?.takenBack && !group.state) {
|
|
26613
|
+
if (now - group.invitation.takenBack > LEFT_KEPT_MS) await this.forget(group.id, false);
|
|
26614
|
+
continue;
|
|
26615
|
+
}
|
|
25914
26616
|
if (group.invitation?.entry && !group.state) {
|
|
25915
26617
|
const every = now - group.createdAt > this.timings.patienceMs ? this.timings.slowKnockMs : this.timings.knockMs;
|
|
25916
26618
|
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(() => {});
|
|
@@ -26005,6 +26707,7 @@ var Groups = class {
|
|
|
26005
26707
|
this.host.emit();
|
|
26006
26708
|
}
|
|
26007
26709
|
if (said.n >= ACCEPT_AGAINS || !said.down && now - said.at < ACCEPT_AGAIN_MS) return;
|
|
26710
|
+
invitation.pieces = [];
|
|
26008
26711
|
try {
|
|
26009
26712
|
this.host.sendOnLink(invitation.linkId, {
|
|
26010
26713
|
t: "group-accept",
|
|
@@ -26028,7 +26731,7 @@ var Groups = class {
|
|
|
26028
26731
|
}
|
|
26029
26732
|
const session = this.sessions.get(groupId), group = this.stored.get(groupId);
|
|
26030
26733
|
if (!session?.isAdmin || !group || !this.pendingEntries.get(groupId)?.has(peer)) return;
|
|
26031
|
-
if (this.growthBlocked(group, session, session.roster.length + 1, linkId)) {
|
|
26734
|
+
if (!rosterHas(session.roster, peer) && this.growthBlocked(group, session, session.roster.length + 1, linkId)) {
|
|
26032
26735
|
this.pendingEntries.get(groupId)?.delete(peer);
|
|
26033
26736
|
this.refused.set(peer, Date.now() + REFUSED_FOR_MS);
|
|
26034
26737
|
this.host.closeEdge(linkId);
|
|
@@ -26075,14 +26778,22 @@ var Groups = class {
|
|
|
26075
26778
|
return this.knocked.has(group.id) ? "knocked" : "knocking";
|
|
26076
26779
|
}
|
|
26077
26780
|
entryOf(group) {
|
|
26078
|
-
|
|
26781
|
+
if (!group.entry) return void 0;
|
|
26782
|
+
const { seedB64 } = group.entry;
|
|
26783
|
+
let host = this.entryHosts.get(group.id);
|
|
26784
|
+
if (host?.seedB64 !== seedB64) this.entryHosts.set(group.id, host = {
|
|
26785
|
+
seedB64,
|
|
26786
|
+
key: identityFromSeedB64(seedB64).pubKeyZ32
|
|
26787
|
+
});
|
|
26788
|
+
return {
|
|
26079
26789
|
link: {
|
|
26080
26790
|
g: group.id,
|
|
26081
|
-
host:
|
|
26791
|
+
host: host.key
|
|
26082
26792
|
},
|
|
26083
|
-
seedB64
|
|
26084
|
-
}
|
|
26793
|
+
seedB64
|
|
26794
|
+
};
|
|
26085
26795
|
}
|
|
26796
|
+
entryHosts = /* @__PURE__ */ new Map();
|
|
26086
26797
|
/** Joiner side: the entry session kept after the welcome goes (the edge to the admin is up, or it was kept long enough). */
|
|
26087
26798
|
async endLinger(groupId) {
|
|
26088
26799
|
const kept = this.lingering.get(groupId);
|
|
@@ -26147,11 +26858,13 @@ var Groups = class {
|
|
|
26147
26858
|
});
|
|
26148
26859
|
let pending = this.pendingEntries.get(group.id);
|
|
26149
26860
|
for (const { key, ts } of knocks) {
|
|
26150
|
-
|
|
26861
|
+
const again = this.unwelcomed(group, session, key);
|
|
26862
|
+
if (now - ts > 18e4 || rosterHas(session.roster, key) && !again || this.refused.has(key) || pending?.has(key) || key === entry.link.host) continue;
|
|
26863
|
+
if (again && this.rewelcomedLately(group.id, key)) continue;
|
|
26151
26864
|
const entryIds = new Set(this.host.entries(group.id).values());
|
|
26152
26865
|
const contactsInvited = [...this.invited.get(group.id) ?? []].filter((id) => !entryIds.has(id)).length;
|
|
26153
26866
|
const size = session.roster.length + contactsInvited + (pending?.size ?? 0);
|
|
26154
|
-
if ((pending?.size ?? 0) >= MAX_PENDING_ENTRIES || size >= GROUP_MEMBER_CAP.max || this.growthBlocked(group, session, size + 1)) break;
|
|
26867
|
+
if ((pending?.size ?? 0) >= MAX_PENDING_ENTRIES || !again && (size >= GROUP_MEMBER_CAP.max || this.growthBlocked(group, session, size + 1))) break;
|
|
26155
26868
|
if (!pending) this.pendingEntries.set(group.id, pending = /* @__PURE__ */ new Map());
|
|
26156
26869
|
pending.set(key, now);
|
|
26157
26870
|
this.warmUntil.set(group.id, now + this.timings.warmMs);
|
|
@@ -26211,9 +26924,9 @@ var Groups = class {
|
|
|
26211
26924
|
}
|
|
26212
26925
|
if (existing?.invitation?.seedB64 && (existing.invitation.linkId !== linkId || existing.invitation.admin !== frame.admin)) return;
|
|
26213
26926
|
if (existing?.left && (existing.left.admin !== frame.admin || existing.contacts?.[existing.left.admin] !== linkId)) return;
|
|
26214
|
-
if (existing?.invitation && !existing.state && existing.invitation.linkId !== linkId && !existing.invitation.genesis) return;
|
|
26215
|
-
if ([...this.stored.values()].filter((x) => x.invitation).length >= 32) return;
|
|
26216
|
-
if (!existing?.invitation && [...this.stored.values()].filter((x) => x.invitation?.linkId === linkId).length >= MAX_INVITATIONS_PER_CHAT) {
|
|
26927
|
+
if (existing?.invitation && !existing.state && existing.invitation.linkId !== linkId && !existing.invitation.genesis && !existing.invitation.takenBack) return;
|
|
26928
|
+
if ([...this.stored.values()].filter((x) => x.invitation && !x.invitation.takenBack).length >= 32) return;
|
|
26929
|
+
if (!existing?.invitation && [...this.stored.values()].filter((x) => x.invitation?.linkId === linkId && !x.invitation.takenBack).length >= MAX_INVITATIONS_PER_CHAT) {
|
|
26217
26930
|
try {
|
|
26218
26931
|
this.host.sendOnLink(linkId, {
|
|
26219
26932
|
t: "group-decline",
|
|
@@ -26256,27 +26969,54 @@ var Groups = class {
|
|
|
26256
26969
|
const session = this.sessions.get(g);
|
|
26257
26970
|
if (typeof frame.key !== "string" || !MEMBER_KEY.test(frame.key)) return;
|
|
26258
26971
|
if (!session?.isAdmin || !this.invited.get(g)?.has(linkId)) {
|
|
26259
|
-
|
|
26972
|
+
const letIn = !!session && rosterHas(session.roster, frame.key) && this.stored.get(g)?.contacts?.[frame.key] === linkId;
|
|
26973
|
+
if (!letIn && ![...this.host.entries(g).values()].includes(linkId)) try {
|
|
26260
26974
|
this.host.sendOnLink(linkId, {
|
|
26261
26975
|
t: "group-decline",
|
|
26262
26976
|
g
|
|
26263
26977
|
});
|
|
26264
26978
|
} catch {}
|
|
26979
|
+
const edge = this.host.edges(g).get(frame.key);
|
|
26980
|
+
if (letIn && session.isAdmin && !(edge && this.host.linkReady(edge))) {
|
|
26981
|
+
if (this.rewelcomedLately(g, frame.key)) return;
|
|
26982
|
+
this.noteRewelcome(g, frame.key);
|
|
26983
|
+
const welcome = await session.rewelcome(frame.key).catch(() => []);
|
|
26984
|
+
try {
|
|
26985
|
+
for (const piece of welcome) this.host.sendOnLink(linkId, piece);
|
|
26986
|
+
} catch {}
|
|
26987
|
+
}
|
|
26265
26988
|
return;
|
|
26266
26989
|
}
|
|
26267
26990
|
const group = this.stored.get(g);
|
|
26268
|
-
if (frame.key === session.myKey
|
|
26991
|
+
if (frame.key === session.myKey) return;
|
|
26269
26992
|
const entryPeer = this.hostEntry(g, linkId);
|
|
26270
26993
|
if ([...this.host.entries(g).values()].includes(linkId) && entryPeer !== frame.key) return;
|
|
26271
|
-
|
|
26994
|
+
const again = !!entryPeer && this.unwelcomed(group, session, frame.key);
|
|
26995
|
+
if (rosterHas(session.roster, frame.key) && !again) return;
|
|
26996
|
+
if (again && this.rewelcomedLately(g, frame.key)) {
|
|
26997
|
+
this.pendingEntries.get(g)?.delete(frame.key);
|
|
26998
|
+
await this.host.closeEdge(linkId);
|
|
26999
|
+
return;
|
|
27000
|
+
}
|
|
27001
|
+
if (again) this.noteRewelcome(g, frame.key);
|
|
27002
|
+
if (!again && (session.roster.length >= GROUP_MEMBER_CAP.max || this.growthBlocked(group, session, session.roster.length + 1, linkId))) return;
|
|
26272
27003
|
if (!entryPeer) await session.setNick(frame.key, this.host.contactName(linkId));
|
|
26273
|
-
this.meet(g, frame.key);
|
|
27004
|
+
if (!again) this.meet(g, frame.key);
|
|
26274
27005
|
if (this.host.linkReady(linkId, 3)) this.markLarge(group, frame.key);
|
|
26275
|
-
|
|
27006
|
+
let welcome;
|
|
27007
|
+
try {
|
|
27008
|
+
welcome = again ? await session.rewelcome(frame.key) : await session.admit(frame.key);
|
|
27009
|
+
} catch (error) {
|
|
27010
|
+
if (!again) throw error;
|
|
27011
|
+
this.pendingEntries.get(g)?.delete(frame.key);
|
|
27012
|
+
await this.host.closeEdge(linkId);
|
|
27013
|
+
return;
|
|
27014
|
+
}
|
|
26276
27015
|
if (!entryPeer) group.contacts = {
|
|
26277
27016
|
...group.contacts,
|
|
26278
27017
|
[frame.key]: linkId
|
|
26279
27018
|
};
|
|
27019
|
+
if (!again) group.unseen = [...(group.unseen ?? []).filter((key) => rosterHas(session.roster, key)), frame.key];
|
|
26280
27020
|
this.answered(group, linkId);
|
|
26281
27021
|
await this.store.putGroup(group);
|
|
26282
27022
|
for (const piece of welcome) this.host.sendOnLink(linkId, piece);
|
|
@@ -26299,7 +27039,7 @@ var Groups = class {
|
|
|
26299
27039
|
case "group-decline": {
|
|
26300
27040
|
const mine = this.stored.get(g);
|
|
26301
27041
|
if (mine?.invitation && !mine.invitation.entry && !mine.state && mine.invitation.linkId === linkId) {
|
|
26302
|
-
await this.
|
|
27042
|
+
await this.takeBackFrom(mine);
|
|
26303
27043
|
return;
|
|
26304
27044
|
}
|
|
26305
27045
|
const entryPeer = this.hostEntry(g, linkId);
|
|
@@ -26308,6 +27048,7 @@ var Groups = class {
|
|
|
26308
27048
|
await this.host.closeEdge(linkId);
|
|
26309
27049
|
}
|
|
26310
27050
|
const group = this.stored.get(g);
|
|
27051
|
+
if (group && await this.declinedUnseen(g, group, linkId)) return;
|
|
26311
27052
|
if (!group || !this.answered(group, linkId)) return;
|
|
26312
27053
|
await this.store.putGroup(group);
|
|
26313
27054
|
this.host.emit();
|
|
@@ -26403,7 +27144,7 @@ var Groups = class {
|
|
|
26403
27144
|
const group = this.stored.get(g);
|
|
26404
27145
|
const member = Object.entries(group?.contacts ?? {}).find(([, id]) => id === linkId)?.[0];
|
|
26405
27146
|
if (!session?.isAdmin || !member || member === session.myKey) return;
|
|
26406
|
-
if (rosterHas(session.roster, member)) await session.remove(member).catch(() => {});
|
|
27147
|
+
if (rosterHas(session.roster, member)) await session.remove(member, Date.now(), { left: true }).catch(() => {});
|
|
26407
27148
|
try {
|
|
26408
27149
|
this.host.sendOnLink(linkId, {
|
|
26409
27150
|
t: "group-removed",
|
|
@@ -26471,7 +27212,8 @@ var Groups = class {
|
|
|
26471
27212
|
return;
|
|
26472
27213
|
}
|
|
26473
27214
|
const taken = await session.handle(peerKey, frame);
|
|
26474
|
-
|
|
27215
|
+
const askedAt = this.askedAt(groupId, peerKey);
|
|
27216
|
+
if (taken.some((f) => f.t === "group-msg" && f.ts < askedAt)) {
|
|
26475
27217
|
let handed = this.lastHanded.get(groupId);
|
|
26476
27218
|
if (!handed) this.lastHanded.set(groupId, handed = /* @__PURE__ */ new Map());
|
|
26477
27219
|
handed.set(peerKey, this.now());
|
|
@@ -26544,9 +27286,14 @@ var Groups = class {
|
|
|
26544
27286
|
return;
|
|
26545
27287
|
}
|
|
26546
27288
|
if (!session || !group || session.status !== "active" || !rosterHas(session.roster, peerKey)) return;
|
|
27289
|
+
const seen = !!group.unseen?.includes(peerKey);
|
|
27290
|
+
if (seen) {
|
|
27291
|
+
const unseen = group.unseen.filter((key) => key !== peerKey);
|
|
27292
|
+
group.unseen = unseen.length ? unseen : void 0;
|
|
27293
|
+
}
|
|
26547
27294
|
const large = this.host.linkReady(linkId, 3) && this.markLarge(group, peerKey);
|
|
26548
27295
|
const legacy = this.hubs.enabled && this.hubs.edgeReady(groupId, session, group, peerKey, this.host.linkReady(linkId, 4), this.now());
|
|
26549
|
-
if (large || legacy) this.store.putGroup(group).catch(() => {});
|
|
27296
|
+
if (large || legacy || seen) this.store.putGroup(group).catch(() => {});
|
|
26550
27297
|
if (!this.holdSync(groupId, peerKey)) try {
|
|
26551
27298
|
this.host.sendOnLink(linkId, session.syncFrame(this.askOf(groupId, session, peerKey)));
|
|
26552
27299
|
} catch {
|
|
@@ -26569,6 +27316,41 @@ var Groups = class {
|
|
|
26569
27316
|
this.host.edgeUp?.(groupId, peerKey);
|
|
26570
27317
|
this.host.emit();
|
|
26571
27318
|
}
|
|
27319
|
+
/**
|
|
27320
|
+
* Admin side: a `group-decline` over the contact chat that let in a member never seen on an edge since. Its welcome
|
|
27321
|
+
* was lost and its app declined the invitation it still showed: it never had the group's keys, so it goes from the
|
|
27322
|
+
* roster (and may be invited again). True when one went.
|
|
27323
|
+
*/
|
|
27324
|
+
async declinedUnseen(groupId, group, linkId) {
|
|
27325
|
+
const session = this.sessions.get(groupId);
|
|
27326
|
+
if (!session?.isAdmin || session.status !== "active" || !group.unseen) return false;
|
|
27327
|
+
const key = group.unseen.find((k) => group.contacts?.[k] === linkId && rosterHas(session.roster, k));
|
|
27328
|
+
if (!key) return false;
|
|
27329
|
+
const edge = this.host.edges(groupId).get(key);
|
|
27330
|
+
if (edge && this.host.linkReady(edge)) return false;
|
|
27331
|
+
const unseen = group.unseen.filter((k) => k !== key);
|
|
27332
|
+
group.unseen = unseen.length ? unseen : void 0;
|
|
27333
|
+
await this.remove(groupId, key).catch(() => {});
|
|
27334
|
+
await this.store.putGroup(group);
|
|
27335
|
+
this.host.emit();
|
|
27336
|
+
return true;
|
|
27337
|
+
}
|
|
27338
|
+
/** Admin side: a member in the roster never seen on an edge since it was let in, whose edge is not up now. */
|
|
27339
|
+
/** This member was sent its welcome again less than `REWELCOME_MS` ago, over its chat or through the link. */
|
|
27340
|
+
rewelcomedLately(groupId, key) {
|
|
27341
|
+
const last = this.rewelcomed.get(groupId)?.get(key);
|
|
27342
|
+
return last !== void 0 && this.now() - last < REWELCOME_MS;
|
|
27343
|
+
}
|
|
27344
|
+
noteRewelcome(groupId, key) {
|
|
27345
|
+
let sent = this.rewelcomed.get(groupId);
|
|
27346
|
+
if (!sent) this.rewelcomed.set(groupId, sent = /* @__PURE__ */ new Map());
|
|
27347
|
+
sent.set(key, this.now());
|
|
27348
|
+
}
|
|
27349
|
+
unwelcomed(group, session, key) {
|
|
27350
|
+
if (!session.isAdmin || !group.unseen?.includes(key) || !rosterHas(session.roster, key)) return false;
|
|
27351
|
+
const edge = this.host.edges(group.id).get(key);
|
|
27352
|
+
return !(edge && this.host.linkReady(edge));
|
|
27353
|
+
}
|
|
26572
27354
|
/** Records that a member's app takes rosters past eight; true when that is news (the caller saves). */
|
|
26573
27355
|
markLarge(group, key) {
|
|
26574
27356
|
if (group.large?.includes(key)) return false;
|
|
@@ -26676,13 +27458,19 @@ var Groups = class {
|
|
|
26676
27458
|
if (out.length) this.lastGossip.set(groupId, now);
|
|
26677
27459
|
return out;
|
|
26678
27460
|
}
|
|
27461
|
+
/** When `via` was last asked for another member's messages (`askOf`), or 0: what it sends written before that is its answer. */
|
|
27462
|
+
askedAt(groupId, via) {
|
|
27463
|
+
let at = 0;
|
|
27464
|
+
for (const asked of this.relayAsked.get(groupId)?.values() ?? []) if (asked.via === via && asked.at > at) at = asked.at;
|
|
27465
|
+
return at;
|
|
27466
|
+
}
|
|
26679
27467
|
/**
|
|
26680
27468
|
* Whether the answer of the member asked for someone's messages may still be coming: its edge up, asked under
|
|
26681
|
-
* `MESH_SYNC_HOLD_MAX_MS` ago, and a message
|
|
27469
|
+
* `MESH_SYNC_HOLD_MAX_MS` ago, and a message of its answer (or the asking) under `MESH_SYNC_HOLD_MS` ago.
|
|
26682
27470
|
*/
|
|
26683
27471
|
answering(groupId, { via, at }, now) {
|
|
26684
27472
|
const edge = this.host.edges(groupId).get(via);
|
|
26685
|
-
if (!edge || !this.host.linkReady(edge) || now - at >=
|
|
27473
|
+
if (!edge || !this.host.linkReady(edge) || now - at >= 18e4) return false;
|
|
26686
27474
|
return now - Math.max(at, this.lastHanded.get(groupId)?.get(via) ?? 0) < MESH_SYNC_HOLD_MS;
|
|
26687
27475
|
}
|
|
26688
27476
|
/**
|
|
@@ -26702,21 +27490,41 @@ var Groups = class {
|
|
|
26702
27490
|
held.set(peerKey, { ...asked });
|
|
26703
27491
|
return true;
|
|
26704
27492
|
}
|
|
26705
|
-
/**
|
|
27493
|
+
/**
|
|
27494
|
+
* The syncs that waited for another member's answer (`holdSync`), once it has come: for what it did not hand on.
|
|
27495
|
+
* One member at a time: the first is told and asked for the others too, which wait for its answer in turn. Told
|
|
27496
|
+
* together, each answered with its own messages alone, and when the member asked first had been away too (it held
|
|
27497
|
+
* little of what was missed) a conversation came as one member's lines, then the next's.
|
|
27498
|
+
*/
|
|
26706
27499
|
heldSyncs(groupId, session, now) {
|
|
26707
27500
|
const held = this.syncHeld.get(groupId);
|
|
26708
27501
|
if (!held?.size) return;
|
|
26709
|
-
const edges = this.host.edges(groupId);
|
|
26710
|
-
|
|
26711
|
-
|
|
27502
|
+
const edges = this.host.edges(groupId), asked = this.relayAsked.get(groupId);
|
|
27503
|
+
const due = [];
|
|
27504
|
+
for (const [key, of] of held) {
|
|
27505
|
+
if (this.answering(groupId, of, now)) continue;
|
|
26712
27506
|
held.delete(key);
|
|
26713
|
-
if (
|
|
27507
|
+
if (asked?.get(key)?.via === of.via) asked.delete(key);
|
|
26714
27508
|
const edge = edges.get(key);
|
|
26715
|
-
if (
|
|
26716
|
-
|
|
26717
|
-
|
|
26718
|
-
|
|
27509
|
+
if (edge && this.host.linkReady(edge) && rosterHas(session.roster, key)) due.push(key);
|
|
27510
|
+
}
|
|
27511
|
+
const [next, ...rest] = due;
|
|
27512
|
+
if (!next) return;
|
|
27513
|
+
const ask = new Set(this.askOf(groupId, session, next));
|
|
27514
|
+
for (const key of rest) {
|
|
27515
|
+
ask.add(key);
|
|
27516
|
+
asked?.set(key, {
|
|
27517
|
+
via: next,
|
|
27518
|
+
at: now
|
|
27519
|
+
});
|
|
27520
|
+
held.set(key, {
|
|
27521
|
+
via: next,
|
|
27522
|
+
at: now
|
|
27523
|
+
});
|
|
26719
27524
|
}
|
|
27525
|
+
try {
|
|
27526
|
+
this.host.sendOnLink(edges.get(next), session.syncFrame([...ask]));
|
|
27527
|
+
} catch {}
|
|
26720
27528
|
}
|
|
26721
27529
|
/**
|
|
26722
27530
|
* Reachable members whose messages below the highest seen never reached me, for `via` to hand on: their own answer
|
|
@@ -26772,15 +27580,16 @@ var Groups = class {
|
|
|
26772
27580
|
let tried = this.gapsAsked.get(groupId);
|
|
26773
27581
|
if (!tried) this.gapsAsked.set(groupId, tried = /* @__PURE__ */ new Map());
|
|
26774
27582
|
let entry = tried.get(via);
|
|
27583
|
+
const now = this.now();
|
|
26775
27584
|
if (!entry || entry.mark !== mark) tried.set(via, entry = {
|
|
26776
27585
|
mark,
|
|
26777
27586
|
of: /* @__PURE__ */ new Set(),
|
|
26778
|
-
noticed: entry?.noticed,
|
|
27587
|
+
noticed: entry?.noticed ?? now,
|
|
26779
27588
|
last: entry?.last
|
|
26780
27589
|
});
|
|
26781
27590
|
if (entry.of.has(via)) return false;
|
|
26782
27591
|
entry.of.add(via);
|
|
26783
|
-
entry.at =
|
|
27592
|
+
entry.at = now;
|
|
26784
27593
|
return true;
|
|
26785
27594
|
}
|
|
26786
27595
|
/**
|
|
@@ -26870,11 +27679,14 @@ var Groups = class {
|
|
|
26870
27679
|
this.host.sendOnLink(edges.get(via), session.syncFrame(keys));
|
|
26871
27680
|
} catch {}
|
|
26872
27681
|
}
|
|
26873
|
-
|
|
26874
|
-
|
|
26875
|
-
|
|
27682
|
+
if (!fresh.size || !connected.length) return;
|
|
27683
|
+
const at = () => connected[(gossipStart(session.myKey) + this.gossipTurn++) % connected.length];
|
|
27684
|
+
let via = at();
|
|
27685
|
+
for (let i = 1; i < connected.length && fresh.has(via); i++) via = at();
|
|
26876
27686
|
const keys = this.gapsOf(groupId, session, via, fresh);
|
|
26877
|
-
if (keys.length)
|
|
27687
|
+
if (!keys.length) return;
|
|
27688
|
+
this.lastGossip.set(groupId, now);
|
|
27689
|
+
try {
|
|
26878
27690
|
this.host.sendOnLink(edges.get(via), session.syncFrame(keys));
|
|
26879
27691
|
} catch {}
|
|
26880
27692
|
}
|
|
@@ -26965,7 +27777,8 @@ var Groups = class {
|
|
|
26965
27777
|
if (!session) throw new Error("Group not found");
|
|
26966
27778
|
return session;
|
|
26967
27779
|
}
|
|
26968
|
-
|
|
27780
|
+
/** `frames`: what the group's rows hold to hand on, oldest first (none yet for a group just made, joined or given back). */
|
|
27781
|
+
attach(state, frames = []) {
|
|
26969
27782
|
const session = new GroupSession(state, {
|
|
26970
27783
|
clock: () => this.now(),
|
|
26971
27784
|
seqFloor: () => this.host.seqFloor?.() ?? 0,
|
|
@@ -27019,8 +27832,21 @@ var Groups = class {
|
|
|
27019
27832
|
...m.frame && { frame: m.frame }
|
|
27020
27833
|
};
|
|
27021
27834
|
const completed = !!m.completes && !!this.host.completeMessage;
|
|
27835
|
+
const placing = completed ? void 0 : this.lineOrder.placing(MESSAGE_LINK(state.id), {
|
|
27836
|
+
id: m.id,
|
|
27837
|
+
epoch: m.epoch,
|
|
27838
|
+
line: false
|
|
27839
|
+
}, this.now());
|
|
27022
27840
|
const stored = completed ? (await this.host.completeMessage(message), false) : await this.host.storeMessage(message);
|
|
27841
|
+
placing?.(stored !== false);
|
|
27023
27842
|
if (m.frame && (completed || stored !== false)) await this.frameKept(state.id, m.id, m.frame);
|
|
27843
|
+
if (!completed && stored !== false) this.authorOrder.placed(state.id, {
|
|
27844
|
+
sender: m.sender,
|
|
27845
|
+
epoch: m.epoch,
|
|
27846
|
+
seq: m.seq,
|
|
27847
|
+
id: m.id,
|
|
27848
|
+
at: this.now()
|
|
27849
|
+
});
|
|
27024
27850
|
const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, arrivalNow(this.now()));
|
|
27025
27851
|
this.lastMessageAt.set(state.id, Math.max(this.lastMessageAt.get(state.id) ?? 0, came));
|
|
27026
27852
|
if (mentioned) this.lastMentionAt.set(state.id, Math.max(this.lastMentionAt.get(state.id) ?? 0, came));
|
|
@@ -27028,6 +27854,10 @@ var Groups = class {
|
|
|
27028
27854
|
noteCame(this.stored.get(state.id), came, m.sender !== session.myKey, mentioned);
|
|
27029
27855
|
this.typings.messageFrom(state.id, m.sender);
|
|
27030
27856
|
},
|
|
27857
|
+
kept: async (id, frame) => {
|
|
27858
|
+
await this.host.keepFrame?.(MESSAGE_LINK(state.id), id, frame).catch(() => {});
|
|
27859
|
+
await this.frameKept(state.id, id, frame);
|
|
27860
|
+
},
|
|
27031
27861
|
edit: (e) => this.host.groupEdit?.(state.id, e),
|
|
27032
27862
|
changed: () => {
|
|
27033
27863
|
if (session.status === "removed" && this.hubs.isHub(state.id) && !this.removedAt.has(state.id)) this.removedAt.set(state.id, this.now());
|
|
@@ -27040,7 +27870,7 @@ var Groups = class {
|
|
|
27040
27870
|
const edge = this.host.edges(state.id).get(to);
|
|
27041
27871
|
return edge && this.host.linkHandled ? this.host.linkHandled(edge) : Promise.resolve(false);
|
|
27042
27872
|
} }
|
|
27043
|
-
});
|
|
27873
|
+
}, frames);
|
|
27044
27874
|
this.sessions.set(state.id, session);
|
|
27045
27875
|
this.lastRoster.set(state.id, session.roster);
|
|
27046
27876
|
this.lastTold.set(state.id, {
|
|
@@ -27069,7 +27899,13 @@ var Groups = class {
|
|
|
27069
27899
|
for (const [key] of after) if (!rosterHas(before, key) && key !== session.myKey) await this.event(groupId, "joined", `${name(key)} joined`, when, top.e, key);
|
|
27070
27900
|
const gone = before.filter(([key]) => !rosterHas(after, key)).map(([key]) => key);
|
|
27071
27901
|
this.hubs.removed(groupId, gone, this.now());
|
|
27072
|
-
|
|
27902
|
+
const left = (key) => {
|
|
27903
|
+
const chain = session.state.chain;
|
|
27904
|
+
for (let e = chain.length - 1; e > 0; e--) if (chain[e].k === "remove" && chain[e].s === key) return !!session.state.leftMarks?.[e];
|
|
27905
|
+
return false;
|
|
27906
|
+
};
|
|
27907
|
+
for (const key of gone) if (left(key)) await this.event(groupId, "left", `${name(key)} left`, when, top.e, key);
|
|
27908
|
+
else await this.event(groupId, "gone", `${name(key)} is no longer a member`, when, top.e, key);
|
|
27073
27909
|
if (top.k === "role") await this.event(groupId, "admin", `${name(top.s)} ${top.s === session.myKey ? "are" : "is"} now the admin`, when, top.e, top.s);
|
|
27074
27910
|
if (top.k === "rotate") await this.event(groupId, "rotated", "Keys rotated: a fresh epoch", when, top.e);
|
|
27075
27911
|
}
|
|
@@ -27124,16 +27960,21 @@ var Groups = class {
|
|
|
27124
27960
|
/** A line in the group's history; lines at the same time (two members gone in one change) get distinct ones, see `eventTime`. */
|
|
27125
27961
|
async event(groupId, event, text, timestamp, epoch, member) {
|
|
27126
27962
|
timestamp = eventTime(this.eventTimes, groupId, timestamp);
|
|
27127
|
-
|
|
27963
|
+
const id = `event:${epoch}:${event}:${timestamp}`;
|
|
27964
|
+
this.lineOrder.placing(MESSAGE_LINK(groupId), {
|
|
27965
|
+
id,
|
|
27966
|
+
epoch,
|
|
27967
|
+
line: true
|
|
27968
|
+
}, this.now())(await this.host.storeMessage({
|
|
27128
27969
|
linkId: MESSAGE_LINK(groupId),
|
|
27129
|
-
id
|
|
27970
|
+
id,
|
|
27130
27971
|
text,
|
|
27131
27972
|
sender: "peer",
|
|
27132
27973
|
event,
|
|
27133
27974
|
member,
|
|
27134
27975
|
timestamp,
|
|
27135
27976
|
via: "datalink"
|
|
27136
|
-
});
|
|
27977
|
+
}) !== false);
|
|
27137
27978
|
}
|
|
27138
27979
|
eventTimes = /* @__PURE__ */ new Map();
|
|
27139
27980
|
};
|
|
@@ -29087,8 +29928,6 @@ function resumeHere(log, runs, peer) {
|
|
|
29087
29928
|
const shared = runs.find((transport) => peer?.includes(transport));
|
|
29088
29929
|
return shared ? { resume: shared } : {};
|
|
29089
29930
|
}
|
|
29090
|
-
/** A round trip that moved less than this is not news in the history. */
|
|
29091
|
-
var RTT_STEP_MS = 10;
|
|
29092
29931
|
var sequence = 0;
|
|
29093
29932
|
var lineId = (at) => `${at.toString(36)}-${(++sequence).toString(36)}`;
|
|
29094
29933
|
/** A row that says the chat is live on its transport from then on. */
|
|
@@ -29212,8 +30051,7 @@ function historyFromRows(rows) {
|
|
|
29212
30051
|
});
|
|
29213
30052
|
else if (e.kind === "flapping") events.push({
|
|
29214
30053
|
...base,
|
|
29215
|
-
kind: e.live ? "live" : "down"
|
|
29216
|
-
reason: `Dropped and came back ${e.count ?? 0} times`
|
|
30054
|
+
kind: e.live ? "live" : "down"
|
|
29217
30055
|
});
|
|
29218
30056
|
else if (e.kind === "connected" || e.kind === "back") events.push({
|
|
29219
30057
|
...base,
|
|
@@ -29493,7 +30331,7 @@ var TransportLog = class {
|
|
|
29493
30331
|
changed = true;
|
|
29494
30332
|
}
|
|
29495
30333
|
const stretch = [...this.history].reverse().find((e) => e.kind === "live" || e.kind === "switched" || e.kind === "down");
|
|
29496
|
-
if (stretch && stretch.kind !== "down" && stretch.transport === on && (stretch.rttMs === void 0 || Math.abs(stretch.rttMs - ms) >=
|
|
30334
|
+
if (stretch && stretch.kind !== "down" && stretch.transport === on && (stretch.rttMs === void 0 || Math.abs(stretch.rttMs - ms) >= 10)) {
|
|
29497
30335
|
stretch.rttMs = ms;
|
|
29498
30336
|
changed = true;
|
|
29499
30337
|
}
|
|
@@ -30975,7 +31813,8 @@ var CashuWallet = class CashuWallet {
|
|
|
30975
31813
|
tx: walletTx(mint, kind, credited, Math.max(0, faceValue - credited), note),
|
|
30976
31814
|
...payment ? { payment: payment(credited, mint) } : {}
|
|
30977
31815
|
});
|
|
30978
|
-
|
|
31816
|
+
const written = (await this.swapsAt(mint)).filter((s) => s.kind === "receive");
|
|
31817
|
+
let swap = written.find((s) => s.token === token);
|
|
30979
31818
|
if (swap?.finished) return {
|
|
30980
31819
|
amount: swap.tx?.amount ?? 0,
|
|
30981
31820
|
mint,
|
|
@@ -30983,6 +31822,8 @@ var CashuWallet = class CashuWallet {
|
|
|
30983
31822
|
};
|
|
30984
31823
|
if (!swap) {
|
|
30985
31824
|
const preview = await (await this.wallet(mint)).prepareSwapToReceive(token);
|
|
31825
|
+
const redeeming = new Set(written.flatMap((s) => (swapOf(s).inputs ?? []).map((p) => p.secret)));
|
|
31826
|
+
if (preview.inputs.some((p) => redeeming.has(p.secret))) throw engineError("ecashBeingReceived");
|
|
30986
31827
|
swap = {
|
|
30987
31828
|
id: swapId(),
|
|
30988
31829
|
mint,
|
|
@@ -31119,7 +31960,17 @@ var CashuWallet = class CashuWallet {
|
|
|
31119
31960
|
id: p.id
|
|
31120
31961
|
})));
|
|
31121
31962
|
if (states.length !== saved.inputs.length) throw new Error("The mint answered for another number of proofs");
|
|
31122
|
-
if (states.some((s) => s.state === "PENDING"))
|
|
31963
|
+
if (states.some((s) => s.state === "PENDING")) {
|
|
31964
|
+
if (swap.kind === "receive" && refused) return {
|
|
31965
|
+
state: "never",
|
|
31966
|
+
gone: /* @__PURE__ */ new Set(),
|
|
31967
|
+
final: true
|
|
31968
|
+
};
|
|
31969
|
+
return {
|
|
31970
|
+
state: "open",
|
|
31971
|
+
pending: true
|
|
31972
|
+
};
|
|
31973
|
+
}
|
|
31123
31974
|
const gone = new Set(saved.inputs.filter((_, i) => states[i]?.state === "SPENT").map((p) => p.secret));
|
|
31124
31975
|
if (gone.size > 0) {
|
|
31125
31976
|
const second = await signed();
|
|
@@ -31241,7 +32092,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31241
32092
|
}
|
|
31242
32093
|
/**
|
|
31243
32094
|
* Swaps still written down: the app closed while the mint was being asked, or the mint did not answer. Each mint is
|
|
31244
|
-
* asked where its own stand, under its lock. A swap it made is stored as its answer would have been; one it says it
|
|
32095
|
+
* asked where its own stand, one swap at a time under its lock. A swap it made is stored as its answer would have been; one it says it
|
|
31245
32096
|
* never made is undone; one that is only overdue gives its inputs back and stays watched; one it cannot speak for
|
|
31246
32097
|
* is asked about again. `mints`: only these mints', once, now.
|
|
31247
32098
|
*/
|
|
@@ -31260,16 +32111,18 @@ var CashuWallet = class CashuWallet {
|
|
|
31260
32111
|
const later = (ms) => {
|
|
31261
32112
|
again = again ? Math.min(again, ms) : ms;
|
|
31262
32113
|
};
|
|
31263
|
-
for (const mint of
|
|
31264
|
-
|
|
32114
|
+
for (const { id, mint } of open) await this.locked(mint, async () => {
|
|
32115
|
+
let swap = await wrap((await store(STORES$1.swaps, "readonly")).get(id));
|
|
32116
|
+
if (!swap) return;
|
|
32117
|
+
try {
|
|
31265
32118
|
if (swap.finished) {
|
|
31266
32119
|
if (await this.tellSwap(swap, true)) await wrap((await store(STORES$1.swaps, "readwrite")).delete(swap.id));
|
|
31267
32120
|
else later(SWAP_POLL_MS);
|
|
31268
|
-
|
|
32121
|
+
return;
|
|
31269
32122
|
}
|
|
31270
32123
|
if (swap.askAt && Date.now() < swap.askAt && !mints) {
|
|
31271
32124
|
later(swap.askAt - Date.now());
|
|
31272
|
-
|
|
32125
|
+
return;
|
|
31273
32126
|
}
|
|
31274
32127
|
if (swap.attemptEndedAt === void 0) {
|
|
31275
32128
|
swap = {
|
|
@@ -31282,7 +32135,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31282
32135
|
if (outcome.state === "open") {
|
|
31283
32136
|
if (swap.released && Date.now() >= swap.createdAt + SWAP_WATCH_MS) {
|
|
31284
32137
|
await this.dropSwap(swap, /* @__PURE__ */ new Set());
|
|
31285
|
-
|
|
32138
|
+
return;
|
|
31286
32139
|
}
|
|
31287
32140
|
if (outcome.slow) {
|
|
31288
32141
|
const stuck = (swap.stuck ?? 0) + 1;
|
|
@@ -31321,8 +32174,20 @@ var CashuWallet = class CashuWallet {
|
|
|
31321
32174
|
this.events.onChange();
|
|
31322
32175
|
} else later(SWAP_POLL_MS);
|
|
31323
32176
|
}
|
|
31324
|
-
else
|
|
31325
|
-
|
|
32177
|
+
else if (outcome.pending && swap.kind === "receive" && !swap.released && Date.now() >= swapOverdueAt(swap)) {
|
|
32178
|
+
if (Date.now() < swap.createdAt + SWAP_WATCH_MS) {
|
|
32179
|
+
await wrap((await store(STORES$1.swaps, "readwrite")).put({
|
|
32180
|
+
...swap,
|
|
32181
|
+
askAt: Date.now() + SWAP_SLOW_POLL_MS
|
|
32182
|
+
}));
|
|
32183
|
+
later(SWAP_SLOW_POLL_MS);
|
|
32184
|
+
} else {
|
|
32185
|
+
await this.dropSwap(swap, /* @__PURE__ */ new Set());
|
|
32186
|
+
await this.tellSwap(swap, false);
|
|
32187
|
+
this.events.onChange();
|
|
32188
|
+
}
|
|
32189
|
+
} else later(swap.released ? SWAP_SLOW_POLL_MS : SWAP_POLL_MS);
|
|
32190
|
+
return;
|
|
31326
32191
|
}
|
|
31327
32192
|
if (outcome.state === "done") {
|
|
31328
32193
|
if (await this.finishSwap(swap, outcome.keep, outcome.send, true)) {
|
|
@@ -31333,7 +32198,7 @@ var CashuWallet = class CashuWallet {
|
|
|
31333
32198
|
} else if (swap.released) {
|
|
31334
32199
|
if (outcome.final || Date.now() >= swap.createdAt + SWAP_WATCH_MS) await this.dropSwap(swap, outcome.gone);
|
|
31335
32200
|
else later(SWAP_SLOW_POLL_MS);
|
|
31336
|
-
|
|
32201
|
+
return;
|
|
31337
32202
|
} else {
|
|
31338
32203
|
await this.dropSwap(swap, outcome.gone, !outcome.final);
|
|
31339
32204
|
await this.tellSwap(swap, false);
|
|
@@ -32787,12 +33652,27 @@ var PaymentDesk = class {
|
|
|
32787
33652
|
}
|
|
32788
33653
|
return;
|
|
32789
33654
|
}
|
|
32790
|
-
if (done)
|
|
32791
|
-
|
|
33655
|
+
if (done) {
|
|
33656
|
+
await this.creditCashu(record, link);
|
|
33657
|
+
return;
|
|
33658
|
+
}
|
|
33659
|
+
if (this.payments.has(record.id)) return;
|
|
33660
|
+
const reason = "The mint did not confirm the ecash";
|
|
33661
|
+
link?.sendPaymentResult({
|
|
32792
33662
|
id: record.id,
|
|
32793
33663
|
ok: false,
|
|
32794
|
-
error:
|
|
33664
|
+
error: reason
|
|
32795
33665
|
});
|
|
33666
|
+
try {
|
|
33667
|
+
await this.host.storeMessage({
|
|
33668
|
+
linkId: record.linkId,
|
|
33669
|
+
id: `peer_${record.createdAt}_refused`,
|
|
33670
|
+
text: `Could not receive ${record.amount.toLocaleString()} sats: ${reason}`,
|
|
33671
|
+
sender: "peer",
|
|
33672
|
+
timestamp: record.createdAt,
|
|
33673
|
+
via: "datalink"
|
|
33674
|
+
});
|
|
33675
|
+
} catch {}
|
|
32796
33676
|
}
|
|
32797
33677
|
async onPaymentResult(linkId, result) {
|
|
32798
33678
|
const payment = this.find(linkId, result.id);
|
|
@@ -34383,24 +35263,6 @@ function messageAttention(type, message, { startedAt, newest, now }) {
|
|
|
34383
35263
|
};
|
|
34384
35264
|
}
|
|
34385
35265
|
//#endregion
|
|
34386
|
-
//#region ../browser/src/shared/hyperdhtRelay.ts
|
|
34387
|
-
/** Only `wss://`, or `ws://` to this machine (a relay for tests or development). */
|
|
34388
|
-
function hyperdhtRelayProblem(url) {
|
|
34389
|
-
let parsed;
|
|
34390
|
-
try {
|
|
34391
|
-
parsed = new URL(url);
|
|
34392
|
-
} catch {
|
|
34393
|
-
return "Enter a relay address (wss://…)";
|
|
34394
|
-
}
|
|
34395
|
-
if (parsed.protocol === "wss:") return null;
|
|
34396
|
-
if (parsed.protocol === "ws:" && [
|
|
34397
|
-
"127.0.0.1",
|
|
34398
|
-
"localhost",
|
|
34399
|
-
"[::1]"
|
|
34400
|
-
].includes(parsed.hostname)) return null;
|
|
34401
|
-
return "Use a wss:// relay address";
|
|
34402
|
-
}
|
|
34403
|
-
//#endregion
|
|
34404
35266
|
//#region ../browser/src/shared/pushRelay.ts
|
|
34405
35267
|
/**
|
|
34406
35268
|
* The push relay a browser hands wake-ups to when it may not post to a push service itself (WISP 401 § Wake-up
|
|
@@ -34551,10 +35413,10 @@ var NATIVE_SLOTS = 8;
|
|
|
34551
35413
|
var IROH_WEB_SLOTS = 16;
|
|
34552
35414
|
/** Of a transport's listeners, the ones a group's links never hold: 1:1 chats keep that room. */
|
|
34553
35415
|
var CHAT_NATIVE_SLOTS = 4;
|
|
34554
|
-
/** How often a group link that found no free native slot tries again. */
|
|
34555
|
-
var
|
|
34556
|
-
/** The longest a group link whose native listener failed to start waits before trying again (its wait doubles up to it). */
|
|
34557
|
-
var
|
|
35416
|
+
/** How often a group link that found no free native slot tries again, and the first wait of a listener that failed to start. */
|
|
35417
|
+
var NATIVE_RETRY_MS = 15e3;
|
|
35418
|
+
/** The longest a chat or a group link whose native listener failed to start waits before trying again (its wait doubles up to it). */
|
|
35419
|
+
var NATIVE_FAILED_MAX_MS = 3e5;
|
|
34558
35420
|
/** "Clear all data": how long taking things back from the network may hold up the clear, in all. */
|
|
34559
35421
|
var CLEAR_WITHDRAW_MS = 6e3;
|
|
34560
35422
|
/**
|
|
@@ -34583,11 +35445,19 @@ var DEFAULT_SETTINGS = {
|
|
|
34583
35445
|
*/
|
|
34584
35446
|
var CARD_RESTORE_MOST = 50;
|
|
34585
35447
|
var CARD_RESTORE_MS = 6048e5;
|
|
35448
|
+
/**
|
|
35449
|
+
* A round trip that moved is shown at once, and written to disk by itself (relaxed, no disk sync of its own) at most
|
|
35450
|
+
* this often: a 15 s sample on an internet path moves by 10 ms most of the time. A power cut loses at most this much
|
|
35451
|
+
* of the round trip's drift, never a row; any other change to the story writes the latest round trip with it.
|
|
35452
|
+
*/
|
|
35453
|
+
var RTT_SAVE_MS = 3e5;
|
|
34586
35454
|
/** A reaction said on the live session and not confirmed is said again after this long. */
|
|
34587
35455
|
var REACTION_RESEND_MS = 3e4;
|
|
34588
35456
|
/** A file or a payment request of mine that waits for the chat to be live (a text that waits is its outbox's). */
|
|
34589
35457
|
var waitsForLive = (m) => m.sender === "me" && m.delivery === "waiting" && (!!m.file || !!m.paymentId);
|
|
34590
35458
|
/** This side's participation key in a chat: derived from its seed once, not at every state built. */
|
|
35459
|
+
/** A read of the contact's packet that found it as the last one did: online the same, the same packet, name and services. */
|
|
35460
|
+
var samePresence = (a, b) => a.online === b.online && a.lastPacketAt === b.lastPacketAt && a.seenAt === b.seenAt && a.nick === b.nick && JSON.stringify(a.services) === JSON.stringify(b.services);
|
|
34591
35461
|
function participationKeyOf(live) {
|
|
34592
35462
|
const seed = live.stored.participationSeed;
|
|
34593
35463
|
if (!seed) return void 0;
|
|
@@ -34765,7 +35635,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34765
35635
|
* Everything that publishes, dials, polls or settles asks this, not the stored setting.
|
|
34766
35636
|
*/
|
|
34767
35637
|
get networkOn() {
|
|
34768
|
-
return !!this.settings.online && !this.limitedMode;
|
|
35638
|
+
return !!this.settings.online && !this.limitedMode && !this.options.offline;
|
|
34769
35639
|
}
|
|
34770
35640
|
transport;
|
|
34771
35641
|
/** The same, for groups' requests: a relay budget keeps a chat's share from them (`CHAT_RESERVE`). */
|
|
@@ -34783,10 +35653,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34783
35653
|
});
|
|
34784
35654
|
clockOff;
|
|
34785
35655
|
/**
|
|
34786
|
-
* Edges of groups whose WebRTC did not connect
|
|
34787
|
-
*
|
|
35656
|
+
* Edges of groups whose WebRTC did not connect (`edgeWithoutRtc`): they go on as an app with no WebRTC does, over a
|
|
35657
|
+
* native transport, until the member is seen online again after a packet said it was gone (`away`, `edgeRtcPresence`).
|
|
35658
|
+
* Not kept: the next start tries WebRTC first again.
|
|
34788
35659
|
*/
|
|
34789
|
-
edgeRtcOff = /* @__PURE__ */ new
|
|
35660
|
+
edgeRtcOff = /* @__PURE__ */ new Map();
|
|
34790
35661
|
pollIntervals;
|
|
34791
35662
|
localFetch;
|
|
34792
35663
|
links = /* @__PURE__ */ new Map();
|
|
@@ -34809,6 +35680,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34809
35680
|
});
|
|
34810
35681
|
/** `start` has put the kept transfers back (EngineState.transfersRestored). */
|
|
34811
35682
|
transfersRestored = false;
|
|
35683
|
+
/** `start` has loaded the groups (EngineState.groupsLoaded). */
|
|
35684
|
+
groupsLoaded = false;
|
|
34812
35685
|
feedbackStartedAt = Date.now();
|
|
34813
35686
|
feedbackIds = /* @__PURE__ */ new Set();
|
|
34814
35687
|
walletFeedbackReady = false;
|
|
@@ -34892,9 +35765,15 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34892
35765
|
groupWakeReceived = /* @__PURE__ */ new Map();
|
|
34893
35766
|
requestCounts = /* @__PURE__ */ new Map();
|
|
34894
35767
|
transfers = /* @__PURE__ */ new Map();
|
|
35768
|
+
/**
|
|
35769
|
+
* Views that go in the next state sent, and then away (`GroupFilesHost.brief`): dropped once a state carried them,
|
|
35770
|
+
* unless another view took their place meanwhile.
|
|
35771
|
+
*/
|
|
35772
|
+
briefViews = /* @__PURE__ */ new Map();
|
|
34895
35773
|
/** files/3 in every chat: offers, resumable transfers, checked by digest (WISP 501 rev 0.3). */
|
|
34896
35774
|
fileDesk = new FileDesk({
|
|
34897
35775
|
send: (linkId, frame) => this.links.get(linkId)?.link?.sendFilesFrame(frame) ?? false,
|
|
35776
|
+
sendData: (linkId, id, offset, data) => this.links.get(linkId)?.link?.sendFileData(id, offset, data) ?? false,
|
|
34898
35777
|
writable: (linkId) => this.links.get(linkId)?.link?.filesWritable(),
|
|
34899
35778
|
receivedBytes: (linkId) => this.links.get(linkId)?.files.receivedBytes ?? 0,
|
|
34900
35779
|
wireIds: (linkId) => this.links.get(linkId)?.files.wireIds,
|
|
@@ -35670,6 +36549,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35670
36549
|
forgetFrame: async (linkId, id) => {
|
|
35671
36550
|
await db.patchMessage(linkId, id, (stored) => stored.frame ? { frame: void 0 } : null, true);
|
|
35672
36551
|
},
|
|
36552
|
+
keepFrame: async (linkId, id, frame) => {
|
|
36553
|
+
await db.patchMessage(linkId, id, () => ({ frame }), true);
|
|
36554
|
+
},
|
|
36555
|
+
placeAgain: (linkId, ids) => this.placeAgain(linkId, ids),
|
|
35673
36556
|
membersChanged: (groupId) => this.groupMembersChanged(groupId),
|
|
35674
36557
|
historyGone: (groupId) => this.groupHistoryGone(groupId),
|
|
35675
36558
|
emit: () => this.emitState(),
|
|
@@ -35690,11 +36573,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35690
36573
|
groupFiles: {
|
|
35691
36574
|
store: storedGroupFiles,
|
|
35692
36575
|
transfers: this.transfers,
|
|
36576
|
+
brief: (id) => {
|
|
36577
|
+
const view = this.transfers.get(id);
|
|
36578
|
+
if (view) this.briefViews.set(id, view);
|
|
36579
|
+
},
|
|
35693
36580
|
settings: () => ({
|
|
35694
36581
|
autoDownloads: this.settings.autoDownloads !== false,
|
|
35695
36582
|
serveFiles: this.settings.serveFiles !== false
|
|
35696
36583
|
}),
|
|
35697
36584
|
sendFiles: (linkId, frame) => this.links.get(linkId)?.link?.sendFilesFrame(frame) ?? false,
|
|
36585
|
+
sendData: (linkId, id, offset, data) => this.links.get(linkId)?.link?.sendFileData(id, offset, data) ?? false,
|
|
35698
36586
|
writable: (linkId) => this.links.get(linkId)?.link?.filesWritable()
|
|
35699
36587
|
}
|
|
35700
36588
|
});
|
|
@@ -36087,7 +36975,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36087
36975
|
const messages = stored.group ? [] : await db.getMessages(stored.id);
|
|
36088
36976
|
const storedFiles = stored.group ? [] : await fileStore.listForLink(stored.id);
|
|
36089
36977
|
const waiting = new Set(messages.flatMap((m) => m.sender === "me" && m.delivery === "waiting" && m.file ? [m.file.id] : []));
|
|
36090
|
-
for (const file of storedFiles) if (file.transfer && !file.wire3 && !waiting.has(file.id)) this.transfers.set(file.id, file.transfer.state === "transferring" ? {
|
|
36978
|
+
for (const file of storedFiles) if (file.transfer && file.transfer.state !== "done" && !file.wire3 && !waiting.has(file.id)) this.transfers.set(file.id, file.transfer.state === "transferring" ? {
|
|
36091
36979
|
...file.transfer,
|
|
36092
36980
|
state: "failed",
|
|
36093
36981
|
error: "Transfer interrupted. Retry when connected."
|
|
@@ -36105,6 +36993,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36105
36993
|
this.transfersRestored = true;
|
|
36106
36994
|
for (const group of await db.getGroups()) await this.settleHistory(`group:${group.id}`);
|
|
36107
36995
|
await this.groups.load(this.limitedMode);
|
|
36996
|
+
this.groupsLoaded = true;
|
|
36108
36997
|
for (const group of this.groups.views()) {
|
|
36109
36998
|
const note = latestReaction(await db.getMessages(`group:${group.id}`));
|
|
36110
36999
|
if (note) this.reactionNotes.set(`group:${group.id}`, note);
|
|
@@ -36123,7 +37012,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36123
37012
|
this.prepareSpare(STARTUP_QUIET_MS);
|
|
36124
37013
|
}
|
|
36125
37014
|
if (!this.limitedMode) this.openStartedWallets(fresh);
|
|
36126
|
-
if (this.appsOn) this.appStore.start().catch(() => {});
|
|
37015
|
+
if (this.appsOn && !this.appsWireOnly) this.appStore.start().catch(() => {});
|
|
36127
37016
|
if (!this.options.singleDevice && this.networkOn && knownDeviceGate()?.state === "active") this.startDeviceSet().catch(() => {});
|
|
36128
37017
|
if (!this.options.singleDevice && this.limitedMode && knownDeviceGate()?.state === "active") this.readTurnWhileLimited();
|
|
36129
37018
|
if (this.restoreCheck === "checking" && this.limitedMode) this.checkRestoredTurn(0);
|
|
@@ -36588,7 +37477,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36588
37477
|
appShelf;
|
|
36589
37478
|
/** The installed apps and stores, refused while the apps feature is off. */
|
|
36590
37479
|
get appStore() {
|
|
36591
|
-
if (!this.appsOn) throw this.appsOff();
|
|
37480
|
+
if (!this.appsOn || this.appsWireOnly) throw this.appsOff();
|
|
36592
37481
|
return this.appShelf ??= new Apps({
|
|
36593
37482
|
fetch: boundedAppFetch({
|
|
36594
37483
|
fetcher: this.options.appFetch,
|
|
@@ -36599,12 +37488,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36599
37488
|
return !!stored && !stored.group && !!stored.profile && !!stored.pairedPeerKey;
|
|
36600
37489
|
},
|
|
36601
37490
|
online: () => this.networkOn && !this.shuttingDown,
|
|
36602
|
-
checked: () => this.events.onAppsChecked?.()
|
|
37491
|
+
checked: () => this.events.onAppsChecked?.(),
|
|
37492
|
+
...this.options.builtInApp && { readBuiltIn: this.options.builtInApp }
|
|
36603
37493
|
});
|
|
36604
37494
|
}
|
|
36605
37495
|
appList() {
|
|
36606
37496
|
return this.appStore.list();
|
|
36607
37497
|
}
|
|
37498
|
+
appBuiltIns() {
|
|
37499
|
+
return this.appStore.builtIns();
|
|
37500
|
+
}
|
|
36608
37501
|
appStoreList() {
|
|
36609
37502
|
return this.appStore.listStores();
|
|
36610
37503
|
}
|
|
@@ -36650,6 +37543,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36650
37543
|
appFile(params) {
|
|
36651
37544
|
return this.appStore.file(params);
|
|
36652
37545
|
}
|
|
37546
|
+
appFileAt(params) {
|
|
37547
|
+
return this.appStore.fileAt(params);
|
|
37548
|
+
}
|
|
36653
37549
|
appStorageGet(params) {
|
|
36654
37550
|
return this.appStore.storageGet(params);
|
|
36655
37551
|
}
|
|
@@ -36665,6 +37561,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36665
37561
|
appDataExport(params) {
|
|
36666
37562
|
return this.appStore.exportData(params);
|
|
36667
37563
|
}
|
|
37564
|
+
appDataExportText(params) {
|
|
37565
|
+
return this.appStore.exportDataText(params);
|
|
37566
|
+
}
|
|
36668
37567
|
getState() {
|
|
36669
37568
|
const groups = this.groups.views();
|
|
36670
37569
|
return {
|
|
@@ -36692,6 +37591,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36692
37591
|
})).sort((a, b) => a.createdAt - b.createdAt),
|
|
36693
37592
|
transfers: Object.fromEntries(this.transfers),
|
|
36694
37593
|
transfersRestored: this.transfersRestored,
|
|
37594
|
+
...this.groupsLoaded && { groupsLoaded: true },
|
|
36695
37595
|
wallet: this.walletView,
|
|
36696
37596
|
payments: this.desk.views(),
|
|
36697
37597
|
identityProofs: this.identities.views(),
|
|
@@ -36849,9 +37749,17 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36849
37749
|
madeAt: Date.now()
|
|
36850
37750
|
};
|
|
36851
37751
|
}
|
|
36852
|
-
/**
|
|
37752
|
+
/**
|
|
37753
|
+
* One invite warmed and waiting (`after` ms from now), warmed again every so often while it waits (the relays forget).
|
|
37754
|
+
* Once too old for `takeInvite` to hand out, it is replaced by a new one, warmed at once: warming it on would spend
|
|
37755
|
+
* two puts every few minutes on keys nobody will use, and leave the next invite on cold keys.
|
|
37756
|
+
*/
|
|
36853
37757
|
prepareSpare(after = 0) {
|
|
36854
37758
|
if (this.shuttingDown || !this.networkOn) return;
|
|
37759
|
+
if (this.spare && after === 0 && Date.now() - this.spare.madeAt >= SPARE_INVITE_MAX_AGE_MS) {
|
|
37760
|
+
for (const identity of [identityFromSeedB64(this.spare.mine.seedB64), this.spare.inviteKey]) this.warmedKeys.delete(identity.pubKeyZ32);
|
|
37761
|
+
this.spare = null;
|
|
37762
|
+
}
|
|
36855
37763
|
if (!this.spare) this.spare = this.makeSpare();
|
|
36856
37764
|
const spare = this.spare;
|
|
36857
37765
|
if (this.spareTimer) clearTimeout(this.spareTimer);
|
|
@@ -36867,10 +37775,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36867
37775
|
this.warmedKeys.set(identity.pubKeyZ32, Date.now());
|
|
36868
37776
|
this.warmKey(identity);
|
|
36869
37777
|
}
|
|
37778
|
+
const untilAged = spare.madeAt + SPARE_INVITE_MAX_AGE_MS - Date.now();
|
|
36870
37779
|
this.spareTimer = setTimeout(() => {
|
|
36871
37780
|
this.spareTimer = null;
|
|
36872
37781
|
if (this.spare === spare) this.prepareSpare();
|
|
36873
|
-
}, SPARE_INVITE_WARM_EVERY_MS);
|
|
37782
|
+
}, Math.min(SPARE_INVITE_WARM_EVERY_MS, untilAged));
|
|
36874
37783
|
}
|
|
36875
37784
|
async joinLink({ inviteCode }) {
|
|
36876
37785
|
const decoded = decodeInviteCode(inviteCode);
|
|
@@ -37535,7 +38444,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37535
38444
|
this.transport.networkChanged?.();
|
|
37536
38445
|
this.directPath.reset();
|
|
37537
38446
|
}
|
|
37538
|
-
if (params.network) for (const failed of this.
|
|
38447
|
+
if (params.network) for (const failed of this.nativeFailed.values()) {
|
|
37539
38448
|
failed.at = 0;
|
|
37540
38449
|
failed.wait = 0;
|
|
37541
38450
|
}
|
|
@@ -37749,14 +38658,23 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37749
38658
|
/**
|
|
37750
38659
|
* The message of a chat a reply answers, by its id here or the id both sides know it by (`replyRef`): what the
|
|
37751
38660
|
* reply keeps of it, or why there is nothing to reply to. A group's reply needs the author's member key too.
|
|
38661
|
+
* Read alone by its id, or a chat's `peer_`/`me_` row for a wire id, as `resolveReply` does: a group's message goes
|
|
38662
|
+
* by its row's id, so a miss there is not here; a chat's other rows (a file's, a payment's) are looked for in the
|
|
38663
|
+
* whole chat.
|
|
37752
38664
|
*/
|
|
37753
38665
|
async replyFor(linkId, messageId) {
|
|
37754
38666
|
if (typeof messageId !== "string" || !messageId) return "No message to reply to";
|
|
37755
|
-
const
|
|
37756
|
-
|
|
38667
|
+
const group = linkId.startsWith("group:");
|
|
38668
|
+
let original = await db.getMessage(linkId, messageId);
|
|
38669
|
+
for (const rowId of group ? [] : [`peer_${messageId}`, `me_${messageId}`]) {
|
|
38670
|
+
if (original) break;
|
|
38671
|
+
const row = await db.getMessage(linkId, rowId);
|
|
38672
|
+
if (row && replyRef(row) === messageId) original = row;
|
|
38673
|
+
}
|
|
38674
|
+
if (!original && !group) original = (await db.getMessages(linkId)).find((m) => replyRef(m) === messageId);
|
|
37757
38675
|
const ref = original && replyRef(original);
|
|
37758
38676
|
if (!original || !ref) return "That message is not in this chat, or cannot be replied to";
|
|
37759
|
-
if (
|
|
38677
|
+
if (group && !original.member) return "That message cannot be replied to";
|
|
37760
38678
|
return replyTo(original, ref);
|
|
37761
38679
|
}
|
|
37762
38680
|
/**
|
|
@@ -38094,6 +39012,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38094
39012
|
const chat = this.links.get(linkId)?.stored;
|
|
38095
39013
|
outbox = new Outbox({
|
|
38096
39014
|
read: () => db.getMessages(linkId),
|
|
39015
|
+
get: (id) => db.getMessage(linkId, id),
|
|
38097
39016
|
update: async (id, delivery, error, extra) => {
|
|
38098
39017
|
if (delivery === "delivered") await db.patchMessage(linkId, id, (message) => message.delivery === "delivered" ? null : {
|
|
38099
39018
|
...message.delivery && {
|
|
@@ -39175,19 +40094,40 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39175
40094
|
const cue = transportCue(mark, log.entries, log.history);
|
|
39176
40095
|
if (cue) this.cueFeedback(cue, linkId);
|
|
39177
40096
|
}
|
|
39178
|
-
|
|
40097
|
+
/** `toDisk: false` shows the story and leaves the disk as it is; `relaxed`: written with no disk sync of its own. */
|
|
40098
|
+
saveTransportLog(live, log, { toDisk = true, relaxed = false } = {}) {
|
|
39179
40099
|
const transportLog = log.entries.map((e) => ({ ...e })), transportHistory = log.history.map((e) => ({ ...e }));
|
|
39180
40100
|
live.stored = {
|
|
39181
40101
|
...live.stored,
|
|
39182
40102
|
transportLog,
|
|
39183
40103
|
transportHistory
|
|
39184
40104
|
};
|
|
39185
|
-
|
|
39186
|
-
|
|
39187
|
-
|
|
39188
|
-
|
|
40105
|
+
if (toDisk) {
|
|
40106
|
+
live.rttUnsaved = false;
|
|
40107
|
+
db.patchLink(live.stored.id, {
|
|
40108
|
+
transportLog,
|
|
40109
|
+
transportHistory
|
|
40110
|
+
}, relaxed).catch(() => {});
|
|
40111
|
+
}
|
|
39189
40112
|
this.emitState();
|
|
39190
40113
|
}
|
|
40114
|
+
/** A round trip measured on the chat's transport: in the view at once, on disk at most every `RTT_SAVE_MS`. */
|
|
40115
|
+
noteRtt(live, ms) {
|
|
40116
|
+
const moved = live.rttPosted === void 0 || Math.abs(live.rttPosted - ms) >= 10;
|
|
40117
|
+
const log = this.transportLogOf(live);
|
|
40118
|
+
if (!log) {
|
|
40119
|
+
if (moved) this.emitState();
|
|
40120
|
+
return;
|
|
40121
|
+
}
|
|
40122
|
+
const changed = log.rtt(ms);
|
|
40123
|
+
if (changed) live.rttUnsaved = true;
|
|
40124
|
+
const now = Date.now();
|
|
40125
|
+
if (live.rttUnsaved && (live.rttSavedAt === void 0 || now - live.rttSavedAt >= RTT_SAVE_MS)) {
|
|
40126
|
+
live.rttSavedAt = now;
|
|
40127
|
+
this.saveTransportLog(live, log, { relaxed: true });
|
|
40128
|
+
} else if (changed) this.saveTransportLog(live, log, { toDisk: false });
|
|
40129
|
+
else if (moved) this.emitState();
|
|
40130
|
+
}
|
|
39191
40131
|
/** Which ways of paying one chat allows. */
|
|
39192
40132
|
/** `networks`: for a way of paying, the networks this chat takes it on (the Accept side's cards); the others are kept. */
|
|
39193
40133
|
async setChatPaymentMethods({ linkId, methods, networks }) {
|
|
@@ -39219,7 +40159,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39219
40159
|
async connect({ linkId }) {
|
|
39220
40160
|
const link = this.links.get(linkId)?.link;
|
|
39221
40161
|
if (!link) throw new Error("You are offline. Go online before reconnecting.");
|
|
39222
|
-
|
|
40162
|
+
const live = this.links.get(linkId), native = this.ensureNativeEndpoints(linkId);
|
|
40163
|
+
const { preferredTransport: preferred, transportFallback: fallback } = live.stored;
|
|
40164
|
+
if (this.reachedOnlyNatively(live) || fallback === false && preferred && preferred !== "webrtc/1") await native;
|
|
39223
40165
|
await link.connect();
|
|
39224
40166
|
}
|
|
39225
40167
|
disconnect({ linkId }) {
|
|
@@ -39729,11 +40671,22 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39729
40671
|
}
|
|
39730
40672
|
/** Mini-apps are on in this build (`APPS_ENABLED`) or for this engine (`apps`). */
|
|
39731
40673
|
get appsOn() {
|
|
39732
|
-
return this.options.apps ?? true;
|
|
40674
|
+
return !!(this.options.apps ?? true);
|
|
39733
40675
|
}
|
|
39734
|
-
/**
|
|
40676
|
+
/** `apps/1` only (`apps: "wire"`): this host runs no mini-app and keeps no store, so nothing is installed here to check. */
|
|
40677
|
+
get appsWireOnly() {
|
|
40678
|
+
return this.options.apps === "wire";
|
|
40679
|
+
}
|
|
40680
|
+
/** Why an app call is refused: this client runs no mini-app (`apps: false`, or `"wire"` for the store's), or the build has them off. */
|
|
39735
40681
|
appsOff() {
|
|
39736
|
-
return /* @__PURE__ */ new Error(this.options.apps === false ? "Apps do not run on this client" : "Apps are unavailable in this release");
|
|
40682
|
+
return /* @__PURE__ */ new Error(this.options.apps === false || this.appsWireOnly ? "Apps do not run on this client" : "Apps are unavailable in this release");
|
|
40683
|
+
}
|
|
40684
|
+
/** Before an app's frames go to a contact: a version removed or revoked here says nothing more (WISP 1200 § Takedowns). */
|
|
40685
|
+
async appChatRunnable(ref, digest) {
|
|
40686
|
+
if (!this.appsWireOnly) await this.appStore.chatRunnable({
|
|
40687
|
+
ref,
|
|
40688
|
+
digest
|
|
40689
|
+
});
|
|
39737
40690
|
}
|
|
39738
40691
|
/** A paired 1:1 chat and the chat app id of `ref` in it, from the two pinned participation keys (WISP 1200 § In a chat). */
|
|
39739
40692
|
appChat(linkId, ref) {
|
|
@@ -39759,10 +40712,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39759
40712
|
async appOpen({ linkId, ref, version, digest }) {
|
|
39760
40713
|
const { live, app } = this.appChat(linkId, ref);
|
|
39761
40714
|
if (!isAppVersion(version)) throw new Error("Not an app version");
|
|
39762
|
-
await this.
|
|
39763
|
-
ref,
|
|
39764
|
-
digest
|
|
39765
|
-
});
|
|
40715
|
+
await this.appChatRunnable(ref, digest);
|
|
39766
40716
|
const open = live.appsOpen ??= /* @__PURE__ */ new Map();
|
|
39767
40717
|
if (live.link) live.link.openApp(app, version);
|
|
39768
40718
|
else open.set(app, version);
|
|
@@ -39778,10 +40728,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39778
40728
|
async appSend({ linkId, ref, data, digest }) {
|
|
39779
40729
|
const { live, app } = this.appChat(linkId, ref);
|
|
39780
40730
|
if (!live.appsOpen?.has(app)) return { error: "not-open" };
|
|
39781
|
-
await this.
|
|
39782
|
-
ref,
|
|
39783
|
-
digest
|
|
39784
|
-
});
|
|
40731
|
+
await this.appChatRunnable(ref, digest);
|
|
39785
40732
|
return { error: live.link ? live.link.sendAppData(app, data) : "offline" };
|
|
39786
40733
|
}
|
|
39787
40734
|
/** Every page went away: no app runs here any more, so each open one is closed (`close` goes where it can). */
|
|
@@ -40908,6 +41855,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40908
41855
|
const settings = rest;
|
|
40909
41856
|
delete settings.backupReminders;
|
|
40910
41857
|
const wasOnline = this.networkOn;
|
|
41858
|
+
if (Array.isArray(settings.iceServers)) settings.iceServers = settings.iceServers.map(iceServerSetting);
|
|
40911
41859
|
for (const server of settings.iceServers ?? []) {
|
|
40912
41860
|
const problem = iceServerProblem(server);
|
|
40913
41861
|
if (problem) throw new Error(problem);
|
|
@@ -41876,7 +42824,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41876
42824
|
this.groupWakeReceived.delete(linkId);
|
|
41877
42825
|
this.quietEdges.delete(linkId);
|
|
41878
42826
|
this.groupNativeWaiting.delete(linkId);
|
|
41879
|
-
this.
|
|
42827
|
+
this.nativeFailed.delete(linkId);
|
|
41880
42828
|
this.groupNativeRetryAt = 0;
|
|
41881
42829
|
if (live.stored.wakeToken && this.meshEdge(live.stored)) {
|
|
41882
42830
|
const group = live.stored.group, peer = live.stored.groupPeer;
|
|
@@ -41992,7 +42940,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41992
42940
|
if (this.groupEntryTimer || this.shuttingDown || this.options.deferGroups) return;
|
|
41993
42941
|
this.groupEntryTimer = setInterval(() => {
|
|
41994
42942
|
this.groups.tick().catch(() => {});
|
|
41995
|
-
this.
|
|
42943
|
+
this.retryNative();
|
|
41996
42944
|
}, 1e3);
|
|
41997
42945
|
}
|
|
41998
42946
|
stopGroupEntries() {
|
|
@@ -42035,6 +42983,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42035
42983
|
usdtPaymentsSupport: !entry,
|
|
42036
42984
|
barkPaymentsSupport: !entry,
|
|
42037
42985
|
largeFilesSupport: !entry,
|
|
42986
|
+
binaryFilesSupport: !entry,
|
|
42038
42987
|
params: stored,
|
|
42039
42988
|
rtcAvailable: typeof RTCPeerConnection !== "undefined" && !this.edgeRtcOff.has(linkId),
|
|
42040
42989
|
resume: !entry && stored.edgeLive ? resumeOn : void 0,
|
|
@@ -42140,9 +43089,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42140
43089
|
role,
|
|
42141
43090
|
packetAt: presence.lastPacketAt
|
|
42142
43091
|
});
|
|
43092
|
+
const same = samePresence(live.presence, presence);
|
|
42143
43093
|
live.presence = presence;
|
|
42144
43094
|
if (!entry && presence.nick && !live.link?.isDataLinkOpen) this.groups.edgeNick(group, peer, presence.nick);
|
|
42145
|
-
this.
|
|
43095
|
+
if (!entry) this.edgeRtcPresence(linkId, presence);
|
|
43096
|
+
if (!same) this.emitState();
|
|
42146
43097
|
},
|
|
42147
43098
|
onPairingState: (state) => {
|
|
42148
43099
|
live.pairing = state;
|
|
@@ -42199,14 +43150,44 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42199
43150
|
* never went live, and the member stayed unreachable for as long as that network lasted. The edge starts again as on an
|
|
42200
43151
|
* app with no WebRTC (WISP 902 § Transports): it runs its native endpoints and says so in its packet (`_tr`, with no
|
|
42201
43152
|
* `webrtc/1`), the member's app starts its own for it as it does for a Linux Desktop, and the two meet over Iroh
|
|
42202
|
-
* through its relay.
|
|
43153
|
+
* through its relay. Until the member comes back (`edgeRtcPresence`), or for this run of the app.
|
|
42203
43154
|
*/
|
|
42204
43155
|
async edgeWithoutRtc(linkId) {
|
|
42205
|
-
const live = this.links.get(linkId)
|
|
42206
|
-
if (!live
|
|
43156
|
+
const live = this.links.get(linkId);
|
|
43157
|
+
if (!live?.link || !live.stored.group || live.stored.groupEntry || this.shuttingDown || this.options.webrtcGroupLinks) return;
|
|
42207
43158
|
if (this.edgeRtcOff.has(linkId) || this.keepsGroupNative(live.stored) || !Object.keys(this.nativeFactories).length) return;
|
|
42208
|
-
this.edgeRtcOff.
|
|
43159
|
+
this.edgeRtcOff.set(linkId, { away: false });
|
|
42209
43160
|
traceLink(live.myPubKeyZ32, "edge-rtc-off", {});
|
|
43161
|
+
await this.restartEdge(linkId);
|
|
43162
|
+
}
|
|
43163
|
+
/**
|
|
43164
|
+
* A member whose edge went native here (`edgeWithoutRtc`) is seen online again after a packet said it was gone: the
|
|
43165
|
+
* attempt that failed may have been its app quitting in the middle of it (a one-shot CLI command, a quick restart),
|
|
43166
|
+
* not a network that blocks direct connections. Held native for this whole run, the edge said so in its packet, the
|
|
43167
|
+
* member's app followed on its next starts, and past the group links' native slots members stayed unreachable for as
|
|
43168
|
+
* long as this app ran (2026-10-09). The edge tries WebRTC again, its `_tr` with `webrtc/1` again (WISP 902 §
|
|
43169
|
+
* Transports); a network that really blocks it costs one more failed attempt per return.
|
|
43170
|
+
*/
|
|
43171
|
+
edgeRtcPresence(linkId, presence) {
|
|
43172
|
+
const off = this.edgeRtcOff.get(linkId);
|
|
43173
|
+
if (!off) return;
|
|
43174
|
+
if (!presence.online) {
|
|
43175
|
+
off.away = true;
|
|
43176
|
+
return;
|
|
43177
|
+
}
|
|
43178
|
+
if (!off.away) return;
|
|
43179
|
+
const live = this.links.get(linkId);
|
|
43180
|
+
this.edgeRtcOff.delete(linkId);
|
|
43181
|
+
if (live) traceLink(live.myPubKeyZ32, "edge-rtc-back", {});
|
|
43182
|
+
this.groupNativeWaiting.delete(linkId);
|
|
43183
|
+
this.nativeFailed.delete(linkId);
|
|
43184
|
+
this.groupNativeRetryAt = 0;
|
|
43185
|
+
this.restartEdge(linkId);
|
|
43186
|
+
}
|
|
43187
|
+
/** An edge starts again, on what `keepsGroupNative` says now. */
|
|
43188
|
+
async restartEdge(linkId) {
|
|
43189
|
+
const live = this.links.get(linkId), link = live?.link;
|
|
43190
|
+
if (!live || !link) return;
|
|
42210
43191
|
live.link = null;
|
|
42211
43192
|
await link.stop(false).catch(() => {});
|
|
42212
43193
|
if (this.links.get(linkId) !== live || live.link || this.shuttingDown || !this.networkOn) return;
|
|
@@ -42240,6 +43221,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42240
43221
|
callsSupport: this.options.callsSupport ?? typeof RTCPeerConnection !== "undefined",
|
|
42241
43222
|
callsMissing: this.options.callsUnavailable,
|
|
42242
43223
|
largeFilesSupport: true,
|
|
43224
|
+
binaryFilesSupport: true,
|
|
42243
43225
|
typingSupport: true,
|
|
42244
43226
|
reactionsSupport: this.options.reactions !== false,
|
|
42245
43227
|
editSupport: !GhostlyNode.testNoEdit(),
|
|
@@ -42391,11 +43373,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42391
43373
|
this.emitState();
|
|
42392
43374
|
},
|
|
42393
43375
|
onLiveAttempt: () => this.emitState(),
|
|
42394
|
-
onRtt: (ms) =>
|
|
42395
|
-
const log = this.transportLogOf(live);
|
|
42396
|
-
if (log?.rtt(ms)) this.saveTransportLog(live, log);
|
|
42397
|
-
else this.emitState();
|
|
42398
|
-
},
|
|
43376
|
+
onRtt: (ms) => this.noteRtt(live, ms),
|
|
42399
43377
|
onDiscoveryError: (error) => {
|
|
42400
43378
|
if ((error ?? void 0) !== live.discoveryError) {
|
|
42401
43379
|
live.discoveryError = error ?? void 0;
|
|
@@ -42428,16 +43406,17 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42428
43406
|
const was = live.status, synced = live.lastSyncAt;
|
|
42429
43407
|
live.status = status;
|
|
42430
43408
|
if (status === "online") live.lastSyncAt = Date.now();
|
|
42431
|
-
if (status !== was || !synced || stored.group || linkId === this.activeLinkId) this.emitState();
|
|
43409
|
+
if (status !== was || !synced && status === "online" || stored.group || linkId === this.activeLinkId) this.emitState();
|
|
42432
43410
|
},
|
|
42433
43411
|
onPoll: ({ polling, nextInMs }) => {
|
|
42434
|
-
const was = live.poll.polling;
|
|
43412
|
+
const was = live.poll.polling, first = !live.lastSyncAt && !live.firstReadDone;
|
|
42435
43413
|
live.poll = {
|
|
42436
43414
|
polling,
|
|
42437
43415
|
nextAt: Date.now() + nextInMs,
|
|
42438
43416
|
interval: nextInMs || live.poll.interval
|
|
42439
43417
|
};
|
|
42440
|
-
if (
|
|
43418
|
+
if (was && !polling) live.firstReadDone = true;
|
|
43419
|
+
if (linkId === this.activeLinkId || first && polling !== was) this.emitState();
|
|
42441
43420
|
},
|
|
42442
43421
|
onMessageReceipt: async (id) => {
|
|
42443
43422
|
const oneToOne = !!stored.profile && !stored.group;
|
|
@@ -42516,6 +43495,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42516
43495
|
this.emitState();
|
|
42517
43496
|
},
|
|
42518
43497
|
onPresence: (presence) => {
|
|
43498
|
+
let same = samePresence(live.presence, presence);
|
|
42519
43499
|
live.presence = presence;
|
|
42520
43500
|
if (!stored.profile && presence.nick && presence.nick !== live.stored.peerNick) {
|
|
42521
43501
|
live.stored = {
|
|
@@ -42523,8 +43503,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42523
43503
|
peerNick: presence.nick
|
|
42524
43504
|
};
|
|
42525
43505
|
db.patchLink(linkId, { peerNick: presence.nick });
|
|
43506
|
+
same = false;
|
|
42526
43507
|
}
|
|
42527
|
-
this.emitState();
|
|
43508
|
+
if (!same) this.emitState();
|
|
42528
43509
|
},
|
|
42529
43510
|
onMessage: (message) => {
|
|
42530
43511
|
if (live.stored.inviteCode) {
|
|
@@ -42719,7 +43700,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42719
43700
|
get nativeFactories() {
|
|
42720
43701
|
const relay = this.relaysHyperdht ? this.hyperdhtRelay : "";
|
|
42721
43702
|
return {
|
|
42722
|
-
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-
|
|
43703
|
+
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-CEmBpT0Q.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
|
|
42723
43704
|
...this.options.nativeTransports,
|
|
42724
43705
|
...this.ownIrohRelays ? { "iroh/1": (seedB64) => this.options.nativeTransports["iroh/1"](seedB64, this.settings.irohRelays?.length ? { relays: [...this.settings.irohRelays] } : {}) } : {},
|
|
42725
43706
|
...this.options.irohWeb ? { "iroh/1": (seedB64) => createIrohWebEndpoint(seedB64, { relays: this.irohRelays }) } : {}
|
|
@@ -42820,17 +43801,18 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42820
43801
|
return Promise.all(starts).then((full) => {
|
|
42821
43802
|
const live = this.links.get(linkId), link = live?.link;
|
|
42822
43803
|
if (this.shuttingDown || !live || !link || link !== expected) return;
|
|
42823
|
-
|
|
43804
|
+
const group = !!live.stored.group;
|
|
43805
|
+
if (group && this.keepsGroupNative(live.stored)) {
|
|
42824
43806
|
if (full.includes("no-slot") && !link.availableTransports.some((t) => t !== "webrtc/1")) this.groupNativeWaiting.add(linkId);
|
|
42825
43807
|
else this.groupNativeWaiting.delete(linkId);
|
|
42826
|
-
|
|
42827
|
-
|
|
42828
|
-
|
|
42829
|
-
|
|
42830
|
-
|
|
42831
|
-
|
|
42832
|
-
}
|
|
42833
|
-
} else this.
|
|
43808
|
+
}
|
|
43809
|
+
if ((group ? this.keepsGroupNative(live.stored) : this.keepsNativeEndpoints(linkId, live.stored)) && full.includes("failed")) {
|
|
43810
|
+
const wait = Math.min(Math.max(NATIVE_RETRY_MS, (this.nativeFailed.get(linkId)?.wait ?? 0) * 2), NATIVE_FAILED_MAX_MS);
|
|
43811
|
+
this.nativeFailed.set(linkId, {
|
|
43812
|
+
at: Date.now() + wait,
|
|
43813
|
+
wait
|
|
43814
|
+
});
|
|
43815
|
+
} else this.nativeFailed.delete(linkId);
|
|
42834
43816
|
this.emitState();
|
|
42835
43817
|
});
|
|
42836
43818
|
}
|
|
@@ -42988,25 +43970,25 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42988
43970
|
/** Group links waiting for a free native slot (`ensureNativeEndpoints`), by link id. */
|
|
42989
43971
|
groupNativeWaiting = /* @__PURE__ */ new Set();
|
|
42990
43972
|
groupNativeRetryAt = 0;
|
|
42991
|
-
/**
|
|
42992
|
-
|
|
43973
|
+
/** Chats and group links whose native listener failed to start, by link id: when each tries again, and how long it waited last. */
|
|
43974
|
+
nativeFailed = /* @__PURE__ */ new Map();
|
|
42993
43975
|
/**
|
|
42994
43976
|
* Now and then, the group links that found no free native slot try again: a chat or another group may have let one go.
|
|
42995
|
-
*
|
|
43977
|
+
* Chats and group links whose listener failed to start try again once their wait is over.
|
|
42996
43978
|
*/
|
|
42997
|
-
|
|
43979
|
+
retryNative() {
|
|
42998
43980
|
const now = Date.now();
|
|
42999
|
-
for (const [linkId, failed] of this.
|
|
43981
|
+
for (const [linkId, failed] of this.nativeFailed) {
|
|
43000
43982
|
if (now < failed.at) continue;
|
|
43001
43983
|
if (!this.links.get(linkId)?.link) {
|
|
43002
|
-
this.
|
|
43984
|
+
this.nativeFailed.delete(linkId);
|
|
43003
43985
|
continue;
|
|
43004
43986
|
}
|
|
43005
43987
|
failed.at = now + failed.wait;
|
|
43006
43988
|
this.ensureNativeEndpoints(linkId);
|
|
43007
43989
|
}
|
|
43008
43990
|
if (!this.groupNativeWaiting.size || Date.now() < this.groupNativeRetryAt) return;
|
|
43009
|
-
this.groupNativeRetryAt = Date.now() +
|
|
43991
|
+
this.groupNativeRetryAt = Date.now() + NATIVE_RETRY_MS;
|
|
43010
43992
|
for (const linkId of [...this.groupNativeWaiting]) {
|
|
43011
43993
|
if (!this.links.get(linkId)?.link) {
|
|
43012
43994
|
this.groupNativeWaiting.delete(linkId);
|
|
@@ -43111,6 +44093,24 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43111
44093
|
} else this.placedAt.set(message.linkId, Math.max(last, Math.min(message.timestamp, now)));
|
|
43112
44094
|
return message;
|
|
43113
44095
|
}
|
|
44096
|
+
/**
|
|
44097
|
+
* These rows, in this order, placed again below everything in their history, as if they came now: a group member's
|
|
44098
|
+
* messages that came above an older one of theirs (WISP 400, requirement 10), and the lines that came after them. The
|
|
44099
|
+
* places are taken before anything is awaited, so a row that comes meanwhile goes below them. Each still shows the
|
|
44100
|
+
* time it showed. All of them in one write: one disk sync, not one a row.
|
|
44101
|
+
*/
|
|
44102
|
+
async placeAgain(linkId, ids) {
|
|
44103
|
+
const places = new Map(ids.map((id) => {
|
|
44104
|
+
const timestamp = arrivalKey(Date.now(), this.placedAt.get(linkId));
|
|
44105
|
+
this.placedAt.set(linkId, timestamp);
|
|
44106
|
+
return [id, timestamp];
|
|
44107
|
+
}));
|
|
44108
|
+
const moved = await db.patchMessages(linkId, [...places.keys()], (m) => ({
|
|
44109
|
+
timestamp: places.get(m.id),
|
|
44110
|
+
sentAt: shownTime(m)
|
|
44111
|
+
}));
|
|
44112
|
+
if (moved.length) await this.messagesChanged(linkId, moved.map((m) => m.id));
|
|
44113
|
+
}
|
|
43114
44114
|
async storeMessage(message) {
|
|
43115
44115
|
await this.storeNewMessage(message);
|
|
43116
44116
|
}
|
|
@@ -43230,7 +44230,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43230
44230
|
transportWait: live.link?.transportWait,
|
|
43231
44231
|
liveAttempt: live.link?.liveAttempt,
|
|
43232
44232
|
liveDialer: live.stored.profile && !live.stored.group ? live.link?.dialer : void 0,
|
|
43233
|
-
transportRttMs: live.link?.rttMs,
|
|
44233
|
+
transportRttMs: live.rttPosted = live.link?.rttMs,
|
|
43234
44234
|
transportRelayed: live.link?.relayedPath,
|
|
43235
44235
|
transportLive: live.transportLog?.liveNow(),
|
|
43236
44236
|
transportLog: stored.id === this.activeLinkId && stored.profile && !stored.group ? stored.transportLog ?? [] : void 0,
|
|
@@ -43252,7 +44252,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43252
44252
|
lastMessageAt: live.lastMessageAt,
|
|
43253
44253
|
peerAck: live.peerAck,
|
|
43254
44254
|
lastSyncAt: live.lastSyncAt,
|
|
43255
|
-
poll: live.
|
|
44255
|
+
poll: live.firstReadDone && !live.lastSyncAt && stored.id !== this.activeLinkId ? {
|
|
44256
|
+
...live.poll,
|
|
44257
|
+
polling: false
|
|
44258
|
+
} : live.poll
|
|
43256
44259
|
};
|
|
43257
44260
|
}
|
|
43258
44261
|
/** What this chat can do right now: on the open session, or held for an away contact (text, files and requests only). */
|
|
@@ -43285,6 +44288,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43285
44288
|
this.stateTimer = null;
|
|
43286
44289
|
if (this.gatedOut) return;
|
|
43287
44290
|
this.events.onState(this.getState());
|
|
44291
|
+
this.dropBriefViews();
|
|
43288
44292
|
if (this.deviceLinks) this.syncDeviceTokens();
|
|
43289
44293
|
}, delayMs);
|
|
43290
44294
|
}
|
|
@@ -43296,11 +44300,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43296
44300
|
}
|
|
43297
44301
|
if (this.gatedOut) return;
|
|
43298
44302
|
this.events.onState(this.getState());
|
|
44303
|
+
this.dropBriefViews();
|
|
44304
|
+
}
|
|
44305
|
+
/** A state went with the brief views: they go, so the next states do not carry them (`briefViews`). */
|
|
44306
|
+
dropBriefViews() {
|
|
44307
|
+
for (const [id, view] of this.briefViews) if (this.transfers.get(id) === view) this.transfers.delete(id);
|
|
44308
|
+
this.briefViews.clear();
|
|
43299
44309
|
}
|
|
43300
44310
|
};
|
|
43301
44311
|
//#endregion
|
|
43302
44312
|
//#region ../browser/src/engine/server.ts
|
|
43303
|
-
var server_exports = /* @__PURE__ */ __exportAll({ EngineServer: () => EngineServer });
|
|
43304
44313
|
var EngineServer = class {
|
|
43305
44314
|
/** The whole engine: never made on a device that is not the active one (`devices/peer.ts` decides, WISP 06 § The gate). */
|
|
43306
44315
|
gated = false;
|
|
@@ -43372,6 +44381,10 @@ var EngineServer = class {
|
|
|
43372
44381
|
get historyMark() {
|
|
43373
44382
|
return this.changes;
|
|
43374
44383
|
}
|
|
44384
|
+
/** Whether a chat's or group's history changed after `mark` (`historyMark`); with none, since this engine was made. */
|
|
44385
|
+
historyChanged(id, mark = 0) {
|
|
44386
|
+
return (this.changedAt.get(id) ?? 0) > mark;
|
|
44387
|
+
}
|
|
43375
44388
|
/**
|
|
43376
44389
|
* `read`: the chats and groups whose whole history the client read itself, after `mark` (`historyMark`). One that has
|
|
43377
44390
|
* not changed since is not read and sent again: the client hears only what changes in it from now on.
|
|
@@ -43534,6 +44547,6 @@ function signalKind(signal) {
|
|
|
43534
44547
|
}
|
|
43535
44548
|
}
|
|
43536
44549
|
//#endregion
|
|
43537
|
-
export { EngineServer
|
|
44550
|
+
export { EngineServer };
|
|
43538
44551
|
|
|
43539
|
-
//# sourceMappingURL=server-
|
|
44552
|
+
//# sourceMappingURL=server-CBCEYKMN.js.map
|