Skip to content

Commit d469c85

Browse files
committed
Keep registered status when a stale concurrent registration attempt fails
1 parent a31735c commit d469c85

2 files changed

Lines changed: 77 additions & 11 deletions

File tree

apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,48 @@ describe("makeRelayDeviceRegistrationRequest", () => {
535535
}).pipe(Effect.provide(relayTestLayer));
536536
});
537537

538+
it.effect("keeps the registered status when a stale concurrent attempt fails", () => {
539+
const fetchMock = vi.fn((request: RequestInfo | URL) => {
540+
const url = request instanceof Request ? request.url : String(request);
541+
return Promise.resolve(
542+
Response.json(
543+
url.endsWith("/v1/client/dpop-token")
544+
? {
545+
access_token: "relay-dpop-token",
546+
issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
547+
token_type: "DPoP",
548+
expires_in: 300,
549+
scope: "mobile:registration",
550+
}
551+
: { ok: true },
552+
),
553+
);
554+
});
555+
vi.stubGlobal("fetch", fetchMock);
556+
Constants.expoConfig!.extra = {
557+
relay: {
558+
url: "https://relay.example.test/",
559+
},
560+
};
561+
562+
// Sign-in enqueues a registration attempt that stays parked in the
563+
// background queue while the direct refresh below runs.
564+
setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a"));
565+
566+
return Effect.gen(function* () {
567+
yield* refreshAgentAwarenessRegistration();
568+
expect(getAgentAwarenessRegistrationStatus()).toBe("registered");
569+
570+
// The queued sign-in attempt now fails; its stale failure must not
571+
// overwrite the registration the relay already accepted.
572+
vi.mocked(loadOrCreateAgentAwarenessDeviceId).mockRejectedValueOnce(
573+
new Error("device id unavailable"),
574+
);
575+
yield* runBackgroundOperations();
576+
expect(getAgentAwarenessRegistrationStatus()).toBe("registered");
577+
}).pipe(Effect.provide(relayTestLayer));
578+
});
579+
538580
it("clears registration status on cloud sign-out", () => {
539581
setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a"));
540582
setAgentAwarenessRelayTokenProvider(null);

apps/mobile/src/features/agent-awareness/remoteRegistration.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ export type AgentAwarenessRegistrationStatus = "unknown" | "pending" | "register
7474
let registrationStatus: AgentAwarenessRegistrationStatus = "unknown";
7575
const registrationStatusListeners = new Set<() => void>();
7676

77+
// Registration runs both through the background queue and directly via
78+
// refreshAgentAwarenessRegistration, so attempts can overlap. Counting relay
79+
// acceptances lets a failing attempt detect that a concurrent attempt already
80+
// succeeded while it was in flight, so a stale failure cannot overwrite a
81+
// registration the relay accepted.
82+
let registrationSuccessCount = 0;
83+
7784
function setRegistrationStatus(next: AgentAwarenessRegistrationStatus): void {
7885
if (registrationStatus === next) {
7986
return;
@@ -84,6 +91,18 @@ function setRegistrationStatus(next: AgentAwarenessRegistrationStatus): void {
8491
}
8592
}
8693

94+
function markRegistrationSucceeded(): void {
95+
registrationSuccessCount++;
96+
setRegistrationStatus("registered");
97+
}
98+
99+
function markRegistrationFailed(successCountAtAttemptStart: number): void {
100+
if (registrationSuccessCount !== successCountAtAttemptStart) {
101+
return;
102+
}
103+
setRegistrationStatus("failed");
104+
}
105+
87106
export function getAgentAwarenessRegistrationStatus(): AgentAwarenessRegistrationStatus {
88107
return registrationStatus;
89108
}
@@ -316,7 +335,7 @@ function registerDeviceWithRelay(
316335
catch: (cause) => cause,
317336
}).pipe(Effect.orElseSucceed(() => null));
318337
if (persisted && persisted.identity === identity && persisted.signature === signature) {
319-
setRegistrationStatus("registered");
338+
markRegistrationSucceeded();
320339
logRegistrationDebug("relay device registration skipped; already registered for account", {
321340
expectedGeneration,
322341
});
@@ -331,7 +350,7 @@ function registerDeviceWithRelay(
331350
clerkToken: token,
332351
payload: body,
333352
});
334-
setRegistrationStatus("registered");
353+
markRegistrationSucceeded();
335354
yield* Effect.promise(() =>
336355
saveAgentAwarenessRegistrationRecord({ identity, signature }).catch((error: unknown) => {
337356
logRegistrationError("persist registration record failed", error);
@@ -466,11 +485,12 @@ function startPendingDeviceRegistration(): void {
466485
};
467486
activeDeviceRegistration = registration;
468487
registration.operation = (async () => {
488+
const successCountAtStart = registrationSuccessCount;
469489
const result = await settleAsyncResult(() =>
470490
runtime.runPromiseExit(registerDevice(next.input, generation)),
471491
);
472492
if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
473-
setRegistrationStatus("failed");
493+
markRegistrationFailed(successCountAtStart);
474494
logRegistrationError(next.context, squashAtomCommandFailure(result));
475495
}
476496
logRegistrationDebug("device registration finished", { generation });
@@ -675,14 +695,17 @@ export function refreshAgentAwarenessRegistration(): Effect.Effect<
675695
never,
676696
ManagedRelay.ManagedRelayClient
677697
> {
678-
return registerDeviceForCurrentUser().pipe(
679-
Effect.catch((error) =>
680-
Effect.sync(() => {
681-
setRegistrationStatus("failed");
682-
logRegistrationError("device registration refresh failed", error);
683-
}),
684-
),
685-
);
698+
return Effect.suspend(() => {
699+
const successCountAtStart = registrationSuccessCount;
700+
return registerDeviceForCurrentUser().pipe(
701+
Effect.catch((error) =>
702+
Effect.sync(() => {
703+
markRegistrationFailed(successCountAtStart);
704+
logRegistrationError("device registration refresh failed", error);
705+
}),
706+
),
707+
);
708+
});
686709
}
687710

688711
export function __resetAgentAwarenessRemoteRegistrationForTest(): void {
@@ -703,6 +726,7 @@ export function __resetAgentAwarenessRemoteRegistrationForTest(): void {
703726
activeDeviceRegistration = null;
704727
pendingDeviceRegistration = null;
705728
registrationStatus = "unknown";
729+
registrationSuccessCount = 0;
706730
registrationStatusListeners.clear();
707731
}
708732

0 commit comments

Comments
 (0)