@ghostlytools/cli 1.2.4 → 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 +16 -6
- 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-CdnKGDbg.js → arkade-D4BVJFEQ.js} +2 -2
- package/dist/assets/{arkade-CdnKGDbg.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/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/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
- 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-C1p0nkle.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-BH6s8HVi.js → mainline-D5D5C-jT.js} +11 -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-0zfNpABQ.js → server-CBCEYKMN.js} +1103 -235
- package/dist/assets/server-CBCEYKMN.js.map +1 -0
- package/dist/assets/{src-CbLkHfz8.js → src-DhfGgAUu.js} +836 -2265
- 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-Qf4UuygS.js → usdt-C1Xao2xH.js} +3 -3
- package/dist/assets/{usdt-Qf4UuygS.js.map → usdt-C1Xao2xH.js.map} +1 -1
- package/dist/ghostly.mjs +4 -22077
- 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-C1p0nkle.js.map +0 -1
- package/dist/assets/idb-CTF6LF5B.js.map +0 -1
- package/dist/assets/mainline-BH6s8HVi.js.map +0 -1
- package/dist/assets/rolldown-runtime-8H4AJuhK.js +0 -14
- package/dist/assets/server-0zfNpABQ.js.map +0 -1
- package/dist/assets/src-CbLkHfz8.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, least recently used first: the ones running are read once, not on every `file`. */
|
|
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,11 +11774,13 @@ 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
11780
|
if (kept) {
|
|
11603
11781
|
this.verified.delete(app.digest);
|
|
11604
11782
|
this.verified.set(app.digest, kept);
|
|
11783
|
+
this.dropUnusedBundles();
|
|
11605
11784
|
return Promise.resolve(kept);
|
|
11606
11785
|
}
|
|
11607
11786
|
const under = this.verifying.get(app.digest);
|
|
@@ -11609,11 +11788,10 @@ var Apps = class {
|
|
|
11609
11788
|
const reading = (async () => {
|
|
11610
11789
|
const bytes = await this.readBundleBytes(app.bytes, app.digest);
|
|
11611
11790
|
if (!bytes) fail("needs-files", "This app's files are not on this device yet");
|
|
11612
|
-
const read =
|
|
11791
|
+
const read = await readAppBundleAsync(bytes);
|
|
11613
11792
|
if (!read.ok || read.bundle.digest !== app.digest) fail("damaged", "This app's stored files do not match what was installed");
|
|
11614
11793
|
this.verified.set(app.digest, read.bundle);
|
|
11615
|
-
|
|
11616
|
-
while (this.verified.size > room) this.verified.delete(this.verified.keys().next().value);
|
|
11794
|
+
this.dropUnusedBundles();
|
|
11617
11795
|
this.icons.set(app.digest, read.bundle.files.get("icon.png")?.slice() ?? null);
|
|
11618
11796
|
return read.bundle;
|
|
11619
11797
|
})().finally(() => {
|
|
@@ -11622,9 +11800,21 @@ var Apps = class {
|
|
|
11622
11800
|
this.verifying.set(app.digest, reading);
|
|
11623
11801
|
return reading;
|
|
11624
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
|
+
}
|
|
11625
11815
|
async fetchBundle(url) {
|
|
11626
11816
|
const bytes = await this.host.fetch(url, { maxBytes: APP_BUNDLE_LIMITS.bundleBytes });
|
|
11627
|
-
const read =
|
|
11817
|
+
const read = await readAppBundleAsync(bytes);
|
|
11628
11818
|
if (!read.ok) fail(read.reason, `This is not an app Ghostly can install${read.detail ? ` (${read.detail})` : ""}`);
|
|
11629
11819
|
return {
|
|
11630
11820
|
bundle: read.bundle,
|
|
@@ -11640,7 +11830,7 @@ var Apps = class {
|
|
|
11640
11830
|
*/
|
|
11641
11831
|
async readStoreAt(url, heldKey, held) {
|
|
11642
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 })]);
|
|
11643
|
-
if (held?.index && held.digest !== void 0 && toBase64Url(
|
|
11833
|
+
if (held?.index && held.digest !== void 0 && toBase64Url(await sha256Paced(indexBytes)) === held.digest) return {
|
|
11644
11834
|
ok: true,
|
|
11645
11835
|
store: {
|
|
11646
11836
|
index: held.index,
|
|
@@ -11649,14 +11839,11 @@ var Apps = class {
|
|
|
11649
11839
|
}
|
|
11650
11840
|
};
|
|
11651
11841
|
const steps = readAppStoreSteps(indexBytes, sigBytes, this.nowS(), heldKey, this.checkedRevocations);
|
|
11652
|
-
|
|
11842
|
+
const pause = pacer(REVOCATIONS_SLICE_MS);
|
|
11653
11843
|
for (;;) {
|
|
11654
11844
|
const step = steps.next();
|
|
11655
11845
|
if (step.done) return step.value;
|
|
11656
|
-
|
|
11657
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
11658
|
-
since = performance.now();
|
|
11659
|
-
}
|
|
11846
|
+
await pause();
|
|
11660
11847
|
}
|
|
11661
11848
|
}
|
|
11662
11849
|
/**
|
|
@@ -11680,19 +11867,16 @@ var Apps = class {
|
|
|
11680
11867
|
* engine shares the page's thread, and each entry is a signature check. The same bytes are not checked again.
|
|
11681
11868
|
*/
|
|
11682
11869
|
async readRevocationsFile(bytes) {
|
|
11683
|
-
const id = toBase64Url(
|
|
11870
|
+
const id = toBase64Url(await sha256Paced(bytes));
|
|
11684
11871
|
const known = this.revocationFiles.get(id);
|
|
11685
|
-
if (known !== void 0) return known;
|
|
11872
|
+
if (known !== void 0) return known.revocations;
|
|
11686
11873
|
const read = readCanonicalJson(bytes);
|
|
11687
11874
|
let out = null;
|
|
11688
11875
|
if (read.ok && Array.isArray(read.value) && read.value.length <= APP_STATEMENT_LIMITS.revocations) {
|
|
11689
11876
|
out = [];
|
|
11690
|
-
|
|
11877
|
+
const pause = pacer(REVOCATIONS_SLICE_MS);
|
|
11691
11878
|
for (const entry of read.value) {
|
|
11692
|
-
|
|
11693
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
11694
|
-
since = performance.now();
|
|
11695
|
-
}
|
|
11879
|
+
await pause();
|
|
11696
11880
|
const one = readAppRevocation(entry);
|
|
11697
11881
|
if (!one.ok) {
|
|
11698
11882
|
out = null;
|
|
@@ -11702,8 +11886,17 @@ var Apps = class {
|
|
|
11702
11886
|
}
|
|
11703
11887
|
}
|
|
11704
11888
|
this.revocationFiles.delete(id);
|
|
11705
|
-
this.revocationFiles.set(id,
|
|
11706
|
-
|
|
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
|
+
}
|
|
11707
11900
|
return out;
|
|
11708
11901
|
}
|
|
11709
11902
|
summary(s) {
|
|
@@ -11881,6 +12074,12 @@ var Apps = class {
|
|
|
11881
12074
|
let urls;
|
|
11882
12075
|
let expect = null;
|
|
11883
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
|
+
}
|
|
11884
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")];
|
|
11885
12084
|
else if ("store" in source) {
|
|
11886
12085
|
const s = await this.storeRecord(source.store);
|
|
@@ -11898,6 +12097,11 @@ var Apps = class {
|
|
|
11898
12097
|
const { ref, url, sequence, digest } = source.card;
|
|
11899
12098
|
if (!isAppRef(ref)) fail("bad-ref", "Not an app reference");
|
|
11900
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
|
+
}
|
|
11901
12105
|
urls = [url];
|
|
11902
12106
|
expect = {
|
|
11903
12107
|
ref,
|
|
@@ -11929,7 +12133,7 @@ var Apps = class {
|
|
|
11929
12133
|
const oldest = this.staged.values().next().value;
|
|
11930
12134
|
if (oldest) this.sweep = setTimeout(() => this.sweepStaged(), STAGED.ms - (this.now() - oldest.at));
|
|
11931
12135
|
}
|
|
11932
|
-
async stage(bundle, bytes, from, fromStore) {
|
|
12136
|
+
async stage(bundle, bytes, from, fromStore, carried = false) {
|
|
11933
12137
|
for (const [digest, s] of this.staged) if (this.now() - s.at > STAGED.ms) this.staged.delete(digest);
|
|
11934
12138
|
this.staged.delete(bundle.digest);
|
|
11935
12139
|
while (this.staged.size >= STAGED.max) this.staged.delete(this.staged.keys().next().value);
|
|
@@ -11960,7 +12164,8 @@ var Apps = class {
|
|
|
11960
12164
|
asks,
|
|
11961
12165
|
run: this.runStatus(version, this.storeViews(stores), installed?.revocations),
|
|
11962
12166
|
listedBy,
|
|
11963
|
-
unknownPublisher: !listedBy.some((s) => s.kind === "curated")
|
|
12167
|
+
unknownPublisher: !listedBy.some((s) => s.kind === "curated"),
|
|
12168
|
+
...carried && { carried: true }
|
|
11964
12169
|
};
|
|
11965
12170
|
}
|
|
11966
12171
|
/**
|
|
@@ -12014,6 +12219,7 @@ var Apps = class {
|
|
|
12014
12219
|
const kind = await this.writeBundle(digest, bytes);
|
|
12015
12220
|
const next = this.versionRecord(installed, bundle, kind, from, fromStore);
|
|
12016
12221
|
await this.putApp(next);
|
|
12222
|
+
if (!installed && this.builtIn(version.ref)) await this.keepBuiltIn(version.ref);
|
|
12017
12223
|
if (installed && installed.digest !== digest) await this.removeBundle(installed.bytes, installed.digest);
|
|
12018
12224
|
if (installed?.pending && next.pending?.digest !== installed.pending.digest && installed.pending.digest !== digest) await this.removeBundle(installed.pending.bytes, installed.pending.digest);
|
|
12019
12225
|
this.staged.delete(digest);
|
|
@@ -12052,7 +12258,7 @@ var Apps = class {
|
|
|
12052
12258
|
const installed = await this.installedOrFail(ref);
|
|
12053
12259
|
const pending = installed.pending ?? fail("no-update", "No update waits for this app");
|
|
12054
12260
|
const bytes = await this.readBundleBytes(pending.bytes, pending.digest);
|
|
12055
|
-
const read = bytes ?
|
|
12261
|
+
const read = bytes ? await readAppBundleAsync(bytes) : null;
|
|
12056
12262
|
if (!read?.ok || read.bundle.digest !== pending.digest) {
|
|
12057
12263
|
const { pending: _p, ...rest } = installed;
|
|
12058
12264
|
await this.putApp(rest);
|
|
@@ -12287,6 +12493,26 @@ var Apps = class {
|
|
|
12287
12493
|
const app = await this.installedOrFail(ref);
|
|
12288
12494
|
const stores = await this.stores();
|
|
12289
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
|
+
}
|
|
12290
12516
|
const urls = [app.from];
|
|
12291
12517
|
for (const s of stores) {
|
|
12292
12518
|
if (app.store !== void 0 && s.key !== app.store) continue;
|
|
@@ -12294,6 +12520,14 @@ var Apps = class {
|
|
|
12294
12520
|
}
|
|
12295
12521
|
if (app.store === void 0) urls.push(...app.manifest.sources ?? []);
|
|
12296
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
|
+
}
|
|
12297
12531
|
let fetched;
|
|
12298
12532
|
try {
|
|
12299
12533
|
fetched = await this.fetchBundle(url);
|
|
@@ -12341,7 +12575,7 @@ var Apps = class {
|
|
|
12341
12575
|
if (run.status === "removed" && runAnyway !== true) fail("removed", `Removed by ${run.by[0].name}: ${run.by[0].reason}`);
|
|
12342
12576
|
if (run.status === "needs-files") fail("needs-files", "This app's files are not on this device yet");
|
|
12343
12577
|
if (run.status === "removed") this.anyway.add(`${app.ref} ${app.digest}`);
|
|
12344
|
-
const bundle = await this.verifiedBundle(app);
|
|
12578
|
+
const bundle = await this.verifiedBundle(app, true);
|
|
12345
12579
|
const entry = bundle.files.get(bundle.manifest.entry) ?? fail("damaged", "The entry is missing");
|
|
12346
12580
|
this.ran.add(app.ref);
|
|
12347
12581
|
const versions = this.started.get(app.ref) ?? /* @__PURE__ */ new Map();
|
|
@@ -12357,6 +12591,29 @@ var Apps = class {
|
|
|
12357
12591
|
};
|
|
12358
12592
|
}
|
|
12359
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
|
+
/**
|
|
12360
12617
|
* A file of the installed bundle, for the broker's `file` (`ghostly.file(path)`): a copy, which the broker transfers to
|
|
12361
12618
|
* the app as it is. `digest`: the version the frame runs.
|
|
12362
12619
|
*/
|
|
@@ -12366,7 +12623,7 @@ var Apps = class {
|
|
|
12366
12623
|
const icon = path === "icon.png" ? this.icons.get(app.digest) : void 0;
|
|
12367
12624
|
if (icon) return icon.slice();
|
|
12368
12625
|
if (icon === null) fail("no-file", "No such file in this app");
|
|
12369
|
-
const bundle = await this.verifiedBundle(app);
|
|
12626
|
+
const bundle = await this.verifiedBundle(app, path !== APP_ICON_PATH);
|
|
12370
12627
|
if (typeof path !== "string" || !bundle.files.has(path)) fail("no-file", "No such file in this app");
|
|
12371
12628
|
return bundle.files.get(path).slice();
|
|
12372
12629
|
}
|
|
@@ -12397,6 +12654,7 @@ var Apps = class {
|
|
|
12397
12654
|
run: await this.runCheckOf(app, stores),
|
|
12398
12655
|
listedBy,
|
|
12399
12656
|
unknownPublisher: !listedBy.some((s) => s.kind === "curated"),
|
|
12657
|
+
...this.builtIn(app.ref)?.digest === app.digest && { builtIn: true },
|
|
12400
12658
|
...app.pending && { pending: {
|
|
12401
12659
|
sequence: app.pending.sequence,
|
|
12402
12660
|
version: app.pending.manifest.version,
|
|
@@ -12416,23 +12674,44 @@ var Apps = class {
|
|
|
12416
12674
|
}
|
|
12417
12675
|
/**
|
|
12418
12676
|
* Uninstalls an app: its bundle (and a waiting update's), its record and its storage in every scope. The person chose
|
|
12419
|
-
* 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).
|
|
12420
12679
|
*/
|
|
12421
12680
|
async uninstall({ ref }) {
|
|
12422
|
-
|
|
12423
|
-
const
|
|
12424
|
-
|
|
12425
|
-
|
|
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
|
+
});
|
|
12426
12695
|
try {
|
|
12427
12696
|
await done(tx);
|
|
12428
12697
|
} finally {
|
|
12429
|
-
this.held.delete(
|
|
12698
|
+
this.held.delete(ref);
|
|
12430
12699
|
}
|
|
12431
|
-
for (const key of [...this.usage.keys()]) if (key.startsWith(`${
|
|
12432
|
-
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;
|
|
12433
12703
|
await this.removeBundle(app.bytes, app.digest);
|
|
12434
12704
|
if (app.pending) await this.removeBundle(app.pending.bytes, app.pending.digest);
|
|
12435
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
|
+
}
|
|
12436
12715
|
scopeOf(scope) {
|
|
12437
12716
|
if (scope === "alone" || typeof scope === "string" && this.host.isChat(scope)) return scope;
|
|
12438
12717
|
fail("bad-scope", "Not a chat of this profile");
|
|
@@ -12451,13 +12730,24 @@ var Apps = class {
|
|
|
12451
12730
|
});
|
|
12452
12731
|
return run;
|
|
12453
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
|
+
*/
|
|
12454
12738
|
async usageOf(ref, scope) {
|
|
12455
12739
|
const id = `${ref}\0${scope}`;
|
|
12456
|
-
|
|
12457
|
-
if (
|
|
12458
|
-
|
|
12459
|
-
|
|
12460
|
-
|
|
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);
|
|
12461
12751
|
return used;
|
|
12462
12752
|
}
|
|
12463
12753
|
async storageGet({ ref, scope, key, digest }) {
|
|
@@ -12471,8 +12761,8 @@ var Apps = class {
|
|
|
12471
12761
|
}
|
|
12472
12762
|
/**
|
|
12473
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
|
|
12474
|
-
*
|
|
12475
|
-
*
|
|
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.
|
|
12476
12766
|
*/
|
|
12477
12767
|
async storageSet({ ref, scope, key, value, digest }) {
|
|
12478
12768
|
const app = await this.runningOrFail(ref, digest);
|
|
@@ -12495,20 +12785,24 @@ var Apps = class {
|
|
|
12495
12785
|
where,
|
|
12496
12786
|
name
|
|
12497
12787
|
]));
|
|
12498
|
-
const next =
|
|
12499
|
-
|
|
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)");
|
|
12500
12793
|
const writing = await store(STORES$1.appStorage, "readwrite");
|
|
12501
12794
|
if (where !== "alone" && !this.host.isChat(where)) {
|
|
12502
12795
|
this.usage.delete(`${app.ref}\0${where}`);
|
|
12503
12796
|
fail("bad-scope", "Not a chat of this profile");
|
|
12504
12797
|
}
|
|
12505
|
-
|
|
12798
|
+
writing.put({
|
|
12506
12799
|
ref: app.ref,
|
|
12507
12800
|
scope: where,
|
|
12508
12801
|
key: name,
|
|
12509
12802
|
value: text,
|
|
12510
12803
|
size
|
|
12511
|
-
})
|
|
12804
|
+
});
|
|
12805
|
+
await wrap(writing.put(counterRow(app.ref, where, next)));
|
|
12512
12806
|
this.usage.set(`${app.ref}\0${where}`, next);
|
|
12513
12807
|
});
|
|
12514
12808
|
}
|
|
@@ -12523,39 +12817,73 @@ var Apps = class {
|
|
|
12523
12817
|
name
|
|
12524
12818
|
]));
|
|
12525
12819
|
if (!old) return;
|
|
12526
|
-
|
|
12820
|
+
const next = plus(used, countOf(old, -1));
|
|
12821
|
+
const writing = await store(STORES$1.appStorage, "readwrite");
|
|
12822
|
+
writing.delete([
|
|
12527
12823
|
app.ref,
|
|
12528
12824
|
where,
|
|
12529
12825
|
name
|
|
12530
|
-
])
|
|
12531
|
-
|
|
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);
|
|
12532
12833
|
});
|
|
12533
12834
|
}
|
|
12534
12835
|
async storageKeys({ ref, scope, digest }) {
|
|
12535
12836
|
const app = await this.runningOrFail(ref, digest);
|
|
12536
|
-
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]);
|
|
12537
12838
|
}
|
|
12538
12839
|
/** An app's storage in every scope, as export files (WISP 1200 § Where installed apps live), offered before an uninstall. */
|
|
12539
12840
|
async exportData({ ref }) {
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
const scopes = /* @__PURE__ */ new Map();
|
|
12543
|
-
for (const row of rows) {
|
|
12544
|
-
const out = scopes.get(row.scope) ?? {
|
|
12841
|
+
return (await this.exportDataText({ ref })).map(({ app, scope, entries }) => {
|
|
12842
|
+
const out = {
|
|
12545
12843
|
ghostlyAppData: 1,
|
|
12546
|
-
app
|
|
12547
|
-
scope
|
|
12844
|
+
app,
|
|
12845
|
+
scope,
|
|
12548
12846
|
entries: {}
|
|
12549
12847
|
};
|
|
12550
|
-
Object.defineProperty(out.entries,
|
|
12551
|
-
value: JSON.parse(
|
|
12848
|
+
for (const [key, json] of entries) Object.defineProperty(out.entries, key, {
|
|
12849
|
+
value: JSON.parse(json),
|
|
12552
12850
|
enumerable: true,
|
|
12553
12851
|
writable: true,
|
|
12554
12852
|
configurable: true
|
|
12555
12853
|
});
|
|
12556
|
-
|
|
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
|
+
});
|
|
12557
12885
|
}
|
|
12558
|
-
return
|
|
12886
|
+
return out;
|
|
12559
12887
|
}
|
|
12560
12888
|
};
|
|
12561
12889
|
//#endregion
|
|
@@ -16680,7 +17008,7 @@ function handoffNotices(was, handoff, session) {
|
|
|
16680
17008
|
* device link must offer every transport both devices run: a Linux Desktop has no WebRTC, and an Iroh relay that
|
|
16681
17009
|
* one side cannot reach leaves HyperDHT as the only way through.
|
|
16682
17010
|
*/
|
|
16683
|
-
var NATIVE_RETRY_MS = 1e4;
|
|
17011
|
+
var NATIVE_RETRY_MS$1 = 1e4;
|
|
16684
17012
|
var NATIVE_RETRY_MAX_MS = 12e4;
|
|
16685
17013
|
/** How often a standby reads the turn while its screen shows (WISP 06 § When a device checks), and the jitter on it. */
|
|
16686
17014
|
var STANDBY_TURN_EVERY_MS = 6e5;
|
|
@@ -16989,7 +17317,7 @@ var DeviceLinks = class {
|
|
|
16989
17317
|
}
|
|
16990
17318
|
retryNative(running) {
|
|
16991
17319
|
if (running.nativeRetry || this.stopped || this.running.get(running.id) !== running) return;
|
|
16992
|
-
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);
|
|
16993
17321
|
running.nativeMisses++;
|
|
16994
17322
|
running.nativeRetry = setTimeout(() => {
|
|
16995
17323
|
running.nativeRetry = void 0;
|
|
@@ -19363,6 +19691,7 @@ var FileDesk = class {
|
|
|
19363
19691
|
openSource: (record) => this.openSource(linkId, record),
|
|
19364
19692
|
changed: (record, transferred, progress) => this.changed(linkId, record, transferred, progress),
|
|
19365
19693
|
room: async () => (await fileBytes()).room(),
|
|
19694
|
+
...this.deps.sendData && { sendData: (id, offset, data) => this.deps.sendData(linkId, id, offset, data) },
|
|
19366
19695
|
writable: () => this.deps.writable?.(linkId)
|
|
19367
19696
|
});
|
|
19368
19697
|
this.chats.set(linkId, made);
|
|
@@ -19382,6 +19711,7 @@ var FileDesk = class {
|
|
|
19382
19711
|
records.push(file.wire3);
|
|
19383
19712
|
if (file.wire3.direction === "in") chat.local.set(file.wire3.id, file.id);
|
|
19384
19713
|
if (!transferEnded(file.wire3)) this.restored.add(file.id);
|
|
19714
|
+
if (file.wire3.state === "done") continue;
|
|
19385
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())));
|
|
19386
19716
|
}
|
|
19387
19717
|
chat.files.restore(records);
|
|
@@ -19822,7 +20152,7 @@ var FileDesk = class {
|
|
|
19822
20152
|
view.room = this.deps.transfers.get(id)?.room;
|
|
19823
20153
|
this.room(id);
|
|
19824
20154
|
}
|
|
19825
|
-
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);
|
|
19826
20156
|
if (!transferEnded(record)) {
|
|
19827
20157
|
this.underway.add(id);
|
|
19828
20158
|
this.ended.delete(id);
|
|
@@ -20350,6 +20680,10 @@ var Outbox = class {
|
|
|
20350
20680
|
get sameLink() {
|
|
20351
20681
|
return !this.stopped && this.closed === this.began;
|
|
20352
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
|
+
}
|
|
20353
20687
|
/** A new message a turn could not send (the link closed under it, or the ones before it still wait): it waits too. */
|
|
20354
20688
|
async waitsItsTurn(message) {
|
|
20355
20689
|
await this.store.update(message.id, "waiting", "Waits for the messages before it to go.", {
|
|
@@ -20454,7 +20788,7 @@ var Outbox = class {
|
|
|
20454
20788
|
this.busy.add(id);
|
|
20455
20789
|
let again;
|
|
20456
20790
|
try {
|
|
20457
|
-
let message = read ??
|
|
20791
|
+
let message = read ?? await this.one(id);
|
|
20458
20792
|
if (!message?.wireId || !message.delivery || message.delivery === "delivered") return;
|
|
20459
20793
|
const wait = this.waiting.get(id);
|
|
20460
20794
|
if (wait?.timer) {
|
|
@@ -20495,7 +20829,7 @@ var Outbox = class {
|
|
|
20495
20829
|
again = this.reopenedWhileBusy.delete(id);
|
|
20496
20830
|
}
|
|
20497
20831
|
if (again) {
|
|
20498
|
-
const message =
|
|
20832
|
+
const message = await this.one(id);
|
|
20499
20833
|
if (message) await this.again(message);
|
|
20500
20834
|
}
|
|
20501
20835
|
}
|
|
@@ -20505,7 +20839,7 @@ var Outbox = class {
|
|
|
20505
20839
|
* nothing about it is unconfirmed; it counts no attempt until it goes.
|
|
20506
20840
|
*/
|
|
20507
20841
|
async wait(id, reason) {
|
|
20508
|
-
const message =
|
|
20842
|
+
const message = await this.one(id);
|
|
20509
20843
|
if (!message || this.stopped) return;
|
|
20510
20844
|
await this.store.update(id, "waiting", reason, { resendUntil: message.resendUntil ?? this.now() + this.policy.windowMs });
|
|
20511
20845
|
this.schedule(id);
|
|
@@ -20595,7 +20929,7 @@ var Outbox = class {
|
|
|
20595
20929
|
*/
|
|
20596
20930
|
async attempt(id, passed) {
|
|
20597
20931
|
if (this.stopped || this.busy.has(id)) return true;
|
|
20598
|
-
const message =
|
|
20932
|
+
const message = await this.one(id);
|
|
20599
20933
|
if (!message || message.delivery !== "queued" && message.delivery !== "waiting" || message.via === "hold") {
|
|
20600
20934
|
this.forget(id);
|
|
20601
20935
|
return true;
|
|
@@ -20622,7 +20956,7 @@ var Outbox = class {
|
|
|
20622
20956
|
}
|
|
20623
20957
|
/** After a failed attempt: queued again, unless its window or its attempts are spent. */
|
|
20624
20958
|
async requeue(id, reason) {
|
|
20625
|
-
const message =
|
|
20959
|
+
const message = await this.one(id);
|
|
20626
20960
|
if (message && message.delivery !== "delivered") await this.queue(message, reason);
|
|
20627
20961
|
}
|
|
20628
20962
|
async queue(message, reason) {
|
|
@@ -20670,7 +21004,7 @@ var Outbox = class {
|
|
|
20670
21004
|
this.turn(async () => {
|
|
20671
21005
|
if (this.receipts.get(id) !== timer) return;
|
|
20672
21006
|
this.receipts.delete(id);
|
|
20673
|
-
const message =
|
|
21007
|
+
const message = await this.one(id);
|
|
20674
21008
|
if (message?.via === "pkarr") await this.expired(message);
|
|
20675
21009
|
else await this.requeue(id, "No receipt received yet.");
|
|
20676
21010
|
}).catch(() => {});
|
|
@@ -21357,8 +21691,9 @@ var db = {
|
|
|
21357
21691
|
tx.onerror = tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("Link storage failed"));
|
|
21358
21692
|
});
|
|
21359
21693
|
},
|
|
21360
|
-
|
|
21361
|
-
|
|
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);
|
|
21362
21697
|
const links = tx.objectStore(STORES$1.links);
|
|
21363
21698
|
const request = links.get(linkId);
|
|
21364
21699
|
request.onsuccess = () => {
|
|
@@ -21619,6 +21954,32 @@ var db = {
|
|
|
21619
21954
|
});
|
|
21620
21955
|
return result;
|
|
21621
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
|
+
},
|
|
21622
21983
|
async deleteMessage(linkId, messageId) {
|
|
21623
21984
|
await wrap((await store(STORES$1.messages, "readwrite")).delete([linkId, messageId]));
|
|
21624
21985
|
},
|
|
@@ -22021,6 +22382,202 @@ async function myPin(host, chat, messageId, remove, current, now = Date.now()) {
|
|
|
22021
22382
|
};
|
|
22022
22383
|
}
|
|
22023
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
|
|
22024
22581
|
//#region ../browser/src/engine/hubClocks.ts
|
|
22025
22582
|
/**
|
|
22026
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
|
|
@@ -22274,11 +22831,14 @@ var Communities = class {
|
|
|
22274
22831
|
/** My frames an edge took, and which of them carry an edit (`frame id → editKey`). */
|
|
22275
22832
|
frames = new FramesTaken();
|
|
22276
22833
|
carriers = /* @__PURE__ */ new Map();
|
|
22834
|
+
/** The lines of each history among the messages of their moment (`GroupLineOrder`). */
|
|
22835
|
+
lineOrder;
|
|
22277
22836
|
constructor(host, store, timings = COMMUNITY_TIMINGS, random = Math.random) {
|
|
22278
22837
|
this.host = host;
|
|
22279
22838
|
this.store = store;
|
|
22280
22839
|
this.timings = timings;
|
|
22281
22840
|
this.random = random;
|
|
22841
|
+
this.lineOrder = new GroupLineOrder(host);
|
|
22282
22842
|
}
|
|
22283
22843
|
/** Application frames and pair payloads handed to the engine, one after the other per group. */
|
|
22284
22844
|
deliveries = /* @__PURE__ */ new Map();
|
|
@@ -22377,6 +22937,7 @@ var Communities = class {
|
|
|
22377
22937
|
accepted: true,
|
|
22378
22938
|
viaLink: true,
|
|
22379
22939
|
stage: this.joinStage(group),
|
|
22940
|
+
since: group.joining.since,
|
|
22380
22941
|
...this.knockingWith.get(group.id) ? { waiting: this.knockingWith.get(group.id) } : {}
|
|
22381
22942
|
} : void 0;
|
|
22382
22943
|
if (invitation && !this.keptWhileKnocking(live)) return [{
|
|
@@ -22607,6 +23168,21 @@ var Communities = class {
|
|
|
22607
23168
|
await s.leave();
|
|
22608
23169
|
await this.forget(groupId);
|
|
22609
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
|
+
}
|
|
22610
23186
|
successor(groupId) {
|
|
22611
23187
|
const s = this.live.get(groupId)?.session;
|
|
22612
23188
|
if (!s) return void 0;
|
|
@@ -22677,6 +23253,7 @@ var Communities = class {
|
|
|
22677
23253
|
this.lastMentionAt.delete(groupId);
|
|
22678
23254
|
this.knockingWith.delete(groupId);
|
|
22679
23255
|
this.knockRead.delete(groupId);
|
|
23256
|
+
this.lineOrder.forget(MESSAGE_LINK$1(groupId));
|
|
22680
23257
|
for (const linkId of [...this.host.edges(groupId).values(), ...this.host.entries(groupId).values()]) await this.host.closeEdge(linkId);
|
|
22681
23258
|
await this.store.deleteGroup(groupId);
|
|
22682
23259
|
await this.files?.drop(groupId);
|
|
@@ -23763,6 +24340,11 @@ var Communities = class {
|
|
|
23763
24340
|
sender: m.sender,
|
|
23764
24341
|
file: m.file
|
|
23765
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());
|
|
23766
24348
|
const stored = await this.host.storeMessage({
|
|
23767
24349
|
linkId: MESSAGE_LINK$1(id),
|
|
23768
24350
|
id: m.id,
|
|
@@ -23777,6 +24359,7 @@ var Communities = class {
|
|
|
23777
24359
|
...m.card && { card: m.card },
|
|
23778
24360
|
...file && { file }
|
|
23779
24361
|
});
|
|
24362
|
+
placing(stored !== false);
|
|
23780
24363
|
const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, arrivalNow(this.now()));
|
|
23781
24364
|
this.lastMessageAt.set(id, Math.max(this.lastMessageAt.get(id) ?? 0, came));
|
|
23782
24365
|
if (mentioned) this.lastMentionAt.set(id, Math.max(this.lastMentionAt.get(id) ?? 0, came));
|
|
@@ -23873,7 +24456,16 @@ var Communities = class {
|
|
|
23873
24456
|
else if (s.status === "active" && before !== after) {
|
|
23874
24457
|
const at = receivedTimestamp(top.ts, when);
|
|
23875
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);
|
|
23876
|
-
|
|
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
|
+
}
|
|
23877
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);
|
|
23878
24470
|
if (top.k === "rotate") await this.event(groupId, "rotated", "Keys rotated: a fresh epoch", at, top.e);
|
|
23879
24471
|
}
|
|
@@ -23882,16 +24474,21 @@ var Communities = class {
|
|
|
23882
24474
|
/** A line in the group's history. Its id carries its time: two lines at the same moment get distinct ones (`eventTime`). */
|
|
23883
24475
|
async event(groupId, event, text, timestamp, epoch, member) {
|
|
23884
24476
|
timestamp = eventTime(this.eventTimes, groupId, timestamp);
|
|
23885
|
-
|
|
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({
|
|
23886
24483
|
linkId: MESSAGE_LINK$1(groupId),
|
|
23887
|
-
id
|
|
24484
|
+
id,
|
|
23888
24485
|
text,
|
|
23889
24486
|
sender: "peer",
|
|
23890
24487
|
event,
|
|
23891
24488
|
member,
|
|
23892
24489
|
timestamp,
|
|
23893
24490
|
via: "datalink"
|
|
23894
|
-
});
|
|
24491
|
+
}) !== false);
|
|
23895
24492
|
}
|
|
23896
24493
|
eventTimes = /* @__PURE__ */ new Map();
|
|
23897
24494
|
};
|
|
@@ -24899,6 +25496,11 @@ var INVITED_KEPT_MS = LEFT_KEPT_MS;
|
|
|
24899
25496
|
*/
|
|
24900
25497
|
var ACCEPT_AGAIN_MS = 6e4;
|
|
24901
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;
|
|
24902
25504
|
/** Invitee side: the invitations one contact chat may have waiting here at a time. More are answered as declined. */
|
|
24903
25505
|
var MAX_INVITATIONS_PER_CHAT = 4;
|
|
24904
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. */
|
|
@@ -24915,9 +25517,13 @@ var FAREWELL_ANSWERS = 3;
|
|
|
24915
25517
|
var FAREWELL_COMMITS = 64;
|
|
24916
25518
|
/**
|
|
24917
25519
|
* A member whose messages were just asked of another (`Groups.askOf`) is told where I am only once that one's answer
|
|
24918
|
-
* 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
|
|
24919
25521
|
* answer raced the other's, which holds the same messages among everyone's in the order they were written, and a
|
|
24920
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.
|
|
24921
25527
|
*/
|
|
24922
25528
|
var MESH_SYNC_HOLD_MS = 1500;
|
|
24923
25529
|
/** Where a member's gossip turns start among the members it is connected to: a number of its own, from its key. */
|
|
@@ -25031,6 +25637,8 @@ var Groups = class {
|
|
|
25031
25637
|
syncHeld = /* @__PURE__ */ new Map();
|
|
25032
25638
|
/** Per mesh group and member, when a message last came over its edge (its own, or one it handed on). */
|
|
25033
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();
|
|
25034
25642
|
lastGossip = /* @__PURE__ */ new Map();
|
|
25035
25643
|
/**
|
|
25036
25644
|
* Per mesh group, each reachable member some of whose messages never reached me (`GroupSession.lacking`): what was
|
|
@@ -25062,6 +25670,10 @@ var Groups = class {
|
|
|
25062
25670
|
communities;
|
|
25063
25671
|
/** My messages and edits an edge of a private group took. */
|
|
25064
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;
|
|
25065
25677
|
/** Hubs of the private groups past 16 members (WISP 902 · Group Mesh § Hubs). */
|
|
25066
25678
|
hubs;
|
|
25067
25679
|
/** Groups whose edges are being reconciled because their hubs changed: one at a time. */
|
|
@@ -25088,6 +25700,16 @@ var Groups = class {
|
|
|
25088
25700
|
}
|
|
25089
25701
|
}, hubTimings);
|
|
25090
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
|
+
});
|
|
25091
25713
|
this.signals = new MeshSignals(host, {
|
|
25092
25714
|
session: (id) => this.sessions.get(id),
|
|
25093
25715
|
stored: (id) => this.stored.get(id),
|
|
@@ -25125,13 +25747,14 @@ var Groups = class {
|
|
|
25125
25747
|
await this.communities.load(all.filter((g) => g.community || g.joining));
|
|
25126
25748
|
for (const group of all.filter((g) => !g.community && !g.joining)) {
|
|
25127
25749
|
this.stored.set(group.id, group);
|
|
25128
|
-
if (group.state) this.attach(group.state);
|
|
25129
|
-
if (group.invited) this.invited.set(group.id, new Set(Object.keys(group.invited)));
|
|
25130
25750
|
const history = await this.store.getMessages(MESSAGE_LINK(group.id)), said = history.filter((m) => !m.event && !m.groupPay);
|
|
25131
|
-
|
|
25751
|
+
const framed = history.filter((m) => m.frame);
|
|
25752
|
+
this.framesKept.set(group.id, framed.map((m) => ({
|
|
25132
25753
|
id: m.id,
|
|
25133
25754
|
bytes: frameBytes(m.frame)
|
|
25134
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)));
|
|
25135
25758
|
const last = said[said.length - 1], lastPeer = [...said].reverse().find((m) => m.sender !== "me");
|
|
25136
25759
|
if (last) this.lastMessageAt.set(group.id, last.timestamp);
|
|
25137
25760
|
const peerAt = cameOrWritten(lastPeer?.timestamp, lastPeer ? group.came?.peer : void 0);
|
|
@@ -25208,6 +25831,7 @@ var Groups = class {
|
|
|
25208
25831
|
members: Math.min(Math.max(invitation.n, 0), GROUP_MEMBER_CAP.max),
|
|
25209
25832
|
accepted: !!invitation.seedB64,
|
|
25210
25833
|
...this.acceptSaid.get(group.id)?.unanswered ? { unanswered: true } : {},
|
|
25834
|
+
...invitation.takenBack ? { takenBack: true } : {},
|
|
25211
25835
|
...invitation.entry ? {
|
|
25212
25836
|
viaLink: true,
|
|
25213
25837
|
stage: this.joinStage(group)
|
|
@@ -25389,6 +26013,7 @@ var Groups = class {
|
|
|
25389
26013
|
async accept(groupId) {
|
|
25390
26014
|
const group = this.stored.get(groupId);
|
|
25391
26015
|
if (!group?.invitation) throw new Error("No invitation to accept");
|
|
26016
|
+
if (group.invitation.takenBack) throw new Error("This invitation was taken back");
|
|
25392
26017
|
if (!this.host.linkReady(group.invitation.linkId)) throw new Error("The contact who invited you is not connected. Try again when they are.");
|
|
25393
26018
|
const seedB64 = group.invitation.seedB64 ?? createIdentity().seedB64;
|
|
25394
26019
|
group.invitation = {
|
|
@@ -25412,6 +26037,7 @@ var Groups = class {
|
|
|
25412
26037
|
if (this.isCommunity(groupId)) return this.communities.decline(groupId);
|
|
25413
26038
|
const group = this.stored.get(groupId);
|
|
25414
26039
|
if (!group?.invitation) throw new Error("No invitation to decline");
|
|
26040
|
+
if (group.invitation.takenBack) return this.forget(groupId, false);
|
|
25415
26041
|
if (!group.previous) return this.forget(groupId);
|
|
25416
26042
|
await this.dropInvitation(group, this.sayDecline(groupId, group.invitation.linkId));
|
|
25417
26043
|
}
|
|
@@ -25427,6 +26053,25 @@ var Groups = class {
|
|
|
25427
26053
|
await this.store.putGroup(previous);
|
|
25428
26054
|
this.host.emit();
|
|
25429
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
|
+
}
|
|
25430
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. */
|
|
25431
26076
|
sayDecline(groupId, linkId) {
|
|
25432
26077
|
try {
|
|
@@ -25619,6 +26264,8 @@ var Groups = class {
|
|
|
25619
26264
|
this.lastMessageAt.delete(groupId);
|
|
25620
26265
|
this.lastPeerMessageAt.delete(groupId);
|
|
25621
26266
|
this.lastMentionAt.delete(groupId);
|
|
26267
|
+
this.authorOrder.forget(groupId);
|
|
26268
|
+
this.lineOrder.forget(MESSAGE_LINK(groupId));
|
|
25622
26269
|
await this.store.deleteGroup(groupId);
|
|
25623
26270
|
await this.store.putGroup(group);
|
|
25624
26271
|
await this.files?.drop(groupId);
|
|
@@ -25812,19 +26459,20 @@ var Groups = class {
|
|
|
25812
26459
|
}
|
|
25813
26460
|
/** `answer`: false when an invitation goes because its inviter took it back, so no decline is said for it. */
|
|
25814
26461
|
async forget(groupId, answer = true) {
|
|
25815
|
-
if (this.isCommunity(groupId)) return this.communities.
|
|
26462
|
+
if (this.isCommunity(groupId)) return this.communities.deleteHere(groupId);
|
|
25816
26463
|
const session = this.sessions.get(groupId);
|
|
25817
26464
|
if (session?.status === "active") try {
|
|
25818
26465
|
await this.leave(groupId);
|
|
25819
26466
|
} catch (e) {
|
|
25820
26467
|
if (session.isAdmin && session.others.length) throw e;
|
|
25821
26468
|
}
|
|
25822
|
-
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;
|
|
25823
26470
|
const owed = invitation && this.sayDecline(groupId, invitation.linkId);
|
|
25824
26471
|
this.sessions.delete(groupId);
|
|
25825
26472
|
this.stored.delete(groupId);
|
|
25826
26473
|
this.takeBack(groupId);
|
|
25827
26474
|
this.acceptSaid.delete(groupId);
|
|
26475
|
+
this.entryHosts.delete(groupId);
|
|
25828
26476
|
this.lastRoster.delete(groupId);
|
|
25829
26477
|
this.lastTold.delete(groupId);
|
|
25830
26478
|
this.pendingEntries.delete(groupId);
|
|
@@ -25838,6 +26486,7 @@ var Groups = class {
|
|
|
25838
26486
|
this.relayAsked.delete(groupId);
|
|
25839
26487
|
this.syncHeld.delete(groupId);
|
|
25840
26488
|
this.lastHanded.delete(groupId);
|
|
26489
|
+
this.rewelcomed.delete(groupId);
|
|
25841
26490
|
this.hubs.forget(groupId);
|
|
25842
26491
|
this.removedAt.delete(groupId);
|
|
25843
26492
|
this.lastGossip.delete(groupId);
|
|
@@ -25850,6 +26499,8 @@ var Groups = class {
|
|
|
25850
26499
|
this.hereHeard.delete(groupId);
|
|
25851
26500
|
this.hereActed.delete(groupId);
|
|
25852
26501
|
this.signals.forget(groupId);
|
|
26502
|
+
this.authorOrder.forget(groupId);
|
|
26503
|
+
this.lineOrder.forget(MESSAGE_LINK(groupId));
|
|
25853
26504
|
for (const linkId of [...this.host.edges(groupId).values(), ...this.host.entries(groupId).values()]) await this.host.closeEdge(linkId);
|
|
25854
26505
|
await this.store.deleteGroup(groupId);
|
|
25855
26506
|
if (owed && !this.stored.has(groupId)) {
|
|
@@ -25948,6 +26599,7 @@ var Groups = class {
|
|
|
25948
26599
|
if (this.ticking) return;
|
|
25949
26600
|
this.ticking = true;
|
|
25950
26601
|
try {
|
|
26602
|
+
this.authorOrder.tick(now);
|
|
25951
26603
|
for (const group of [...this.stored.values()]) {
|
|
25952
26604
|
if (group.left) {
|
|
25953
26605
|
if (now - group.left.at > LEFT_KEPT_MS) await this.forget(group.id);
|
|
@@ -25957,6 +26609,10 @@ var Groups = class {
|
|
|
25957
26609
|
await this.declineOwed(group, now);
|
|
25958
26610
|
if (!group.state) continue;
|
|
25959
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
|
+
}
|
|
25960
26616
|
if (group.invitation?.entry && !group.state) {
|
|
25961
26617
|
const every = now - group.createdAt > this.timings.patienceMs ? this.timings.slowKnockMs : this.timings.knockMs;
|
|
25962
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(() => {});
|
|
@@ -26075,7 +26731,7 @@ var Groups = class {
|
|
|
26075
26731
|
}
|
|
26076
26732
|
const session = this.sessions.get(groupId), group = this.stored.get(groupId);
|
|
26077
26733
|
if (!session?.isAdmin || !group || !this.pendingEntries.get(groupId)?.has(peer)) return;
|
|
26078
|
-
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)) {
|
|
26079
26735
|
this.pendingEntries.get(groupId)?.delete(peer);
|
|
26080
26736
|
this.refused.set(peer, Date.now() + REFUSED_FOR_MS);
|
|
26081
26737
|
this.host.closeEdge(linkId);
|
|
@@ -26122,14 +26778,22 @@ var Groups = class {
|
|
|
26122
26778
|
return this.knocked.has(group.id) ? "knocked" : "knocking";
|
|
26123
26779
|
}
|
|
26124
26780
|
entryOf(group) {
|
|
26125
|
-
|
|
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 {
|
|
26126
26789
|
link: {
|
|
26127
26790
|
g: group.id,
|
|
26128
|
-
host:
|
|
26791
|
+
host: host.key
|
|
26129
26792
|
},
|
|
26130
|
-
seedB64
|
|
26131
|
-
}
|
|
26793
|
+
seedB64
|
|
26794
|
+
};
|
|
26132
26795
|
}
|
|
26796
|
+
entryHosts = /* @__PURE__ */ new Map();
|
|
26133
26797
|
/** Joiner side: the entry session kept after the welcome goes (the edge to the admin is up, or it was kept long enough). */
|
|
26134
26798
|
async endLinger(groupId) {
|
|
26135
26799
|
const kept = this.lingering.get(groupId);
|
|
@@ -26194,11 +26858,13 @@ var Groups = class {
|
|
|
26194
26858
|
});
|
|
26195
26859
|
let pending = this.pendingEntries.get(group.id);
|
|
26196
26860
|
for (const { key, ts } of knocks) {
|
|
26197
|
-
|
|
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;
|
|
26198
26864
|
const entryIds = new Set(this.host.entries(group.id).values());
|
|
26199
26865
|
const contactsInvited = [...this.invited.get(group.id) ?? []].filter((id) => !entryIds.has(id)).length;
|
|
26200
26866
|
const size = session.roster.length + contactsInvited + (pending?.size ?? 0);
|
|
26201
|
-
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;
|
|
26202
26868
|
if (!pending) this.pendingEntries.set(group.id, pending = /* @__PURE__ */ new Map());
|
|
26203
26869
|
pending.set(key, now);
|
|
26204
26870
|
this.warmUntil.set(group.id, now + this.timings.warmMs);
|
|
@@ -26258,9 +26924,9 @@ var Groups = class {
|
|
|
26258
26924
|
}
|
|
26259
26925
|
if (existing?.invitation?.seedB64 && (existing.invitation.linkId !== linkId || existing.invitation.admin !== frame.admin)) return;
|
|
26260
26926
|
if (existing?.left && (existing.left.admin !== frame.admin || existing.contacts?.[existing.left.admin] !== linkId)) return;
|
|
26261
|
-
if (existing?.invitation && !existing.state && existing.invitation.linkId !== linkId && !existing.invitation.genesis) return;
|
|
26262
|
-
if ([...this.stored.values()].filter((x) => x.invitation).length >= 32) return;
|
|
26263
|
-
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) {
|
|
26264
26930
|
try {
|
|
26265
26931
|
this.host.sendOnLink(linkId, {
|
|
26266
26932
|
t: "group-decline",
|
|
@@ -26312,6 +26978,8 @@ var Groups = class {
|
|
|
26312
26978
|
} catch {}
|
|
26313
26979
|
const edge = this.host.edges(g).get(frame.key);
|
|
26314
26980
|
if (letIn && session.isAdmin && !(edge && this.host.linkReady(edge))) {
|
|
26981
|
+
if (this.rewelcomedLately(g, frame.key)) return;
|
|
26982
|
+
this.noteRewelcome(g, frame.key);
|
|
26315
26983
|
const welcome = await session.rewelcome(frame.key).catch(() => []);
|
|
26316
26984
|
try {
|
|
26317
26985
|
for (const piece of welcome) this.host.sendOnLink(linkId, piece);
|
|
@@ -26320,18 +26988,35 @@ var Groups = class {
|
|
|
26320
26988
|
return;
|
|
26321
26989
|
}
|
|
26322
26990
|
const group = this.stored.get(g);
|
|
26323
|
-
if (frame.key === session.myKey
|
|
26991
|
+
if (frame.key === session.myKey) return;
|
|
26324
26992
|
const entryPeer = this.hostEntry(g, linkId);
|
|
26325
26993
|
if ([...this.host.entries(g).values()].includes(linkId) && entryPeer !== frame.key) return;
|
|
26326
|
-
|
|
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;
|
|
26327
27003
|
if (!entryPeer) await session.setNick(frame.key, this.host.contactName(linkId));
|
|
26328
|
-
this.meet(g, frame.key);
|
|
27004
|
+
if (!again) this.meet(g, frame.key);
|
|
26329
27005
|
if (this.host.linkReady(linkId, 3)) this.markLarge(group, frame.key);
|
|
26330
|
-
|
|
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
|
+
}
|
|
26331
27015
|
if (!entryPeer) group.contacts = {
|
|
26332
27016
|
...group.contacts,
|
|
26333
27017
|
[frame.key]: linkId
|
|
26334
27018
|
};
|
|
27019
|
+
if (!again) group.unseen = [...(group.unseen ?? []).filter((key) => rosterHas(session.roster, key)), frame.key];
|
|
26335
27020
|
this.answered(group, linkId);
|
|
26336
27021
|
await this.store.putGroup(group);
|
|
26337
27022
|
for (const piece of welcome) this.host.sendOnLink(linkId, piece);
|
|
@@ -26354,7 +27039,7 @@ var Groups = class {
|
|
|
26354
27039
|
case "group-decline": {
|
|
26355
27040
|
const mine = this.stored.get(g);
|
|
26356
27041
|
if (mine?.invitation && !mine.invitation.entry && !mine.state && mine.invitation.linkId === linkId) {
|
|
26357
|
-
await this.
|
|
27042
|
+
await this.takeBackFrom(mine);
|
|
26358
27043
|
return;
|
|
26359
27044
|
}
|
|
26360
27045
|
const entryPeer = this.hostEntry(g, linkId);
|
|
@@ -26363,6 +27048,7 @@ var Groups = class {
|
|
|
26363
27048
|
await this.host.closeEdge(linkId);
|
|
26364
27049
|
}
|
|
26365
27050
|
const group = this.stored.get(g);
|
|
27051
|
+
if (group && await this.declinedUnseen(g, group, linkId)) return;
|
|
26366
27052
|
if (!group || !this.answered(group, linkId)) return;
|
|
26367
27053
|
await this.store.putGroup(group);
|
|
26368
27054
|
this.host.emit();
|
|
@@ -26526,7 +27212,8 @@ var Groups = class {
|
|
|
26526
27212
|
return;
|
|
26527
27213
|
}
|
|
26528
27214
|
const taken = await session.handle(peerKey, frame);
|
|
26529
|
-
|
|
27215
|
+
const askedAt = this.askedAt(groupId, peerKey);
|
|
27216
|
+
if (taken.some((f) => f.t === "group-msg" && f.ts < askedAt)) {
|
|
26530
27217
|
let handed = this.lastHanded.get(groupId);
|
|
26531
27218
|
if (!handed) this.lastHanded.set(groupId, handed = /* @__PURE__ */ new Map());
|
|
26532
27219
|
handed.set(peerKey, this.now());
|
|
@@ -26599,9 +27286,14 @@ var Groups = class {
|
|
|
26599
27286
|
return;
|
|
26600
27287
|
}
|
|
26601
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
|
+
}
|
|
26602
27294
|
const large = this.host.linkReady(linkId, 3) && this.markLarge(group, peerKey);
|
|
26603
27295
|
const legacy = this.hubs.enabled && this.hubs.edgeReady(groupId, session, group, peerKey, this.host.linkReady(linkId, 4), this.now());
|
|
26604
|
-
if (large || legacy) this.store.putGroup(group).catch(() => {});
|
|
27296
|
+
if (large || legacy || seen) this.store.putGroup(group).catch(() => {});
|
|
26605
27297
|
if (!this.holdSync(groupId, peerKey)) try {
|
|
26606
27298
|
this.host.sendOnLink(linkId, session.syncFrame(this.askOf(groupId, session, peerKey)));
|
|
26607
27299
|
} catch {
|
|
@@ -26624,6 +27316,41 @@ var Groups = class {
|
|
|
26624
27316
|
this.host.edgeUp?.(groupId, peerKey);
|
|
26625
27317
|
this.host.emit();
|
|
26626
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
|
+
}
|
|
26627
27354
|
/** Records that a member's app takes rosters past eight; true when that is news (the caller saves). */
|
|
26628
27355
|
markLarge(group, key) {
|
|
26629
27356
|
if (group.large?.includes(key)) return false;
|
|
@@ -26731,13 +27458,19 @@ var Groups = class {
|
|
|
26731
27458
|
if (out.length) this.lastGossip.set(groupId, now);
|
|
26732
27459
|
return out;
|
|
26733
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
|
+
}
|
|
26734
27467
|
/**
|
|
26735
27468
|
* Whether the answer of the member asked for someone's messages may still be coming: its edge up, asked under
|
|
26736
|
-
* `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.
|
|
26737
27470
|
*/
|
|
26738
27471
|
answering(groupId, { via, at }, now) {
|
|
26739
27472
|
const edge = this.host.edges(groupId).get(via);
|
|
26740
|
-
if (!edge || !this.host.linkReady(edge) || now - at >=
|
|
27473
|
+
if (!edge || !this.host.linkReady(edge) || now - at >= 18e4) return false;
|
|
26741
27474
|
return now - Math.max(at, this.lastHanded.get(groupId)?.get(via) ?? 0) < MESH_SYNC_HOLD_MS;
|
|
26742
27475
|
}
|
|
26743
27476
|
/**
|
|
@@ -26757,21 +27490,41 @@ var Groups = class {
|
|
|
26757
27490
|
held.set(peerKey, { ...asked });
|
|
26758
27491
|
return true;
|
|
26759
27492
|
}
|
|
26760
|
-
/**
|
|
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
|
+
*/
|
|
26761
27499
|
heldSyncs(groupId, session, now) {
|
|
26762
27500
|
const held = this.syncHeld.get(groupId);
|
|
26763
27501
|
if (!held?.size) return;
|
|
26764
|
-
const edges = this.host.edges(groupId);
|
|
26765
|
-
|
|
26766
|
-
|
|
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;
|
|
26767
27506
|
held.delete(key);
|
|
26768
|
-
if (
|
|
27507
|
+
if (asked?.get(key)?.via === of.via) asked.delete(key);
|
|
26769
27508
|
const edge = edges.get(key);
|
|
26770
|
-
if (
|
|
26771
|
-
|
|
26772
|
-
|
|
26773
|
-
|
|
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
|
+
});
|
|
26774
27524
|
}
|
|
27525
|
+
try {
|
|
27526
|
+
this.host.sendOnLink(edges.get(next), session.syncFrame([...ask]));
|
|
27527
|
+
} catch {}
|
|
26775
27528
|
}
|
|
26776
27529
|
/**
|
|
26777
27530
|
* Reachable members whose messages below the highest seen never reached me, for `via` to hand on: their own answer
|
|
@@ -27024,7 +27777,8 @@ var Groups = class {
|
|
|
27024
27777
|
if (!session) throw new Error("Group not found");
|
|
27025
27778
|
return session;
|
|
27026
27779
|
}
|
|
27027
|
-
|
|
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 = []) {
|
|
27028
27782
|
const session = new GroupSession(state, {
|
|
27029
27783
|
clock: () => this.now(),
|
|
27030
27784
|
seqFloor: () => this.host.seqFloor?.() ?? 0,
|
|
@@ -27078,8 +27832,21 @@ var Groups = class {
|
|
|
27078
27832
|
...m.frame && { frame: m.frame }
|
|
27079
27833
|
};
|
|
27080
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());
|
|
27081
27840
|
const stored = completed ? (await this.host.completeMessage(message), false) : await this.host.storeMessage(message);
|
|
27841
|
+
placing?.(stored !== false);
|
|
27082
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
|
+
});
|
|
27083
27850
|
const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, arrivalNow(this.now()));
|
|
27084
27851
|
this.lastMessageAt.set(state.id, Math.max(this.lastMessageAt.get(state.id) ?? 0, came));
|
|
27085
27852
|
if (mentioned) this.lastMentionAt.set(state.id, Math.max(this.lastMentionAt.get(state.id) ?? 0, came));
|
|
@@ -27087,6 +27854,10 @@ var Groups = class {
|
|
|
27087
27854
|
noteCame(this.stored.get(state.id), came, m.sender !== session.myKey, mentioned);
|
|
27088
27855
|
this.typings.messageFrom(state.id, m.sender);
|
|
27089
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
|
+
},
|
|
27090
27861
|
edit: (e) => this.host.groupEdit?.(state.id, e),
|
|
27091
27862
|
changed: () => {
|
|
27092
27863
|
if (session.status === "removed" && this.hubs.isHub(state.id) && !this.removedAt.has(state.id)) this.removedAt.set(state.id, this.now());
|
|
@@ -27099,7 +27870,7 @@ var Groups = class {
|
|
|
27099
27870
|
const edge = this.host.edges(state.id).get(to);
|
|
27100
27871
|
return edge && this.host.linkHandled ? this.host.linkHandled(edge) : Promise.resolve(false);
|
|
27101
27872
|
} }
|
|
27102
|
-
});
|
|
27873
|
+
}, frames);
|
|
27103
27874
|
this.sessions.set(state.id, session);
|
|
27104
27875
|
this.lastRoster.set(state.id, session.roster);
|
|
27105
27876
|
this.lastTold.set(state.id, {
|
|
@@ -27128,7 +27899,13 @@ var Groups = class {
|
|
|
27128
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);
|
|
27129
27900
|
const gone = before.filter(([key]) => !rosterHas(after, key)).map(([key]) => key);
|
|
27130
27901
|
this.hubs.removed(groupId, gone, this.now());
|
|
27131
|
-
|
|
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);
|
|
27132
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);
|
|
27133
27910
|
if (top.k === "rotate") await this.event(groupId, "rotated", "Keys rotated: a fresh epoch", when, top.e);
|
|
27134
27911
|
}
|
|
@@ -27183,16 +27960,21 @@ var Groups = class {
|
|
|
27183
27960
|
/** A line in the group's history; lines at the same time (two members gone in one change) get distinct ones, see `eventTime`. */
|
|
27184
27961
|
async event(groupId, event, text, timestamp, epoch, member) {
|
|
27185
27962
|
timestamp = eventTime(this.eventTimes, groupId, timestamp);
|
|
27186
|
-
|
|
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({
|
|
27187
27969
|
linkId: MESSAGE_LINK(groupId),
|
|
27188
|
-
id
|
|
27970
|
+
id,
|
|
27189
27971
|
text,
|
|
27190
27972
|
sender: "peer",
|
|
27191
27973
|
event,
|
|
27192
27974
|
member,
|
|
27193
27975
|
timestamp,
|
|
27194
27976
|
via: "datalink"
|
|
27195
|
-
});
|
|
27977
|
+
}) !== false);
|
|
27196
27978
|
}
|
|
27197
27979
|
eventTimes = /* @__PURE__ */ new Map();
|
|
27198
27980
|
};
|
|
@@ -29146,8 +29928,6 @@ function resumeHere(log, runs, peer) {
|
|
|
29146
29928
|
const shared = runs.find((transport) => peer?.includes(transport));
|
|
29147
29929
|
return shared ? { resume: shared } : {};
|
|
29148
29930
|
}
|
|
29149
|
-
/** A round trip that moved less than this is not news in the history. */
|
|
29150
|
-
var RTT_STEP_MS = 10;
|
|
29151
29931
|
var sequence = 0;
|
|
29152
29932
|
var lineId = (at) => `${at.toString(36)}-${(++sequence).toString(36)}`;
|
|
29153
29933
|
/** A row that says the chat is live on its transport from then on. */
|
|
@@ -29551,7 +30331,7 @@ var TransportLog = class {
|
|
|
29551
30331
|
changed = true;
|
|
29552
30332
|
}
|
|
29553
30333
|
const stretch = [...this.history].reverse().find((e) => e.kind === "live" || e.kind === "switched" || e.kind === "down");
|
|
29554
|
-
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)) {
|
|
29555
30335
|
stretch.rttMs = ms;
|
|
29556
30336
|
changed = true;
|
|
29557
30337
|
}
|
|
@@ -34483,24 +35263,6 @@ function messageAttention(type, message, { startedAt, newest, now }) {
|
|
|
34483
35263
|
};
|
|
34484
35264
|
}
|
|
34485
35265
|
//#endregion
|
|
34486
|
-
//#region ../browser/src/shared/hyperdhtRelay.ts
|
|
34487
|
-
/** Only `wss://`, or `ws://` to this machine (a relay for tests or development). */
|
|
34488
|
-
function hyperdhtRelayProblem(url) {
|
|
34489
|
-
let parsed;
|
|
34490
|
-
try {
|
|
34491
|
-
parsed = new URL(url);
|
|
34492
|
-
} catch {
|
|
34493
|
-
return "Enter a relay address (wss://…)";
|
|
34494
|
-
}
|
|
34495
|
-
if (parsed.protocol === "wss:") return null;
|
|
34496
|
-
if (parsed.protocol === "ws:" && [
|
|
34497
|
-
"127.0.0.1",
|
|
34498
|
-
"localhost",
|
|
34499
|
-
"[::1]"
|
|
34500
|
-
].includes(parsed.hostname)) return null;
|
|
34501
|
-
return "Use a wss:// relay address";
|
|
34502
|
-
}
|
|
34503
|
-
//#endregion
|
|
34504
35266
|
//#region ../browser/src/shared/pushRelay.ts
|
|
34505
35267
|
/**
|
|
34506
35268
|
* The push relay a browser hands wake-ups to when it may not post to a push service itself (WISP 401 § Wake-up
|
|
@@ -34651,10 +35413,10 @@ var NATIVE_SLOTS = 8;
|
|
|
34651
35413
|
var IROH_WEB_SLOTS = 16;
|
|
34652
35414
|
/** Of a transport's listeners, the ones a group's links never hold: 1:1 chats keep that room. */
|
|
34653
35415
|
var CHAT_NATIVE_SLOTS = 4;
|
|
34654
|
-
/** How often a group link that found no free native slot tries again. */
|
|
34655
|
-
var
|
|
34656
|
-
/** The longest a group link whose native listener failed to start waits before trying again (its wait doubles up to it). */
|
|
34657
|
-
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;
|
|
34658
35420
|
/** "Clear all data": how long taking things back from the network may hold up the clear, in all. */
|
|
34659
35421
|
var CLEAR_WITHDRAW_MS = 6e3;
|
|
34660
35422
|
/**
|
|
@@ -34683,11 +35445,19 @@ var DEFAULT_SETTINGS = {
|
|
|
34683
35445
|
*/
|
|
34684
35446
|
var CARD_RESTORE_MOST = 50;
|
|
34685
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;
|
|
34686
35454
|
/** A reaction said on the live session and not confirmed is said again after this long. */
|
|
34687
35455
|
var REACTION_RESEND_MS = 3e4;
|
|
34688
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). */
|
|
34689
35457
|
var waitsForLive = (m) => m.sender === "me" && m.delivery === "waiting" && (!!m.file || !!m.paymentId);
|
|
34690
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);
|
|
34691
35461
|
function participationKeyOf(live) {
|
|
34692
35462
|
const seed = live.stored.participationSeed;
|
|
34693
35463
|
if (!seed) return void 0;
|
|
@@ -34865,7 +35635,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34865
35635
|
* Everything that publishes, dials, polls or settles asks this, not the stored setting.
|
|
34866
35636
|
*/
|
|
34867
35637
|
get networkOn() {
|
|
34868
|
-
return !!this.settings.online && !this.limitedMode;
|
|
35638
|
+
return !!this.settings.online && !this.limitedMode && !this.options.offline;
|
|
34869
35639
|
}
|
|
34870
35640
|
transport;
|
|
34871
35641
|
/** The same, for groups' requests: a relay budget keeps a chat's share from them (`CHAT_RESERVE`). */
|
|
@@ -34910,6 +35680,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34910
35680
|
});
|
|
34911
35681
|
/** `start` has put the kept transfers back (EngineState.transfersRestored). */
|
|
34912
35682
|
transfersRestored = false;
|
|
35683
|
+
/** `start` has loaded the groups (EngineState.groupsLoaded). */
|
|
35684
|
+
groupsLoaded = false;
|
|
34913
35685
|
feedbackStartedAt = Date.now();
|
|
34914
35686
|
feedbackIds = /* @__PURE__ */ new Set();
|
|
34915
35687
|
walletFeedbackReady = false;
|
|
@@ -34993,9 +35765,15 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34993
35765
|
groupWakeReceived = /* @__PURE__ */ new Map();
|
|
34994
35766
|
requestCounts = /* @__PURE__ */ new Map();
|
|
34995
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();
|
|
34996
35773
|
/** files/3 in every chat: offers, resumable transfers, checked by digest (WISP 501 rev 0.3). */
|
|
34997
35774
|
fileDesk = new FileDesk({
|
|
34998
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,
|
|
34999
35777
|
writable: (linkId) => this.links.get(linkId)?.link?.filesWritable(),
|
|
35000
35778
|
receivedBytes: (linkId) => this.links.get(linkId)?.files.receivedBytes ?? 0,
|
|
35001
35779
|
wireIds: (linkId) => this.links.get(linkId)?.files.wireIds,
|
|
@@ -35771,6 +36549,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35771
36549
|
forgetFrame: async (linkId, id) => {
|
|
35772
36550
|
await db.patchMessage(linkId, id, (stored) => stored.frame ? { frame: void 0 } : null, true);
|
|
35773
36551
|
},
|
|
36552
|
+
keepFrame: async (linkId, id, frame) => {
|
|
36553
|
+
await db.patchMessage(linkId, id, () => ({ frame }), true);
|
|
36554
|
+
},
|
|
36555
|
+
placeAgain: (linkId, ids) => this.placeAgain(linkId, ids),
|
|
35774
36556
|
membersChanged: (groupId) => this.groupMembersChanged(groupId),
|
|
35775
36557
|
historyGone: (groupId) => this.groupHistoryGone(groupId),
|
|
35776
36558
|
emit: () => this.emitState(),
|
|
@@ -35791,11 +36573,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35791
36573
|
groupFiles: {
|
|
35792
36574
|
store: storedGroupFiles,
|
|
35793
36575
|
transfers: this.transfers,
|
|
36576
|
+
brief: (id) => {
|
|
36577
|
+
const view = this.transfers.get(id);
|
|
36578
|
+
if (view) this.briefViews.set(id, view);
|
|
36579
|
+
},
|
|
35794
36580
|
settings: () => ({
|
|
35795
36581
|
autoDownloads: this.settings.autoDownloads !== false,
|
|
35796
36582
|
serveFiles: this.settings.serveFiles !== false
|
|
35797
36583
|
}),
|
|
35798
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,
|
|
35799
36586
|
writable: (linkId) => this.links.get(linkId)?.link?.filesWritable()
|
|
35800
36587
|
}
|
|
35801
36588
|
});
|
|
@@ -36188,7 +36975,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36188
36975
|
const messages = stored.group ? [] : await db.getMessages(stored.id);
|
|
36189
36976
|
const storedFiles = stored.group ? [] : await fileStore.listForLink(stored.id);
|
|
36190
36977
|
const waiting = new Set(messages.flatMap((m) => m.sender === "me" && m.delivery === "waiting" && m.file ? [m.file.id] : []));
|
|
36191
|
-
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" ? {
|
|
36192
36979
|
...file.transfer,
|
|
36193
36980
|
state: "failed",
|
|
36194
36981
|
error: "Transfer interrupted. Retry when connected."
|
|
@@ -36206,6 +36993,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36206
36993
|
this.transfersRestored = true;
|
|
36207
36994
|
for (const group of await db.getGroups()) await this.settleHistory(`group:${group.id}`);
|
|
36208
36995
|
await this.groups.load(this.limitedMode);
|
|
36996
|
+
this.groupsLoaded = true;
|
|
36209
36997
|
for (const group of this.groups.views()) {
|
|
36210
36998
|
const note = latestReaction(await db.getMessages(`group:${group.id}`));
|
|
36211
36999
|
if (note) this.reactionNotes.set(`group:${group.id}`, note);
|
|
@@ -36224,7 +37012,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36224
37012
|
this.prepareSpare(STARTUP_QUIET_MS);
|
|
36225
37013
|
}
|
|
36226
37014
|
if (!this.limitedMode) this.openStartedWallets(fresh);
|
|
36227
|
-
if (this.appsOn) this.appStore.start().catch(() => {});
|
|
37015
|
+
if (this.appsOn && !this.appsWireOnly) this.appStore.start().catch(() => {});
|
|
36228
37016
|
if (!this.options.singleDevice && this.networkOn && knownDeviceGate()?.state === "active") this.startDeviceSet().catch(() => {});
|
|
36229
37017
|
if (!this.options.singleDevice && this.limitedMode && knownDeviceGate()?.state === "active") this.readTurnWhileLimited();
|
|
36230
37018
|
if (this.restoreCheck === "checking" && this.limitedMode) this.checkRestoredTurn(0);
|
|
@@ -36689,7 +37477,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36689
37477
|
appShelf;
|
|
36690
37478
|
/** The installed apps and stores, refused while the apps feature is off. */
|
|
36691
37479
|
get appStore() {
|
|
36692
|
-
if (!this.appsOn) throw this.appsOff();
|
|
37480
|
+
if (!this.appsOn || this.appsWireOnly) throw this.appsOff();
|
|
36693
37481
|
return this.appShelf ??= new Apps({
|
|
36694
37482
|
fetch: boundedAppFetch({
|
|
36695
37483
|
fetcher: this.options.appFetch,
|
|
@@ -36700,12 +37488,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36700
37488
|
return !!stored && !stored.group && !!stored.profile && !!stored.pairedPeerKey;
|
|
36701
37489
|
},
|
|
36702
37490
|
online: () => this.networkOn && !this.shuttingDown,
|
|
36703
|
-
checked: () => this.events.onAppsChecked?.()
|
|
37491
|
+
checked: () => this.events.onAppsChecked?.(),
|
|
37492
|
+
...this.options.builtInApp && { readBuiltIn: this.options.builtInApp }
|
|
36704
37493
|
});
|
|
36705
37494
|
}
|
|
36706
37495
|
appList() {
|
|
36707
37496
|
return this.appStore.list();
|
|
36708
37497
|
}
|
|
37498
|
+
appBuiltIns() {
|
|
37499
|
+
return this.appStore.builtIns();
|
|
37500
|
+
}
|
|
36709
37501
|
appStoreList() {
|
|
36710
37502
|
return this.appStore.listStores();
|
|
36711
37503
|
}
|
|
@@ -36751,6 +37543,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36751
37543
|
appFile(params) {
|
|
36752
37544
|
return this.appStore.file(params);
|
|
36753
37545
|
}
|
|
37546
|
+
appFileAt(params) {
|
|
37547
|
+
return this.appStore.fileAt(params);
|
|
37548
|
+
}
|
|
36754
37549
|
appStorageGet(params) {
|
|
36755
37550
|
return this.appStore.storageGet(params);
|
|
36756
37551
|
}
|
|
@@ -36766,6 +37561,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36766
37561
|
appDataExport(params) {
|
|
36767
37562
|
return this.appStore.exportData(params);
|
|
36768
37563
|
}
|
|
37564
|
+
appDataExportText(params) {
|
|
37565
|
+
return this.appStore.exportDataText(params);
|
|
37566
|
+
}
|
|
36769
37567
|
getState() {
|
|
36770
37568
|
const groups = this.groups.views();
|
|
36771
37569
|
return {
|
|
@@ -36793,6 +37591,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36793
37591
|
})).sort((a, b) => a.createdAt - b.createdAt),
|
|
36794
37592
|
transfers: Object.fromEntries(this.transfers),
|
|
36795
37593
|
transfersRestored: this.transfersRestored,
|
|
37594
|
+
...this.groupsLoaded && { groupsLoaded: true },
|
|
36796
37595
|
wallet: this.walletView,
|
|
36797
37596
|
payments: this.desk.views(),
|
|
36798
37597
|
identityProofs: this.identities.views(),
|
|
@@ -37645,7 +38444,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37645
38444
|
this.transport.networkChanged?.();
|
|
37646
38445
|
this.directPath.reset();
|
|
37647
38446
|
}
|
|
37648
|
-
if (params.network) for (const failed of this.
|
|
38447
|
+
if (params.network) for (const failed of this.nativeFailed.values()) {
|
|
37649
38448
|
failed.at = 0;
|
|
37650
38449
|
failed.wait = 0;
|
|
37651
38450
|
}
|
|
@@ -37859,14 +38658,23 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37859
38658
|
/**
|
|
37860
38659
|
* The message of a chat a reply answers, by its id here or the id both sides know it by (`replyRef`): what the
|
|
37861
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.
|
|
37862
38664
|
*/
|
|
37863
38665
|
async replyFor(linkId, messageId) {
|
|
37864
38666
|
if (typeof messageId !== "string" || !messageId) return "No message to reply to";
|
|
37865
|
-
const
|
|
37866
|
-
|
|
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);
|
|
37867
38675
|
const ref = original && replyRef(original);
|
|
37868
38676
|
if (!original || !ref) return "That message is not in this chat, or cannot be replied to";
|
|
37869
|
-
if (
|
|
38677
|
+
if (group && !original.member) return "That message cannot be replied to";
|
|
37870
38678
|
return replyTo(original, ref);
|
|
37871
38679
|
}
|
|
37872
38680
|
/**
|
|
@@ -38204,6 +39012,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38204
39012
|
const chat = this.links.get(linkId)?.stored;
|
|
38205
39013
|
outbox = new Outbox({
|
|
38206
39014
|
read: () => db.getMessages(linkId),
|
|
39015
|
+
get: (id) => db.getMessage(linkId, id),
|
|
38207
39016
|
update: async (id, delivery, error, extra) => {
|
|
38208
39017
|
if (delivery === "delivered") await db.patchMessage(linkId, id, (message) => message.delivery === "delivered" ? null : {
|
|
38209
39018
|
...message.delivery && {
|
|
@@ -39285,19 +40094,40 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39285
40094
|
const cue = transportCue(mark, log.entries, log.history);
|
|
39286
40095
|
if (cue) this.cueFeedback(cue, linkId);
|
|
39287
40096
|
}
|
|
39288
|
-
|
|
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 } = {}) {
|
|
39289
40099
|
const transportLog = log.entries.map((e) => ({ ...e })), transportHistory = log.history.map((e) => ({ ...e }));
|
|
39290
40100
|
live.stored = {
|
|
39291
40101
|
...live.stored,
|
|
39292
40102
|
transportLog,
|
|
39293
40103
|
transportHistory
|
|
39294
40104
|
};
|
|
39295
|
-
|
|
39296
|
-
|
|
39297
|
-
|
|
39298
|
-
|
|
40105
|
+
if (toDisk) {
|
|
40106
|
+
live.rttUnsaved = false;
|
|
40107
|
+
db.patchLink(live.stored.id, {
|
|
40108
|
+
transportLog,
|
|
40109
|
+
transportHistory
|
|
40110
|
+
}, relaxed).catch(() => {});
|
|
40111
|
+
}
|
|
39299
40112
|
this.emitState();
|
|
39300
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
|
+
}
|
|
39301
40131
|
/** Which ways of paying one chat allows. */
|
|
39302
40132
|
/** `networks`: for a way of paying, the networks this chat takes it on (the Accept side's cards); the others are kept. */
|
|
39303
40133
|
async setChatPaymentMethods({ linkId, methods, networks }) {
|
|
@@ -39329,7 +40159,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39329
40159
|
async connect({ linkId }) {
|
|
39330
40160
|
const link = this.links.get(linkId)?.link;
|
|
39331
40161
|
if (!link) throw new Error("You are offline. Go online before reconnecting.");
|
|
39332
|
-
|
|
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;
|
|
39333
40165
|
await link.connect();
|
|
39334
40166
|
}
|
|
39335
40167
|
disconnect({ linkId }) {
|
|
@@ -39839,11 +40671,22 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39839
40671
|
}
|
|
39840
40672
|
/** Mini-apps are on in this build (`APPS_ENABLED`) or for this engine (`apps`). */
|
|
39841
40673
|
get appsOn() {
|
|
39842
|
-
return this.options.apps ?? true;
|
|
40674
|
+
return !!(this.options.apps ?? true);
|
|
40675
|
+
}
|
|
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";
|
|
39843
40679
|
}
|
|
39844
|
-
/** Why an app call is refused: this client runs no mini-app (`apps: false`), or the build has them off. */
|
|
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. */
|
|
39845
40681
|
appsOff() {
|
|
39846
|
-
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
|
+
});
|
|
39847
40690
|
}
|
|
39848
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). */
|
|
39849
40692
|
appChat(linkId, ref) {
|
|
@@ -39869,10 +40712,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39869
40712
|
async appOpen({ linkId, ref, version, digest }) {
|
|
39870
40713
|
const { live, app } = this.appChat(linkId, ref);
|
|
39871
40714
|
if (!isAppVersion(version)) throw new Error("Not an app version");
|
|
39872
|
-
await this.
|
|
39873
|
-
ref,
|
|
39874
|
-
digest
|
|
39875
|
-
});
|
|
40715
|
+
await this.appChatRunnable(ref, digest);
|
|
39876
40716
|
const open = live.appsOpen ??= /* @__PURE__ */ new Map();
|
|
39877
40717
|
if (live.link) live.link.openApp(app, version);
|
|
39878
40718
|
else open.set(app, version);
|
|
@@ -39888,10 +40728,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39888
40728
|
async appSend({ linkId, ref, data, digest }) {
|
|
39889
40729
|
const { live, app } = this.appChat(linkId, ref);
|
|
39890
40730
|
if (!live.appsOpen?.has(app)) return { error: "not-open" };
|
|
39891
|
-
await this.
|
|
39892
|
-
ref,
|
|
39893
|
-
digest
|
|
39894
|
-
});
|
|
40731
|
+
await this.appChatRunnable(ref, digest);
|
|
39895
40732
|
return { error: live.link ? live.link.sendAppData(app, data) : "offline" };
|
|
39896
40733
|
}
|
|
39897
40734
|
/** Every page went away: no app runs here any more, so each open one is closed (`close` goes where it can). */
|
|
@@ -41018,6 +41855,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41018
41855
|
const settings = rest;
|
|
41019
41856
|
delete settings.backupReminders;
|
|
41020
41857
|
const wasOnline = this.networkOn;
|
|
41858
|
+
if (Array.isArray(settings.iceServers)) settings.iceServers = settings.iceServers.map(iceServerSetting);
|
|
41021
41859
|
for (const server of settings.iceServers ?? []) {
|
|
41022
41860
|
const problem = iceServerProblem(server);
|
|
41023
41861
|
if (problem) throw new Error(problem);
|
|
@@ -41986,7 +42824,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41986
42824
|
this.groupWakeReceived.delete(linkId);
|
|
41987
42825
|
this.quietEdges.delete(linkId);
|
|
41988
42826
|
this.groupNativeWaiting.delete(linkId);
|
|
41989
|
-
this.
|
|
42827
|
+
this.nativeFailed.delete(linkId);
|
|
41990
42828
|
this.groupNativeRetryAt = 0;
|
|
41991
42829
|
if (live.stored.wakeToken && this.meshEdge(live.stored)) {
|
|
41992
42830
|
const group = live.stored.group, peer = live.stored.groupPeer;
|
|
@@ -42102,7 +42940,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42102
42940
|
if (this.groupEntryTimer || this.shuttingDown || this.options.deferGroups) return;
|
|
42103
42941
|
this.groupEntryTimer = setInterval(() => {
|
|
42104
42942
|
this.groups.tick().catch(() => {});
|
|
42105
|
-
this.
|
|
42943
|
+
this.retryNative();
|
|
42106
42944
|
}, 1e3);
|
|
42107
42945
|
}
|
|
42108
42946
|
stopGroupEntries() {
|
|
@@ -42145,6 +42983,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42145
42983
|
usdtPaymentsSupport: !entry,
|
|
42146
42984
|
barkPaymentsSupport: !entry,
|
|
42147
42985
|
largeFilesSupport: !entry,
|
|
42986
|
+
binaryFilesSupport: !entry,
|
|
42148
42987
|
params: stored,
|
|
42149
42988
|
rtcAvailable: typeof RTCPeerConnection !== "undefined" && !this.edgeRtcOff.has(linkId),
|
|
42150
42989
|
resume: !entry && stored.edgeLive ? resumeOn : void 0,
|
|
@@ -42250,10 +43089,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42250
43089
|
role,
|
|
42251
43090
|
packetAt: presence.lastPacketAt
|
|
42252
43091
|
});
|
|
43092
|
+
const same = samePresence(live.presence, presence);
|
|
42253
43093
|
live.presence = presence;
|
|
42254
43094
|
if (!entry && presence.nick && !live.link?.isDataLinkOpen) this.groups.edgeNick(group, peer, presence.nick);
|
|
42255
43095
|
if (!entry) this.edgeRtcPresence(linkId, presence);
|
|
42256
|
-
this.emitState();
|
|
43096
|
+
if (!same) this.emitState();
|
|
42257
43097
|
},
|
|
42258
43098
|
onPairingState: (state) => {
|
|
42259
43099
|
live.pairing = state;
|
|
@@ -42340,7 +43180,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42340
43180
|
this.edgeRtcOff.delete(linkId);
|
|
42341
43181
|
if (live) traceLink(live.myPubKeyZ32, "edge-rtc-back", {});
|
|
42342
43182
|
this.groupNativeWaiting.delete(linkId);
|
|
42343
|
-
this.
|
|
43183
|
+
this.nativeFailed.delete(linkId);
|
|
42344
43184
|
this.groupNativeRetryAt = 0;
|
|
42345
43185
|
this.restartEdge(linkId);
|
|
42346
43186
|
}
|
|
@@ -42381,6 +43221,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42381
43221
|
callsSupport: this.options.callsSupport ?? typeof RTCPeerConnection !== "undefined",
|
|
42382
43222
|
callsMissing: this.options.callsUnavailable,
|
|
42383
43223
|
largeFilesSupport: true,
|
|
43224
|
+
binaryFilesSupport: true,
|
|
42384
43225
|
typingSupport: true,
|
|
42385
43226
|
reactionsSupport: this.options.reactions !== false,
|
|
42386
43227
|
editSupport: !GhostlyNode.testNoEdit(),
|
|
@@ -42532,11 +43373,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42532
43373
|
this.emitState();
|
|
42533
43374
|
},
|
|
42534
43375
|
onLiveAttempt: () => this.emitState(),
|
|
42535
|
-
onRtt: (ms) =>
|
|
42536
|
-
const log = this.transportLogOf(live);
|
|
42537
|
-
if (log?.rtt(ms)) this.saveTransportLog(live, log);
|
|
42538
|
-
else this.emitState();
|
|
42539
|
-
},
|
|
43376
|
+
onRtt: (ms) => this.noteRtt(live, ms),
|
|
42540
43377
|
onDiscoveryError: (error) => {
|
|
42541
43378
|
if ((error ?? void 0) !== live.discoveryError) {
|
|
42542
43379
|
live.discoveryError = error ?? void 0;
|
|
@@ -42658,6 +43495,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42658
43495
|
this.emitState();
|
|
42659
43496
|
},
|
|
42660
43497
|
onPresence: (presence) => {
|
|
43498
|
+
let same = samePresence(live.presence, presence);
|
|
42661
43499
|
live.presence = presence;
|
|
42662
43500
|
if (!stored.profile && presence.nick && presence.nick !== live.stored.peerNick) {
|
|
42663
43501
|
live.stored = {
|
|
@@ -42665,8 +43503,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42665
43503
|
peerNick: presence.nick
|
|
42666
43504
|
};
|
|
42667
43505
|
db.patchLink(linkId, { peerNick: presence.nick });
|
|
43506
|
+
same = false;
|
|
42668
43507
|
}
|
|
42669
|
-
this.emitState();
|
|
43508
|
+
if (!same) this.emitState();
|
|
42670
43509
|
},
|
|
42671
43510
|
onMessage: (message) => {
|
|
42672
43511
|
if (live.stored.inviteCode) {
|
|
@@ -42861,7 +43700,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42861
43700
|
get nativeFactories() {
|
|
42862
43701
|
const relay = this.relaysHyperdht ? this.hyperdhtRelay : "";
|
|
42863
43702
|
return {
|
|
42864
|
-
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-
|
|
43703
|
+
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-CEmBpT0Q.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
|
|
42865
43704
|
...this.options.nativeTransports,
|
|
42866
43705
|
...this.ownIrohRelays ? { "iroh/1": (seedB64) => this.options.nativeTransports["iroh/1"](seedB64, this.settings.irohRelays?.length ? { relays: [...this.settings.irohRelays] } : {}) } : {},
|
|
42867
43706
|
...this.options.irohWeb ? { "iroh/1": (seedB64) => createIrohWebEndpoint(seedB64, { relays: this.irohRelays }) } : {}
|
|
@@ -42962,17 +43801,18 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42962
43801
|
return Promise.all(starts).then((full) => {
|
|
42963
43802
|
const live = this.links.get(linkId), link = live?.link;
|
|
42964
43803
|
if (this.shuttingDown || !live || !link || link !== expected) return;
|
|
42965
|
-
|
|
43804
|
+
const group = !!live.stored.group;
|
|
43805
|
+
if (group && this.keepsGroupNative(live.stored)) {
|
|
42966
43806
|
if (full.includes("no-slot") && !link.availableTransports.some((t) => t !== "webrtc/1")) this.groupNativeWaiting.add(linkId);
|
|
42967
43807
|
else this.groupNativeWaiting.delete(linkId);
|
|
42968
|
-
|
|
42969
|
-
|
|
42970
|
-
|
|
42971
|
-
|
|
42972
|
-
|
|
42973
|
-
|
|
42974
|
-
}
|
|
42975
|
-
} 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);
|
|
42976
43816
|
this.emitState();
|
|
42977
43817
|
});
|
|
42978
43818
|
}
|
|
@@ -43130,25 +43970,25 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43130
43970
|
/** Group links waiting for a free native slot (`ensureNativeEndpoints`), by link id. */
|
|
43131
43971
|
groupNativeWaiting = /* @__PURE__ */ new Set();
|
|
43132
43972
|
groupNativeRetryAt = 0;
|
|
43133
|
-
/**
|
|
43134
|
-
|
|
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();
|
|
43135
43975
|
/**
|
|
43136
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.
|
|
43137
|
-
*
|
|
43977
|
+
* Chats and group links whose listener failed to start try again once their wait is over.
|
|
43138
43978
|
*/
|
|
43139
|
-
|
|
43979
|
+
retryNative() {
|
|
43140
43980
|
const now = Date.now();
|
|
43141
|
-
for (const [linkId, failed] of this.
|
|
43981
|
+
for (const [linkId, failed] of this.nativeFailed) {
|
|
43142
43982
|
if (now < failed.at) continue;
|
|
43143
43983
|
if (!this.links.get(linkId)?.link) {
|
|
43144
|
-
this.
|
|
43984
|
+
this.nativeFailed.delete(linkId);
|
|
43145
43985
|
continue;
|
|
43146
43986
|
}
|
|
43147
43987
|
failed.at = now + failed.wait;
|
|
43148
43988
|
this.ensureNativeEndpoints(linkId);
|
|
43149
43989
|
}
|
|
43150
43990
|
if (!this.groupNativeWaiting.size || Date.now() < this.groupNativeRetryAt) return;
|
|
43151
|
-
this.groupNativeRetryAt = Date.now() +
|
|
43991
|
+
this.groupNativeRetryAt = Date.now() + NATIVE_RETRY_MS;
|
|
43152
43992
|
for (const linkId of [...this.groupNativeWaiting]) {
|
|
43153
43993
|
if (!this.links.get(linkId)?.link) {
|
|
43154
43994
|
this.groupNativeWaiting.delete(linkId);
|
|
@@ -43253,6 +44093,24 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43253
44093
|
} else this.placedAt.set(message.linkId, Math.max(last, Math.min(message.timestamp, now)));
|
|
43254
44094
|
return message;
|
|
43255
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
|
+
}
|
|
43256
44114
|
async storeMessage(message) {
|
|
43257
44115
|
await this.storeNewMessage(message);
|
|
43258
44116
|
}
|
|
@@ -43372,7 +44230,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43372
44230
|
transportWait: live.link?.transportWait,
|
|
43373
44231
|
liveAttempt: live.link?.liveAttempt,
|
|
43374
44232
|
liveDialer: live.stored.profile && !live.stored.group ? live.link?.dialer : void 0,
|
|
43375
|
-
transportRttMs: live.link?.rttMs,
|
|
44233
|
+
transportRttMs: live.rttPosted = live.link?.rttMs,
|
|
43376
44234
|
transportRelayed: live.link?.relayedPath,
|
|
43377
44235
|
transportLive: live.transportLog?.liveNow(),
|
|
43378
44236
|
transportLog: stored.id === this.activeLinkId && stored.profile && !stored.group ? stored.transportLog ?? [] : void 0,
|
|
@@ -43430,6 +44288,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43430
44288
|
this.stateTimer = null;
|
|
43431
44289
|
if (this.gatedOut) return;
|
|
43432
44290
|
this.events.onState(this.getState());
|
|
44291
|
+
this.dropBriefViews();
|
|
43433
44292
|
if (this.deviceLinks) this.syncDeviceTokens();
|
|
43434
44293
|
}, delayMs);
|
|
43435
44294
|
}
|
|
@@ -43441,11 +44300,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
43441
44300
|
}
|
|
43442
44301
|
if (this.gatedOut) return;
|
|
43443
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();
|
|
43444
44309
|
}
|
|
43445
44310
|
};
|
|
43446
44311
|
//#endregion
|
|
43447
44312
|
//#region ../browser/src/engine/server.ts
|
|
43448
|
-
var server_exports = /* @__PURE__ */ __exportAll({ EngineServer: () => EngineServer });
|
|
43449
44313
|
var EngineServer = class {
|
|
43450
44314
|
/** The whole engine: never made on a device that is not the active one (`devices/peer.ts` decides, WISP 06 § The gate). */
|
|
43451
44315
|
gated = false;
|
|
@@ -43517,6 +44381,10 @@ var EngineServer = class {
|
|
|
43517
44381
|
get historyMark() {
|
|
43518
44382
|
return this.changes;
|
|
43519
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
|
+
}
|
|
43520
44388
|
/**
|
|
43521
44389
|
* `read`: the chats and groups whose whole history the client read itself, after `mark` (`historyMark`). One that has
|
|
43522
44390
|
* not changed since is not read and sent again: the client hears only what changes in it from now on.
|
|
@@ -43679,6 +44547,6 @@ function signalKind(signal) {
|
|
|
43679
44547
|
}
|
|
43680
44548
|
}
|
|
43681
44549
|
//#endregion
|
|
43682
|
-
export { EngineServer
|
|
44550
|
+
export { EngineServer };
|
|
43683
44551
|
|
|
43684
|
-
//# sourceMappingURL=server-
|
|
44552
|
+
//# sourceMappingURL=server-CBCEYKMN.js.map
|