@ghostlytools/cli 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/SKILL.md +2 -1
- package/dist/assets/{arkade-CtCpFaF7.js → arkade-B-ALutmU.js} +2 -2
- package/dist/assets/{arkade-CtCpFaF7.js.map → arkade-B-ALutmU.js.map} +1 -1
- package/dist/assets/{hyperdhtRelay-C0XRWySP.js → hyperdhtRelay-hKC9DpIH.js} +3 -3
- package/dist/assets/{hyperdhtRelay-C0XRWySP.js.map → hyperdhtRelay-hKC9DpIH.js.map} +1 -1
- package/dist/assets/{mainline-DVYdrus6.js → mainline-DriRu_hb.js} +2 -2
- package/dist/assets/{mainline-DVYdrus6.js.map → mainline-DriRu_hb.js.map} +1 -1
- package/dist/assets/{server-D5FBZYW8.js → server-DHMXMwNn.js} +1177 -215
- package/dist/assets/server-DHMXMwNn.js.map +1 -0
- package/dist/assets/{src-aKGjb_yz.js → src-_FzJkeAJ.js} +1458 -1136
- package/dist/assets/src-_FzJkeAJ.js.map +1 -0
- package/dist/assets/{usdt-CUfucwZf.js → usdt-D0lO2pRR.js} +2 -2
- package/dist/assets/{usdt-CUfucwZf.js.map → usdt-D0lO2pRR.js.map} +1 -1
- package/dist/ghostly.mjs +593 -145
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/server-D5FBZYW8.js.map +0 -1
- package/dist/assets/src-aKGjb_yz.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
|
2
|
-
import { $
|
|
2
|
+
import { $ as parseCommunityEdit, $a as receivedTimestamp, $c as identityFromSeed, $o as HANDOFF_PAKE, $r as vapidKeysMatch, $s as verifyEnrollProof, $t as signDidDhtPacket, A as encodeCommunityLink, Aa as reactionEmoji, Ac as signTurnPacket, Ai as DHT_TEXT_CAPABILITY, Ao as webLocalFetch, Ar as FILE_LIMITS, As as readHandoffTurnFrame, B as GroupWakeLimiter, Bc as normalizeRelayUrl, Bi as identityRevocationValue, Bn as INVITE_TAKEN, Br as mayPin, C as openGroupTyping, Ca as readForwarded, Cc as classifyTurnRead, Co as DHT_POLL_INTERVALS, Cr as pickHubs, Cs as newHandoffId, D as GROUP_READ_NOTE_COMMUNITY, Da as ReactionWindow, Dc as firstDeviceSetSecret, Do as GhostlyHttpError, Dr as readLobby, Ds as readHandoffHave, E as CommunitySession, Ea as REACTION_LIMITS, Ec as TURN_MAX, Eo as presenceSeenAt, Er as readBeaconHead, Es as readHandoffDone, F as knockIdentity, Fa as pairedReplyAuthor, Fc as turnStatedVerifier, Fi as IDENTITY_REVOCATION_LABEL, Fn as findEndpoint, Fo as RTC_CONFIG, Fr as TypingSender, Fs as readSetUpdate, Gc as withRequestOptions, Gi as lastSharedWithMe, Gn as GROUP_FILE_LIMITS, Go as LEGACY_SERVICES, Gr as WAKE_CALL_INTERVAL_MS, Gs as enrollCancelReason, H as groupWakes, Hc as traceLink, Hi as newIdentityBinding, Hn as pairedMessageFrame, Hr as pinIsNewer, I as knockRecords, Ia as readReply, Ic as turnVerifierMark, In as isFederationId, Is as setAckFrame, J as frameBytes, Ja as sameButtonText, Jc as createRelayPayload, Ji as validExternalKey, Jo as HANDOFF_CANCEL, Jr as newWakeToken, Js as enrollGrantFrame, Jt as didDhtDocument, K as GROUP_READ_NOTE, Kc as PacketTooLargeError, Ki as PeerProofs, Ko as serviceIdFromName, Kr as WakeLimiter, Ks as enrollDigits, L as mergeKnocks, La as replySnippet, Lc as DEFAULT_RELAYS, Li as IdentityExchange, Ln as parseCashuRequestPayload, Lr as typingActivity, Ls as setUpdateFrame, M as decodeGroupEntryLink, Ma as readReaction, Mc as turnInstance, Mi as dialDescriptors, Mn as ENDPOINT, Mo as decodeControl, Mr as transferStalled, Ms as sealHandoffPiece, N as encodeGroupEntryLink, Na as validReactionNumber, Nc as turnKeys, Nn as cashuRequestPayload, No as encodeControl, O as communityHasFrame, Oa as nextReactionNumber, Oc as nextTurnPosition, Oo as formatLocalTarget, Or as shouldBeHub, Os as readHandoffHello, P as entryParams, Pa as wireReaction, Pc as turnName, Pn as fedimintRequestPayload, Q as carryMentions, Qa as heardTime, Qc as createIdentity, Qn as readGroupFileMeta, Qo as HANDOFF_OFFER, Qr as checkPushEndpoint, Qs as readEnrollHello, R as readKnocks, Ra as BUTTON_ID, Rc as RelayTransport, Rn as parseFedimintRequestPayload, Rs as signTombstone, S as groupTypingEpoch, Sa as forwardedAgain, Sc as TURN_SETTLE_MS, So as DirectPathWatch, Sr as newerHead, Ss as handoffVersions, St as normalizeAtprotoHandle, T as COMMUNITY_LIMITS, Ta as GROUP_REACTION_FRAME, Tc as TURN_LAST_SEQUENCE, To as RELAY_POLL_INTERVALS, Tr as readBeacon, Ts as readHandoffCancel, U as parseGroupWakeFrame, Ua as STATUS_CARD_LIMITS, Uc as budgetRetryMs, Ui as receivedIdentityStatus, Un as engineError, Uo as emptyLinkRecords, Ur as pinNumberHolds, V as groupWakeFrame, Vi as identityStatement, Vn as LIVENESS_PING_MS, Vo as parseCallSignal, Vr as nextPinNumber, W as GROUP_LIMITS, Wc as isDiscoveryBudgetError, Wi as revokesIdentity, Wn as engineText, Wo as HTTP_SERVICE_PROTO, Wr as readPin, Ws as enrollCancelFrame, Xa as arrivalKey, Xi as automaticTransport, Xn as groupFileFallback, Xo as HANDOFF_HAVE, Xr as relayRequest, Xs as enrollProofFrame, Xt as encodeDidDhtDeactivated, Ya as statusCardText, Yi as TRANSPORTS, Yo as HANDOFF_DONE, Yr as parseWakeFrame, Ys as enrollHelloFrame, Yt as didDhtFromPublicKey, Z as GROUP_EDIT_TEXT_BYTES, Za as claimedTime, Zo as HANDOFF_HELLO, Zr as wakeRequest, Zs as readEnrollGrant, Zt as encodeDidDhtPacket, _a as EDIT_BUFFER_MS, _c as newDeviceSetSecret, _n as sparkInvoiceDetails, _o as MEMBER_KEY, _r as freshHubs, _s as handoffOfferFrame, a as planAppUpdate, ac as decodeInviteCode, ai as STATUS_CARD_CAPABILITY, al as bytesEqual, as as handoffAttemptAllowed, ba as dhtEditId, bn as isPubkyKey, bo as verifyCommitSignature, br as mergeBeacon, bs as handoffRetryAfter, ca as sanitizeFileName, cc as inviteOwnership, ci as HOLD_DATED_BACK_MS, cl as toBase64Url, cn as WALLET_NETWORKS, co as readAppRevocation, cr as readGroupSignal, cs as handoffBusyFrame, d as APP_BUNDLE_LIMITS, dc as HANDOFF_CAPABILITY, di as HoldRefusedError, dl as utf8Encode, dn as formatPaymentAmount, dr as meshHubs, ds as handoffContextProof, ea as DHT_MESSAGE_TTL, ec as createDeviceInvite, ei as chatAppId, el as identityFromSeedB64, en as ETHEREUM_USDT, eo as shownTime, es as HANDOFF_PIECE_BYTES, et as mentionsMember, f as APP_BUNDLE_MAGIC, fc as deviceEchoFrame, fi as isHoldUrl, fn as validatePaymentTarget, fo as canonicalJson, fr as pickMeshHubs, fs as handoffDataFrame, ga as parseVideoMeta, gc as devicePingFrame, gn as sparkAddressKind, gr as doorHubs, gs as handoffManifestFrames, h as appViewOf, hc as deviceLinkPairing, hn as SPARK_PROVIDER, ho as GROUP_MEMBER_CAP, hr as beaconRecords, hs as handoffHelloFrame, i as checkAppBeforeRun, ia as parseImageMeta, ic as createChatInvite, ii as EDIT_CAPABILITY, il as sanitizeAvatar, in as SEPOLIA_TEST_USDT, io as appRef, is as filePartName, ja as reactionIsNewer, jc as signTurnRelease, ji as capsDescriptors, jn as decodeBolt11, jo as LIMITS, jr as transferEnded, js as readHandoffVerified, k as decodeCommunityLink, ka as queueReaction, kc as readTurnPacket, ki as CapsExchange, ko as parseLocalTarget, kr as ChatFiles, ks as readHandoffPake, l as readAppStoreSteps, la as sanitizeMime, lc as DEVICES_CAPABILITY, li as HOLD_LIMITS, ll as toZ32, ln as assertTokenUnits, lr as MESH_HUBS, ls as handoffCancelFrame, m as appDigest, ma as parseVoiceMeta, mc as deviceKeyZ32, mi as readManifest, mn as SPARK_NETWORKS, mo as readCanonicalJson, mr as beaconKeys, ms as handoffEphemeral, n as appStoreDecision, na as DHT_FILE_CAPABILITY, nc as readDeviceInvite, ni as APPS_CAPABILITY, nl as verify, nn as ONCHAIN_PROVIDER, no as APP_STATEMENT_LIMITS, nr as CarriedTransport, ns as HANDOFF_TIMINGS, o as APP_STORE_LIMITS, oa as formatFileSize, oc as encodeInviteCode, oi as UPGRADE_CAPABILITY, ol as fromBase64Url, on as TEST_USDT_FAUCET_AMOUNT, oo as isAppKey, os as handoffAttemptFailed, pa as fileMessageText, pc as deviceEchoNonce, pi as newMailbox, pn as walletNetworkOf, pr as COMMUNITY_TOPOLOGY, ps as handoffDigest, q as GroupSession, qa as checkStatusCard, qi as emptyProofLedger, qo as HANDOFF_ACK, qr as deviceWakeRequest, qs as enrollDoneFrame, r as appUpdateDecision, ra as dhtFileId, rc as OwnInviteError, ri as BUTTONS_CAPABILITY, rn as PaymentPreflightError, ro as appFingerprint, rs as HANDOFF_VERIFIED, sa as safeBlobType, si as HOLD_CAPABILITY, sl as randomBytes, sn as USDT_PUBLIC_RPC, so as isAppRef, sr as groupSignalFrame, ss as handoffAttemptSucceeded, t as appRevocationFor, ta as peekDhtMailbox, tc as deviceInviteJoinerParams, ti as isAppVersion, tl as publicKeyFromZ32, tn as EVM_TEST_CHAINS, ts as HANDOFF_REQUEST, tt as groupName, uc as ENROLL_CAPABILITY, ui as HoldKeys, ul as utf8Decode, un as assertWholeSats, ur as mayBeHub, us as handoffContext, v as isAppUrl, va as EDIT_RATE_WINDOW_MS, vc as seedSigner, vn as isBitcoinAddress, vo as commitHash, vr as lobbyKeys, vs as handoffPakeFrame, w as sealGroupTyping, wa as COMMUNITY_REACTION_FRAME, wc as turnPutSummary, wr as rankHubs, ws as readHandoffAck, x as GroupTypingBoard, xa as parseLinkPreview, xc as TURN_RAISE_READ_TIMEOUT_MS, xo as edgeParams, xr as mergeLobby, xs as handoffStreamKey, xt as isAtprotoDid, y as readAppBundle, ya as RateWindow, yc as webCryptoSigner, yo as rosterHas, yr as lobbyRecords, ys as handoffReleaseFrame, zc as currentRelays, zi as emptyIdentityLedger, zn as GhostLink, zr as PIN_LIMITS, zs as verifySetUpdate } from "./src-_FzJkeAJ.js";
|
|
3
3
|
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-CTF6LF5B.js";
|
|
4
4
|
import { a as fileBytesOf, c as removeFileBytes, i as fileBytes, l as FILE_BYTES_STEP, n as blobDigest } from "./fileBytes-BcYZhNgF.js";
|
|
5
5
|
import { $ as BACKUP_MEDIA_TYPE, A as requireTurn, At as paymentWireId, B as networkLabel, C as breezDatabasesFor, Ct as storedGroupFiles, D as sourceKey, Dt as replyRef, E as ProviderSources, Et as receivedPairedReply, F as sealSeed, Ft as streamStored, G as BITCOIN_INVOICE_ON_TESTNET, H as cashuMint, I as unsealSeed, J as fakesLightning, K as DEFAULT_MINTS, L as ModeChanged, M as setSingleWriterGate, Mt as readStored$1, N as intentRepository, Nt as removeStored, O as awayFrom, Ot as replyTo, P as newDeviceKey, Pt as storedSize, Q as paysItsOwnInvoices, R as ModeGate, S as breezDatabaseInUse, St as groupOfFile, T as lightningPaying, Tt as pairedWireReply, V as CASHU_MINT_SOURCE, W as WALLET_TYPES, X as isWorthlessMint, Y as isTestMint, Z as mintNetwork, _ as LightningCards, _t as verifyIdentity, a as irohRelayProblem, at as reserveAdapterIds, b as normalizePhrase, bt as groupFileId, c as createFailure, ct as SourceConfigError, d as paymentNetwork, dt as networkMode, et as MAX_BACKUP_BYTES, f as paymentNetworksOf, ft as redact, g as CASHU_CARD, gt as boundedIdentityFetch, ht as setOwnDidSource, i as createIrohWebEndpoint, it as registeredOnchainProviders, j as setAwayWallets, jt as FileAppender, k as refuseAway, kt as paymentAlias, l as createTiming, lt as SourceUnreachableError, mt as resolveAtprotoDid, n as loadFedimintSdk, nt as onAdaptersChanged, o as WALLET_NAMES, ot as NothingSpentError, p as walletInstances, pt as assertPublicServer, q as TEST_MINT, r as DEFAULT_IROH_RELAYS, rt as registeredLightningProviders, s as assertConfirmedReal, st as PROVIDER_PLATFORMS, t as fedimintDatabase, tt as identityProviders, u as crossNetwork, ut as isNothingSpentError, v as cardsKey, vt as chosenResolver, w as dropBreezDatabase, wt as groupReply, x as breezDatabase, xt as groupFileText, y as isRecoveryPhrase, yt as GroupFileDesk, z as WrongNetworkError } from "../ghostly.mjs";
|
|
@@ -180,7 +180,7 @@ async function migrateWalletNetworks(now = Date.now()) {
|
|
|
180
180
|
//#endregion
|
|
181
181
|
//#region ../browser/src/engine/paymentAdapters/usdtWallet.ts
|
|
182
182
|
/** 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-
|
|
183
|
+
var loadUsdt = () => import("./usdt-D0lO2pRR.js");
|
|
184
184
|
/** Every new profile starts with this wallet: an address to receive on, nothing to set up. */
|
|
185
185
|
var DEFAULT_USDT = {
|
|
186
186
|
network: "ethereum",
|
|
@@ -642,7 +642,9 @@ async function restoreArkDatabase(walletId, snapshot) {
|
|
|
642
642
|
//#endregion
|
|
643
643
|
//#region ../browser/src/engine/paymentAdapters/arkWallet.ts
|
|
644
644
|
/** 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-
|
|
645
|
+
var loadArkade = () => import("./arkade-B-ALutmU.js");
|
|
646
|
+
/** How long a Testnet wallet about to make way for another waits for its server to say what it holds. */
|
|
647
|
+
var RETIRE_READ_MS = 1e4;
|
|
646
648
|
/** Every new profile starts with this wallet: nothing to set up before receiving. */
|
|
647
649
|
var DEFAULT_ARK = {
|
|
648
650
|
network: "bitcoin",
|
|
@@ -1048,10 +1050,23 @@ var ArkWallet = class {
|
|
|
1048
1050
|
async retirable(refusal) {
|
|
1049
1051
|
const saved = this.saved;
|
|
1050
1052
|
if ((await wrap((await store(STORES$1.intents, "readonly")).getAll())).some((i) => this.ours(i))) throw new Error(refusal);
|
|
1051
|
-
if (this.adapter && await this.gate.within(this.adapter
|
|
1053
|
+
if (this.adapter && await this.gate.within(this.held(this.adapter)) > 0) throw new Error(refusal);
|
|
1052
1054
|
await this.lock();
|
|
1053
1055
|
return saved;
|
|
1054
1056
|
}
|
|
1057
|
+
/**
|
|
1058
|
+
* What the wallet holds, asked of its server. A test network's server that stopped answering must not hold the
|
|
1059
|
+
* wallet on it (one that never answered does not either): past RETIRE_READ_MS the last balance read stands.
|
|
1060
|
+
* Mainnet waits for its server's own word.
|
|
1061
|
+
*/
|
|
1062
|
+
held(adapter) {
|
|
1063
|
+
const asked = adapter.balance();
|
|
1064
|
+
if (this.network !== "testnet") return asked;
|
|
1065
|
+
return new Promise((resolve, reject) => {
|
|
1066
|
+
const timer = setTimeout(() => resolve(this.view.balance), RETIRE_READ_MS);
|
|
1067
|
+
asked.then(resolve, reject).finally(() => clearTimeout(timer));
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1055
1070
|
/** A payment of this wallet's network (the other network's wallet has its own). */
|
|
1056
1071
|
ours(intent) {
|
|
1057
1072
|
return intent.review.method === "arkade" && arkMode(intent.review.network) === this.network;
|
|
@@ -1876,13 +1891,24 @@ var BarkWallet = class {
|
|
|
1876
1891
|
async retirable(refusal) {
|
|
1877
1892
|
const saved = this.saved;
|
|
1878
1893
|
if ((await intentRepository.list()).some((i) => this.ours(i))) throw new Error(refusal);
|
|
1879
|
-
if (this.adapter)
|
|
1880
|
-
const b = await this.gate.within(this.adapter.balance());
|
|
1881
|
-
if (b.spendableSats + b.pendingInRoundSats + b.pendingBoardSats + b.pendingExitSats + b.pendingLightningSendSats + b.claimableLightningReceiveSats > 0) throw new Error(refusal);
|
|
1882
|
-
}
|
|
1894
|
+
if (this.adapter && await this.gate.within(this.held(this.adapter)) > 0) throw new Error(refusal);
|
|
1883
1895
|
await this.lock();
|
|
1884
1896
|
return saved;
|
|
1885
1897
|
}
|
|
1898
|
+
/**
|
|
1899
|
+
* What the wallet holds, asked of its server: spendable, on its way in and on its way out. A test network's server
|
|
1900
|
+
* that stopped answering must not hold the wallet on it (one that never answered does not either): past
|
|
1901
|
+
* RETIRE_READ_MS the last reading stands, whose three sums are those same six parts. Mainnet waits for its
|
|
1902
|
+
* server's own word.
|
|
1903
|
+
*/
|
|
1904
|
+
held(adapter) {
|
|
1905
|
+
const asked = adapter.balance().then((b) => b.spendableSats + b.pendingInRoundSats + b.pendingBoardSats + b.pendingExitSats + b.pendingLightningSendSats + b.claimableLightningReceiveSats);
|
|
1906
|
+
if (this.network !== "testnet") return asked;
|
|
1907
|
+
return new Promise((resolve, reject) => {
|
|
1908
|
+
const timer = setTimeout(() => resolve(this.view.balance + (this.view.pending ?? 0) + (this.view.exiting ?? 0)), RETIRE_READ_MS);
|
|
1909
|
+
asked.then(resolve, reject).finally(() => clearTimeout(timer));
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1886
1912
|
/** A payment of this wallet's network (the other network's wallet has its own). */
|
|
1887
1913
|
ours(intent) {
|
|
1888
1914
|
return intent.review.method === "bark" && barkMode(intent.review.network) === this.network;
|
|
@@ -11133,7 +11159,13 @@ var APP_STORAGE_LIMITS = {
|
|
|
11133
11159
|
keyBytes: 256,
|
|
11134
11160
|
/** A value, as compact JSON in UTF-8 bytes. */
|
|
11135
11161
|
valueBytes: 65536,
|
|
11136
|
-
/**
|
|
11162
|
+
/**
|
|
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 and the key again, in the row's own key and in its two indexes). Without it a scope of one-byte values
|
|
11165
|
+
* held 20 times its 5 MiB.
|
|
11166
|
+
*/
|
|
11167
|
+
rowBytes: 128,
|
|
11168
|
+
/** Every key and value of one app in one scope, with `rowBytes` for each key. */
|
|
11137
11169
|
scopeBytes: 5242880
|
|
11138
11170
|
};
|
|
11139
11171
|
var APP_CHECK_TIMINGS = {
|
|
@@ -11145,6 +11177,11 @@ var APP_CHECK_TIMINGS = {
|
|
|
11145
11177
|
};
|
|
11146
11178
|
/** Revocations are checked this long at a time before the page's thread is let go. */
|
|
11147
11179
|
var REVOCATIONS_SLICE_MS = 10;
|
|
11180
|
+
/**
|
|
11181
|
+
* Revocations of store indexes whose signature was checked, kept by `appRevocationId` so an index that changes around
|
|
11182
|
+
* them does not have each checked again (the one read longest ago dropped first): four indexes' worth, about 1 MiB.
|
|
11183
|
+
*/
|
|
11184
|
+
var STORE_REVOCATIONS_KEPT = 4 * APP_STORE_LIMITS.revoked;
|
|
11148
11185
|
/** `ghostly-revoke.json` files whose result is kept, so an unchanged one is not checked again (the oldest dropped first). */
|
|
11149
11186
|
var REVOCATION_FILES_KEPT = 64;
|
|
11150
11187
|
/** A fetched bundle kept for its install screen: at most this many, for this long. */
|
|
@@ -11155,6 +11192,8 @@ var STAGED = {
|
|
|
11155
11192
|
/** The first bytes of a bundle: magic, the manifest's length and the largest manifest. */
|
|
11156
11193
|
var PEEK_BYTES = APP_BUNDLE_MAGIC.length + 4 + APP_BUNDLE_LIMITS.manifestBytes;
|
|
11157
11194
|
var bundleId = (digest) => `app-${digest}`;
|
|
11195
|
+
/** What a row takes of its scope's 5 MiB: its key and value, and `rowBytes`. Not kept in the row, so rows from before count alike. */
|
|
11196
|
+
var costOf = (row) => APP_STORAGE_LIMITS.rowBytes + row.size;
|
|
11158
11197
|
/**
|
|
11159
11198
|
* A listing's URLs this client reads, in order, a jsDelivr copy first: one this client reads names a full commit, so it
|
|
11160
11199
|
* holds exactly the version the store reviewed, where a raw `HEAD` may have moved on to one it has not (WISP 1200 §
|
|
@@ -11231,7 +11270,7 @@ var Apps = class {
|
|
|
11231
11270
|
/** Reads of a bundle under way, by digest: icons drawn at once share one read. */
|
|
11232
11271
|
verifying = /* @__PURE__ */ new Map();
|
|
11233
11272
|
/**
|
|
11234
|
-
* The `icon.png` of each checked bundle, by digest (at most 256 KiB, or null for none): every icon shown on the Apps
|
|
11273
|
+
* The `icon.png` of each checked bundle, by digest (at most 256 KiB and 1024 × 1024 pixels, or null for none): every icon shown on the Apps
|
|
11235
11274
|
* page, in a chat's cards and in the composer's Apps is read without the whole bundle again.
|
|
11236
11275
|
*/
|
|
11237
11276
|
icons = /* @__PURE__ */ new Map();
|
|
@@ -11244,6 +11283,13 @@ var Apps = class {
|
|
|
11244
11283
|
checking = null;
|
|
11245
11284
|
/** Runs the person started with Run anyway (`<ref> <digest>`), for this engine's life: a removal does not cut them off. */
|
|
11246
11285
|
anyway = /* @__PURE__ */ new Set();
|
|
11286
|
+
/** The apps started here (`entry`), for this engine's life: one of them uninstalled since may still have a frame open. */
|
|
11287
|
+
ran = /* @__PURE__ */ new Set();
|
|
11288
|
+
/**
|
|
11289
|
+
* The versions started here (`entry`), by reference: digest to `sequence`, for this engine's life. A frame goes on
|
|
11290
|
+
* running the version it started with whatever is installed since, so that version is the one judged when it asks.
|
|
11291
|
+
*/
|
|
11292
|
+
started = /* @__PURE__ */ new Map();
|
|
11247
11293
|
views = null;
|
|
11248
11294
|
held = /* @__PURE__ */ new Map();
|
|
11249
11295
|
heldStores = null;
|
|
@@ -11254,6 +11300,19 @@ var Apps = class {
|
|
|
11254
11300
|
listingMisses = /* @__PURE__ */ new Map();
|
|
11255
11301
|
/** `ghostly-revoke.json` files checked, by the SHA-256 of their bytes: their revocations, or null when refused. */
|
|
11256
11302
|
revocationFiles = /* @__PURE__ */ new Map();
|
|
11303
|
+
/** Revocations read in a store's index and verified, by `appRevocationId`: at most `STORE_REVOCATIONS_KEPT`. */
|
|
11304
|
+
storeRevocations = /* @__PURE__ */ new Set();
|
|
11305
|
+
checkedRevocations = {
|
|
11306
|
+
has: (id) => {
|
|
11307
|
+
if (!this.storeRevocations.delete(id)) return false;
|
|
11308
|
+
this.storeRevocations.add(id);
|
|
11309
|
+
return true;
|
|
11310
|
+
},
|
|
11311
|
+
add: (id) => {
|
|
11312
|
+
this.storeRevocations.add(id);
|
|
11313
|
+
if (this.storeRevocations.size > STORE_REVOCATIONS_KEPT) this.storeRevocations.delete(this.storeRevocations.values().next().value);
|
|
11314
|
+
}
|
|
11315
|
+
};
|
|
11257
11316
|
stopped = false;
|
|
11258
11317
|
constructor(host) {
|
|
11259
11318
|
this.host = host;
|
|
@@ -11337,22 +11396,48 @@ var Apps = class {
|
|
|
11337
11396
|
* a store of the person's removed (unless this run was started with Run anyway), is cut off with `stopped`, whether
|
|
11338
11397
|
* or not the page stops its frame.
|
|
11339
11398
|
*/
|
|
11340
|
-
async stillRunnable(app) {
|
|
11341
|
-
const
|
|
11399
|
+
async stillRunnable(app, digest) {
|
|
11400
|
+
const version = this.versionRunning(app, digest);
|
|
11401
|
+
const run = this.runStatus(version, await this.heldViews(), app.revocations);
|
|
11342
11402
|
if (run.status === "revoked") fail("stopped", "Its publisher revoked this version");
|
|
11343
|
-
if (run.status === "removed" && !this.anyway.has(`${
|
|
11403
|
+
if (run.status === "removed" && !this.anyway.has(`${version.ref} ${version.digest}`)) fail("stopped", `Removed by ${run.by[0].name}: ${run.by[0].reason}`);
|
|
11344
11404
|
return app;
|
|
11345
11405
|
}
|
|
11346
|
-
|
|
11347
|
-
|
|
11406
|
+
/**
|
|
11407
|
+
* The version a frame runs: the one it names by `digest` (what `entry` gave it), which an update installed since does
|
|
11408
|
+
* not change. None named: the installed one. One never started here does not run.
|
|
11409
|
+
*/
|
|
11410
|
+
versionRunning(app, digest) {
|
|
11411
|
+
if (digest === void 0 || digest === app.digest) return app;
|
|
11412
|
+
const sequence = typeof digest === "string" ? this.started.get(app.ref)?.get(digest) : void 0;
|
|
11413
|
+
if (sequence === void 0) fail("stopped", "This version is not running");
|
|
11414
|
+
return {
|
|
11415
|
+
ref: app.ref,
|
|
11416
|
+
sequence,
|
|
11417
|
+
digest
|
|
11418
|
+
};
|
|
11419
|
+
}
|
|
11420
|
+
/** The versions started here that are not the installed one any more. */
|
|
11421
|
+
startedBefore(app) {
|
|
11422
|
+
return [...this.started.get(app.ref) ?? []].filter(([digest]) => digest !== app.digest).map(([digest, sequence]) => ({
|
|
11423
|
+
ref: app.ref,
|
|
11424
|
+
sequence,
|
|
11425
|
+
digest
|
|
11426
|
+
}));
|
|
11427
|
+
}
|
|
11428
|
+
async runningOrFail(ref, digest) {
|
|
11429
|
+
return this.stillRunnable(await this.installedOrFail(ref), digest);
|
|
11348
11430
|
}
|
|
11349
11431
|
/**
|
|
11350
11432
|
* Before an app's frames go to a contact (`appOpen`, `appSend`): refused with `stopped` as its storage is. A reference
|
|
11351
|
-
* not installed here (a bot that speaks an app's messages) is not this store's to refuse
|
|
11433
|
+
* not installed here (a bot that speaks an app's messages) is not this store's to refuse, unless it ran here and was
|
|
11434
|
+
* uninstalled since: the person removed it, so a frame of it still open says nothing more (`not-installed`). `digest`:
|
|
11435
|
+
* the version the frame runs (`versionRunning`).
|
|
11352
11436
|
*/
|
|
11353
|
-
async chatRunnable({ ref }) {
|
|
11437
|
+
async chatRunnable({ ref, digest }) {
|
|
11354
11438
|
const app = isAppRef(ref) ? await this.app(ref) : void 0;
|
|
11355
|
-
if (app
|
|
11439
|
+
if (!app && this.ran.has(ref)) fail("not-installed", "This app is not installed");
|
|
11440
|
+
if (app) await this.stillRunnable(app, digest);
|
|
11356
11441
|
}
|
|
11357
11442
|
async putApp(app) {
|
|
11358
11443
|
try {
|
|
@@ -11543,7 +11628,10 @@ var Apps = class {
|
|
|
11543
11628
|
}
|
|
11544
11629
|
/**
|
|
11545
11630
|
* Reads the index at `url`. With the index held for this store, bytes equal to it (by digest) are the held index: its
|
|
11546
|
-
* signature and every revocation in it were checked when it was read, so only its expiry is taken again.
|
|
11631
|
+
* signature and every revocation in it were checked when it was read, so only its expiry is taken again. Any other
|
|
11632
|
+
* index is read with a pause every few ms (the engine shares the page's thread, and an index holds up to 4096
|
|
11633
|
+
* listings and 4096 revocations, each of these a signature check), and a revocation checked in an earlier read, of
|
|
11634
|
+
* this store or another, a preview included, is not checked again.
|
|
11547
11635
|
*/
|
|
11548
11636
|
async readStoreAt(url, heldKey, held) {
|
|
11549
11637
|
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 })]);
|
|
@@ -11555,7 +11643,16 @@ var Apps = class {
|
|
|
11555
11643
|
expired: held.index.expires < this.nowS()
|
|
11556
11644
|
}
|
|
11557
11645
|
};
|
|
11558
|
-
|
|
11646
|
+
const steps = readAppStoreSteps(indexBytes, sigBytes, this.nowS(), heldKey, this.checkedRevocations);
|
|
11647
|
+
let since = performance.now();
|
|
11648
|
+
for (;;) {
|
|
11649
|
+
const step = steps.next();
|
|
11650
|
+
if (step.done) return step.value;
|
|
11651
|
+
if (performance.now() - since >= REVOCATIONS_SLICE_MS) {
|
|
11652
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
11653
|
+
since = performance.now();
|
|
11654
|
+
}
|
|
11655
|
+
}
|
|
11559
11656
|
}
|
|
11560
11657
|
/**
|
|
11561
11658
|
* The revocations its publisher signed, read beside each URL (`ghostly-revoke.json`); none there is not an error.
|
|
@@ -12043,13 +12140,14 @@ var Apps = class {
|
|
|
12043
12140
|
maxBytes: PEEK_BYTES,
|
|
12044
12141
|
peek: true
|
|
12045
12142
|
}));
|
|
12046
|
-
if (peek && newer(peek)) candidates.set(url, {});
|
|
12143
|
+
if (peek && newer(peek)) candidates.set(url, { holds: peek });
|
|
12047
12144
|
} catch {}
|
|
12048
12145
|
}
|
|
12049
12146
|
const revokeRead = /* @__PURE__ */ new Set();
|
|
12050
12147
|
const before = await this.readRevocations(app.ref, [app.from, ...app.manifest.sources ?? []], revokeRead);
|
|
12051
12148
|
app = withRevocations(app, before ?? []);
|
|
12052
12149
|
for (const [url, want] of candidates) {
|
|
12150
|
+
let holds = want.holds;
|
|
12053
12151
|
if (want.peek) {
|
|
12054
12152
|
let peek;
|
|
12055
12153
|
try {
|
|
@@ -12064,7 +12162,10 @@ var Apps = class {
|
|
|
12064
12162
|
if (want.miss) this.listingMisses.set(...want.miss);
|
|
12065
12163
|
continue;
|
|
12066
12164
|
}
|
|
12165
|
+
holds = peek;
|
|
12067
12166
|
}
|
|
12167
|
+
const twin = holds && (holds.sequence === app.sequence || holds.sequence === app.pending?.sequence);
|
|
12168
|
+
if (holds && !twin && this.runStatus(holds, this.storeViews(stores), app.revocations).status !== "ok") continue;
|
|
12068
12169
|
let fetched;
|
|
12069
12170
|
try {
|
|
12070
12171
|
fetched = await this.fetchBundle(url);
|
|
@@ -12142,7 +12243,11 @@ var Apps = class {
|
|
|
12142
12243
|
}
|
|
12143
12244
|
const after = await this.readRevocations(app.ref, [app.from, ...app.manifest.sources ?? []], revokeRead);
|
|
12144
12245
|
const revocations = before || after ? [...before ?? [], ...after ?? []] : null;
|
|
12145
|
-
const versions = [
|
|
12246
|
+
const versions = [
|
|
12247
|
+
app,
|
|
12248
|
+
...app.pending ? [versionOf(app.pending.manifest, app.pending.digest)] : [],
|
|
12249
|
+
...this.startedBefore(app)
|
|
12250
|
+
];
|
|
12146
12251
|
const stays = (app.revocations ?? []).filter((r) => !revocations || versions.some((v) => appRevocationFor(v, [r])));
|
|
12147
12252
|
const merged = new Map([...stays, ...revocations ?? []].map((r) => [canonicalJson(r), r]));
|
|
12148
12253
|
if ((await this.app(app.ref))?.store === void 0 && app.store !== void 0) {
|
|
@@ -12233,6 +12338,9 @@ var Apps = class {
|
|
|
12233
12338
|
if (run.status === "removed") this.anyway.add(`${app.ref} ${app.digest}`);
|
|
12234
12339
|
const bundle = await this.verifiedBundle(app);
|
|
12235
12340
|
const entry = bundle.files.get(bundle.manifest.entry) ?? fail("damaged", "The entry is missing");
|
|
12341
|
+
this.ran.add(app.ref);
|
|
12342
|
+
const versions = this.started.get(app.ref) ?? /* @__PURE__ */ new Map();
|
|
12343
|
+
this.started.set(app.ref, versions.set(app.digest, app.sequence));
|
|
12236
12344
|
return {
|
|
12237
12345
|
ref: app.ref,
|
|
12238
12346
|
digest: app.digest,
|
|
@@ -12243,9 +12351,10 @@ var Apps = class {
|
|
|
12243
12351
|
entry: utf8Decode(entry)
|
|
12244
12352
|
};
|
|
12245
12353
|
}
|
|
12246
|
-
/** A file of the installed bundle, for the broker's `file` (`ghostly.file(path)`). */
|
|
12247
|
-
async file({ ref, path }) {
|
|
12248
|
-
const app = path === "icon.png" ? await this.installedOrFail(ref) : await this.runningOrFail(ref);
|
|
12354
|
+
/** A file of the installed bundle, for the broker's `file` (`ghostly.file(path)`). `digest`: the version the frame runs. */
|
|
12355
|
+
async file({ ref, path, digest }) {
|
|
12356
|
+
const app = path === "icon.png" ? await this.installedOrFail(ref) : await this.runningOrFail(ref, digest);
|
|
12357
|
+
if (path !== "icon.png" && digest !== void 0 && digest !== app.digest) fail("no-file", "No such file in this app");
|
|
12249
12358
|
const icon = path === "icon.png" ? this.icons.get(app.digest) : void 0;
|
|
12250
12359
|
if (icon) return icon.slice();
|
|
12251
12360
|
if (icon === null) fail("no-file", "No such file in this app");
|
|
@@ -12256,6 +12365,10 @@ var Apps = class {
|
|
|
12256
12365
|
async view(app, stores) {
|
|
12257
12366
|
const { manifest } = app;
|
|
12258
12367
|
const listedBy = this.listedBy(app.ref, stores);
|
|
12368
|
+
const stopped = this.startedBefore(app).map((v) => ({
|
|
12369
|
+
digest: v.digest,
|
|
12370
|
+
run: this.runStatus(v, this.storeViews(stores), app.revocations)
|
|
12371
|
+
})).filter((v) => v.run.status !== "ok");
|
|
12259
12372
|
return {
|
|
12260
12373
|
ref: app.ref,
|
|
12261
12374
|
name: app.name,
|
|
@@ -12285,7 +12398,8 @@ var Apps = class {
|
|
|
12285
12398
|
sequence: app.equivocation.sequence,
|
|
12286
12399
|
at: app.equivocation.at
|
|
12287
12400
|
} },
|
|
12288
|
-
...app.checkedAt !== void 0 && { checkedAt: app.checkedAt }
|
|
12401
|
+
...app.checkedAt !== void 0 && { checkedAt: app.checkedAt },
|
|
12402
|
+
...stopped.length && { stopped }
|
|
12289
12403
|
};
|
|
12290
12404
|
}
|
|
12291
12405
|
async list() {
|
|
@@ -12307,6 +12421,7 @@ var Apps = class {
|
|
|
12307
12421
|
this.held.delete(app.ref);
|
|
12308
12422
|
}
|
|
12309
12423
|
for (const key of [...this.usage.keys()]) if (key.startsWith(`${app.ref}\0`)) this.usage.delete(key);
|
|
12424
|
+
this.started.delete(app.ref);
|
|
12310
12425
|
await this.removeBundle(app.bytes, app.digest);
|
|
12311
12426
|
if (app.pending) await this.removeBundle(app.pending.bytes, app.pending.digest);
|
|
12312
12427
|
}
|
|
@@ -12332,13 +12447,13 @@ var Apps = class {
|
|
|
12332
12447
|
const id = `${ref}\0${scope}`;
|
|
12333
12448
|
let used = this.usage.get(id);
|
|
12334
12449
|
if (used === void 0) {
|
|
12335
|
-
used = (await wrap((await store(STORES$1.appStorage, "readonly")).index(APP_STORAGE_SCOPE_INDEX).getAll([ref, scope]))).reduce((sum, r) => sum + r
|
|
12450
|
+
used = (await wrap((await store(STORES$1.appStorage, "readonly")).index(APP_STORAGE_SCOPE_INDEX).getAll([ref, scope]))).reduce((sum, r) => sum + costOf(r), 0);
|
|
12336
12451
|
this.usage.set(id, used);
|
|
12337
12452
|
}
|
|
12338
12453
|
return used;
|
|
12339
12454
|
}
|
|
12340
|
-
async storageGet({ ref, scope, key }) {
|
|
12341
|
-
const app = await this.runningOrFail(ref);
|
|
12455
|
+
async storageGet({ ref, scope, key, digest }) {
|
|
12456
|
+
const app = await this.runningOrFail(ref, digest);
|
|
12342
12457
|
const row = await wrap((await store(STORES$1.appStorage, "readonly")).get([
|
|
12343
12458
|
app.ref,
|
|
12344
12459
|
this.scopeOf(scope),
|
|
@@ -12346,9 +12461,13 @@ var Apps = class {
|
|
|
12346
12461
|
]));
|
|
12347
12462
|
return row ? { value: JSON.parse(row.value) } : null;
|
|
12348
12463
|
}
|
|
12349
|
-
/**
|
|
12350
|
-
|
|
12351
|
-
|
|
12464
|
+
/**
|
|
12465
|
+
* Keeps a JSON value under a key: at most 64 KiB, and 5 MiB for every key of this app in this scope, each counted
|
|
12466
|
+
* with `rowBytes`. A scope already past that (filled before a key counted `rowBytes`) still takes a write that does
|
|
12467
|
+
* not grow it, so the app can make room.
|
|
12468
|
+
*/
|
|
12469
|
+
async storageSet({ ref, scope, key, value, digest }) {
|
|
12470
|
+
const app = await this.runningOrFail(ref, digest);
|
|
12352
12471
|
const where = this.scopeOf(scope), name = this.keyOf(key);
|
|
12353
12472
|
let text;
|
|
12354
12473
|
try {
|
|
@@ -12363,12 +12482,13 @@ var Apps = class {
|
|
|
12363
12482
|
await this.serial(`${app.ref}\0${where}`, async () => {
|
|
12364
12483
|
const used = await this.usageOf(app.ref, where);
|
|
12365
12484
|
const rows = await store(STORES$1.appStorage, "readonly");
|
|
12366
|
-
const
|
|
12485
|
+
const old = await wrap(rows.get([
|
|
12367
12486
|
app.ref,
|
|
12368
12487
|
where,
|
|
12369
12488
|
name
|
|
12370
|
-
]))
|
|
12371
|
-
|
|
12489
|
+
]));
|
|
12490
|
+
const next = used - (old ? costOf(old) : 0) + APP_STORAGE_LIMITS.rowBytes + size;
|
|
12491
|
+
if (next > APP_STORAGE_LIMITS.scopeBytes && next > used) fail("full", "This app's storage in this chat is full (5 MiB)");
|
|
12372
12492
|
const writing = await store(STORES$1.appStorage, "readwrite");
|
|
12373
12493
|
if (where !== "alone" && !this.host.isChat(where)) {
|
|
12374
12494
|
this.usage.delete(`${app.ref}\0${where}`);
|
|
@@ -12384,8 +12504,8 @@ var Apps = class {
|
|
|
12384
12504
|
this.usage.set(`${app.ref}\0${where}`, next);
|
|
12385
12505
|
});
|
|
12386
12506
|
}
|
|
12387
|
-
async storageDelete({ ref, scope, key }) {
|
|
12388
|
-
const app = await this.runningOrFail(ref);
|
|
12507
|
+
async storageDelete({ ref, scope, key, digest }) {
|
|
12508
|
+
const app = await this.runningOrFail(ref, digest);
|
|
12389
12509
|
const where = this.scopeOf(scope), name = this.keyOf(key);
|
|
12390
12510
|
await this.serial(`${app.ref}\0${where}`, async () => {
|
|
12391
12511
|
const used = await this.usageOf(app.ref, where);
|
|
@@ -12400,11 +12520,11 @@ var Apps = class {
|
|
|
12400
12520
|
where,
|
|
12401
12521
|
name
|
|
12402
12522
|
]));
|
|
12403
|
-
this.usage.set(`${app.ref}\0${where}`, used - old
|
|
12523
|
+
this.usage.set(`${app.ref}\0${where}`, used - costOf(old));
|
|
12404
12524
|
});
|
|
12405
12525
|
}
|
|
12406
|
-
async storageKeys({ ref, scope }) {
|
|
12407
|
-
const app = await this.runningOrFail(ref);
|
|
12526
|
+
async storageKeys({ ref, scope, digest }) {
|
|
12527
|
+
const app = await this.runningOrFail(ref, digest);
|
|
12408
12528
|
return (await wrap((await store(STORES$1.appStorage, "readonly")).getAllKeys(rowsOf(app.ref, this.scopeOf(scope))))).map((k) => k[2]);
|
|
12409
12529
|
}
|
|
12410
12530
|
/** An app's storage in every scope, as export files (WISP 1200 § Where installed apps live), offered before an uninstall. */
|
|
@@ -13955,6 +14075,23 @@ function takeoverTurn(record, read) {
|
|
|
13955
14075
|
if (highest + 1 > TURN_MAX) throw new TurnClosedError();
|
|
13956
14076
|
return highest + 1;
|
|
13957
14077
|
}
|
|
14078
|
+
/**
|
|
14079
|
+
* The `instance` of the record a device writes as the active one: it states the mark of the verifier beside its device
|
|
14080
|
+
* state (`turnInstance`, WISP 06 § Record), which on an active device is the profile's. `unstated`: a handoff's taker,
|
|
14081
|
+
* whose device state does not hold the verifier of the profile it just installed; it states it once its engine runs.
|
|
14082
|
+
*/
|
|
14083
|
+
function instanceFor(record, unstated = false) {
|
|
14084
|
+
return turnInstance(unstated ? null : turnVerifierMark(record.verifier ?? null));
|
|
14085
|
+
}
|
|
14086
|
+
/**
|
|
14087
|
+
* The last record of the device set this device knows after a read: the one the sources returned when it is above
|
|
14088
|
+
* the packet it stored, else the stored one. A lower record (`behind`) and a tombstone say nothing here.
|
|
14089
|
+
*/
|
|
14090
|
+
function latestKnown(read, stored) {
|
|
14091
|
+
const news = (read.result === "other" || read.result === "clone" || read.result === "mine") && read.record && !read.record.tombstone ? read.record : null;
|
|
14092
|
+
if (!news) return stored;
|
|
14093
|
+
return !stored || news.sequence >= stored.sequence ? news : stored;
|
|
14094
|
+
}
|
|
13958
14095
|
/** The record's slots as the device state keeps them. */
|
|
13959
14096
|
function deviceSetOf(record) {
|
|
13960
14097
|
return record.slots.map((slot) => slot && {
|
|
@@ -14150,7 +14287,7 @@ var TurnKeeper = class {
|
|
|
14150
14287
|
author: ownSlot,
|
|
14151
14288
|
active: ownSlot,
|
|
14152
14289
|
slots,
|
|
14153
|
-
instance:
|
|
14290
|
+
instance: this.options.instance?.() ?? instanceFor(record, !!options.release),
|
|
14154
14291
|
...release ? { release } : {}
|
|
14155
14292
|
};
|
|
14156
14293
|
const payload = await signTurnPacket(keys, fields, this.options.signer);
|
|
@@ -14171,6 +14308,31 @@ var TurnKeeper = class {
|
|
|
14171
14308
|
return this.put(held, payload, conditions, raising);
|
|
14172
14309
|
}
|
|
14173
14310
|
/**
|
|
14311
|
+
* The active device says which verifier the profile has now (WISP 06 § Record, the verifier mark): when the record it
|
|
14312
|
+
* stored states another one (the lock password changed, or the record was written before the verifier was at hand),
|
|
14313
|
+
* it reads, and on `mine` or `none` writes its next record. True when the stored record states it (already, or now
|
|
14314
|
+
* written); false when nothing was written (not the active device, no good read, another device's record there: the
|
|
14315
|
+
* next check or start settles that, and every start writes a record anyway). Null for a `single` profile.
|
|
14316
|
+
*/
|
|
14317
|
+
stateVerifier() {
|
|
14318
|
+
return this.exclusive(async () => {
|
|
14319
|
+
const held = await this.held();
|
|
14320
|
+
if (!held) return null;
|
|
14321
|
+
if (held.record.state !== "active" || !held.wrote || !held.storedRecord) return false;
|
|
14322
|
+
const stated = turnStatedVerifier(held.storedRecord.instance);
|
|
14323
|
+
if (stated && bytesEqual(stated, turnVerifierMark(held.record.verifier ?? null))) return true;
|
|
14324
|
+
const read = await this.readWith(held);
|
|
14325
|
+
if (!read.good || read.result !== "mine" && read.result !== "none") return false;
|
|
14326
|
+
try {
|
|
14327
|
+
await this.writeWith(held, conditionsAfter(read, read.result === "none" ? "none" : "seen"));
|
|
14328
|
+
} catch (error) {
|
|
14329
|
+
if (error instanceof TurnClosedError) return false;
|
|
14330
|
+
throw error;
|
|
14331
|
+
}
|
|
14332
|
+
return true;
|
|
14333
|
+
});
|
|
14334
|
+
}
|
|
14335
|
+
/**
|
|
14174
14336
|
* A forced takeover (WISP 06 § Settle): reads the turn, makes the device `taking` (with the state it had, to go
|
|
14175
14337
|
* back to), writes the record of `turn` with no release within `P` of that read, and settles as every device that
|
|
14176
14338
|
* raises the turn does. `start` says it is the active device now; `gated` that it lost and is what it was before.
|
|
@@ -14184,6 +14346,7 @@ var TurnKeeper = class {
|
|
|
14184
14346
|
if (prior !== "standby" && prior !== "superseded") throw new Error(`A ${prior} device does not force a takeover`);
|
|
14185
14347
|
const read = await this.readWith(held, true);
|
|
14186
14348
|
if (!read.good || read.result === "tombstone") return this.settle(held, read, true);
|
|
14349
|
+
options.before?.(latestKnown(read, held.storedRecord));
|
|
14187
14350
|
const turn = options.turn ?? takeoverTurn(held.record, read);
|
|
14188
14351
|
const stopping = held.record.activeSlot !== void 0 && held.record.activeSlot !== held.ownSlot ? held.record.deviceSet[held.record.activeSlot]?.key : void 0;
|
|
14189
14352
|
held.record = await this.store.move(this.options.profile, "taking", { handoff: {
|
|
@@ -14193,8 +14356,8 @@ var TurnKeeper = class {
|
|
|
14193
14356
|
} });
|
|
14194
14357
|
try {
|
|
14195
14358
|
await this.writeWith(held, read.conditions, {
|
|
14196
|
-
|
|
14197
|
-
|
|
14359
|
+
turn,
|
|
14360
|
+
...options.slots ? { slots: options.slots } : {}
|
|
14198
14361
|
}, true);
|
|
14199
14362
|
} catch (error) {
|
|
14200
14363
|
if (!held.wroteNow) held.record = await this.store.move(this.options.profile, prior, {
|
|
@@ -14963,7 +15126,7 @@ var EnrollInviter = class extends Enrollment {
|
|
|
14963
15126
|
rev: 0,
|
|
14964
15127
|
author: 0,
|
|
14965
15128
|
active: 0,
|
|
14966
|
-
instance:
|
|
15129
|
+
instance: turnInstance(),
|
|
14967
15130
|
slots: granted.slots.map((slot) => slot && {
|
|
14968
15131
|
key: slot.key,
|
|
14969
15132
|
name: slot.name
|
|
@@ -15741,7 +15904,7 @@ async function planSetMove(record, signer, options = {}) {
|
|
|
15741
15904
|
author: record.ownSlot,
|
|
15742
15905
|
active: record.ownSlot,
|
|
15743
15906
|
slots: turnSlots,
|
|
15744
|
-
instance:
|
|
15907
|
+
instance: turnInstance(turnVerifierMark(record.verifier ?? null), random)
|
|
15745
15908
|
}, sign);
|
|
15746
15909
|
const update = {
|
|
15747
15910
|
d,
|
|
@@ -15964,6 +16127,21 @@ function takeoverTarget(record) {
|
|
|
15964
16127
|
function canTakeOver(record) {
|
|
15965
16128
|
return !!record && (record.state === "standby" || record.state === "superseded") && !!record.copy;
|
|
15966
16129
|
}
|
|
16130
|
+
/**
|
|
16131
|
+
* Whether the verifier this standby keeps is still the profile's, by the last record of the device set it knows (the
|
|
16132
|
+
* one just read, or the one it stored): false only when that record states the mark of another verifier. A record that
|
|
16133
|
+
* states none (an app from before wrote it) leaves the device with what it has, and so does this device's own record.
|
|
16134
|
+
* Only a standby is held to it: a device replaced by a takeover it did not want takes the turn back with the password
|
|
16135
|
+
* it knows, whatever the device that replaced it states.
|
|
16136
|
+
*/
|
|
16137
|
+
function verifierStands(record, latest) {
|
|
16138
|
+
if (record.state !== "standby" || !record.verifier || !latest || latest.tombstone) return true;
|
|
16139
|
+
const own = record.ownSlot === void 0 ? void 0 : record.deviceSet[record.ownSlot]?.key;
|
|
16140
|
+
const author = latest.slots[latest.author];
|
|
16141
|
+
if (own && author && bytesEqual(author.key, fromBase64Url(own))) return true;
|
|
16142
|
+
const stated = turnStatedVerifier(latest.instance);
|
|
16143
|
+
return !stated || bytesEqual(stated, turnVerifierMark(record.verifier));
|
|
16144
|
+
}
|
|
15967
16145
|
var sameName = (a, b) => a.normalize("NFC").trim().toLocaleLowerCase() === b.normalize("NFC").trim().toLocaleLowerCase();
|
|
15968
16146
|
/**
|
|
15969
16147
|
* Checks the request and forces the takeover. Resolves to the keeper's outcome: `start` when this device is the active
|
|
@@ -15990,7 +16168,9 @@ async function forceTakeover(ports, request) {
|
|
|
15990
16168
|
if (record.takeoverAttempts && (record.takeoverAttempts.total > 0 || record.takeoverAttempts.recent.length)) await ports.amend({ takeoverAttempts: handoffAttemptSucceeded() });
|
|
15991
16169
|
const keeper = await ports.keeper();
|
|
15992
16170
|
if (!keeper) throw new TakeoverRefusal("offline", "Go online to take over.");
|
|
15993
|
-
const outcome = await keeper.raise()
|
|
16171
|
+
const outcome = await keeper.raise({ before: (latest) => {
|
|
16172
|
+
if (!verifierStands(record, latest)) throw new TakeoverRefusal("changed", `The lock password was changed${target ? ` on ${target}` : ""}. Use here moves the profile with the new one.`);
|
|
16173
|
+
} });
|
|
15994
16174
|
if (!outcome) throw new TakeoverRefusal("state", "This profile has no device set.");
|
|
15995
16175
|
return outcome;
|
|
15996
16176
|
}
|
|
@@ -16079,7 +16259,7 @@ async function planOwnSet(record, ports, under) {
|
|
|
16079
16259
|
author: record.ownSlot,
|
|
16080
16260
|
active: record.ownSlot,
|
|
16081
16261
|
slots,
|
|
16082
|
-
instance:
|
|
16262
|
+
instance: turnInstance(turnVerifierMark(record.verifier ?? null), random)
|
|
16083
16263
|
}, ports.signer);
|
|
16084
16264
|
return {
|
|
16085
16265
|
d: toBase64Url(d),
|
|
@@ -16317,6 +16497,10 @@ function isHandoffVerifier(value) {
|
|
|
16317
16497
|
const v = value;
|
|
16318
16498
|
return !!v && typeof v === "object" && v.v === 1 && typeof v.setup === "string" && B64URL.test(v.setup) && typeof v.record === "string" && B64URL.test(v.record);
|
|
16319
16499
|
}
|
|
16500
|
+
/** Loads the password proof. Throws when it cannot be loaded: nothing can then be made or checked (`provesHandoffPassword` says false). */
|
|
16501
|
+
async function handoffPakeReady() {
|
|
16502
|
+
await opaque();
|
|
16503
|
+
}
|
|
16320
16504
|
/** A verifier for this password: both halves of the registration, run on this device. */
|
|
16321
16505
|
async function makeHandoffVerifier(password) {
|
|
16322
16506
|
if (!password) throw new Error("A password is needed");
|
|
@@ -19882,6 +20066,14 @@ function forgetWallet(records, id) {
|
|
|
19882
20066
|
return rest;
|
|
19883
20067
|
}
|
|
19884
20068
|
//#endregion
|
|
20069
|
+
//#region ../browser/src/shared/messageChanges.ts
|
|
20070
|
+
/** A row as a page gets it: without the frame a private group's message came in (`StoredMessage.frame`), which only the engine reads. */
|
|
20071
|
+
function withoutFrame(message) {
|
|
20072
|
+
if (message.frame === void 0) return message;
|
|
20073
|
+
const { frame: _frame, ...row } = message;
|
|
20074
|
+
return row;
|
|
20075
|
+
}
|
|
20076
|
+
//#endregion
|
|
19885
20077
|
//#region ../browser/src/shared/buttons.ts
|
|
19886
20078
|
/** The buttons of a message, when it has any. */
|
|
19887
20079
|
function messageButtons(message) {
|
|
@@ -21138,6 +21330,8 @@ function newestMessages(index, query, count) {
|
|
|
21138
21330
|
};
|
|
21139
21331
|
});
|
|
21140
21332
|
}
|
|
21333
|
+
/** A write that waits for no disk sync of its own (in a browser, the engine's default already; on Node, the CLI's store). */
|
|
21334
|
+
var RELAXED = { durability: "relaxed" };
|
|
21141
21335
|
/**
|
|
21142
21336
|
* Durable local state of the browser peer. Identity seeds live here the same
|
|
21143
21337
|
* way Desktop keeps them in its WebView storage: local to this profile, never
|
|
@@ -21370,12 +21564,13 @@ var db = {
|
|
|
21370
21564
|
await wrap((await store(STORES$1.messages, "readwrite")).put(message));
|
|
21371
21565
|
},
|
|
21372
21566
|
async updateDelivery(linkId, id, delivery, deliveryError, extra) {
|
|
21373
|
-
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite");
|
|
21567
|
+
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite", delivery === "sent" || delivery === "delivered" ? RELAXED : void 0);
|
|
21374
21568
|
const messages = tx.objectStore(STORES$1.messages);
|
|
21375
21569
|
const request = messages.get([linkId, id]);
|
|
21376
21570
|
request.onsuccess = () => {
|
|
21377
21571
|
const message = request.result;
|
|
21378
21572
|
if (!message?.delivery || message.delivery === "delivered") return;
|
|
21573
|
+
if (message.delivery === delivery && message.deliveryError === deliveryError && Object.entries(extra ?? {}).every(([key, value]) => message[key] === value)) return;
|
|
21379
21574
|
messages.put({
|
|
21380
21575
|
...message,
|
|
21381
21576
|
...extra,
|
|
@@ -21391,10 +21586,11 @@ var db = {
|
|
|
21391
21586
|
/**
|
|
21392
21587
|
* Changes one message row in place, in one transaction: `change` sees the row as stored and gives the fields to
|
|
21393
21588
|
* set (`null`: leave it). A row that is not there is not made. Used for what the delivery states do not carry
|
|
21394
|
-
* (the message's details record), on rows in any state.
|
|
21589
|
+
* (the message's details record), on rows in any state. `relaxed`: for what can be made again or is only a note
|
|
21590
|
+
* (a receipt's mark, the details record), written with no disk sync of its own.
|
|
21395
21591
|
*/
|
|
21396
|
-
async patchMessage(linkId, id, change) {
|
|
21397
|
-
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite");
|
|
21592
|
+
async patchMessage(linkId, id, change, relaxed = false) {
|
|
21593
|
+
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite", relaxed ? RELAXED : void 0);
|
|
21398
21594
|
const messages = tx.objectStore(STORES$1.messages);
|
|
21399
21595
|
const request = messages.get([linkId, id]);
|
|
21400
21596
|
let result;
|
|
@@ -21540,6 +21736,10 @@ var Reactions = class {
|
|
|
21540
21736
|
queue = Promise.resolve();
|
|
21541
21737
|
/** A group member's reactions taken per window (chat and member key), as a paired chat's session limits them. */
|
|
21542
21738
|
memberPace = /* @__PURE__ */ new Map();
|
|
21739
|
+
/** Per group chat, per member: its reactions past the window, by message, waiting for room (`hold`). */
|
|
21740
|
+
held = /* @__PURE__ */ new Map();
|
|
21741
|
+
heldTimers = /* @__PURE__ */ new Map();
|
|
21742
|
+
stopped = false;
|
|
21543
21743
|
/**
|
|
21544
21744
|
* This side's highest number per chat: read from its history the first time I react in it, then each number given
|
|
21545
21745
|
* out. It only has to grow, so a row deleted since leaves it too high and nothing is wrong.
|
|
@@ -21588,14 +21788,19 @@ var Reactions = class {
|
|
|
21588
21788
|
/**
|
|
21589
21789
|
* A reaction that came from `by` (`peer`, or a member's key), already authenticated as theirs. A message not here
|
|
21590
21790
|
* yet keeps it for five minutes; past the room for that sender, it is dropped (and, on a 1:1 session, not confirmed).
|
|
21791
|
+
* Past `REACTION_LIMITS.receive` a window from one group member, the rest wait for the next window (`hold`): a
|
|
21792
|
+
* community's catch-up hands back every reaction of a member at once, and one dropped is never handed again.
|
|
21591
21793
|
*/
|
|
21592
21794
|
receive(chat, by, reaction) {
|
|
21593
21795
|
if (isGroup$1(chat)) {
|
|
21594
21796
|
const key = `${chat}\n${by}`;
|
|
21595
21797
|
let pace = this.memberPace.get(key);
|
|
21596
21798
|
if (!pace) this.memberPace.set(key, pace = new ReactionWindow(REACTION_LIMITS.receive, REACTION_LIMITS.windowMs, () => this.now()));
|
|
21597
|
-
if (!pace.take()) return Promise.resolve(
|
|
21799
|
+
if (this.held.get(chat)?.has(by) || !pace.take()) return Promise.resolve(this.hold(chat, by, reaction, pace));
|
|
21598
21800
|
}
|
|
21801
|
+
return this.take(chat, by, reaction);
|
|
21802
|
+
}
|
|
21803
|
+
take(chat, by, reaction) {
|
|
21599
21804
|
return this.run(async () => {
|
|
21600
21805
|
const target = await this.find(chat, reaction.id);
|
|
21601
21806
|
if (!target) return this.wait(chat, by, reaction);
|
|
@@ -21604,6 +21809,75 @@ var Reactions = class {
|
|
|
21604
21809
|
return "applied";
|
|
21605
21810
|
});
|
|
21606
21811
|
}
|
|
21812
|
+
/**
|
|
21813
|
+
* A reaction past what a group member's window takes: kept, the highest number per message, `REACTION_LIMITS.held` a
|
|
21814
|
+
* group at most (the oldest go first), and taken when the window has room. The work stays what the window allows.
|
|
21815
|
+
*/
|
|
21816
|
+
hold(chat, by, reaction, pace) {
|
|
21817
|
+
let group = this.held.get(chat);
|
|
21818
|
+
if (!group) this.held.set(chat, group = /* @__PURE__ */ new Map());
|
|
21819
|
+
let waiting = group.get(by);
|
|
21820
|
+
if (!waiting) group.set(by, waiting = /* @__PURE__ */ new Map());
|
|
21821
|
+
const known = waiting.get(reaction.id);
|
|
21822
|
+
if (known && known.n >= reaction.n) return "stale";
|
|
21823
|
+
waiting.delete(reaction.id);
|
|
21824
|
+
waiting.set(reaction.id, reaction);
|
|
21825
|
+
let count = 0;
|
|
21826
|
+
for (const member of group.values()) count += member.size;
|
|
21827
|
+
for (const [member, reactions] of group) {
|
|
21828
|
+
if (count <= REACTION_LIMITS.held) break;
|
|
21829
|
+
while (count > REACTION_LIMITS.held && reactions.size) {
|
|
21830
|
+
reactions.delete(reactions.keys().next().value);
|
|
21831
|
+
count--;
|
|
21832
|
+
}
|
|
21833
|
+
if (!reactions.size && member !== by) group.delete(member);
|
|
21834
|
+
}
|
|
21835
|
+
this.takeLater(chat, by, pace);
|
|
21836
|
+
return waiting.has(reaction.id) ? "waiting" : "dropped";
|
|
21837
|
+
}
|
|
21838
|
+
takeLater(chat, by, pace) {
|
|
21839
|
+
const key = `${chat}\n${by}`;
|
|
21840
|
+
if (this.heldTimers.has(key) || this.stopped) return;
|
|
21841
|
+
this.heldTimers.set(key, setTimeout(() => {
|
|
21842
|
+
this.takeHeld(chat, by).catch(() => {});
|
|
21843
|
+
}, Math.max(50, pace.wait())));
|
|
21844
|
+
}
|
|
21845
|
+
/** The window of a member has room again: its reactions that waited are taken, oldest first, as many as it allows. */
|
|
21846
|
+
async takeHeld(chat, by) {
|
|
21847
|
+
const key = `${chat}\n${by}`;
|
|
21848
|
+
this.heldTimers.delete(key);
|
|
21849
|
+
const group = this.held.get(chat), waiting = group?.get(by), pace = this.memberPace.get(key);
|
|
21850
|
+
if (this.stopped || !group || !waiting || !pace) return;
|
|
21851
|
+
for (const [id, reaction] of [...waiting]) {
|
|
21852
|
+
if (this.host.believed && !this.host.believed(chat, by)) {
|
|
21853
|
+
waiting.clear();
|
|
21854
|
+
break;
|
|
21855
|
+
}
|
|
21856
|
+
if (!pace.take()) break;
|
|
21857
|
+
if (waiting.get(id) !== reaction) continue;
|
|
21858
|
+
waiting.delete(id);
|
|
21859
|
+
await this.take(chat, by, reaction).catch(() => {});
|
|
21860
|
+
}
|
|
21861
|
+
if (waiting.size) {
|
|
21862
|
+
this.takeLater(chat, by, pace);
|
|
21863
|
+
return;
|
|
21864
|
+
}
|
|
21865
|
+
if (group.get(by) === waiting) group.delete(by);
|
|
21866
|
+
if (!group.size) this.held.delete(chat);
|
|
21867
|
+
}
|
|
21868
|
+
/** The engine stops: what waits for a window is let go, and no timer is left. */
|
|
21869
|
+
stop() {
|
|
21870
|
+
this.stopped = true;
|
|
21871
|
+
this.dropHeld();
|
|
21872
|
+
}
|
|
21873
|
+
dropHeld(chat) {
|
|
21874
|
+
for (const [key, timer] of this.heldTimers) if (chat === void 0 || key.startsWith(`${chat}\n`)) {
|
|
21875
|
+
clearTimeout(timer);
|
|
21876
|
+
this.heldTimers.delete(key);
|
|
21877
|
+
}
|
|
21878
|
+
if (chat === void 0) this.held.clear();
|
|
21879
|
+
else this.held.delete(chat);
|
|
21880
|
+
}
|
|
21607
21881
|
/** A message was kept: the reactions that waited for it are shown now. */
|
|
21608
21882
|
stored(message) {
|
|
21609
21883
|
const list = this.waiting.get(message.linkId);
|
|
@@ -21641,6 +21915,7 @@ var Reactions = class {
|
|
|
21641
21915
|
}
|
|
21642
21916
|
/** A chat is gone: nothing waits for it any more, and my highest number in it is read again if it comes back. */
|
|
21643
21917
|
forget(chat) {
|
|
21918
|
+
this.dropHeld(chat);
|
|
21644
21919
|
this.waiting.delete(chat);
|
|
21645
21920
|
this.last.delete(chat);
|
|
21646
21921
|
}
|
|
@@ -21699,13 +21974,16 @@ function pinOfFormerMember(pin, inRoster) {
|
|
|
21699
21974
|
function mayPinIn(group) {
|
|
21700
21975
|
return !!group?.myKey && group.status === "active" && mayPin(group.profile, group.myKey, group.members.find((m) => m.role === "admin")?.key);
|
|
21701
21976
|
}
|
|
21702
|
-
/** The row a pin names, when it is here: the row of that id (a group's), or of `peer_`/`me_` and that id, or any other. */
|
|
21977
|
+
/** The row a pin names, when it is here: the row of that id (a group's), or of `peer_`/`me_` and that id, or any other of a chat's. */
|
|
21703
21978
|
async function pinnedRow(host, chat, id) {
|
|
21704
21979
|
if (!id) return void 0;
|
|
21705
21980
|
const group = isGroup(chat);
|
|
21706
|
-
if (host.message)
|
|
21707
|
-
const
|
|
21708
|
-
|
|
21981
|
+
if (host.message) {
|
|
21982
|
+
for (const rowId of group ? [id] : [`peer_${id}`, `me_${id}`]) {
|
|
21983
|
+
const row = await host.message(chat, rowId);
|
|
21984
|
+
if (row && replyRef(row, group) === id) return row;
|
|
21985
|
+
}
|
|
21986
|
+
if (group) return void 0;
|
|
21709
21987
|
}
|
|
21710
21988
|
return reactionTarget(await host.messages(chat), id, group);
|
|
21711
21989
|
}
|
|
@@ -21930,6 +22208,17 @@ var FAREWELL_RETRY_MS = 6e4;
|
|
|
21930
22208
|
var FAREWELL_TOLD_MS = 6e5;
|
|
21931
22209
|
var FAREWELL_SAY_MS = 1e4;
|
|
21932
22210
|
/**
|
|
22211
|
+
* A key asking in my lobby that no commit I hold names may be a member let in by a commit I have not heard yet: a hub
|
|
22212
|
+
* keeps an edge to it for a moment, on which it says where it is and takes commits (`wantAhead`). At most `AHEAD_EDGES`
|
|
22213
|
+
* at a time, each awaited `AHEAD_WAIT_MS` and closed `AHEAD_LINGER_MS` after it came up unless a commit named the key
|
|
22214
|
+
* by then; the same key is not waited for again for a while (longer once its edge was up).
|
|
22215
|
+
*/
|
|
22216
|
+
var AHEAD_EDGES = 4;
|
|
22217
|
+
var AHEAD_WAIT_MS = 12e4;
|
|
22218
|
+
var AHEAD_LINGER_MS = 2e4;
|
|
22219
|
+
var AHEAD_RETRY_MS = 6e4;
|
|
22220
|
+
var AHEAD_HEARD_MS = 6e5;
|
|
22221
|
+
/**
|
|
21933
22222
|
* A key asking from before the commit that took it out was there and was sent that commit: not waited for. The two
|
|
21934
22223
|
* times are two devices' clocks (the asker's, the commit's signer's), so "before" allows this much between them:
|
|
21935
22224
|
* a returning member whose clock runs behind the admin's is told all the same.
|
|
@@ -22062,22 +22351,23 @@ var Communities = class {
|
|
|
22062
22351
|
invited: [],
|
|
22063
22352
|
memberLinks: {}
|
|
22064
22353
|
};
|
|
22065
|
-
|
|
22354
|
+
const invitation = this.knocking(group, live) ? {
|
|
22355
|
+
linkId: group.joining.linkId,
|
|
22356
|
+
contact: "",
|
|
22357
|
+
admin: group.joining.inviter,
|
|
22358
|
+
members: 0,
|
|
22359
|
+
accepted: true,
|
|
22360
|
+
viaLink: true,
|
|
22361
|
+
stage: this.joinStage(group),
|
|
22362
|
+
...this.knockingWith.get(group.id) ? { waiting: this.knockingWith.get(group.id) } : {}
|
|
22363
|
+
} : void 0;
|
|
22364
|
+
if (invitation && !this.keptWhileKnocking(live)) return [{
|
|
22066
22365
|
...base,
|
|
22067
22366
|
name: groupName(group.joining.name) ?? live?.session.name ?? "",
|
|
22068
22367
|
isAdmin: false,
|
|
22069
22368
|
members: [],
|
|
22070
22369
|
canSend: false,
|
|
22071
|
-
invitation
|
|
22072
|
-
linkId: group.joining.linkId,
|
|
22073
|
-
contact: "",
|
|
22074
|
-
admin: group.joining.inviter,
|
|
22075
|
-
members: 0,
|
|
22076
|
-
accepted: true,
|
|
22077
|
-
viaLink: true,
|
|
22078
|
-
stage: this.joinStage(group),
|
|
22079
|
-
...this.knockingWith.get(group.id) ? { waiting: this.knockingWith.get(group.id) } : {}
|
|
22080
|
-
}
|
|
22370
|
+
invitation
|
|
22081
22371
|
}];
|
|
22082
22372
|
if (!live) return [];
|
|
22083
22373
|
const s = live.session, edges = this.host.edges(group.id);
|
|
@@ -22102,6 +22392,7 @@ var Communities = class {
|
|
|
22102
22392
|
}) } : {},
|
|
22103
22393
|
canSend: s.canSend,
|
|
22104
22394
|
...s.picture ? { picture: s.picture } : {},
|
|
22395
|
+
...invitation ? { invitation } : {},
|
|
22105
22396
|
community: {
|
|
22106
22397
|
hub: live.hub,
|
|
22107
22398
|
hubs: hubs.length,
|
|
@@ -22169,6 +22460,13 @@ var Communities = class {
|
|
|
22169
22460
|
knocking(group, live) {
|
|
22170
22461
|
return !!group.joining && (!live || live.session.status !== "active");
|
|
22171
22462
|
}
|
|
22463
|
+
/**
|
|
22464
|
+
* Someone removed keeps the group as it was here while it knocks to get back in, and when it gives up (`decline`).
|
|
22465
|
+
* Not a session whose admission lost a race: that knock is still its join.
|
|
22466
|
+
*/
|
|
22467
|
+
keptWhileKnocking(live) {
|
|
22468
|
+
return live?.session.status === "removed";
|
|
22469
|
+
}
|
|
22172
22470
|
/** `check`: a member, as far as this device knows, asking the door whether it still is (`joinByLink`). */
|
|
22173
22471
|
async startJoining(link, seedB64, since, check = false) {
|
|
22174
22472
|
this.lastKnock.delete(link.g);
|
|
@@ -22340,11 +22638,14 @@ var Communities = class {
|
|
|
22340
22638
|
}
|
|
22341
22639
|
/**
|
|
22342
22640
|
* A join through the link that no member answered yet, given up: listed as an invitation, it is declined as one.
|
|
22343
|
-
*
|
|
22641
|
+
* Someone removed who knocked to get back in only stops knocking: the group and its history stay here, as a private
|
|
22642
|
+
* group's do for someone removed, invited again, who declines. A member trying to get back in (its session lost)
|
|
22643
|
+
* leaves instead, which keeps nothing.
|
|
22344
22644
|
*/
|
|
22345
22645
|
async decline(groupId) {
|
|
22346
22646
|
const group = this.stored.get(groupId), live = this.live.get(groupId);
|
|
22347
22647
|
if (!group || !this.knocking(group, live)) throw new Error("No invitation to decline");
|
|
22648
|
+
if (this.keptWhileKnocking(live)) return this.endCheck(group);
|
|
22348
22649
|
if (live) throw new Error("You were in this group: leave it to stop trying to get back in");
|
|
22349
22650
|
await this.forget(groupId);
|
|
22350
22651
|
}
|
|
@@ -22401,6 +22702,7 @@ var Communities = class {
|
|
|
22401
22702
|
await this.publishBeacon(group.id, live, now, false).catch(() => {});
|
|
22402
22703
|
}
|
|
22403
22704
|
live.farewells.clear();
|
|
22705
|
+
live.ahead.clear();
|
|
22404
22706
|
await this.reconcile(group.id, live, now);
|
|
22405
22707
|
continue;
|
|
22406
22708
|
}
|
|
@@ -22494,6 +22796,7 @@ var Communities = class {
|
|
|
22494
22796
|
if (this.leftItself(live, h.key) ? h.ts - FAREWELL_SKEW_MS > out : h.ts + FAREWELL_SKEW_MS > out) this.wantFarewell(groupId, live, h.key, now);
|
|
22495
22797
|
}
|
|
22496
22798
|
this.endFarewells(live, now);
|
|
22799
|
+
this.endAhead(groupId, live, now);
|
|
22497
22800
|
if (live.hub) {
|
|
22498
22801
|
const busy = this.admitting(groupId, live, now);
|
|
22499
22802
|
if (s.state.pendingLeaves.some((r) => r.s !== me && this.recentHub(live, r.s))) live.lobbyBusyUntil = Math.max(live.lobbyBusyUntil, now + this.timings.lobbyBusyMs);
|
|
@@ -22572,8 +22875,9 @@ var Communities = class {
|
|
|
22572
22875
|
}
|
|
22573
22876
|
await this.keepLooking(groupId, live, now);
|
|
22574
22877
|
for (const [key, since] of live.pendingEntries) {
|
|
22575
|
-
const linkId = this.host.entries(groupId).get(key);
|
|
22576
|
-
|
|
22878
|
+
const linkId = this.host.entries(groupId).get(key), up = !!linkId && this.host.linkReady(linkId, 2);
|
|
22879
|
+
const elsewhere = rosterHas(s.roster, key) && since < this.addedAt(s, key) + this.timings.knockFallbackMs;
|
|
22880
|
+
if (!up && (elsewhere || now - since > ENTRY_UNANSWERED_MS)) live.pendingEntries.delete(key);
|
|
22577
22881
|
else if (now - since > ENTRY_TIMEOUT_MS$1) {
|
|
22578
22882
|
live.pendingEntries.delete(key);
|
|
22579
22883
|
this.refused.set(key, now + REFUSED_FOR_MS$1);
|
|
@@ -22632,7 +22936,7 @@ var Communities = class {
|
|
|
22632
22936
|
const edges = this.host.edges(groupId);
|
|
22633
22937
|
if (now - live.beaconAt <= 2 * this.timings.beaconReadMs) {
|
|
22634
22938
|
const hubs = new Set(freshHubs(this.hubs(live), now).map((h) => h.key));
|
|
22635
|
-
for (const [key, id] of edges) if (!hubs.has(key) && !live.members.has(key) && this.host.linkReady(id, 2) && !this.gone(live, key)) live.members.set(key, now);
|
|
22939
|
+
for (const [key, id] of edges) if (!hubs.has(key) && !live.members.has(key) && this.host.linkReady(id, 2) && rosterHas(live.session.roster, key) && !this.gone(live, key)) live.members.set(key, now);
|
|
22636
22940
|
for (const key of [...live.members.keys()]) if (hubs.has(key)) live.members.delete(key);
|
|
22637
22941
|
}
|
|
22638
22942
|
for (const [key, since] of live.members) {
|
|
@@ -22770,16 +23074,21 @@ var Communities = class {
|
|
|
22770
23074
|
for (let i = chain.length - 1; i >= 0; i--) if ((chain[i].k === "leave" || chain[i].k === "remove") && chain[i].s === key) return chain[i].k === "leave";
|
|
22771
23075
|
return false;
|
|
22772
23076
|
}
|
|
22773
|
-
/**
|
|
22774
|
-
|
|
22775
|
-
|
|
23077
|
+
/**
|
|
23078
|
+
* A frame goes on an edge to a member; to someone the chain took out, only the commit that took it out. What the
|
|
23079
|
+
* group says (`said`: a frame of mine for everyone, or one a hub relays) goes to the members of the roster only:
|
|
23080
|
+
* a key no commit names hears none of it, and one let in on a branch that lost hears the commits.
|
|
23081
|
+
*/
|
|
23082
|
+
mayHear(live, key, frame, said = false) {
|
|
22776
23083
|
const f = frame;
|
|
22777
|
-
return f.t === "group-commit" && (f.commit?.k === "remove" || f.commit?.k === "leave") && f.commit.s === key;
|
|
23084
|
+
if (live?.session.wasRemoved(key)) return f.t === "group-commit" && (f.commit?.k === "remove" || f.commit?.k === "leave") && f.commit.s === key;
|
|
23085
|
+
if (!live || !said || rosterHas(live.session.roster, key)) return true;
|
|
23086
|
+
return f.t === "group-commit" && live.session.isRecentMember(key);
|
|
22778
23087
|
}
|
|
22779
|
-
/** The edges of the group that may hear `frame
|
|
23088
|
+
/** The edges of the group that may hear `frame`, said to everyone. */
|
|
22780
23089
|
hearers(groupId, frame) {
|
|
22781
23090
|
const live = this.live.get(groupId);
|
|
22782
|
-
return [...this.host.edges(groupId)].filter(([key]) => this.mayHear(live, key, frame));
|
|
23091
|
+
return [...this.host.edges(groupId)].filter(([key]) => this.mayHear(live, key, frame, true));
|
|
22783
23092
|
}
|
|
22784
23093
|
/** As a hub: someone the chain took out is asking (in my lobby, or listing itself as a hub). An edge to tell it, within bounds. */
|
|
22785
23094
|
wantFarewell(groupId, live, key, now) {
|
|
@@ -22800,6 +23109,34 @@ var Communities = class {
|
|
|
22800
23109
|
for (const [key, at] of live.farewellSaid) if (now - at >= FAREWELL_SAY_MS) live.farewellSaid.delete(key);
|
|
22801
23110
|
}
|
|
22802
23111
|
/**
|
|
23112
|
+
* As a hub: a key no commit I hold names is asking in my lobby. It may be a member ahead of me on the roster, who needs
|
|
23113
|
+
* the edge for me to find out (its answer to my `group-sync` carries the commit); or nobody, since whoever was once
|
|
23114
|
+
* given the group's rendezvous secret can write a key there. An edge, within bounds, that is no member's: it takes
|
|
23115
|
+
* no place among my members, and nothing the group says goes on it (`mayHear`).
|
|
23116
|
+
*/
|
|
23117
|
+
wantAhead(groupId, live, key, now) {
|
|
23118
|
+
if (live.ahead.has(key) || now < (live.aheadQuiet.get(key) ?? 0)) return;
|
|
23119
|
+
if (live.ahead.size >= AHEAD_EDGES || this.capacity(groupId, live, now) <= 0) return;
|
|
23120
|
+
live.ahead.set(key, { since: now });
|
|
23121
|
+
if (!this.host.edges(groupId).has(key)) live.expect.add(key);
|
|
23122
|
+
}
|
|
23123
|
+
/** Those edges end: a commit named the key (a member of mine now, or out), it never came, or its moment is over; and a hub no more keeps none. */
|
|
23124
|
+
endAhead(groupId, live, now) {
|
|
23125
|
+
const edges = this.host.edges(groupId);
|
|
23126
|
+
for (const [key, a] of live.ahead) {
|
|
23127
|
+
const member = rosterHas(live.session.roster, key), named = member || live.session.wasRemoved(key);
|
|
23128
|
+
if (!named && live.hub) {
|
|
23129
|
+
const id = edges.get(key);
|
|
23130
|
+
if (!a.up && id && this.host.linkReady(id, 2)) a.up = now;
|
|
23131
|
+
if (a.up ? now - a.up < AHEAD_LINGER_MS : now - a.since < AHEAD_WAIT_MS) continue;
|
|
23132
|
+
}
|
|
23133
|
+
live.ahead.delete(key);
|
|
23134
|
+
if (!named) live.aheadQuiet.set(key, now + (a.up ? AHEAD_HEARD_MS : AHEAD_RETRY_MS));
|
|
23135
|
+
else if (member && live.hub && live.members.size < this.capacity(groupId, live, now)) live.members.set(key, now);
|
|
23136
|
+
}
|
|
23137
|
+
for (const [key, until] of live.aheadQuiet) if (until <= now) live.aheadQuiet.delete(key);
|
|
23138
|
+
}
|
|
23139
|
+
/**
|
|
22803
23140
|
* Someone the chain took out says where it is (`group-sync`): it is handed the commits up to the one that took it
|
|
22804
23141
|
* out, and nothing else (`CommunitySession.farewell`). Nothing else it says is heard.
|
|
22805
23142
|
*/
|
|
@@ -22863,6 +23200,10 @@ var Communities = class {
|
|
|
22863
23200
|
if (ts + FAREWELL_SKEW_MS > (live.session.outAt(key) ?? Infinity)) this.wantFarewell(groupId, live, key, now);
|
|
22864
23201
|
continue;
|
|
22865
23202
|
}
|
|
23203
|
+
if (!rosterHas(live.session.roster, key)) {
|
|
23204
|
+
this.wantAhead(groupId, live, key, now);
|
|
23205
|
+
continue;
|
|
23206
|
+
}
|
|
22866
23207
|
live.emptySince = now;
|
|
22867
23208
|
if (!live.members.has(key) && live.members.size >= this.capacity(groupId, live, now)) continue;
|
|
22868
23209
|
if (!live.members.has(key)) {
|
|
@@ -22883,6 +23224,7 @@ var Communities = class {
|
|
|
22883
23224
|
for (const h of freshHubs(this.hubs(live), now)) if (h.key !== s.myKey && !this.hubDead(groupId, live, h, now)) wanted.add(h.key);
|
|
22884
23225
|
for (const key of live.members.keys()) wanted.add(key);
|
|
22885
23226
|
for (const key of live.farewells.keys()) wanted.add(key);
|
|
23227
|
+
for (const key of live.ahead.keys()) wanted.add(key);
|
|
22886
23228
|
} else for (const key of live.myHubs) wanted.add(key);
|
|
22887
23229
|
for (const [key, id] of existing) if (this.host.linkReady(id, 2) && (live.members.has(key) || this.recentHub(live, key)) && !s.wasRemoved(key)) wanted.add(key);
|
|
22888
23230
|
}
|
|
@@ -23003,8 +23345,7 @@ var Communities = class {
|
|
|
23003
23345
|
if (now - ts > 18e4 || this.refused.has(key) || live.pendingEntries.has(key) || key === link.host || key === s.myKey) continue;
|
|
23004
23346
|
const stillKnocking = now - ts < 2 * this.timings.slowKnockMs;
|
|
23005
23347
|
if (rosterHas(s.roster, key)) {
|
|
23006
|
-
|
|
23007
|
-
if (!stillKnocking || ts < added + this.timings.knockFallbackMs) continue;
|
|
23348
|
+
if (!stillKnocking || ts < this.addedAt(s, key) + this.timings.knockFallbackMs) continue;
|
|
23008
23349
|
}
|
|
23009
23350
|
const seen = live.knocksSeen.get(key);
|
|
23010
23351
|
const changed = !!seen && seen.doors !== doorSig;
|
|
@@ -23034,6 +23375,10 @@ var Communities = class {
|
|
|
23034
23375
|
}
|
|
23035
23376
|
for (const [key, { first }] of live.knocksSeen) if (now - first > 36e4) live.knocksSeen.delete(key);
|
|
23036
23377
|
}
|
|
23378
|
+
/** When the chain last added this member (0: never). */
|
|
23379
|
+
addedAt(s, key) {
|
|
23380
|
+
return [...s.state.chain].reverse().find((c) => c.k === "add" && c.s === key)?.ts ?? 0;
|
|
23381
|
+
}
|
|
23037
23382
|
/** Leave requests: the door commits them (see `answerKnocks`); any hub after a while. */
|
|
23038
23383
|
async commitLeaves(live, otherHubs, now) {
|
|
23039
23384
|
const s = live.session;
|
|
@@ -23280,6 +23625,7 @@ var Communities = class {
|
|
|
23280
23625
|
if (f.t === "group-sync" && live.session.isMember) this.sayFarewell(groupId, live, peerKey, f);
|
|
23281
23626
|
return;
|
|
23282
23627
|
}
|
|
23628
|
+
if (!live.session.isRecentMember(peerKey) && f.t !== "group-commit" && f.t !== "group-msg" && f.t !== "group-secrets" && !(f.t === "group-secret" && f.to === live.session.myKey)) return;
|
|
23283
23629
|
if ((f.t === "group-secret" || f.t === "group-entry") && typeof f.to === "string" && f.to !== live.session.myKey) {
|
|
23284
23630
|
if (!live.hub) return;
|
|
23285
23631
|
const id = `${String(f.t)}:${f.to}:${String(f.h ?? f.x)}`;
|
|
@@ -23334,7 +23680,7 @@ var Communities = class {
|
|
|
23334
23680
|
edgeReady(groupId, peerKey, linkId) {
|
|
23335
23681
|
const live = this.live.get(groupId);
|
|
23336
23682
|
if (!live || live.session.wasRemoved(peerKey)) return;
|
|
23337
|
-
if (live.hub && !this.peerIsHub(live, peerKey)) live.members.set(peerKey, this.now());
|
|
23683
|
+
if (live.hub && !this.peerIsHub(live, peerKey) && rosterHas(live.session.roster, peerKey)) live.members.set(peerKey, this.now());
|
|
23338
23684
|
this.sendTo(linkId, live.session.syncFrame());
|
|
23339
23685
|
this.host.emit();
|
|
23340
23686
|
}
|
|
@@ -23449,6 +23795,8 @@ var Communities = class {
|
|
|
23449
23795
|
farewells: /* @__PURE__ */ new Map(),
|
|
23450
23796
|
farewellQuiet: /* @__PURE__ */ new Map(),
|
|
23451
23797
|
farewellSaid: /* @__PURE__ */ new Map(),
|
|
23798
|
+
ahead: /* @__PURE__ */ new Map(),
|
|
23799
|
+
aheadQuiet: /* @__PURE__ */ new Map(),
|
|
23452
23800
|
knocksScanned: false,
|
|
23453
23801
|
knockCursor: 0,
|
|
23454
23802
|
lastShardPoll: 0,
|
|
@@ -24445,8 +24793,8 @@ function eventTime(taken, groupId, timestamp) {
|
|
|
24445
24793
|
var arrivalNow = (tick) => Math.max(tick, Date.now());
|
|
24446
24794
|
/**
|
|
24447
24795
|
* Keeps on the group's record when a member's message came, where that is later than what it holds: the record is
|
|
24448
|
-
* saved with the session
|
|
24449
|
-
* the group unread after the app starts again. Read from the history alone, by its own time, it sorted among what I
|
|
24796
|
+
* saved with the session soon after (a message taken moves its state, saved within `GROUP_LIMITS.saveWithinMs` and when
|
|
24797
|
+
* the app stops), so a message handed to me late still makes the group unread after the app starts again. Read from the history alone, by its own time, it sorted among what I
|
|
24450
24798
|
* had read and the mark was gone on the next start.
|
|
24451
24799
|
*/
|
|
24452
24800
|
function noteCame(group, came, peer, mentioned) {
|
|
@@ -24497,6 +24845,8 @@ var ENTRY_RELEASE_MS = 2e3;
|
|
|
24497
24845
|
var REMOVED_LINGER_MS = 15e3;
|
|
24498
24846
|
/** How long the tombstone of a group I left waits for the admin to hear it. */
|
|
24499
24847
|
var LEFT_KEPT_MS = 6048e5;
|
|
24848
|
+
/** Admin side: how long a contact invited over its chat is waited for. After, it holds no place and may be invited again. */
|
|
24849
|
+
var INVITED_KEPT_MS = LEFT_KEPT_MS;
|
|
24500
24850
|
/**
|
|
24501
24851
|
* Invitee side: an accept the admin has not answered with a welcome is said again this often while the inviter's chat
|
|
24502
24852
|
* is up, and at once when it comes up, at most `ACCEPT_AGAINS` times each time it does (its app may have restarted).
|
|
@@ -24517,6 +24867,13 @@ function invitationClaims(frame) {
|
|
|
24517
24867
|
var FAREWELL_ANSWERS = 3;
|
|
24518
24868
|
/** Commits sent to a removed member that was behind when it left, at most. */
|
|
24519
24869
|
var FAREWELL_COMMITS = 64;
|
|
24870
|
+
/**
|
|
24871
|
+
* A member whose messages were just asked of another (`Groups.askOf`) is told where I am only once that one's answer
|
|
24872
|
+
* has come: no message from it for this long, or `MESH_SYNC_HOLD_MAX_MS` after it was asked. Told at once, its own
|
|
24873
|
+
* answer raced the other's, which holds the same messages among everyone's in the order they were written, and a
|
|
24874
|
+
* member back in a group of five read each member's lines apart, the newest above older ones (WISP 902 § Catch-up).
|
|
24875
|
+
*/
|
|
24876
|
+
var MESH_SYNC_HOLD_MS = 1500;
|
|
24520
24877
|
/** Where a member's gossip turns start among the members it is connected to: a number of its own, from its key. */
|
|
24521
24878
|
var gossipStart = (key) => {
|
|
24522
24879
|
let n = 0;
|
|
@@ -24574,6 +24931,8 @@ var Groups = class {
|
|
|
24574
24931
|
timings;
|
|
24575
24932
|
stored = /* @__PURE__ */ new Map();
|
|
24576
24933
|
sessions = /* @__PURE__ */ new Map();
|
|
24934
|
+
/** Per private group, the rows that hold the frame their message came in (`StoredMessage.frame`), oldest first, and what each counts for. */
|
|
24935
|
+
framesKept = /* @__PURE__ */ new Map();
|
|
24577
24936
|
/** Contacts (chat ids, kept in `StoredGroup.invited`) and entry sessions I invited to each group, until they answer. */
|
|
24578
24937
|
invited = /* @__PURE__ */ new Map();
|
|
24579
24938
|
/** Invitee side: when my accept of each invitation was last said, how many times since the inviter's chat came up, and whether it was down. */
|
|
@@ -24619,6 +24978,13 @@ var Groups = class {
|
|
|
24619
24978
|
* time, not of everyone whose edge opens, so a member coming back is not sent the same messages by every other.
|
|
24620
24979
|
*/
|
|
24621
24980
|
relayAsked = /* @__PURE__ */ new Map();
|
|
24981
|
+
/**
|
|
24982
|
+
* Per mesh group, the members whose edge opened while their messages were being handed on by another (`holdSync`):
|
|
24983
|
+
* whom, and when it was asked. Their sync goes once that answer has come.
|
|
24984
|
+
*/
|
|
24985
|
+
syncHeld = /* @__PURE__ */ new Map();
|
|
24986
|
+
/** Per mesh group and member, when a message last came over its edge (its own, or one it handed on). */
|
|
24987
|
+
lastHanded = /* @__PURE__ */ new Map();
|
|
24622
24988
|
lastGossip = /* @__PURE__ */ new Map();
|
|
24623
24989
|
/**
|
|
24624
24990
|
* Per mesh group, each reachable member some of whose messages never reached me (`GroupSession.lacking`): what was
|
|
@@ -24626,6 +24992,11 @@ var Groups = class {
|
|
|
24626
24992
|
* holds any more are not asked for every gossip turn. `last`: the latest ask, its member and the numbers it named.
|
|
24627
24993
|
*/
|
|
24628
24994
|
gapsAsked = /* @__PURE__ */ new Map();
|
|
24995
|
+
/**
|
|
24996
|
+
* Per mesh group and member, per epoch: the number up to which nobody holds what never reached me (`gapsSettle`).
|
|
24997
|
+
* The member list stops counting those; they are still asked of a member that connects later.
|
|
24998
|
+
*/
|
|
24999
|
+
gapsGone = /* @__PURE__ */ new Map();
|
|
24629
25000
|
/** Per mesh group, when `gapsAgain` last looked: every half `MESH_GAP_AGAIN_MS`, not every tick. */
|
|
24630
25001
|
gapsLooked = /* @__PURE__ */ new Map();
|
|
24631
25002
|
/** Per mesh group: member key → since when its edge to me is down, and when someone last told me it is here. */
|
|
@@ -24653,6 +25024,8 @@ var Groups = class {
|
|
|
24653
25024
|
removedAt = /* @__PURE__ */ new Map();
|
|
24654
25025
|
/** Who is typing in each private group, and this side's word there (WISP 902 · Group Mesh § Typing). */
|
|
24655
25026
|
typings;
|
|
25027
|
+
/** A member's pins taken per window in a private group (group and member key). */
|
|
25028
|
+
pinPaces = /* @__PURE__ */ new Map();
|
|
24656
25029
|
/** Edge signaling through members (WISP 902 · Group Mesh § Signaling through members). */
|
|
24657
25030
|
signals;
|
|
24658
25031
|
/** Files and voice messages in every group, private or community (WISP 503 · Group Files). */
|
|
@@ -24696,6 +25069,10 @@ var Groups = class {
|
|
|
24696
25069
|
this.communities.files = this.files;
|
|
24697
25070
|
}
|
|
24698
25071
|
}
|
|
25072
|
+
/** Writes what the private groups' sessions still have to save (`GROUP_LIMITS.saveWithinMs`): the app is stopping. */
|
|
25073
|
+
async flush() {
|
|
25074
|
+
await Promise.allSettled([...this.sessions.values()].map((session) => session.flush()));
|
|
25075
|
+
}
|
|
24699
25076
|
/** `asItIs`: a copy that must stay as it was stored (limited mode, WISP 06), so nothing left over is cleared from it. */
|
|
24700
25077
|
async load(asItIs = false) {
|
|
24701
25078
|
const all = await this.store.getGroups();
|
|
@@ -24705,6 +25082,10 @@ var Groups = class {
|
|
|
24705
25082
|
if (group.state) this.attach(group.state);
|
|
24706
25083
|
if (group.invited) this.invited.set(group.id, new Set(Object.keys(group.invited)));
|
|
24707
25084
|
const history = await this.store.getMessages(MESSAGE_LINK(group.id)), said = history.filter((m) => !m.event && !m.groupPay);
|
|
25085
|
+
this.framesKept.set(group.id, history.filter((m) => m.frame).map((m) => ({
|
|
25086
|
+
id: m.id,
|
|
25087
|
+
bytes: frameBytes(m.frame)
|
|
25088
|
+
})));
|
|
24708
25089
|
const last = said[said.length - 1], lastPeer = [...said].reverse().find((m) => m.sender !== "me");
|
|
24709
25090
|
if (last) this.lastMessageAt.set(group.id, last.timestamp);
|
|
24710
25091
|
const peerAt = cameOrWritten(lastPeer?.timestamp, lastPeer ? group.came?.peer : void 0);
|
|
@@ -24718,6 +25099,13 @@ var Groups = class {
|
|
|
24718
25099
|
if (!asItIs) await this.files?.sweep(all.map((g) => g.id));
|
|
24719
25100
|
for (const group of this.stored.values()) for (const [, linkId] of this.host.entries(group.id)) if (!group.invitation?.entry || group.invitation.linkId !== linkId) await this.host.closeEdge(linkId);
|
|
24720
25101
|
}
|
|
25102
|
+
/** The pins taken per window from one member's edge in a private group, as a paired chat's session limits them. */
|
|
25103
|
+
pinPace(groupId, member) {
|
|
25104
|
+
const key = `${groupId}\n${member}`;
|
|
25105
|
+
let pace = this.pinPaces.get(key);
|
|
25106
|
+
if (!pace) this.pinPaces.set(key, pace = new ReactionWindow(PIN_LIMITS.receive, PIN_LIMITS.windowMs));
|
|
25107
|
+
return pace;
|
|
25108
|
+
}
|
|
24721
25109
|
/** A group's pin now (WISP 400 § Pinned message), the latest one taken, unpinned or not. */
|
|
24722
25110
|
pinOf(groupId) {
|
|
24723
25111
|
return this.record(groupId)?.pin;
|
|
@@ -24773,6 +25161,7 @@ var Groups = class {
|
|
|
24773
25161
|
admin: invitation.admin,
|
|
24774
25162
|
members: Math.min(Math.max(invitation.n, 0), GROUP_MEMBER_CAP.max),
|
|
24775
25163
|
accepted: !!invitation.seedB64,
|
|
25164
|
+
...this.acceptSaid.get(group.id)?.unanswered ? { unanswered: true } : {},
|
|
24776
25165
|
...invitation.entry ? {
|
|
24777
25166
|
viaLink: true,
|
|
24778
25167
|
stage: this.joinStage(group)
|
|
@@ -24814,7 +25203,7 @@ var Groups = class {
|
|
|
24814
25203
|
...viaHub ? { viaHub } : {},
|
|
24815
25204
|
...hubKeys.has(key) ? { hub: true } : {},
|
|
24816
25205
|
...hubRole ? { hubRole } : {},
|
|
24817
|
-
missing: session.missing(key)
|
|
25206
|
+
missing: session.missing(key, this.gapsGone.get(group.id)?.get(key)?.[session.epoch])
|
|
24818
25207
|
};
|
|
24819
25208
|
})
|
|
24820
25209
|
};
|
|
@@ -24919,6 +25308,22 @@ var Groups = class {
|
|
|
24919
25308
|
await this.store.putGroup(group);
|
|
24920
25309
|
this.host.emit();
|
|
24921
25310
|
}
|
|
25311
|
+
/**
|
|
25312
|
+
* Admin side: the invitations still unanswered are taken back, each over the contact chat it went on, when the group
|
|
25313
|
+
* goes from this app or the role from me: nobody here could let them in. A chat that is down hears it as the answer
|
|
25314
|
+
* to its accept. Entry sessions are not told: they close with the group, or the new admin answers the next knock.
|
|
25315
|
+
*/
|
|
25316
|
+
takeBack(groupId, group) {
|
|
25317
|
+
const entries = new Set(this.host.entries(groupId).values());
|
|
25318
|
+
for (const linkId of this.invited.get(groupId) ?? []) if (!entries.has(linkId)) try {
|
|
25319
|
+
this.host.sendOnLink(linkId, {
|
|
25320
|
+
t: "group-decline",
|
|
25321
|
+
g: groupId
|
|
25322
|
+
});
|
|
25323
|
+
} catch {}
|
|
25324
|
+
this.invited.delete(groupId);
|
|
25325
|
+
if (group) delete group.invited;
|
|
25326
|
+
}
|
|
24922
25327
|
/** Admin side: a contact chat answered its invitation (accepted or declined); false when it had none. */
|
|
24923
25328
|
answered(group, linkId) {
|
|
24924
25329
|
const had = !!this.invited.get(group.id)?.delete(linkId);
|
|
@@ -24927,6 +25332,14 @@ var Groups = class {
|
|
|
24927
25332
|
group.invited = Object.keys(rest).length ? rest : void 0;
|
|
24928
25333
|
return true;
|
|
24929
25334
|
}
|
|
25335
|
+
/** Admin side: a contact whose answer did not come in `INVITED_KEPT_MS` (a decline lost on the way, an app gone) is not waited for any more. */
|
|
25336
|
+
async ageInvited(group, now) {
|
|
25337
|
+
const stale = Object.entries(group.invited).filter(([, at]) => now - at > INVITED_KEPT_MS);
|
|
25338
|
+
if (!stale.length) return;
|
|
25339
|
+
for (const [linkId] of stale) this.answered(group, linkId);
|
|
25340
|
+
await this.store.putGroup(group);
|
|
25341
|
+
this.host.emit();
|
|
25342
|
+
}
|
|
24930
25343
|
async accept(groupId) {
|
|
24931
25344
|
const group = this.stored.get(groupId);
|
|
24932
25345
|
if (!group?.invitation) throw new Error("No invitation to accept");
|
|
@@ -24953,19 +25366,56 @@ var Groups = class {
|
|
|
24953
25366
|
if (this.isCommunity(groupId)) return this.communities.decline(groupId);
|
|
24954
25367
|
const group = this.stored.get(groupId);
|
|
24955
25368
|
if (!group?.invitation) throw new Error("No invitation to decline");
|
|
24956
|
-
|
|
24957
|
-
|
|
24958
|
-
|
|
24959
|
-
|
|
24960
|
-
|
|
24961
|
-
} catch {}
|
|
25369
|
+
if (!group.previous) return this.forget(groupId);
|
|
25370
|
+
await this.dropInvitation(group, this.sayDecline(groupId, group.invitation.linkId));
|
|
25371
|
+
}
|
|
25372
|
+
/** An invitation goes from the list: declined here (`owed`: the answer, when it could not be sent), or taken back by who sent it. */
|
|
25373
|
+
async dropInvitation(group, owed) {
|
|
24962
25374
|
const previous = group.previous;
|
|
24963
|
-
if (!previous) return this.forget(
|
|
24964
|
-
|
|
25375
|
+
if (!previous) return this.forget(group.id, false);
|
|
25376
|
+
if (owed) previous.declined = owed;
|
|
25377
|
+
else delete previous.declined;
|
|
25378
|
+
this.acceptSaid.delete(group.id);
|
|
25379
|
+
this.stored.set(group.id, previous);
|
|
24965
25380
|
if (previous.state) this.attach(previous.state);
|
|
24966
25381
|
await this.store.putGroup(previous);
|
|
24967
25382
|
this.host.emit();
|
|
24968
25383
|
}
|
|
25384
|
+
/** Invitee side: says no to an invitation. With the inviter's chat down (its app is closed, say), returns the answer owed, said when that chat is next up. */
|
|
25385
|
+
sayDecline(groupId, linkId) {
|
|
25386
|
+
try {
|
|
25387
|
+
this.host.sendOnLink(linkId, {
|
|
25388
|
+
t: "group-decline",
|
|
25389
|
+
g: groupId
|
|
25390
|
+
});
|
|
25391
|
+
return;
|
|
25392
|
+
} catch {
|
|
25393
|
+
return {
|
|
25394
|
+
at: Date.now(),
|
|
25395
|
+
linkId
|
|
25396
|
+
};
|
|
25397
|
+
}
|
|
25398
|
+
}
|
|
25399
|
+
/** Invitee side: a decline its inviter's chat was down for is said once that chat is up, or dropped after `LEFT_KEPT_MS`. */
|
|
25400
|
+
async declineOwed(group, now) {
|
|
25401
|
+
const { at, linkId } = group.declined;
|
|
25402
|
+
if (now - at <= LEFT_KEPT_MS) {
|
|
25403
|
+
if (!this.host.linkReady(linkId)) return;
|
|
25404
|
+
try {
|
|
25405
|
+
this.host.sendOnLink(linkId, {
|
|
25406
|
+
t: "group-decline",
|
|
25407
|
+
g: group.id
|
|
25408
|
+
});
|
|
25409
|
+
} catch {
|
|
25410
|
+
return;
|
|
25411
|
+
}
|
|
25412
|
+
}
|
|
25413
|
+
if (this.stored.get(group.id) !== group) return;
|
|
25414
|
+
delete group.declined;
|
|
25415
|
+
if (group.state) return this.store.putGroup(group);
|
|
25416
|
+
this.stored.delete(group.id);
|
|
25417
|
+
await this.store.deleteGroup(group.id);
|
|
25418
|
+
}
|
|
24969
25419
|
/**
|
|
24970
25420
|
* Sends a text; `messageId` is the id it is kept under here (what history, replies and reactions name). `forwarded`:
|
|
24971
25421
|
* the hop count of a forwarded text (WISP 902 § Forwards). `card`: a checked status card, the text its fallback.
|
|
@@ -25030,6 +25480,15 @@ var Groups = class {
|
|
|
25030
25480
|
if (!groupOfFile(fileId, [...this.stored.keys(), ...this.communities.ids()]) || !this.files) throw new Error("No such file");
|
|
25031
25481
|
return this.files.download(fileId);
|
|
25032
25482
|
}
|
|
25483
|
+
/** The person stops a group's file that is coming or would be fetched (Cancel, Decline), or pauses it (`keep`). */
|
|
25484
|
+
haltFile(fileId, keep = false) {
|
|
25485
|
+
if (!groupOfFile(fileId, [...this.stored.keys(), ...this.communities.ids()]) || !this.files) throw new Error("No such file");
|
|
25486
|
+
return this.files.halt(fileId, keep);
|
|
25487
|
+
}
|
|
25488
|
+
/** Automatic downloads were turned off: the groups' files chosen by themselves and not here yet stop coming. */
|
|
25489
|
+
autoDownloadsOff() {
|
|
25490
|
+
return this.files?.autoDownloadsOff() ?? Promise.resolve();
|
|
25491
|
+
}
|
|
25033
25492
|
/** An edge's files/3 session opened or closed (WISP 503: the bytes of a group's files go over it). */
|
|
25034
25493
|
fileSession(groupId, peerKey, linkId, open) {
|
|
25035
25494
|
this.files?.session(groupId, peerKey, linkId, open);
|
|
@@ -25107,8 +25566,7 @@ var Groups = class {
|
|
|
25107
25566
|
...bye ? { bye } : {}
|
|
25108
25567
|
};
|
|
25109
25568
|
delete group.entry;
|
|
25110
|
-
|
|
25111
|
-
this.invited.delete(groupId);
|
|
25569
|
+
this.takeBack(groupId, group);
|
|
25112
25570
|
this.pendingEntries.delete(groupId);
|
|
25113
25571
|
this.welcomed.delete(groupId);
|
|
25114
25572
|
this.lingering.delete(groupId);
|
|
@@ -25275,8 +25733,14 @@ var Groups = class {
|
|
|
25275
25733
|
farewell.told ??= this.now();
|
|
25276
25734
|
this.store.putGroup(group).catch(() => {});
|
|
25277
25735
|
}
|
|
25278
|
-
makeAdmin(groupId, key) {
|
|
25279
|
-
|
|
25736
|
+
async makeAdmin(groupId, key) {
|
|
25737
|
+
if (this.isCommunity(groupId)) return this.communities.makeAdmin(groupId, key);
|
|
25738
|
+
await this.session(groupId).transferAdmin(key);
|
|
25739
|
+
const group = this.stored.get(groupId);
|
|
25740
|
+
if (!group || !this.invited.get(groupId)?.size) return;
|
|
25741
|
+
this.takeBack(groupId, group);
|
|
25742
|
+
await this.store.putGroup(group);
|
|
25743
|
+
this.host.emit();
|
|
25280
25744
|
}
|
|
25281
25745
|
/**
|
|
25282
25746
|
* The admin pins a member of a private group as a hub, excludes one from being a hub, or leaves it to the member's
|
|
@@ -25300,7 +25764,8 @@ var Groups = class {
|
|
|
25300
25764
|
if (this.isCommunity(groupId)) return this.communities.rename(groupId, name);
|
|
25301
25765
|
await this.session(groupId).rename(name);
|
|
25302
25766
|
}
|
|
25303
|
-
|
|
25767
|
+
/** `answer`: false when an invitation goes because its inviter took it back, so no decline is said for it. */
|
|
25768
|
+
async forget(groupId, answer = true) {
|
|
25304
25769
|
if (this.isCommunity(groupId)) return this.communities.leave(groupId);
|
|
25305
25770
|
const session = this.sessions.get(groupId);
|
|
25306
25771
|
if (session?.status === "active") try {
|
|
@@ -25308,13 +25773,16 @@ var Groups = class {
|
|
|
25308
25773
|
} catch (e) {
|
|
25309
25774
|
if (session.isAdmin && session.others.length) throw e;
|
|
25310
25775
|
}
|
|
25776
|
+
const gone = this.stored.get(groupId), invitation = answer && gone?.invitation && !gone.invitation.entry ? gone.invitation : void 0;
|
|
25777
|
+
const owed = invitation && this.sayDecline(groupId, invitation.linkId);
|
|
25311
25778
|
this.sessions.delete(groupId);
|
|
25312
25779
|
this.stored.delete(groupId);
|
|
25313
|
-
this.
|
|
25780
|
+
this.takeBack(groupId);
|
|
25314
25781
|
this.acceptSaid.delete(groupId);
|
|
25315
25782
|
this.lastRoster.delete(groupId);
|
|
25316
25783
|
this.lastTold.delete(groupId);
|
|
25317
25784
|
this.pendingEntries.delete(groupId);
|
|
25785
|
+
this.framesKept.delete(groupId);
|
|
25318
25786
|
this.knocked.delete(groupId);
|
|
25319
25787
|
this.knockCheckAt.delete(groupId);
|
|
25320
25788
|
this.knockRewrites.delete(groupId);
|
|
@@ -25322,18 +25790,31 @@ var Groups = class {
|
|
|
25322
25790
|
this.welcomed.delete(groupId);
|
|
25323
25791
|
this.lingering.delete(groupId);
|
|
25324
25792
|
this.relayAsked.delete(groupId);
|
|
25793
|
+
this.syncHeld.delete(groupId);
|
|
25794
|
+
this.lastHanded.delete(groupId);
|
|
25325
25795
|
this.hubs.forget(groupId);
|
|
25326
25796
|
this.removedAt.delete(groupId);
|
|
25327
25797
|
this.lastGossip.delete(groupId);
|
|
25328
25798
|
this.gapsAsked.delete(groupId);
|
|
25799
|
+
this.gapsGone.delete(groupId);
|
|
25329
25800
|
this.gapsLooked.delete(groupId);
|
|
25330
25801
|
this.typings.forget(groupId);
|
|
25802
|
+
for (const key of this.pinPaces.keys()) if (key.startsWith(`${groupId}\n`)) this.pinPaces.delete(key);
|
|
25331
25803
|
this.downSince.delete(groupId);
|
|
25332
25804
|
this.hereHeard.delete(groupId);
|
|
25333
25805
|
this.hereActed.delete(groupId);
|
|
25334
25806
|
this.signals.forget(groupId);
|
|
25335
25807
|
for (const linkId of [...this.host.edges(groupId).values(), ...this.host.entries(groupId).values()]) await this.host.closeEdge(linkId);
|
|
25336
25808
|
await this.store.deleteGroup(groupId);
|
|
25809
|
+
if (owed && !this.stored.has(groupId)) {
|
|
25810
|
+
const tombstone = {
|
|
25811
|
+
id: groupId,
|
|
25812
|
+
createdAt: gone.createdAt,
|
|
25813
|
+
declined: owed
|
|
25814
|
+
};
|
|
25815
|
+
this.stored.set(groupId, tombstone);
|
|
25816
|
+
await this.store.putGroup(tombstone);
|
|
25817
|
+
}
|
|
25337
25818
|
await this.files?.drop(groupId);
|
|
25338
25819
|
this.host.historyGone?.(groupId);
|
|
25339
25820
|
this.host.membersChanged?.(groupId);
|
|
@@ -25426,6 +25907,10 @@ var Groups = class {
|
|
|
25426
25907
|
if (now - group.left.at > LEFT_KEPT_MS) await this.forget(group.id);
|
|
25427
25908
|
continue;
|
|
25428
25909
|
}
|
|
25910
|
+
if (group.declined) {
|
|
25911
|
+
await this.declineOwed(group, now);
|
|
25912
|
+
if (!group.state) continue;
|
|
25913
|
+
}
|
|
25429
25914
|
if (group.invitation?.entry && !group.state) {
|
|
25430
25915
|
const every = now - group.createdAt > this.timings.patienceMs ? this.timings.slowKnockMs : this.timings.knockMs;
|
|
25431
25916
|
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(() => {});
|
|
@@ -25436,6 +25921,7 @@ var Groups = class {
|
|
|
25436
25921
|
continue;
|
|
25437
25922
|
}
|
|
25438
25923
|
const session = this.sessions.get(group.id);
|
|
25924
|
+
if (group.invited) await this.ageInvited(group, now);
|
|
25439
25925
|
if (!group.entry || !session) continue;
|
|
25440
25926
|
if (!session.isAdmin) {
|
|
25441
25927
|
await this.disableLink(group.id);
|
|
@@ -25511,6 +25997,13 @@ var Groups = class {
|
|
|
25511
25997
|
});
|
|
25512
25998
|
return;
|
|
25513
25999
|
}
|
|
26000
|
+
if (said.n >= ACCEPT_AGAINS && !said.down && !said.unanswered && !invitation.entry && now - said.at >= ACCEPT_AGAIN_MS) {
|
|
26001
|
+
this.acceptSaid.set(group.id, {
|
|
26002
|
+
...said,
|
|
26003
|
+
unanswered: true
|
|
26004
|
+
});
|
|
26005
|
+
this.host.emit();
|
|
26006
|
+
}
|
|
25514
26007
|
if (said.n >= ACCEPT_AGAINS || !said.down && now - said.at < ACCEPT_AGAIN_MS) return;
|
|
25515
26008
|
try {
|
|
25516
26009
|
this.host.sendOnLink(invitation.linkId, {
|
|
@@ -25523,7 +26016,8 @@ var Groups = class {
|
|
|
25523
26016
|
}
|
|
25524
26017
|
this.acceptSaid.set(group.id, {
|
|
25525
26018
|
at: now,
|
|
25526
|
-
n: said.n + 1
|
|
26019
|
+
n: said.n + 1,
|
|
26020
|
+
...said.unanswered && { unanswered: true }
|
|
25527
26021
|
});
|
|
25528
26022
|
}
|
|
25529
26023
|
/** An entry session came up with groups on both sides: the admin's side invites over it. */
|
|
@@ -25760,7 +26254,16 @@ var Groups = class {
|
|
|
25760
26254
|
}
|
|
25761
26255
|
case "group-accept": {
|
|
25762
26256
|
const session = this.sessions.get(g);
|
|
25763
|
-
if (
|
|
26257
|
+
if (typeof frame.key !== "string" || !MEMBER_KEY.test(frame.key)) return;
|
|
26258
|
+
if (!session?.isAdmin || !this.invited.get(g)?.has(linkId)) {
|
|
26259
|
+
if (!(!!session && rosterHas(session.roster, frame.key) && this.stored.get(g)?.contacts?.[frame.key] === linkId) && ![...this.host.entries(g).values()].includes(linkId)) try {
|
|
26260
|
+
this.host.sendOnLink(linkId, {
|
|
26261
|
+
t: "group-decline",
|
|
26262
|
+
g
|
|
26263
|
+
});
|
|
26264
|
+
} catch {}
|
|
26265
|
+
return;
|
|
26266
|
+
}
|
|
25764
26267
|
const group = this.stored.get(g);
|
|
25765
26268
|
if (frame.key === session.myKey || rosterHas(session.roster, frame.key)) return;
|
|
25766
26269
|
const entryPeer = this.hostEntry(g, linkId);
|
|
@@ -25794,6 +26297,11 @@ var Groups = class {
|
|
|
25794
26297
|
return;
|
|
25795
26298
|
}
|
|
25796
26299
|
case "group-decline": {
|
|
26300
|
+
const mine = this.stored.get(g);
|
|
26301
|
+
if (mine?.invitation && !mine.invitation.entry && !mine.state && mine.invitation.linkId === linkId) {
|
|
26302
|
+
await this.dropInvitation(mine);
|
|
26303
|
+
return;
|
|
26304
|
+
}
|
|
25797
26305
|
const entryPeer = this.hostEntry(g, linkId);
|
|
25798
26306
|
if (entryPeer) {
|
|
25799
26307
|
this.pendingEntries.get(g)?.delete(entryPeer);
|
|
@@ -25956,11 +26464,18 @@ var Groups = class {
|
|
|
25956
26464
|
return;
|
|
25957
26465
|
}
|
|
25958
26466
|
if (t === "group-pin") {
|
|
26467
|
+
const said = this.pinPace(groupId, peerKey).take() ? readPin(frame) : null;
|
|
26468
|
+
if (!said || !pinIsNewer(this.record(groupId)?.pin, said)) return;
|
|
25959
26469
|
const signed = session.signedPin(frame);
|
|
25960
26470
|
if (signed && rosterHas(session.roster, peerKey)) await this.host.groupPin?.(groupId, peerKey, signed);
|
|
25961
26471
|
return;
|
|
25962
26472
|
}
|
|
25963
26473
|
const taken = await session.handle(peerKey, frame);
|
|
26474
|
+
if (taken.some((f) => f.t === "group-msg")) {
|
|
26475
|
+
let handed = this.lastHanded.get(groupId);
|
|
26476
|
+
if (!handed) this.lastHanded.set(groupId, handed = /* @__PURE__ */ new Map());
|
|
26477
|
+
handed.set(peerKey, this.now());
|
|
26478
|
+
}
|
|
25964
26479
|
if (taken.length) this.hubs.passOn(groupId, session, peerKey, taken);
|
|
25965
26480
|
}
|
|
25966
26481
|
/**
|
|
@@ -26032,7 +26547,7 @@ var Groups = class {
|
|
|
26032
26547
|
const large = this.host.linkReady(linkId, 3) && this.markLarge(group, peerKey);
|
|
26033
26548
|
const legacy = this.hubs.enabled && this.hubs.edgeReady(groupId, session, group, peerKey, this.host.linkReady(linkId, 4), this.now());
|
|
26034
26549
|
if (large || legacy) this.store.putGroup(group).catch(() => {});
|
|
26035
|
-
try {
|
|
26550
|
+
if (!this.holdSync(groupId, peerKey)) try {
|
|
26036
26551
|
this.host.sendOnLink(linkId, session.syncFrame(this.askOf(groupId, session, peerKey)));
|
|
26037
26552
|
} catch {
|
|
26038
26553
|
return;
|
|
@@ -26155,11 +26670,55 @@ var Groups = class {
|
|
|
26155
26670
|
});
|
|
26156
26671
|
out.push(key);
|
|
26157
26672
|
}
|
|
26158
|
-
|
|
26673
|
+
const lacking = session.lacking();
|
|
26674
|
+
out.push(...this.gapsOf(groupId, session, via, lacking));
|
|
26675
|
+
this.ownGap(groupId, via, lacking);
|
|
26159
26676
|
if (out.length) this.lastGossip.set(groupId, now);
|
|
26160
26677
|
return out;
|
|
26161
26678
|
}
|
|
26162
26679
|
/**
|
|
26680
|
+
* Whether the answer of the member asked for someone's messages may still be coming: its edge up, asked under
|
|
26681
|
+
* `MESH_SYNC_HOLD_MAX_MS` ago, and a message from it (or the asking) under `MESH_SYNC_HOLD_MS` ago.
|
|
26682
|
+
*/
|
|
26683
|
+
answering(groupId, { via, at }, now) {
|
|
26684
|
+
const edge = this.host.edges(groupId).get(via);
|
|
26685
|
+
if (!edge || !this.host.linkReady(edge) || now - at >= 1e4) return false;
|
|
26686
|
+
return now - Math.max(at, this.lastHanded.get(groupId)?.get(via) ?? 0) < MESH_SYNC_HOLD_MS;
|
|
26687
|
+
}
|
|
26688
|
+
/**
|
|
26689
|
+
* Whether the sync to a member whose edge just opened waits (`heldSyncs` sends it): its messages were asked of
|
|
26690
|
+
* another member a moment ago, and that answer holds them among everyone's in the order they were written. Its own
|
|
26691
|
+
* answer now would bring the same messages on their own, ahead of or behind the others' lines.
|
|
26692
|
+
*/
|
|
26693
|
+
holdSync(groupId, peerKey) {
|
|
26694
|
+
const asked = this.relayAsked.get(groupId)?.get(peerKey);
|
|
26695
|
+
if (!asked || asked.via === peerKey || !this.answering(groupId, asked, this.now())) {
|
|
26696
|
+
this.syncHeld.get(groupId)?.delete(peerKey);
|
|
26697
|
+
this.relayAsked.get(groupId)?.delete(peerKey);
|
|
26698
|
+
return false;
|
|
26699
|
+
}
|
|
26700
|
+
let held = this.syncHeld.get(groupId);
|
|
26701
|
+
if (!held) this.syncHeld.set(groupId, held = /* @__PURE__ */ new Map());
|
|
26702
|
+
held.set(peerKey, { ...asked });
|
|
26703
|
+
return true;
|
|
26704
|
+
}
|
|
26705
|
+
/** The syncs that waited for another member's answer (`holdSync`), once it has come: for what it did not hand on. */
|
|
26706
|
+
heldSyncs(groupId, session, now) {
|
|
26707
|
+
const held = this.syncHeld.get(groupId);
|
|
26708
|
+
if (!held?.size) return;
|
|
26709
|
+
const edges = this.host.edges(groupId);
|
|
26710
|
+
for (const [key, asked] of held) {
|
|
26711
|
+
if (this.answering(groupId, asked, now)) continue;
|
|
26712
|
+
held.delete(key);
|
|
26713
|
+
if (this.relayAsked.get(groupId)?.get(key)?.via === asked.via) this.relayAsked.get(groupId).delete(key);
|
|
26714
|
+
const edge = edges.get(key);
|
|
26715
|
+
if (!edge || !this.host.linkReady(edge) || !rosterHas(session.roster, key)) continue;
|
|
26716
|
+
try {
|
|
26717
|
+
this.host.sendOnLink(edge, session.syncFrame(this.askOf(groupId, session, key)));
|
|
26718
|
+
} catch {}
|
|
26719
|
+
}
|
|
26720
|
+
}
|
|
26721
|
+
/**
|
|
26163
26722
|
* Reachable members whose messages below the highest seen never reached me, for `via` to hand on: their own answer
|
|
26164
26723
|
* comes from their last `GROUP_LIMITS.outlog` only (WISP 902 § Catch-up), the rest another member may hold. As
|
|
26165
26724
|
* `askOf`, one member at a time, and each member once until what is missing changes.
|
|
@@ -26189,6 +26748,7 @@ var Groups = class {
|
|
|
26189
26748
|
});
|
|
26190
26749
|
if (entry.of.has(via)) continue;
|
|
26191
26750
|
entry.of.add(via);
|
|
26751
|
+
entry.at = now;
|
|
26192
26752
|
entry.last = {
|
|
26193
26753
|
via,
|
|
26194
26754
|
at: now,
|
|
@@ -26203,6 +26763,55 @@ var Groups = class {
|
|
|
26203
26763
|
return out;
|
|
26204
26764
|
}
|
|
26205
26765
|
/**
|
|
26766
|
+
* Whether a sync to `via` asks it for its own messages that never reached me, for the first time since what is
|
|
26767
|
+
* missing changed: any sync names them (`miss`), and an author answers from its own log (`GROUP_LIMITS.outlog`).
|
|
26768
|
+
*/
|
|
26769
|
+
ownGap(groupId, via, lacking) {
|
|
26770
|
+
const mark = lacking.get(via);
|
|
26771
|
+
if (mark === void 0) return false;
|
|
26772
|
+
let tried = this.gapsAsked.get(groupId);
|
|
26773
|
+
if (!tried) this.gapsAsked.set(groupId, tried = /* @__PURE__ */ new Map());
|
|
26774
|
+
let entry = tried.get(via);
|
|
26775
|
+
if (!entry || entry.mark !== mark) tried.set(via, entry = {
|
|
26776
|
+
mark,
|
|
26777
|
+
of: /* @__PURE__ */ new Set(),
|
|
26778
|
+
noticed: entry?.noticed,
|
|
26779
|
+
last: entry?.last
|
|
26780
|
+
});
|
|
26781
|
+
if (entry.of.has(via)) return false;
|
|
26782
|
+
entry.of.add(via);
|
|
26783
|
+
entry.at = this.now();
|
|
26784
|
+
return true;
|
|
26785
|
+
}
|
|
26786
|
+
/**
|
|
26787
|
+
* Messages nobody holds any more: every connected member, their author included, was asked for the same missing
|
|
26788
|
+
* numbers `MESH_GAP_AGAIN_MS` ago or more and none arrived. An author keeps its last 32 and the others the group's
|
|
26789
|
+
* last 256 (WISP 902 § Bounds), so what a sync names and nobody hands on is gone, and so is what is below it (logs
|
|
26790
|
+
* drop the oldest first). The member list counted them until the next epoch, as if they were still on their way.
|
|
26791
|
+
*/
|
|
26792
|
+
gapsSettle(groupId, session, now, connected, lacking) {
|
|
26793
|
+
const tried = this.gapsAsked.get(groupId);
|
|
26794
|
+
if (!tried) return;
|
|
26795
|
+
let changed = false;
|
|
26796
|
+
for (const [key, mark] of lacking) {
|
|
26797
|
+
const entry = tried.get(key);
|
|
26798
|
+
if (!entry || entry.mark !== mark || entry.at === void 0 || now - entry.at < 1e4) continue;
|
|
26799
|
+
if (!connected.includes(key) || !connected.every((member) => entry.of.has(member))) continue;
|
|
26800
|
+
let gone = this.gapsGone.get(groupId);
|
|
26801
|
+
if (!gone) this.gapsGone.set(groupId, gone = /* @__PURE__ */ new Map());
|
|
26802
|
+
const upTo = { ...gone.get(key) };
|
|
26803
|
+
for (const [e, numbers] of Object.entries(session.missOf(key))) {
|
|
26804
|
+
const top = Math.max(...numbers);
|
|
26805
|
+
if (top > (upTo[e] ?? -1)) {
|
|
26806
|
+
upTo[e] = top;
|
|
26807
|
+
changed = true;
|
|
26808
|
+
}
|
|
26809
|
+
}
|
|
26810
|
+
gone.set(key, upTo);
|
|
26811
|
+
}
|
|
26812
|
+
if (changed) this.host.emit();
|
|
26813
|
+
}
|
|
26814
|
+
/**
|
|
26206
26815
|
* Gaps asked for sooner than the gossip turn: one noticed `MESH_GAP_AGAIN_MS` ago and not asked for yet (an author's
|
|
26207
26816
|
* own answer, its last 32, showed what is below them) goes to a connected member now; and a member that handed on some
|
|
26208
26817
|
* of what I last asked it for likely holds the rest, so it is asked for the next numbers `MESH_GAP_AGAIN_MS` after. A
|
|
@@ -26217,6 +26826,7 @@ var Groups = class {
|
|
|
26217
26826
|
if (!lacking.size && !tried?.size) return;
|
|
26218
26827
|
if (!tried) this.gapsAsked.set(groupId, tried = /* @__PURE__ */ new Map());
|
|
26219
26828
|
for (const key of tried.keys()) if (!lacking.has(key)) tried.delete(key);
|
|
26829
|
+
this.gapsSettle(groupId, session, now, connected, lacking);
|
|
26220
26830
|
const edges = this.host.edges(groupId), again = /* @__PURE__ */ new Map(), fresh = /* @__PURE__ */ new Map();
|
|
26221
26831
|
const up = (key) => {
|
|
26222
26832
|
const edge = edges.get(key);
|
|
@@ -26244,6 +26854,7 @@ var Groups = class {
|
|
|
26244
26854
|
tried.set(key, {
|
|
26245
26855
|
mark: lacking.get(key),
|
|
26246
26856
|
of: /* @__PURE__ */ new Set([via]),
|
|
26857
|
+
at: now,
|
|
26247
26858
|
last: {
|
|
26248
26859
|
via,
|
|
26249
26860
|
at: now,
|
|
@@ -26281,6 +26892,7 @@ var Groups = class {
|
|
|
26281
26892
|
if (session.status !== "active" || this.stored.get(groupId)?.left) continue;
|
|
26282
26893
|
session.reseal().catch(() => {});
|
|
26283
26894
|
this.farewellsTick(groupId, session, now);
|
|
26895
|
+
this.heldSyncs(groupId, session, now);
|
|
26284
26896
|
if (reconcile) {
|
|
26285
26897
|
const edges = this.host.edges(groupId);
|
|
26286
26898
|
if (!this.hubs.large(session) && session.others.some((key) => !edges.has(key))) this.reconcileEdges(groupId);
|
|
@@ -26303,7 +26915,7 @@ var Groups = class {
|
|
|
26303
26915
|
at: now
|
|
26304
26916
|
});
|
|
26305
26917
|
const ask = [...away, ...this.gapsOf(groupId, session, via, lacking)];
|
|
26306
|
-
if (!ask.length) continue;
|
|
26918
|
+
if (!this.ownGap(groupId, via, lacking) && !ask.length) continue;
|
|
26307
26919
|
try {
|
|
26308
26920
|
this.host.sendOnLink(edges.get(via), session.syncFrame(ask));
|
|
26309
26921
|
} catch {}
|
|
@@ -26329,6 +26941,25 @@ var Groups = class {
|
|
|
26329
26941
|
if (session && this.stored.get(groupId)?.farewells?.[peerKey] && !rosterHas(session.roster, peerKey)) return;
|
|
26330
26942
|
session?.setNick(peerKey, nick);
|
|
26331
26943
|
}
|
|
26944
|
+
/**
|
|
26945
|
+
* A message's row took the frame it came in: past `GROUP_LIMITS.relay` frames (or their bytes) in a group, the oldest
|
|
26946
|
+
* rows give theirs up, so a group's history holds the frames of its newest messages only.
|
|
26947
|
+
*/
|
|
26948
|
+
async frameKept(groupId, id, frame) {
|
|
26949
|
+
let kept = this.framesKept.get(groupId);
|
|
26950
|
+
if (!kept) this.framesKept.set(groupId, kept = []);
|
|
26951
|
+
if (kept.some((k) => k.id === id)) return;
|
|
26952
|
+
kept.push({
|
|
26953
|
+
id,
|
|
26954
|
+
bytes: frameBytes(frame)
|
|
26955
|
+
});
|
|
26956
|
+
let bytes = kept.reduce((sum, k) => sum + k.bytes, 0);
|
|
26957
|
+
while (kept.length > GROUP_LIMITS.relay || bytes > GROUP_LIMITS.relayBytes) {
|
|
26958
|
+
const oldest = kept.shift();
|
|
26959
|
+
bytes -= oldest.bytes;
|
|
26960
|
+
await this.host.forgetFrame?.(MESSAGE_LINK(groupId), oldest.id).catch(() => {});
|
|
26961
|
+
}
|
|
26962
|
+
}
|
|
26332
26963
|
session(groupId) {
|
|
26333
26964
|
const session = this.sessions.get(groupId);
|
|
26334
26965
|
if (!session) throw new Error("Group not found");
|
|
@@ -26342,7 +26973,7 @@ var Groups = class {
|
|
|
26342
26973
|
adminTurn: () => this.host.adminTurn?.(state.id) ?? Promise.resolve(true),
|
|
26343
26974
|
save: async (next) => {
|
|
26344
26975
|
const group = this.stored.get(state.id);
|
|
26345
|
-
if (!group) return;
|
|
26976
|
+
if (!group || this.sessions.get(state.id) !== session) return;
|
|
26346
26977
|
group.state = next;
|
|
26347
26978
|
await this.store.putGroup(group);
|
|
26348
26979
|
},
|
|
@@ -26384,9 +27015,12 @@ var Groups = class {
|
|
|
26384
27015
|
...m.reply && { replyTo: groupReply(m.reply, session.myKey) },
|
|
26385
27016
|
...m.forwarded && { forwarded: m.forwarded },
|
|
26386
27017
|
...m.card && { card: m.card },
|
|
26387
|
-
...file && { file }
|
|
27018
|
+
...file && { file },
|
|
27019
|
+
...m.frame && { frame: m.frame }
|
|
26388
27020
|
};
|
|
26389
|
-
const
|
|
27021
|
+
const completed = !!m.completes && !!this.host.completeMessage;
|
|
27022
|
+
const stored = completed ? (await this.host.completeMessage(message), false) : await this.host.storeMessage(message);
|
|
27023
|
+
if (m.frame && (completed || stored !== false)) await this.frameKept(state.id, m.id, m.frame);
|
|
26390
27024
|
const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, arrivalNow(this.now()));
|
|
26391
27025
|
this.lastMessageAt.set(state.id, Math.max(this.lastMessageAt.get(state.id) ?? 0, came));
|
|
26392
27026
|
if (mentioned) this.lastMentionAt.set(state.id, Math.max(this.lastMentionAt.get(state.id) ?? 0, came));
|
|
@@ -26860,9 +27494,12 @@ function summary(note) {
|
|
|
26860
27494
|
const amount = note.decimals ? `${note.amount} base units of ${note.unit}` : `${Number(note.amount).toLocaleString()} ${note.test ? "test sats" : "sats"}`;
|
|
26861
27495
|
return note.kind === "request" ? `⚡ Requested ${amount}` : `⚡ Sent ${amount}`;
|
|
26862
27496
|
}
|
|
27497
|
+
/** A second past the receiver's window, so one window's edits have left it on the member's side whatever the edge's delay. */
|
|
27498
|
+
var GROUP_EDIT_RESEND_WINDOW_MS = EDIT_RATE_WINDOW_MS + 1e3;
|
|
26863
27499
|
/** An edit that could not go (no epoch key yet) is tried again after this long. */
|
|
26864
27500
|
var RETRY_MS = 1e4;
|
|
26865
27501
|
var chatOf = (groupId) => `group:${groupId}`;
|
|
27502
|
+
var resendKey = (groupId, member) => `${groupId}\n${member}`;
|
|
26866
27503
|
var GroupEdits = class {
|
|
26867
27504
|
host;
|
|
26868
27505
|
now;
|
|
@@ -26871,10 +27508,23 @@ var GroupEdits = class {
|
|
|
26871
27508
|
cards;
|
|
26872
27509
|
sendPace = /* @__PURE__ */ new Map();
|
|
26873
27510
|
receivePace = /* @__PURE__ */ new Map();
|
|
27511
|
+
/** Per group, per member (`group\nmember`): its edits past the window, by message, waiting for room (`hold`). */
|
|
27512
|
+
held = /* @__PURE__ */ new Map();
|
|
27513
|
+
heldTimers = /* @__PURE__ */ new Map();
|
|
26874
27514
|
timers = /* @__PURE__ */ new Map();
|
|
26875
27515
|
flushing = /* @__PURE__ */ new Map();
|
|
26876
27516
|
/** Per group: my rows known to have an edit to say, each with when it was last told (`edit`), by `tick`. */
|
|
26877
27517
|
known = /* @__PURE__ */ new Map();
|
|
27518
|
+
/**
|
|
27519
|
+
* Per group and member (`resendKey`): the last edit of mine said on its edge, in the order `resend` says them. What
|
|
27520
|
+
* comes after it is what the member has not heard there. Not kept across a restart.
|
|
27521
|
+
*/
|
|
27522
|
+
heard = /* @__PURE__ */ new Map();
|
|
27523
|
+
/** The connection `resend` last spoke on, per group and member: a new one hears my latest edits again. */
|
|
27524
|
+
resentOn = /* @__PURE__ */ new Map();
|
|
27525
|
+
resendPace = /* @__PURE__ */ new Map();
|
|
27526
|
+
resendTimers = /* @__PURE__ */ new Map();
|
|
27527
|
+
resending = /* @__PURE__ */ new Map();
|
|
26878
27528
|
tick = 0;
|
|
26879
27529
|
/**
|
|
26880
27530
|
* The groups whose history was read for the rows with an edit to say: `known` has them all, and from then on those
|
|
@@ -27023,7 +27673,8 @@ var GroupEdits = class {
|
|
|
27023
27673
|
/**
|
|
27024
27674
|
* An edit from a member, authenticated as `sender` by the group. Believed only while the sender is in the group (a
|
|
27025
27675
|
* removed member's edits are dropped) and only of the sender's own message; the highest number wins. One of a message
|
|
27026
|
-
* not here yet waits a minute for it. Past `EDIT_RECEIVE_LIMIT` a window from one member, the rest
|
|
27676
|
+
* not here yet waits a minute for it. Past `EDIT_RECEIVE_LIMIT` a window from one member, the rest wait for the next
|
|
27677
|
+
* window (`hold`): a catch-up hands back every edit of an author at once, and one dropped is never handed again.
|
|
27027
27678
|
*/
|
|
27028
27679
|
async receive(groupId, sender, edit) {
|
|
27029
27680
|
const membership = this.host.membership(groupId);
|
|
@@ -27031,7 +27682,65 @@ var GroupEdits = class {
|
|
|
27031
27682
|
const key = `${groupId}\n${sender}`;
|
|
27032
27683
|
let pace = this.receivePace.get(key);
|
|
27033
27684
|
if (!pace) this.receivePace.set(key, pace = new RateWindow(30, EDIT_RATE_WINDOW_MS, this.now));
|
|
27034
|
-
if (!pace.take()) return
|
|
27685
|
+
if (this.held.get(groupId)?.has(key) || !pace.take()) return this.hold(groupId, sender, edit, pace);
|
|
27686
|
+
return this.take(groupId, membership, sender, edit);
|
|
27687
|
+
}
|
|
27688
|
+
/**
|
|
27689
|
+
* An edit past what a member's window takes: kept, the highest number per message, `GROUP_EDITS_HELD` messages a
|
|
27690
|
+
* group at most (the oldest go first), and taken when the window has room. The work stays what the window allows.
|
|
27691
|
+
*/
|
|
27692
|
+
hold(groupId, sender, edit, pace) {
|
|
27693
|
+
const key = `${groupId}\n${sender}`;
|
|
27694
|
+
let group = this.held.get(groupId);
|
|
27695
|
+
if (!group) this.held.set(groupId, group = /* @__PURE__ */ new Map());
|
|
27696
|
+
let waiting = group.get(key);
|
|
27697
|
+
if (!waiting) group.set(key, waiting = /* @__PURE__ */ new Map());
|
|
27698
|
+
const known = waiting.get(edit.id);
|
|
27699
|
+
if (known && known.e >= edit.e) return "dropped";
|
|
27700
|
+
waiting.delete(edit.id);
|
|
27701
|
+
waiting.set(edit.id, edit);
|
|
27702
|
+
let count = 0;
|
|
27703
|
+
for (const member of group.values()) count += member.size;
|
|
27704
|
+
for (const [member, edits] of group) {
|
|
27705
|
+
if (count <= 256) break;
|
|
27706
|
+
while (count > 256 && edits.size) {
|
|
27707
|
+
edits.delete(edits.keys().next().value);
|
|
27708
|
+
count--;
|
|
27709
|
+
}
|
|
27710
|
+
if (!edits.size && member !== key) group.delete(member);
|
|
27711
|
+
}
|
|
27712
|
+
if (!this.heldTimers.has(key) && !this.stopped) this.heldTimers.set(key, setTimeout(() => {
|
|
27713
|
+
this.takeHeld(groupId, sender).catch(() => {});
|
|
27714
|
+
}, Math.max(50, pace.wait())));
|
|
27715
|
+
return waiting.has(edit.id) ? "waiting" : "dropped";
|
|
27716
|
+
}
|
|
27717
|
+
/** The window of a member has room again: its edits that waited are taken, oldest first, as many as it allows. */
|
|
27718
|
+
async takeHeld(groupId, sender) {
|
|
27719
|
+
const key = `${groupId}\n${sender}`;
|
|
27720
|
+
this.heldTimers.delete(key);
|
|
27721
|
+
const group = this.held.get(groupId), waiting = group?.get(key), pace = this.receivePace.get(key);
|
|
27722
|
+
if (this.stopped || !group || !waiting || !pace) return;
|
|
27723
|
+
for (const [id, edit] of [...waiting]) {
|
|
27724
|
+
const membership = this.host.membership(groupId);
|
|
27725
|
+
if (!membership?.members.has(sender)) {
|
|
27726
|
+
waiting.clear();
|
|
27727
|
+
break;
|
|
27728
|
+
}
|
|
27729
|
+
if (!pace.take()) break;
|
|
27730
|
+
if (waiting.get(id) !== edit) continue;
|
|
27731
|
+
waiting.delete(id);
|
|
27732
|
+
await this.take(groupId, membership, sender, edit).catch(() => {});
|
|
27733
|
+
}
|
|
27734
|
+
if (waiting.size) {
|
|
27735
|
+
if (!this.heldTimers.has(key) && !this.stopped) this.heldTimers.set(key, setTimeout(() => {
|
|
27736
|
+
this.takeHeld(groupId, sender).catch(() => {});
|
|
27737
|
+
}, Math.max(50, pace.wait())));
|
|
27738
|
+
return;
|
|
27739
|
+
}
|
|
27740
|
+
if (group.get(key) === waiting) group.delete(key);
|
|
27741
|
+
if (!group.size) this.held.delete(groupId);
|
|
27742
|
+
}
|
|
27743
|
+
async take(groupId, membership, sender, edit) {
|
|
27035
27744
|
const chat = chatOf(groupId);
|
|
27036
27745
|
const message = await this.find(chat, edit.id);
|
|
27037
27746
|
if (!message) {
|
|
@@ -27079,24 +27788,80 @@ var GroupEdits = class {
|
|
|
27079
27788
|
await this.host.changed(chat, message.id);
|
|
27080
27789
|
return "applied";
|
|
27081
27790
|
}
|
|
27082
|
-
/**
|
|
27083
|
-
|
|
27084
|
-
|
|
27085
|
-
|
|
27086
|
-
|
|
27087
|
-
|
|
27088
|
-
|
|
27089
|
-
|
|
27090
|
-
|
|
27091
|
-
|
|
27092
|
-
|
|
27093
|
-
}
|
|
27791
|
+
/**
|
|
27792
|
+
* An edge of a private group opened: the member at its other end hears the edits of mine it has not heard on that
|
|
27793
|
+
* edge, the latest of each message, oldest first. `GROUP_EDIT_RESEND` go a window and the rest a window later each,
|
|
27794
|
+
* while the edge is the same: a member away while I closed forty task cards reads them all as closed, not the latest
|
|
27795
|
+
* sixteen. Nothing known of what it heard (the first time since the app started): my newest `GROUP_EDIT_CATCH_UP`.
|
|
27796
|
+
* A new connection hears my latest `GROUP_EDIT_RESEND` again whatever it heard: one may have come before its message
|
|
27797
|
+
* and been dropped. Called again while the edge is the same, it goes on from where it was.
|
|
27798
|
+
*/
|
|
27799
|
+
resend(groupId, to) {
|
|
27800
|
+
const key = resendKey(groupId, to);
|
|
27801
|
+
const run = (this.resending.get(key) ?? Promise.resolve()).then(() => this.resendNow(groupId, to));
|
|
27802
|
+
const settled = run.catch(() => {}).finally(() => {
|
|
27803
|
+
if (this.resending.get(key) === settled) this.resending.delete(key);
|
|
27804
|
+
});
|
|
27805
|
+
this.resending.set(key, settled);
|
|
27806
|
+
return run;
|
|
27807
|
+
}
|
|
27808
|
+
async resendNow(groupId, to) {
|
|
27809
|
+
const key = resendKey(groupId, to), chat = chatOf(groupId);
|
|
27810
|
+
clearTimeout(this.resendTimers.get(key));
|
|
27811
|
+
this.resendTimers.delete(key);
|
|
27812
|
+
const open = () => this.host.edge ? this.host.edge(groupId, to) : true;
|
|
27813
|
+
const edge = open(), membership = this.host.membership(groupId);
|
|
27814
|
+
if (this.stopped || edge === void 0 || !membership || membership.community || !membership.members.has(to)) return;
|
|
27815
|
+
const mine = (await (this.host.history ? this.host.history(chat) : this.host.messages(chat))).filter((m) => m.sender === "me" && m.edit && !m.edit.pending && canEditInGroup(m)).sort((a, b) => a.edit.at - b.edit.at || (a.id < b.id ? -1 : 1));
|
|
27816
|
+
const last = this.heard.get(key);
|
|
27817
|
+
const after = (m) => !last || m.edit.at > last.at || m.edit.at === last.at && m.id > last.id;
|
|
27818
|
+
const unheard = last ? mine.filter(after).length : Math.min(mine.length, 256);
|
|
27819
|
+
const again = this.resentOn.get(key) === edge ? 0 : Math.min(mine.length, 16);
|
|
27820
|
+
this.resentOn.set(key, edge);
|
|
27821
|
+
const due = mine.slice(mine.length - Math.max(unheard, again));
|
|
27822
|
+
let pace = this.resendPace.get(key);
|
|
27823
|
+
if (!pace) this.resendPace.set(key, pace = new RateWindow(16, GROUP_EDIT_RESEND_WINDOW_MS, this.now));
|
|
27824
|
+
for (const m of due) {
|
|
27825
|
+
if (this.stopped || open() !== edge) return;
|
|
27826
|
+
if (!pace.take()) {
|
|
27827
|
+
this.resendTimers.set(key, setTimeout(() => {
|
|
27828
|
+
this.resend(groupId, to).catch(() => {});
|
|
27829
|
+
}, Math.max(50, pace.wait())));
|
|
27830
|
+
return;
|
|
27831
|
+
}
|
|
27832
|
+
await this.host.send(groupId, {
|
|
27833
|
+
id: m.id,
|
|
27834
|
+
e: m.edit.seq,
|
|
27835
|
+
ts: m.edit.at,
|
|
27836
|
+
m: m.text,
|
|
27837
|
+
...m.mentions?.length && { k: m.mentions },
|
|
27838
|
+
...m.card && { sc: m.card }
|
|
27839
|
+
}, to);
|
|
27840
|
+
if (open() !== edge) return;
|
|
27841
|
+
if (after(m)) this.heard.set(key, {
|
|
27842
|
+
at: m.edit.at,
|
|
27843
|
+
id: m.id
|
|
27844
|
+
});
|
|
27845
|
+
}
|
|
27846
|
+
}
|
|
27847
|
+
/** A group is forgotten, or the app stops: what was known of its members' edges goes with it. */
|
|
27848
|
+
dropResends(groupId) {
|
|
27849
|
+
const of = (key) => groupId === void 0 || key.startsWith(`${groupId}\n`);
|
|
27850
|
+
for (const [key, timer] of this.resendTimers) if (of(key)) {
|
|
27851
|
+
clearTimeout(timer);
|
|
27852
|
+
this.resendTimers.delete(key);
|
|
27853
|
+
}
|
|
27854
|
+
for (const key of [...this.heard.keys()]) if (of(key)) this.heard.delete(key);
|
|
27855
|
+
for (const key of [...this.resendPace.keys()]) if (of(key)) this.resendPace.delete(key);
|
|
27856
|
+
for (const key of [...this.resentOn.keys()]) if (of(key)) this.resentOn.delete(key);
|
|
27094
27857
|
}
|
|
27095
27858
|
forget(groupId) {
|
|
27096
27859
|
clearTimeout(this.timers.get(groupId));
|
|
27097
27860
|
this.timers.delete(groupId);
|
|
27098
27861
|
this.known.delete(groupId);
|
|
27099
27862
|
this.learnt.delete(groupId);
|
|
27863
|
+
this.dropResends(groupId);
|
|
27864
|
+
this.dropHeld(groupId);
|
|
27100
27865
|
this.buffer.forget(chatOf(groupId));
|
|
27101
27866
|
}
|
|
27102
27867
|
stop() {
|
|
@@ -27104,6 +27869,16 @@ var GroupEdits = class {
|
|
|
27104
27869
|
this.cards.stop();
|
|
27105
27870
|
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
27106
27871
|
this.timers.clear();
|
|
27872
|
+
this.dropResends();
|
|
27873
|
+
this.dropHeld();
|
|
27874
|
+
}
|
|
27875
|
+
dropHeld(groupId) {
|
|
27876
|
+
for (const [key, timer] of this.heldTimers) if (groupId === void 0 || key.startsWith(`${groupId}\n`)) {
|
|
27877
|
+
clearTimeout(timer);
|
|
27878
|
+
this.heldTimers.delete(key);
|
|
27879
|
+
}
|
|
27880
|
+
if (groupId === void 0) this.held.clear();
|
|
27881
|
+
else this.held.delete(groupId);
|
|
27107
27882
|
}
|
|
27108
27883
|
};
|
|
27109
27884
|
//#endregion
|
|
@@ -28068,7 +28843,7 @@ var HoldEngine = class {
|
|
|
28068
28843
|
this.host.changed();
|
|
28069
28844
|
return;
|
|
28070
28845
|
}
|
|
28071
|
-
const pointer = packet ? keys.readPointer(packet) : null;
|
|
28846
|
+
const pointer = packet ? keys.readPointer(packet, this.host.loopbackStorage?.()) : null;
|
|
28072
28847
|
if (!pointer) return;
|
|
28073
28848
|
let hold = this.state(linkId);
|
|
28074
28849
|
if (pointer.rev < hold.peerPointerRev) return;
|
|
@@ -28107,11 +28882,12 @@ var HoldEngine = class {
|
|
|
28107
28882
|
try {
|
|
28108
28883
|
const manifest = keys.open(await this.fetchBytes(pointer.manifestUrl, HOLD_LIMITS.maxManifestBytes), {
|
|
28109
28884
|
maxBytes: HOLD_LIMITS.maxManifestBytes,
|
|
28110
|
-
now
|
|
28885
|
+
now,
|
|
28886
|
+
loopback: this.host.loopbackStorage?.()
|
|
28111
28887
|
});
|
|
28112
28888
|
if (manifest.header.kind !== "manifest") throw new HoldRefusedError("format", "Not a manifest");
|
|
28113
28889
|
mailbox = manifest.header.mailbox;
|
|
28114
|
-
entries = readManifest(manifest.header.meta).filter(([seq]) => seq > this.state(linkId).inSeq).sort((a, b) => a[0] - b[0]);
|
|
28890
|
+
entries = readManifest(manifest.header.meta, this.host.loopbackStorage?.()).filter(([seq]) => seq > this.state(linkId).inSeq).sort((a, b) => a[0] - b[0]);
|
|
28115
28891
|
} catch (error) {
|
|
28116
28892
|
if (error instanceof HoldRefusedError) {
|
|
28117
28893
|
await this.save(linkId, {
|
|
@@ -28253,9 +29029,11 @@ var HoldEngine = class {
|
|
|
28253
29029
|
}
|
|
28254
29030
|
}
|
|
28255
29031
|
async readBytes(url, max, signal) {
|
|
29032
|
+
if (!isHoldUrl(url, this.host.loopbackStorage?.())) throw new Error("The storage address is not one this app reads");
|
|
28256
29033
|
const response = await (this.host.fetch ?? ((...args) => fetch(...args)))(url, {
|
|
28257
29034
|
cache: "no-store",
|
|
28258
29035
|
credentials: "omit",
|
|
29036
|
+
redirect: "error",
|
|
28259
29037
|
signal
|
|
28260
29038
|
});
|
|
28261
29039
|
if (!response.ok) throw new Error(`The storage answered ${response.status}`);
|
|
@@ -33762,6 +34540,17 @@ var WalletSetup = class {
|
|
|
33762
34540
|
var RELAY_RETRY_MS = 3e4;
|
|
33763
34541
|
/** Native listeners an app may hold per transport (the Desktop's Rust Iroh allows eight, `paired_transport.rs`). */
|
|
33764
34542
|
var NATIVE_SLOTS = 8;
|
|
34543
|
+
/**
|
|
34544
|
+
* Iroh's listeners where it is the browser build (the web app, the extension, the CLI): Rust's eight are not its limit.
|
|
34545
|
+
* Each is a WebSocket to its relay and about 0.2 MiB: a phone-sized Chromium page held 128 of them, and the public
|
|
34546
|
+
* relays took sixteen started one after another, as the queue here starts them (six of sixteen started at once were
|
|
34547
|
+
* refused). 1:1 chats hold eight of them at most, as before: the rest is room for group links. HyperDHT keeps eight
|
|
34548
|
+
* there too: a page's listeners share one WebSocket to its relay, which drops a client past sixteen listens (device
|
|
34549
|
+
* links listen there as well), and each of the CLI's is a whole DHT node.
|
|
34550
|
+
*/
|
|
34551
|
+
var IROH_WEB_SLOTS = 16;
|
|
34552
|
+
/** Of a transport's listeners, the ones a group's links never hold: 1:1 chats keep that room. */
|
|
34553
|
+
var CHAT_NATIVE_SLOTS = 4;
|
|
33765
34554
|
/** How often a group link that found no free native slot tries again. */
|
|
33766
34555
|
var GROUP_NATIVE_RETRY_MS = 15e3;
|
|
33767
34556
|
/** The longest a group link whose native listener failed to start waits before trying again (its wait doubles up to it). */
|
|
@@ -33796,6 +34585,8 @@ var CARD_RESTORE_MOST = 50;
|
|
|
33796
34585
|
var CARD_RESTORE_MS = 6048e5;
|
|
33797
34586
|
/** A reaction said on the live session and not confirmed is said again after this long. */
|
|
33798
34587
|
var REACTION_RESEND_MS = 3e4;
|
|
34588
|
+
/** A file or a payment request of mine that waits for the chat to be live (a text that waits is its outbox's). */
|
|
34589
|
+
var waitsForLive = (m) => m.sender === "me" && m.delivery === "waiting" && (!!m.file || !!m.paymentId);
|
|
33799
34590
|
/** This side's participation key in a chat: derived from its seed once, not at every state built. */
|
|
33800
34591
|
function participationKeyOf(live) {
|
|
33801
34592
|
const seed = live.stored.participationSeed;
|
|
@@ -34131,6 +34922,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34131
34922
|
});
|
|
34132
34923
|
stateTimer = null;
|
|
34133
34924
|
paymentTimer = null;
|
|
34925
|
+
/**
|
|
34926
|
+
* The reviews whose outcome is still open (submitted, unknown), by id: what the payment poll asks about, so that it
|
|
34927
|
+
* does not read every review ever made every 10 s. `null` until the store was read once. An id is only ever added
|
|
34928
|
+
* from a read of the store or a change of a review, and leaves when the poll finds its review ended or gone.
|
|
34929
|
+
*/
|
|
34930
|
+
openReviews = null;
|
|
34931
|
+
noteOpenReviews(reviews) {
|
|
34932
|
+
const open = this.openReviews ??= /* @__PURE__ */ new Set();
|
|
34933
|
+
for (const review of reviews) if (review.state === "submitted" || review.state === "unknown") open.add(review.id);
|
|
34934
|
+
}
|
|
34134
34935
|
/** Every minute, the reviews no Approve can send any more are cancelled (dropStaleReviews). */
|
|
34135
34936
|
staleReviewTimer;
|
|
34136
34937
|
/** Group links: admins read knocks, joiners knock (WISP 902 § Entry link). */
|
|
@@ -34253,6 +35054,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34253
35054
|
}
|
|
34254
35055
|
}
|
|
34255
35056
|
], (review) => {
|
|
35057
|
+
if (this.openReviews) this.noteOpenReviews([review]);
|
|
34256
35058
|
if (review.state === "settled") this.feedback("confirmed", review.id);
|
|
34257
35059
|
if (review.state === "failed") this.cueFeedback({
|
|
34258
35060
|
cue: "failed",
|
|
@@ -34460,12 +35262,12 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34460
35262
|
await this.messagesChanged(linkId, [messageId]);
|
|
34461
35263
|
this.emitState();
|
|
34462
35264
|
},
|
|
34463
|
-
text: async (linkId, messageId) => (await db.
|
|
35265
|
+
text: async (linkId, messageId) => (await db.getMessage(linkId, messageId))?.text ?? null,
|
|
34464
35266
|
reply: async (linkId, messageId) => {
|
|
34465
|
-
const message =
|
|
35267
|
+
const message = await db.getMessage(linkId, messageId);
|
|
34466
35268
|
return message && GhostlyNode.wireReply(message);
|
|
34467
35269
|
},
|
|
34468
|
-
forwarded: async (linkId, messageId) => (await db.
|
|
35270
|
+
forwarded: async (linkId, messageId) => (await db.getMessage(linkId, messageId))?.forwarded,
|
|
34469
35271
|
file: async (fileId) => {
|
|
34470
35272
|
const stored = await fileStore.get(fileId);
|
|
34471
35273
|
if (!stored?.metadata) return null;
|
|
@@ -34569,6 +35371,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34569
35371
|
},
|
|
34570
35372
|
receivePaymentRequest: (linkId, request) => this.desk.onPaymentRequest(linkId, request, true),
|
|
34571
35373
|
changed: () => this.emitState(),
|
|
35374
|
+
loopbackStorage: () => this.options.holdLoopback === true,
|
|
34572
35375
|
ttlMs: () => {
|
|
34573
35376
|
try {
|
|
34574
35377
|
const raw = typeof localStorage !== "undefined" ? localStorage.getItem("ghostly-test-hold-ttl") : null;
|
|
@@ -34774,7 +35577,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34774
35577
|
await this.messagesChanged(chat, [id]);
|
|
34775
35578
|
this.emitState();
|
|
34776
35579
|
},
|
|
34777
|
-
notify: (chat, key) => this.feedback("reaction", `${chat}:${key}`, chat)
|
|
35580
|
+
notify: (chat, key) => this.feedback("reaction", `${chat}:${key}`, chat),
|
|
35581
|
+
believed: (chat, by) => {
|
|
35582
|
+
const membership = this.membership(chat.slice(6));
|
|
35583
|
+
return !!membership?.members.has(by) && by !== membership.me;
|
|
35584
|
+
}
|
|
34778
35585
|
});
|
|
34779
35586
|
/**
|
|
34780
35587
|
* The chat list's reaction once one to its message was taken back: the same one if it still stands, else the latest
|
|
@@ -34807,10 +35614,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34807
35614
|
adminTurn: (_groupId, options) => this.confirmTurn({ within: options?.wait === false ? 0 : GhostlyNode.TURN_CONFIRM_WAIT_MS }),
|
|
34808
35615
|
staysOnline: () => this.options.staysOnline ?? (this.options.platform === "desktop" && typeof RTCPeerConnection !== "undefined"),
|
|
34809
35616
|
peerRoom: (groupId) => {
|
|
34810
|
-
|
|
35617
|
+
const native = Object.keys(this.nativeFactories);
|
|
35618
|
+
if (typeof RTCPeerConnection === "undefined" && native.length) {
|
|
34811
35619
|
let held = 0;
|
|
34812
35620
|
for (const live of this.links.values()) if (live.stored.group && live.stored.group !== groupId && live.link) held++;
|
|
34813
|
-
return Math.max(0,
|
|
35621
|
+
return Math.max(0, Math.max(...native.map((key) => this.groupNativeSlots(key))) - held);
|
|
34814
35622
|
}
|
|
34815
35623
|
const budget = this.options.peerBudget;
|
|
34816
35624
|
if (budget === void 0) return void 0;
|
|
@@ -34859,6 +35667,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34859
35667
|
edgeNick: (linkId) => this.links.get(linkId)?.presence.nick || void 0,
|
|
34860
35668
|
storeMessage: (message) => this.storeNewMessage(message),
|
|
34861
35669
|
completeMessage: (message) => this.completeGroupMessage(message),
|
|
35670
|
+
forgetFrame: async (linkId, id) => {
|
|
35671
|
+
await db.patchMessage(linkId, id, (stored) => stored.frame ? { frame: void 0 } : null, true);
|
|
35672
|
+
},
|
|
34862
35673
|
membersChanged: (groupId) => this.groupMembersChanged(groupId),
|
|
34863
35674
|
historyGone: (groupId) => this.groupHistoryGone(groupId),
|
|
34864
35675
|
emit: () => this.emitState(),
|
|
@@ -34921,7 +35732,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34921
35732
|
admin: !!this.groups.views().find((g) => g.id === groupId)?.isAdmin
|
|
34922
35733
|
};
|
|
34923
35734
|
},
|
|
34924
|
-
send: (groupId, edit, to) => this.groups.sendEdit(groupId, edit, to)
|
|
35735
|
+
send: (groupId, edit, to) => this.groups.sendEdit(groupId, edit, to),
|
|
35736
|
+
edge: (groupId, member) => {
|
|
35737
|
+
const link = this.links.get(this.memberEdges(groupId).get(member) ?? "")?.link;
|
|
35738
|
+
return link?.supportsGroupVersion(1) ? link : void 0;
|
|
35739
|
+
}
|
|
34925
35740
|
});
|
|
34926
35741
|
/** A community member's edit, signed by them: of their own message, and only while they are a member. */
|
|
34927
35742
|
async receiveCommunityEdit(groupId, sender, frame) {
|
|
@@ -35142,12 +35957,14 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35142
35957
|
};
|
|
35143
35958
|
}
|
|
35144
35959
|
const wallets = this.withHomes(walletInstances(networks));
|
|
35960
|
+
const intents = (await intentRepository.list()).map((saved) => saved.review);
|
|
35961
|
+
this.noteOpenReviews(intents);
|
|
35145
35962
|
this.walletView = {
|
|
35146
35963
|
...networks.mainnet,
|
|
35147
35964
|
networks,
|
|
35148
35965
|
wallets,
|
|
35149
35966
|
offers: this.walletOffers(networks, wallets),
|
|
35150
|
-
intents
|
|
35967
|
+
intents,
|
|
35151
35968
|
setup: this.walletSetup.view()
|
|
35152
35969
|
};
|
|
35153
35970
|
this.announcePaymentNetworks(wallets);
|
|
@@ -35216,9 +36033,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35216
36033
|
async pollPaymentStatus() {
|
|
35217
36034
|
if (this.shuttingDown) return;
|
|
35218
36035
|
try {
|
|
35219
|
-
|
|
36036
|
+
if (!this.openReviews) this.noteOpenReviews((await intentRepository.list()).map((saved) => saved.review));
|
|
36037
|
+
const open = this.openReviews;
|
|
36038
|
+
for (const id of [...open]) {
|
|
36039
|
+
const review = (await intentRepository.get(id))?.review;
|
|
36040
|
+
if (!review || !["submitted", "unknown"].includes(review.state)) {
|
|
36041
|
+
open.delete(id);
|
|
36042
|
+
continue;
|
|
36043
|
+
}
|
|
35220
36044
|
const network = walletNetworkOf(review.network);
|
|
35221
|
-
if (
|
|
36045
|
+
if (review.method === "arkade" && !this.arkWallets[network].adapter || review.method === "bark" && !this.barkWallets[network].adapter || review.method === "spark" && !this.sparkWallets[network].adapter || review.method === "usdt" && !this.usdtWallets[network].adapter || review.method === "bitcoin" && !this.bitcoins[network].sources.active || review.method === "fedimint" && !this.fedimintWallets[network].federation(review.provider)) continue;
|
|
35222
36046
|
await this.reconcilePayment({ id: review.id }).catch(() => {});
|
|
35223
36047
|
}
|
|
35224
36048
|
} finally {
|
|
@@ -35271,6 +36095,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35271
36095
|
if (!stored.group) this.fileDesk.restore(stored.id, storedFiles);
|
|
35272
36096
|
const files = linkFilesFrom(stored.id, storedFiles, messages);
|
|
35273
36097
|
this.links.set(stored.id, newLiveLink(stored, messages[messages.length - 1]?.timestamp ?? 0, files));
|
|
36098
|
+
if (!stored.group) this.links.get(stored.id).waitingSends = Promise.resolve(new Set(messages.filter(waitsForLive).map((m) => m.id)));
|
|
35274
36099
|
history.set(stored.id, messages);
|
|
35275
36100
|
for (const message of messages.slice(-20)) this.clockSample(message);
|
|
35276
36101
|
const note = latestReaction(messages);
|
|
@@ -35724,10 +36549,12 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35724
36549
|
this.publicActivity.clear();
|
|
35725
36550
|
for (const queue of this.editQueues.values()) queue.stop();
|
|
35726
36551
|
this.groupEdits.stop();
|
|
36552
|
+
this.reactions.stop();
|
|
35727
36553
|
this.cardEdits.stop();
|
|
35728
36554
|
await Promise.allSettled([
|
|
35729
36555
|
linksStopped,
|
|
35730
36556
|
links?.stop(),
|
|
36557
|
+
this.groups.flush(),
|
|
35731
36558
|
...WALLET_NETWORKS.flatMap((network) => [
|
|
35732
36559
|
this.arkWallets,
|
|
35733
36560
|
this.barkWallets,
|
|
@@ -35805,8 +36632,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35805
36632
|
appCheckUpdates() {
|
|
35806
36633
|
return this.appStore.checkUpdates();
|
|
35807
36634
|
}
|
|
35808
|
-
|
|
35809
|
-
|
|
36635
|
+
/** Uninstalls an app. One running then is closed in every chat (the contact hears `close`), and the pages are told, to stop its frames. */
|
|
36636
|
+
async appUninstall(params) {
|
|
36637
|
+
await this.appStore.uninstall(params);
|
|
36638
|
+
this.appsCloseOf(params.ref);
|
|
36639
|
+
this.events.onAppsChecked?.();
|
|
35810
36640
|
}
|
|
35811
36641
|
appRunCheck(params) {
|
|
35812
36642
|
return this.appStore.runCheck(params);
|
|
@@ -35924,14 +36754,23 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35924
36754
|
edgeView(groupId, member) {
|
|
35925
36755
|
for (const live of this.links.values()) if (live.stored.group === groupId && live.stored.groupPeer === member && !live.stored.groupEntry) return edgeView(live, Date.now(), this.groupNativeWaiting.has(live.stored.id));
|
|
35926
36756
|
}
|
|
35927
|
-
|
|
35928
|
-
|
|
36757
|
+
/** A chat's or a group's history, as a page gets it (`withoutFrame`). */
|
|
36758
|
+
async getMessages(linkId) {
|
|
36759
|
+
return (await db.getMessages(linkId)).map(withoutFrame);
|
|
35929
36760
|
}
|
|
35930
36761
|
/** One message of a chat, read by its id: the whole history costs a read of every row. */
|
|
35931
|
-
getMessage(linkId, id) {
|
|
35932
|
-
|
|
36762
|
+
async getMessage(linkId, id) {
|
|
36763
|
+
const message = await db.getMessage(linkId, id);
|
|
36764
|
+
return message && withoutFrame(message);
|
|
35933
36765
|
}
|
|
35934
|
-
async messagePage(
|
|
36766
|
+
async messagePage(asked) {
|
|
36767
|
+
const page = await this.storedPage(asked);
|
|
36768
|
+
return {
|
|
36769
|
+
...page,
|
|
36770
|
+
messages: page.messages.map(withoutFrame)
|
|
36771
|
+
};
|
|
36772
|
+
}
|
|
36773
|
+
async storedPage({ linkId, limit = 50, before }) {
|
|
35935
36774
|
if (!Number.isSafeInteger(limit) || limit < 1) throw new Error("limit: a whole number of messages, at least 1");
|
|
35936
36775
|
if (before === void 0) return db.getMessagePage(linkId, { limit });
|
|
35937
36776
|
if (typeof before === "number") {
|
|
@@ -37206,7 +38045,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37206
38045
|
async retryMessage({ linkId, messageId }) {
|
|
37207
38046
|
const live = this.links.get(linkId);
|
|
37208
38047
|
if (!live?.stored.profile) throw new Error("Retry is only available for paired chat messages");
|
|
37209
|
-
const message =
|
|
38048
|
+
const message = await db.getMessage(linkId, messageId);
|
|
37210
38049
|
if (message?.via === "hold") {
|
|
37211
38050
|
if (await this.hold.retry(linkId, messageId)) return;
|
|
37212
38051
|
if (!this.holdingFor(live)) throw new Error("Held messages need S3 storage (Profile → Backups) and a contact that allows them.");
|
|
@@ -37256,11 +38095,18 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37256
38095
|
outbox = new Outbox({
|
|
37257
38096
|
read: () => db.getMessages(linkId),
|
|
37258
38097
|
update: async (id, delivery, error, extra) => {
|
|
37259
|
-
await db.
|
|
37260
|
-
|
|
37261
|
-
|
|
37262
|
-
|
|
37263
|
-
|
|
38098
|
+
if (delivery === "delivered") await db.patchMessage(linkId, id, (message) => message.delivery === "delivered" ? null : {
|
|
38099
|
+
...message.delivery && {
|
|
38100
|
+
...extra,
|
|
38101
|
+
delivery,
|
|
38102
|
+
deliveryError: error
|
|
38103
|
+
},
|
|
38104
|
+
details: {
|
|
38105
|
+
...message.details,
|
|
38106
|
+
receiptAt: Date.now()
|
|
38107
|
+
}
|
|
38108
|
+
}, true);
|
|
38109
|
+
else await db.updateDelivery(linkId, id, delivery, error, extra);
|
|
37264
38110
|
if (delivery === "sent" || delivery === "delivered") {
|
|
37265
38111
|
const message = await db.getMessage(linkId, id);
|
|
37266
38112
|
if (message) this.messageFeedback("sent", message);
|
|
@@ -37326,7 +38172,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37326
38172
|
*/
|
|
37327
38173
|
async messageDetails({ linkId, messageId }) {
|
|
37328
38174
|
if (typeof linkId !== "string" || typeof messageId !== "string") throw new Error("Chat not found");
|
|
37329
|
-
const message =
|
|
38175
|
+
const message = await db.getMessage(linkId, messageId);
|
|
37330
38176
|
if (!message) return null;
|
|
37331
38177
|
const live = this.links.get(linkId), stored = live?.stored;
|
|
37332
38178
|
const group = linkId.startsWith("group:") ? (await db.getGroups()).find((g) => g.id === linkId.slice(6)) : void 0;
|
|
@@ -37351,7 +38197,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37351
38197
|
}
|
|
37352
38198
|
/** Adds to a message's details record, whatever the row's delivery state. Nothing is pushed for it: the view asks. */
|
|
37353
38199
|
noteDetails(linkId, id, change) {
|
|
37354
|
-
return db.patchMessage(linkId, id, (message) => ({ details: change(message.details ?? {}, message) })).then(() => {}, () => {});
|
|
38200
|
+
return db.patchMessage(linkId, id, (message) => ({ details: change(message.details ?? {}, message) }), true).then(() => {}, () => {});
|
|
37355
38201
|
}
|
|
37356
38202
|
/** One send of a text on its details: the path, the frame and its size, and the DHT envelope when that was the way. */
|
|
37357
38203
|
noteTextSend(linkId, message, snapshot, at, error, link) {
|
|
@@ -37533,12 +38379,22 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37533
38379
|
}
|
|
37534
38380
|
}
|
|
37535
38381
|
/**
|
|
38382
|
+
* The message of mine that carries a payment or a request, read by its id: `me_<the time the desk keeps for it>`.
|
|
38383
|
+
* The chat's history is read only when that row does not carry it, so it costs the same in a long chat as in a short one.
|
|
38384
|
+
*/
|
|
38385
|
+
async paymentMessage(linkId, paymentId) {
|
|
38386
|
+
const at = this.desk.payment(paymentId)?.createdAt;
|
|
38387
|
+
const message = at === void 0 ? void 0 : await db.getMessage(linkId, `me_${at}`);
|
|
38388
|
+
if (message?.sender === "me" && message.paymentId === paymentId) return message;
|
|
38389
|
+
return (await db.getMessages(linkId)).find((m) => m.sender === "me" && m.paymentId === paymentId);
|
|
38390
|
+
}
|
|
38391
|
+
/**
|
|
37536
38392
|
* The contact's app answered for a payment of mine (`pay-res`): its message gets two ticks, as a text gets from its
|
|
37537
38393
|
* receipt. A payment goes with no delivery state of its own, so its mark read one tick for good. 1:1 chats only: a
|
|
37538
38394
|
* group's payments live in the group's history, under other ids.
|
|
37539
38395
|
*/
|
|
37540
38396
|
async notePaymentReceipt(linkId, paymentId) {
|
|
37541
|
-
const message =
|
|
38397
|
+
const message = await this.paymentMessage(linkId, paymentId);
|
|
37542
38398
|
if (!message || message.delivery === "delivered") return;
|
|
37543
38399
|
if (await db.patchMessage(linkId, message.id, (m) => m.delivery === "delivered" ? null : {
|
|
37544
38400
|
delivery: "delivered",
|
|
@@ -37756,9 +38612,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37756
38612
|
}
|
|
37757
38613
|
fileAction({ linkId, fileId, action }) {
|
|
37758
38614
|
if (typeof linkId === "string" && linkId.startsWith("group:")) {
|
|
37759
|
-
if (action !== "accept" && action !== "request") throw new Error("A group's file is downloaded (accept) or asked for again (request)");
|
|
37760
38615
|
if (!fileId.startsWith(`group-${linkId.slice(6)}-`)) throw new Error("No such file in this group");
|
|
37761
|
-
return this.groups.downloadFile(fileId);
|
|
38616
|
+
if (action === "accept" || action === "request" || action === "resume") return this.groups.downloadFile(fileId);
|
|
38617
|
+
if (action === "cancel" || action === "decline" || action === "pause") return this.groups.haltFile(fileId, action === "pause");
|
|
38618
|
+
throw new Error("A group's file is not sent again from here");
|
|
37762
38619
|
}
|
|
37763
38620
|
const live = this.links.get(linkId);
|
|
37764
38621
|
if (!live) throw new Error("No such chat");
|
|
@@ -37847,7 +38704,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37847
38704
|
const error = await link.announceFile(offer, message.timestamp, GhostlyNode.wireReply(message), message.forwarded);
|
|
37848
38705
|
const said = !error && link.dhtPendingId === dhtFileId(offer.wireId);
|
|
37849
38706
|
if (!said && error !== "This file's name is too long to say over the DHT. It is sent when you are live.") return;
|
|
37850
|
-
await db.patchMessage(linkId, message.id, (current) => current.delivery !== "waiting" || current.fileOnFloor ? null : {
|
|
38707
|
+
const noted = await db.patchMessage(linkId, message.id, (current) => current.delivery !== "waiting" || current.fileOnFloor ? null : {
|
|
37851
38708
|
fileOnFloor: said ? "sent" : "unfit",
|
|
37852
38709
|
...said && { details: withSend(current.details, {
|
|
37853
38710
|
at,
|
|
@@ -37855,17 +38712,32 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37855
38712
|
result: "sent"
|
|
37856
38713
|
}) }
|
|
37857
38714
|
});
|
|
38715
|
+
if (said && noted) await live.floorFiles?.then((known) => known.set(dhtFileId(offer.wireId), message.id), () => {});
|
|
37858
38716
|
await this.messagesChanged(linkId, [message.id]);
|
|
37859
38717
|
}
|
|
37860
38718
|
/** A receipt on the DHT floor that names a waiting file's offer (`dhtFileId`): its bubble is in place on the contact's side. */
|
|
37861
38719
|
async fileSeenOnFloor(linkId, id) {
|
|
37862
|
-
const
|
|
37863
|
-
|
|
37864
|
-
|
|
37865
|
-
if (
|
|
38720
|
+
const live = this.links.get(linkId);
|
|
38721
|
+
if (!live) return;
|
|
38722
|
+
const said = await this.floorFilesOf(live), messageId = said.get(id);
|
|
38723
|
+
if (!messageId) return;
|
|
38724
|
+
said.delete(id);
|
|
38725
|
+
if (await db.patchMessage(linkId, messageId, (current) => current.fileOnFloor !== "sent" ? null : {
|
|
37866
38726
|
fileOnFloor: "seen",
|
|
37867
38727
|
...current.delivery === "waiting" && { deliveryError: "Your contact sees it. The file goes when you are live." }
|
|
37868
|
-
})) await this.messagesChanged(linkId, [
|
|
38728
|
+
})) await this.messagesChanged(linkId, [messageId]);
|
|
38729
|
+
}
|
|
38730
|
+
/**
|
|
38731
|
+
* The files of mine said on the floor and not seen there yet, their message by their floor id (`dhtFileId`). Read from
|
|
38732
|
+
* the chat's history once in a run, then kept as files are said (`sayFileOnFloor`): a receipt that names none of them
|
|
38733
|
+
* costs the same in a long chat as in a short one.
|
|
38734
|
+
*/
|
|
38735
|
+
floorFilesOf(live) {
|
|
38736
|
+
const linkId = live.stored.id, prefix = `${linkId}-out-`;
|
|
38737
|
+
return live.floorFiles ??= db.getMessages(linkId).then((history) => new Map(history.filter((m) => m.sender === "me" && m.file?.id.startsWith(prefix) && m.fileOnFloor === "sent").map((m) => [dhtFileId(m.file.id.slice(prefix.length)), m.id])), (error) => {
|
|
38738
|
+
live.floorFiles = void 0;
|
|
38739
|
+
throw error;
|
|
38740
|
+
});
|
|
37869
38741
|
}
|
|
37870
38742
|
/**
|
|
37871
38743
|
* A file the contact said on the DHT floor (WISP 403 § Files): its bubble takes its place now, waiting for its bytes,
|
|
@@ -37963,40 +38835,59 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37963
38835
|
const live = this.links.get(linkId);
|
|
37964
38836
|
if (!live?.link?.isDataLinkOpen) return;
|
|
37965
38837
|
const withTexts = this.outboxFor(linkId).sendsFiles;
|
|
37966
|
-
const
|
|
38838
|
+
const known = await this.waitingSendsOf(live), waiting = [];
|
|
38839
|
+
for (const id of [...known]) {
|
|
38840
|
+
const message = await db.getMessage(linkId, id);
|
|
38841
|
+
if (!message || !waitsForLive(message)) known.delete(id);
|
|
38842
|
+
else if (message.file && !withTexts || message.paymentId) waiting.push(message);
|
|
38843
|
+
}
|
|
38844
|
+
waiting.sort((a, b) => a.timestamp - b.timestamp);
|
|
37967
38845
|
if (waiting.some((m) => m.file) && !GhostlyNode.takesFiles(live.link)) await GhostlyNode.largeFilesAgreed(live.link);
|
|
37968
|
-
for (const message of waiting)
|
|
37969
|
-
if (
|
|
37970
|
-
|
|
37971
|
-
|
|
37972
|
-
|
|
37973
|
-
const file = message.file, wireId = file.id.slice(`${linkId}-out-`.length);
|
|
37974
|
-
await db.putMessage(sentNow(message));
|
|
37975
|
-
this.transferFile(live, file, wireId, message.timestamp, (error) => {
|
|
37976
|
-
this.transfers.set(file.id, {
|
|
37977
|
-
state: "failed",
|
|
37978
|
-
transferred: 0,
|
|
37979
|
-
size: file.size,
|
|
37980
|
-
error
|
|
37981
|
-
});
|
|
37982
|
-
this.emitState();
|
|
37983
|
-
}, GhostlyNode.wireReply(message));
|
|
37984
|
-
} else if (live.link.supportsPayments) {
|
|
37985
|
-
const at = Date.now();
|
|
37986
|
-
await db.putMessage({
|
|
37987
|
-
...sentNow(message),
|
|
37988
|
-
details: {
|
|
37989
|
-
...withSend(message.details, {
|
|
37990
|
-
at,
|
|
37991
|
-
...pathSnapshot(live, "datalink"),
|
|
37992
|
-
result: "sent"
|
|
37993
|
-
}),
|
|
37994
|
-
sentAt: at
|
|
38846
|
+
for (const message of waiting) {
|
|
38847
|
+
if (message.file) {
|
|
38848
|
+
if (!GhostlyNode.takesFiles(live.link)) {
|
|
38849
|
+
await db.updateDelivery(linkId, message.id, "failed", "Your contact's app cannot receive files.");
|
|
38850
|
+
continue;
|
|
37995
38851
|
}
|
|
37996
|
-
|
|
37997
|
-
|
|
38852
|
+
const file = message.file, wireId = file.id.slice(`${linkId}-out-`.length);
|
|
38853
|
+
await db.putMessage(sentNow(message));
|
|
38854
|
+
this.transferFile(live, file, wireId, message.timestamp, (error) => {
|
|
38855
|
+
this.transfers.set(file.id, {
|
|
38856
|
+
state: "failed",
|
|
38857
|
+
transferred: 0,
|
|
38858
|
+
size: file.size,
|
|
38859
|
+
error
|
|
38860
|
+
});
|
|
38861
|
+
this.emitState();
|
|
38862
|
+
}, GhostlyNode.wireReply(message));
|
|
38863
|
+
} else if (live.link.supportsPayments) {
|
|
38864
|
+
const at = Date.now();
|
|
38865
|
+
await db.putMessage({
|
|
38866
|
+
...sentNow(message),
|
|
38867
|
+
details: {
|
|
38868
|
+
...withSend(message.details, {
|
|
38869
|
+
at,
|
|
38870
|
+
...pathSnapshot(live, "datalink"),
|
|
38871
|
+
result: "sent"
|
|
38872
|
+
}),
|
|
38873
|
+
sentAt: at
|
|
38874
|
+
}
|
|
38875
|
+
});
|
|
38876
|
+
} else await db.updateDelivery(linkId, message.id, "failed", "Your contact's app cannot receive payment requests.");
|
|
38877
|
+
known.delete(message.id);
|
|
38878
|
+
}
|
|
37998
38879
|
if (waiting.length) await this.messagesChanged(linkId, waiting.map((m) => m.id));
|
|
37999
38880
|
}
|
|
38881
|
+
/**
|
|
38882
|
+
* The ids of my messages that wait for the live link (`waitsForLive`). A chat loaded at the start has them from that
|
|
38883
|
+
* read; any other reads its history once in a run. A session that opens then reads those rows, however long the chat.
|
|
38884
|
+
*/
|
|
38885
|
+
waitingSendsOf(live) {
|
|
38886
|
+
return live.waitingSends ??= db.getMessages(live.stored.id).then((history) => new Set(history.filter(waitsForLive).map((m) => m.id)), (error) => {
|
|
38887
|
+
live.waitingSends = void 0;
|
|
38888
|
+
throw error;
|
|
38889
|
+
});
|
|
38890
|
+
}
|
|
38000
38891
|
receiveFile(linkId, wire) {
|
|
38001
38892
|
const files = this.links.get(linkId)?.files;
|
|
38002
38893
|
if (!files) return "refused";
|
|
@@ -38444,8 +39335,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38444
39335
|
};
|
|
38445
39336
|
return this.groups.fileCheck(groupId);
|
|
38446
39337
|
}
|
|
38447
|
-
groupMessages({ groupId }) {
|
|
38448
|
-
return this.groups.messages(groupId);
|
|
39338
|
+
async groupMessages({ groupId }) {
|
|
39339
|
+
return (await this.groups.messages(groupId)).map(withoutFrame);
|
|
38449
39340
|
}
|
|
38450
39341
|
/**
|
|
38451
39342
|
* Presses a button of a message someone else sent to a chat or a group (`group:<id>`), WISP 406 · Message Buttons:
|
|
@@ -38865,10 +39756,13 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38865
39756
|
* This side opened an app in a paired chat, or updated it: `open` goes to the contact now when the session is live,
|
|
38866
39757
|
* and again on every session that comes back while it stays open.
|
|
38867
39758
|
*/
|
|
38868
|
-
async appOpen({ linkId, ref, version }) {
|
|
39759
|
+
async appOpen({ linkId, ref, version, digest }) {
|
|
38869
39760
|
const { live, app } = this.appChat(linkId, ref);
|
|
38870
39761
|
if (!isAppVersion(version)) throw new Error("Not an app version");
|
|
38871
|
-
await this.appStore.chatRunnable({
|
|
39762
|
+
await this.appStore.chatRunnable({
|
|
39763
|
+
ref,
|
|
39764
|
+
digest
|
|
39765
|
+
});
|
|
38872
39766
|
const open = live.appsOpen ??= /* @__PURE__ */ new Map();
|
|
38873
39767
|
if (live.link) live.link.openApp(app, version);
|
|
38874
39768
|
else open.set(app, version);
|
|
@@ -38881,10 +39775,13 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38881
39775
|
else live.appsOpen?.delete(app);
|
|
38882
39776
|
}
|
|
38883
39777
|
/** One data frame of an app open on both sides, on the live session only; nothing is kept to send later. */
|
|
38884
|
-
async appSend({ linkId, ref, data }) {
|
|
39778
|
+
async appSend({ linkId, ref, data, digest }) {
|
|
38885
39779
|
const { live, app } = this.appChat(linkId, ref);
|
|
38886
39780
|
if (!live.appsOpen?.has(app)) return { error: "not-open" };
|
|
38887
|
-
await this.appStore.chatRunnable({
|
|
39781
|
+
await this.appStore.chatRunnable({
|
|
39782
|
+
ref,
|
|
39783
|
+
digest
|
|
39784
|
+
});
|
|
38888
39785
|
return { error: live.link ? live.link.sendAppData(app, data) : "offline" };
|
|
38889
39786
|
}
|
|
38890
39787
|
/** Every page went away: no app runs here any more, so each open one is closed (`close` goes where it can). */
|
|
@@ -38892,6 +39789,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38892
39789
|
for (const live of this.links.values()) for (const app of [...live.appsOpen?.keys() ?? []]) if (live.link) live.link.closeApp(app);
|
|
38893
39790
|
else live.appsOpen?.delete(app);
|
|
38894
39791
|
}
|
|
39792
|
+
/** An app that went (uninstalled) is closed in every chat it was open in, as `appClose` does. */
|
|
39793
|
+
appsCloseOf(ref) {
|
|
39794
|
+
for (const [linkId, live] of this.links) {
|
|
39795
|
+
if (!live.appsOpen?.size) continue;
|
|
39796
|
+
const { app } = this.appChat(linkId, ref);
|
|
39797
|
+
if (!live.appsOpen.has(app)) continue;
|
|
39798
|
+
if (live.link) live.link.closeApp(app);
|
|
39799
|
+
else live.appsOpen.delete(app);
|
|
39800
|
+
}
|
|
39801
|
+
}
|
|
38895
39802
|
/** Keeps a pin of a 1:1 chat when it is newer than the chat's; `out`: mine, to say until the contact confirms it. */
|
|
38896
39803
|
async keepLinkPin(linkId, pin, out) {
|
|
38897
39804
|
const live = this.links.get(linkId);
|
|
@@ -38954,6 +39861,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38954
39861
|
const view = this.groups.views().find((g) => g.id === groupId);
|
|
38955
39862
|
if (!pin || !view?.myKey || view.status !== "active" || !view.members.some((m) => m.key === member)) return;
|
|
38956
39863
|
if (!mayPin(view.profile, member, view.members.find((m) => m.role === "admin")?.key)) return;
|
|
39864
|
+
if (!pinIsNewer(this.groups.pinOf(groupId), pin)) return;
|
|
38957
39865
|
const kept = await this.pinCame(`group:${groupId}`, member === view.myKey ? "me" : member, pin);
|
|
38958
39866
|
if (!await this.groups.setPin(groupId, {
|
|
38959
39867
|
...kept,
|
|
@@ -39241,7 +40149,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
39241
40149
|
async closeRequest(paymentId, reason) {
|
|
39242
40150
|
const request = this.desk.payment(paymentId);
|
|
39243
40151
|
if (!request || !await this.desk.close(paymentId, reason, "your contact removed the wallet it was paid to")) return;
|
|
39244
|
-
const message =
|
|
40152
|
+
const message = await this.paymentMessage(request.linkId, paymentId);
|
|
39245
40153
|
if (message) await this.hold.forget(request.linkId, message.id).catch(() => {});
|
|
39246
40154
|
}
|
|
39247
40155
|
/**
|
|
@@ -40080,6 +40988,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40080
40988
|
if (settings[key] !== false) delete this.settings[key];
|
|
40081
40989
|
else this.settings[key] = false;
|
|
40082
40990
|
await db.putSettings(this.settings);
|
|
40991
|
+
if (key === "autoDownloads" && settings[key] === false) await this.groups.autoDownloadsOff();
|
|
40083
40992
|
}
|
|
40084
40993
|
if (settings.publicProfiles !== void 0) {
|
|
40085
40994
|
if (settings.publicProfiles !== false) delete this.settings.publicProfiles;
|
|
@@ -40198,8 +41107,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40198
41107
|
if (this.options.singleDevice) return;
|
|
40199
41108
|
const record = await readDeviceRecord(databaseName()).catch(() => null);
|
|
40200
41109
|
if (!record || record.state !== "active") return;
|
|
40201
|
-
const verifier = await this.handoffVerifier()
|
|
40202
|
-
if (verifier
|
|
41110
|
+
const verifier = await this.handoffVerifier();
|
|
41111
|
+
if (verifier ? JSON.stringify(verifier) !== JSON.stringify(record.verifier) : record.verifier) await amendDevice(databaseName(), { verifier: verifier ?? void 0 });
|
|
40203
41112
|
}
|
|
40204
41113
|
/**
|
|
40205
41114
|
* Copies the network settings into the device record (WISP 06, `devices/network.ts`), where device-link-only mode
|
|
@@ -40283,8 +41192,12 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40283
41192
|
this.emitState();
|
|
40284
41193
|
await recoverEnrollment(databaseName(), this.turnNetwork()).catch(() => false);
|
|
40285
41194
|
await this.syncDeviceVerifier().catch(() => {});
|
|
40286
|
-
const
|
|
40287
|
-
|
|
41195
|
+
const keeper = await this.deviceLinks.turnKeeper();
|
|
41196
|
+
const outcome = await keeper?.check(false);
|
|
41197
|
+
if (outcome?.kind === "go-on" && !outcome.restricted) {
|
|
41198
|
+
this.noteGoodTurn();
|
|
41199
|
+
await keeper?.stateVerifier().catch(() => null);
|
|
41200
|
+
}
|
|
40288
41201
|
if (outcome?.kind === "gated" && !this.shuttingDown) {
|
|
40289
41202
|
const record = await readDeviceRecord(databaseName());
|
|
40290
41203
|
if (record) await this.stopReplaced(viewOf(record));
|
|
@@ -40320,7 +41233,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40320
41233
|
if (knownDeviceGate()?.state !== "active") return;
|
|
40321
41234
|
const outcome = await links.checkTurn(false).catch(() => null);
|
|
40322
41235
|
if (this.shuttingDown || this.gatedOut) return;
|
|
40323
|
-
if (outcome?.kind === "go-on" && !outcome.restricted)
|
|
41236
|
+
if (outcome?.kind === "go-on" && !outcome.restricted) {
|
|
41237
|
+
this.noteGoodTurn();
|
|
41238
|
+
await this.stateDeviceVerifier();
|
|
41239
|
+
}
|
|
40324
41240
|
if (outcome?.kind === "gated") {
|
|
40325
41241
|
const record = await readDeviceRecord(databaseName()).catch(() => null);
|
|
40326
41242
|
if (record) await this.stopReplaced(viewOf(record));
|
|
@@ -40634,16 +41550,45 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40634
41550
|
if (this.options.singleDevice) return;
|
|
40635
41551
|
if (typeof password !== "string" || !password) throw new Error("A password is needed");
|
|
40636
41552
|
const stored = await this.handoffVerifier();
|
|
40637
|
-
|
|
41553
|
+
const active = knownDeviceGate()?.state === "active";
|
|
41554
|
+
const ready = await handoffPakeReady().then(() => true, () => false);
|
|
41555
|
+
if (stored && !ready) {
|
|
41556
|
+
if (password !== current) await this.dropHandoffVerifier();
|
|
41557
|
+
throw new Error("The password proof could not be loaded");
|
|
41558
|
+
}
|
|
41559
|
+
if (stored && active && !await provesHandoffPassword(stored, typeof current === "string" ? current : "")) throw new Error("handoff-password: The current password is wrong.");
|
|
41560
|
+
if (stored && await provesHandoffPassword(stored, password)) {
|
|
41561
|
+
await this.syncDeviceVerifier().catch(() => {});
|
|
41562
|
+
await this.stateDeviceVerifier();
|
|
41563
|
+
return;
|
|
41564
|
+
}
|
|
40638
41565
|
try {
|
|
40639
41566
|
const verifier = await makeHandoffVerifier(password);
|
|
40640
41567
|
await wrap((await store(STORES$1.settings, "readwrite")).put(verifier, GhostlyNode.HANDOFF_VERIFIER));
|
|
40641
41568
|
await this.syncDeviceVerifier().catch(() => {});
|
|
41569
|
+
await this.stateDeviceVerifier();
|
|
40642
41570
|
} catch (error) {
|
|
40643
|
-
await
|
|
41571
|
+
await this.dropHandoffVerifier();
|
|
40644
41572
|
throw error;
|
|
40645
41573
|
}
|
|
40646
41574
|
}
|
|
41575
|
+
/** The profile has no verifier any more: its settings, the device state, and what the turn record states. */
|
|
41576
|
+
async dropHandoffVerifier() {
|
|
41577
|
+
await wrap((await store(STORES$1.settings, "readwrite")).delete(GhostlyNode.HANDOFF_VERIFIER)).catch(() => {});
|
|
41578
|
+
await this.syncDeviceVerifier().catch(() => {});
|
|
41579
|
+
await this.stateDeviceVerifier();
|
|
41580
|
+
}
|
|
41581
|
+
/**
|
|
41582
|
+
* The turn record states which verifier the profile has now (WISP 06 § Record, the verifier mark), so a standby that
|
|
41583
|
+
* keeps an older one does not take over with the password it checks. Asked after the device state took the
|
|
41584
|
+
* verifier: at start, at each change, and after a good read of the turn (a change made offline is stated then).
|
|
41585
|
+
*/
|
|
41586
|
+
async stateDeviceVerifier() {
|
|
41587
|
+
if (this.options.singleDevice || this.shuttingDown || this.gatedOut) return;
|
|
41588
|
+
try {
|
|
41589
|
+
await (await this.deviceLinks?.turnKeeper())?.stateVerifier();
|
|
41590
|
+
} catch {}
|
|
41591
|
+
}
|
|
40647
41592
|
/**
|
|
40648
41593
|
* The wallets in a handoff to `taker` (WISP 06 § Wallets, `planHandoffWallets`): what moves, what stays home, and
|
|
40649
41594
|
* what keeps the profile here now. `view`: the wallets as they are now, read fresh by default.
|
|
@@ -40748,10 +41693,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40748
41693
|
}
|
|
40749
41694
|
await writeWalletHomes(plan.wallets);
|
|
40750
41695
|
const breezDatabases = [...new Set(plan.wallets.flatMap((w) => w.route === "moves" && w.breez && breez.has(w.id) ? [breez.get(w.id)] : []))];
|
|
40751
|
-
const verifier = await this.handoffVerifier().
|
|
41696
|
+
const verifier = await this.handoffVerifier().then((value) => ({ verifier: value ?? void 0 }), () => ({}));
|
|
40752
41697
|
await moveDevice(profile, "releasing", {
|
|
40753
41698
|
...patch,
|
|
40754
|
-
...verifier
|
|
41699
|
+
...verifier,
|
|
40755
41700
|
breezDatabases: breezDatabases.length ? breezDatabases : void 0
|
|
40756
41701
|
});
|
|
40757
41702
|
this.events.onDeviceGate?.({
|
|
@@ -41207,6 +42152,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41207
42152
|
this.directPath.note(peer, evidence);
|
|
41208
42153
|
if (!entry && evidence !== "open" && evidence !== "closed") this.edgeWithoutRtc(linkId);
|
|
41209
42154
|
},
|
|
42155
|
+
onAnswersRefused: () => {
|
|
42156
|
+
if (!entry) this.edgeWithoutRtc(linkId);
|
|
42157
|
+
},
|
|
41210
42158
|
onDataLinkState: (state) => {
|
|
41211
42159
|
traceJoin(group, `link.${state}`, { role });
|
|
41212
42160
|
if (state === "open" || live.dataLink === "open") live.lastSyncAt = Date.now();
|
|
@@ -41377,6 +42325,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41377
42325
|
role: stored.inviteCode ? "inviter" : "joiner",
|
|
41378
42326
|
startedAt: stored.createdAt
|
|
41379
42327
|
} : void 0,
|
|
42328
|
+
madeAt: stored.createdAt,
|
|
41380
42329
|
transport: this.transport,
|
|
41381
42330
|
nick: this.sharedNick,
|
|
41382
42331
|
avatar: this.sharedAvatar,
|
|
@@ -41627,10 +42576,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41627
42576
|
this.notePaymentReceipt(linkId, result.id).catch(() => {});
|
|
41628
42577
|
},
|
|
41629
42578
|
onFileStored: async (wire) => {
|
|
41630
|
-
|
|
41631
|
-
|
|
41632
|
-
if (
|
|
41633
|
-
return
|
|
42579
|
+
if (!stored.group && !this.links.get(linkId)?.files.wireIds.has(wire.id)) return void 0;
|
|
42580
|
+
const held = (await fileStore.listForLink(linkId)).find((file) => file.direction === "in" && file.wireId === wire.id);
|
|
42581
|
+
if (!held?.digest || !held.metadata) return void 0;
|
|
42582
|
+
if (held.metadata.name !== wire.name || held.metadata.size !== wire.size || held.metadata.mime !== wire.mime || held.metadata.timestamp !== wire.timestamp) return void 0;
|
|
42583
|
+
return held.digest;
|
|
41634
42584
|
},
|
|
41635
42585
|
onFileIncoming: (file) => this.receiveFile(linkId, file),
|
|
41636
42586
|
onFileProgress: (id, transferred, direction) => this.fileProgress(this.localFileId(linkId, id, direction), transferred),
|
|
@@ -41769,7 +42719,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41769
42719
|
get nativeFactories() {
|
|
41770
42720
|
const relay = this.relaysHyperdht ? this.hyperdhtRelay : "";
|
|
41771
42721
|
return {
|
|
41772
|
-
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-
|
|
42722
|
+
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-hKC9DpIH.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
|
|
41773
42723
|
...this.options.nativeTransports,
|
|
41774
42724
|
...this.ownIrohRelays ? { "iroh/1": (seedB64) => this.options.nativeTransports["iroh/1"](seedB64, this.settings.irohRelays?.length ? { relays: [...this.settings.irohRelays] } : {}) } : {},
|
|
41775
42725
|
...this.options.irohWeb ? { "iroh/1": (seedB64) => createIrohWebEndpoint(seedB64, { relays: this.irohRelays }) } : {}
|
|
@@ -41884,6 +42834,14 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41884
42834
|
this.emitState();
|
|
41885
42835
|
});
|
|
41886
42836
|
}
|
|
42837
|
+
/** The listeners this app may hold for a transport: sixteen for Iroh's browser build (`IROH_WEB_SLOTS`), eight for the others. */
|
|
42838
|
+
nativeSlots(key) {
|
|
42839
|
+
return key === "iroh/1" && this.options.irohWeb ? IROH_WEB_SLOTS : NATIVE_SLOTS;
|
|
42840
|
+
}
|
|
42841
|
+
/** Of those, what its groups' links may hold, all groups together. */
|
|
42842
|
+
groupNativeSlots(key) {
|
|
42843
|
+
return this.nativeSlots(key) - CHAT_NATIVE_SLOTS;
|
|
42844
|
+
}
|
|
41887
42845
|
/**
|
|
41888
42846
|
* One transport's listener for one chat, in that transport's queue. "no-slot" when a group's link found no slot free,
|
|
41889
42847
|
* "failed" when the listener could not start.
|
|
@@ -41897,11 +42855,13 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41897
42855
|
live.transportErrors ??= {};
|
|
41898
42856
|
try {
|
|
41899
42857
|
const owners = [...this.links.values()].filter((other) => other.link?.availableTransports.includes(key));
|
|
41900
|
-
|
|
41901
|
-
if (owners.length >=
|
|
42858
|
+
const slots = this.nativeSlots(key), groups = owners.filter((other) => other.stored.group).length;
|
|
42859
|
+
if (group && (owners.length >= slots || groups >= this.groupNativeSlots(key))) return "no-slot";
|
|
42860
|
+
const full = owners.length >= slots;
|
|
42861
|
+
if (full || !group && owners.length - groups >= NATIVE_SLOTS) {
|
|
41902
42862
|
const now = Date.now();
|
|
41903
42863
|
const taking = !group && (this.activeLinkId === linkId || inUse);
|
|
41904
|
-
const victim = owners.filter((other) => other.stored.group && other.link?.canReleaseEndpoint(key)).sort((a, b) => (a.link?.isDataLinkOpen ? 1 : 0) - (b.link?.isDataLinkOpen ? 1 : 0))[0] ?? (taking ? owners.filter((other) => other !== live && other.stored.id !== this.activeLinkId && other.link?.canReleaseEndpoint(key) && now - (other.nativeTakenAt?.[key] ?? 0) >= 12e4).sort((a, b) => a.lastMessageAt - b.lastMessageAt)[0] : void 0);
|
|
42864
|
+
const victim = (!full ? void 0 : owners.filter((other) => other.stored.group && other.link?.canReleaseEndpoint(key)).sort((a, b) => (a.link?.isDataLinkOpen ? 1 : 0) - (b.link?.isDataLinkOpen ? 1 : 0))[0]) ?? (taking ? owners.filter((other) => other !== live && other.stored.id !== this.activeLinkId && other.link?.canReleaseEndpoint(key) && now - (other.nativeTakenAt?.[key] ?? 0) >= 12e4).sort((a, b) => a.lastMessageAt - b.lastMessageAt)[0] : void 0);
|
|
41905
42865
|
const idle = victim || !taking || !this.reachedOnlyNatively(live) ? void 0 : owners.filter((other) => other !== live && !other.stored.group && other.stored.id !== this.activeLinkId && !this.fileDesk.moving(other.stored.id) && other.link?.canYieldEndpoint(key, 12e4)).sort((a, b) => a.link.lastActivityAt - b.link.lastActivityAt)[0];
|
|
41906
42866
|
const idleFor = idle ? now - idle.link.lastActivityAt : 0;
|
|
41907
42867
|
if (idle && await idle.link.yieldEndpoint(key, 12e4)) {
|
|
@@ -41922,7 +42882,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41922
42882
|
active: this.activeLinkId === linkId
|
|
41923
42883
|
});
|
|
41924
42884
|
if (this.activeLinkId === linkId && !group) this.retryActiveSlot(linkId);
|
|
41925
|
-
throw new Error("All
|
|
42885
|
+
throw new Error("All native connection slots are in use. This chat takes one once a chat live over one has been quiet for 2 minutes. Disconnect a native connection in another chat to free one now.");
|
|
41926
42886
|
} else {
|
|
41927
42887
|
await victim.link.releaseEndpoint(key);
|
|
41928
42888
|
victim.transportErrors ??= {};
|
|
@@ -42072,7 +43032,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42072
43032
|
...whole.file && {
|
|
42073
43033
|
file: whole.file,
|
|
42074
43034
|
text: whole.text
|
|
42075
|
-
}
|
|
43035
|
+
},
|
|
43036
|
+
...whole.frame && { frame: whole.frame }
|
|
42076
43037
|
};
|
|
42077
43038
|
if (!Object.keys(added).length) return;
|
|
42078
43039
|
if (!await db.patchMessage(message.linkId, message.id, (stored) => {
|
|
@@ -42098,12 +43059,12 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42098
43059
|
}
|
|
42099
43060
|
}
|
|
42100
43061
|
const onChanges = this.events.onMessageChanges;
|
|
42101
|
-
if (!onChanges) return this.events.onMessages(linkId, await
|
|
43062
|
+
if (!onChanges) return this.events.onMessages(linkId, await this.getMessages(linkId));
|
|
42102
43063
|
const unique = [...new Set(ids)];
|
|
42103
43064
|
if (!unique.length) return;
|
|
42104
43065
|
const rows = await Promise.all(unique.map((id) => db.getMessage(linkId, id)));
|
|
42105
43066
|
onChanges.call(this.events, linkId, {
|
|
42106
|
-
messages: rows.filter((row) => !!row),
|
|
43067
|
+
messages: rows.filter((row) => !!row).map(withoutFrame),
|
|
42107
43068
|
deleted: unique.filter((_, i) => !rows[i])
|
|
42108
43069
|
});
|
|
42109
43070
|
}
|
|
@@ -42198,6 +43159,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42198
43159
|
};
|
|
42199
43160
|
}
|
|
42200
43161
|
if (!await db.addMessage(message)) return false;
|
|
43162
|
+
if (live && waitsForLive(message)) live.waitingSends?.then((known) => known.add(message.id), () => {});
|
|
42201
43163
|
this.clockSample(message);
|
|
42202
43164
|
const newest = this.newestAt.get(message.linkId);
|
|
42203
43165
|
if (!message.event) this.newestAt.set(message.linkId, Math.max(newest ?? 0, writtenAt(message, this.clockLeads.get(message.linkId))));
|
|
@@ -42574,4 +43536,4 @@ function signalKind(signal) {
|
|
|
42574
43536
|
//#endregion
|
|
42575
43537
|
export { EngineServer, hyperdhtRelayProblem as n, server_exports as t };
|
|
42576
43538
|
|
|
42577
|
-
//# sourceMappingURL=server-
|
|
43539
|
+
//# sourceMappingURL=server-DHMXMwNn.js.map
|