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
7 changes: 7 additions & 0 deletions .changeset/every-ducks-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"houdini-svelte": patch
"houdini-core": patch
"houdini": patch
---

Refresh active SvelteKit queries after `setSession` updates the Houdini session, ensuring refetched operations run with the latest session.
4 changes: 3 additions & 1 deletion e2e/kit/src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { sequence } from '@sveltejs/kit/hooks'

const handleTests: Handle = async ({ event, resolve }) => {
// set the session information for this event
setSession(event, { user: { token: '1234-Houdini-Token-5678' } })
setSession(event, {
user: { token: event.cookies.get('houdini-session-token') ?? '1234-Houdini-Token-5678' },
})

// pass the event onto the default handle
return await resolve(event)
Expand Down
1 change: 1 addition & 0 deletions e2e/kit/src/lib/utils/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const routes = {
Stores_action_mutation: '/stores/action-mutation',
Stores_Endpoint_Query: '/stores/endpoint-query',
Stores_Session: '/stores/session',
Stores_Session_Refresh: '/stores/session-refresh',
Stores_Comp_disable_auto_fetch: '/stores/comp_disable_auto_fetch',

Stores_Partial_List: '/stores/partial/partial_List',
Expand Down
30 changes: 30 additions & 0 deletions e2e/kit/src/routes/stores/session-refresh/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation'
import { CachePolicy, graphql } from '$houdini'
import { onMount } from 'svelte'

// This query deliberately lives outside a SvelteKit load function. invalidateAll()
// cannot rerun it directly; a second request must come from Houdini's session watcher.
const session = graphql(`
query ActiveSessionAfterInvalidate {
session
}
`)

onMount(() => {
session.fetch({ policy: CachePolicy.NetworkOnly })
})

async function updateSession() {
await fetch('/stores/session-refresh/update', { method: 'POST' })
await invalidateAll()
}
</script>

<h1>Session Refresh</h1>

<div id="result">
{$session.data?.session}
</div>

<button id="update-session" on:click={updateSession}>Update session</button>
33 changes: 33 additions & 0 deletions e2e/kit/src/routes/stores/session-refresh/spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { expect, test, type Request } from '@playwright/test'

import { routes } from '../../../lib/utils/routes.js'

const initialToken = '1234-Houdini-Token-5678'
const updatedToken = 'updated-Houdini-Token-0000'

function isActiveSessionQuery(request: Request) {
return (
request.url().endsWith(routes.GraphQL) &&
(request.postData()?.includes('query ActiveSessionAfterInvalidate') ?? false)
)
}

test.describe('SvelteKit session refresh', () => {
test('refreshes active queries with the new Houdini session', async ({ page }) => {
const initialRequestPromise = page.waitForRequest(isActiveSessionQuery)
await page.goto(routes.Stores_Session_Refresh)

const initialRequest = await initialRequestPromise
expect(initialRequest.headers().authorization).toBe(`Bearer ${initialToken}`)
await expect(page.locator('#result')).toHaveText(initialToken)

const [refetchRequest] = await Promise.all([
page.waitForRequest(isActiveSessionQuery),
page.waitForResponse((response) => response.url().endsWith('session-refresh/update')),
page.getByRole('button', { name: 'Update session' }).click(),
])

expect(refetchRequest.headers().authorization).toBe(`Bearer ${updatedToken}`)
await expect(page.locator('#result')).toHaveText(updatedToken)
})
})
15 changes: 15 additions & 0 deletions e2e/kit/src/routes/stores/session-refresh/update/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { setSession } from '$houdini'
import { json, type RequestHandler } from '@sveltejs/kit'

const updatedToken = 'updated-Houdini-Token-0000'

export const POST: RequestHandler = (event) => {
event.cookies.set('houdini-session-token', updatedToken, {
path: '/',
httpOnly: true,
sameSite: 'lax',
})
setSession(event, { user: { token: updatedToken } })

return json({ token: updatedToken })
}
2 changes: 1 addition & 1 deletion packages/houdini-core/runtime/plugins/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const query = (cache: Cache) =>
if (message.kind === 'refetch') {
ctx.documentStore.send({
policy: CachePolicy.NetworkOnly,
session: lastSession,
session: 'session' in message ? message.session : lastSession,
metadata: ctx.metadata,
})
return
Expand Down
85 changes: 85 additions & 0 deletions packages/houdini-core/runtime/public/tests/query.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Cache } from '../../../../houdini/src/runtime/cache/index.js'
import { createPluginHooks, DocumentStore, HoudiniClient } from 'houdini/runtime/client'
import { ArtifactKind, DataSource } from 'houdini/runtime/types'
import { beforeEach, expect, test, vi } from 'vitest'

import { testConfigFile } from '../../../../houdini/src/test/index.js'
import { setMockConfig } from '../../config.js'
import { query } from '../../plugins/query.js'

const config = testConfigFile()
beforeEach(async () => {
setMockConfig(config)
})

test('refreshAll with a new session refetches active queries with that session', async () => {
const cache = new Cache()

const selection = {
fields: {
viewer: {
type: 'User',
visible: true,
keyRaw: 'viewer',
selection: {
fields: {
id: { type: 'ID', visible: true, keyRaw: 'id' },
firstName: { type: 'String', visible: true, keyRaw: 'firstName' },
},
},
},
},
}

cache.write({
selection,
data: { viewer: { id: '1', firstName: 'bob' } },
})

const fetchSpy = vi.fn()
const fakeFetch = () => ({
network(ctx, { resolve }) {
fetchSpy(ctx)
resolve(ctx, {
data: { viewer: { id: '1', firstName: 'bob', __typename: 'User' } },
errors: null,
fetching: false,
variables: null,
source: DataSource.Network,
partial: false,
stale: false,
})
},
})

const artifact = {
kind: ArtifactKind.Query,
hash: '7777',
raw: 'RAW_TEXT',
name: 'TestArtifact',
rootType: 'Query',
pluginData: {},
stripVariables: [],
selection,
}
const client = new HoudiniClient({
config: () => config,
plugins: [query(cache), fakeFetch],
})
const store = new DocumentStore({
client,
plugins: createPluginHooks([query(cache), fakeFetch]),
artifact,
config,
})

await store.send({ session: { token: 'old' }, variables: {} })
fetchSpy.mockClear()

cache.refreshAll({ token: 'new' })

await new Promise((r) => setTimeout(r, 0))

expect(fetchSpy).toHaveBeenCalledOnce()
expect(fetchSpy.mock.calls[0][0].session).toEqual({ token: 'new' })
})
14 changes: 12 additions & 2 deletions packages/houdini-svelte/package/vite/transform/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,25 @@ test('modifies root +layout.svelte to import adapter', async function () {
)

expect(result).toMatchInlineSnapshot(`
import { deepEquals } from "houdini/runtime";
import { getCache } from "$houdini";
import { page } from "$app/state";
import { extractSession, setClientSession } from "$houdini/plugins/houdini-svelte/runtime/session";
import { extractSession, getClientSession, setClientSession } from "$houdini/plugins/houdini-svelte/runtime/session";
import { onMount } from "svelte";
import { setClientStarted } from "$houdini/plugins/houdini-svelte/runtime/adapter";
export let data
let houdini__session__initialized = false;
onMount(() => setClientStarted());

$effect(() => {
setClientSession(extractSession(page.data));
const nextSession = extractSession(page.data);
const sessionChanged = houdini__session__initialized && !deepEquals(getClientSession(), nextSession);
setClientSession(nextSession);
houdini__session__initialized = true;

if (sessionChanged) {
getCache().refreshAll(nextSession);
}
});
`)
})
79 changes: 67 additions & 12 deletions packages/houdini-svelte/package/vite/transform/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,39 @@ export default async function kit_init(config: Config, page: SvelteTransformPage
sourceModule: 'svelte',
import: ['onMount'],
}).ids[0]
const [extract_session, set_session] = ensure_imports({
const [extract_session, get_client_session, set_session] = ensure_imports({
script: page.script,
sourceModule: '$houdini/plugins/houdini-svelte/runtime/session',
import: ['extractSession', 'setClientSession'],
import: ['extractSession', 'getClientSession', 'setClientSession'],
}).ids

// we need to track updates in page data as the client-side session
const page_store = ensure_imports({
script: page.script,
sourceModule: '$app/state',
import: ['page'],
}).ids[0]
const get_cache = ensure_imports({
script: page.script,
sourceModule: '$houdini',
import: ['getCache'],
}).ids[0]
const deep_equals = ensure_imports({
script: page.script,
sourceModule: 'houdini/runtime',
import: ['deepEquals'],
}).ids[0]

const session_initialized = AST.identifier('houdini__session__initialized')
const next_session = AST.identifier('nextSession')
const session_changed = AST.identifier('sessionChanged')

page.script.body.push(
AST.variableDeclaration('let', [
AST.variableDeclarator(session_initialized, AST.literal(false)),
])
)

// add the onMount at the end of the component
page.script.body.push(
AST.expressionStatement(
Expand All @@ -40,25 +67,53 @@ export default async function kit_init(config: Config, page: SvelteTransformPage
)
)

// we need to track updates in page data as the client-side session
const page_store = ensure_imports({
script: page.script,
sourceModule: '$app/state',
import: ['page'],
}).ids[0]

// $effect dont get anyting in callback so we got to use store_page that is like page
page.script.body.push(
AST.expressionStatement(
AST.callExpression(AST.identifier('$effect'), [
AST.arrowFunctionExpression(
[],
AST.blockStatement([
AST.expressionStatement(
AST.callExpression(set_session, [
AST.variableDeclaration('const', [
AST.variableDeclarator(
next_session,
AST.callExpression(extract_session, [
AST.memberExpression(page_store, AST.identifier('data')),
]),
])
),
]),
AST.variableDeclaration('const', [
AST.variableDeclarator(
session_changed,
AST.logicalExpression(
'&&',
session_initialized,
AST.unaryExpression(
'!',
AST.callExpression(deep_equals, [
AST.callExpression(get_client_session, []),
next_session,
])
)
)
),
]),
AST.expressionStatement(AST.callExpression(set_session, [next_session])),
AST.expressionStatement(
AST.assignmentExpression('=', session_initialized, AST.literal(true))
),
AST.ifStatement(
session_changed,
AST.blockStatement([
AST.expressionStatement(
AST.callExpression(
AST.memberExpression(
AST.callExpression(get_cache, []),
AST.identifier('refreshAll')
),
[next_session]
)
),
])
),
])
Expand Down
28 changes: 24 additions & 4 deletions packages/houdini-svelte/package/vite/transform/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,25 @@ test('modifies root +layout.svelte with data prop', async function () {
)

expect(result).toMatchInlineSnapshot(`
Comment thread
AlecAivazis marked this conversation as resolved.
import { deepEquals } from "houdini/runtime";
import { getCache } from "$houdini";
import { page } from "$app/state";
import { extractSession, setClientSession } from "$houdini/plugins/houdini-svelte/runtime/session";
import { extractSession, getClientSession, setClientSession } from "$houdini/plugins/houdini-svelte/runtime/session";
import { onMount } from "svelte";
import { setClientStarted } from "$houdini/plugins/houdini-svelte/runtime/adapter";
export let data
let houdini__session__initialized = false;
onMount(() => setClientStarted());

$effect(() => {
setClientSession(extractSession(page.data));
const nextSession = extractSession(page.data);
const sessionChanged = houdini__session__initialized && !deepEquals(getClientSession(), nextSession);
setClientSession(nextSession);
houdini__session__initialized = true;

if (sessionChanged) {
getCache().refreshAll(nextSession);
}
});
`)
})
Expand Down Expand Up @@ -54,14 +64,24 @@ test('modifies root +layout.svelte without data prop', async function () {
const result = await test_transform_svelte('src/routes/+layout.svelte', ``)

expect(result).toMatchInlineSnapshot(`
import { deepEquals } from "houdini/runtime";
import { getCache } from "$houdini";
import { page } from "$app/state";
import { extractSession, setClientSession } from "$houdini/plugins/houdini-svelte/runtime/session";
import { extractSession, getClientSession, setClientSession } from "$houdini/plugins/houdini-svelte/runtime/session";
import { onMount } from "svelte";
import { setClientStarted } from "$houdini/plugins/houdini-svelte/runtime/adapter";
let houdini__session__initialized = false;
onMount(() => setClientStarted());

$effect(() => {
setClientSession(extractSession(page.data));
const nextSession = extractSession(page.data);
const sessionChanged = houdini__session__initialized && !deepEquals(getClientSession(), nextSession);
setClientSession(nextSession);
houdini__session__initialized = true;

if (sessionChanged) {
getCache().refreshAll(nextSession);
}
});
`)
})
Expand Down
Loading
Loading