Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions __tests__/unit/services/warm-reader-is-browser-only.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @jest-environment node
*
* Warming the reader's id must do NOTHING on the server.
*
* `warmCurrentUserId` is called at the top of every timeline read, and timeline
* reads happen during server rendering too. Unguarded, it started an
* unconditional `/auth/v1/user` request from the server using the BROWSER
* Supabase client — which carries no request cookies, so it can only fail, and
* it fails while the route is rendering.
*
* That wedged the server render of every feed page. /timeline, /community and
* /dashboard sat on their route-level loading.tsx skeleton forever on a fresh
* page load, while navigating to the same route from inside the app worked,
* because that path never server-renders. Nothing appeared in the console and
* every check was green; the skeleton is near-white on white, so it read as a
* blank page.
*
* This test runs in the `node` environment on purpose. Under jsdom `window`
* exists, the guard is never exercised, and this file would pass while the
* server path stayed broken — which is exactly how the bug shipped.
*/

const getUser = jest.fn();

jest.mock('@/lib/supabase/browser', () => ({
__esModule: true,
default: {
auth: {
getUser: (...a: unknown[]) => getUser(...a),
onAuthStateChange: () => ({ data: { subscription: { unsubscribe: () => {} } } }),
},
},
}));

import { warmCurrentUserId } from '@/services/supabase/auth/session';

describe('warmCurrentUserId on the server', () => {
it('has no window to speak of', () => {
// Guard the premise: if this environment ever gains a window, the test
// below stops testing anything and must not silently pass.
expect(typeof window).toBe('undefined');
});

it('does not touch the network', async () => {
warmCurrentUserId();
// Let any promise it might have started settle.
await new Promise(resolve => setTimeout(resolve, 0));

expect(getUser).not.toHaveBeenCalled();
});
});
20 changes: 20 additions & 0 deletions src/services/supabase/auth/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,28 @@ export async function getCurrentUserId(): Promise<string | null> {
* For callers that know they will need it later and can overlap the round-trip
* with work that does not depend on it — fetching a feed, say. Returns nothing,
* so it cannot be mistaken for the id itself.
*
* BROWSER ONLY, and the guard is load-bearing. This is called at the top of
* every timeline read, and timeline reads happen during server rendering too.
* Without the guard it started an unconditional `/auth/v1/user` request from
* the server using the BROWSER client — which has no request cookies, so it
* can only ever fail, and it does so while the route is rendering.
*
* That is not a wasted request, it is an outage: it wedged the server render
* of every feed page. /timeline, /community and /dashboard stayed on their
* route-level loading.tsx skeleton forever on a fresh page load, while
* navigating to the very same route from inside the app worked, because that
* path never server-renders. A near-white skeleton on a white page reads as a
* blank screen, so the symptom was "the timeline is empty", with nothing in
* the console and every check green.
*
* Deliberately silent on the server rather than throwing: this is an
* optimisation, and an optimisation must never be the reason a page fails.
*/
export function warmCurrentUserId(): void {
if (typeof window === 'undefined') {
return;
}
void getCurrentUserId();
}

Expand Down
Loading