fix: un rejet tardif ne ferme plus un canal vivant, un registre illisible ne perd plus toutes les inbox
This commit is contained in:
@@ -57,7 +57,7 @@
|
||||
|
||||
import { getCaps, getCurrentUser } from "../shared-wallet/bootstrap";
|
||||
import { accessLogPrefix, logStage, shortNuri } from "../shared-wallet/access-log";
|
||||
import { myInboxes } from "./branch-registers";
|
||||
import { enumerateMyInboxes, type InboxEnumeration } from "./branch-registers";
|
||||
import { drainInboxSerially, drainsSettled } from "./inbox-drain";
|
||||
import { lookupAccount } from "../shared-wallet/account-registry";
|
||||
import { processInbox } from "../surface/inbox";
|
||||
@@ -78,12 +78,6 @@ interface Observation {
|
||||
register: Unsubscribe | null;
|
||||
/** Unsubscribe from the held-caps change signal — the second "which inboxes" channel. */
|
||||
holdings: Unsubscribe | null;
|
||||
/**
|
||||
* The inboxes whose subscription has already been re-attempted once after failing to be
|
||||
* OPENED. What bounds the repair below to one extra try per inbox, so a document the broker
|
||||
* genuinely cannot serve costs two calls rather than a loop.
|
||||
*/
|
||||
reattempted: Set<Nuri>;
|
||||
/** True while a re-enumeration is running, so its own effects do not restart it. */
|
||||
enumerating: boolean;
|
||||
/** A trigger that arrived mid-cycle: the running one repeats once rather than lose it. */
|
||||
@@ -171,19 +165,35 @@ async function applyWhatArrived(obs: Observation, inbox: Nuri): Promise<void> {
|
||||
*
|
||||
* Never removes: the list only grows within a session (an `AddInboxCap` record is durable),
|
||||
* and dropping a subscription on a guess would silently stop applying an inbox.
|
||||
*
|
||||
* ── A list that came back SHORT still gets watched ────────────────────────
|
||||
* `enumerateMyInboxes` and not `myInboxes`: the list is read from two independent registers
|
||||
* and one of them being unreachable used to discard the other's answer too, so a broker
|
||||
* hiccup spanning sign-in left the identity connected with ZERO inboxes watched rather than
|
||||
* with one register's worth missing. Whatever came back is watched here and now — including,
|
||||
* crucially, the user's own two inboxes, which are where a share addressed to a PERSON
|
||||
* lands, and therefore the channel by which a read-only identity converges at all.
|
||||
*
|
||||
* The shortfall is reported, never passed over: an incomplete list is not a complete one,
|
||||
* and the inboxes it is missing are watched on the next enumeration — which is what the
|
||||
* register push and the holdings signal are for. Applying what lands in a user inbox is
|
||||
* itself one of those signals (it files a cap), so the ordinary case repairs itself.
|
||||
*/
|
||||
async function watchTheInboxes(obs: Observation): Promise<void> {
|
||||
if (!current(obs)) return;
|
||||
let inboxes: Nuri[];
|
||||
let listed: InboxEnumeration;
|
||||
try {
|
||||
inboxes = await myInboxes();
|
||||
listed = await enumerateMyInboxes();
|
||||
} catch (error) {
|
||||
// Not knowing WHICH inboxes exist is the whole observation failing, not one queue.
|
||||
// Not knowing WHICH inboxes exist AT ALL is the whole observation failing, not one queue.
|
||||
reportUnobserved("the inboxes to watch could not be listed", error);
|
||||
return;
|
||||
}
|
||||
if (!current(obs)) return;
|
||||
for (const inbox of inboxes) {
|
||||
if (listed.incomplete !== null) {
|
||||
reportUnobserved("the inboxes to watch could not all be listed", listed.incomplete.error);
|
||||
}
|
||||
for (const inbox of listed.inboxes) {
|
||||
if (obs.inboxes.has(inbox)) continue;
|
||||
try {
|
||||
// The push is the signal; the drain is the work. The FIRST push is the initial
|
||||
@@ -212,20 +222,28 @@ async function watchTheInboxes(obs: Observation): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* The subscription on `inbox` could not be OPENED — report it, forget it, and try once more.
|
||||
* The subscription on `inbox` could not be OPENED — report it, and forget it.
|
||||
*
|
||||
* **Forget it**, because the entry this observation holds is the whole record of "already
|
||||
* watched": leaving a dead one in place is how one rejection at connection turned into a
|
||||
* session-long silence. Removed, the next enumeration subscribes again as if it had never
|
||||
* been attempted.
|
||||
*
|
||||
* **Try once more**, because the usual cause is a repo the verifier has not synced yet, and
|
||||
* the whole cost of finding out is one call. Exactly one extra attempt per inbox, tracked in
|
||||
* {@link Observation.reattempted}: a broker that genuinely cannot serve this document must
|
||||
* not be asked again and again by a loop that has no reason to stop. If the second attempt
|
||||
* fails too, the failure has been reported twice and the inbox waits for the next
|
||||
* enumeration signal — or for the next connection, where an unapplied deposit has always
|
||||
* waited.
|
||||
* ── And that is ALL it does: recovery waits for the next SIGNAL ───────────
|
||||
* Until 2026-08-17 this also re-enumerated on the spot, once per inbox. That call runs
|
||||
* synchronously out of the very rejection it is reacting to, so it asks the broker that has
|
||||
* just refused, in the same turn, with nothing having changed — and spends the one extra
|
||||
* attempt it was allowed doing it. Deleting it left the whole suite green, which is the
|
||||
* measure of what it achieved.
|
||||
*
|
||||
* What actually repairs this is an event, and this module already listens to the two that
|
||||
* exist: the register push (`watchTheRegister`) and the held-caps signal
|
||||
* (`CapRegistry.onChange`), each of which re-enters {@link enumerate} and finds this inbox
|
||||
* unwatched. Applying anything at all fires the second one, so a session that is doing
|
||||
* something catches up on its own. A session that is doing nothing waits for its next
|
||||
* connection — where an unapplied deposit has always waited, since an inbox is not consumed
|
||||
* by being unread. Polling for it is ruled out here for the same reason it is in
|
||||
* `inbox-processor.ts`: this regime is push-driven, and a timer is what it replaced.
|
||||
*/
|
||||
function watchFailed(obs: Observation, inbox: Nuri, error: unknown): void {
|
||||
if (!current(obs)) return;
|
||||
@@ -239,9 +257,6 @@ function watchFailed(obs: Observation, inbox: Nuri, error: unknown): void {
|
||||
}
|
||||
}
|
||||
reportUnobserved("this inbox could not be watched: " + shortNuri(inbox), error);
|
||||
if (obs.reattempted.has(inbox)) return;
|
||||
obs.reattempted.add(inbox);
|
||||
track(obs, enumerate(obs));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,7 +352,6 @@ export async function startObservingInboxes(): Promise<void> {
|
||||
inboxes: new Map(),
|
||||
register: null,
|
||||
holdings: null,
|
||||
reattempted: new Set(),
|
||||
enumerating: false,
|
||||
enumerateAgain: false,
|
||||
pending: new Set(),
|
||||
|
||||
Reference in New Issue
Block a user