Skip to content

Commit 95313b1

Browse files
Programcodeclaude
andcommitted
Throw a server refusal at once instead of waiting for a stalled query
readAll held a refusal until every other query settled, so a 401 behind a query that never answered left the screen loading. A refusal now rejects as soon as it arrives; only queries that got no answer wait for the rest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a846afc commit 95313b1

2 files changed

Lines changed: 40 additions & 7 deletions

File tree

‎apps/mobile/src/lib/offline-cache.test.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,14 @@ describe('which failures fall back', () => {
212212
supabase.results.push(unreachable, refused);
213213
await expect(cache.fetchDebates('user-a')).rejects.toThrow('JWT expired');
214214
});
215+
216+
it('throws a refusal at once instead of waiting for a query that never answers', async () => {
217+
const cache = await load();
218+
const refused = Promise.reject(cache.readFailed({ message: 'JWT expired' }, 401));
219+
const outcome = cache.readAll([refused, new Promise<never>(() => {})]).catch((cause: unknown) => cause);
220+
const first = await Promise.race([outcome, new Promise((resolve) => setTimeout(() => resolve('still waiting'), 50))]);
221+
expect(first).toMatchObject({ message: 'JWT expired' });
222+
});
215223
});
216224

217225
describe('the saved-copies notice', () => {

‎apps/mobile/src/lib/offline-cache.ts‎

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,39 @@ export function pickFailure(causes: unknown[]): unknown {
8484
}
8585

8686
/**
87-
* Promise.all for the queries of one read. It waits for all of them, so a
88-
* refusal from one is thrown even when another got no answer first.
87+
* Promise.all for the queries of one read. A server's refusal is thrown as
88+
* soon as it arrives; a query that got no answer is held until the others
89+
* settle, so a refusal from one of them still wins over it.
8990
*/
90-
export async function readAll<T extends readonly unknown[] | []>(reads: T): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }> {
91-
const results = await Promise.allSettled(reads);
92-
const failures = results.flatMap((result) => (result.status === 'rejected' ? [result.reason as unknown] : []));
93-
if (failures.length > 0) throw pickFailure(failures);
94-
return results.map((result) => (result as PromiseFulfilledResult<unknown>).value) as { -readonly [K in keyof T]: Awaited<T[K]> };
91+
export function readAll<T extends readonly unknown[] | []>(reads: T): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }> {
92+
return new Promise((resolve, reject) => {
93+
const values: unknown[] = new Array(reads.length);
94+
const unanswered: unknown[] = [];
95+
let pending = reads.length;
96+
const settle = () => {
97+
pending -= 1;
98+
if (pending > 0) return;
99+
if (unanswered.length > 0) reject(unanswered[0]);
100+
else resolve(values as { -readonly [K in keyof T]: Awaited<T[K]> });
101+
};
102+
if (pending === 0) {
103+
resolve(values as { -readonly [K in keyof T]: Awaited<T[K]> });
104+
return;
105+
}
106+
reads.forEach((read, i) => {
107+
Promise.resolve(read).then(
108+
(value) => {
109+
values[i] = value;
110+
settle();
111+
},
112+
(cause: unknown) => {
113+
if (!isUnreachable(cause)) reject(cause);
114+
else unanswered.push(cause);
115+
settle();
116+
},
117+
);
118+
});
119+
});
95120
}
96121

97122
const queue = createSerialQueue();

0 commit comments

Comments
 (0)