@ghostlytools/cli 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{arkade-_gS9wShF.js → arkade-B-ALutmU.js} +2 -2
- package/dist/assets/{arkade-_gS9wShF.js.map → arkade-B-ALutmU.js.map} +1 -1
- package/dist/assets/{hyperdhtRelay-CgYLaQz3.js → hyperdhtRelay-hKC9DpIH.js} +3 -3
- package/dist/assets/{hyperdhtRelay-CgYLaQz3.js.map → hyperdhtRelay-hKC9DpIH.js.map} +1 -1
- package/dist/assets/{mainline-1v8OTYvp.js → mainline-DriRu_hb.js} +2 -2
- package/dist/assets/{mainline-1v8OTYvp.js.map → mainline-DriRu_hb.js.map} +1 -1
- package/dist/assets/{server-Cy4ox7oD.js → server-DHMXMwNn.js} +867 -142
- package/dist/assets/server-DHMXMwNn.js.map +1 -0
- package/dist/assets/{src-XzcHzneo.js → src-_FzJkeAJ.js} +187 -44
- package/dist/assets/src-_FzJkeAJ.js.map +1 -0
- package/dist/assets/{usdt-Deo3g0Nq.js → usdt-D0lO2pRR.js} +2 -2
- package/dist/assets/{usdt-Deo3g0Nq.js.map → usdt-D0lO2pRR.js.map} +1 -1
- package/dist/ghostly.mjs +245 -82
- 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-_FzJkeAJ.js";
|
|
3
3
|
import { a as clearProfileStores, c as fileStore, d as profileOpenFailure, f as store, i as STORES$1, l as newestWalletTx, m as wrap, n as APP_STORAGE_SCOPE_INDEX, o as databaseName, p as transact, r as CARD_INDEX, s as deleteDatabase, t as APP_STORAGE_CHAT_INDEX, u as openDb } from "./idb-CTF6LF5B.js";
|
|
4
4
|
import { a as fileBytesOf, c as removeFileBytes, i as fileBytes, l as FILE_BYTES_STEP, n as blobDigest } from "./fileBytes-BcYZhNgF.js";
|
|
5
5
|
import { $ as BACKUP_MEDIA_TYPE, A as requireTurn, At as paymentWireId, B as networkLabel, C as breezDatabasesFor, Ct as storedGroupFiles, D as sourceKey, Dt as replyRef, E as ProviderSources, Et as receivedPairedReply, F as sealSeed, Ft as streamStored, G as BITCOIN_INVOICE_ON_TESTNET, H as cashuMint, I as unsealSeed, J as fakesLightning, K as DEFAULT_MINTS, L as ModeChanged, M as setSingleWriterGate, Mt as readStored$1, N as intentRepository, Nt as removeStored, O as awayFrom, Ot as replyTo, P as newDeviceKey, Pt as storedSize, Q as paysItsOwnInvoices, R as ModeGate, S as breezDatabaseInUse, St as groupOfFile, T as lightningPaying, Tt as pairedWireReply, V as CASHU_MINT_SOURCE, W as WALLET_TYPES, X as isWorthlessMint, Y as isTestMint, Z as mintNetwork, _ as LightningCards, _t as verifyIdentity, a as irohRelayProblem, at as reserveAdapterIds, b as normalizePhrase, bt as groupFileId, c as createFailure, ct as SourceConfigError, d as paymentNetwork, dt as networkMode, et as MAX_BACKUP_BYTES, f as paymentNetworksOf, ft as redact, g as CASHU_CARD, gt as boundedIdentityFetch, ht as setOwnDidSource, i as createIrohWebEndpoint, it as registeredOnchainProviders, j as setAwayWallets, jt as FileAppender, k as refuseAway, kt as paymentAlias, l as createTiming, lt as SourceUnreachableError, mt as resolveAtprotoDid, n as loadFedimintSdk, nt as onAdaptersChanged, o as WALLET_NAMES, ot as NothingSpentError, p as walletInstances, pt as assertPublicServer, q as TEST_MINT, r as DEFAULT_IROH_RELAYS, rt as registeredLightningProviders, s as assertConfirmedReal, st as PROVIDER_PLATFORMS, t as fedimintDatabase, tt as identityProviders, u as crossNetwork, ut as isNothingSpentError, v as cardsKey, vt as chosenResolver, w as dropBreezDatabase, wt as groupReply, x as breezDatabase, xt as groupFileText, y as isRecoveryPhrase, yt as GroupFileDesk, z as WrongNetworkError } from "../ghostly.mjs";
|
|
@@ -180,7 +180,7 @@ async function migrateWalletNetworks(now = Date.now()) {
|
|
|
180
180
|
//#endregion
|
|
181
181
|
//#region ../browser/src/engine/paymentAdapters/usdtWallet.ts
|
|
182
182
|
/** The Ethereum libraries, a large part of the app: loaded once a USDT wallet is made, opened or restored, not with the engine. */
|
|
183
|
-
var loadUsdt = () => import("./usdt-
|
|
183
|
+
var loadUsdt = () => import("./usdt-D0lO2pRR.js");
|
|
184
184
|
/** Every new profile starts with this wallet: an address to receive on, nothing to set up. */
|
|
185
185
|
var DEFAULT_USDT = {
|
|
186
186
|
network: "ethereum",
|
|
@@ -642,7 +642,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-B-ALutmU.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 §
|
|
@@ -12132,13 +12140,14 @@ var Apps = class {
|
|
|
12132
12140
|
maxBytes: PEEK_BYTES,
|
|
12133
12141
|
peek: true
|
|
12134
12142
|
}));
|
|
12135
|
-
if (peek && newer(peek)) candidates.set(url, {});
|
|
12143
|
+
if (peek && newer(peek)) candidates.set(url, { holds: peek });
|
|
12136
12144
|
} catch {}
|
|
12137
12145
|
}
|
|
12138
12146
|
const revokeRead = /* @__PURE__ */ new Set();
|
|
12139
12147
|
const before = await this.readRevocations(app.ref, [app.from, ...app.manifest.sources ?? []], revokeRead);
|
|
12140
12148
|
app = withRevocations(app, before ?? []);
|
|
12141
12149
|
for (const [url, want] of candidates) {
|
|
12150
|
+
let holds = want.holds;
|
|
12142
12151
|
if (want.peek) {
|
|
12143
12152
|
let peek;
|
|
12144
12153
|
try {
|
|
@@ -12153,7 +12162,10 @@ var Apps = class {
|
|
|
12153
12162
|
if (want.miss) this.listingMisses.set(...want.miss);
|
|
12154
12163
|
continue;
|
|
12155
12164
|
}
|
|
12165
|
+
holds = peek;
|
|
12156
12166
|
}
|
|
12167
|
+
const twin = holds && (holds.sequence === app.sequence || holds.sequence === app.pending?.sequence);
|
|
12168
|
+
if (holds && !twin && this.runStatus(holds, this.storeViews(stores), app.revocations).status !== "ok") continue;
|
|
12157
12169
|
let fetched;
|
|
12158
12170
|
try {
|
|
12159
12171
|
fetched = await this.fetchBundle(url);
|
|
@@ -12435,7 +12447,7 @@ var Apps = class {
|
|
|
12435
12447
|
const id = `${ref}\0${scope}`;
|
|
12436
12448
|
let used = this.usage.get(id);
|
|
12437
12449
|
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
|
|
12450
|
+
used = (await wrap((await store(STORES$1.appStorage, "readonly")).index(APP_STORAGE_SCOPE_INDEX).getAll([ref, scope]))).reduce((sum, r) => sum + costOf(r), 0);
|
|
12439
12451
|
this.usage.set(id, used);
|
|
12440
12452
|
}
|
|
12441
12453
|
return used;
|
|
@@ -12449,7 +12461,11 @@ var Apps = class {
|
|
|
12449
12461
|
]));
|
|
12450
12462
|
return row ? { value: JSON.parse(row.value) } : null;
|
|
12451
12463
|
}
|
|
12452
|
-
/**
|
|
12464
|
+
/**
|
|
12465
|
+
* Keeps a JSON value under a key: at most 64 KiB, and 5 MiB for every key of this app in this scope, each counted
|
|
12466
|
+
* with `rowBytes`. A scope already past that (filled before a key counted `rowBytes`) still takes a write that does
|
|
12467
|
+
* not grow it, so the app can make room.
|
|
12468
|
+
*/
|
|
12453
12469
|
async storageSet({ ref, scope, key, value, digest }) {
|
|
12454
12470
|
const app = await this.runningOrFail(ref, digest);
|
|
12455
12471
|
const where = this.scopeOf(scope), name = this.keyOf(key);
|
|
@@ -12466,12 +12482,13 @@ var Apps = class {
|
|
|
12466
12482
|
await this.serial(`${app.ref}\0${where}`, async () => {
|
|
12467
12483
|
const used = await this.usageOf(app.ref, where);
|
|
12468
12484
|
const rows = await store(STORES$1.appStorage, "readonly");
|
|
12469
|
-
const
|
|
12485
|
+
const old = await wrap(rows.get([
|
|
12470
12486
|
app.ref,
|
|
12471
12487
|
where,
|
|
12472
12488
|
name
|
|
12473
|
-
]))
|
|
12474
|
-
|
|
12489
|
+
]));
|
|
12490
|
+
const next = used - (old ? costOf(old) : 0) + APP_STORAGE_LIMITS.rowBytes + size;
|
|
12491
|
+
if (next > APP_STORAGE_LIMITS.scopeBytes && next > used) fail("full", "This app's storage in this chat is full (5 MiB)");
|
|
12475
12492
|
const writing = await store(STORES$1.appStorage, "readwrite");
|
|
12476
12493
|
if (where !== "alone" && !this.host.isChat(where)) {
|
|
12477
12494
|
this.usage.delete(`${app.ref}\0${where}`);
|
|
@@ -12503,7 +12520,7 @@ var Apps = class {
|
|
|
12503
12520
|
where,
|
|
12504
12521
|
name
|
|
12505
12522
|
]));
|
|
12506
|
-
this.usage.set(`${app.ref}\0${where}`, used - old
|
|
12523
|
+
this.usage.set(`${app.ref}\0${where}`, used - costOf(old));
|
|
12507
12524
|
});
|
|
12508
12525
|
}
|
|
12509
12526
|
async storageKeys({ ref, scope, digest }) {
|
|
@@ -14058,6 +14075,23 @@ function takeoverTurn(record, read) {
|
|
|
14058
14075
|
if (highest + 1 > TURN_MAX) throw new TurnClosedError();
|
|
14059
14076
|
return highest + 1;
|
|
14060
14077
|
}
|
|
14078
|
+
/**
|
|
14079
|
+
* The `instance` of the record a device writes as the active one: it states the mark of the verifier beside its device
|
|
14080
|
+
* state (`turnInstance`, WISP 06 § Record), which on an active device is the profile's. `unstated`: a handoff's taker,
|
|
14081
|
+
* whose device state does not hold the verifier of the profile it just installed; it states it once its engine runs.
|
|
14082
|
+
*/
|
|
14083
|
+
function instanceFor(record, unstated = false) {
|
|
14084
|
+
return turnInstance(unstated ? null : turnVerifierMark(record.verifier ?? null));
|
|
14085
|
+
}
|
|
14086
|
+
/**
|
|
14087
|
+
* The last record of the device set this device knows after a read: the one the sources returned when it is above
|
|
14088
|
+
* the packet it stored, else the stored one. A lower record (`behind`) and a tombstone say nothing here.
|
|
14089
|
+
*/
|
|
14090
|
+
function latestKnown(read, stored) {
|
|
14091
|
+
const news = (read.result === "other" || read.result === "clone" || read.result === "mine") && read.record && !read.record.tombstone ? read.record : null;
|
|
14092
|
+
if (!news) return stored;
|
|
14093
|
+
return !stored || news.sequence >= stored.sequence ? news : stored;
|
|
14094
|
+
}
|
|
14061
14095
|
/** The record's slots as the device state keeps them. */
|
|
14062
14096
|
function deviceSetOf(record) {
|
|
14063
14097
|
return record.slots.map((slot) => slot && {
|
|
@@ -14253,7 +14287,7 @@ var TurnKeeper = class {
|
|
|
14253
14287
|
author: ownSlot,
|
|
14254
14288
|
active: ownSlot,
|
|
14255
14289
|
slots,
|
|
14256
|
-
instance:
|
|
14290
|
+
instance: this.options.instance?.() ?? instanceFor(record, !!options.release),
|
|
14257
14291
|
...release ? { release } : {}
|
|
14258
14292
|
};
|
|
14259
14293
|
const payload = await signTurnPacket(keys, fields, this.options.signer);
|
|
@@ -14274,6 +14308,31 @@ var TurnKeeper = class {
|
|
|
14274
14308
|
return this.put(held, payload, conditions, raising);
|
|
14275
14309
|
}
|
|
14276
14310
|
/**
|
|
14311
|
+
* The active device says which verifier the profile has now (WISP 06 § Record, the verifier mark): when the record it
|
|
14312
|
+
* stored states another one (the lock password changed, or the record was written before the verifier was at hand),
|
|
14313
|
+
* it reads, and on `mine` or `none` writes its next record. True when the stored record states it (already, or now
|
|
14314
|
+
* written); false when nothing was written (not the active device, no good read, another device's record there: the
|
|
14315
|
+
* next check or start settles that, and every start writes a record anyway). Null for a `single` profile.
|
|
14316
|
+
*/
|
|
14317
|
+
stateVerifier() {
|
|
14318
|
+
return this.exclusive(async () => {
|
|
14319
|
+
const held = await this.held();
|
|
14320
|
+
if (!held) return null;
|
|
14321
|
+
if (held.record.state !== "active" || !held.wrote || !held.storedRecord) return false;
|
|
14322
|
+
const stated = turnStatedVerifier(held.storedRecord.instance);
|
|
14323
|
+
if (stated && bytesEqual(stated, turnVerifierMark(held.record.verifier ?? null))) return true;
|
|
14324
|
+
const read = await this.readWith(held);
|
|
14325
|
+
if (!read.good || read.result !== "mine" && read.result !== "none") return false;
|
|
14326
|
+
try {
|
|
14327
|
+
await this.writeWith(held, conditionsAfter(read, read.result === "none" ? "none" : "seen"));
|
|
14328
|
+
} catch (error) {
|
|
14329
|
+
if (error instanceof TurnClosedError) return false;
|
|
14330
|
+
throw error;
|
|
14331
|
+
}
|
|
14332
|
+
return true;
|
|
14333
|
+
});
|
|
14334
|
+
}
|
|
14335
|
+
/**
|
|
14277
14336
|
* A forced takeover (WISP 06 § Settle): reads the turn, makes the device `taking` (with the state it had, to go
|
|
14278
14337
|
* back to), writes the record of `turn` with no release within `P` of that read, and settles as every device that
|
|
14279
14338
|
* raises the turn does. `start` says it is the active device now; `gated` that it lost and is what it was before.
|
|
@@ -14287,6 +14346,7 @@ var TurnKeeper = class {
|
|
|
14287
14346
|
if (prior !== "standby" && prior !== "superseded") throw new Error(`A ${prior} device does not force a takeover`);
|
|
14288
14347
|
const read = await this.readWith(held, true);
|
|
14289
14348
|
if (!read.good || read.result === "tombstone") return this.settle(held, read, true);
|
|
14349
|
+
options.before?.(latestKnown(read, held.storedRecord));
|
|
14290
14350
|
const turn = options.turn ?? takeoverTurn(held.record, read);
|
|
14291
14351
|
const stopping = held.record.activeSlot !== void 0 && held.record.activeSlot !== held.ownSlot ? held.record.deviceSet[held.record.activeSlot]?.key : void 0;
|
|
14292
14352
|
held.record = await this.store.move(this.options.profile, "taking", { handoff: {
|
|
@@ -14296,8 +14356,8 @@ var TurnKeeper = class {
|
|
|
14296
14356
|
} });
|
|
14297
14357
|
try {
|
|
14298
14358
|
await this.writeWith(held, read.conditions, {
|
|
14299
|
-
|
|
14300
|
-
|
|
14359
|
+
turn,
|
|
14360
|
+
...options.slots ? { slots: options.slots } : {}
|
|
14301
14361
|
}, true);
|
|
14302
14362
|
} catch (error) {
|
|
14303
14363
|
if (!held.wroteNow) held.record = await this.store.move(this.options.profile, prior, {
|
|
@@ -15066,7 +15126,7 @@ var EnrollInviter = class extends Enrollment {
|
|
|
15066
15126
|
rev: 0,
|
|
15067
15127
|
author: 0,
|
|
15068
15128
|
active: 0,
|
|
15069
|
-
instance:
|
|
15129
|
+
instance: turnInstance(),
|
|
15070
15130
|
slots: granted.slots.map((slot) => slot && {
|
|
15071
15131
|
key: slot.key,
|
|
15072
15132
|
name: slot.name
|
|
@@ -15844,7 +15904,7 @@ async function planSetMove(record, signer, options = {}) {
|
|
|
15844
15904
|
author: record.ownSlot,
|
|
15845
15905
|
active: record.ownSlot,
|
|
15846
15906
|
slots: turnSlots,
|
|
15847
|
-
instance:
|
|
15907
|
+
instance: turnInstance(turnVerifierMark(record.verifier ?? null), random)
|
|
15848
15908
|
}, sign);
|
|
15849
15909
|
const update = {
|
|
15850
15910
|
d,
|
|
@@ -16067,6 +16127,21 @@ function takeoverTarget(record) {
|
|
|
16067
16127
|
function canTakeOver(record) {
|
|
16068
16128
|
return !!record && (record.state === "standby" || record.state === "superseded") && !!record.copy;
|
|
16069
16129
|
}
|
|
16130
|
+
/**
|
|
16131
|
+
* Whether the verifier this standby keeps is still the profile's, by the last record of the device set it knows (the
|
|
16132
|
+
* one just read, or the one it stored): false only when that record states the mark of another verifier. A record that
|
|
16133
|
+
* states none (an app from before wrote it) leaves the device with what it has, and so does this device's own record.
|
|
16134
|
+
* Only a standby is held to it: a device replaced by a takeover it did not want takes the turn back with the password
|
|
16135
|
+
* it knows, whatever the device that replaced it states.
|
|
16136
|
+
*/
|
|
16137
|
+
function verifierStands(record, latest) {
|
|
16138
|
+
if (record.state !== "standby" || !record.verifier || !latest || latest.tombstone) return true;
|
|
16139
|
+
const own = record.ownSlot === void 0 ? void 0 : record.deviceSet[record.ownSlot]?.key;
|
|
16140
|
+
const author = latest.slots[latest.author];
|
|
16141
|
+
if (own && author && bytesEqual(author.key, fromBase64Url(own))) return true;
|
|
16142
|
+
const stated = turnStatedVerifier(latest.instance);
|
|
16143
|
+
return !stated || bytesEqual(stated, turnVerifierMark(record.verifier));
|
|
16144
|
+
}
|
|
16070
16145
|
var sameName = (a, b) => a.normalize("NFC").trim().toLocaleLowerCase() === b.normalize("NFC").trim().toLocaleLowerCase();
|
|
16071
16146
|
/**
|
|
16072
16147
|
* Checks the request and forces the takeover. Resolves to the keeper's outcome: `start` when this device is the active
|
|
@@ -16093,7 +16168,9 @@ async function forceTakeover(ports, request) {
|
|
|
16093
16168
|
if (record.takeoverAttempts && (record.takeoverAttempts.total > 0 || record.takeoverAttempts.recent.length)) await ports.amend({ takeoverAttempts: handoffAttemptSucceeded() });
|
|
16094
16169
|
const keeper = await ports.keeper();
|
|
16095
16170
|
if (!keeper) throw new TakeoverRefusal("offline", "Go online to take over.");
|
|
16096
|
-
const outcome = await keeper.raise()
|
|
16171
|
+
const outcome = await keeper.raise({ before: (latest) => {
|
|
16172
|
+
if (!verifierStands(record, latest)) throw new TakeoverRefusal("changed", `The lock password was changed${target ? ` on ${target}` : ""}. Use here moves the profile with the new one.`);
|
|
16173
|
+
} });
|
|
16097
16174
|
if (!outcome) throw new TakeoverRefusal("state", "This profile has no device set.");
|
|
16098
16175
|
return outcome;
|
|
16099
16176
|
}
|
|
@@ -16182,7 +16259,7 @@ async function planOwnSet(record, ports, under) {
|
|
|
16182
16259
|
author: record.ownSlot,
|
|
16183
16260
|
active: record.ownSlot,
|
|
16184
16261
|
slots,
|
|
16185
|
-
instance:
|
|
16262
|
+
instance: turnInstance(turnVerifierMark(record.verifier ?? null), random)
|
|
16186
16263
|
}, ports.signer);
|
|
16187
16264
|
return {
|
|
16188
16265
|
d: toBase64Url(d),
|
|
@@ -16420,6 +16497,10 @@ function isHandoffVerifier(value) {
|
|
|
16420
16497
|
const v = value;
|
|
16421
16498
|
return !!v && typeof v === "object" && v.v === 1 && typeof v.setup === "string" && B64URL.test(v.setup) && typeof v.record === "string" && B64URL.test(v.record);
|
|
16422
16499
|
}
|
|
16500
|
+
/** Loads the password proof. Throws when it cannot be loaded: nothing can then be made or checked (`provesHandoffPassword` says false). */
|
|
16501
|
+
async function handoffPakeReady() {
|
|
16502
|
+
await opaque();
|
|
16503
|
+
}
|
|
16423
16504
|
/** A verifier for this password: both halves of the registration, run on this device. */
|
|
16424
16505
|
async function makeHandoffVerifier(password) {
|
|
16425
16506
|
if (!password) throw new Error("A password is needed");
|
|
@@ -19985,6 +20066,14 @@ function forgetWallet(records, id) {
|
|
|
19985
20066
|
return rest;
|
|
19986
20067
|
}
|
|
19987
20068
|
//#endregion
|
|
20069
|
+
//#region ../browser/src/shared/messageChanges.ts
|
|
20070
|
+
/** A row as a page gets it: without the frame a private group's message came in (`StoredMessage.frame`), which only the engine reads. */
|
|
20071
|
+
function withoutFrame(message) {
|
|
20072
|
+
if (message.frame === void 0) return message;
|
|
20073
|
+
const { frame: _frame, ...row } = message;
|
|
20074
|
+
return row;
|
|
20075
|
+
}
|
|
20076
|
+
//#endregion
|
|
19988
20077
|
//#region ../browser/src/shared/buttons.ts
|
|
19989
20078
|
/** The buttons of a message, when it has any. */
|
|
19990
20079
|
function messageButtons(message) {
|
|
@@ -21241,6 +21330,8 @@ function newestMessages(index, query, count) {
|
|
|
21241
21330
|
};
|
|
21242
21331
|
});
|
|
21243
21332
|
}
|
|
21333
|
+
/** A write that waits for no disk sync of its own (in a browser, the engine's default already; on Node, the CLI's store). */
|
|
21334
|
+
var RELAXED = { durability: "relaxed" };
|
|
21244
21335
|
/**
|
|
21245
21336
|
* Durable local state of the browser peer. Identity seeds live here the same
|
|
21246
21337
|
* way Desktop keeps them in its WebView storage: local to this profile, never
|
|
@@ -21473,12 +21564,13 @@ var db = {
|
|
|
21473
21564
|
await wrap((await store(STORES$1.messages, "readwrite")).put(message));
|
|
21474
21565
|
},
|
|
21475
21566
|
async updateDelivery(linkId, id, delivery, deliveryError, extra) {
|
|
21476
|
-
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite");
|
|
21567
|
+
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite", delivery === "sent" || delivery === "delivered" ? RELAXED : void 0);
|
|
21477
21568
|
const messages = tx.objectStore(STORES$1.messages);
|
|
21478
21569
|
const request = messages.get([linkId, id]);
|
|
21479
21570
|
request.onsuccess = () => {
|
|
21480
21571
|
const message = request.result;
|
|
21481
21572
|
if (!message?.delivery || message.delivery === "delivered") return;
|
|
21573
|
+
if (message.delivery === delivery && message.deliveryError === deliveryError && Object.entries(extra ?? {}).every(([key, value]) => message[key] === value)) return;
|
|
21482
21574
|
messages.put({
|
|
21483
21575
|
...message,
|
|
21484
21576
|
...extra,
|
|
@@ -21494,10 +21586,11 @@ var db = {
|
|
|
21494
21586
|
/**
|
|
21495
21587
|
* Changes one message row in place, in one transaction: `change` sees the row as stored and gives the fields to
|
|
21496
21588
|
* 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.
|
|
21589
|
+
* (the message's details record), on rows in any state. `relaxed`: for what can be made again or is only a note
|
|
21590
|
+
* (a receipt's mark, the details record), written with no disk sync of its own.
|
|
21498
21591
|
*/
|
|
21499
|
-
async patchMessage(linkId, id, change) {
|
|
21500
|
-
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite");
|
|
21592
|
+
async patchMessage(linkId, id, change, relaxed = false) {
|
|
21593
|
+
const tx = (await openDb()).transaction(STORES$1.messages, "readwrite", relaxed ? RELAXED : void 0);
|
|
21501
21594
|
const messages = tx.objectStore(STORES$1.messages);
|
|
21502
21595
|
const request = messages.get([linkId, id]);
|
|
21503
21596
|
let result;
|
|
@@ -21643,6 +21736,10 @@ var Reactions = class {
|
|
|
21643
21736
|
queue = Promise.resolve();
|
|
21644
21737
|
/** A group member's reactions taken per window (chat and member key), as a paired chat's session limits them. */
|
|
21645
21738
|
memberPace = /* @__PURE__ */ new Map();
|
|
21739
|
+
/** Per group chat, per member: its reactions past the window, by message, waiting for room (`hold`). */
|
|
21740
|
+
held = /* @__PURE__ */ new Map();
|
|
21741
|
+
heldTimers = /* @__PURE__ */ new Map();
|
|
21742
|
+
stopped = false;
|
|
21646
21743
|
/**
|
|
21647
21744
|
* This side's highest number per chat: read from its history the first time I react in it, then each number given
|
|
21648
21745
|
* out. It only has to grow, so a row deleted since leaves it too high and nothing is wrong.
|
|
@@ -21691,14 +21788,19 @@ var Reactions = class {
|
|
|
21691
21788
|
/**
|
|
21692
21789
|
* A reaction that came from `by` (`peer`, or a member's key), already authenticated as theirs. A message not here
|
|
21693
21790
|
* yet keeps it for five minutes; past the room for that sender, it is dropped (and, on a 1:1 session, not confirmed).
|
|
21791
|
+
* Past `REACTION_LIMITS.receive` a window from one group member, the rest wait for the next window (`hold`): a
|
|
21792
|
+
* community's catch-up hands back every reaction of a member at once, and one dropped is never handed again.
|
|
21694
21793
|
*/
|
|
21695
21794
|
receive(chat, by, reaction) {
|
|
21696
21795
|
if (isGroup$1(chat)) {
|
|
21697
21796
|
const key = `${chat}\n${by}`;
|
|
21698
21797
|
let pace = this.memberPace.get(key);
|
|
21699
21798
|
if (!pace) this.memberPace.set(key, pace = new ReactionWindow(REACTION_LIMITS.receive, REACTION_LIMITS.windowMs, () => this.now()));
|
|
21700
|
-
if (!pace.take()) return Promise.resolve(
|
|
21799
|
+
if (this.held.get(chat)?.has(by) || !pace.take()) return Promise.resolve(this.hold(chat, by, reaction, pace));
|
|
21701
21800
|
}
|
|
21801
|
+
return this.take(chat, by, reaction);
|
|
21802
|
+
}
|
|
21803
|
+
take(chat, by, reaction) {
|
|
21702
21804
|
return this.run(async () => {
|
|
21703
21805
|
const target = await this.find(chat, reaction.id);
|
|
21704
21806
|
if (!target) return this.wait(chat, by, reaction);
|
|
@@ -21707,6 +21809,75 @@ var Reactions = class {
|
|
|
21707
21809
|
return "applied";
|
|
21708
21810
|
});
|
|
21709
21811
|
}
|
|
21812
|
+
/**
|
|
21813
|
+
* A reaction past what a group member's window takes: kept, the highest number per message, `REACTION_LIMITS.held` a
|
|
21814
|
+
* group at most (the oldest go first), and taken when the window has room. The work stays what the window allows.
|
|
21815
|
+
*/
|
|
21816
|
+
hold(chat, by, reaction, pace) {
|
|
21817
|
+
let group = this.held.get(chat);
|
|
21818
|
+
if (!group) this.held.set(chat, group = /* @__PURE__ */ new Map());
|
|
21819
|
+
let waiting = group.get(by);
|
|
21820
|
+
if (!waiting) group.set(by, waiting = /* @__PURE__ */ new Map());
|
|
21821
|
+
const known = waiting.get(reaction.id);
|
|
21822
|
+
if (known && known.n >= reaction.n) return "stale";
|
|
21823
|
+
waiting.delete(reaction.id);
|
|
21824
|
+
waiting.set(reaction.id, reaction);
|
|
21825
|
+
let count = 0;
|
|
21826
|
+
for (const member of group.values()) count += member.size;
|
|
21827
|
+
for (const [member, reactions] of group) {
|
|
21828
|
+
if (count <= REACTION_LIMITS.held) break;
|
|
21829
|
+
while (count > REACTION_LIMITS.held && reactions.size) {
|
|
21830
|
+
reactions.delete(reactions.keys().next().value);
|
|
21831
|
+
count--;
|
|
21832
|
+
}
|
|
21833
|
+
if (!reactions.size && member !== by) group.delete(member);
|
|
21834
|
+
}
|
|
21835
|
+
this.takeLater(chat, by, pace);
|
|
21836
|
+
return waiting.has(reaction.id) ? "waiting" : "dropped";
|
|
21837
|
+
}
|
|
21838
|
+
takeLater(chat, by, pace) {
|
|
21839
|
+
const key = `${chat}\n${by}`;
|
|
21840
|
+
if (this.heldTimers.has(key) || this.stopped) return;
|
|
21841
|
+
this.heldTimers.set(key, setTimeout(() => {
|
|
21842
|
+
this.takeHeld(chat, by).catch(() => {});
|
|
21843
|
+
}, Math.max(50, pace.wait())));
|
|
21844
|
+
}
|
|
21845
|
+
/** The window of a member has room again: its reactions that waited are taken, oldest first, as many as it allows. */
|
|
21846
|
+
async takeHeld(chat, by) {
|
|
21847
|
+
const key = `${chat}\n${by}`;
|
|
21848
|
+
this.heldTimers.delete(key);
|
|
21849
|
+
const group = this.held.get(chat), waiting = group?.get(by), pace = this.memberPace.get(key);
|
|
21850
|
+
if (this.stopped || !group || !waiting || !pace) return;
|
|
21851
|
+
for (const [id, reaction] of [...waiting]) {
|
|
21852
|
+
if (this.host.believed && !this.host.believed(chat, by)) {
|
|
21853
|
+
waiting.clear();
|
|
21854
|
+
break;
|
|
21855
|
+
}
|
|
21856
|
+
if (!pace.take()) break;
|
|
21857
|
+
if (waiting.get(id) !== reaction) continue;
|
|
21858
|
+
waiting.delete(id);
|
|
21859
|
+
await this.take(chat, by, reaction).catch(() => {});
|
|
21860
|
+
}
|
|
21861
|
+
if (waiting.size) {
|
|
21862
|
+
this.takeLater(chat, by, pace);
|
|
21863
|
+
return;
|
|
21864
|
+
}
|
|
21865
|
+
if (group.get(by) === waiting) group.delete(by);
|
|
21866
|
+
if (!group.size) this.held.delete(chat);
|
|
21867
|
+
}
|
|
21868
|
+
/** The engine stops: what waits for a window is let go, and no timer is left. */
|
|
21869
|
+
stop() {
|
|
21870
|
+
this.stopped = true;
|
|
21871
|
+
this.dropHeld();
|
|
21872
|
+
}
|
|
21873
|
+
dropHeld(chat) {
|
|
21874
|
+
for (const [key, timer] of this.heldTimers) if (chat === void 0 || key.startsWith(`${chat}\n`)) {
|
|
21875
|
+
clearTimeout(timer);
|
|
21876
|
+
this.heldTimers.delete(key);
|
|
21877
|
+
}
|
|
21878
|
+
if (chat === void 0) this.held.clear();
|
|
21879
|
+
else this.held.delete(chat);
|
|
21880
|
+
}
|
|
21710
21881
|
/** A message was kept: the reactions that waited for it are shown now. */
|
|
21711
21882
|
stored(message) {
|
|
21712
21883
|
const list = this.waiting.get(message.linkId);
|
|
@@ -21744,6 +21915,7 @@ var Reactions = class {
|
|
|
21744
21915
|
}
|
|
21745
21916
|
/** A chat is gone: nothing waits for it any more, and my highest number in it is read again if it comes back. */
|
|
21746
21917
|
forget(chat) {
|
|
21918
|
+
this.dropHeld(chat);
|
|
21747
21919
|
this.waiting.delete(chat);
|
|
21748
21920
|
this.last.delete(chat);
|
|
21749
21921
|
}
|
|
@@ -22179,22 +22351,23 @@ var Communities = class {
|
|
|
22179
22351
|
invited: [],
|
|
22180
22352
|
memberLinks: {}
|
|
22181
22353
|
};
|
|
22182
|
-
|
|
22354
|
+
const invitation = this.knocking(group, live) ? {
|
|
22355
|
+
linkId: group.joining.linkId,
|
|
22356
|
+
contact: "",
|
|
22357
|
+
admin: group.joining.inviter,
|
|
22358
|
+
members: 0,
|
|
22359
|
+
accepted: true,
|
|
22360
|
+
viaLink: true,
|
|
22361
|
+
stage: this.joinStage(group),
|
|
22362
|
+
...this.knockingWith.get(group.id) ? { waiting: this.knockingWith.get(group.id) } : {}
|
|
22363
|
+
} : void 0;
|
|
22364
|
+
if (invitation && !this.keptWhileKnocking(live)) return [{
|
|
22183
22365
|
...base,
|
|
22184
22366
|
name: groupName(group.joining.name) ?? live?.session.name ?? "",
|
|
22185
22367
|
isAdmin: false,
|
|
22186
22368
|
members: [],
|
|
22187
22369
|
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
|
-
}
|
|
22370
|
+
invitation
|
|
22198
22371
|
}];
|
|
22199
22372
|
if (!live) return [];
|
|
22200
22373
|
const s = live.session, edges = this.host.edges(group.id);
|
|
@@ -22219,6 +22392,7 @@ var Communities = class {
|
|
|
22219
22392
|
}) } : {},
|
|
22220
22393
|
canSend: s.canSend,
|
|
22221
22394
|
...s.picture ? { picture: s.picture } : {},
|
|
22395
|
+
...invitation ? { invitation } : {},
|
|
22222
22396
|
community: {
|
|
22223
22397
|
hub: live.hub,
|
|
22224
22398
|
hubs: hubs.length,
|
|
@@ -22286,6 +22460,13 @@ var Communities = class {
|
|
|
22286
22460
|
knocking(group, live) {
|
|
22287
22461
|
return !!group.joining && (!live || live.session.status !== "active");
|
|
22288
22462
|
}
|
|
22463
|
+
/**
|
|
22464
|
+
* Someone removed keeps the group as it was here while it knocks to get back in, and when it gives up (`decline`).
|
|
22465
|
+
* Not a session whose admission lost a race: that knock is still its join.
|
|
22466
|
+
*/
|
|
22467
|
+
keptWhileKnocking(live) {
|
|
22468
|
+
return live?.session.status === "removed";
|
|
22469
|
+
}
|
|
22289
22470
|
/** `check`: a member, as far as this device knows, asking the door whether it still is (`joinByLink`). */
|
|
22290
22471
|
async startJoining(link, seedB64, since, check = false) {
|
|
22291
22472
|
this.lastKnock.delete(link.g);
|
|
@@ -22457,11 +22638,14 @@ var Communities = class {
|
|
|
22457
22638
|
}
|
|
22458
22639
|
/**
|
|
22459
22640
|
* A join through the link that no member answered yet, given up: listed as an invitation, it is declined as one.
|
|
22460
|
-
*
|
|
22641
|
+
* Someone removed who knocked to get back in only stops knocking: the group and its history stay here, as a private
|
|
22642
|
+
* group's do for someone removed, invited again, who declines. A member trying to get back in (its session lost)
|
|
22643
|
+
* leaves instead, which keeps nothing.
|
|
22461
22644
|
*/
|
|
22462
22645
|
async decline(groupId) {
|
|
22463
22646
|
const group = this.stored.get(groupId), live = this.live.get(groupId);
|
|
22464
22647
|
if (!group || !this.knocking(group, live)) throw new Error("No invitation to decline");
|
|
22648
|
+
if (this.keptWhileKnocking(live)) return this.endCheck(group);
|
|
22465
22649
|
if (live) throw new Error("You were in this group: leave it to stop trying to get back in");
|
|
22466
22650
|
await this.forget(groupId);
|
|
22467
22651
|
}
|
|
@@ -24609,8 +24793,8 @@ function eventTime(taken, groupId, timestamp) {
|
|
|
24609
24793
|
var arrivalNow = (tick) => Math.max(tick, Date.now());
|
|
24610
24794
|
/**
|
|
24611
24795
|
* 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
|
|
24796
|
+
* saved with the session soon after (a message taken moves its state, saved within `GROUP_LIMITS.saveWithinMs` and when
|
|
24797
|
+
* the app stops), so a message handed to me late still makes the group unread after the app starts again. Read from the history alone, by its own time, it sorted among what I
|
|
24614
24798
|
* had read and the mark was gone on the next start.
|
|
24615
24799
|
*/
|
|
24616
24800
|
function noteCame(group, came, peer, mentioned) {
|
|
@@ -24661,6 +24845,8 @@ var ENTRY_RELEASE_MS = 2e3;
|
|
|
24661
24845
|
var REMOVED_LINGER_MS = 15e3;
|
|
24662
24846
|
/** How long the tombstone of a group I left waits for the admin to hear it. */
|
|
24663
24847
|
var LEFT_KEPT_MS = 6048e5;
|
|
24848
|
+
/** Admin side: how long a contact invited over its chat is waited for. After, it holds no place and may be invited again. */
|
|
24849
|
+
var INVITED_KEPT_MS = LEFT_KEPT_MS;
|
|
24664
24850
|
/**
|
|
24665
24851
|
* Invitee side: an accept the admin has not answered with a welcome is said again this often while the inviter's chat
|
|
24666
24852
|
* 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 +24867,13 @@ function invitationClaims(frame) {
|
|
|
24681
24867
|
var FAREWELL_ANSWERS = 3;
|
|
24682
24868
|
/** Commits sent to a removed member that was behind when it left, at most. */
|
|
24683
24869
|
var FAREWELL_COMMITS = 64;
|
|
24870
|
+
/**
|
|
24871
|
+
* A member whose messages were just asked of another (`Groups.askOf`) is told where I am only once that one's answer
|
|
24872
|
+
* has come: no message from it for this long, or `MESH_SYNC_HOLD_MAX_MS` after it was asked. Told at once, its own
|
|
24873
|
+
* answer raced the other's, which holds the same messages among everyone's in the order they were written, and a
|
|
24874
|
+
* member back in a group of five read each member's lines apart, the newest above older ones (WISP 902 § Catch-up).
|
|
24875
|
+
*/
|
|
24876
|
+
var MESH_SYNC_HOLD_MS = 1500;
|
|
24684
24877
|
/** Where a member's gossip turns start among the members it is connected to: a number of its own, from its key. */
|
|
24685
24878
|
var gossipStart = (key) => {
|
|
24686
24879
|
let n = 0;
|
|
@@ -24738,6 +24931,8 @@ var Groups = class {
|
|
|
24738
24931
|
timings;
|
|
24739
24932
|
stored = /* @__PURE__ */ new Map();
|
|
24740
24933
|
sessions = /* @__PURE__ */ new Map();
|
|
24934
|
+
/** Per private group, the rows that hold the frame their message came in (`StoredMessage.frame`), oldest first, and what each counts for. */
|
|
24935
|
+
framesKept = /* @__PURE__ */ new Map();
|
|
24741
24936
|
/** Contacts (chat ids, kept in `StoredGroup.invited`) and entry sessions I invited to each group, until they answer. */
|
|
24742
24937
|
invited = /* @__PURE__ */ new Map();
|
|
24743
24938
|
/** Invitee side: when my accept of each invitation was last said, how many times since the inviter's chat came up, and whether it was down. */
|
|
@@ -24783,6 +24978,13 @@ var Groups = class {
|
|
|
24783
24978
|
* time, not of everyone whose edge opens, so a member coming back is not sent the same messages by every other.
|
|
24784
24979
|
*/
|
|
24785
24980
|
relayAsked = /* @__PURE__ */ new Map();
|
|
24981
|
+
/**
|
|
24982
|
+
* Per mesh group, the members whose edge opened while their messages were being handed on by another (`holdSync`):
|
|
24983
|
+
* whom, and when it was asked. Their sync goes once that answer has come.
|
|
24984
|
+
*/
|
|
24985
|
+
syncHeld = /* @__PURE__ */ new Map();
|
|
24986
|
+
/** Per mesh group and member, when a message last came over its edge (its own, or one it handed on). */
|
|
24987
|
+
lastHanded = /* @__PURE__ */ new Map();
|
|
24786
24988
|
lastGossip = /* @__PURE__ */ new Map();
|
|
24787
24989
|
/**
|
|
24788
24990
|
* Per mesh group, each reachable member some of whose messages never reached me (`GroupSession.lacking`): what was
|
|
@@ -24790,6 +24992,11 @@ var Groups = class {
|
|
|
24790
24992
|
* holds any more are not asked for every gossip turn. `last`: the latest ask, its member and the numbers it named.
|
|
24791
24993
|
*/
|
|
24792
24994
|
gapsAsked = /* @__PURE__ */ new Map();
|
|
24995
|
+
/**
|
|
24996
|
+
* Per mesh group and member, per epoch: the number up to which nobody holds what never reached me (`gapsSettle`).
|
|
24997
|
+
* The member list stops counting those; they are still asked of a member that connects later.
|
|
24998
|
+
*/
|
|
24999
|
+
gapsGone = /* @__PURE__ */ new Map();
|
|
24793
25000
|
/** Per mesh group, when `gapsAgain` last looked: every half `MESH_GAP_AGAIN_MS`, not every tick. */
|
|
24794
25001
|
gapsLooked = /* @__PURE__ */ new Map();
|
|
24795
25002
|
/** Per mesh group: member key → since when its edge to me is down, and when someone last told me it is here. */
|
|
@@ -24862,6 +25069,10 @@ var Groups = class {
|
|
|
24862
25069
|
this.communities.files = this.files;
|
|
24863
25070
|
}
|
|
24864
25071
|
}
|
|
25072
|
+
/** Writes what the private groups' sessions still have to save (`GROUP_LIMITS.saveWithinMs`): the app is stopping. */
|
|
25073
|
+
async flush() {
|
|
25074
|
+
await Promise.allSettled([...this.sessions.values()].map((session) => session.flush()));
|
|
25075
|
+
}
|
|
24865
25076
|
/** `asItIs`: a copy that must stay as it was stored (limited mode, WISP 06), so nothing left over is cleared from it. */
|
|
24866
25077
|
async load(asItIs = false) {
|
|
24867
25078
|
const all = await this.store.getGroups();
|
|
@@ -24871,6 +25082,10 @@ var Groups = class {
|
|
|
24871
25082
|
if (group.state) this.attach(group.state);
|
|
24872
25083
|
if (group.invited) this.invited.set(group.id, new Set(Object.keys(group.invited)));
|
|
24873
25084
|
const history = await this.store.getMessages(MESSAGE_LINK(group.id)), said = history.filter((m) => !m.event && !m.groupPay);
|
|
25085
|
+
this.framesKept.set(group.id, history.filter((m) => m.frame).map((m) => ({
|
|
25086
|
+
id: m.id,
|
|
25087
|
+
bytes: frameBytes(m.frame)
|
|
25088
|
+
})));
|
|
24874
25089
|
const last = said[said.length - 1], lastPeer = [...said].reverse().find((m) => m.sender !== "me");
|
|
24875
25090
|
if (last) this.lastMessageAt.set(group.id, last.timestamp);
|
|
24876
25091
|
const peerAt = cameOrWritten(lastPeer?.timestamp, lastPeer ? group.came?.peer : void 0);
|
|
@@ -24946,6 +25161,7 @@ var Groups = class {
|
|
|
24946
25161
|
admin: invitation.admin,
|
|
24947
25162
|
members: Math.min(Math.max(invitation.n, 0), GROUP_MEMBER_CAP.max),
|
|
24948
25163
|
accepted: !!invitation.seedB64,
|
|
25164
|
+
...this.acceptSaid.get(group.id)?.unanswered ? { unanswered: true } : {},
|
|
24949
25165
|
...invitation.entry ? {
|
|
24950
25166
|
viaLink: true,
|
|
24951
25167
|
stage: this.joinStage(group)
|
|
@@ -24987,7 +25203,7 @@ var Groups = class {
|
|
|
24987
25203
|
...viaHub ? { viaHub } : {},
|
|
24988
25204
|
...hubKeys.has(key) ? { hub: true } : {},
|
|
24989
25205
|
...hubRole ? { hubRole } : {},
|
|
24990
|
-
missing: session.missing(key)
|
|
25206
|
+
missing: session.missing(key, this.gapsGone.get(group.id)?.get(key)?.[session.epoch])
|
|
24991
25207
|
};
|
|
24992
25208
|
})
|
|
24993
25209
|
};
|
|
@@ -25092,6 +25308,22 @@ var Groups = class {
|
|
|
25092
25308
|
await this.store.putGroup(group);
|
|
25093
25309
|
this.host.emit();
|
|
25094
25310
|
}
|
|
25311
|
+
/**
|
|
25312
|
+
* Admin side: the invitations still unanswered are taken back, each over the contact chat it went on, when the group
|
|
25313
|
+
* goes from this app or the role from me: nobody here could let them in. A chat that is down hears it as the answer
|
|
25314
|
+
* to its accept. Entry sessions are not told: they close with the group, or the new admin answers the next knock.
|
|
25315
|
+
*/
|
|
25316
|
+
takeBack(groupId, group) {
|
|
25317
|
+
const entries = new Set(this.host.entries(groupId).values());
|
|
25318
|
+
for (const linkId of this.invited.get(groupId) ?? []) if (!entries.has(linkId)) try {
|
|
25319
|
+
this.host.sendOnLink(linkId, {
|
|
25320
|
+
t: "group-decline",
|
|
25321
|
+
g: groupId
|
|
25322
|
+
});
|
|
25323
|
+
} catch {}
|
|
25324
|
+
this.invited.delete(groupId);
|
|
25325
|
+
if (group) delete group.invited;
|
|
25326
|
+
}
|
|
25095
25327
|
/** Admin side: a contact chat answered its invitation (accepted or declined); false when it had none. */
|
|
25096
25328
|
answered(group, linkId) {
|
|
25097
25329
|
const had = !!this.invited.get(group.id)?.delete(linkId);
|
|
@@ -25100,6 +25332,14 @@ var Groups = class {
|
|
|
25100
25332
|
group.invited = Object.keys(rest).length ? rest : void 0;
|
|
25101
25333
|
return true;
|
|
25102
25334
|
}
|
|
25335
|
+
/** Admin side: a contact whose answer did not come in `INVITED_KEPT_MS` (a decline lost on the way, an app gone) is not waited for any more. */
|
|
25336
|
+
async ageInvited(group, now) {
|
|
25337
|
+
const stale = Object.entries(group.invited).filter(([, at]) => now - at > INVITED_KEPT_MS);
|
|
25338
|
+
if (!stale.length) return;
|
|
25339
|
+
for (const [linkId] of stale) this.answered(group, linkId);
|
|
25340
|
+
await this.store.putGroup(group);
|
|
25341
|
+
this.host.emit();
|
|
25342
|
+
}
|
|
25103
25343
|
async accept(groupId) {
|
|
25104
25344
|
const group = this.stored.get(groupId);
|
|
25105
25345
|
if (!group?.invitation) throw new Error("No invitation to accept");
|
|
@@ -25126,19 +25366,56 @@ var Groups = class {
|
|
|
25126
25366
|
if (this.isCommunity(groupId)) return this.communities.decline(groupId);
|
|
25127
25367
|
const group = this.stored.get(groupId);
|
|
25128
25368
|
if (!group?.invitation) throw new Error("No invitation to decline");
|
|
25129
|
-
|
|
25130
|
-
|
|
25131
|
-
|
|
25132
|
-
|
|
25133
|
-
|
|
25134
|
-
} catch {}
|
|
25369
|
+
if (!group.previous) return this.forget(groupId);
|
|
25370
|
+
await this.dropInvitation(group, this.sayDecline(groupId, group.invitation.linkId));
|
|
25371
|
+
}
|
|
25372
|
+
/** An invitation goes from the list: declined here (`owed`: the answer, when it could not be sent), or taken back by who sent it. */
|
|
25373
|
+
async dropInvitation(group, owed) {
|
|
25135
25374
|
const previous = group.previous;
|
|
25136
|
-
if (!previous) return this.forget(
|
|
25137
|
-
|
|
25375
|
+
if (!previous) return this.forget(group.id, false);
|
|
25376
|
+
if (owed) previous.declined = owed;
|
|
25377
|
+
else delete previous.declined;
|
|
25378
|
+
this.acceptSaid.delete(group.id);
|
|
25379
|
+
this.stored.set(group.id, previous);
|
|
25138
25380
|
if (previous.state) this.attach(previous.state);
|
|
25139
25381
|
await this.store.putGroup(previous);
|
|
25140
25382
|
this.host.emit();
|
|
25141
25383
|
}
|
|
25384
|
+
/** Invitee side: says no to an invitation. With the inviter's chat down (its app is closed, say), returns the answer owed, said when that chat is next up. */
|
|
25385
|
+
sayDecline(groupId, linkId) {
|
|
25386
|
+
try {
|
|
25387
|
+
this.host.sendOnLink(linkId, {
|
|
25388
|
+
t: "group-decline",
|
|
25389
|
+
g: groupId
|
|
25390
|
+
});
|
|
25391
|
+
return;
|
|
25392
|
+
} catch {
|
|
25393
|
+
return {
|
|
25394
|
+
at: Date.now(),
|
|
25395
|
+
linkId
|
|
25396
|
+
};
|
|
25397
|
+
}
|
|
25398
|
+
}
|
|
25399
|
+
/** Invitee side: a decline its inviter's chat was down for is said once that chat is up, or dropped after `LEFT_KEPT_MS`. */
|
|
25400
|
+
async declineOwed(group, now) {
|
|
25401
|
+
const { at, linkId } = group.declined;
|
|
25402
|
+
if (now - at <= LEFT_KEPT_MS) {
|
|
25403
|
+
if (!this.host.linkReady(linkId)) return;
|
|
25404
|
+
try {
|
|
25405
|
+
this.host.sendOnLink(linkId, {
|
|
25406
|
+
t: "group-decline",
|
|
25407
|
+
g: group.id
|
|
25408
|
+
});
|
|
25409
|
+
} catch {
|
|
25410
|
+
return;
|
|
25411
|
+
}
|
|
25412
|
+
}
|
|
25413
|
+
if (this.stored.get(group.id) !== group) return;
|
|
25414
|
+
delete group.declined;
|
|
25415
|
+
if (group.state) return this.store.putGroup(group);
|
|
25416
|
+
this.stored.delete(group.id);
|
|
25417
|
+
await this.store.deleteGroup(group.id);
|
|
25418
|
+
}
|
|
25142
25419
|
/**
|
|
25143
25420
|
* Sends a text; `messageId` is the id it is kept under here (what history, replies and reactions name). `forwarded`:
|
|
25144
25421
|
* the hop count of a forwarded text (WISP 902 § Forwards). `card`: a checked status card, the text its fallback.
|
|
@@ -25289,8 +25566,7 @@ var Groups = class {
|
|
|
25289
25566
|
...bye ? { bye } : {}
|
|
25290
25567
|
};
|
|
25291
25568
|
delete group.entry;
|
|
25292
|
-
|
|
25293
|
-
this.invited.delete(groupId);
|
|
25569
|
+
this.takeBack(groupId, group);
|
|
25294
25570
|
this.pendingEntries.delete(groupId);
|
|
25295
25571
|
this.welcomed.delete(groupId);
|
|
25296
25572
|
this.lingering.delete(groupId);
|
|
@@ -25457,8 +25733,14 @@ var Groups = class {
|
|
|
25457
25733
|
farewell.told ??= this.now();
|
|
25458
25734
|
this.store.putGroup(group).catch(() => {});
|
|
25459
25735
|
}
|
|
25460
|
-
makeAdmin(groupId, key) {
|
|
25461
|
-
|
|
25736
|
+
async makeAdmin(groupId, key) {
|
|
25737
|
+
if (this.isCommunity(groupId)) return this.communities.makeAdmin(groupId, key);
|
|
25738
|
+
await this.session(groupId).transferAdmin(key);
|
|
25739
|
+
const group = this.stored.get(groupId);
|
|
25740
|
+
if (!group || !this.invited.get(groupId)?.size) return;
|
|
25741
|
+
this.takeBack(groupId, group);
|
|
25742
|
+
await this.store.putGroup(group);
|
|
25743
|
+
this.host.emit();
|
|
25462
25744
|
}
|
|
25463
25745
|
/**
|
|
25464
25746
|
* The admin pins a member of a private group as a hub, excludes one from being a hub, or leaves it to the member's
|
|
@@ -25482,7 +25764,8 @@ var Groups = class {
|
|
|
25482
25764
|
if (this.isCommunity(groupId)) return this.communities.rename(groupId, name);
|
|
25483
25765
|
await this.session(groupId).rename(name);
|
|
25484
25766
|
}
|
|
25485
|
-
|
|
25767
|
+
/** `answer`: false when an invitation goes because its inviter took it back, so no decline is said for it. */
|
|
25768
|
+
async forget(groupId, answer = true) {
|
|
25486
25769
|
if (this.isCommunity(groupId)) return this.communities.leave(groupId);
|
|
25487
25770
|
const session = this.sessions.get(groupId);
|
|
25488
25771
|
if (session?.status === "active") try {
|
|
@@ -25490,13 +25773,16 @@ var Groups = class {
|
|
|
25490
25773
|
} catch (e) {
|
|
25491
25774
|
if (session.isAdmin && session.others.length) throw e;
|
|
25492
25775
|
}
|
|
25776
|
+
const gone = this.stored.get(groupId), invitation = answer && gone?.invitation && !gone.invitation.entry ? gone.invitation : void 0;
|
|
25777
|
+
const owed = invitation && this.sayDecline(groupId, invitation.linkId);
|
|
25493
25778
|
this.sessions.delete(groupId);
|
|
25494
25779
|
this.stored.delete(groupId);
|
|
25495
|
-
this.
|
|
25780
|
+
this.takeBack(groupId);
|
|
25496
25781
|
this.acceptSaid.delete(groupId);
|
|
25497
25782
|
this.lastRoster.delete(groupId);
|
|
25498
25783
|
this.lastTold.delete(groupId);
|
|
25499
25784
|
this.pendingEntries.delete(groupId);
|
|
25785
|
+
this.framesKept.delete(groupId);
|
|
25500
25786
|
this.knocked.delete(groupId);
|
|
25501
25787
|
this.knockCheckAt.delete(groupId);
|
|
25502
25788
|
this.knockRewrites.delete(groupId);
|
|
@@ -25504,10 +25790,13 @@ var Groups = class {
|
|
|
25504
25790
|
this.welcomed.delete(groupId);
|
|
25505
25791
|
this.lingering.delete(groupId);
|
|
25506
25792
|
this.relayAsked.delete(groupId);
|
|
25793
|
+
this.syncHeld.delete(groupId);
|
|
25794
|
+
this.lastHanded.delete(groupId);
|
|
25507
25795
|
this.hubs.forget(groupId);
|
|
25508
25796
|
this.removedAt.delete(groupId);
|
|
25509
25797
|
this.lastGossip.delete(groupId);
|
|
25510
25798
|
this.gapsAsked.delete(groupId);
|
|
25799
|
+
this.gapsGone.delete(groupId);
|
|
25511
25800
|
this.gapsLooked.delete(groupId);
|
|
25512
25801
|
this.typings.forget(groupId);
|
|
25513
25802
|
for (const key of this.pinPaces.keys()) if (key.startsWith(`${groupId}\n`)) this.pinPaces.delete(key);
|
|
@@ -25517,6 +25806,15 @@ var Groups = class {
|
|
|
25517
25806
|
this.signals.forget(groupId);
|
|
25518
25807
|
for (const linkId of [...this.host.edges(groupId).values(), ...this.host.entries(groupId).values()]) await this.host.closeEdge(linkId);
|
|
25519
25808
|
await this.store.deleteGroup(groupId);
|
|
25809
|
+
if (owed && !this.stored.has(groupId)) {
|
|
25810
|
+
const tombstone = {
|
|
25811
|
+
id: groupId,
|
|
25812
|
+
createdAt: gone.createdAt,
|
|
25813
|
+
declined: owed
|
|
25814
|
+
};
|
|
25815
|
+
this.stored.set(groupId, tombstone);
|
|
25816
|
+
await this.store.putGroup(tombstone);
|
|
25817
|
+
}
|
|
25520
25818
|
await this.files?.drop(groupId);
|
|
25521
25819
|
this.host.historyGone?.(groupId);
|
|
25522
25820
|
this.host.membersChanged?.(groupId);
|
|
@@ -25609,6 +25907,10 @@ var Groups = class {
|
|
|
25609
25907
|
if (now - group.left.at > LEFT_KEPT_MS) await this.forget(group.id);
|
|
25610
25908
|
continue;
|
|
25611
25909
|
}
|
|
25910
|
+
if (group.declined) {
|
|
25911
|
+
await this.declineOwed(group, now);
|
|
25912
|
+
if (!group.state) continue;
|
|
25913
|
+
}
|
|
25612
25914
|
if (group.invitation?.entry && !group.state) {
|
|
25613
25915
|
const every = now - group.createdAt > this.timings.patienceMs ? this.timings.slowKnockMs : this.timings.knockMs;
|
|
25614
25916
|
if (!(this.host.linkReady(group.invitation.linkId) || !!this.host.linkSeen?.(group.invitation.linkId)) && (now - (this.lastKnock.get(group.id) ?? 0) >= every || now >= (this.knockCheckAt.get(group.id) ?? Infinity))) await this.knock(group, now).catch(() => {});
|
|
@@ -25619,6 +25921,7 @@ var Groups = class {
|
|
|
25619
25921
|
continue;
|
|
25620
25922
|
}
|
|
25621
25923
|
const session = this.sessions.get(group.id);
|
|
25924
|
+
if (group.invited) await this.ageInvited(group, now);
|
|
25622
25925
|
if (!group.entry || !session) continue;
|
|
25623
25926
|
if (!session.isAdmin) {
|
|
25624
25927
|
await this.disableLink(group.id);
|
|
@@ -25694,6 +25997,13 @@ var Groups = class {
|
|
|
25694
25997
|
});
|
|
25695
25998
|
return;
|
|
25696
25999
|
}
|
|
26000
|
+
if (said.n >= ACCEPT_AGAINS && !said.down && !said.unanswered && !invitation.entry && now - said.at >= ACCEPT_AGAIN_MS) {
|
|
26001
|
+
this.acceptSaid.set(group.id, {
|
|
26002
|
+
...said,
|
|
26003
|
+
unanswered: true
|
|
26004
|
+
});
|
|
26005
|
+
this.host.emit();
|
|
26006
|
+
}
|
|
25697
26007
|
if (said.n >= ACCEPT_AGAINS || !said.down && now - said.at < ACCEPT_AGAIN_MS) return;
|
|
25698
26008
|
try {
|
|
25699
26009
|
this.host.sendOnLink(invitation.linkId, {
|
|
@@ -25706,7 +26016,8 @@ var Groups = class {
|
|
|
25706
26016
|
}
|
|
25707
26017
|
this.acceptSaid.set(group.id, {
|
|
25708
26018
|
at: now,
|
|
25709
|
-
n: said.n + 1
|
|
26019
|
+
n: said.n + 1,
|
|
26020
|
+
...said.unanswered && { unanswered: true }
|
|
25710
26021
|
});
|
|
25711
26022
|
}
|
|
25712
26023
|
/** An entry session came up with groups on both sides: the admin's side invites over it. */
|
|
@@ -25943,7 +26254,16 @@ var Groups = class {
|
|
|
25943
26254
|
}
|
|
25944
26255
|
case "group-accept": {
|
|
25945
26256
|
const session = this.sessions.get(g);
|
|
25946
|
-
if (
|
|
26257
|
+
if (typeof frame.key !== "string" || !MEMBER_KEY.test(frame.key)) return;
|
|
26258
|
+
if (!session?.isAdmin || !this.invited.get(g)?.has(linkId)) {
|
|
26259
|
+
if (!(!!session && rosterHas(session.roster, frame.key) && this.stored.get(g)?.contacts?.[frame.key] === linkId) && ![...this.host.entries(g).values()].includes(linkId)) try {
|
|
26260
|
+
this.host.sendOnLink(linkId, {
|
|
26261
|
+
t: "group-decline",
|
|
26262
|
+
g
|
|
26263
|
+
});
|
|
26264
|
+
} catch {}
|
|
26265
|
+
return;
|
|
26266
|
+
}
|
|
25947
26267
|
const group = this.stored.get(g);
|
|
25948
26268
|
if (frame.key === session.myKey || rosterHas(session.roster, frame.key)) return;
|
|
25949
26269
|
const entryPeer = this.hostEntry(g, linkId);
|
|
@@ -25977,6 +26297,11 @@ var Groups = class {
|
|
|
25977
26297
|
return;
|
|
25978
26298
|
}
|
|
25979
26299
|
case "group-decline": {
|
|
26300
|
+
const mine = this.stored.get(g);
|
|
26301
|
+
if (mine?.invitation && !mine.invitation.entry && !mine.state && mine.invitation.linkId === linkId) {
|
|
26302
|
+
await this.dropInvitation(mine);
|
|
26303
|
+
return;
|
|
26304
|
+
}
|
|
25980
26305
|
const entryPeer = this.hostEntry(g, linkId);
|
|
25981
26306
|
if (entryPeer) {
|
|
25982
26307
|
this.pendingEntries.get(g)?.delete(entryPeer);
|
|
@@ -26146,6 +26471,11 @@ var Groups = class {
|
|
|
26146
26471
|
return;
|
|
26147
26472
|
}
|
|
26148
26473
|
const taken = await session.handle(peerKey, frame);
|
|
26474
|
+
if (taken.some((f) => f.t === "group-msg")) {
|
|
26475
|
+
let handed = this.lastHanded.get(groupId);
|
|
26476
|
+
if (!handed) this.lastHanded.set(groupId, handed = /* @__PURE__ */ new Map());
|
|
26477
|
+
handed.set(peerKey, this.now());
|
|
26478
|
+
}
|
|
26149
26479
|
if (taken.length) this.hubs.passOn(groupId, session, peerKey, taken);
|
|
26150
26480
|
}
|
|
26151
26481
|
/**
|
|
@@ -26217,7 +26547,7 @@ var Groups = class {
|
|
|
26217
26547
|
const large = this.host.linkReady(linkId, 3) && this.markLarge(group, peerKey);
|
|
26218
26548
|
const legacy = this.hubs.enabled && this.hubs.edgeReady(groupId, session, group, peerKey, this.host.linkReady(linkId, 4), this.now());
|
|
26219
26549
|
if (large || legacy) this.store.putGroup(group).catch(() => {});
|
|
26220
|
-
try {
|
|
26550
|
+
if (!this.holdSync(groupId, peerKey)) try {
|
|
26221
26551
|
this.host.sendOnLink(linkId, session.syncFrame(this.askOf(groupId, session, peerKey)));
|
|
26222
26552
|
} catch {
|
|
26223
26553
|
return;
|
|
@@ -26340,11 +26670,55 @@ var Groups = class {
|
|
|
26340
26670
|
});
|
|
26341
26671
|
out.push(key);
|
|
26342
26672
|
}
|
|
26343
|
-
|
|
26673
|
+
const lacking = session.lacking();
|
|
26674
|
+
out.push(...this.gapsOf(groupId, session, via, lacking));
|
|
26675
|
+
this.ownGap(groupId, via, lacking);
|
|
26344
26676
|
if (out.length) this.lastGossip.set(groupId, now);
|
|
26345
26677
|
return out;
|
|
26346
26678
|
}
|
|
26347
26679
|
/**
|
|
26680
|
+
* Whether the answer of the member asked for someone's messages may still be coming: its edge up, asked under
|
|
26681
|
+
* `MESH_SYNC_HOLD_MAX_MS` ago, and a message from it (or the asking) under `MESH_SYNC_HOLD_MS` ago.
|
|
26682
|
+
*/
|
|
26683
|
+
answering(groupId, { via, at }, now) {
|
|
26684
|
+
const edge = this.host.edges(groupId).get(via);
|
|
26685
|
+
if (!edge || !this.host.linkReady(edge) || now - at >= 1e4) return false;
|
|
26686
|
+
return now - Math.max(at, this.lastHanded.get(groupId)?.get(via) ?? 0) < MESH_SYNC_HOLD_MS;
|
|
26687
|
+
}
|
|
26688
|
+
/**
|
|
26689
|
+
* Whether the sync to a member whose edge just opened waits (`heldSyncs` sends it): its messages were asked of
|
|
26690
|
+
* another member a moment ago, and that answer holds them among everyone's in the order they were written. Its own
|
|
26691
|
+
* answer now would bring the same messages on their own, ahead of or behind the others' lines.
|
|
26692
|
+
*/
|
|
26693
|
+
holdSync(groupId, peerKey) {
|
|
26694
|
+
const asked = this.relayAsked.get(groupId)?.get(peerKey);
|
|
26695
|
+
if (!asked || asked.via === peerKey || !this.answering(groupId, asked, this.now())) {
|
|
26696
|
+
this.syncHeld.get(groupId)?.delete(peerKey);
|
|
26697
|
+
this.relayAsked.get(groupId)?.delete(peerKey);
|
|
26698
|
+
return false;
|
|
26699
|
+
}
|
|
26700
|
+
let held = this.syncHeld.get(groupId);
|
|
26701
|
+
if (!held) this.syncHeld.set(groupId, held = /* @__PURE__ */ new Map());
|
|
26702
|
+
held.set(peerKey, { ...asked });
|
|
26703
|
+
return true;
|
|
26704
|
+
}
|
|
26705
|
+
/** The syncs that waited for another member's answer (`holdSync`), once it has come: for what it did not hand on. */
|
|
26706
|
+
heldSyncs(groupId, session, now) {
|
|
26707
|
+
const held = this.syncHeld.get(groupId);
|
|
26708
|
+
if (!held?.size) return;
|
|
26709
|
+
const edges = this.host.edges(groupId);
|
|
26710
|
+
for (const [key, asked] of held) {
|
|
26711
|
+
if (this.answering(groupId, asked, now)) continue;
|
|
26712
|
+
held.delete(key);
|
|
26713
|
+
if (this.relayAsked.get(groupId)?.get(key)?.via === asked.via) this.relayAsked.get(groupId).delete(key);
|
|
26714
|
+
const edge = edges.get(key);
|
|
26715
|
+
if (!edge || !this.host.linkReady(edge) || !rosterHas(session.roster, key)) continue;
|
|
26716
|
+
try {
|
|
26717
|
+
this.host.sendOnLink(edge, session.syncFrame(this.askOf(groupId, session, key)));
|
|
26718
|
+
} catch {}
|
|
26719
|
+
}
|
|
26720
|
+
}
|
|
26721
|
+
/**
|
|
26348
26722
|
* Reachable members whose messages below the highest seen never reached me, for `via` to hand on: their own answer
|
|
26349
26723
|
* comes from their last `GROUP_LIMITS.outlog` only (WISP 902 § Catch-up), the rest another member may hold. As
|
|
26350
26724
|
* `askOf`, one member at a time, and each member once until what is missing changes.
|
|
@@ -26374,6 +26748,7 @@ var Groups = class {
|
|
|
26374
26748
|
});
|
|
26375
26749
|
if (entry.of.has(via)) continue;
|
|
26376
26750
|
entry.of.add(via);
|
|
26751
|
+
entry.at = now;
|
|
26377
26752
|
entry.last = {
|
|
26378
26753
|
via,
|
|
26379
26754
|
at: now,
|
|
@@ -26388,6 +26763,55 @@ var Groups = class {
|
|
|
26388
26763
|
return out;
|
|
26389
26764
|
}
|
|
26390
26765
|
/**
|
|
26766
|
+
* Whether a sync to `via` asks it for its own messages that never reached me, for the first time since what is
|
|
26767
|
+
* missing changed: any sync names them (`miss`), and an author answers from its own log (`GROUP_LIMITS.outlog`).
|
|
26768
|
+
*/
|
|
26769
|
+
ownGap(groupId, via, lacking) {
|
|
26770
|
+
const mark = lacking.get(via);
|
|
26771
|
+
if (mark === void 0) return false;
|
|
26772
|
+
let tried = this.gapsAsked.get(groupId);
|
|
26773
|
+
if (!tried) this.gapsAsked.set(groupId, tried = /* @__PURE__ */ new Map());
|
|
26774
|
+
let entry = tried.get(via);
|
|
26775
|
+
if (!entry || entry.mark !== mark) tried.set(via, entry = {
|
|
26776
|
+
mark,
|
|
26777
|
+
of: /* @__PURE__ */ new Set(),
|
|
26778
|
+
noticed: entry?.noticed,
|
|
26779
|
+
last: entry?.last
|
|
26780
|
+
});
|
|
26781
|
+
if (entry.of.has(via)) return false;
|
|
26782
|
+
entry.of.add(via);
|
|
26783
|
+
entry.at = this.now();
|
|
26784
|
+
return true;
|
|
26785
|
+
}
|
|
26786
|
+
/**
|
|
26787
|
+
* Messages nobody holds any more: every connected member, their author included, was asked for the same missing
|
|
26788
|
+
* numbers `MESH_GAP_AGAIN_MS` ago or more and none arrived. An author keeps its last 32 and the others the group's
|
|
26789
|
+
* last 256 (WISP 902 § Bounds), so what a sync names and nobody hands on is gone, and so is what is below it (logs
|
|
26790
|
+
* drop the oldest first). The member list counted them until the next epoch, as if they were still on their way.
|
|
26791
|
+
*/
|
|
26792
|
+
gapsSettle(groupId, session, now, connected, lacking) {
|
|
26793
|
+
const tried = this.gapsAsked.get(groupId);
|
|
26794
|
+
if (!tried) return;
|
|
26795
|
+
let changed = false;
|
|
26796
|
+
for (const [key, mark] of lacking) {
|
|
26797
|
+
const entry = tried.get(key);
|
|
26798
|
+
if (!entry || entry.mark !== mark || entry.at === void 0 || now - entry.at < 1e4) continue;
|
|
26799
|
+
if (!connected.includes(key) || !connected.every((member) => entry.of.has(member))) continue;
|
|
26800
|
+
let gone = this.gapsGone.get(groupId);
|
|
26801
|
+
if (!gone) this.gapsGone.set(groupId, gone = /* @__PURE__ */ new Map());
|
|
26802
|
+
const upTo = { ...gone.get(key) };
|
|
26803
|
+
for (const [e, numbers] of Object.entries(session.missOf(key))) {
|
|
26804
|
+
const top = Math.max(...numbers);
|
|
26805
|
+
if (top > (upTo[e] ?? -1)) {
|
|
26806
|
+
upTo[e] = top;
|
|
26807
|
+
changed = true;
|
|
26808
|
+
}
|
|
26809
|
+
}
|
|
26810
|
+
gone.set(key, upTo);
|
|
26811
|
+
}
|
|
26812
|
+
if (changed) this.host.emit();
|
|
26813
|
+
}
|
|
26814
|
+
/**
|
|
26391
26815
|
* Gaps asked for sooner than the gossip turn: one noticed `MESH_GAP_AGAIN_MS` ago and not asked for yet (an author's
|
|
26392
26816
|
* own answer, its last 32, showed what is below them) goes to a connected member now; and a member that handed on some
|
|
26393
26817
|
* of what I last asked it for likely holds the rest, so it is asked for the next numbers `MESH_GAP_AGAIN_MS` after. A
|
|
@@ -26402,6 +26826,7 @@ var Groups = class {
|
|
|
26402
26826
|
if (!lacking.size && !tried?.size) return;
|
|
26403
26827
|
if (!tried) this.gapsAsked.set(groupId, tried = /* @__PURE__ */ new Map());
|
|
26404
26828
|
for (const key of tried.keys()) if (!lacking.has(key)) tried.delete(key);
|
|
26829
|
+
this.gapsSettle(groupId, session, now, connected, lacking);
|
|
26405
26830
|
const edges = this.host.edges(groupId), again = /* @__PURE__ */ new Map(), fresh = /* @__PURE__ */ new Map();
|
|
26406
26831
|
const up = (key) => {
|
|
26407
26832
|
const edge = edges.get(key);
|
|
@@ -26429,6 +26854,7 @@ var Groups = class {
|
|
|
26429
26854
|
tried.set(key, {
|
|
26430
26855
|
mark: lacking.get(key),
|
|
26431
26856
|
of: /* @__PURE__ */ new Set([via]),
|
|
26857
|
+
at: now,
|
|
26432
26858
|
last: {
|
|
26433
26859
|
via,
|
|
26434
26860
|
at: now,
|
|
@@ -26466,6 +26892,7 @@ var Groups = class {
|
|
|
26466
26892
|
if (session.status !== "active" || this.stored.get(groupId)?.left) continue;
|
|
26467
26893
|
session.reseal().catch(() => {});
|
|
26468
26894
|
this.farewellsTick(groupId, session, now);
|
|
26895
|
+
this.heldSyncs(groupId, session, now);
|
|
26469
26896
|
if (reconcile) {
|
|
26470
26897
|
const edges = this.host.edges(groupId);
|
|
26471
26898
|
if (!this.hubs.large(session) && session.others.some((key) => !edges.has(key))) this.reconcileEdges(groupId);
|
|
@@ -26488,7 +26915,7 @@ var Groups = class {
|
|
|
26488
26915
|
at: now
|
|
26489
26916
|
});
|
|
26490
26917
|
const ask = [...away, ...this.gapsOf(groupId, session, via, lacking)];
|
|
26491
|
-
if (!ask.length) continue;
|
|
26918
|
+
if (!this.ownGap(groupId, via, lacking) && !ask.length) continue;
|
|
26492
26919
|
try {
|
|
26493
26920
|
this.host.sendOnLink(edges.get(via), session.syncFrame(ask));
|
|
26494
26921
|
} catch {}
|
|
@@ -26514,6 +26941,25 @@ var Groups = class {
|
|
|
26514
26941
|
if (session && this.stored.get(groupId)?.farewells?.[peerKey] && !rosterHas(session.roster, peerKey)) return;
|
|
26515
26942
|
session?.setNick(peerKey, nick);
|
|
26516
26943
|
}
|
|
26944
|
+
/**
|
|
26945
|
+
* A message's row took the frame it came in: past `GROUP_LIMITS.relay` frames (or their bytes) in a group, the oldest
|
|
26946
|
+
* rows give theirs up, so a group's history holds the frames of its newest messages only.
|
|
26947
|
+
*/
|
|
26948
|
+
async frameKept(groupId, id, frame) {
|
|
26949
|
+
let kept = this.framesKept.get(groupId);
|
|
26950
|
+
if (!kept) this.framesKept.set(groupId, kept = []);
|
|
26951
|
+
if (kept.some((k) => k.id === id)) return;
|
|
26952
|
+
kept.push({
|
|
26953
|
+
id,
|
|
26954
|
+
bytes: frameBytes(frame)
|
|
26955
|
+
});
|
|
26956
|
+
let bytes = kept.reduce((sum, k) => sum + k.bytes, 0);
|
|
26957
|
+
while (kept.length > GROUP_LIMITS.relay || bytes > GROUP_LIMITS.relayBytes) {
|
|
26958
|
+
const oldest = kept.shift();
|
|
26959
|
+
bytes -= oldest.bytes;
|
|
26960
|
+
await this.host.forgetFrame?.(MESSAGE_LINK(groupId), oldest.id).catch(() => {});
|
|
26961
|
+
}
|
|
26962
|
+
}
|
|
26517
26963
|
session(groupId) {
|
|
26518
26964
|
const session = this.sessions.get(groupId);
|
|
26519
26965
|
if (!session) throw new Error("Group not found");
|
|
@@ -26527,7 +26973,7 @@ var Groups = class {
|
|
|
26527
26973
|
adminTurn: () => this.host.adminTurn?.(state.id) ?? Promise.resolve(true),
|
|
26528
26974
|
save: async (next) => {
|
|
26529
26975
|
const group = this.stored.get(state.id);
|
|
26530
|
-
if (!group) return;
|
|
26976
|
+
if (!group || this.sessions.get(state.id) !== session) return;
|
|
26531
26977
|
group.state = next;
|
|
26532
26978
|
await this.store.putGroup(group);
|
|
26533
26979
|
},
|
|
@@ -26569,9 +27015,12 @@ var Groups = class {
|
|
|
26569
27015
|
...m.reply && { replyTo: groupReply(m.reply, session.myKey) },
|
|
26570
27016
|
...m.forwarded && { forwarded: m.forwarded },
|
|
26571
27017
|
...m.card && { card: m.card },
|
|
26572
|
-
...file && { file }
|
|
27018
|
+
...file && { file },
|
|
27019
|
+
...m.frame && { frame: m.frame }
|
|
26573
27020
|
};
|
|
26574
|
-
const
|
|
27021
|
+
const completed = !!m.completes && !!this.host.completeMessage;
|
|
27022
|
+
const stored = completed ? (await this.host.completeMessage(message), false) : await this.host.storeMessage(message);
|
|
27023
|
+
if (m.frame && (completed || stored !== false)) await this.frameKept(state.id, m.id, m.frame);
|
|
26575
27024
|
const came = m.sender === session.myKey ? timestamp : cameAt(timestamp, stored, arrivalNow(this.now()));
|
|
26576
27025
|
this.lastMessageAt.set(state.id, Math.max(this.lastMessageAt.get(state.id) ?? 0, came));
|
|
26577
27026
|
if (mentioned) this.lastMentionAt.set(state.id, Math.max(this.lastMentionAt.get(state.id) ?? 0, came));
|
|
@@ -27045,9 +27494,12 @@ function summary(note) {
|
|
|
27045
27494
|
const amount = note.decimals ? `${note.amount} base units of ${note.unit}` : `${Number(note.amount).toLocaleString()} ${note.test ? "test sats" : "sats"}`;
|
|
27046
27495
|
return note.kind === "request" ? `⚡ Requested ${amount}` : `⚡ Sent ${amount}`;
|
|
27047
27496
|
}
|
|
27497
|
+
/** A second past the receiver's window, so one window's edits have left it on the member's side whatever the edge's delay. */
|
|
27498
|
+
var GROUP_EDIT_RESEND_WINDOW_MS = EDIT_RATE_WINDOW_MS + 1e3;
|
|
27048
27499
|
/** An edit that could not go (no epoch key yet) is tried again after this long. */
|
|
27049
27500
|
var RETRY_MS = 1e4;
|
|
27050
27501
|
var chatOf = (groupId) => `group:${groupId}`;
|
|
27502
|
+
var resendKey = (groupId, member) => `${groupId}\n${member}`;
|
|
27051
27503
|
var GroupEdits = class {
|
|
27052
27504
|
host;
|
|
27053
27505
|
now;
|
|
@@ -27056,10 +27508,23 @@ var GroupEdits = class {
|
|
|
27056
27508
|
cards;
|
|
27057
27509
|
sendPace = /* @__PURE__ */ new Map();
|
|
27058
27510
|
receivePace = /* @__PURE__ */ new Map();
|
|
27511
|
+
/** Per group, per member (`group\nmember`): its edits past the window, by message, waiting for room (`hold`). */
|
|
27512
|
+
held = /* @__PURE__ */ new Map();
|
|
27513
|
+
heldTimers = /* @__PURE__ */ new Map();
|
|
27059
27514
|
timers = /* @__PURE__ */ new Map();
|
|
27060
27515
|
flushing = /* @__PURE__ */ new Map();
|
|
27061
27516
|
/** Per group: my rows known to have an edit to say, each with when it was last told (`edit`), by `tick`. */
|
|
27062
27517
|
known = /* @__PURE__ */ new Map();
|
|
27518
|
+
/**
|
|
27519
|
+
* Per group and member (`resendKey`): the last edit of mine said on its edge, in the order `resend` says them. What
|
|
27520
|
+
* comes after it is what the member has not heard there. Not kept across a restart.
|
|
27521
|
+
*/
|
|
27522
|
+
heard = /* @__PURE__ */ new Map();
|
|
27523
|
+
/** The connection `resend` last spoke on, per group and member: a new one hears my latest edits again. */
|
|
27524
|
+
resentOn = /* @__PURE__ */ new Map();
|
|
27525
|
+
resendPace = /* @__PURE__ */ new Map();
|
|
27526
|
+
resendTimers = /* @__PURE__ */ new Map();
|
|
27527
|
+
resending = /* @__PURE__ */ new Map();
|
|
27063
27528
|
tick = 0;
|
|
27064
27529
|
/**
|
|
27065
27530
|
* 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 +27673,8 @@ var GroupEdits = class {
|
|
|
27208
27673
|
/**
|
|
27209
27674
|
* An edit from a member, authenticated as `sender` by the group. Believed only while the sender is in the group (a
|
|
27210
27675
|
* 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
|
|
27676
|
+
* not here yet waits a minute for it. Past `EDIT_RECEIVE_LIMIT` a window from one member, the rest wait for the next
|
|
27677
|
+
* window (`hold`): a catch-up hands back every edit of an author at once, and one dropped is never handed again.
|
|
27212
27678
|
*/
|
|
27213
27679
|
async receive(groupId, sender, edit) {
|
|
27214
27680
|
const membership = this.host.membership(groupId);
|
|
@@ -27216,7 +27682,65 @@ var GroupEdits = class {
|
|
|
27216
27682
|
const key = `${groupId}\n${sender}`;
|
|
27217
27683
|
let pace = this.receivePace.get(key);
|
|
27218
27684
|
if (!pace) this.receivePace.set(key, pace = new RateWindow(30, EDIT_RATE_WINDOW_MS, this.now));
|
|
27219
|
-
if (!pace.take()) return
|
|
27685
|
+
if (this.held.get(groupId)?.has(key) || !pace.take()) return this.hold(groupId, sender, edit, pace);
|
|
27686
|
+
return this.take(groupId, membership, sender, edit);
|
|
27687
|
+
}
|
|
27688
|
+
/**
|
|
27689
|
+
* An edit past what a member's window takes: kept, the highest number per message, `GROUP_EDITS_HELD` messages a
|
|
27690
|
+
* group at most (the oldest go first), and taken when the window has room. The work stays what the window allows.
|
|
27691
|
+
*/
|
|
27692
|
+
hold(groupId, sender, edit, pace) {
|
|
27693
|
+
const key = `${groupId}\n${sender}`;
|
|
27694
|
+
let group = this.held.get(groupId);
|
|
27695
|
+
if (!group) this.held.set(groupId, group = /* @__PURE__ */ new Map());
|
|
27696
|
+
let waiting = group.get(key);
|
|
27697
|
+
if (!waiting) group.set(key, waiting = /* @__PURE__ */ new Map());
|
|
27698
|
+
const known = waiting.get(edit.id);
|
|
27699
|
+
if (known && known.e >= edit.e) return "dropped";
|
|
27700
|
+
waiting.delete(edit.id);
|
|
27701
|
+
waiting.set(edit.id, edit);
|
|
27702
|
+
let count = 0;
|
|
27703
|
+
for (const member of group.values()) count += member.size;
|
|
27704
|
+
for (const [member, edits] of group) {
|
|
27705
|
+
if (count <= 256) break;
|
|
27706
|
+
while (count > 256 && edits.size) {
|
|
27707
|
+
edits.delete(edits.keys().next().value);
|
|
27708
|
+
count--;
|
|
27709
|
+
}
|
|
27710
|
+
if (!edits.size && member !== key) group.delete(member);
|
|
27711
|
+
}
|
|
27712
|
+
if (!this.heldTimers.has(key) && !this.stopped) this.heldTimers.set(key, setTimeout(() => {
|
|
27713
|
+
this.takeHeld(groupId, sender).catch(() => {});
|
|
27714
|
+
}, Math.max(50, pace.wait())));
|
|
27715
|
+
return waiting.has(edit.id) ? "waiting" : "dropped";
|
|
27716
|
+
}
|
|
27717
|
+
/** The window of a member has room again: its edits that waited are taken, oldest first, as many as it allows. */
|
|
27718
|
+
async takeHeld(groupId, sender) {
|
|
27719
|
+
const key = `${groupId}\n${sender}`;
|
|
27720
|
+
this.heldTimers.delete(key);
|
|
27721
|
+
const group = this.held.get(groupId), waiting = group?.get(key), pace = this.receivePace.get(key);
|
|
27722
|
+
if (this.stopped || !group || !waiting || !pace) return;
|
|
27723
|
+
for (const [id, edit] of [...waiting]) {
|
|
27724
|
+
const membership = this.host.membership(groupId);
|
|
27725
|
+
if (!membership?.members.has(sender)) {
|
|
27726
|
+
waiting.clear();
|
|
27727
|
+
break;
|
|
27728
|
+
}
|
|
27729
|
+
if (!pace.take()) break;
|
|
27730
|
+
if (waiting.get(id) !== edit) continue;
|
|
27731
|
+
waiting.delete(id);
|
|
27732
|
+
await this.take(groupId, membership, sender, edit).catch(() => {});
|
|
27733
|
+
}
|
|
27734
|
+
if (waiting.size) {
|
|
27735
|
+
if (!this.heldTimers.has(key) && !this.stopped) this.heldTimers.set(key, setTimeout(() => {
|
|
27736
|
+
this.takeHeld(groupId, sender).catch(() => {});
|
|
27737
|
+
}, Math.max(50, pace.wait())));
|
|
27738
|
+
return;
|
|
27739
|
+
}
|
|
27740
|
+
if (group.get(key) === waiting) group.delete(key);
|
|
27741
|
+
if (!group.size) this.held.delete(groupId);
|
|
27742
|
+
}
|
|
27743
|
+
async take(groupId, membership, sender, edit) {
|
|
27220
27744
|
const chat = chatOf(groupId);
|
|
27221
27745
|
const message = await this.find(chat, edit.id);
|
|
27222
27746
|
if (!message) {
|
|
@@ -27264,24 +27788,80 @@ var GroupEdits = class {
|
|
|
27264
27788
|
await this.host.changed(chat, message.id);
|
|
27265
27789
|
return "applied";
|
|
27266
27790
|
}
|
|
27267
|
-
/**
|
|
27268
|
-
|
|
27269
|
-
|
|
27270
|
-
|
|
27271
|
-
|
|
27272
|
-
|
|
27273
|
-
|
|
27274
|
-
|
|
27275
|
-
|
|
27276
|
-
|
|
27277
|
-
|
|
27278
|
-
}
|
|
27791
|
+
/**
|
|
27792
|
+
* An edge of a private group opened: the member at its other end hears the edits of mine it has not heard on that
|
|
27793
|
+
* edge, the latest of each message, oldest first. `GROUP_EDIT_RESEND` go a window and the rest a window later each,
|
|
27794
|
+
* while the edge is the same: a member away while I closed forty task cards reads them all as closed, not the latest
|
|
27795
|
+
* sixteen. Nothing known of what it heard (the first time since the app started): my newest `GROUP_EDIT_CATCH_UP`.
|
|
27796
|
+
* A new connection hears my latest `GROUP_EDIT_RESEND` again whatever it heard: one may have come before its message
|
|
27797
|
+
* and been dropped. Called again while the edge is the same, it goes on from where it was.
|
|
27798
|
+
*/
|
|
27799
|
+
resend(groupId, to) {
|
|
27800
|
+
const key = resendKey(groupId, to);
|
|
27801
|
+
const run = (this.resending.get(key) ?? Promise.resolve()).then(() => this.resendNow(groupId, to));
|
|
27802
|
+
const settled = run.catch(() => {}).finally(() => {
|
|
27803
|
+
if (this.resending.get(key) === settled) this.resending.delete(key);
|
|
27804
|
+
});
|
|
27805
|
+
this.resending.set(key, settled);
|
|
27806
|
+
return run;
|
|
27807
|
+
}
|
|
27808
|
+
async resendNow(groupId, to) {
|
|
27809
|
+
const key = resendKey(groupId, to), chat = chatOf(groupId);
|
|
27810
|
+
clearTimeout(this.resendTimers.get(key));
|
|
27811
|
+
this.resendTimers.delete(key);
|
|
27812
|
+
const open = () => this.host.edge ? this.host.edge(groupId, to) : true;
|
|
27813
|
+
const edge = open(), membership = this.host.membership(groupId);
|
|
27814
|
+
if (this.stopped || edge === void 0 || !membership || membership.community || !membership.members.has(to)) return;
|
|
27815
|
+
const mine = (await (this.host.history ? this.host.history(chat) : this.host.messages(chat))).filter((m) => m.sender === "me" && m.edit && !m.edit.pending && canEditInGroup(m)).sort((a, b) => a.edit.at - b.edit.at || (a.id < b.id ? -1 : 1));
|
|
27816
|
+
const last = this.heard.get(key);
|
|
27817
|
+
const after = (m) => !last || m.edit.at > last.at || m.edit.at === last.at && m.id > last.id;
|
|
27818
|
+
const unheard = last ? mine.filter(after).length : Math.min(mine.length, 256);
|
|
27819
|
+
const again = this.resentOn.get(key) === edge ? 0 : Math.min(mine.length, 16);
|
|
27820
|
+
this.resentOn.set(key, edge);
|
|
27821
|
+
const due = mine.slice(mine.length - Math.max(unheard, again));
|
|
27822
|
+
let pace = this.resendPace.get(key);
|
|
27823
|
+
if (!pace) this.resendPace.set(key, pace = new RateWindow(16, GROUP_EDIT_RESEND_WINDOW_MS, this.now));
|
|
27824
|
+
for (const m of due) {
|
|
27825
|
+
if (this.stopped || open() !== edge) return;
|
|
27826
|
+
if (!pace.take()) {
|
|
27827
|
+
this.resendTimers.set(key, setTimeout(() => {
|
|
27828
|
+
this.resend(groupId, to).catch(() => {});
|
|
27829
|
+
}, Math.max(50, pace.wait())));
|
|
27830
|
+
return;
|
|
27831
|
+
}
|
|
27832
|
+
await this.host.send(groupId, {
|
|
27833
|
+
id: m.id,
|
|
27834
|
+
e: m.edit.seq,
|
|
27835
|
+
ts: m.edit.at,
|
|
27836
|
+
m: m.text,
|
|
27837
|
+
...m.mentions?.length && { k: m.mentions },
|
|
27838
|
+
...m.card && { sc: m.card }
|
|
27839
|
+
}, to);
|
|
27840
|
+
if (open() !== edge) return;
|
|
27841
|
+
if (after(m)) this.heard.set(key, {
|
|
27842
|
+
at: m.edit.at,
|
|
27843
|
+
id: m.id
|
|
27844
|
+
});
|
|
27845
|
+
}
|
|
27846
|
+
}
|
|
27847
|
+
/** A group is forgotten, or the app stops: what was known of its members' edges goes with it. */
|
|
27848
|
+
dropResends(groupId) {
|
|
27849
|
+
const of = (key) => groupId === void 0 || key.startsWith(`${groupId}\n`);
|
|
27850
|
+
for (const [key, timer] of this.resendTimers) if (of(key)) {
|
|
27851
|
+
clearTimeout(timer);
|
|
27852
|
+
this.resendTimers.delete(key);
|
|
27853
|
+
}
|
|
27854
|
+
for (const key of [...this.heard.keys()]) if (of(key)) this.heard.delete(key);
|
|
27855
|
+
for (const key of [...this.resendPace.keys()]) if (of(key)) this.resendPace.delete(key);
|
|
27856
|
+
for (const key of [...this.resentOn.keys()]) if (of(key)) this.resentOn.delete(key);
|
|
27279
27857
|
}
|
|
27280
27858
|
forget(groupId) {
|
|
27281
27859
|
clearTimeout(this.timers.get(groupId));
|
|
27282
27860
|
this.timers.delete(groupId);
|
|
27283
27861
|
this.known.delete(groupId);
|
|
27284
27862
|
this.learnt.delete(groupId);
|
|
27863
|
+
this.dropResends(groupId);
|
|
27864
|
+
this.dropHeld(groupId);
|
|
27285
27865
|
this.buffer.forget(chatOf(groupId));
|
|
27286
27866
|
}
|
|
27287
27867
|
stop() {
|
|
@@ -27289,6 +27869,16 @@ var GroupEdits = class {
|
|
|
27289
27869
|
this.cards.stop();
|
|
27290
27870
|
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
27291
27871
|
this.timers.clear();
|
|
27872
|
+
this.dropResends();
|
|
27873
|
+
this.dropHeld();
|
|
27874
|
+
}
|
|
27875
|
+
dropHeld(groupId) {
|
|
27876
|
+
for (const [key, timer] of this.heldTimers) if (groupId === void 0 || key.startsWith(`${groupId}\n`)) {
|
|
27877
|
+
clearTimeout(timer);
|
|
27878
|
+
this.heldTimers.delete(key);
|
|
27879
|
+
}
|
|
27880
|
+
if (groupId === void 0) this.held.clear();
|
|
27881
|
+
else this.held.delete(groupId);
|
|
27292
27882
|
}
|
|
27293
27883
|
};
|
|
27294
27884
|
//#endregion
|
|
@@ -33950,6 +34540,17 @@ var WalletSetup = class {
|
|
|
33950
34540
|
var RELAY_RETRY_MS = 3e4;
|
|
33951
34541
|
/** Native listeners an app may hold per transport (the Desktop's Rust Iroh allows eight, `paired_transport.rs`). */
|
|
33952
34542
|
var NATIVE_SLOTS = 8;
|
|
34543
|
+
/**
|
|
34544
|
+
* Iroh's listeners where it is the browser build (the web app, the extension, the CLI): Rust's eight are not its limit.
|
|
34545
|
+
* Each is a WebSocket to its relay and about 0.2 MiB: a phone-sized Chromium page held 128 of them, and the public
|
|
34546
|
+
* relays took sixteen started one after another, as the queue here starts them (six of sixteen started at once were
|
|
34547
|
+
* refused). 1:1 chats hold eight of them at most, as before: the rest is room for group links. HyperDHT keeps eight
|
|
34548
|
+
* there too: a page's listeners share one WebSocket to its relay, which drops a client past sixteen listens (device
|
|
34549
|
+
* links listen there as well), and each of the CLI's is a whole DHT node.
|
|
34550
|
+
*/
|
|
34551
|
+
var IROH_WEB_SLOTS = 16;
|
|
34552
|
+
/** Of a transport's listeners, the ones a group's links never hold: 1:1 chats keep that room. */
|
|
34553
|
+
var CHAT_NATIVE_SLOTS = 4;
|
|
33953
34554
|
/** How often a group link that found no free native slot tries again. */
|
|
33954
34555
|
var GROUP_NATIVE_RETRY_MS = 15e3;
|
|
33955
34556
|
/** The longest a group link whose native listener failed to start waits before trying again (its wait doubles up to it). */
|
|
@@ -33984,6 +34585,8 @@ var CARD_RESTORE_MOST = 50;
|
|
|
33984
34585
|
var CARD_RESTORE_MS = 6048e5;
|
|
33985
34586
|
/** A reaction said on the live session and not confirmed is said again after this long. */
|
|
33986
34587
|
var REACTION_RESEND_MS = 3e4;
|
|
34588
|
+
/** A file or a payment request of mine that waits for the chat to be live (a text that waits is its outbox's). */
|
|
34589
|
+
var waitsForLive = (m) => m.sender === "me" && m.delivery === "waiting" && (!!m.file || !!m.paymentId);
|
|
33987
34590
|
/** This side's participation key in a chat: derived from its seed once, not at every state built. */
|
|
33988
34591
|
function participationKeyOf(live) {
|
|
33989
34592
|
const seed = live.stored.participationSeed;
|
|
@@ -34319,6 +34922,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34319
34922
|
});
|
|
34320
34923
|
stateTimer = null;
|
|
34321
34924
|
paymentTimer = null;
|
|
34925
|
+
/**
|
|
34926
|
+
* The reviews whose outcome is still open (submitted, unknown), by id: what the payment poll asks about, so that it
|
|
34927
|
+
* does not read every review ever made every 10 s. `null` until the store was read once. An id is only ever added
|
|
34928
|
+
* from a read of the store or a change of a review, and leaves when the poll finds its review ended or gone.
|
|
34929
|
+
*/
|
|
34930
|
+
openReviews = null;
|
|
34931
|
+
noteOpenReviews(reviews) {
|
|
34932
|
+
const open = this.openReviews ??= /* @__PURE__ */ new Set();
|
|
34933
|
+
for (const review of reviews) if (review.state === "submitted" || review.state === "unknown") open.add(review.id);
|
|
34934
|
+
}
|
|
34322
34935
|
/** Every minute, the reviews no Approve can send any more are cancelled (dropStaleReviews). */
|
|
34323
34936
|
staleReviewTimer;
|
|
34324
34937
|
/** Group links: admins read knocks, joiners knock (WISP 902 § Entry link). */
|
|
@@ -34441,6 +35054,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34441
35054
|
}
|
|
34442
35055
|
}
|
|
34443
35056
|
], (review) => {
|
|
35057
|
+
if (this.openReviews) this.noteOpenReviews([review]);
|
|
34444
35058
|
if (review.state === "settled") this.feedback("confirmed", review.id);
|
|
34445
35059
|
if (review.state === "failed") this.cueFeedback({
|
|
34446
35060
|
cue: "failed",
|
|
@@ -34963,7 +35577,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34963
35577
|
await this.messagesChanged(chat, [id]);
|
|
34964
35578
|
this.emitState();
|
|
34965
35579
|
},
|
|
34966
|
-
notify: (chat, key) => this.feedback("reaction", `${chat}:${key}`, chat)
|
|
35580
|
+
notify: (chat, key) => this.feedback("reaction", `${chat}:${key}`, chat),
|
|
35581
|
+
believed: (chat, by) => {
|
|
35582
|
+
const membership = this.membership(chat.slice(6));
|
|
35583
|
+
return !!membership?.members.has(by) && by !== membership.me;
|
|
35584
|
+
}
|
|
34967
35585
|
});
|
|
34968
35586
|
/**
|
|
34969
35587
|
* 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 +35614,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
34996
35614
|
adminTurn: (_groupId, options) => this.confirmTurn({ within: options?.wait === false ? 0 : GhostlyNode.TURN_CONFIRM_WAIT_MS }),
|
|
34997
35615
|
staysOnline: () => this.options.staysOnline ?? (this.options.platform === "desktop" && typeof RTCPeerConnection !== "undefined"),
|
|
34998
35616
|
peerRoom: (groupId) => {
|
|
34999
|
-
|
|
35617
|
+
const native = Object.keys(this.nativeFactories);
|
|
35618
|
+
if (typeof RTCPeerConnection === "undefined" && native.length) {
|
|
35000
35619
|
let held = 0;
|
|
35001
35620
|
for (const live of this.links.values()) if (live.stored.group && live.stored.group !== groupId && live.link) held++;
|
|
35002
|
-
return Math.max(0,
|
|
35621
|
+
return Math.max(0, Math.max(...native.map((key) => this.groupNativeSlots(key))) - held);
|
|
35003
35622
|
}
|
|
35004
35623
|
const budget = this.options.peerBudget;
|
|
35005
35624
|
if (budget === void 0) return void 0;
|
|
@@ -35048,6 +35667,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35048
35667
|
edgeNick: (linkId) => this.links.get(linkId)?.presence.nick || void 0,
|
|
35049
35668
|
storeMessage: (message) => this.storeNewMessage(message),
|
|
35050
35669
|
completeMessage: (message) => this.completeGroupMessage(message),
|
|
35670
|
+
forgetFrame: async (linkId, id) => {
|
|
35671
|
+
await db.patchMessage(linkId, id, (stored) => stored.frame ? { frame: void 0 } : null, true);
|
|
35672
|
+
},
|
|
35051
35673
|
membersChanged: (groupId) => this.groupMembersChanged(groupId),
|
|
35052
35674
|
historyGone: (groupId) => this.groupHistoryGone(groupId),
|
|
35053
35675
|
emit: () => this.emitState(),
|
|
@@ -35110,7 +35732,11 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35110
35732
|
admin: !!this.groups.views().find((g) => g.id === groupId)?.isAdmin
|
|
35111
35733
|
};
|
|
35112
35734
|
},
|
|
35113
|
-
send: (groupId, edit, to) => this.groups.sendEdit(groupId, edit, to)
|
|
35735
|
+
send: (groupId, edit, to) => this.groups.sendEdit(groupId, edit, to),
|
|
35736
|
+
edge: (groupId, member) => {
|
|
35737
|
+
const link = this.links.get(this.memberEdges(groupId).get(member) ?? "")?.link;
|
|
35738
|
+
return link?.supportsGroupVersion(1) ? link : void 0;
|
|
35739
|
+
}
|
|
35114
35740
|
});
|
|
35115
35741
|
/** A community member's edit, signed by them: of their own message, and only while they are a member. */
|
|
35116
35742
|
async receiveCommunityEdit(groupId, sender, frame) {
|
|
@@ -35331,12 +35957,14 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35331
35957
|
};
|
|
35332
35958
|
}
|
|
35333
35959
|
const wallets = this.withHomes(walletInstances(networks));
|
|
35960
|
+
const intents = (await intentRepository.list()).map((saved) => saved.review);
|
|
35961
|
+
this.noteOpenReviews(intents);
|
|
35334
35962
|
this.walletView = {
|
|
35335
35963
|
...networks.mainnet,
|
|
35336
35964
|
networks,
|
|
35337
35965
|
wallets,
|
|
35338
35966
|
offers: this.walletOffers(networks, wallets),
|
|
35339
|
-
intents
|
|
35967
|
+
intents,
|
|
35340
35968
|
setup: this.walletSetup.view()
|
|
35341
35969
|
};
|
|
35342
35970
|
this.announcePaymentNetworks(wallets);
|
|
@@ -35405,9 +36033,16 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35405
36033
|
async pollPaymentStatus() {
|
|
35406
36034
|
if (this.shuttingDown) return;
|
|
35407
36035
|
try {
|
|
35408
|
-
|
|
36036
|
+
if (!this.openReviews) this.noteOpenReviews((await intentRepository.list()).map((saved) => saved.review));
|
|
36037
|
+
const open = this.openReviews;
|
|
36038
|
+
for (const id of [...open]) {
|
|
36039
|
+
const review = (await intentRepository.get(id))?.review;
|
|
36040
|
+
if (!review || !["submitted", "unknown"].includes(review.state)) {
|
|
36041
|
+
open.delete(id);
|
|
36042
|
+
continue;
|
|
36043
|
+
}
|
|
35409
36044
|
const network = walletNetworkOf(review.network);
|
|
35410
|
-
if (
|
|
36045
|
+
if (review.method === "arkade" && !this.arkWallets[network].adapter || review.method === "bark" && !this.barkWallets[network].adapter || review.method === "spark" && !this.sparkWallets[network].adapter || review.method === "usdt" && !this.usdtWallets[network].adapter || review.method === "bitcoin" && !this.bitcoins[network].sources.active || review.method === "fedimint" && !this.fedimintWallets[network].federation(review.provider)) continue;
|
|
35411
36046
|
await this.reconcilePayment({ id: review.id }).catch(() => {});
|
|
35412
36047
|
}
|
|
35413
36048
|
} finally {
|
|
@@ -35460,6 +36095,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35460
36095
|
if (!stored.group) this.fileDesk.restore(stored.id, storedFiles);
|
|
35461
36096
|
const files = linkFilesFrom(stored.id, storedFiles, messages);
|
|
35462
36097
|
this.links.set(stored.id, newLiveLink(stored, messages[messages.length - 1]?.timestamp ?? 0, files));
|
|
36098
|
+
if (!stored.group) this.links.get(stored.id).waitingSends = Promise.resolve(new Set(messages.filter(waitsForLive).map((m) => m.id)));
|
|
35463
36099
|
history.set(stored.id, messages);
|
|
35464
36100
|
for (const message of messages.slice(-20)) this.clockSample(message);
|
|
35465
36101
|
const note = latestReaction(messages);
|
|
@@ -35913,10 +36549,12 @@ var GhostlyNode = class GhostlyNode {
|
|
|
35913
36549
|
this.publicActivity.clear();
|
|
35914
36550
|
for (const queue of this.editQueues.values()) queue.stop();
|
|
35915
36551
|
this.groupEdits.stop();
|
|
36552
|
+
this.reactions.stop();
|
|
35916
36553
|
this.cardEdits.stop();
|
|
35917
36554
|
await Promise.allSettled([
|
|
35918
36555
|
linksStopped,
|
|
35919
36556
|
links?.stop(),
|
|
36557
|
+
this.groups.flush(),
|
|
35920
36558
|
...WALLET_NETWORKS.flatMap((network) => [
|
|
35921
36559
|
this.arkWallets,
|
|
35922
36560
|
this.barkWallets,
|
|
@@ -36116,14 +36754,23 @@ var GhostlyNode = class GhostlyNode {
|
|
|
36116
36754
|
edgeView(groupId, member) {
|
|
36117
36755
|
for (const live of this.links.values()) if (live.stored.group === groupId && live.stored.groupPeer === member && !live.stored.groupEntry) return edgeView(live, Date.now(), this.groupNativeWaiting.has(live.stored.id));
|
|
36118
36756
|
}
|
|
36119
|
-
|
|
36120
|
-
|
|
36757
|
+
/** A chat's or a group's history, as a page gets it (`withoutFrame`). */
|
|
36758
|
+
async getMessages(linkId) {
|
|
36759
|
+
return (await db.getMessages(linkId)).map(withoutFrame);
|
|
36121
36760
|
}
|
|
36122
36761
|
/** One message of a chat, read by its id: the whole history costs a read of every row. */
|
|
36123
|
-
getMessage(linkId, id) {
|
|
36124
|
-
|
|
36762
|
+
async getMessage(linkId, id) {
|
|
36763
|
+
const message = await db.getMessage(linkId, id);
|
|
36764
|
+
return message && withoutFrame(message);
|
|
36765
|
+
}
|
|
36766
|
+
async messagePage(asked) {
|
|
36767
|
+
const page = await this.storedPage(asked);
|
|
36768
|
+
return {
|
|
36769
|
+
...page,
|
|
36770
|
+
messages: page.messages.map(withoutFrame)
|
|
36771
|
+
};
|
|
36125
36772
|
}
|
|
36126
|
-
async
|
|
36773
|
+
async storedPage({ linkId, limit = 50, before }) {
|
|
36127
36774
|
if (!Number.isSafeInteger(limit) || limit < 1) throw new Error("limit: a whole number of messages, at least 1");
|
|
36128
36775
|
if (before === void 0) return db.getMessagePage(linkId, { limit });
|
|
36129
36776
|
if (typeof before === "number") {
|
|
@@ -37448,11 +38095,18 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37448
38095
|
outbox = new Outbox({
|
|
37449
38096
|
read: () => db.getMessages(linkId),
|
|
37450
38097
|
update: async (id, delivery, error, extra) => {
|
|
37451
|
-
await db.
|
|
37452
|
-
|
|
37453
|
-
|
|
37454
|
-
|
|
37455
|
-
|
|
38098
|
+
if (delivery === "delivered") await db.patchMessage(linkId, id, (message) => message.delivery === "delivered" ? null : {
|
|
38099
|
+
...message.delivery && {
|
|
38100
|
+
...extra,
|
|
38101
|
+
delivery,
|
|
38102
|
+
deliveryError: error
|
|
38103
|
+
},
|
|
38104
|
+
details: {
|
|
38105
|
+
...message.details,
|
|
38106
|
+
receiptAt: Date.now()
|
|
38107
|
+
}
|
|
38108
|
+
}, true);
|
|
38109
|
+
else await db.updateDelivery(linkId, id, delivery, error, extra);
|
|
37456
38110
|
if (delivery === "sent" || delivery === "delivered") {
|
|
37457
38111
|
const message = await db.getMessage(linkId, id);
|
|
37458
38112
|
if (message) this.messageFeedback("sent", message);
|
|
@@ -37543,7 +38197,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
37543
38197
|
}
|
|
37544
38198
|
/** Adds to a message's details record, whatever the row's delivery state. Nothing is pushed for it: the view asks. */
|
|
37545
38199
|
noteDetails(linkId, id, change) {
|
|
37546
|
-
return db.patchMessage(linkId, id, (message) => ({ details: change(message.details ?? {}, message) })).then(() => {}, () => {});
|
|
38200
|
+
return db.patchMessage(linkId, id, (message) => ({ details: change(message.details ?? {}, message) }), true).then(() => {}, () => {});
|
|
37547
38201
|
}
|
|
37548
38202
|
/** One send of a text on its details: the path, the frame and its size, and the DHT envelope when that was the way. */
|
|
37549
38203
|
noteTextSend(linkId, message, snapshot, at, error, link) {
|
|
@@ -38181,40 +38835,59 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38181
38835
|
const live = this.links.get(linkId);
|
|
38182
38836
|
if (!live?.link?.isDataLinkOpen) return;
|
|
38183
38837
|
const withTexts = this.outboxFor(linkId).sendsFiles;
|
|
38184
|
-
const
|
|
38838
|
+
const known = await this.waitingSendsOf(live), waiting = [];
|
|
38839
|
+
for (const id of [...known]) {
|
|
38840
|
+
const message = await db.getMessage(linkId, id);
|
|
38841
|
+
if (!message || !waitsForLive(message)) known.delete(id);
|
|
38842
|
+
else if (message.file && !withTexts || message.paymentId) waiting.push(message);
|
|
38843
|
+
}
|
|
38844
|
+
waiting.sort((a, b) => a.timestamp - b.timestamp);
|
|
38185
38845
|
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
|
|
38846
|
+
for (const message of waiting) {
|
|
38847
|
+
if (message.file) {
|
|
38848
|
+
if (!GhostlyNode.takesFiles(live.link)) {
|
|
38849
|
+
await db.updateDelivery(linkId, message.id, "failed", "Your contact's app cannot receive files.");
|
|
38850
|
+
continue;
|
|
38213
38851
|
}
|
|
38214
|
-
|
|
38215
|
-
|
|
38852
|
+
const file = message.file, wireId = file.id.slice(`${linkId}-out-`.length);
|
|
38853
|
+
await db.putMessage(sentNow(message));
|
|
38854
|
+
this.transferFile(live, file, wireId, message.timestamp, (error) => {
|
|
38855
|
+
this.transfers.set(file.id, {
|
|
38856
|
+
state: "failed",
|
|
38857
|
+
transferred: 0,
|
|
38858
|
+
size: file.size,
|
|
38859
|
+
error
|
|
38860
|
+
});
|
|
38861
|
+
this.emitState();
|
|
38862
|
+
}, GhostlyNode.wireReply(message));
|
|
38863
|
+
} else if (live.link.supportsPayments) {
|
|
38864
|
+
const at = Date.now();
|
|
38865
|
+
await db.putMessage({
|
|
38866
|
+
...sentNow(message),
|
|
38867
|
+
details: {
|
|
38868
|
+
...withSend(message.details, {
|
|
38869
|
+
at,
|
|
38870
|
+
...pathSnapshot(live, "datalink"),
|
|
38871
|
+
result: "sent"
|
|
38872
|
+
}),
|
|
38873
|
+
sentAt: at
|
|
38874
|
+
}
|
|
38875
|
+
});
|
|
38876
|
+
} else await db.updateDelivery(linkId, message.id, "failed", "Your contact's app cannot receive payment requests.");
|
|
38877
|
+
known.delete(message.id);
|
|
38878
|
+
}
|
|
38216
38879
|
if (waiting.length) await this.messagesChanged(linkId, waiting.map((m) => m.id));
|
|
38217
38880
|
}
|
|
38881
|
+
/**
|
|
38882
|
+
* The ids of my messages that wait for the live link (`waitsForLive`). A chat loaded at the start has them from that
|
|
38883
|
+
* read; any other reads its history once in a run. A session that opens then reads those rows, however long the chat.
|
|
38884
|
+
*/
|
|
38885
|
+
waitingSendsOf(live) {
|
|
38886
|
+
return live.waitingSends ??= db.getMessages(live.stored.id).then((history) => new Set(history.filter(waitsForLive).map((m) => m.id)), (error) => {
|
|
38887
|
+
live.waitingSends = void 0;
|
|
38888
|
+
throw error;
|
|
38889
|
+
});
|
|
38890
|
+
}
|
|
38218
38891
|
receiveFile(linkId, wire) {
|
|
38219
38892
|
const files = this.links.get(linkId)?.files;
|
|
38220
38893
|
if (!files) return "refused";
|
|
@@ -38662,8 +39335,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
38662
39335
|
};
|
|
38663
39336
|
return this.groups.fileCheck(groupId);
|
|
38664
39337
|
}
|
|
38665
|
-
groupMessages({ groupId }) {
|
|
38666
|
-
return this.groups.messages(groupId);
|
|
39338
|
+
async groupMessages({ groupId }) {
|
|
39339
|
+
return (await this.groups.messages(groupId)).map(withoutFrame);
|
|
38667
39340
|
}
|
|
38668
39341
|
/**
|
|
38669
39342
|
* Presses a button of a message someone else sent to a chat or a group (`group:<id>`), WISP 406 · Message Buttons:
|
|
@@ -40434,8 +41107,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40434
41107
|
if (this.options.singleDevice) return;
|
|
40435
41108
|
const record = await readDeviceRecord(databaseName()).catch(() => null);
|
|
40436
41109
|
if (!record || record.state !== "active") return;
|
|
40437
|
-
const verifier = await this.handoffVerifier()
|
|
40438
|
-
if (verifier
|
|
41110
|
+
const verifier = await this.handoffVerifier();
|
|
41111
|
+
if (verifier ? JSON.stringify(verifier) !== JSON.stringify(record.verifier) : record.verifier) await amendDevice(databaseName(), { verifier: verifier ?? void 0 });
|
|
40439
41112
|
}
|
|
40440
41113
|
/**
|
|
40441
41114
|
* Copies the network settings into the device record (WISP 06, `devices/network.ts`), where device-link-only mode
|
|
@@ -40519,8 +41192,12 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40519
41192
|
this.emitState();
|
|
40520
41193
|
await recoverEnrollment(databaseName(), this.turnNetwork()).catch(() => false);
|
|
40521
41194
|
await this.syncDeviceVerifier().catch(() => {});
|
|
40522
|
-
const
|
|
40523
|
-
|
|
41195
|
+
const keeper = await this.deviceLinks.turnKeeper();
|
|
41196
|
+
const outcome = await keeper?.check(false);
|
|
41197
|
+
if (outcome?.kind === "go-on" && !outcome.restricted) {
|
|
41198
|
+
this.noteGoodTurn();
|
|
41199
|
+
await keeper?.stateVerifier().catch(() => null);
|
|
41200
|
+
}
|
|
40524
41201
|
if (outcome?.kind === "gated" && !this.shuttingDown) {
|
|
40525
41202
|
const record = await readDeviceRecord(databaseName());
|
|
40526
41203
|
if (record) await this.stopReplaced(viewOf(record));
|
|
@@ -40556,7 +41233,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40556
41233
|
if (knownDeviceGate()?.state !== "active") return;
|
|
40557
41234
|
const outcome = await links.checkTurn(false).catch(() => null);
|
|
40558
41235
|
if (this.shuttingDown || this.gatedOut) return;
|
|
40559
|
-
if (outcome?.kind === "go-on" && !outcome.restricted)
|
|
41236
|
+
if (outcome?.kind === "go-on" && !outcome.restricted) {
|
|
41237
|
+
this.noteGoodTurn();
|
|
41238
|
+
await this.stateDeviceVerifier();
|
|
41239
|
+
}
|
|
40560
41240
|
if (outcome?.kind === "gated") {
|
|
40561
41241
|
const record = await readDeviceRecord(databaseName()).catch(() => null);
|
|
40562
41242
|
if (record) await this.stopReplaced(viewOf(record));
|
|
@@ -40870,16 +41550,45 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40870
41550
|
if (this.options.singleDevice) return;
|
|
40871
41551
|
if (typeof password !== "string" || !password) throw new Error("A password is needed");
|
|
40872
41552
|
const stored = await this.handoffVerifier();
|
|
40873
|
-
|
|
41553
|
+
const active = knownDeviceGate()?.state === "active";
|
|
41554
|
+
const ready = await handoffPakeReady().then(() => true, () => false);
|
|
41555
|
+
if (stored && !ready) {
|
|
41556
|
+
if (password !== current) await this.dropHandoffVerifier();
|
|
41557
|
+
throw new Error("The password proof could not be loaded");
|
|
41558
|
+
}
|
|
41559
|
+
if (stored && active && !await provesHandoffPassword(stored, typeof current === "string" ? current : "")) throw new Error("handoff-password: The current password is wrong.");
|
|
41560
|
+
if (stored && await provesHandoffPassword(stored, password)) {
|
|
41561
|
+
await this.syncDeviceVerifier().catch(() => {});
|
|
41562
|
+
await this.stateDeviceVerifier();
|
|
41563
|
+
return;
|
|
41564
|
+
}
|
|
40874
41565
|
try {
|
|
40875
41566
|
const verifier = await makeHandoffVerifier(password);
|
|
40876
41567
|
await wrap((await store(STORES$1.settings, "readwrite")).put(verifier, GhostlyNode.HANDOFF_VERIFIER));
|
|
40877
41568
|
await this.syncDeviceVerifier().catch(() => {});
|
|
41569
|
+
await this.stateDeviceVerifier();
|
|
40878
41570
|
} catch (error) {
|
|
40879
|
-
await
|
|
41571
|
+
await this.dropHandoffVerifier();
|
|
40880
41572
|
throw error;
|
|
40881
41573
|
}
|
|
40882
41574
|
}
|
|
41575
|
+
/** The profile has no verifier any more: its settings, the device state, and what the turn record states. */
|
|
41576
|
+
async dropHandoffVerifier() {
|
|
41577
|
+
await wrap((await store(STORES$1.settings, "readwrite")).delete(GhostlyNode.HANDOFF_VERIFIER)).catch(() => {});
|
|
41578
|
+
await this.syncDeviceVerifier().catch(() => {});
|
|
41579
|
+
await this.stateDeviceVerifier();
|
|
41580
|
+
}
|
|
41581
|
+
/**
|
|
41582
|
+
* The turn record states which verifier the profile has now (WISP 06 § Record, the verifier mark), so a standby that
|
|
41583
|
+
* keeps an older one does not take over with the password it checks. Asked after the device state took the
|
|
41584
|
+
* verifier: at start, at each change, and after a good read of the turn (a change made offline is stated then).
|
|
41585
|
+
*/
|
|
41586
|
+
async stateDeviceVerifier() {
|
|
41587
|
+
if (this.options.singleDevice || this.shuttingDown || this.gatedOut) return;
|
|
41588
|
+
try {
|
|
41589
|
+
await (await this.deviceLinks?.turnKeeper())?.stateVerifier();
|
|
41590
|
+
} catch {}
|
|
41591
|
+
}
|
|
40883
41592
|
/**
|
|
40884
41593
|
* The wallets in a handoff to `taker` (WISP 06 § Wallets, `planHandoffWallets`): what moves, what stays home, and
|
|
40885
41594
|
* what keeps the profile here now. `view`: the wallets as they are now, read fresh by default.
|
|
@@ -40984,10 +41693,10 @@ var GhostlyNode = class GhostlyNode {
|
|
|
40984
41693
|
}
|
|
40985
41694
|
await writeWalletHomes(plan.wallets);
|
|
40986
41695
|
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().
|
|
41696
|
+
const verifier = await this.handoffVerifier().then((value) => ({ verifier: value ?? void 0 }), () => ({}));
|
|
40988
41697
|
await moveDevice(profile, "releasing", {
|
|
40989
41698
|
...patch,
|
|
40990
|
-
...verifier
|
|
41699
|
+
...verifier,
|
|
40991
41700
|
breezDatabases: breezDatabases.length ? breezDatabases : void 0
|
|
40992
41701
|
});
|
|
40993
41702
|
this.events.onDeviceGate?.({
|
|
@@ -41443,6 +42152,9 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41443
42152
|
this.directPath.note(peer, evidence);
|
|
41444
42153
|
if (!entry && evidence !== "open" && evidence !== "closed") this.edgeWithoutRtc(linkId);
|
|
41445
42154
|
},
|
|
42155
|
+
onAnswersRefused: () => {
|
|
42156
|
+
if (!entry) this.edgeWithoutRtc(linkId);
|
|
42157
|
+
},
|
|
41446
42158
|
onDataLinkState: (state) => {
|
|
41447
42159
|
traceJoin(group, `link.${state}`, { role });
|
|
41448
42160
|
if (state === "open" || live.dataLink === "open") live.lastSyncAt = Date.now();
|
|
@@ -41613,6 +42325,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
41613
42325
|
role: stored.inviteCode ? "inviter" : "joiner",
|
|
41614
42326
|
startedAt: stored.createdAt
|
|
41615
42327
|
} : void 0,
|
|
42328
|
+
madeAt: stored.createdAt,
|
|
41616
42329
|
transport: this.transport,
|
|
41617
42330
|
nick: this.sharedNick,
|
|
41618
42331
|
avatar: this.sharedAvatar,
|
|
@@ -42006,7 +42719,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42006
42719
|
get nativeFactories() {
|
|
42007
42720
|
const relay = this.relaysHyperdht ? this.hyperdhtRelay : "";
|
|
42008
42721
|
return {
|
|
42009
|
-
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-
|
|
42722
|
+
...relay ? { "hyperdht/1": async (seedB64) => (await import("./hyperdhtRelay-hKC9DpIH.js")).createRelayedHyperEndpoint(seedB64, relay) } : {},
|
|
42010
42723
|
...this.options.nativeTransports,
|
|
42011
42724
|
...this.ownIrohRelays ? { "iroh/1": (seedB64) => this.options.nativeTransports["iroh/1"](seedB64, this.settings.irohRelays?.length ? { relays: [...this.settings.irohRelays] } : {}) } : {},
|
|
42012
42725
|
...this.options.irohWeb ? { "iroh/1": (seedB64) => createIrohWebEndpoint(seedB64, { relays: this.irohRelays }) } : {}
|
|
@@ -42121,6 +42834,14 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42121
42834
|
this.emitState();
|
|
42122
42835
|
});
|
|
42123
42836
|
}
|
|
42837
|
+
/** The listeners this app may hold for a transport: sixteen for Iroh's browser build (`IROH_WEB_SLOTS`), eight for the others. */
|
|
42838
|
+
nativeSlots(key) {
|
|
42839
|
+
return key === "iroh/1" && this.options.irohWeb ? IROH_WEB_SLOTS : NATIVE_SLOTS;
|
|
42840
|
+
}
|
|
42841
|
+
/** Of those, what its groups' links may hold, all groups together. */
|
|
42842
|
+
groupNativeSlots(key) {
|
|
42843
|
+
return this.nativeSlots(key) - CHAT_NATIVE_SLOTS;
|
|
42844
|
+
}
|
|
42124
42845
|
/**
|
|
42125
42846
|
* One transport's listener for one chat, in that transport's queue. "no-slot" when a group's link found no slot free,
|
|
42126
42847
|
* "failed" when the listener could not start.
|
|
@@ -42134,11 +42855,13 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42134
42855
|
live.transportErrors ??= {};
|
|
42135
42856
|
try {
|
|
42136
42857
|
const owners = [...this.links.values()].filter((other) => other.link?.availableTransports.includes(key));
|
|
42137
|
-
|
|
42138
|
-
if (owners.length >=
|
|
42858
|
+
const slots = this.nativeSlots(key), groups = owners.filter((other) => other.stored.group).length;
|
|
42859
|
+
if (group && (owners.length >= slots || groups >= this.groupNativeSlots(key))) return "no-slot";
|
|
42860
|
+
const full = owners.length >= slots;
|
|
42861
|
+
if (full || !group && owners.length - groups >= NATIVE_SLOTS) {
|
|
42139
42862
|
const now = Date.now();
|
|
42140
42863
|
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);
|
|
42864
|
+
const victim = (!full ? void 0 : owners.filter((other) => other.stored.group && other.link?.canReleaseEndpoint(key)).sort((a, b) => (a.link?.isDataLinkOpen ? 1 : 0) - (b.link?.isDataLinkOpen ? 1 : 0))[0]) ?? (taking ? owners.filter((other) => other !== live && other.stored.id !== this.activeLinkId && other.link?.canReleaseEndpoint(key) && now - (other.nativeTakenAt?.[key] ?? 0) >= 12e4).sort((a, b) => a.lastMessageAt - b.lastMessageAt)[0] : void 0);
|
|
42142
42865
|
const idle = victim || !taking || !this.reachedOnlyNatively(live) ? void 0 : owners.filter((other) => other !== live && !other.stored.group && other.stored.id !== this.activeLinkId && !this.fileDesk.moving(other.stored.id) && other.link?.canYieldEndpoint(key, 12e4)).sort((a, b) => a.link.lastActivityAt - b.link.lastActivityAt)[0];
|
|
42143
42866
|
const idleFor = idle ? now - idle.link.lastActivityAt : 0;
|
|
42144
42867
|
if (idle && await idle.link.yieldEndpoint(key, 12e4)) {
|
|
@@ -42159,7 +42882,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42159
42882
|
active: this.activeLinkId === linkId
|
|
42160
42883
|
});
|
|
42161
42884
|
if (this.activeLinkId === linkId && !group) this.retryActiveSlot(linkId);
|
|
42162
|
-
throw new Error("All
|
|
42885
|
+
throw new Error("All native connection slots are in use. This chat takes one once a chat live over one has been quiet for 2 minutes. Disconnect a native connection in another chat to free one now.");
|
|
42163
42886
|
} else {
|
|
42164
42887
|
await victim.link.releaseEndpoint(key);
|
|
42165
42888
|
victim.transportErrors ??= {};
|
|
@@ -42309,7 +43032,8 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42309
43032
|
...whole.file && {
|
|
42310
43033
|
file: whole.file,
|
|
42311
43034
|
text: whole.text
|
|
42312
|
-
}
|
|
43035
|
+
},
|
|
43036
|
+
...whole.frame && { frame: whole.frame }
|
|
42313
43037
|
};
|
|
42314
43038
|
if (!Object.keys(added).length) return;
|
|
42315
43039
|
if (!await db.patchMessage(message.linkId, message.id, (stored) => {
|
|
@@ -42335,12 +43059,12 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42335
43059
|
}
|
|
42336
43060
|
}
|
|
42337
43061
|
const onChanges = this.events.onMessageChanges;
|
|
42338
|
-
if (!onChanges) return this.events.onMessages(linkId, await
|
|
43062
|
+
if (!onChanges) return this.events.onMessages(linkId, await this.getMessages(linkId));
|
|
42339
43063
|
const unique = [...new Set(ids)];
|
|
42340
43064
|
if (!unique.length) return;
|
|
42341
43065
|
const rows = await Promise.all(unique.map((id) => db.getMessage(linkId, id)));
|
|
42342
43066
|
onChanges.call(this.events, linkId, {
|
|
42343
|
-
messages: rows.filter((row) => !!row),
|
|
43067
|
+
messages: rows.filter((row) => !!row).map(withoutFrame),
|
|
42344
43068
|
deleted: unique.filter((_, i) => !rows[i])
|
|
42345
43069
|
});
|
|
42346
43070
|
}
|
|
@@ -42435,6 +43159,7 @@ var GhostlyNode = class GhostlyNode {
|
|
|
42435
43159
|
};
|
|
42436
43160
|
}
|
|
42437
43161
|
if (!await db.addMessage(message)) return false;
|
|
43162
|
+
if (live && waitsForLive(message)) live.waitingSends?.then((known) => known.add(message.id), () => {});
|
|
42438
43163
|
this.clockSample(message);
|
|
42439
43164
|
const newest = this.newestAt.get(message.linkId);
|
|
42440
43165
|
if (!message.event) this.newestAt.set(message.linkId, Math.max(newest ?? 0, writtenAt(message, this.clockLeads.get(message.linkId))));
|
|
@@ -42811,4 +43536,4 @@ function signalKind(signal) {
|
|
|
42811
43536
|
//#endregion
|
|
42812
43537
|
export { EngineServer, hyperdhtRelayProblem as n, server_exports as t };
|
|
42813
43538
|
|
|
42814
|
-
//# sourceMappingURL=server-
|
|
43539
|
+
//# sourceMappingURL=server-DHMXMwNn.js.map
|