Skip to content

Commit 5fa1eaf

Browse files
mikepitreclaude
andcommitted
fix(expo): ignore session activity in the native sync fingerprint
Session updatedAt can move on session activity and token refresh, which would refresh the native client on every token refresh. Profile edits still reach native through the active user's updatedAt. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent fe9ed7f commit 5fa1eaf

4 files changed

Lines changed: 36 additions & 8 deletions

File tree

‎packages/expo/src/provider/__tests__/nativeClientSyncBehavior/fakeClerk.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { FakeClerkServer, FakeServerClientJSON, FapiResponse } from './fake
66
export type FakeSessionResource = {
77
id: string;
88
status: 'active';
9+
activityVersion: number;
910
updatedAt: Date;
1011
user: { id: string; profileVersion: number; updatedAt: Date };
1112
};
@@ -19,12 +20,12 @@ type ListenerCallback = (resources: { client: FakeClientResource; session: FakeS
1920
type StatusListener = (status: string) => void;
2021

2122
function toSessionResource(session: FakeServerClientJSON['sessions'][number]): FakeSessionResource {
22-
const updatedAt = new Date(session.profileVersion);
2323
return {
2424
id: session.id,
2525
status: 'active',
26-
updatedAt,
27-
user: { id: session.userId, profileVersion: session.profileVersion, updatedAt },
26+
activityVersion: session.activityVersion,
27+
updatedAt: new Date(session.profileVersion + session.activityVersion),
28+
user: { id: session.userId, profileVersion: session.profileVersion, updatedAt: new Date(session.profileVersion) },
2829
};
2930
}
3031

@@ -63,6 +64,7 @@ export class FakeClientResource {
6364
id: session.id,
6465
userId: session.user.id,
6566
profileVersion: session.user.profileVersion,
67+
activityVersion: session.activityVersion,
6668
})),
6769
};
6870
}
@@ -238,6 +240,7 @@ export class FakeClerk {
238240
await this.handleUnauthenticated();
239241
return false;
240242
}
243+
this.#applyPiggybackedClient(response);
241244
return true;
242245
}
243246

‎packages/expo/src/provider/__tests__/nativeClientSyncBehavior/fakeServer.ts‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ export type FakeServerSessionJSON = {
22
id: string;
33
userId: string;
44
profileVersion: number;
5+
/** Bumped by session activity such as minting a session token. */
6+
activityVersion: number;
57
};
68

79
export type FakeServerClientJSON = {
@@ -95,7 +97,7 @@ export class FakeClerkServer {
9597
const { client, token } = this.#createClient();
9698
const sessionIds: Record<string, string> = {};
9799
for (const userId of users) {
98-
const session = { id: this.#nextId('sess'), userId, profileVersion: 0 };
100+
const session = { id: this.#nextId('sess'), userId, profileVersion: 0, activityVersion: 0 };
99101
client.sessions.push(session);
100102
sessionIds[userId] = session.id;
101103
}
@@ -151,7 +153,7 @@ export class FakeClerkServer {
151153
client = created.client;
152154
responseToken = created.token;
153155
}
154-
const session = { id: this.#nextId('sess'), userId, profileVersion: 0 };
156+
const session = { id: this.#nextId('sess'), userId, profileVersion: 0, activityVersion: 0 };
155157
client.sessions.push(session);
156158
client.lastActiveSessionId = session.id;
157159
return { ...this.#respond(responseToken, client), sessionId: session.id };
@@ -193,8 +195,11 @@ export class FakeClerkServer {
193195
async createSessionToken(caller: FapiCaller, token: string | null, sessionId: string): Promise<FapiResponse> {
194196
await this.#request(caller, 'POST /client/sessions/:id/tokens', token);
195197
const client = this.#clientFor(token);
196-
const status = client?.sessions.some(session => session.id === sessionId) ? 200 : 401;
197-
return this.#respond(token, client, status);
198+
const session = client?.sessions.find(candidate => candidate.id === sessionId);
199+
if (session) {
200+
session.activityVersion += 1;
201+
}
202+
return this.#respond(token, client, session ? 200 : 401);
198203
}
199204

200205
async updateUser(caller: FapiCaller, token: string | null, sessionId: string): Promise<FapiResponse> {

‎packages/expo/src/provider/__tests__/nativeClientSyncBehavior/nativeClientSync.behavior.test.tsx‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,26 @@ describe('native client sync behavior', () => {
289289
await expectConverged(h);
290290
});
291291

292+
it('refreshes native for a profile update but not for session activity that only bumps the session', async () => {
293+
const { server, token, sessionIds } = seedDevice(['user_1']);
294+
const h = await start({ server, jsDeviceToken: token, nativeDeviceToken: token });
295+
const refreshesBefore = h.native.clientRefreshCount;
296+
const updatedAtBefore = h.js.session?.updatedAt.getTime();
297+
298+
await h.run(() => Promise.all([h.js.getSessionToken(), h.js.getSessionToken()]));
299+
await h.settle();
300+
301+
expect(h.js.session?.updatedAt.getTime()).toBeGreaterThan(updatedAtBefore ?? Infinity);
302+
expect(h.native.clientRefreshCount - refreshesBefore).toBe(0);
303+
304+
await h.run(() => h.js.updateProfile());
305+
await h.settle();
306+
307+
expect(h.native.clientRefreshCount - refreshesBefore).toBe(1);
308+
expect(h.native.client?.sessions.find(session => session.id === sessionIds.user_1)?.profileVersion).toBe(1);
309+
await expectConverged(h);
310+
});
311+
292312
it('settles a JS change without an echo loop', async () => {
293313
const { server, token, sessionIds } = seedDevice(['user_1', 'user_2'], 'user_1');
294314
const h = await start({ server, jsDeviceToken: token, nativeDeviceToken: token });

‎packages/expo/src/provider/nativeClientSync.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ function fingerprint(clerk: SyncableClerk): string {
9797
return JSON.stringify([
9898
client?.id ?? null,
9999
client?.lastActiveSessionId ?? null,
100-
client?.signedInSessions.map(session => [session.id, session.status, +session.updatedAt]) ?? [],
100+
client?.signedInSessions.map(session => [session.id, session.status]) ?? [],
101101
user?.id ?? null,
102102
+(user?.updatedAt ?? 0),
103103
]);

0 commit comments

Comments
 (0)