@ghostlytools/cli 1.2.2 → 1.2.4
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 +2 -1
- package/dist/assets/{arkade-_gS9wShF.js → arkade-CdnKGDbg.js} +2 -2
- package/dist/assets/{arkade-_gS9wShF.js.map → arkade-CdnKGDbg.js.map} +1 -1
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
- package/dist/assets/{hyperdhtRelay-CgYLaQz3.js → hyperdhtRelay-C1p0nkle.js} +3 -3
- package/dist/assets/{hyperdhtRelay-CgYLaQz3.js.map → hyperdhtRelay-C1p0nkle.js.map} +1 -1
- package/dist/assets/{mainline-1v8OTYvp.js → mainline-BH6s8HVi.js} +5 -2
- package/dist/assets/{mainline-1v8OTYvp.js.map → mainline-BH6s8HVi.js.map} +1 -1
- package/dist/assets/{server-Cy4ox7oD.js → server-0zfNpABQ.js} +1055 -185
- package/dist/assets/server-0zfNpABQ.js.map +1 -0
- package/dist/assets/{src-XzcHzneo.js → src-CbLkHfz8.js} +454 -80
- package/dist/assets/src-CbLkHfz8.js.map +1 -0
- package/dist/assets/{usdt-Deo3g0Nq.js → usdt-Qf4UuygS.js} +2 -2
- package/dist/assets/{usdt-Deo3g0Nq.js.map → usdt-Qf4UuygS.js.map} +1 -1
- package/dist/ghostly.mjs +362 -114
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/assets/server-Cy4ox7oD.js.map +0 -1
- package/dist/assets/src-XzcHzneo.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
|
2
|
-
import { $ as
|
|
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-CbLkHfz8.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-Qf4UuygS.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,7 @@ 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-CdnKGDbg.js");
|
|
646
646
|
/** How long a Testnet wallet about to make way for another waits for its server to say what it holds. */
|
|
647
647
|
var RETIRE_READ_MS = 1e4;
|
|
648
648
|
/** Every new profile starts with this wallet: nothing to set up before receiving. */
|
|
@@ -11159,7 +11159,13 @@ var APP_STORAGE_LIMITS = {
|
|
|
11159
11159
|
keyBytes: 256,
|
|
11160
11160
|
/** A value, as compact JSON in UTF-8 bytes. */
|
|
11161
11161
|
valueBytes: 65536,
|
|
11162
|
-
/**
|
|
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. */
|
|
11163
11169
|
scopeBytes: 5242880
|
|
11164
11170
|
};
|
|
11165
11171
|
var APP_CHECK_TIMINGS = {
|
|
@@ -11186,6 +11192,8 @@ var STAGED = {
|
|
|
11186
11192
|
/** The first bytes of a bundle: magic, the manifest's length and the largest manifest. */
|
|
11187
11193
|
var PEEK_BYTES = APP_BUNDLE_MAGIC.length + 4 + APP_BUNDLE_LIMITS.manifestBytes;
|
|
11188
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;
|
|
11189
11197
|
/**
|
|
11190
11198
|
* A listing's URLs this client reads, in order, a jsDelivr copy first: one this client reads names a full commit, so it
|
|
11191
11199
|
* holds exactly the version the store reviewed, where a raw `HEAD` may have moved on to one it has not (WISP 1200 §
|
|
@@ -11257,7 +11265,7 @@ function peekAppManifest(bytes) {
|
|
|
11257
11265
|
var Apps = class {
|
|
11258
11266
|
host;
|
|
11259
11267
|
staged = /* @__PURE__ */ new Map();
|
|
11260
|
-
/** Bundles read back and checked, by digest: the
|
|
11268
|
+
/** Bundles read back and checked, by digest, least recently used first: the ones running are read once, not on every `file`. */
|
|
11261
11269
|
verified = /* @__PURE__ */ new Map();
|
|
11262
11270
|
/** Reads of a bundle under way, by digest: icons drawn at once share one read. */
|
|
11263
11271
|
verifying = /* @__PURE__ */ new Map();
|
|
@@ -11591,7 +11599,11 @@ var Apps = class {
|
|
|
11591
11599
|
/** The installed bundle read back and checked again before it runs (WISP 1200 § Signatures: "again before it runs one"). */
|
|
11592
11600
|
verifiedBundle(app) {
|
|
11593
11601
|
const kept = this.verified.get(app.digest);
|
|
11594
|
-
if (kept)
|
|
11602
|
+
if (kept) {
|
|
11603
|
+
this.verified.delete(app.digest);
|
|
11604
|
+
this.verified.set(app.digest, kept);
|
|
11605
|
+
return Promise.resolve(kept);
|
|
11606
|
+
}
|
|
11595
11607
|
const under = this.verifying.get(app.digest);
|
|
11596
11608
|
if (under) return under;
|
|
11597
11609
|
const reading = (async () => {
|
|
@@ -11599,8 +11611,9 @@ var Apps = class {
|
|
|
11599
11611
|
if (!bytes) fail("needs-files", "This app's files are not on this device yet");
|
|
11600
11612
|
const read = readAppBundle(bytes);
|
|
11601
11613
|
if (!read.ok || read.bundle.digest !== app.digest) fail("damaged", "This app's stored files do not match what was installed");
|
|
11602
|
-
if (this.verified.size >= 2) this.verified.delete(this.verified.keys().next().value);
|
|
11603
11614
|
this.verified.set(app.digest, read.bundle);
|
|
11615
|
+
const room = Math.max(2, this.started.size + (this.started.has(app.ref) ? 0 : 1));
|
|
11616
|
+
while (this.verified.size > room) this.verified.delete(this.verified.keys().next().value);
|
|
11604
11617
|
this.icons.set(app.digest, read.bundle.files.get("icon.png")?.slice() ?? null);
|
|
11605
11618
|
return read.bundle;
|
|
11606
11619
|
})().finally(() => {
|
|
@@ -12132,13 +12145,14 @@ var Apps = class {
|
|
|
12132
12145
|
maxBytes: PEEK_BYTES,
|
|
12133
12146
|
peek: true
|
|
12134
12147
|
}));
|
|
12135
|
-
if (peek && newer(peek)) candidates.set(url, {});
|
|
12148
|
+
if (peek && newer(peek)) candidates.set(url, { holds: peek });
|
|
12136
12149
|
} catch {}
|
|
12137
12150
|
}
|
|
12138
12151
|
const revokeRead = /* @__PURE__ */ new Set();
|
|
12139
12152
|
const before = await this.readRevocations(app.ref, [app.from, ...app.manifest.sources ?? []], revokeRead);
|
|
12140
12153
|
app = withRevocations(app, before ?? []);
|
|
12141
12154
|
for (const [url, want] of candidates) {
|
|
12155
|
+
let holds = want.holds;
|
|
12142
12156
|
if (want.peek) {
|
|
12143
12157
|
let peek;
|
|
12144
12158
|
try {
|
|
@@ -12153,7 +12167,10 @@ var Apps = class {
|
|
|
12153
12167
|
if (want.miss) this.listingMisses.set(...want.miss);
|
|
12154
12168
|
continue;
|
|
12155
12169
|
}
|
|
12170
|
+
holds = peek;
|
|
12156
12171
|
}
|
|
12172
|
+
const twin = holds && (holds.sequence === app.sequence || holds.sequence === app.pending?.sequence);
|
|
12173
|
+
if (holds && !twin && this.runStatus(holds, this.storeViews(stores), app.revocations).status !== "ok") continue;
|
|
12157
12174
|
let fetched;
|
|
12158
12175
|
try {
|
|
12159
12176
|
fetched = await this.fetchBundle(url);
|
|
@@ -12339,7 +12356,10 @@ var Apps = class {
|
|
|
12339
12356
|
entry: utf8Decode(entry)
|
|
12340
12357
|
};
|
|
12341
12358
|
}
|
|
12342
|
-
/**
|
|
12359
|
+
/**
|
|
12360
|
+
* A file of the installed bundle, for the broker's `file` (`ghostly.file(path)`): a copy, which the broker transfers to
|
|
12361
|
+
* the app as it is. `digest`: the version the frame runs.
|
|
12362
|
+
*/
|
|
12343
12363
|
async file({ ref, path, digest }) {
|
|
12344
12364
|
const app = path === "icon.png" ? await this.installedOrFail(ref) : await this.runningOrFail(ref, digest);
|
|
12345
12365
|
if (path !== "icon.png" && digest !== void 0 && digest !== app.digest) fail("no-file", "No such file in this app");
|
|
@@ -12435,7 +12455,7 @@ var Apps = class {
|
|
|
12435
12455
|
const id = `${ref}\0${scope}`;
|
|
12436
12456
|
let used = this.usage.get(id);
|
|
12437
12457
|
if (used === void 0) {
|
|
12438
|
-
used = (await wrap((await store(STORES$1.appStorage, "readonly")).index(APP_STORAGE_SCOPE_INDEX).getAll([ref, scope]))).reduce((sum, r) => sum + r
|
|
12458
|
+
used = (await wrap((await store(STORES$1.appStorage, "readonly")).index(APP_STORAGE_SCOPE_INDEX).getAll([ref, scope]))).reduce((sum, r) => sum + costOf(r), 0);
|
|
12439
12459
|
this.usage.set(id, used);
|
|
12440
12460
|
}
|
|
12441
12461
|
return used;
|
|
@@ -12449,7 +12469,11 @@ var Apps = class {
|
|
|
12449
12469
|
]));
|
|
12450
12470
|
return row ? { value: JSON.parse(row.value) } : null;
|
|
12451
12471
|
}
|
|
12452
|
-
/**
|
|
12472
|
+
/**
|
|
12473
|
+
* Keeps a JSON value under a key: at most 64 KiB, and 5 MiB for every key of this app in this scope, each counted
|
|
12474
|
+
* with `rowBytes`. A scope already past that (filled before a key counted `rowBytes`) still takes a write that does
|
|
12475
|
+
* not grow it, so the app can make room.
|
|
12476
|
+
*/
|
|
12453
12477
|
async storageSet({ ref, scope, key, value, digest }) {
|
|
12454
12478
|
const app = await this.runningOrFail(ref, digest);
|
|
12455
12479
|
const where = this.scopeOf(scope), name = this.keyOf(key);
|
|
@@ -12466,12 +12490,13 @@ var Apps = class {
|
|
|
12466
12490
|
await this.serial(`${app.ref}\0${where}`, async () => {
|
|
12467
12491
|
const used = await this.usageOf(app.ref, where);
|
|
12468
12492
|
const rows = await store(STORES$1.appStorage, "readonly");
|
|
12469
|
-
const
|
|
12493
|
+
const old = await wrap(rows.get([
|
|
12470
12494
|
app.ref,
|
|
12471
12495
|
where,
|
|
12472
12496
|
name
|
|
12473
|
-
]))
|
|
12474
|
-
|
|
12497
|
+
]));
|
|
12498
|
+
const next = used - (old ? costOf(old) : 0) + APP_STORAGE_LIMITS.rowBytes + size;
|
|
12499
|
+
if (next > APP_STORAGE_LIMITS.scopeBytes && next > used) fail("full", "This app's storage in this chat is full (5 MiB)");
|
|
12475
12500
|
const writing = await store(STORES$1.appStorage, "readwrite");
|
|
12476
12501
|
if (where !== "alone" && !this.host.isChat(where)) {
|
|
12477
12502
|
this.usage.delete(`${app.ref}\0${where}`);
|
|
@@ -12503,7 +12528,7 @@ var Apps = class {
|
|
|
12503
12528
|
where,
|
|
12504
12529
|
name
|
|
12505
12530
|
]));
|
|
12506
|
-
this.usage.set(`${app.ref}\0${where}`, used - old
|
|
12531
|
+
this.usage.set(`${app.ref}\0${where}`, used - costOf(old));
|
|
12507
12532
|
});
|
|
12508
12533
|
}
|
|
12509
12534
|
async storageKeys({ ref, scope, digest }) {
|
|
@@ -14058,6 +14083,23 @@ function takeoverTurn(record, read) {
|
|
|
14058
14083
|
if (highest + 1 > TURN_MAX) throw new TurnClosedError();
|
|
14059
14084
|
return highest + 1;
|
|
14060
14085
|
}
|
|
14086
|
+
/**
|
|
14087
|
+
* The `instance` of the record a device writes as the active one: it states the mark of the verifier beside its device
|
|
14088
|
+
* state (`turnInstance`, WISP 06 § Record), which on an active device is the profile's. `unstated`: a handoff's taker,
|
|
14089
|
+
* whose device state does not hold the verifier of the profile it just installed; it states it once its engine runs.
|
|
14090
|
+
*/
|
|
14091
|
+
function instanceFor(record, unstated = false) {
|
|
14092
|
+
return turnInstance(unstated ? null : turnVerifierMark(record.verifier ?? null));
|
|
14093
|
+
}
|
|
14094
|
+
/**
|
|
14095
|
+
* The last record of the device set this device knows after a read: the one the sources returned when it is above
|
|
14096
|
+
* the packet it stored, else the stored one. A lower record (`behind`) and a tombstone say nothing here.
|
|
14097
|
+
*/
|
|
14098
|
+
function latestKnown(read, stored) {
|
|
14099
|
+
const news = (read.result === "other" || read.result === "clone" || read.result === "mine") && read.record && !read.record.tombstone ? read.record : null;
|
|
14100
|
+
if (!news) return stored;
|
|
14101
|
+
return !stored || news.sequence >= stored.sequence ? news : stored;
|
|
14102
|
+
}
|
|
14061
14103
|
/** The record's slots as the device state keeps them. */
|
|
14062
14104
|
function deviceSetOf(record) {
|
|
14063
14105
|
return record.slots.map((slot) => slot && {
|
|
@@ -14253,7 +14295,7 @@ var TurnKeeper = class {
|
|
|
14253
14295
|
author: ownSlot,
|
|
14254
14296
|
active: ownSlot,
|
|
14255
14297
|
slots,
|
|
14256
|
-
instance:
|
|
14298
|
+
instance: this.options.instance?.() ?? instanceFor(record, !!options.release),
|
|
14257
14299
|
...release ? { release } : {}
|
|
14258
14300
|
};
|
|
14259
14301
|
const payload = await signTurnPacket(keys, fields, this.options.signer);
|
|
@@ -14274,6 +14316,31 @@ var TurnKeeper = class {
|
|
|
14274
14316
|
return this.put(held, payload, conditions, raising);
|
|
14275
14317
|
}
|
|
14276
14318
|
/**
|
|
14319
|
+
* The active device says which verifier the profile has now (WISP 06 § Record, the verifier mark): when the record it
|
|
14320
|
+
* stored states another one (the lock password changed, or the record was written before the verifier was at hand),
|
|
14321
|
+
* it reads, and on `mine` or `none` writes its next record. True when the stored record states it (already, or now
|
|
14322
|
+
* written); false when nothing was written (not the active device, no good read, another device's record there: the
|
|
14323
|
+
* next check or start settles that, and every start writes a record anyway). Null for a `single` profile.
|
|
14324
|
+
*/
|
|
14325
|
+
stateVerifier() {
|
|
14326
|
+
return this.exclusive(async () => {
|
|
14327
|
+
const held = await this.held();
|
|
14328
|
+
if (!held) return null;
|
|
14329
|
+
if (held.record.state !== "active" || !held.wrote || !held.storedRecord) return false;
|
|
14330
|
+
const stated = turnStatedVerifier(held.storedRecord.instance);
|
|
14331
|
+
if (stated && bytesEqual(stated, turnVerifierMark(held.record.verifier ?? null))) return true;
|
|
14332
|
+
const read = await this.readWith(held);
|
|
14333
|
+
if (!read.good || read.result !== "mine" && read.result !== "none") return false;
|
|
14334
|
+
try {
|
|
14335
|
+
await this.writeWith(held, conditionsAfter(read, read.result === "none" ? "none" : "seen"));
|
|
14336
|
+
} catch (error) {
|
|
14337
|
+
if (error instanceof TurnClosedError) return false;
|
|
14338
|
+
throw error;
|
|
14339
|
+
}
|
|
14340
|
+
return true;
|
|
14341
|
+
});
|
|
14342
|
+
}
|
|
14343
|
+
/**
|
|
14277
14344
|
* A forced takeover (WISP 06 § Settle): reads the turn, makes the device `taking` (with the state it had, to go
|
|
14278
14345
|
* back to), writes the record of `turn` with no release within `P` of that read, and settles as every device that
|
|
14279
14346
|
* raises the turn does. `start` says it is the active device now; `gated` that it lost and is what it was before.
|
|
@@ -14287,6 +14354,7 @@ var TurnKeeper = class {
|
|
|
14287
14354
|
if (prior !== "standby" && prior !== "superseded") throw new Error(`A ${prior} device does not force a takeover`);
|
|
14288
14355
|
const read = await this.readWith(held, true);
|
|
14289
14356
|
if (!read.good || read.result === "tombstone") return this.settle(held, read, true);
|
|
14357
|
+
options.before?.(latestKnown(read, held.storedRecord));
|
|
14290
14358
|
const turn = options.turn ?? takeoverTurn(held.record, read);
|
|
14291
14359
|
const stopping = held.record.activeSlot !== void 0 && held.record.activeSlot !== held.ownSlot ? held.record.deviceSet[held.record.activeSlot]?.key : void 0;
|
|
14292
14360
|
held.record = await this.store.move(this.options.profile, "taking", { handoff: {
|
|
@@ -14296,8 +14364,8 @@ var TurnKeeper = class {
|
|
|
14296
14364
|
} });
|
|
14297
14365
|
try {
|
|
14298
14366
|
await this.writeWith(held, read.conditions, {
|
|
14299
|
-
|
|
14300
|
-
|
|
14367
|
+
turn,
|
|
14368
|
+
...options.slots ? { slots: options.slots } : {}
|
|
14301
14369
|
}, true);
|
|
14302
14370
|
} catch (error) {
|
|
14303
14371
|
if (!held.wroteNow) held.record = await this.store.move(this.options.profile, prior, {
|
|
@@ -15066,7 +15134,7 @@ var EnrollInviter = class extends Enrollment {
|
|
|
15066
15134
|
rev: 0,
|
|
15067
15135
|
author: 0,
|
|
15068
15136
|
active: 0,
|
|
15069
|
-
instance:
|
|
15137
|
+
instance: turnInstance(),
|
|
15070
15138
|
slots: granted.slots.map((slot) => slot && {
|
|
15071
15139
|
key: slot.key,
|
|
15072
15140
|
name: slot.name
|
|
@@ -15844,7 +15912,7 @@ async function planSetMove(record, signer, options = {}) {
|
|
|
15844
15912
|
author: record.ownSlot,
|
|
15845
15913
|
active: record.ownSlot,
|
|
15846
15914
|
slots: turnSlots,
|
|
15847
|
-
instance:
|
|
15915
|
+
instance: turnInstance(turnVerifierMark(record.verifier ?? null), random)
|
|
15848
15916
|
}, sign);
|
|
15849
15917
|
const update = {
|
|
15850
15918
|
d,
|
|
@@ -16067,6 +16135,21 @@ function takeoverTarget(record) {
|
|
|
16067
16135
|
function canTakeOver(record) {
|
|
16068
16136
|
return !!record && (record.state === "standby" || record.state === "superseded") && !!record.copy;
|
|
16069
16137
|
}
|
|
16138
|
+
/**
|
|
16139
|
+
* Whether the verifier this standby keeps is still the profile's, by the last record of the device set it knows (the
|
|
16140
|
+
* one just read, or the one it stored): false only when that record states the mark of another verifier. A record that
|
|
16141
|
+
* states none (an app from before wrote it) leaves the device with what it has, and so does this device's own record.
|
|
16142
|
+
* Only a standby is held to it: a device replaced by a takeover it did not want takes the turn back with the password
|
|
16143
|
+
* it knows, whatever the device that replaced it states.
|
|
16144
|
+
*/
|
|
16145
|
+
function verifierStands(record, latest) {
|
|
16146
|
+
if (record.state !== "standby" || !record.verifier || !latest || latest.tombstone) return true;
|
|
16147
|
+
const own = record.ownSlot === void 0 ? void 0 : record.deviceSet[record.ownSlot]?.key;
|
|
16148
|
+
const author = latest.slots[latest.author];
|
|
16149
|
+
if (own && author && bytesEqual(author.key, fromBase64Url(own))) return true;
|
|
16150
|
+
const stated = turnStatedVerifier(latest.instance);
|
|
16151
|
+
return !stated || bytesEqual(stated, turnVerifierMark(record.verifier));
|
|
16152
|
+
}
|
|
16070
16153
|
var sameName = (a, b) => a.normalize("NFC").trim().toLocaleLowerCase() === b.normalize("NFC").trim().toLocaleLowerCase();
|
|
16071
16154
|
/**
|
|
16072
16155
|
* Checks the request and forces the takeover. Resolves to the keeper's outcome: `start` when this device is the active
|
|
@@ -16093,7 +16176,9 @@ async function forceTakeover(ports, request) {
|
|
|
16093
16176
|
if (record.takeoverAttempts && (record.takeoverAttempts.total > 0 || record.takeoverAttempts.recent.length)) await ports.amend({ takeoverAttempts: handoffAttemptSucceeded() });
|
|
16094
16177
|
const keeper = await ports.keeper();
|
|
16095
16178
|
if (!keeper) throw new TakeoverRefusal("offline", "Go online to take over.");
|
|
16096
|
-
const outcome = await keeper.raise()
|
|
16179
|
+
const outcome = await keeper.raise({ before: (latest) => {
|
|
16180
|
+
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.`);
|
|
16181
|
+
} });
|
|
16097
16182
|
if (!outcome) throw new TakeoverRefusal("state", "This profile has no device set.");
|
|
16098
16183
|
return outcome;
|
|
16099
16184
|
}
|
|
@@ -16182,7 +16267,7 @@ async function planOwnSet(record, ports, under) {
|
|
|
16182
16267
|
author: record.ownSlot,
|
|
16183
16268
|
active: record.ownSlot,
|
|
16184
16269
|
slots,
|
|
16185
|
-
instance:
|
|
16270
|
+
instance: turnInstance(turnVerifierMark(record.verifier ?? null), random)
|
|
16186
16271
|
}, ports.signer);
|
|
16187
16272
|
return {
|
|
16188
16273
|
d: toBase64Url(d),
|
|
@@ -16420,6 +16505,10 @@ function isHandoffVerifier(value) {
|
|
|
16420
16505
|
const v = value;
|
|
16421
16506
|
return !!v && typeof v === "object" && v.v === 1 && typeof v.setup === "string" && B64URL.test(v.setup) && typeof v.record === "string" && B64URL.test(v.record);
|
|
16422
16507
|
}
|
|
16508
|
+
/** Loads the password proof. Throws when it cannot be loaded: nothing can then be made or checked (`provesHandoffPassword` says false). */
|
|
16509
|
+
async function handoffPakeReady() {
|
|
16510
|
+
await opaque();
|
|
16511
|
+
}
|
|
16423
16512
|
/** A verifier for this password: both halves of the registration, run on this device. */
|
|
16424
16513
|
async function makeHandoffVerifier(password) {
|
|
16425
16514
|
if (!password) throw new Error("A password is needed");
|
|
@@ -19985,6 +20074,14 @@ function forgetWallet(records, id) {
|
|
|
19985
20074
|
return rest;
|
|
19986
20075
|
}
|
|
19987
20076
|
//#endregion
|
|
20077
|
+
//#region ../browser/src/shared/messageChanges.ts
|
|
20078
|
+
/** A row as a page gets it: without the frame a private group's message came in (`StoredMessage.frame`), which only the engine reads. */
|
|
20079
|
+
function withoutFrame(message) {
|
|
20080
|
+
if (message.frame === void 0) return message;
|
|
20081
|
+
const { frame: _frame, ...row } = message;
|
|
20082
|
+
return row;
|
|
20083
|
+
}
|
|
20084
|
+
//#endregion
|
|
19988
20085
|
//#region ../browser/src/shared/buttons.ts
|
|
19989
20086
|
/** The buttons of a message, when it has any. */
|
|
19990
20087
|
function messageButtons(message) {
|
|
@@ -21241,6 +21338,8 @@ function newestMessages(index, query, count) {
|
|
|
21241
21338
|
};
|
|
21242
21339
|
});
|
|
21243
21340
|
}
|
|
21341
|
+
/** 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). */
|
|
21342
|
+
var RELAXED = { durability: "relaxed" };
|
|
21244
21343
|
/**
|
|
21245
21344
|
* Durable local state of the browser peer. Identity seeds live here the same
|
|
21246
21345
|
* way Desktop keeps them in its WebView storage: local to this profile, never
|
|
@@ -21473,12 +21572,13 @@ var db = {
|
|
|
21473
21572
|
await wrap((await store(STORES$1.messages, "readwrite")).put(message));
|
|
21474
21573
|
},
|
|
21475
21574
|
async updateDelivery(linkId, id, delivery, deliveryError, extra) {
|
|
21476
|
-
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite");
|
|
21575
|
+
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite", delivery === "sent" || delivery === "delivered" ? RELAXED : void 0);
|
|
21477
21576
|
const messages = tx.objectStore(STORES$1.messages);
|
|
21478
21577
|
const request = messages.get([linkId, id]);
|
|
21479
21578
|
request.onsuccess = () => {
|
|
21480
21579
|
const message = request.result;
|
|
21481
21580
|
if (!message?.delivery || message.delivery === "delivered") return;
|
|
21581
|
+
if (message.delivery === delivery && message.deliveryError === deliveryError && Object.entries(extra ?? {}).every(([key, value]) => message[key] === value)) return;
|
|
21482
21582
|
messages.put({
|
|
21483
21583
|
...message,
|
|
21484
21584
|
...extra,
|
|
@@ -21494,10 +21594,11 @@ var db = {
|
|
|
21494
21594
|
/**
|
|
21495
21595
|
* Changes one message row in place, in one transaction: `change` sees the row as stored and gives the fields to
|
|
21496
21596
|
* set (`null`: leave it). A row that is not there is not made. Used for what the delivery states do not carry
|
|
21497
|
-
* (the message's details record), on rows in any state.
|
|
21597
|
+
* (the message's details record), on rows in any state. `relaxed`: for what can be made again or is only a note
|
|
21598
|
+
* (a receipt's mark, the details record), written with no disk sync of its own.
|
|
21498
21599
|
*/
|
|
21499
|
-
async patchMessage(linkId, id, change) {
|
|
21500
|
-
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite");
|
|
21600
|
+
async patchMessage(linkId, id, change, relaxed = false) {
|
|
21601
|
+
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite", relaxed ? RELAXED : void 0);
|
|
21501
21602
|
const messages = tx.objectStore(STORES$1.messages);
|
|
21502
21603
|
const request = messages.get([linkId, id]);
|
|
21503
21604
|
let result;
|
|
@@ -21643,6 +21744,10 @@ var Reactions = class {
|
|
|
21643
21744
|
queue = Promise.resolve();
|
|
21644
21745
|
/** A group member's reactions taken per window (chat and member key), as a paired chat's session limits them. */
|
|
21645
21746
|
memberPace = /* @__PURE__ */ new Map();
|
|
21747
|
+
/** Per group chat, per member: its reactions past the window, by message, waiting for room (`hold`). */
|
|
21748
|
+
held = /* @__PURE__ */ new Map();
|
|
21749
|
+
heldTimers = /* @__PURE__ */ new Map();
|
|
21750
|
+
stopped = false;
|
|
21646
21751
|
/**
|
|
21647
21752
|
* This side's highest number per chat: read from its history the first time I react in it, then each number given
|
|
21648
21753
|
* out. It only has to grow, so a row deleted since leaves it too high and nothing is wrong.
|
|
@@ -21691,14 +21796,19 @@ var Reactions = class {
|
|
|
21691
21796
|
/**
|
|
21692
21797
|
* A reaction that came from `by` (`peer`, or a member's key), already authenticated as theirs. A message not here
|
|
21693
21798
|
* yet keeps it for five minutes; past the room for that sender, it is dropped (and, on a 1:1 session, not confirmed).
|
|
21799
|
+
* Past `REACTION_LIMITS.receive` a window from one group member, the rest wait for the next window (`hold`): a
|
|
21800
|
+
* community's catch-up hands back every reaction of a member at once, and one dropped is never handed again.
|
|
21694
21801
|
*/
|
|
21695
21802
|
receive(chat, by, reaction) {
|
|
21696
21803
|
if (isGroup$1(chat)) {
|
|
21697
21804
|
const key = `${chat}\n${by}`;
|
|
21698
21805
|
let pace = this.memberPace.get(key);
|
|
21699
21806
|
if (!pace) this.memberPace.set(key, pace = new ReactionWindow(REACTION_LIMITS.receive, REACTION_LIMITS.windowMs, () => this.now()));
|
|
21700
|
-
if (!pace.take()) return Promise.resolve(
|
|
21807
|
+
if (this.held.get(chat)?.has(by) || !pace.take()) return Promise.resolve(this.hold(chat, by, reaction, pace));
|
|
21701
21808
|
}
|
|
21809
|
+
return this.take(chat, by, reaction);
|
|
21810
|
+
}
|
|
21811
|
+
take(chat, by, reaction) {
|
|
21702
21812
|
return this.run(async () => {
|
|
21703
21813
|
const target = await this.find(chat, reaction.id);
|
|
21704
21814
|
if (!target) return this.wait(chat, by, reaction);
|
|
@@ -21707,6 +21817,75 @@ var Reactions = class {
|
|
|
21707
21817
|
return "applied";
|
|
21708
21818
|
});
|
|
21709
21819
|
}
|
|
21820
|
+
/**
|
|
21821
|
+
* A reaction past what a group member's window takes: kept, the highest number per message, `REACTION_LIMITS.held` a
|
|
21822
|
+
* group at most (the oldest go first), and taken when the window has room. The work stays what the window allows.
|
|
21823
|
+
*/
|
|
21824
|
+
hold(chat, by, reaction, pace) {
|
|
21825
|
+
let group = this.held.get(chat);
|
|
21826
|
+
if (!group) this.held.set(chat, group = /* @__PURE__ */ new Map());
|
|
21827
|
+
let waiting = group.get(by);
|
|
21828
|
+
if (!waiting) group.set(by, waiting = /* @__PURE__ */ new Map());
|
|
21829
|
+
const known = waiting.get(reaction.id);
|
|
21830
|
+
if (known && known.n >= reaction.n) return "stale";
|
|
21831
|
+
waiting.delete(reaction.id);
|
|
21832
|
+
waiting.set(reaction.id, reaction);
|
|
21833
|
+
let count = 0;
|
|
21834
|
+
for (const member of group.values()) count += member.size;
|
|
21835
|
+
for (const [member, reactions] of group) {
|
|
21836
|
+
if (count <= REACTION_LIMITS.held) break;
|
|
21837
|
+
while (count > REACTION_LIMITS.held && reactions.size) {
|
|
21838
|
+
reactions.delete(reactions.keys().next().value);
|
|
21839
|
+
count--;
|
|
21840
|
+
}
|
|
21841
|
+
if (!reactions.size && member !== by) group.delete(member);
|
|
21842
|
+
}
|
|
21843
|
+
this.takeLater(chat, by, pace);
|
|
21844
|
+
return waiting.has(reaction.id) ? "waiting" : "dropped";
|
|
21845
|
+
}
|
|
21846
|
+
takeLater(chat, by, pace) {
|
|
21847
|
+
const key = `${chat}\n${by}`;
|
|
21848
|
+
if (this.heldTimers.has(key) || this.stopped) return;
|
|
21849
|
+
this.heldTimers.set(key, setTimeout(() => {
|
|
21850
|
+
this.takeHeld(chat, by).catch(() => {});
|
|
21851
|
+
}, Math.max(50, pace.wait())));
|
|
21852
|
+
}
|
|
21853
|
+
/** The window of a member has room again: its reactions that waited are taken, oldest first, as many as it allows. */
|
|
21854
|
+
async takeHeld(chat, by) {
|
|
21855
|
+
const key = `${chat}\n${by}`;
|
|
21856
|
+
this.heldTimers.delete(key);
|
|
21857
|
+
const group = this.held.get(chat), waiting = group?.get(by), pace = this.memberPace.get(key);
|
|
21858
|
+
if (this.stopped || !group || !waiting || !pace) return;
|
|
21859
|
+
for (const [id, reaction] of [...waiting]) {
|
|
21860
|
+
if (this.host.believed && !this.host.believed(chat, by)) {
|
|
21861
|
+
waiting.clear();
|
|
21862
|
+
break;
|
|
21863
|
+
}
|
|
21864
|
+
if (!pace.take()) break;
|
|
21865
|
+
if (waiting.get(id) !== reaction) continue;
|
|
21866
|
+
waiting.delete(id);
|
|
21867
|
+
await this.take(chat, by, reaction).catch(() => {});
|
|
21868
|
+
}
|
|
21869
|
+
if (waiting.size) {
|
|
21870
|
+
this.takeLater(chat, by, pace);
|
|
21871
|
+
return;
|
|
21872
|
+
}
|
|
21873
|
+
if (group.get(by) === waiting) group.delete(by);
|
|
21874
|
+
if (!group.size) this.held.delete(chat);
|
|
21875
|
+
}
|
|
21876
|
+
/** The engine stops: what waits for a window is let go, and no timer is left. */
|
|
21877
|
+
stop() {
|
|
21878
|
+
this.stopped = true;
|
|
21879
|
+
this.dropHeld();
|
|
21880
|
+
}
|
|
21881
|
+
dropHeld(chat) {
|
|
21882
|
+
for (const [key, timer] of this.heldTimers) if (chat === void 0 || key.startsWith(`${chat}\n`)) {
|
|
21883
|
+
clearTimeout(timer);
|
|
21884
|
+
this.heldTimers.delete(key);
|
|
21885
|
+
}
|
|
21886
|
+
if (chat === void 0) this.held.clear();
|
|
21887
|
+
else this.held.delete(chat);
|
|
21888
|
+
}
|
|
21710
21889
|
/** A message was kept: the reactions that waited for it are shown now. */
|
|
21711
21890
|
stored(message) {
|
|
21712
21891
|
const list = this.waiting.get(message.linkId);
|
|
@@ -21744,6 +21923,7 @@ var Reactions = class {
|
|
|
21744
21923
|
}
|
|
21745
21924
|
/** A chat is gone: nothing waits for it any more, and my highest number in it is read again if it comes back. */
|
|
21746
21925
|
forget(chat) {
|
|
21926
|
+
this.dropHeld(chat);
|
|
21747
21927
|
this.waiting.delete(chat);
|
|
21748
21928
|
this.last.delete(chat);
|
|
21749
21929
|
}
|
|
@@ -22115,6 +22295,16 @@ var Communities = class {
|
|
|
22115
22295
|
}
|
|
22116
22296
|
/** The clock: the last tick's time (the engine ticks every second), so every decision reads one clock. */
|
|
22117
22297
|
time = 0;
|
|
22298
|
+
/**
|
|
22299
|
+
* The communities this app is in, and those it is a hub of, as of the last tick: their idle looks share one
|
|
22300
|
+
* community's pace (`beaconEvery`, `knockRecordsToRead`). Each community a hub held cost it about 10 requests a minute
|
|
22301
|
+
* on each relay with nobody coming in: 4 of them on one relay, or 8 on two, spent the relays' budget on those looks,
|
|
22302
|
+
* and the edges of some of them stayed down 10 minutes and more (CLI daemons, 2026-10-13).
|
|
22303
|
+
*/
|
|
22304
|
+
sharing = {
|
|
22305
|
+
groups: 1,
|
|
22306
|
+
hubs: 1
|
|
22307
|
+
};
|
|
22118
22308
|
now() {
|
|
22119
22309
|
return this.time || Date.now();
|
|
22120
22310
|
}
|
|
@@ -22179,22 +22369,23 @@ var Communities = class {
|
|
|
22179
22369
|
invited: [],
|
|
22180
22370
|
memberLinks: {}
|
|
22181
22371
|
};
|
|
22182
|
-
|
|
22372
|
+
const invitation = this.knocking(group, live) ? {
|
|
22373
|
+
linkId: group.joining.linkId,
|
|
22374
|
+
contact: "",
|
|
22375
|
+
admin: group.joining.inviter,
|
|
22376
|
+
members: 0,
|
|
22377
|
+
accepted: true,
|
|
22378
|
+
viaLink: true,
|
|
22379
|
+
stage: this.joinStage(group),
|
|
22380
|
+
...this.knockingWith.get(group.id) ? { waiting: this.knockingWith.get(group.id) } : {}
|
|
22381
|
+
} : void 0;
|
|
22382
|
+
if (invitation && !this.keptWhileKnocking(live)) return [{
|
|
22183
22383
|
...base,
|
|
22184
22384
|
name: groupName(group.joining.name) ?? live?.session.name ?? "",
|
|
22185
22385
|
isAdmin: false,
|
|
22186
22386
|
members: [],
|
|
22187
22387
|
canSend: false,
|
|
22188
|
-
invitation
|
|
22189
|
-
linkId: group.joining.linkId,
|
|
22190
|
-
contact: "",
|
|
22191
|
-
admin: group.joining.inviter,
|
|
22192
|
-
members: 0,
|
|
22193
|
-
accepted: true,
|
|
22194
|
-
viaLink: true,
|
|
22195
|
-
stage: this.joinStage(group),
|
|
22196
|
-
...this.knockingWith.get(group.id) ? { waiting: this.knockingWith.get(group.id) } : {}
|
|
22197
|
-
}
|
|
22388
|
+
invitation
|
|
22198
22389
|
}];
|
|
22199
22390
|
if (!live) return [];
|
|
22200
22391
|
const s = live.session, edges = this.host.edges(group.id);
|
|
@@ -22219,6 +22410,7 @@ var Communities = class {
|
|
|
22219
22410
|
}) } : {},
|
|
22220
22411
|
canSend: s.canSend,
|
|
22221
22412
|
...s.picture ? { picture: s.picture } : {},
|
|
22413
|
+
...invitation ? { invitation } : {},
|
|
22222
22414
|
community: {
|
|
22223
22415
|
hub: live.hub,
|
|
22224
22416
|
hubs: hubs.length,
|
|
@@ -22286,6 +22478,13 @@ var Communities = class {
|
|
|
22286
22478
|
knocking(group, live) {
|
|
22287
22479
|
return !!group.joining && (!live || live.session.status !== "active");
|
|
22288
22480
|
}
|
|
22481
|
+
/**
|
|
22482
|
+
* Someone removed keeps the group as it was here while it knocks to get back in, and when it gives up (`decline`).
|
|
22483
|
+
* Not a session whose admission lost a race: that knock is still its join.
|
|
22484
|
+
*/
|
|
22485
|
+
keptWhileKnocking(live) {
|
|
22486
|
+
return live?.session.status === "removed";
|
|
22487
|
+
}
|
|
22289
22488
|
/** `check`: a member, as far as this device knows, asking the door whether it still is (`joinByLink`). */
|
|
22290
22489
|
async startJoining(link, seedB64, since, check = false) {
|
|
22291
22490
|
this.lastKnock.delete(link.g);
|
|
@@ -22457,11 +22656,14 @@ var Communities = class {
|
|
|
22457
22656
|
}
|
|
22458
22657
|
/**
|
|
22459
22658
|
* A join through the link that no member answered yet, given up: listed as an invitation, it is declined as one.
|
|
22460
|
-
*
|
|
22659
|
+
* Someone removed who knocked to get back in only stops knocking: the group and its history stay here, as a private
|
|
22660
|
+
* group's do for someone removed, invited again, who declines. A member trying to get back in (its session lost)
|
|
22661
|
+
* leaves instead, which keeps nothing.
|
|
22461
22662
|
*/
|
|
22462
22663
|
async decline(groupId) {
|
|
22463
22664
|
const group = this.stored.get(groupId), live = this.live.get(groupId);
|
|
22464
22665
|
if (!group || !this.knocking(group, live)) throw new Error("No invitation to decline");
|
|
22666
|
+
if (this.keptWhileKnocking(live)) return this.endCheck(group);
|
|
22465
22667
|
if (live) throw new Error("You were in this group: leave it to stop trying to get back in");
|
|
22466
22668
|
await this.forget(groupId);
|
|
22467
22669
|
}
|
|
@@ -22489,6 +22691,11 @@ var Communities = class {
|
|
|
22489
22691
|
/** Everything timed: beacon, hub election, lobbies, knocks, leaves, edges. The engine calls it every second or so. */
|
|
22490
22692
|
async tick(now = Date.now()) {
|
|
22491
22693
|
this.time = now;
|
|
22694
|
+
const active = [...this.live.values()].filter((l) => l.session.status === "active");
|
|
22695
|
+
this.sharing = {
|
|
22696
|
+
groups: Math.max(1, active.length),
|
|
22697
|
+
hubs: Math.max(1, active.filter((l) => l.hub).length)
|
|
22698
|
+
};
|
|
22492
22699
|
for (const group of [...this.stored.values()]) {
|
|
22493
22700
|
const live = this.live.get(group.id);
|
|
22494
22701
|
if (group.joining && (this.knocking(group, live) || group.joining.check)) {
|
|
@@ -22535,7 +22742,7 @@ var Communities = class {
|
|
|
22535
22742
|
if ([...this.host.entries(groupId).values()].includes(linkId)) await this.host.closeEdge(linkId);
|
|
22536
22743
|
}
|
|
22537
22744
|
const alone = live.hub && !freshHubs(this.hubs(live), now).some((h) => h.key !== me);
|
|
22538
|
-
if ((live.beaconFailedAt ? now - live.beaconFailedAt >= BEACON_RETRY_MS : now - live.lastBeaconRead >= (
|
|
22745
|
+
if ((live.beaconFailedAt ? now - live.beaconFailedAt >= BEACON_RETRY_MS : now - live.lastBeaconRead >= this.beaconEvery(groupId, live, alone)) || live.lastBeaconRead === 0) await this.readBeacon(groupId, live, now);
|
|
22539
22746
|
if (!live.beaconKnown) {
|
|
22540
22747
|
await this.keepLooking(groupId, live, now);
|
|
22541
22748
|
await this.reconcile(groupId, live, now);
|
|
@@ -22797,11 +23004,22 @@ var Communities = class {
|
|
|
22797
23004
|
const n = live.members.size;
|
|
22798
23005
|
return n >= this.capacity(groupId, live, now, false) ? Math.max(n, COMMUNITY_TOPOLOGY.hubCapacity) : n;
|
|
22799
23006
|
}
|
|
23007
|
+
/**
|
|
23008
|
+
* How often the beacon is read: alone, as often as I write my entry. A member with an edge up shares one community's
|
|
23009
|
+
* pace with the others it is in (`sharing`), up to that of a lone hub: an entry stays fresh three such readings. A
|
|
23010
|
+
* hub does not: the door is the hubs whose entries it read lately (`doorHubs`, an entry 45 s old at most), and read
|
|
23011
|
+
* slower, the door kept changing and read every knock record again each time. Nor a member with no edge up: who the
|
|
23012
|
+
* hubs are is what it needs to connect.
|
|
23013
|
+
*/
|
|
23014
|
+
beaconEvery(groupId, live, alone) {
|
|
23015
|
+
if (alone) return COMMUNITY_TOPOLOGY.beaconEveryMs;
|
|
23016
|
+
return !live.hub && [...this.host.edges(groupId).values()].some((id) => this.host.linkReady(id, 2)) ? Math.min(this.timings.beaconReadMs * this.sharing.groups, COMMUNITY_TOPOLOGY.beaconEveryMs) : this.timings.beaconReadMs;
|
|
23017
|
+
}
|
|
22800
23018
|
/** Reads the beacon; false when the reading did not happen (the relays held it back, or none answered). */
|
|
22801
23019
|
async readBeacon(groupId, live, now) {
|
|
22802
23020
|
live.lastBeaconRead = now;
|
|
22803
23021
|
const keys = beaconKeys(live.session.state.rv, groupId);
|
|
22804
|
-
const records = await this.host.resolve(keys.identity.pubKeyZ32, true, live
|
|
23022
|
+
const records = await this.host.resolve(keys.identity.pubKeyZ32, true, this.doorBusy(groupId, live, now)).catch(() => void 0);
|
|
22805
23023
|
if (records === void 0) {
|
|
22806
23024
|
live.beaconFailedAt = now;
|
|
22807
23025
|
live.beaconAskedAt ||= now;
|
|
@@ -23009,7 +23227,7 @@ var Communities = class {
|
|
|
23009
23227
|
}
|
|
23010
23228
|
async pollLobby(groupId, live, now) {
|
|
23011
23229
|
const keys = lobbyKeys(live.session.state.rv, groupId, live.session.myKey);
|
|
23012
|
-
const entries = readLobby(keys, await this.host.resolve(keys.identity.pubKeyZ32, true,
|
|
23230
|
+
const entries = readLobby(keys, await this.host.resolve(keys.identity.pubKeyZ32, true, this.doorBusy(groupId, live, now)).catch(() => null) ?? []);
|
|
23013
23231
|
for (const { key, ts } of entries) {
|
|
23014
23232
|
if (now - ts > COMMUNITY_TOPOLOGY.lobbyFreshMs || key === live.session.myKey) continue;
|
|
23015
23233
|
if (live.session.wasRemoved(key)) {
|
|
@@ -23099,6 +23317,17 @@ var Communities = class {
|
|
|
23099
23317
|
return now - live.admittedAt < ADMITTED_QUIET_MS || [...live.pendingEntries.keys()].some((key) => down(entries.get(key))) || [...live.expect].some((key) => down(edges.get(key)));
|
|
23100
23318
|
}
|
|
23101
23319
|
/**
|
|
23320
|
+
* As a hub: someone is coming in, or about to. I am letting someone in, the link was just shown, a crowd fills the
|
|
23321
|
+
* bell, or members ask in my lobby (a hub just left, say). Then my reads of the bell, the beacon and my lobby go as
|
|
23322
|
+
* the door's (`PkarrRequestOptions.door`): not held back while links signal. Otherwise they give way to the links
|
|
23323
|
+
* that signal, my edges in my other communities first: an app that was a hub of 4 communities on one relay (8 on
|
|
23324
|
+
* two) spent its relays' background share on reads that each said nobody was there, and its edges, whose signaling
|
|
23325
|
+
* had what was left, stayed down 10 minutes and more in some of them (CLI daemons, 2026-10-13).
|
|
23326
|
+
*/
|
|
23327
|
+
doorBusy(groupId, live, now) {
|
|
23328
|
+
return live.hub && (now < live.warmUntil || now < live.crowdUntil || now < live.lobbyBusyUntil || this.admitting(groupId, live, now));
|
|
23329
|
+
}
|
|
23330
|
+
/**
|
|
23102
23331
|
* Which knock records to read now (none, most of the time): every one at once the first time
|
|
23103
23332
|
* (a hub just elected, an app back, a change of door), since a knock left while nobody was at the
|
|
23104
23333
|
* door may be in any; then the door the bell, and the others in turn; the other hubs one in turn, slowly.
|
|
@@ -23111,7 +23340,7 @@ var Communities = class {
|
|
|
23111
23340
|
return Array.from({ length: 4 }, (_, n) => n);
|
|
23112
23341
|
}
|
|
23113
23342
|
const crowd = now < live.crowdUntil;
|
|
23114
|
-
const every = !door ? this.timings.otherHubKnockPollMs : busy && !crowd ? this.timings.knockBusyPollMs : now < live.warmUntil ? this.timings.knockWarmPollMs : this.timings.knockPollMs;
|
|
23343
|
+
const every = !door ? this.timings.otherHubKnockPollMs : busy && !crowd ? this.timings.knockBusyPollMs : now < live.warmUntil ? this.timings.knockWarmPollMs : Math.min(this.timings.knockPollMs * this.sharing.hubs, this.timings.otherHubKnockPollMs);
|
|
23115
23344
|
if (now - live.lastKnockPoll < every) return [];
|
|
23116
23345
|
live.lastKnockPoll = now;
|
|
23117
23346
|
if (!door) return [live.knockCursor = (live.knockCursor + 1) % 4];
|
|
@@ -23137,9 +23366,10 @@ var Communities = class {
|
|
|
23137
23366
|
const records = this.knockRecordsToRead(live, door === s.myKey, busy, now);
|
|
23138
23367
|
if (!records.length) return;
|
|
23139
23368
|
let failed = 0;
|
|
23369
|
+
const open = door === s.myKey && this.doorBusy(groupId, live, now);
|
|
23140
23370
|
const read = await Promise.all(records.map(async (n) => {
|
|
23141
23371
|
const record = knockRecord(link, n);
|
|
23142
|
-
return readKnocks(record, await this.host.resolve(knockIdentity(record).pubKeyZ32, true,
|
|
23372
|
+
return readKnocks(record, await this.host.resolve(knockIdentity(record).pubKeyZ32, true, open && n === KNOCK_BELL).catch(() => {
|
|
23143
23373
|
failed++;
|
|
23144
23374
|
return null;
|
|
23145
23375
|
}) ?? []);
|
|
@@ -24609,8 +24839,8 @@ function eventTime(taken, groupId, timestamp) {
|
|
|
24609
24839
|
var arrivalNow = (tick) => Math.max(tick, Date.now());
|
|
24610
24840
|
/**
|
|
24611
24841
|
* Keeps on the group's record when a member's message came, where that is later than what it holds: the record is
|
|
24612
|
-
* saved with the session
|
|
24613
|
-
* the group unread after the app starts again. Read from the history alone, by its own time, it sorted among what I
|
|
24842
|
+
* saved with the session soon after (a message taken moves its state, saved within `GROUP_LIMITS.saveWithinMs` and when
|
|
24843
|
+
* 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
|
|
24614
24844
|
* had read and the mark was gone on the next start.
|
|
24615
24845
|
*/
|
|
24616
24846
|
function noteCame(group, came, peer, mentioned) {
|
|
@@ -24661,6 +24891,8 @@ var ENTRY_RELEASE_MS = 2e3;
|
|
|
24661
24891
|
var REMOVED_LINGER_MS = 15e3;
|
|
24662
24892
|
/** How long the tombstone of a group I left waits for the admin to hear it. */
|
|
24663
24893
|
var LEFT_KEPT_MS = 6048e5;
|
|
24894
|
+
/** Admin side: how long a contact invited over its chat is waited for. After, it holds no place and may be invited again. */
|
|
24895
|
+
var INVITED_KEPT_MS = LEFT_KEPT_MS;
|
|
24664
24896
|
/**
|
|
24665
24897
|
* Invitee side: an accept the admin has not answered with a welcome is said again this often while the inviter's chat
|
|
24666
24898
|
* is up, and at once when it comes up, at most `ACCEPT_AGAINS` times each time it does (its app may have restarted).
|
|
@@ -24681,6 +24913,13 @@ function invitationClaims(frame) {
|
|
|
24681
24913
|
var FAREWELL_ANSWERS = 3;
|
|
24682
24914
|
/** Commits sent to a removed member that was behind when it left, at most. */
|
|
24683
24915
|
var FAREWELL_COMMITS = 64;
|
|
24916
|
+
/**
|
|
24917
|
+
* A member whose messages were just asked of another (`Groups.askOf`) is told where I am only once that one's answer
|
|
24918
|
+
* has come: no message from it for this long, or `MESH_SYNC_HOLD_MAX_MS` after it was asked. Told at once, its own
|
|
24919
|
+
* answer raced the other's, which holds the same messages among everyone's in the order they were written, and a
|
|
24920
|
+
* member back in a group of five read each member's lines apart, the newest above older ones (WISP 902 § Catch-up).
|
|
24921
|
+
*/
|
|
24922
|
+
var MESH_SYNC_HOLD_MS = 1500;
|
|
24684
24923
|
/** Where a member's gossip turns start among the members it is connected to: a number of its own, from its key. */
|
|
24685
24924
|
var gossipStart = (key) => {
|
|
24686
24925
|
let n = 0;
|
|
@@ -24738,6 +24977,8 @@ var Groups = class {
|
|
|
24738
24977
|
timings;
|
|
24739
24978
|
stored = /* @__PURE__ */ new Map();
|
|
24740
24979
|
sessions = /* @__PURE__ */ new Map();
|
|
24980
|
+
/** Per private group, the rows that hold the frame their message came in (`StoredMessage.frame`), oldest first, and what each counts for. */
|
|
24981
|
+
framesKept = /* @__PURE__ */ new Map();
|
|
24741
24982
|
/** Contacts (chat ids, kept in `StoredGroup.invited`) and entry sessions I invited to each group, until they answer. */
|
|
24742
24983
|
invited = /* @__PURE__ */ new Map();
|
|
24743
24984
|
/** 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. */
|
|
@@ -24783,6 +25024,13 @@ var Groups = class {
|
|
|
24783
25024
|
* time, not of everyone whose edge opens, so a member coming back is not sent the same messages by every other.
|
|
24784
25025
|
*/
|
|
24785
25026
|
relayAsked = /* @__PURE__ */ new Map();
|
|
25027
|
+
/**
|
|
25028
|
+
* Per mesh group, the members whose edge opened while their messages were being handed on by another (`holdSync`):
|
|
25029
|
+
* whom, and when it was asked. Their sync goes once that answer has come.
|
|
25030
|
+
*/
|
|
25031
|
+
syncHeld = /* @__PURE__ */ new Map();
|
|
25032
|
+
/** Per mesh group and member, when a message last came over its edge (its own, or one it handed on). */
|
|
25033
|
+
lastHanded = /* @__PURE__ */ new Map();
|
|
24786
25034
|
lastGossip = /* @__PURE__ */ new Map();
|
|
24787
25035
|
/**
|
|
24788
25036
|
* Per mesh group, each reachable member some of whose messages never reached me (`GroupSession.lacking`): what was
|
|
@@ -24790,6 +25038,11 @@ var Groups = class {
|
|
|
24790
25038
|
* holds any more are not asked for every gossip turn. `last`: the latest ask, its member and the numbers it named.
|
|
24791
25039
|
*/
|
|
24792
25040
|
gapsAsked = /* @__PURE__ */ new Map();
|
|
25041
|
+
/**
|
|
25042
|
+
* Per mesh group and member, per epoch: the number up to which nobody holds what never reached me (`gapsSettle`).
|
|
25043
|
+
* The member list stops counting those; they are still asked of a member that connects later.
|
|
25044
|
+
*/
|
|
25045
|
+
gapsGone = /* @__PURE__ */ new Map();
|
|
24793
25046
|
/** Per mesh group, when `gapsAgain` last looked: every half `MESH_GAP_AGAIN_MS`, not every tick. */
|
|
24794
25047
|
gapsLooked = /* @__PURE__ */ new Map();
|
|
24795
25048
|
/** Per mesh group: member key → since when its edge to me is down, and when someone last told me it is here. */
|
|
@@ -24862,6 +25115,10 @@ var Groups = class {
|
|
|
24862
25115
|
this.communities.files = this.files;
|
|
24863
25116
|
}
|
|
24864
25117
|
}
|
|
25118
|
+
/** Writes what the private groups' sessions still have to save (`GROUP_LIMITS.saveWithinMs`): the app is stopping. */
|
|
25119
|
+
async flush() {
|
|
25120
|
+
await Promise.allSettled([...this.sessions.values()].map((session) => session.flush()));
|
|
25121
|
+
}
|
|
24865
25122
|
/** `asItIs`: a copy that must stay as it was stored (limited mode, WISP 06), so nothing left over is cleared from it. */
|
|
24866
25123
|
async load(asItIs = false) {
|
|
24867
25124
|
const all = await this.store.getGroups();
|
|
@@ -24871,6 +25128,10 @@ var Groups = class {
|
|
|
24871
25128
|
if (group.state) this.attach(group.state);
|
|
24872
25129
|
if (group.invited) this.invited.set(group.id, new Set(Object.keys(group.invited)));
|
|
24873
25130
|
const history = await this.store.getMessages(MESSAGE_LINK(group.id)), said = history.filter((m) => !m.event && !m.groupPay);
|
|
25131
|
+
this.framesKept.set(group.id, history.filter((m) => m.frame).map((m) => ({
|
|
25132
|
+
id: m.id,
|
|
25133
|
+
bytes: frameBytes(m.frame)
|
|
25134
|
+
})));
|
|
24874
25135
|
const last = said[said.length - 1], lastPeer = [...said].reverse().find((m) => m.sender !== "me");
|
|
24875
25136
|
if (last) this.lastMessageAt.set(group.id, last.timestamp);
|
|
24876
25137
|
const peerAt = cameOrWritten(lastPeer?.timestamp, lastPeer ? group.came?.peer : void 0);
|
|
@@ -24946,6 +25207,7 @@ var Groups = class {
|
|
|
24946
25207
|
admin: invitation.admin,
|
|
24947
25208
|
members: Math.min(Math.max(invitation.n, 0), GROUP_MEMBER_CAP.max),
|
|
24948
25209
|
accepted: !!invitation.seedB64,
|
|
25210
|
+
...this.acceptSaid.get(group.id)?.unanswered ? { unanswered: true } : {},
|
|
24949
25211
|
...invitation.entry ? {
|
|
24950
25212
|
viaLink: true,
|
|
24951
25213
|
stage: this.joinStage(group)
|
|
@@ -24987,7 +25249,7 @@ var Groups = class {
|
|
|
24987
25249
|
...viaHub ? { viaHub } : {},
|
|
24988
25250
|
...hubKeys.has(key) ? { hub: true } : {},
|
|
24989
25251
|
...hubRole ? { hubRole } : {},
|
|
24990
|
-
missing: session.missing(key)
|
|
25252
|
+
missing: session.missing(key, this.gapsGone.get(group.id)?.get(key)?.[session.epoch])
|
|
24991
25253
|
};
|
|
24992
25254
|
})
|
|
24993
25255
|
};
|
|
@@ -25092,6 +25354,22 @@ var Groups = class {
|
|
|
25092
25354
|
await this.store.putGroup(group);
|
|
25093
25355
|
this.host.emit();
|
|
25094
25356
|
}
|
|
25357
|
+
/**
|
|
25358
|
+
* Admin side: the invitations still unanswered are taken back, each over the contact chat it went on, when the group
|
|
25359
|
+
* 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
|
|
25360
|
+
* to its accept. Entry sessions are not told: they close with the group, or the new admin answers the next knock.
|
|
25361
|
+
*/
|
|
25362
|
+
takeBack(groupId, group) {
|
|
25363
|
+
const entries = new Set(this.host.entries(groupId).values());
|
|
25364
|
+
for (const linkId of this.invited.get(groupId) ?? []) if (!entries.has(linkId)) try {
|
|
25365
|
+
this.host.sendOnLink(linkId, {
|
|
25366
|
+
t: "group-decline",
|
|
25367
|
+
g: groupId
|
|
25368
|
+
});
|
|
25369
|
+
} catch {}
|
|
25370
|
+
this.invited.delete(groupId);
|
|
25371
|
+
if (group) delete group.invited;
|
|
25372
|
+
}
|
|
25095
25373
|
/** Admin side: a contact chat answered its invitation (accepted or declined); false when it had none. */
|
|
25096
25374
|
answered(group, linkId) {
|
|
25097
25375
|
const had = !!this.invited.get(group.id)?.delete(linkId);
|
|
@@ -25100,6 +25378,14 @@ var Groups = class {
|
|
|
25100
25378
|
group.invited = Object.keys(rest).length ? rest : void 0;
|
|
25101
25379
|
return true;
|
|
25102
25380
|
}
|
|
25381
|
+
/** 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. */
|
|
25382
|
+
async ageInvited(group, now) {
|
|
25383
|
+
const stale = Object.entries(group.invited).filter(([, at]) => now - at > INVITED_KEPT_MS);
|
|
25384
|
+
if (!stale.length) return;
|
|
25385
|
+
for (const [linkId] of stale) this.answered(group, linkId);
|
|
25386
|
+
await this.store.putGroup(group);
|
|
25387
|
+
this.host.emit();
|
|
25388
|
+
}
|
|
25103
25389
|
async accept(groupId) {
|
|
25104
25390
|
const group = this.stored.get(groupId);
|
|
25105
25391
|
if (!group?.invitation) throw new Error("No invitation to accept");
|
|
@@ -25126,19 +25412,56 @@ var Groups = class {
|
|
|
25126
25412
|
if (this.isCommunity(groupId)) return this.communities.decline(groupId);
|
|
25127
25413
|
const group = this.stored.get(groupId);
|
|
25128
25414
|
if (!group?.invitation) throw new Error("No invitation to decline");
|
|
25129
|
-
|
|
25130
|
-
|
|
25131
|
-
|
|
25132
|
-
|
|
25133
|
-
|
|
25134
|
-
} catch {}
|
|
25415
|
+
if (!group.previous) return this.forget(groupId);
|
|
25416
|
+
await this.dropInvitation(group, this.sayDecline(groupId, group.invitation.linkId));
|
|
25417
|
+
}
|
|
25418
|
+
/** An invitation goes from the list: declined here (`owed`: the answer, when it could not be sent), or taken back by who sent it. */
|
|
25419
|
+
async dropInvitation(group, owed) {
|
|
25135
25420
|
const previous = group.previous;
|
|
25136
|
-
if (!previous) return this.forget(
|
|
25137
|
-
|
|
25421
|
+
if (!previous) return this.forget(group.id, false);
|
|
25422
|
+
if (owed) previous.declined = owed;
|
|
25423
|
+
else delete previous.declined;
|
|
25424
|
+
this.acceptSaid.delete(group.id);
|
|
25425
|
+
this.stored.set(group.id, previous);
|
|
25138
25426
|
if (previous.state) this.attach(previous.state);
|
|
25139
25427
|
await this.store.putGroup(previous);
|
|
25140
25428
|
this.host.emit();
|
|
25141
25429
|
}
|
|
25430
|
+
/** Invitee side: says no to an invitation. With the inviter's chat down (its app is closed, say), returns the answer owed, said when that chat is next up. */
|
|
25431
|
+
sayDecline(groupId, linkId) {
|
|
25432
|
+
try {
|
|
25433
|
+
this.host.sendOnLink(linkId, {
|
|
25434
|
+
t: "group-decline",
|
|
25435
|
+
g: groupId
|
|
25436
|
+
});
|
|
25437
|
+
return;
|
|
25438
|
+
} catch {
|
|
25439
|
+
return {
|
|
25440
|
+
at: Date.now(),
|
|
25441
|
+
linkId
|
|
25442
|
+
};
|
|
25443
|
+
}
|
|
25444
|
+
}
|
|
25445
|
+
/** Invitee side: a decline its inviter's chat was down for is said once that chat is up, or dropped after `LEFT_KEPT_MS`. */
|
|
25446
|
+
async declineOwed(group, now) {
|
|
25447
|
+
const { at, linkId } = group.declined;
|
|
25448
|
+
if (now - at <= LEFT_KEPT_MS) {
|
|
25449
|
+
if (!this.host.linkReady(linkId)) return;
|
|
25450
|
+
try {
|
|
25451
|
+
this.host.sendOnLink(linkId, {
|
|
25452
|
+
t: "group-decline",
|
|
25453
|
+
g: group.id
|
|
25454
|
+
});
|
|
25455
|
+
} catch {
|
|
25456
|
+
return;
|
|
25457
|
+
}
|
|
25458
|
+
}
|
|
25459
|
+
if (this.stored.get(group.id) !== group) return;
|
|
25460
|
+
delete group.declined;
|
|
25461
|
+
if (group.state) return this.store.putGroup(group);
|
|
25462
|
+
this.stored.delete(group.id);
|
|
25463
|
+
await this.store.deleteGroup(group.id);
|
|
25464
|
+
}
|
|
25142
25465
|
/**
|
|
25143
25466
|
* Sends a text; `messageId` is the id it is kept under here (what history, replies and reactions name). `forwarded`:
|
|
25144
25467
|
* the hop count of a forwarded text (WISP 902 § Forwards). `card`: a checked status card, the text its fallback.
|
|
@@ -25289,8 +25612,7 @@ var Groups = class {
|
|
|
25289
25612
|
...bye ? { bye } : {}
|
|
25290
25613
|
};
|
|
25291
25614
|
delete group.entry;
|
|
25292
|
-
|
|
25293
|
-
this.invited.delete(groupId);
|
|
25615
|
+
this.takeBack(groupId, group);
|
|
25294
25616
|
this.pendingEntries.delete(groupId);
|
|
25295
25617
|
this.welcomed.delete(groupId);
|
|
25296
25618
|
this.lingering.delete(groupId);
|
|
@@ -25457,8 +25779,14 @@ var Groups = class {
|
|
|
25457
25779
|
farewell.told ??= this.now();
|
|
25458
25780
|
this.store.putGroup(group).catch(() => {});
|
|
25459
25781
|
}
|
|
25460
|
-
makeAdmin(groupId, key) {
|
|
25461
|
-
|
|
25782
|
+
async makeAdmin(groupId, key) {
|
|
25783
|
+
if (this.isCommunity(groupId)) return this.communities.makeAdmin(groupId, key);
|
|
25784
|
+
await this.session(groupId).transferAdmin(key);
|
|
25785
|
+
const group = this.stored.get(groupId);
|
|
25786
|
+
if (!group || !this.invited.get(groupId)?.size) return;
|
|
25787
|
+
this.takeBack(groupId, group);
|
|
25788
|
+
await this.store.putGroup(group);
|
|
25789
|
+
this.host.emit();
|
|
25462
25790
|
}
|
|
25463
25791
|
/**
|
|
25464
25792
|
* 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
|
|
@@ -25482,7 +25810,8 @@ var Groups = class {
|
|
|
25482
25810
|
if (this.isCommunity(groupId)) return this.communities.rename(groupId, name);
|
|
25483
25811
|
await this.session(groupId).rename(name);
|
|
25484
25812
|
}
|
|
25485
|
-
|
|
25813
|
+
/** `answer`: false when an invitation goes because its inviter took it back, so no decline is said for it. */
|
|
25814
|
+
async forget(groupId, answer = true) {
|
|
25486
25815
|
if (this.isCommunity(groupId)) return this.communities.leave(groupId);
|
|
25487
25816
|
const session = this.sessions.get(groupId);
|
|
25488
25817
|
if (session?.status === "active") try {
|
|
@@ -25490,13 +25819,16 @@ var Groups = class {
|
|
|
25490
25819
|
} catch (e) {
|
|
25491
25820
|
if (session.isAdmin && session.others.length) throw e;
|
|
25492
25821
|
}
|
|
25822
|
+
const gone = this.stored.get(groupId), invitation = answer && gone?.invitation && !gone.invitation.entry ? gone.invitation : void 0;
|
|
25823
|
+
const owed = invitation && this.sayDecline(groupId, invitation.linkId);
|
|
25493
25824
|
this.sessions.delete(groupId);
|
|
25494
25825
|
this.stored.delete(groupId);
|
|
25495
|
-
this.
|
|
25826
|
+
this.takeBack(groupId);
|
|
25496
25827
|
this.acceptSaid.delete(groupId);
|
|
25497
25828
|
this.lastRoster.delete(groupId);
|
|
25498
25829
|
this.lastTold.delete(groupId);
|
|
25499
25830
|
this.pendingEntries.delete(groupId);
|
|
25831
|
+
this.framesKept.delete(groupId);
|
|
25500
25832
|
this.knocked.delete(groupId);
|
|
25501
25833
|
this.knockCheckAt.delete(groupId);
|
|
25502
25834
|
this.knockRewrites.delete(groupId);
|
|
@@ -25504,10 +25836,13 @@ var Groups = class {
|
|
|
25504
25836
|
this.welcomed.delete(groupId);
|
|
25505
25837
|
this.lingering.delete(groupId);
|
|
25506
25838
|
this.relayAsked.delete(groupId);
|
|
25839
|
+
this.syncHeld.delete(groupId);
|
|
25840
|
+
this.lastHanded.delete(groupId);
|
|
25507
25841
|
this.hubs.forget(groupId);
|
|
25508
25842
|
this.removedAt.delete(groupId);
|
|
25509
25843
|
this.lastGossip.delete(groupId);
|
|
25510
25844
|
this.gapsAsked.delete(groupId);
|
|
25845
|
+
this.gapsGone.delete(groupId);
|
|
25511
25846
|
this.gapsLooked.delete(groupId);
|
|
25512
25847
|
this.typings.forget(groupId);
|
|
25513
25848
|
for (const key of this.pinPaces.keys()) if (key.startsWith(`${groupId}\n`)) this.pinPaces.delete(key);
|
|
@@ -25517,6 +25852,15 @@ var Groups = class {
|
|
|
25517
25852
|
this.signals.forget(groupId);
|
|
25518
25853
|
for (const linkId of [...this.host.edges(groupId).values(), ...this.host.entries(groupId).values()]) await this.host.closeEdge(linkId);
|
|
25519
25854
|
await this.store.deleteGroup(groupId);
|
|
25855
|
+
if (owed && !this.stored.has(groupId)) {
|
|
25856
|
+
const tombstone = {
|
|
25857
|
+
id: groupId,
|
|
25858
|
+
createdAt: gone.createdAt,
|
|
25859
|
+
declined: owed
|
|
25860
|
+
};
|
|
25861
|
+
this.stored.set(groupId, tombstone);
|
|
25862
|
+
await this.store.putGroup(tombstone);
|
|
25863
|
+
}
|
|
25520
25864
|
await this.files?.drop(groupId);
|
|
25521
25865
|
this.host.historyGone?.(groupId);
|
|
25522
25866
|
this.host.membersChanged?.(groupId);
|
|
@@ -25609,6 +25953,10 @@ var Groups = class {
|
|
|
25609
25953
|
if (now - group.left.at > LEFT_KEPT_MS) await this.forget(group.id);
|
|
25610
25954
|
continue;
|
|
25611
25955
|
}
|
|
25956
|
+
if (group.declined) {
|
|
25957
|
+
await this.declineOwed(group, now);
|
|
25958
|
+
if (!group.state) continue;
|
|
25959
|
+
}
|
|
25612
25960
|
if (group.invitation?.entry && !group.state) {
|
|
25613
25961
|
const every = now - group.createdAt > this.timings.patienceMs ? this.timings.slowKnockMs : this.timings.knockMs;
|
|
25614
25962
|
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(() => {});
|
|
@@ -25619,6 +25967,7 @@ var Groups = class {
|
|
|
25619
25967
|
continue;
|
|
25620
25968
|
}
|
|
25621
25969
|
const session = this.sessions.get(group.id);
|
|
25970
|
+
if (group.invited) await this.ageInvited(group, now);
|
|
25622
25971
|
if (!group.entry || !session) continue;
|
|
25623
25972
|
if (!session.isAdmin) {
|
|
25624
25973
|
await this.disableLink(group.id);
|
|
@@ -25694,7 +26043,15 @@ var Groups = class {
|
|
|
25694
26043
|
});
|
|
25695
26044
|
return;
|
|
25696
26045
|
}
|
|
26046
|
+
if (said.n >= ACCEPT_AGAINS && !said.down && !said.unanswered && !invitation.entry && now - said.at >= ACCEPT_AGAIN_MS) {
|
|
26047
|
+
this.acceptSaid.set(group.id, {
|
|
26048
|
+
...said,
|
|
26049
|
+
unanswered: true
|
|
26050
|
+
});
|
|
26051
|
+
this.host.emit();
|
|
26052
|
+
}
|
|
25697
26053
|
if (said.n >= ACCEPT_AGAINS || !said.down && now - said.at < ACCEPT_AGAIN_MS) return;
|
|
26054
|
+
invitation.pieces = [];
|
|
25698
26055
|
try {
|
|
25699
26056
|
this.host.sendOnLink(invitation.linkId, {
|
|
25700
26057
|
t: "group-accept",
|
|
@@ -25706,7 +26063,8 @@ var Groups = class {
|
|
|
25706
26063
|
}
|
|
25707
26064
|
this.acceptSaid.set(group.id, {
|
|
25708
26065
|
at: now,
|
|
25709
|
-
n: said.n + 1
|
|
26066
|
+
n: said.n + 1,
|
|
26067
|
+
...said.unanswered && { unanswered: true }
|
|
25710
26068
|
});
|
|
25711
26069
|
}
|
|
25712
26070
|
/** An entry session came up with groups on both sides: the admin's side invites over it. */
|
|
@@ -25943,7 +26301,24 @@ var Groups = class {
|
|
|
25943
26301
|
}
|
|
25944
26302
|
case "group-accept": {
|
|
25945
26303
|
const session = this.sessions.get(g);
|
|
25946
|
-
if (
|
|
26304
|
+
if (typeof frame.key !== "string" || !MEMBER_KEY.test(frame.key)) return;
|
|
26305
|
+
if (!session?.isAdmin || !this.invited.get(g)?.has(linkId)) {
|
|
26306
|
+
const letIn = !!session && rosterHas(session.roster, frame.key) && this.stored.get(g)?.contacts?.[frame.key] === linkId;
|
|
26307
|
+
if (!letIn && ![...this.host.entries(g).values()].includes(linkId)) try {
|
|
26308
|
+
this.host.sendOnLink(linkId, {
|
|
26309
|
+
t: "group-decline",
|
|
26310
|
+
g
|
|
26311
|
+
});
|
|
26312
|
+
} catch {}
|
|
26313
|
+
const edge = this.host.edges(g).get(frame.key);
|
|
26314
|
+
if (letIn && session.isAdmin && !(edge && this.host.linkReady(edge))) {
|
|
26315
|
+
const welcome = await session.rewelcome(frame.key).catch(() => []);
|
|
26316
|
+
try {
|
|
26317
|
+
for (const piece of welcome) this.host.sendOnLink(linkId, piece);
|
|
26318
|
+
} catch {}
|
|
26319
|
+
}
|
|
26320
|
+
return;
|
|
26321
|
+
}
|
|
25947
26322
|
const group = this.stored.get(g);
|
|
25948
26323
|
if (frame.key === session.myKey || rosterHas(session.roster, frame.key)) return;
|
|
25949
26324
|
const entryPeer = this.hostEntry(g, linkId);
|
|
@@ -25977,6 +26352,11 @@ var Groups = class {
|
|
|
25977
26352
|
return;
|
|
25978
26353
|
}
|
|
25979
26354
|
case "group-decline": {
|
|
26355
|
+
const mine = this.stored.get(g);
|
|
26356
|
+
if (mine?.invitation && !mine.invitation.entry && !mine.state && mine.invitation.linkId === linkId) {
|
|
26357
|
+
await this.dropInvitation(mine);
|
|
26358
|
+
return;
|
|
26359
|
+
}
|
|
25980
26360
|
const entryPeer = this.hostEntry(g, linkId);
|
|
25981
26361
|
if (entryPeer) {
|
|
25982
26362
|
this.pendingEntries.get(g)?.delete(entryPeer);
|
|
@@ -26078,7 +26458,7 @@ var Groups = class {
|
|
|
26078
26458
|
const group = this.stored.get(g);
|
|
26079
26459
|
const member = Object.entries(group?.contacts ?? {}).find(([, id]) => id === linkId)?.[0];
|
|
26080
26460
|
if (!session?.isAdmin || !member || member === session.myKey) return;
|
|
26081
|
-
if (rosterHas(session.roster, member)) await session.remove(member).catch(() => {});
|
|
26461
|
+
if (rosterHas(session.roster, member)) await session.remove(member, Date.now(), { left: true }).catch(() => {});
|
|
26082
26462
|
try {
|
|
26083
26463
|
this.host.sendOnLink(linkId, {
|
|
26084
26464
|
t: "group-removed",
|
|
@@ -26146,6 +26526,11 @@ var Groups = class {
|
|
|
26146
26526
|
return;
|
|
26147
26527
|
}
|
|
26148
26528
|
const taken = await session.handle(peerKey, frame);
|
|
26529
|
+
if (taken.some((f) => f.t === "group-msg")) {
|
|
26530
|
+
let handed = this.lastHanded.get(groupId);
|
|
26531
|
+
if (!handed) this.lastHanded.set(groupId, handed = /* @__PURE__ */ new Map());
|
|
26532
|
+
handed.set(peerKey, this.now());
|
|
26533
|
+
}
|
|
26149
26534
|
if (taken.length) this.hubs.passOn(groupId, session, peerKey, taken);
|
|
26150
26535
|
}
|
|
26151
26536
|
/**
|
|
@@ -26217,7 +26602,7 @@ var Groups = class {
|
|
|
26217
26602
|
const large = this.host.linkReady(linkId, 3) && this.markLarge(group, peerKey);
|
|
26218
26603
|
const legacy = this.hubs.enabled && this.hubs.edgeReady(groupId, session, group, peerKey, this.host.linkReady(linkId, 4), this.now());
|
|
26219
26604
|
if (large || legacy) this.store.putGroup(group).catch(() => {});
|
|
26220
|
-
try {
|
|
26605
|
+
if (!this.holdSync(groupId, peerKey)) try {
|
|
26221
26606
|
this.host.sendOnLink(linkId, session.syncFrame(this.askOf(groupId, session, peerKey)));
|
|
26222
26607
|
} catch {
|
|
26223
26608
|
return;
|
|
@@ -26340,11 +26725,55 @@ var Groups = class {
|
|
|
26340
26725
|
});
|
|
26341
26726
|
out.push(key);
|
|
26342
26727
|
}
|
|
26343
|
-
|
|
26728
|
+
const lacking = session.lacking();
|
|
26729
|
+
out.push(...this.gapsOf(groupId, session, via, lacking));
|
|
26730
|
+
this.ownGap(groupId, via, lacking);
|
|
26344
26731
|
if (out.length) this.lastGossip.set(groupId, now);
|
|
26345
26732
|
return out;
|
|
26346
26733
|
}
|
|
26347
26734
|
/**
|
|
26735
|
+
* Whether the answer of the member asked for someone's messages may still be coming: its edge up, asked under
|
|
26736
|
+
* `MESH_SYNC_HOLD_MAX_MS` ago, and a message from it (or the asking) under `MESH_SYNC_HOLD_MS` ago.
|
|
26737
|
+
*/
|
|
26738
|
+
answering(groupId, { via, at }, now) {
|
|
26739
|
+
const edge = this.host.edges(groupId).get(via);
|
|
26740
|
+
if (!edge || !this.host.linkReady(edge) || now - at >= 1e4) return false;
|
|
26741
|
+
return now - Math.max(at, this.lastHanded.get(groupId)?.get(via) ?? 0) < MESH_SYNC_HOLD_MS;
|
|
26742
|
+
}
|
|
26743
|
+
/**
|
|
26744
|
+
* Whether the sync to a member whose edge just opened waits (`heldSyncs` sends it): its messages were asked of
|
|
26745
|
+
* another member a moment ago, and that answer holds them among everyone's in the order they were written. Its own
|
|
26746
|
+
* answer now would bring the same messages on their own, ahead of or behind the others' lines.
|
|
26747
|
+
*/
|
|
26748
|
+
holdSync(groupId, peerKey) {
|
|
26749
|
+
const asked = this.relayAsked.get(groupId)?.get(peerKey);
|
|
26750
|
+
if (!asked || asked.via === peerKey || !this.answering(groupId, asked, this.now())) {
|
|
26751
|
+
this.syncHeld.get(groupId)?.delete(peerKey);
|
|
26752
|
+
this.relayAsked.get(groupId)?.delete(peerKey);
|
|
26753
|
+
return false;
|
|
26754
|
+
}
|
|
26755
|
+
let held = this.syncHeld.get(groupId);
|
|
26756
|
+
if (!held) this.syncHeld.set(groupId, held = /* @__PURE__ */ new Map());
|
|
26757
|
+
held.set(peerKey, { ...asked });
|
|
26758
|
+
return true;
|
|
26759
|
+
}
|
|
26760
|
+
/** The syncs that waited for another member's answer (`holdSync`), once it has come: for what it did not hand on. */
|
|
26761
|
+
heldSyncs(groupId, session, now) {
|
|
26762
|
+
const held = this.syncHeld.get(groupId);
|
|
26763
|
+
if (!held?.size) return;
|
|
26764
|
+
const edges = this.host.edges(groupId);
|
|
26765
|
+
for (const [key, asked] of held) {
|
|
26766
|
+
if (this.answering(groupId, asked, now)) continue;
|
|
26767
|
+
held.delete(key);
|
|
26768
|
+
if (this.relayAsked.get(groupId)?.get(key)?.via === asked.via) this.relayAsked.get(groupId).delete(key);
|
|
26769
|
+
const edge = edges.get(key);
|
|
26770
|
+
if (!edge || !this.host.linkReady(edge) || !rosterHas(session.roster, key)) continue;
|
|
26771
|
+
try {
|
|
26772
|
+
this.host.sendOnLink(edge, session.syncFrame(this.askOf(groupId, session, key)));
|
|
26773
|
+
} catch {}
|
|
26774
|
+
}
|
|
26775
|
+
}
|
|
26776
|
+
/**
|
|
26348
26777
|
* Reachable members whose messages below the highest seen never reached me, for `via` to hand on: their own answer
|
|
26349
26778
|
* comes from their last `GROUP_LIMITS.outlog` only (WISP 902 § Catch-up), the rest another member may hold. As
|
|
26350
26779
|
* `askOf`, one member at a time, and each member once until what is missing changes.
|
|
@@ -26374,6 +26803,7 @@ var Groups = class {
|
|
|
26374
26803
|
});
|
|
26375
26804
|
if (entry.of.has(via)) continue;
|
|
26376
26805
|
entry.of.add(via);
|
|
26806
|
+
entry.at = now;
|
|
26377
26807
|
entry.last = {
|
|
26378
26808
|
via,
|
|
26379
26809
|
at: now,
|
|
@@ -26388,6 +26818,56 @@ var Groups = class {
|
|
|
26388
26818
|
return out;
|
|
26389
26819
|
}
|
|
26390
26820
|
/**
|
|
26821
|
+
* Whether a sync to `via` asks it for its own messages that never reached me, for the first time since what is
|
|
26822
|
+
* missing changed: any sync names them (`miss`), and an author answers from its own log (`GROUP_LIMITS.outlog`).
|
|
26823
|
+
*/
|
|
26824
|
+
ownGap(groupId, via, lacking) {
|
|
26825
|
+
const mark = lacking.get(via);
|
|
26826
|
+
if (mark === void 0) return false;
|
|
26827
|
+
let tried = this.gapsAsked.get(groupId);
|
|
26828
|
+
if (!tried) this.gapsAsked.set(groupId, tried = /* @__PURE__ */ new Map());
|
|
26829
|
+
let entry = tried.get(via);
|
|
26830
|
+
const now = this.now();
|
|
26831
|
+
if (!entry || entry.mark !== mark) tried.set(via, entry = {
|
|
26832
|
+
mark,
|
|
26833
|
+
of: /* @__PURE__ */ new Set(),
|
|
26834
|
+
noticed: entry?.noticed ?? now,
|
|
26835
|
+
last: entry?.last
|
|
26836
|
+
});
|
|
26837
|
+
if (entry.of.has(via)) return false;
|
|
26838
|
+
entry.of.add(via);
|
|
26839
|
+
entry.at = now;
|
|
26840
|
+
return true;
|
|
26841
|
+
}
|
|
26842
|
+
/**
|
|
26843
|
+
* Messages nobody holds any more: every connected member, their author included, was asked for the same missing
|
|
26844
|
+
* numbers `MESH_GAP_AGAIN_MS` ago or more and none arrived. An author keeps its last 32 and the others the group's
|
|
26845
|
+
* last 256 (WISP 902 § Bounds), so what a sync names and nobody hands on is gone, and so is what is below it (logs
|
|
26846
|
+
* drop the oldest first). The member list counted them until the next epoch, as if they were still on their way.
|
|
26847
|
+
*/
|
|
26848
|
+
gapsSettle(groupId, session, now, connected, lacking) {
|
|
26849
|
+
const tried = this.gapsAsked.get(groupId);
|
|
26850
|
+
if (!tried) return;
|
|
26851
|
+
let changed = false;
|
|
26852
|
+
for (const [key, mark] of lacking) {
|
|
26853
|
+
const entry = tried.get(key);
|
|
26854
|
+
if (!entry || entry.mark !== mark || entry.at === void 0 || now - entry.at < 1e4) continue;
|
|
26855
|
+
if (!connected.includes(key) || !connected.every((member) => entry.of.has(member))) continue;
|
|
26856
|
+
let gone = this.gapsGone.get(groupId);
|
|
26857
|
+
if (!gone) this.gapsGone.set(groupId, gone = /* @__PURE__ */ new Map());
|
|
26858
|
+
const upTo = { ...gone.get(key) };
|
|
26859
|
+
for (const [e, numbers] of Object.entries(session.missOf(key))) {
|
|
26860
|
+
const top = Math.max(...numbers);
|
|
26861
|
+
if (top > (upTo[e] ?? -1)) {
|
|
26862
|
+
upTo[e] = top;
|
|
26863
|
+
changed = true;
|
|
26864
|
+
}
|
|
26865
|
+
}
|
|
26866
|
+
gone.set(key, upTo);
|
|
26867
|
+
}
|
|
26868
|
+
if (changed) this.host.emit();
|
|
26869
|
+
}
|
|
26870
|
+
/**
|
|
26391
26871
|
* Gaps asked for sooner than the gossip turn: one noticed `MESH_GAP_AGAIN_MS` ago and not asked for yet (an author's
|
|
26392
26872
|
* own answer, its last 32, showed what is below them) goes to a connected member now; and a member that handed on some
|
|
26393
26873
|
* 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
|
|
@@ -26402,6 +26882,7 @@ var Groups = class {
|
|
|
26402
26882
|
if (!lacking.size && !tried?.size) return;
|
|
26403
26883
|
if (!tried) this.gapsAsked.set(groupId, tried = /* @__PURE__ */ new Map());
|
|
26404
26884
|
for (const key of tried.keys()) if (!lacking.has(key)) tried.delete(key);
|
|
26885
|
+
this.gapsSettle(groupId, session, now, connected, lacking);
|
|
26405
26886
|
const edges = this.host.edges(groupId), again = /* @__PURE__ */ new Map(), fresh = /* @__PURE__ */ new Map();
|
|
26406
26887
|
const up = (key) => {
|
|
26407
26888
|
const edge = edges.get(key);
|
|
@@ -26429,6 +26910,7 @@ var Groups = class {
|
|
|
26429
26910
|
tried.set(key, {
|
|
26430
26911
|
mark: lacking.get(key),
|
|
26431
26912
|
of: /* @__PURE__ */ new Set([via]),
|
|
26913
|
+
at: now,
|
|
26432
26914
|
last: {
|
|
26433
26915
|
via,
|
|
26434
26916
|
at: now,
|
|
@@ -26444,11 +26926,14 @@ var Groups = class {
|
|
|
26444
26926
|
this.host.sendOnLink(edges.get(via), session.syncFrame(keys));
|
|
26445
26927
|
} catch {}
|
|
26446
26928
|
}
|
|
26447
|
-
|
|
26448
|
-
|
|
26449
|
-
|
|
26929
|
+
if (!fresh.size || !connected.length) return;
|
|
26930
|
+
const at = () => connected[(gossipStart(session.myKey) + this.gossipTurn++) % connected.length];
|
|
26931
|
+
let via = at();
|
|
26932
|
+
for (let i = 1; i < connected.length && fresh.has(via); i++) via = at();
|
|
26450
26933
|
const keys = this.gapsOf(groupId, session, via, fresh);
|
|
26451
|
-
if (keys.length)
|
|
26934
|
+
if (!keys.length) return;
|
|
26935
|
+
this.lastGossip.set(groupId, now);
|
|
26936
|
+
try {
|
|
26452
26937
|
this.host.sendOnLink(edges.get(via), session.syncFrame(keys));
|
|
26453
26938
|
} catch {}
|
|
26454
26939
|
}
|
|
@@ -26466,6 +26951,7 @@ var Groups = class {
|
|
|
26466
26951
|
if (session.status !== "active" || this.stored.get(groupId)?.left) continue;
|
|
26467
26952
|
session.reseal().catch(() => {});
|
|
26468
26953
|
this.farewellsTick(groupId, session, now);
|
|
26954
|
+
this.heldSyncs(groupId, session, now);
|
|
26469
26955
|
if (reconcile) {
|
|
26470
26956
|
const edges = this.host.edges(groupId);
|
|
26471
26957
|
if (!this.hubs.large(session) && session.others.some((key) => !edges.has(key))) this.reconcileEdges(groupId);
|
|
@@ -26488,7 +26974,7 @@ var Groups = class {
|
|
|
26488
26974
|
at: now
|
|
26489
26975
|
});
|
|
26490
26976
|
const ask = [...away, ...this.gapsOf(groupId, session, via, lacking)];
|
|
26491
|
-
if (!ask.length) continue;
|
|
26977
|
+
if (!this.ownGap(groupId, via, lacking) && !ask.length) continue;
|
|
26492
26978
|
try {
|
|
26493
26979
|
this.host.sendOnLink(edges.get(via), session.syncFrame(ask));
|
|
26494
26980
|
} catch {}
|
|
@@ -26514,6 +27000,25 @@ var Groups = class {
|
|
|
26514
27000
|
if (session && this.stored.get(groupId)?.farewells?.[peerKey] && !rosterHas(session.roster, peerKey)) return;
|
|
26515
27001
|
session?.setNick(peerKey, nick);
|
|
26516
27002
|
}
|
|
27003
|
+
/**
|
|
27004
|
+
* A message's row took the frame it came in: past `GROUP_LIMITS.relay` frames (or their bytes) in a group, the oldest
|
|
27005
|
+
* rows give theirs up, so a group's history holds the frames of its newest messages only.
|
|
27006
|
+
*/
|
|
27007
|
+
async frameKept(groupId, id, frame) {
|
|
27008
|
+
let kept = this.framesKept.get(groupId);
|
|
27009
|
+
if (!kept) this.framesKept.set(groupId, kept = []);
|
|
27010
|
+
if (kept.some((k) => k.id === id)) return;
|
|
27011
|
+
kept.push({
|
|
27012
|
+
id,
|
|
27013
|
+
bytes: frameBytes(frame)
|
|
27014
|
+
});
|
|
27015
|
+
let bytes = kept.reduce((sum, k) => sum + k.bytes, 0);
|
|
27016
|
+
while (kept.length > GROUP_LIMITS.relay || bytes > GROUP_LIMITS.relayBytes) {
|
|
27017
|
+
const oldest = kept.shift();
|
|
27018
|
+
bytes -= oldest.bytes;
|
|
27019
|
+
await this.host.forgetFrame?.(MESSAGE_LINK(groupId), oldest.id).catch(() => {});
|
|
27020
|
+
}
|
|
27021
|
+
}
|
|
26517
27022
|
session(groupId) {
|
|
26518
27023
|
const session = this.sessions.get(groupId);
|
|
26519
27024
|
if (!session) throw new Error("Group not found");
|
|
@@ -26527,7 +27032,7 @@ var Groups = class {
|
|
|
26527
27032
|
adminTurn: () => this.host.adminTurn?.(state.id) ?? Promise.resolve(true),
|
|
26528
27033
|
save: async (next) => {
|
|
26529
27034
|
const group = this.stored.get(state.id);
|
|
26530
|
-
if (!group) return;
|
|
27035
|
+
if (!group || this.sessions.get(state.id) !== session) return;
|
|
26531
27036
|
group.state = next;
|
|
26532
27037
|
await this.store.putGroup(group);
|
|
26533
27038
|
},
|
|
@@ -26569,9 +27074,12 @@ var Groups = class {
|
|
|
26569
27074
|
...m.reply && { replyTo: groupReply(m.reply, session.myKey) },
|
|
26570
27075
|
...m.forwarded && { forwarded: m.forwarded },
|
|
26571
27076
|
...m.card && { card: m.card },
|
|
26572
|
-
...file && { file }
|
|
27077
|
+
...file && { file },
|
|
27078
|
+
...m.frame && { frame: m.frame }
|
|
26573
27079
|
};
|
|
26574
|
-
const
|
|
27080
|
+
const completed = !!m.completes && !!this.host.completeMessage;
|
|
27081
|
+
const stored = completed ? (await this.host.completeMessage(message), false) : await this.host.storeMessage(message);
|
|
27082
|
+
if (m.frame && (completed || stored !== false)) await this.frameKept(state.id, m.id, m.frame);
|
|
26575
27083
|
const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, arrivalNow(this.now()));
|
|
26576
27084
|
this.lastMessageAt.set(state.id, Math.max(this.lastMessageAt.get(state.id) ?? 0, came));
|
|
26577
27085
|
if (mentioned) this.lastMentionAt.set(state.id, Math.max(this.lastMentionAt.get(state.id) ?? 0, came));
|
|
@@ -27045,9 +27553,12 @@ function summary(note) {
|
|
|
27045
27553
|
const amount = note.decimals ? `${note.amount} base units of ${note.unit}` : `${Number(note.amount).toLocaleString()} ${note.test ? "test sats" : "sats"}`;
|
|
27046
27554
|
return note.kind === "request" ? `⚡ Requested ${amount}` : `⚡ Sent ${amount}`;
|
|
27047
27555
|
}
|
|
27556
|
+
/** 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. */
|
|
27557
|
+
var GROUP_EDIT_RESEND_WINDOW_MS = EDIT_RATE_WINDOW_MS + 1e3;
|
|
27048
27558
|
/** An edit that could not go (no epoch key yet) is tried again after this long. */
|
|
27049
27559
|
var RETRY_MS = 1e4;
|
|
27050
27560
|
var chatOf = (groupId) => `group:${groupId}`;
|
|
27561
|
+
var resendKey = (groupId, member) => `${groupId}\n${member}`;
|
|
27051
27562
|
var GroupEdits = class {
|
|
27052
27563
|
host;
|
|
27053
27564
|
now;
|
|
@@ -27056,10 +27567,23 @@ var GroupEdits = class {
|
|
|
27056
27567
|
cards;
|
|
27057
27568
|
sendPace = /* @__PURE__ */ new Map();
|
|
27058
27569
|
receivePace = /* @__PURE__ */ new Map();
|
|
27570
|
+
/** Per group, per member (`group\nmember`): its edits past the window, by message, waiting for room (`hold`). */
|
|
27571
|
+
held = /* @__PURE__ */ new Map();
|
|
27572
|
+
heldTimers = /* @__PURE__ */ new Map();
|
|
27059
27573
|
timers = /* @__PURE__ */ new Map();
|
|
27060
27574
|
flushing = /* @__PURE__ */ new Map();
|
|
27061
27575
|
/** Per group: my rows known to have an edit to say, each with when it was last told (`edit`), by `tick`. */
|
|
27062
27576
|
known = /* @__PURE__ */ new Map();
|
|
27577
|
+
/**
|
|
27578
|
+
* Per group and member (`resendKey`): the last edit of mine said on its edge, in the order `resend` says them. What
|
|
27579
|
+
* comes after it is what the member has not heard there. Not kept across a restart.
|
|
27580
|
+
*/
|
|
27581
|
+
heard = /* @__PURE__ */ new Map();
|
|
27582
|
+
/** The connection `resend` last spoke on, per group and member: a new one hears my latest edits again. */
|
|
27583
|
+
resentOn = /* @__PURE__ */ new Map();
|
|
27584
|
+
resendPace = /* @__PURE__ */ new Map();
|
|
27585
|
+
resendTimers = /* @__PURE__ */ new Map();
|
|
27586
|
+
resending = /* @__PURE__ */ new Map();
|
|
27063
27587
|
tick = 0;
|
|
27064
27588
|
/**
|
|
27065
27589
|
* The groups whose history was read for the rows with an edit to say: `known` has them all, and from then on those
|
|
@@ -27208,7 +27732,8 @@ var GroupEdits = class {
|
|
|
27208
27732
|
/**
|
|
27209
27733
|
* An edit from a member, authenticated as `sender` by the group. Believed only while the sender is in the group (a
|
|
27210
27734
|
* removed member's edits are dropped) and only of the sender's own message; the highest number wins. One of a message
|
|
27211
|
-
* not here yet waits a minute for it. Past `EDIT_RECEIVE_LIMIT` a window from one member, the rest
|
|
27735
|
+
* not here yet waits a minute for it. Past `EDIT_RECEIVE_LIMIT` a window from one member, the rest wait for the next
|
|
27736
|
+
* window (`hold`): a catch-up hands back every edit of an author at once, and one dropped is never handed again.
|
|
27212
27737
|
*/
|
|
27213
27738
|
async receive(groupId, sender, edit) {
|
|
27214
27739
|
const membership = this.host.membership(groupId);
|
|
@@ -27216,7 +27741,65 @@ var GroupEdits = class {
|
|
|
27216
27741
|
const key = `${groupId}\n${sender}`;
|
|
27217
27742
|
let pace = this.receivePace.get(key);
|
|
27218
27743
|
if (!pace) this.receivePace.set(key, pace = new RateWindow(30, EDIT_RATE_WINDOW_MS, this.now));
|
|
27219
|
-
if (!pace.take()) return
|
|
27744
|
+
if (this.held.get(groupId)?.has(key) || !pace.take()) return this.hold(groupId, sender, edit, pace);
|
|
27745
|
+
return this.take(groupId, membership, sender, edit);
|
|
27746
|
+
}
|
|
27747
|
+
/**
|
|
27748
|
+
* An edit past what a member's window takes: kept, the highest number per message, `GROUP_EDITS_HELD` messages a
|
|
27749
|
+
* group at most (the oldest go first), and taken when the window has room. The work stays what the window allows.
|
|
27750
|
+
*/
|
|
27751
|
+
hold(groupId, sender, edit, pace) {
|
|
27752
|
+
const key = `${groupId}\n${sender}`;
|
|
27753
|
+
let group = this.held.get(groupId);
|
|
27754
|
+
if (!group) this.held.set(groupId, group = /* @__PURE__ */ new Map());
|
|
27755
|
+
let waiting = group.get(key);
|
|
27756
|
+
if (!waiting) group.set(key, waiting = /* @__PURE__ */ new Map());
|
|
27757
|
+
const known = waiting.get(edit.id);
|
|
27758
|
+
if (known && known.e >= edit.e) return "dropped";
|
|
27759
|
+
waiting.delete(edit.id);
|
|
27760
|
+
waiting.set(edit.id, edit);
|
|
27761
|
+
let count = 0;
|
|
27762
|
+
for (const member of group.values()) count += member.size;
|
|
27763
|
+
for (const [member, edits] of group) {
|
|
27764
|
+
if (count <= 256) break;
|
|
27765
|
+
while (count > 256 && edits.size) {
|
|
27766
|
+
edits.delete(edits.keys().next().value);
|
|
27767
|
+
count--;
|
|
27768
|
+
}
|
|
27769
|
+
if (!edits.size && member !== key) group.delete(member);
|
|
27770
|
+
}
|
|
27771
|
+
if (!this.heldTimers.has(key) && !this.stopped) this.heldTimers.set(key, setTimeout(() => {
|
|
27772
|
+
this.takeHeld(groupId, sender).catch(() => {});
|
|
27773
|
+
}, Math.max(50, pace.wait())));
|
|
27774
|
+
return waiting.has(edit.id) ? "waiting" : "dropped";
|
|
27775
|
+
}
|
|
27776
|
+
/** The window of a member has room again: its edits that waited are taken, oldest first, as many as it allows. */
|
|
27777
|
+
async takeHeld(groupId, sender) {
|
|
27778
|
+
const key = `${groupId}\n${sender}`;
|
|
27779
|
+
this.heldTimers.delete(key);
|
|
27780
|
+
const group = this.held.get(groupId), waiting = group?.get(key), pace = this.receivePace.get(key);
|
|
27781
|
+
if (this.stopped || !group || !waiting || !pace) return;
|
|
27782
|
+
for (const [id, edit] of [...waiting]) {
|
|
27783
|
+
const membership = this.host.membership(groupId);
|
|
27784
|
+
if (!membership?.members.has(sender)) {
|
|
27785
|
+
waiting.clear();
|
|
27786
|
+
break;
|
|
27787
|
+
}
|
|
27788
|
+
if (!pace.take()) break;
|
|
27789
|
+
if (waiting.get(id) !== edit) continue;
|
|
27790
|
+
waiting.delete(id);
|
|
27791
|
+
await this.take(groupId, membership, sender, edit).catch(() => {});
|
|
27792
|
+
}
|
|
27793
|
+
if (waiting.size) {
|
|
27794
|
+
if (!this.heldTimers.has(key) && !this.stopped) this.heldTimers.set(key, setTimeout(() => {
|
|
27795
|
+
this.takeHeld(groupId, sender).catch(() => {});
|
|
27796
|
+
}, Math.max(50, pace.wait())));
|
|
27797
|
+
return;
|
|
27798
|
+
}
|
|
27799
|
+
if (group.get(key) === waiting) group.delete(key);
|
|
27800
|
+
if (!group.size) this.held.delete(groupId);
|
|
27801
|
+
}
|
|
27802
|
+
async take(groupId, membership, sender, edit) {
|
|
27220
27803
|
const chat = chatOf(groupId);
|
|
27221
27804
|
const message = await this.find(chat, edit.id);
|
|
27222
27805
|
if (!message) {
|
|
@@ -27264,24 +27847,80 @@ var GroupEdits = class {
|
|
|
27264
27847
|
await this.host.changed(chat, message.id);
|
|
27265
27848
|
return "applied";
|
|
27266
27849
|
}
|
|
27267
|
-
/**
|
|
27268
|
-
|
|
27269
|
-
|
|
27270
|
-
|
|
27271
|
-
|
|
27272
|
-
|
|
27273
|
-
|
|
27274
|
-
|
|
27275
|
-
|
|
27276
|
-
|
|
27277
|
-
|
|
27278
|
-
}
|
|
27850
|
+
/**
|
|
27851
|
+
* An edge of a private group opened: the member at its other end hears the edits of mine it has not heard on that
|
|
27852
|
+
* edge, the latest of each message, oldest first. `GROUP_EDIT_RESEND` go a window and the rest a window later each,
|
|
27853
|
+
* while the edge is the same: a member away while I closed forty task cards reads them all as closed, not the latest
|
|
27854
|
+
* sixteen. Nothing known of what it heard (the first time since the app started): my newest `GROUP_EDIT_CATCH_UP`.
|
|
27855
|
+
* A new connection hears my latest `GROUP_EDIT_RESEND` again whatever it heard: one may have come before its message
|
|
27856
|
+
* and been dropped. Called again while the edge is the same, it goes on from where it was.
|
|
27857
|
+
*/
|
|
27858
|
+
resend(groupId, to) {
|
|
27859
|
+
const key = resendKey(groupId, to);
|
|
27860
|
+
const run = (this.resending.get(key) ?? Promise.resolve()).then(() => this.resendNow(groupId, to));
|
|
27861
|
+
const settled = run.catch(() => {}).finally(() => {
|
|
27862
|
+
if (this.resending.get(key) === settled) this.resending.delete(key);
|
|
27863
|
+
});
|
|
27864
|
+
this.resending.set(key, settled);
|
|
27865
|
+
return run;
|
|
27866
|
+
}
|
|
27867
|
+
async resendNow(groupId, to) {
|
|
27868
|
+
const key = resendKey(groupId, to), chat = chatOf(groupId);
|
|
27869
|
+
clearTimeout(this.resendTimers.get(key));
|
|
27870
|
+
this.resendTimers.delete(key);
|
|
27871
|
+
const open = () => this.host.edge ? this.host.edge(groupId, to) : true;
|
|
27872
|
+
const edge = open(), membership = this.host.membership(groupId);
|
|
27873
|
+
if (this.stopped || edge === void 0 || !membership || membership.community || !membership.members.has(to)) return;
|
|
27874
|
+
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));
|
|
27875
|
+
const last = this.heard.get(key);
|
|
27876
|
+
const after = (m) => !last || m.edit.at > last.at || m.edit.at === last.at && m.id > last.id;
|
|
27877
|
+
const unheard = last ? mine.filter(after).length : Math.min(mine.length, 256);
|
|
27878
|
+
const again = this.resentOn.get(key) === edge ? 0 : Math.min(mine.length, 16);
|
|
27879
|
+
this.resentOn.set(key, edge);
|
|
27880
|
+
const due = mine.slice(mine.length - Math.max(unheard, again));
|
|
27881
|
+
let pace = this.resendPace.get(key);
|
|
27882
|
+
if (!pace) this.resendPace.set(key, pace = new RateWindow(16, GROUP_EDIT_RESEND_WINDOW_MS, this.now));
|
|
27883
|
+
for (const m of due) {
|
|
27884
|
+
if (this.stopped || open() !== edge) return;
|
|
27885
|
+
if (!pace.take()) {
|
|
27886
|
+
this.resendTimers.set(key, setTimeout(() => {
|
|
27887
|
+
this.resend(groupId, to).catch(() => {});
|
|
27888
|
+
}, Math.max(50, pace.wait())));
|
|
27889
|
+
return;
|
|
27890
|
+
}
|
|
27891
|
+
await this.host.send(groupId, {
|
|
27892
|
+
id: m.id,
|
|
27893
|
+
e: m.edit.seq,
|
|
27894
|
+
ts: m.edit.at,
|
|
27895
|
+
m: m.text,
|
|
27896
|
+
...m.mentions?.length && { k: m.mentions },
|
|
27897
|
+
...m.card && { sc: m.card }
|
|
27898
|
+
}, to);
|
|
27899
|
+
if (open() !== edge) return;
|
|
27900
|
+
if (after(m)) this.heard.set(key, {
|
|
27901
|
+
at: m.edit.at,
|
|
27902
|
+
id: m.id
|
|
27903
|
+
});
|
|
27904
|
+
}
|
|
27905
|
+
}
|
|
27906
|
+
/** A group is forgotten, or the app stops: what was known of its members' edges goes with it. */
|
|
27907
|
+
dropResends(groupId) {
|
|
27908
|
+
const of = (key) => groupId === void 0 || key.startsWith(`${groupId}\n`);
|
|
27909
|
+
for (const [key, timer] of this.resendTimers) if (of(key)) {
|
|
27910
|
+
clearTimeout(timer);
|
|
27911
|
+
this.resendTimers.delete(key);
|
|
27912
|
+
}
|
|
27913
|
+
for (const key of [...this.heard.keys()]) if (of(key)) this.heard.delete(key);
|
|
27914
|
+
for (const key of [...this.resendPace.keys()]) if (of(key)) this.resendPace.delete(key);
|
|
27915
|
+
for (const key of [...this.resentOn.keys()]) if (of(key)) this.resentOn.delete(key);
|
|
27279
27916
|
}
|
|
27280
27917
|
forget(groupId) {
|
|
27281
27918
|
clearTimeout(this.timers.get(groupId));
|
|
27282
27919
|
this.timers.delete(groupId);
|
|
27283
27920
|
this.known.delete(groupId);
|
|
27284
27921
|
this.learnt.delete(groupId);
|
|
27922
|
+
this.dropResends(groupId);
|
|
27923
|
+
this.dropHeld(groupId);
|
|
27285
27924
|
this.buffer.forget(chatOf(groupId));
|
|
27286
27925
|
}
|
|
27287
27926
|
stop() {
|
|
@@ -27289,6 +27928,16 @@ var GroupEdits = class {
|
|
|
27289
27928
|
this.cards.stop();
|
|
27290
27929
|
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
27291
27930
|
this.timers.clear();
|
|
27931
|
+
this.dropResends();
|
|
27932
|
+
this.dropHeld();
|
|
27933
|
+
}
|
|
27934
|
+
dropHeld(groupId) {
|
|
27935
|
+
for (const [key, timer] of this.heldTimers) if (groupId === void 0 || key.startsWith(`${groupId}\n`)) {
|
|
27936
|
+
clearTimeout(timer);
|
|
27937
|
+
this.heldTimers.delete(key);
|
|
27938
|
+
}
|
|
27939
|
+
if (groupId === void 0) this.held.clear();
|
|
27940
|
+
else this.held.delete(groupId);
|
|
27292
27941
|
}
|
|
27293
27942
|
};
|
|
27294
27943
|
//#endregion
|
|
@@ -28622,8 +29271,7 @@ function historyFromRows(rows) {
|
|
|
28622
29271
|
});
|
|
28623
29272
|
else if (e.kind === "flapping") events.push({
|
|
28624
29273
|
...base,
|
|
28625
|
-
kind: e.live ? "live" : "down"
|
|
28626
|
-
reason: `Dropped and came back ${e.count ?? 0} times`
|
|
29274
|
+
kind: e.live ? "live" : "down"
|
|
28627
29275
|
});
|
|
28628
29276
|
else if (e.kind === "connected" || e.kind === "back") events.push({
|
|
28629
29277
|
...base,
|
|
@@ -30385,7 +31033,8 @@ var CashuWallet = class CashuWallet {
|
|
|
30385
31033
|
tx: walletTx(mint, kind, credited, Math.max(0, faceValue - credited), note),
|
|
30386
31034
|
...payment ? { payment: payment(credited, mint) } : {}
|
|
30387
31035
|
});
|
|
30388
|
-
|
|
31036
|
+
const written = (await this.swapsAt(mint)).filter((s) => s.kind === "receive");
|
|
31037
|
+
let swap = written.find((s) => s.token === token);
|
|
30389
31038
|
if (swap?.finished) return {
|
|
30390
31039
|
amount: swap.tx?.amount ?? 0,
|
|
30391
31040
|
mint,
|
|
@@ -30393,6 +31042,8 @@ var CashuWallet = class CashuWallet {
|
|
|
30393
31042
|
};
|
|
30394
31043
|
if (!swap) {
|
|
30395
31044
|
const preview = await (await this.wallet(mint)).prepareSwapToReceive(token);
|
|
31045
|
+
const redeeming = new Set(written.flatMap((s) => (swapOf(s).inputs ?? []).map((p) => p.secret)));
|
|
31046
|
+
if (preview.inputs.some((p) => redeeming.has(p.secret))) throw engineError("ecashBeingReceived");
|
|
30396
31047
|
swap = {
|
|
30397
31048
|
id: swapId(),
|
|
30398
31049
|
mint,
|
|
@@ -30529,7 +31180,17 @@ var CashuWallet = class CashuWallet {
|
|
|
30529
31180
|
id: p.id
|
|
30530
31181
|
})));
|
|
30531
31182
|
if (states.length !== saved.inputs.length) throw new Error("The mint answered for another number of proofs");
|
|
30532
|
-
if (states.some((s) => s.state === "PENDING"))
|
|
31183
|
+
if (states.some((s) => s.state === "PENDING")) {
|
|
31184
|
+
if (swap.kind === "receive" && refused) return {
|
|
31185
|
+
state: "never",
|
|
31186
|
+
gone: /* @__PURE__ */ new Set(),
|
|
31187
|
+
final: true
|
|
31188
|
+
};
|
|
31189
|
+
return {
|
|
31190
|
+
state: "open",
|
|
31191
|
+
pending: true
|
|
31192
|
+
};
|
|
31193
|
+
}
|
|
30533
31194
|
const gone = new Set(saved.inputs.filter((_, i) => states[i]?.state === "SPENT").map((p) => p.secret));
|
|
30534
31195
|
if (gone.size > 0) {
|
|
30535
31196
|
const second = await signed();
|
|
@@ -30651,7 +31312,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30651
31312
|
}
|
|
30652
31313
|
/**
|
|
30653
31314
|
* Swaps still written down: the app closed while the mint was being asked, or the mint did not answer. Each mint is
|
|
30654
|
-
* asked where its own stand, under its lock. A swap it made is stored as its answer would have been; one it says it
|
|
31315
|
+
* asked where its own stand, one swap at a time under its lock. A swap it made is stored as its answer would have been; one it says it
|
|
30655
31316
|
* never made is undone; one that is only overdue gives its inputs back and stays watched; one it cannot speak for
|
|
30656
31317
|
* is asked about again. `mints`: only these mints', once, now.
|
|
30657
31318
|
*/
|
|
@@ -30670,16 +31331,18 @@ var CashuWallet = class CashuWallet {
|
|
|
30670
31331
|
const later = (ms) => {
|
|
30671
31332
|
again = again ? Math.min(again, ms) : ms;
|
|
30672
31333
|
};
|
|
30673
|
-
for (const mint of
|
|
30674
|
-
|
|
31334
|
+
for (const { id, mint } of open) await this.locked(mint, async () => {
|
|
31335
|
+
let swap = await wrap((await store(STORES$1.swaps, "readonly")).get(id));
|
|
31336
|
+
if (!swap) return;
|
|
31337
|
+
try {
|
|
30675
31338
|
if (swap.finished) {
|
|
30676
31339
|
if (await this.tellSwap(swap, true)) await wrap((await store(STORES$1.swaps, "readwrite")).delete(swap.id));
|
|
30677
31340
|
else later(SWAP_POLL_MS);
|
|
30678
|
-
|
|
31341
|
+
return;
|
|
30679
31342
|
}
|
|
30680
31343
|
if (swap.askAt && Date.now() < swap.askAt && !mints) {
|
|
30681
31344
|
later(swap.askAt - Date.now());
|
|
30682
|
-
|
|
31345
|
+
return;
|
|
30683
31346
|
}
|
|
30684
31347
|
if (swap.attemptEndedAt === void 0) {
|
|
30685
31348
|
swap = {
|
|
@@ -30692,7 +31355,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30692
31355
|
if (outcome.state === "open") {
|
|
30693
31356
|
if (swap.released && Date.now() >= swap.createdAt + SWAP_WATCH_MS) {
|
|
30694
31357
|
await this.dropSwap(swap, /* @__PURE__ */ new Set());
|
|
30695
|
-
|
|
31358
|
+
return;
|
|
30696
31359
|
}
|
|
30697
31360
|
if (outcome.slow) {
|
|
30698
31361
|
const stuck = (swap.stuck ?? 0) + 1;
|
|
@@ -30731,8 +31394,20 @@ var CashuWallet = class CashuWallet {
|
|
|
30731
31394
|
this.events.onChange();
|
|
30732
31395
|
} else later(SWAP_POLL_MS);
|
|
30733
31396
|
}
|
|
30734
|
-
else
|
|
30735
|
-
|
|
31397
|
+
else if (outcome.pending && swap.kind === "receive" && !swap.released && Date.now() >= swapOverdueAt(swap)) {
|
|
31398
|
+
if (Date.now() < swap.createdAt + SWAP_WATCH_MS) {
|
|
31399
|
+
await wrap((await store(STORES$1.swaps, "readwrite")).put({
|
|
31400
|
+
...swap,
|
|
31401
|
+
askAt: Date.now() + SWAP_SLOW_POLL_MS
|
|
31402
|
+
}));
|
|
31403
|
+
later(SWAP_SLOW_POLL_MS);
|
|
31404
|
+
} else {
|
|
31405
|
+
await this.dropSwap(swap, /* @__PURE__ */ new Set());
|
|
31406
|
+
await this.tellSwap(swap, false);
|
|
31407
|
+
this.events.onChange();
|
|
31408
|
+
}
|
|
31409
|
+
} else later(swap.released ? SWAP_SLOW_POLL_MS : SWAP_POLL_MS);
|
|
31410
|
+
return;
|
|
30736
31411
|
}
|
|
30737
31412
|
if (outcome.state === "done") {
|
|
30738
31413
|
if (await this.finishSwap(swap, outcome.keep, outcome.send, true)) {
|
|
@@ -30743,7 +31418,7 @@ var CashuWallet = class CashuWallet {
|
|
|
30743
31418
|
} else if (swap.released) {
|
|
30744
31419
|
if (outcome.final || Date.now() >= swap.createdAt + SWAP_WATCH_MS) await this.dropSwap(swap, outcome.gone);
|
|
30745
31420
|
else later(SWAP_SLOW_POLL_MS);
|
|
30746
|
-
|
|
31421
|
+
return;
|
|
30747
31422
|
} else {
|
|
30748
31423
|
await this.dropSwap(swap, outcome.gone, !outcome.final);
|
|
30749
31424
|
await this.tellSwap(swap, false);
|
|
@@ -32197,12 +32872,27 @@ var PaymentDesk = class {
|
|
|
32197
32872
|
}
|
|
32198
32873
|
return;
|
|
32199
32874
|
}
|
|
32200
|
-
if (done)
|
|
32201
|
-
|
|
32875
|
+
if (done) {
|
|
32876
|
+
await this.creditCashu(record, link);
|
|
32877
|
+
return;
|
|
32878
|
+
}
|
|
32879
|
+
if (this.payments.has(record.id)) return;
|
|
32880
|
+
const reason = "The mint did not confirm the ecash";
|
|
32881
|
+
link?.sendPaymentResult({
|
|
32202
32882
|
id: record.id,
|
|
32203
32883
|
ok: false,
|
|
32204
|
-
error:
|
|
32884
|
+
error: reason
|
|
32205
32885
|
});
|
|
32886
|
+
try {
|
|
32887
|
+
await this.host.storeMessage({
|
|
32888
|
+
linkId: record.linkId,
|
|
32889
|
+
id: `peer_${record.createdAt}_refused`,
|
|
32890
|
+
text: `Could not receive ${record.amount.toLocaleString()} sats: ${reason}`,
|
|
32891
|
+
sender: "peer",
|
|
32892
|
+
timestamp: record.createdAt,
|
|
32893
|
+
via: "datalink"
|
|
32894
|
+
});
|
|
32895
|
+
} catch {}
|
|
32206
32896
|
}
|
|
32207
32897
|
async onPaymentResult(linkId, result) {
|
|
32208
32898
|
const payment = this.find(linkId, result.id);
|
|
@@ -33950,6 +34640,17 @@ var WalletSetup = class {
|
|
|
33950
34640
|
var RELAY_RETRY_MS = 3e4;
|
|
33951
34641
|
/** Native listeners an app may hold per transport (the Desktop's Rust Iroh allows eight, `paired_transport.rs`). */
|
|
33952
34642
|
var NATIVE_SLOTS = 8;
|
|
34643
|
+
/**
|
|
34644
|
+
* Iroh's listeners where it is the browser build (the web app, the extension, the CLI): Rust's eight are not its limit.
|
|
34645
|
+
* Each is a WebSocket to its relay and about 0.2 MiB: a phone-sized Chromium page held 128 of them, and the public
|
|
34646
|
+
* relays took sixteen started one after another, as the queue here starts them (six of sixteen started at once were
|
|
34647
|
+
* refused). 1:1 chats hold eight of them at most, as before: the rest is room for group links. HyperDHT keeps eight
|
|
34648
|
+
* there too: a page's listeners share one WebSocket to its relay, which drops a client past sixteen listens (device
|
|
34649
|
+
* links listen there as well), and each of the CLI's is a whole DHT node.
|
|
34650
|
+
*/
|
|
34651
|
+
var IROH_WEB_SLOTS = 16;
|
|
34652
|
+
/** Of a transport's listeners, the ones a group's links never hold: 1:1 chats keep that room. */
|
|
34653
|
+
var CHAT_NATIVE_SLOTS = 4;
|
|
33953
34654
|
/** How often a group link that found no free native slot tries again. */
|
|
33954
34655
|
var GROUP_NATIVE_RETRY_MS = 15e3;
|
|
33955
34656
|
/** The longest a group link whose native listener failed to start waits before trying again (its wait doubles up to it). */
|
|
@@ -33984,6 +34685,8 @@ var CARD_RESTORE_MOST = 50;
|
|
|
33984
34685
|
var CARD_RESTORE_MS = 6048e5;
|
|
33985
34686
|
/** A reaction said on the live session and not confirmed is said again after this long. */
|
|
33986
34687
|
var REACTION_RESEND_MS = 3e4;
|
|
34688
|
+
/** A file or a payment request of mine that waits for the chat to be live (a text that waits is its outbox's). */
|
|
34689
|
+
var waitsForLive = (m) => m.sender === "me" && m.delivery === "waiting" && (!!m.file || !!m.paymentId);
|
|
33987
34690
|
/** This side's participation key in a chat: derived from its seed once, not at every state built. */
|
|
33988
34691
|
function participationKeyOf(live) {
|
|
33989
34692
|
const seed = live.stored.participationSeed;
|
|
@@ -34180,10 +34883,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34180
34883
|
});
|
|
34181
34884
|
clockOff;
|
|
34182
34885
|
/**
|
|
34183
|
-
* Edges of groups whose WebRTC did not connect
|
|
34184
|
-
*
|
|
34886
|
+
* Edges of groups whose WebRTC did not connect (`edgeWithoutRtc`): they go on as an app with no WebRTC does, over a
|
|
34887
|
+
* native transport, until the member is seen online again after a packet said it was gone (`away`, `edgeRtcPresence`).
|
|
34888
|
+
* Not kept: the next start tries WebRTC first again.
|
|
34185
34889
|
*/
|
|
34186
|
-
edgeRtcOff = /* @__PURE__ */ new
|
|
34890
|
+
edgeRtcOff = /* @__PURE__ */ new Map();
|
|
34187
34891
|
pollIntervals;
|
|
34188
34892
|
localFetch;
|
|
34189
34893
|
links = /* @__PURE__ */ new Map();
|
|
@@ -34319,6 +35023,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34319
35023
|
});
|
|
34320
35024
|
stateTimer = null;
|
|
34321
35025
|
paymentTimer = null;
|
|
35026
|
+
/**
|
|
35027
|
+
* The reviews whose outcome is still open (submitted, unknown), by id: what the payment poll asks about, so that it
|
|
35028
|
+
* does not read every review ever made every 10 s. `null` until the store was read once. An id is only ever added
|
|
35029
|
+
* from a read of the store or a change of a review, and leaves when the poll finds its review ended or gone.
|
|
35030
|
+
*/
|
|
35031
|
+
openReviews = null;
|
|
35032
|
+
noteOpenReviews(reviews) {
|
|
35033
|
+
const open = this.openReviews ??= /* @__PURE__ */ new Set();
|
|
35034
|
+
for (const review of reviews) if (review.state === "submitted" || review.state === "unknown") open.add(review.id);
|
|
35035
|
+
}
|
|
34322
35036
|
/** Every minute, the reviews no Approve can send any more are cancelled (dropStaleReviews). */
|
|
34323
35037
|
staleReviewTimer;
|
|
34324
35038
|
/** Group links: admins read knocks, joiners knock (WISP 902 § Entry link). */
|
|
@@ -34441,6 +35155,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34441
35155
|
}
|
|
34442
35156
|
}
|
|
34443
35157
|
], (review) => {
|
|
35158
|
+
if (this.openReviews) this.noteOpenReviews([review]);
|
|
34444
35159
|
if (review.state === "settled") this.feedback("confirmed", review.id);
|
|
34445
35160
|
if (review.state === "failed") this.cueFeedback({
|
|
34446
35161
|
cue: "failed",
|
|
@@ -34963,7 +35678,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34963
35678
|
await this.messagesChanged(chat, [id]);
|
|
34964
35679
|
this.emitState();
|
|
34965
35680
|
},
|
|
34966
|
-
notify: (chat, key) => this.feedback("reaction", `${chat}:${key}`, chat)
|
|
35681
|
+
notify: (chat, key) => this.feedback("reaction", `${chat}:${key}`, chat),
|
|
35682
|
+
believed: (chat, by) => {
|
|
35683
|
+
const membership = this.membership(chat.slice(6));
|
|
35684
|
+
return !!membership?.members.has(by) && by !== membership.me;
|
|
35685
|
+
}
|
|
34967
35686
|
});
|
|
34968
35687
|
/**
|
|
34969
35688
|
* The chat list's reaction once one to its message was taken back: the same one if it still stands, else the latest
|
|
@@ -34996,10 +35715,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34996
35715
|
adminTurn: (_groupId, options) => this.confirmTurn({ within: options?.wait === false ? 0 : GhostlyNode.TURN_CONFIRM_WAIT_MS }),
|
|
34997
35716
|
staysOnline: () => this.options.staysOnline ?? (this.options.platform === "desktop" && typeof RTCPeerConnection !== "undefined"),
|
|
34998
35717
|
peerRoom: (groupId) => {
|
|
34999
|
-
|
|
35718
|
+
const native = Object.keys(this.nativeFactories);
|
|
35719
|
+
if (typeof RTCPeerConnection === "undefined" && native.length) {
|
|
35000
35720
|
let held = 0;
|
|
35001
35721
|
for (const live of this.links.values()) if (live.stored.group && live.stored.group !== groupId && live.link) held++;
|
|
35002
|
-
return Math.max(0,
|
|
35722
|
+
return Math.max(0, Math.max(...native.map((key) => this.groupNativeSlots(key))) - held);
|
|
35003
35723
|
}
|
|
35004
35724
|
const budget = this.options.peerBudget;
|
|
35005
35725
|
if (budget === void 0) return void 0;
|
|
@@ -35048,6 +35768,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35048
35768
|
edgeNick: (linkId) => this.links.get(linkId)?.presence.nick || void 0,
|
|
35049
35769
|
storeMessage: (message) => this.storeNewMessage(message),
|
|
35050
35770
|
completeMessage: (message) => this.completeGroupMessage(message),
|
|
35771
|
+
forgetFrame: async (linkId, id) => {
|
|
35772
|
+
await db.patchMessage(linkId, id, (stored) => stored.frame ? { frame: void 0 } : null, true);
|
|
35773
|
+
},
|
|
35051
35774
|
membersChanged: (groupId) => this.groupMembersChanged(groupId),
|
|
35052
35775
|
historyGone: (groupId) => this.groupHistoryGone(groupId),
|
|
35053
35776
|
emit: () => this.emitState(),
|
|
@@ -35110,7 +35833,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35110
35833
|
admin: !!this.groups.views().find((g) => g.id === groupId)?.isAdmin
|
|
35111
35834
|
};
|
|
35112
35835
|
},
|
|
35113
|
-
send: (groupId, edit, to) => this.groups.sendEdit(groupId, edit, to)
|
|
35836
|
+
send: (groupId, edit, to) => this.groups.sendEdit(groupId, edit, to),
|
|
35837
|
+
edge: (groupId, member) => {
|
|
35838
|
+
const link = this.links.get(this.memberEdges(groupId).get(member) ?? "")?.link;
|
|
35839
|
+
return link?.supportsGroupVersion(1) ? link : void 0;
|
|
35840
|
+
}
|
|
35114
35841
|
});
|
|
35115
35842
|
/** A community member's edit, signed by them: of their own message, and only while they are a member. */
|
|
35116
35843
|
async receiveCommunityEdit(groupId, sender, frame) {
|
|
@@ -35331,12 +36058,14 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35331
36058
|
};
|
|
35332
36059
|
}
|
|
35333
36060
|
const wallets = this.withHomes(walletInstances(networks));
|
|
36061
|
+
const intents = (await intentRepository.list()).map((saved) => saved.review);
|
|
36062
|
+
this.noteOpenReviews(intents);
|
|
35334
36063
|
this.walletView = {
|
|
35335
36064
|
...networks.mainnet,
|
|
35336
36065
|
networks,
|
|
35337
36066
|
wallets,
|
|
35338
36067
|
offers: this.walletOffers(networks, wallets),
|
|
35339
|
-
intents
|
|
36068
|
+
intents,
|
|
35340
36069
|
setup: this.walletSetup.view()
|
|
35341
36070
|
};
|
|
35342
36071
|
this.announcePaymentNetworks(wallets);
|
|
@@ -35405,9 +36134,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35405
36134
|
async pollPaymentStatus() {
|
|
35406
36135
|
if (this.shuttingDown) return;
|
|
35407
36136
|
try {
|
|
35408
|
-
|
|
36137
|
+
if (!this.openReviews) this.noteOpenReviews((await intentRepository.list()).map((saved) => saved.review));
|
|
36138
|
+
const open = this.openReviews;
|
|
36139
|
+
for (const id of [...open]) {
|
|
36140
|
+
const review = (await intentRepository.get(id))?.review;
|
|
36141
|
+
if (!review || !["submitted", "unknown"].includes(review.state)) {
|
|
36142
|
+
open.delete(id);
|
|
36143
|
+
continue;
|
|
36144
|
+
}
|
|
35409
36145
|
const network = walletNetworkOf(review.network);
|
|
35410
|
-
if (
|
|
36146
|
+
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;
|
|
35411
36147
|
await this.reconcilePayment({ id: review.id }).catch(() => {});
|
|
35412
36148
|
}
|
|
35413
36149
|
} finally {
|
|
@@ -35460,6 +36196,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35460
36196
|
if (!stored.group) this.fileDesk.restore(stored.id, storedFiles);
|
|
35461
36197
|
const files = linkFilesFrom(stored.id, storedFiles, messages);
|
|
35462
36198
|
this.links.set(stored.id, newLiveLink(stored, messages[messages.length - 1]?.timestamp ?? 0, files));
|
|
36199
|
+
if (!stored.group) this.links.get(stored.id).waitingSends = Promise.resolve(new Set(messages.filter(waitsForLive).map((m) => m.id)));
|
|
35463
36200
|
history.set(stored.id, messages);
|
|
35464
36201
|
for (const message of messages.slice(-20)) this.clockSample(message);
|
|
35465
36202
|
const note = latestReaction(messages);
|
|
@@ -35913,10 +36650,12 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35913
36650
|
this.publicActivity.clear();
|
|
35914
36651
|
for (const queue of this.editQueues.values()) queue.stop();
|
|
35915
36652
|
this.groupEdits.stop();
|
|
36653
|
+
this.reactions.stop();
|
|
35916
36654
|
this.cardEdits.stop();
|
|
35917
36655
|
await Promise.allSettled([
|
|
35918
36656
|
linksStopped,
|
|
35919
36657
|
links?.stop(),
|
|
36658
|
+
this.groups.flush(),
|
|
35920
36659
|
...WALLET_NETWORKS.flatMap((network) => [
|
|
35921
36660
|
this.arkWallets,
|
|
35922
36661
|
this.barkWallets,
|
|
@@ -36116,14 +36855,23 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36116
36855
|
edgeView(groupId, member) {
|
|
36117
36856
|
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));
|
|
36118
36857
|
}
|
|
36119
|
-
|
|
36120
|
-
|
|
36858
|
+
/** A chat's or a group's history, as a page gets it (`withoutFrame`). */
|
|
36859
|
+
async getMessages(linkId) {
|
|
36860
|
+
return (await db.getMessages(linkId)).map(withoutFrame);
|
|
36121
36861
|
}
|
|
36122
36862
|
/** One message of a chat, read by its id: the whole history costs a read of every row. */
|
|
36123
|
-
getMessage(linkId, id) {
|
|
36124
|
-
|
|
36863
|
+
async getMessage(linkId, id) {
|
|
36864
|
+
const message = await db.getMessage(linkId, id);
|
|
36865
|
+
return message && withoutFrame(message);
|
|
36125
36866
|
}
|
|
36126
|
-
async messagePage(
|
|
36867
|
+
async messagePage(asked) {
|
|
36868
|
+
const page = await this.storedPage(asked);
|
|
36869
|
+
return {
|
|
36870
|
+
...page,
|
|
36871
|
+
messages: page.messages.map(withoutFrame)
|
|
36872
|
+
};
|
|
36873
|
+
}
|
|
36874
|
+
async storedPage({ linkId, limit = 50, before }) {
|
|
36127
36875
|
if (!Number.isSafeInteger(limit) || limit < 1) throw new Error("limit: a whole number of messages, at least 1");
|
|
36128
36876
|
if (before === void 0) return db.getMessagePage(linkId, { limit });
|
|
36129
36877
|
if (typeof before === "number") {
|
|
@@ -36202,9 +36950,17 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36202
36950
|
madeAt: Date.now()
|
|
36203
36951
|
};
|
|
36204
36952
|
}
|
|
36205
|
-
/**
|
|
36953
|
+
/**
|
|
36954
|
+
* One invite warmed and waiting (`after` ms from now), warmed again every so often while it waits (the relays forget).
|
|
36955
|
+
* Once too old for `takeInvite` to hand out, it is replaced by a new one, warmed at once: warming it on would spend
|
|
36956
|
+
* two puts every few minutes on keys nobody will use, and leave the next invite on cold keys.
|
|
36957
|
+
*/
|
|
36206
36958
|
prepareSpare(after = 0) {
|
|
36207
36959
|
if (this.shuttingDown || !this.networkOn) return;
|
|
36960
|
+
if (this.spare && after === 0 && Date.now() - this.spare.madeAt >= SPARE_INVITE_MAX_AGE_MS) {
|
|
36961
|
+
for (const identity of [identityFromSeedB64(this.spare.mine.seedB64), this.spare.inviteKey]) this.warmedKeys.delete(identity.pubKeyZ32);
|
|
36962
|
+
this.spare = null;
|
|
36963
|
+
}
|
|
36208
36964
|
if (!this.spare) this.spare = this.makeSpare();
|
|
36209
36965
|
const spare = this.spare;
|
|
36210
36966
|
if (this.spareTimer) clearTimeout(this.spareTimer);
|
|
@@ -36220,10 +36976,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36220
36976
|
this.warmedKeys.set(identity.pubKeyZ32, Date.now());
|
|
36221
36977
|
this.warmKey(identity);
|
|
36222
36978
|
}
|
|
36979
|
+
const untilAged = spare.madeAt + SPARE_INVITE_MAX_AGE_MS - Date.now();
|
|
36223
36980
|
this.spareTimer = setTimeout(() => {
|
|
36224
36981
|
this.spareTimer = null;
|
|
36225
36982
|
if (this.spare === spare) this.prepareSpare();
|
|
36226
|
-
}, SPARE_INVITE_WARM_EVERY_MS);
|
|
36983
|
+
}, Math.min(SPARE_INVITE_WARM_EVERY_MS, untilAged));
|
|
36227
36984
|
}
|
|
36228
36985
|
async joinLink({ inviteCode }) {
|
|
36229
36986
|
const decoded = decodeInviteCode(inviteCode);
|
|
@@ -37448,11 +38205,18 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37448
38205
|
outbox = new Outbox({
|
|
37449
38206
|
read: () => db.getMessages(linkId),
|
|
37450
38207
|
update: async (id, delivery, error, extra) => {
|
|
37451
|
-
await db.
|
|
37452
|
-
|
|
37453
|
-
|
|
37454
|
-
|
|
37455
|
-
|
|
38208
|
+
if (delivery === "delivered") await db.patchMessage(linkId, id, (message) => message.delivery === "delivered" ? null : {
|
|
38209
|
+
...message.delivery && {
|
|
38210
|
+
...extra,
|
|
38211
|
+
delivery,
|
|
38212
|
+
deliveryError: error
|
|
38213
|
+
},
|
|
38214
|
+
details: {
|
|
38215
|
+
...message.details,
|
|
38216
|
+
receiptAt: Date.now()
|
|
38217
|
+
}
|
|
38218
|
+
}, true);
|
|
38219
|
+
else await db.updateDelivery(linkId, id, delivery, error, extra);
|
|
37456
38220
|
if (delivery === "sent" || delivery === "delivered") {
|
|
37457
38221
|
const message = await db.getMessage(linkId, id);
|
|
37458
38222
|
if (message) this.messageFeedback("sent", message);
|
|
@@ -37543,7 +38307,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37543
38307
|
}
|
|
37544
38308
|
/** Adds to a message's details record, whatever the row's delivery state. Nothing is pushed for it: the view asks. */
|
|
37545
38309
|
noteDetails(linkId, id, change) {
|
|
37546
|
-
return db.patchMessage(linkId, id, (message) => ({ details: change(message.details ?? {}, message) })).then(() => {}, () => {});
|
|
38310
|
+
return db.patchMessage(linkId, id, (message) => ({ details: change(message.details ?? {}, message) }), true).then(() => {}, () => {});
|
|
37547
38311
|
}
|
|
37548
38312
|
/** One send of a text on its details: the path, the frame and its size, and the DHT envelope when that was the way. */
|
|
37549
38313
|
noteTextSend(linkId, message, snapshot, at, error, link) {
|
|
@@ -38181,40 +38945,59 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38181
38945
|
const live = this.links.get(linkId);
|
|
38182
38946
|
if (!live?.link?.isDataLinkOpen) return;
|
|
38183
38947
|
const withTexts = this.outboxFor(linkId).sendsFiles;
|
|
38184
|
-
const
|
|
38948
|
+
const known = await this.waitingSendsOf(live), waiting = [];
|
|
38949
|
+
for (const id of [...known]) {
|
|
38950
|
+
const message = await db.getMessage(linkId, id);
|
|
38951
|
+
if (!message || !waitsForLive(message)) known.delete(id);
|
|
38952
|
+
else if (message.file && !withTexts || message.paymentId) waiting.push(message);
|
|
38953
|
+
}
|
|
38954
|
+
waiting.sort((a, b) => a.timestamp - b.timestamp);
|
|
38185
38955
|
if (waiting.some((m) => m.file) && !GhostlyNode.takesFiles(live.link)) await GhostlyNode.largeFilesAgreed(live.link);
|
|
38186
|
-
for (const message of waiting)
|
|
38187
|
-
if (
|
|
38188
|
-
|
|
38189
|
-
|
|
38190
|
-
|
|
38191
|
-
const file = message.file, wireId = file.id.slice(`${linkId}-out-`.length);
|
|
38192
|
-
await db.putMessage(sentNow(message));
|
|
38193
|
-
this.transferFile(live, file, wireId, message.timestamp, (error) => {
|
|
38194
|
-
this.transfers.set(file.id, {
|
|
38195
|
-
state: "failed",
|
|
38196
|
-
transferred: 0,
|
|
38197
|
-
size: file.size,
|
|
38198
|
-
error
|
|
38199
|
-
});
|
|
38200
|
-
this.emitState();
|
|
38201
|
-
}, GhostlyNode.wireReply(message));
|
|
38202
|
-
} else if (live.link.supportsPayments) {
|
|
38203
|
-
const at = Date.now();
|
|
38204
|
-
await db.putMessage({
|
|
38205
|
-
...sentNow(message),
|
|
38206
|
-
details: {
|
|
38207
|
-
...withSend(message.details, {
|
|
38208
|
-
at,
|
|
38209
|
-
...pathSnapshot(live, "datalink"),
|
|
38210
|
-
result: "sent"
|
|
38211
|
-
}),
|
|
38212
|
-
sentAt: at
|
|
38956
|
+
for (const message of waiting) {
|
|
38957
|
+
if (message.file) {
|
|
38958
|
+
if (!GhostlyNode.takesFiles(live.link)) {
|
|
38959
|
+
await db.updateDelivery(linkId, message.id, "failed", "Your contact's app cannot receive files.");
|
|
38960
|
+
continue;
|
|
38213
38961
|
}
|
|
38214
|
-
|
|
38215
|
-
|
|
38962
|
+
const file = message.file, wireId = file.id.slice(`${linkId}-out-`.length);
|
|
38963
|
+
await db.putMessage(sentNow(message));
|
|
38964
|
+
this.transferFile(live, file, wireId, message.timestamp, (error) => {
|
|
38965
|
+
this.transfers.set(file.id, {
|
|
38966
|
+
state: "failed",
|
|
38967
|
+
transferred: 0,
|
|
38968
|
+
size: file.size,
|
|
38969
|
+
error
|
|
38970
|
+
});
|
|
38971
|
+
this.emitState();
|
|
38972
|
+
}, GhostlyNode.wireReply(message));
|
|
38973
|
+
} else if (live.link.supportsPayments) {
|
|
38974
|
+
const at = Date.now();
|
|
38975
|
+
await db.putMessage({
|
|
38976
|
+
...sentNow(message),
|
|
38977
|
+
details: {
|
|
38978
|
+
...withSend(message.details, {
|
|
38979
|
+
at,
|
|
38980
|
+
...pathSnapshot(live, "datalink"),
|
|
38981
|
+
result: "sent"
|
|
38982
|
+
}),
|
|
38983
|
+
sentAt: at
|
|
38984
|
+
}
|
|
38985
|
+
});
|
|
38986
|
+
} else await db.updateDelivery(linkId, message.id, "failed", "Your contact's app cannot receive payment requests.");
|
|
38987
|
+
known.delete(message.id);
|
|
38988
|
+
}
|
|
38216
38989
|
if (waiting.length) await this.messagesChanged(linkId, waiting.map((m) => m.id));
|
|
38217
38990
|
}
|
|
38991
|
+
/**
|
|
38992
|
+
* The ids of my messages that wait for the live link (`waitsForLive`). A chat loaded at the start has them from that
|
|
38993
|
+
* read; any other reads its history once in a run. A session that opens then reads those rows, however long the chat.
|
|
38994
|
+
*/
|
|
38995
|
+
waitingSendsOf(live) {
|
|
38996
|
+
return live.waitingSends ??= db.getMessages(live.stored.id).then((history) => new Set(history.filter(waitsForLive).map((m) => m.id)), (error) => {
|
|
38997
|
+
live.waitingSends = void 0;
|
|
38998
|
+
throw error;
|
|
38999
|
+
});
|
|
39000
|
+
}
|
|
38218
39001
|
receiveFile(linkId, wire) {
|
|
38219
39002
|
const files = this.links.get(linkId)?.files;
|
|
38220
39003
|
if (!files) return "refused";
|
|
@@ -38662,8 +39445,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38662
39445
|
};
|
|
38663
39446
|
return this.groups.fileCheck(groupId);
|
|
38664
39447
|
}
|
|
38665
|
-
groupMessages({ groupId }) {
|
|
38666
|
-
return this.groups.messages(groupId);
|
|
39448
|
+
async groupMessages({ groupId }) {
|
|
39449
|
+
return (await this.groups.messages(groupId)).map(withoutFrame);
|
|
38667
39450
|
}
|
|
38668
39451
|
/**
|
|
38669
39452
|
* Presses a button of a message someone else sent to a chat or a group (`group:<id>`), WISP 406 · Message Buttons:
|
|
@@ -40434,8 +41217,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40434
41217
|
if (this.options.singleDevice) return;
|
|
40435
41218
|
const record = await readDeviceRecord(databaseName()).catch(() => null);
|
|
40436
41219
|
if (!record || record.state !== "active") return;
|
|
40437
|
-
const verifier = await this.handoffVerifier()
|
|
40438
|
-
if (verifier
|
|
41220
|
+
const verifier = await this.handoffVerifier();
|
|
41221
|
+
if (verifier ? JSON.stringify(verifier) !== JSON.stringify(record.verifier) : record.verifier) await amendDevice(databaseName(), { verifier: verifier ?? void 0 });
|
|
40439
41222
|
}
|
|
40440
41223
|
/**
|
|
40441
41224
|
* Copies the network settings into the device record (WISP 06, `devices/network.ts`), where device-link-only mode
|
|
@@ -40519,8 +41302,12 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40519
41302
|
this.emitState();
|
|
40520
41303
|
await recoverEnrollment(databaseName(), this.turnNetwork()).catch(() => false);
|
|
40521
41304
|
await this.syncDeviceVerifier().catch(() => {});
|
|
40522
|
-
const
|
|
40523
|
-
|
|
41305
|
+
const keeper = await this.deviceLinks.turnKeeper();
|
|
41306
|
+
const outcome = await keeper?.check(false);
|
|
41307
|
+
if (outcome?.kind === "go-on" && !outcome.restricted) {
|
|
41308
|
+
this.noteGoodTurn();
|
|
41309
|
+
await keeper?.stateVerifier().catch(() => null);
|
|
41310
|
+
}
|
|
40524
41311
|
if (outcome?.kind === "gated" && !this.shuttingDown) {
|
|
40525
41312
|
const record = await readDeviceRecord(databaseName());
|
|
40526
41313
|
if (record) await this.stopReplaced(viewOf(record));
|
|
@@ -40556,7 +41343,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40556
41343
|
if (knownDeviceGate()?.state !== "active") return;
|
|
40557
41344
|
const outcome = await links.checkTurn(false).catch(() => null);
|
|
40558
41345
|
if (this.shuttingDown || this.gatedOut) return;
|
|
40559
|
-
if (outcome?.kind === "go-on" && !outcome.restricted)
|
|
41346
|
+
if (outcome?.kind === "go-on" && !outcome.restricted) {
|
|
41347
|
+
this.noteGoodTurn();
|
|
41348
|
+
await this.stateDeviceVerifier();
|
|
41349
|
+
}
|
|
40560
41350
|
if (outcome?.kind === "gated") {
|
|
40561
41351
|
const record = await readDeviceRecord(databaseName()).catch(() => null);
|
|
40562
41352
|
if (record) await this.stopReplaced(viewOf(record));
|
|
@@ -40870,16 +41660,45 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40870
41660
|
if (this.options.singleDevice) return;
|
|
40871
41661
|
if (typeof password !== "string" || !password) throw new Error("A password is needed");
|
|
40872
41662
|
const stored = await this.handoffVerifier();
|
|
40873
|
-
|
|
41663
|
+
const active = knownDeviceGate()?.state === "active";
|
|
41664
|
+
const ready = await handoffPakeReady().then(() => true, () => false);
|
|
41665
|
+
if (stored && !ready) {
|
|
41666
|
+
if (password !== current) await this.dropHandoffVerifier();
|
|
41667
|
+
throw new Error("The password proof could not be loaded");
|
|
41668
|
+
}
|
|
41669
|
+
if (stored && active && !await provesHandoffPassword(stored, typeof current === "string" ? current : "")) throw new Error("handoff-password: The current password is wrong.");
|
|
41670
|
+
if (stored && await provesHandoffPassword(stored, password)) {
|
|
41671
|
+
await this.syncDeviceVerifier().catch(() => {});
|
|
41672
|
+
await this.stateDeviceVerifier();
|
|
41673
|
+
return;
|
|
41674
|
+
}
|
|
40874
41675
|
try {
|
|
40875
41676
|
const verifier = await makeHandoffVerifier(password);
|
|
40876
41677
|
await wrap((await store(STORES$1.settings, "readwrite")).put(verifier, GhostlyNode.HANDOFF_VERIFIER));
|
|
40877
41678
|
await this.syncDeviceVerifier().catch(() => {});
|
|
41679
|
+
await this.stateDeviceVerifier();
|
|
40878
41680
|
} catch (error) {
|
|
40879
|
-
await
|
|
41681
|
+
await this.dropHandoffVerifier();
|
|
40880
41682
|
throw error;
|
|
40881
41683
|
}
|
|
40882
41684
|
}
|
|
41685
|
+
/** The profile has no verifier any more: its settings, the device state, and what the turn record states. */
|
|
41686
|
+
async dropHandoffVerifier() {
|
|
41687
|
+
await wrap((await store(STORES$1.settings, "readwrite")).delete(GhostlyNode.HANDOFF_VERIFIER)).catch(() => {});
|
|
41688
|
+
await this.syncDeviceVerifier().catch(() => {});
|
|
41689
|
+
await this.stateDeviceVerifier();
|
|
41690
|
+
}
|
|
41691
|
+
/**
|
|
41692
|
+
* The turn record states which verifier the profile has now (WISP 06 § Record, the verifier mark), so a standby that
|
|
41693
|
+
* keeps an older one does not take over with the password it checks. Asked after the device state took the
|
|
41694
|
+
* verifier: at start, at each change, and after a good read of the turn (a change made offline is stated then).
|
|
41695
|
+
*/
|
|
41696
|
+
async stateDeviceVerifier() {
|
|
41697
|
+
if (this.options.singleDevice || this.shuttingDown || this.gatedOut) return;
|
|
41698
|
+
try {
|
|
41699
|
+
await (await this.deviceLinks?.turnKeeper())?.stateVerifier();
|
|
41700
|
+
} catch {}
|
|
41701
|
+
}
|
|
40883
41702
|
/**
|
|
40884
41703
|
* The wallets in a handoff to `taker` (WISP 06 § Wallets, `planHandoffWallets`): what moves, what stays home, and
|
|
40885
41704
|
* what keeps the profile here now. `view`: the wallets as they are now, read fresh by default.
|
|
@@ -40984,10 +41803,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40984
41803
|
}
|
|
40985
41804
|
await writeWalletHomes(plan.wallets);
|
|
40986
41805
|
const breezDatabases = [...new Set(plan.wallets.flatMap((w) => w.route === "moves" && w.breez && breez.has(w.id) ? [breez.get(w.id)] : []))];
|
|
40987
|
-
const verifier = await this.handoffVerifier().
|
|
41806
|
+
const verifier = await this.handoffVerifier().then((value) => ({ verifier: value ?? void 0 }), () => ({}));
|
|
40988
41807
|
await moveDevice(profile, "releasing", {
|
|
40989
41808
|
...patch,
|
|
40990
|
-
...verifier
|
|
41809
|
+
...verifier,
|
|
40991
41810
|
breezDatabases: breezDatabases.length ? breezDatabases : void 0
|
|
40992
41811
|
});
|
|
40993
41812
|
this.events.onDeviceGate?.({
|
|
@@ -41433,6 +42252,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41433
42252
|
});
|
|
41434
42253
|
live.presence = presence;
|
|
41435
42254
|
if (!entry && presence.nick && !live.link?.isDataLinkOpen) this.groups.edgeNick(group, peer, presence.nick);
|
|
42255
|
+
if (!entry) this.edgeRtcPresence(linkId, presence);
|
|
41436
42256
|
this.emitState();
|
|
41437
42257
|
},
|
|
41438
42258
|
onPairingState: (state) => {
|
|
@@ -41443,6 +42263,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41443
42263
|
this.directPath.note(peer, evidence);
|
|
41444
42264
|
if (!entry && evidence !== "open" && evidence !== "closed") this.edgeWithoutRtc(linkId);
|
|
41445
42265
|
},
|
|
42266
|
+
onAnswersRefused: () => {
|
|
42267
|
+
if (!entry) this.edgeWithoutRtc(linkId);
|
|
42268
|
+
},
|
|
41446
42269
|
onDataLinkState: (state) => {
|
|
41447
42270
|
traceJoin(group, `link.${state}`, { role });
|
|
41448
42271
|
if (state === "open" || live.dataLink === "open") live.lastSyncAt = Date.now();
|
|
@@ -41487,14 +42310,44 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41487
42310
|
* never went live, and the member stayed unreachable for as long as that network lasted. The edge starts again as on an
|
|
41488
42311
|
* app with no WebRTC (WISP 902 § Transports): it runs its native endpoints and says so in its packet (`_tr`, with no
|
|
41489
42312
|
* `webrtc/1`), the member's app starts its own for it as it does for a Linux Desktop, and the two meet over Iroh
|
|
41490
|
-
* through its relay.
|
|
42313
|
+
* through its relay. Until the member comes back (`edgeRtcPresence`), or for this run of the app.
|
|
41491
42314
|
*/
|
|
41492
42315
|
async edgeWithoutRtc(linkId) {
|
|
41493
|
-
const live = this.links.get(linkId)
|
|
41494
|
-
if (!live
|
|
42316
|
+
const live = this.links.get(linkId);
|
|
42317
|
+
if (!live?.link || !live.stored.group || live.stored.groupEntry || this.shuttingDown || this.options.webrtcGroupLinks) return;
|
|
41495
42318
|
if (this.edgeRtcOff.has(linkId) || this.keepsGroupNative(live.stored) || !Object.keys(this.nativeFactories).length) return;
|
|
41496
|
-
this.edgeRtcOff.
|
|
42319
|
+
this.edgeRtcOff.set(linkId, { away: false });
|
|
41497
42320
|
traceLink(live.myPubKeyZ32, "edge-rtc-off", {});
|
|
42321
|
+
await this.restartEdge(linkId);
|
|
42322
|
+
}
|
|
42323
|
+
/**
|
|
42324
|
+
* A member whose edge went native here (`edgeWithoutRtc`) is seen online again after a packet said it was gone: the
|
|
42325
|
+
* attempt that failed may have been its app quitting in the middle of it (a one-shot CLI command, a quick restart),
|
|
42326
|
+
* not a network that blocks direct connections. Held native for this whole run, the edge said so in its packet, the
|
|
42327
|
+
* member's app followed on its next starts, and past the group links' native slots members stayed unreachable for as
|
|
42328
|
+
* long as this app ran (2026-10-09). The edge tries WebRTC again, its `_tr` with `webrtc/1` again (WISP 902 §
|
|
42329
|
+
* Transports); a network that really blocks it costs one more failed attempt per return.
|
|
42330
|
+
*/
|
|
42331
|
+
edgeRtcPresence(linkId, presence) {
|
|
42332
|
+
const off = this.edgeRtcOff.get(linkId);
|
|
42333
|
+
if (!off) return;
|
|
42334
|
+
if (!presence.online) {
|
|
42335
|
+
off.away = true;
|
|
42336
|
+
return;
|
|
42337
|
+
}
|
|
42338
|
+
if (!off.away) return;
|
|
42339
|
+
const live = this.links.get(linkId);
|
|
42340
|
+
this.edgeRtcOff.delete(linkId);
|
|
42341
|
+
if (live) traceLink(live.myPubKeyZ32, "edge-rtc-back", {});
|
|
42342
|
+
this.groupNativeWaiting.delete(linkId);
|
|
42343
|
+
this.groupNativeFailed.delete(linkId);
|
|
42344
|
+
this.groupNativeRetryAt = 0;
|
|
42345
|
+
this.restartEdge(linkId);
|
|
42346
|
+
}
|
|
42347
|
+
/** An edge starts again, on what `keepsGroupNative` says now. */
|
|
42348
|
+
async restartEdge(linkId) {
|
|
42349
|
+
const live = this.links.get(linkId), link = live?.link;
|
|
42350
|
+
if (!live || !link) return;
|
|
41498
42351
|
live.link = null;
|
|
41499
42352
|
await link.stop(false).catch(() => {});
|
|
41500
42353
|
if (this.links.get(linkId) !== live || live.link || this.shuttingDown || !this.networkOn) return;
|
|
@@ -41613,6 +42466,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41613
42466
|
role: stored.inviteCode ? "inviter" : "joiner",
|
|
41614
42467
|
startedAt: stored.createdAt
|
|
41615
42468
|
} : void 0,
|
|
42469
|
+
madeAt: stored.createdAt,
|
|
41616
42470
|
transport: this.transport,
|
|
41617
42471
|
nick: this.sharedNick,
|
|
41618
42472
|
avatar: this.sharedAvatar,
|
|
@@ -41715,16 +42569,17 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41715
42569
|
const was = live.status, synced = live.lastSyncAt;
|
|
41716
42570
|
live.status = status;
|
|
41717
42571
|
if (status === "online") live.lastSyncAt = Date.now();
|
|
41718
|
-
if (status !== was || !synced || stored.group || linkId === this.activeLinkId) this.emitState();
|
|
42572
|
+
if (status !== was || !synced && status === "online" || stored.group || linkId === this.activeLinkId) this.emitState();
|
|
41719
42573
|
},
|
|
41720
42574
|
onPoll: ({ polling, nextInMs }) => {
|
|
41721
|
-
const was = live.poll.polling;
|
|
42575
|
+
const was = live.poll.polling, first = !live.lastSyncAt && !live.firstReadDone;
|
|
41722
42576
|
live.poll = {
|
|
41723
42577
|
polling,
|
|
41724
42578
|
nextAt: Date.now() + nextInMs,
|
|
41725
42579
|
interval: nextInMs || live.poll.interval
|
|
41726
42580
|
};
|
|
41727
|
-
if (
|
|
42581
|
+
if (was && !polling) live.firstReadDone = true;
|
|
42582
|
+
if (linkId === this.activeLinkId || first && polling !== was) this.emitState();
|
|
41728
42583
|
},
|
|
41729
42584
|
onMessageReceipt: async (id) => {
|
|
41730
42585
|
const oneToOne = !!stored.profile && !stored.group;
|
|
@@ -42006,7 +42861,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42006
42861
|
get nativeFactories() {
|
|
42007
42862
|
const relay = this.relaysHyperdht ? this.hyperdhtRelay : "";
|
|
42008
42863
|
return {
|
|
42009
|
-
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-
|
|
42864
|
+
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-C1p0nkle.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
|
|
42010
42865
|
...this.options.nativeTransports,
|
|
42011
42866
|
...this.ownIrohRelays ? { "iroh/1": (seedB64) => this.options.nativeTransports["iroh/1"](seedB64, this.settings.irohRelays?.length ? { relays: [...this.settings.irohRelays] } : {}) } : {},
|
|
42012
42867
|
...this.options.irohWeb ? { "iroh/1": (seedB64) => createIrohWebEndpoint(seedB64, { relays: this.irohRelays }) } : {}
|
|
@@ -42121,6 +42976,14 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42121
42976
|
this.emitState();
|
|
42122
42977
|
});
|
|
42123
42978
|
}
|
|
42979
|
+
/** The listeners this app may hold for a transport: sixteen for Iroh's browser build (`IROH_WEB_SLOTS`), eight for the others. */
|
|
42980
|
+
nativeSlots(key) {
|
|
42981
|
+
return key === "iroh/1" && this.options.irohWeb ? IROH_WEB_SLOTS : NATIVE_SLOTS;
|
|
42982
|
+
}
|
|
42983
|
+
/** Of those, what its groups' links may hold, all groups together. */
|
|
42984
|
+
groupNativeSlots(key) {
|
|
42985
|
+
return this.nativeSlots(key) - CHAT_NATIVE_SLOTS;
|
|
42986
|
+
}
|
|
42124
42987
|
/**
|
|
42125
42988
|
* One transport's listener for one chat, in that transport's queue. "no-slot" when a group's link found no slot free,
|
|
42126
42989
|
* "failed" when the listener could not start.
|
|
@@ -42134,11 +42997,13 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42134
42997
|
live.transportErrors ??= {};
|
|
42135
42998
|
try {
|
|
42136
42999
|
const owners = [...this.links.values()].filter((other) => other.link?.availableTransports.includes(key));
|
|
42137
|
-
|
|
42138
|
-
if (owners.length >=
|
|
43000
|
+
const slots = this.nativeSlots(key), groups = owners.filter((other) => other.stored.group).length;
|
|
43001
|
+
if (group && (owners.length >= slots || groups >= this.groupNativeSlots(key))) return "no-slot";
|
|
43002
|
+
const full = owners.length >= slots;
|
|
43003
|
+
if (full || !group && owners.length - groups >= NATIVE_SLOTS) {
|
|
42139
43004
|
const now = Date.now();
|
|
42140
43005
|
const taking = !group && (this.activeLinkId === linkId || inUse);
|
|
42141
|
-
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);
|
|
43006
|
+
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);
|
|
42142
43007
|
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];
|
|
42143
43008
|
const idleFor = idle ? now - idle.link.lastActivityAt : 0;
|
|
42144
43009
|
if (idle && await idle.link.yieldEndpoint(key, 12e4)) {
|
|
@@ -42159,7 +43024,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42159
43024
|
active: this.activeLinkId === linkId
|
|
42160
43025
|
});
|
|
42161
43026
|
if (this.activeLinkId === linkId && !group) this.retryActiveSlot(linkId);
|
|
42162
|
-
throw new Error("All
|
|
43027
|
+
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.");
|
|
42163
43028
|
} else {
|
|
42164
43029
|
await victim.link.releaseEndpoint(key);
|
|
42165
43030
|
victim.transportErrors ??= {};
|
|
@@ -42309,7 +43174,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42309
43174
|
...whole.file && {
|
|
42310
43175
|
file: whole.file,
|
|
42311
43176
|
text: whole.text
|
|
42312
|
-
}
|
|
43177
|
+
},
|
|
43178
|
+
...whole.frame && { frame: whole.frame }
|
|
42313
43179
|
};
|
|
42314
43180
|
if (!Object.keys(added).length) return;
|
|
42315
43181
|
if (!await db.patchMessage(message.linkId, message.id, (stored) => {
|
|
@@ -42335,12 +43201,12 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42335
43201
|
}
|
|
42336
43202
|
}
|
|
42337
43203
|
const onChanges = this.events.onMessageChanges;
|
|
42338
|
-
if (!onChanges) return this.events.onMessages(linkId, await
|
|
43204
|
+
if (!onChanges) return this.events.onMessages(linkId, await this.getMessages(linkId));
|
|
42339
43205
|
const unique = [...new Set(ids)];
|
|
42340
43206
|
if (!unique.length) return;
|
|
42341
43207
|
const rows = await Promise.all(unique.map((id) => db.getMessage(linkId, id)));
|
|
42342
43208
|
onChanges.call(this.events, linkId, {
|
|
42343
|
-
messages: rows.filter((row) => !!row),
|
|
43209
|
+
messages: rows.filter((row) => !!row).map(withoutFrame),
|
|
42344
43210
|
deleted: unique.filter((_, i) => !rows[i])
|
|
42345
43211
|
});
|
|
42346
43212
|
}
|
|
@@ -42435,6 +43301,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42435
43301
|
};
|
|
42436
43302
|
}
|
|
42437
43303
|
if (!await db.addMessage(message)) return false;
|
|
43304
|
+
if (live && waitsForLive(message)) live.waitingSends?.then((known) => known.add(message.id), () => {});
|
|
42438
43305
|
this.clockSample(message);
|
|
42439
43306
|
const newest = this.newestAt.get(message.linkId);
|
|
42440
43307
|
if (!message.event) this.newestAt.set(message.linkId, Math.max(newest ?? 0, writtenAt(message, this.clockLeads.get(message.linkId))));
|
|
@@ -42527,7 +43394,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42527
43394
|
lastMessageAt: live.lastMessageAt,
|
|
42528
43395
|
peerAck: live.peerAck,
|
|
42529
43396
|
lastSyncAt: live.lastSyncAt,
|
|
42530
|
-
poll: live.
|
|
43397
|
+
poll: live.firstReadDone && !live.lastSyncAt && stored.id !== this.activeLinkId ? {
|
|
43398
|
+
...live.poll,
|
|
43399
|
+
polling: false
|
|
43400
|
+
} : live.poll
|
|
42531
43401
|
};
|
|
42532
43402
|
}
|
|
42533
43403
|
/** What this chat can do right now: on the open session, or held for an away contact (text, files and requests only). */
|
|
@@ -42811,4 +43681,4 @@ function signalKind(signal) {
|
|
|
42811
43681
|
//#endregion
|
|
42812
43682
|
export { EngineServer, hyperdhtRelayProblem as n, server_exports as t };
|
|
42813
43683
|
|
|
42814
|
-
//# sourceMappingURL=server-
|
|
43684
|
+
//# sourceMappingURL=server-0zfNpABQ.js.map
|