diff --git a/.changeset/every-ducks-fry.md b/.changeset/every-ducks-fry.md
new file mode 100644
index 000000000..e252fba77
--- /dev/null
+++ b/.changeset/every-ducks-fry.md
@@ -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.
diff --git a/e2e/kit/src/hooks.server.ts b/e2e/kit/src/hooks.server.ts
index daac935d7..600ec4f97 100644
--- a/e2e/kit/src/hooks.server.ts
+++ b/e2e/kit/src/hooks.server.ts
@@ -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)
diff --git a/e2e/kit/src/lib/utils/routes.ts b/e2e/kit/src/lib/utils/routes.ts
index 4d9bb3bb0..06d63b320 100644
--- a/e2e/kit/src/lib/utils/routes.ts
+++ b/e2e/kit/src/lib/utils/routes.ts
@@ -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',
diff --git a/e2e/kit/src/routes/stores/session-refresh/+page.svelte b/e2e/kit/src/routes/stores/session-refresh/+page.svelte
new file mode 100644
index 000000000..97ae4bd40
--- /dev/null
+++ b/e2e/kit/src/routes/stores/session-refresh/+page.svelte
@@ -0,0 +1,30 @@
+
+
+
Session Refresh
+
+
+ {$session.data?.session}
+
+
+
diff --git a/e2e/kit/src/routes/stores/session-refresh/spec.ts b/e2e/kit/src/routes/stores/session-refresh/spec.ts
new file mode 100644
index 000000000..38c09ca7a
--- /dev/null
+++ b/e2e/kit/src/routes/stores/session-refresh/spec.ts
@@ -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)
+ })
+})
diff --git a/e2e/kit/src/routes/stores/session-refresh/update/+server.ts b/e2e/kit/src/routes/stores/session-refresh/update/+server.ts
new file mode 100644
index 000000000..4af15042d
--- /dev/null
+++ b/e2e/kit/src/routes/stores/session-refresh/update/+server.ts
@@ -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 })
+}
diff --git a/packages/houdini-core/runtime/plugins/query.ts b/packages/houdini-core/runtime/plugins/query.ts
index a9c6eb15d..507c12d57 100644
--- a/packages/houdini-core/runtime/plugins/query.ts
+++ b/packages/houdini-core/runtime/plugins/query.ts
@@ -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
diff --git a/packages/houdini-core/runtime/public/tests/query.test.ts b/packages/houdini-core/runtime/public/tests/query.test.ts
new file mode 100644
index 000000000..419747dee
--- /dev/null
+++ b/packages/houdini-core/runtime/public/tests/query.test.ts
@@ -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' })
+})
diff --git a/packages/houdini-svelte/package/vite/transform/init.test.ts b/packages/houdini-svelte/package/vite/transform/init.test.ts
index 3ab975112..038707d5f 100644
--- a/packages/houdini-svelte/package/vite/transform/init.test.ts
+++ b/packages/houdini-svelte/package/vite/transform/init.test.ts
@@ -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);
+ }
});
`)
})
diff --git a/packages/houdini-svelte/package/vite/transform/init.ts b/packages/houdini-svelte/package/vite/transform/init.ts
index 68b1b845a..8780f3cf2 100644
--- a/packages/houdini-svelte/package/vite/transform/init.ts
+++ b/packages/houdini-svelte/package/vite/transform/init.ts
@@ -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(
@@ -40,13 +67,6 @@ 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(
@@ -54,11 +74,46 @@ export default async function kit_init(config: Config, page: SvelteTransformPage
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]
+ )
+ ),
])
),
])
diff --git a/packages/houdini-svelte/package/vite/transform/session.test.ts b/packages/houdini-svelte/package/vite/transform/session.test.ts
index 3f7c83e69..dd168494e 100644
--- a/packages/houdini-svelte/package/vite/transform/session.test.ts
+++ b/packages/houdini-svelte/package/vite/transform/session.test.ts
@@ -14,15 +14,25 @@ test('modifies root +layout.svelte with data prop', 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);
+ }
});
`)
})
@@ -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);
+ }
});
`)
})
diff --git a/packages/houdini/src/runtime/cache/index.ts b/packages/houdini/src/runtime/cache/index.ts
index 6ba8d9304..0ffa5272f 100644
--- a/packages/houdini/src/runtime/cache/index.ts
+++ b/packages/houdini/src/runtime/cache/index.ts
@@ -233,6 +233,26 @@ export class Cache {
}
}
+ // ask every active query document to refetch itself.
+ // this is used when global request context changes (for example, the
+ // SvelteKit session) and any query might resolve differently even if its
+ // cached records did not change. non-query artifacts are skipped because
+ // only queries can be refetched through this cache notification path.
+ refreshAll(session?: App.Session | null) {
+ const message: { kind: 'refetch'; session?: App.Session | null } =
+ session === undefined ? { kind: 'refetch' } : { kind: 'refetch', session }
+
+ for (const [spec] of this._internal_unstable.subscriptions.all({
+ includeMaskedParents: true,
+ })) {
+ if (spec.kind !== ArtifactKind.Query) {
+ continue
+ }
+
+ spec.onMessage(message)
+ }
+ }
+
markRecordStale(id: string, options: { field?: string; when?: {} }) {
if (options.field) {
const key = computeKey({ field: options.field, args: options.when ?? {} })
diff --git a/packages/houdini/src/runtime/cache/subscription.ts b/packages/houdini/src/runtime/cache/subscription.ts
index fecfa944e..04397326b 100644
--- a/packages/houdini/src/runtime/cache/subscription.ts
+++ b/packages/houdini/src/runtime/cache/subscription.ts
@@ -348,6 +348,33 @@ export class InMemorySubscriptions {
)
}
+ all({ includeMaskedParents = false }: { includeMaskedParents?: boolean } = {}) {
+ const result: FieldSelection[] = []
+ const notified = new Set()
+
+ for (const fields of this.subscribers.values()) {
+ for (const fieldSub of fields.values()) {
+ for (const selection of fieldSub.selections) {
+ if (!notified.has(selection[0].onMessage)) {
+ notified.add(selection[0].onMessage)
+ result.push(selection)
+ }
+ }
+
+ if (includeMaskedParents) {
+ for (const selection of fieldSub.maskedParentSelections) {
+ if (!notified.has(selection[0].onMessage)) {
+ notified.add(selection[0].onMessage)
+ result.push(selection)
+ }
+ }
+ }
+ }
+ }
+
+ return result
+ }
+
remove(
id: string,
selection: SubscriptionSelection,
diff --git a/packages/houdini/src/runtime/types.ts b/packages/houdini/src/runtime/types.ts
index c10d78191..4117027d9 100644
--- a/packages/houdini/src/runtime/types.ts
+++ b/packages/houdini/src/runtime/types.ts
@@ -319,6 +319,7 @@ export type CacheMessage<_Data = any> =
}
| {
kind: 'refetch'
+ session?: App.Session | null
}
export type SubscriptionSpec = Readonly<{