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
2 changes: 2 additions & 0 deletions .github/workflows/integration-gateway.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ on:
- 'tests/edge_query_delivery*'
- 'js/src/replica/**'
- 'js/tests/replica*'
- 'js/src/sveltekit/**'
- 'js/tests/sveltekit*'
- 'tests/gateway*'
- 'tests/gateway*/**'
- '.github/workflows/integration-gateway.yaml'
Expand Down
9 changes: 5 additions & 4 deletions js/src/sveltekit/replica.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,13 +1038,14 @@ function createAuthorizationFence(
let queue = Promise.resolve();
let disposed = false;
const read = (): Promise<GqlAuth> => {
const candidate = Promise.resolve().then(() => {
const candidate = (async () => {
// Capture the credential and its independent server transfer together,
// before another source notification can replace either value.
// synchronously at notification time. A queued read would observe a
// later seedless page update and lose the refresh's scope evidence.
const credential = source.getAuth();
const transfer = source.getHydration?.();
return Promise.resolve(credential).then((auth) => ({ auth, transfer }));
});
return { auth: await credential, transfer };
})();
const transition = queue.then(async () => {
try {
const { auth, transfer } = await candidate;
Expand Down
39 changes: 39 additions & 0 deletions js/tests/sveltekit-ssr.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,45 @@ test('fresh same-scope SSR authority rotates credentials without emptying the vi
unsubscribe(); client.destroy();
});

test('queued page-data updates retain the fresh authority before seedless invalidation', async () => {
const harness = serverHarness();
const first = await harness.server.load(harness.event('alice', '1'));
const second = await harness.server.load(harness.event('alice', '2'));
const pageData = createPageDataSessionSource(first);
SsrWebSocket.instances.length = 0;
let fetches = 0;
const client = createDistributedSvelteKit({
boundaries: [todosBoundary], session: pageData.session,
hydration: first.distributed, authority: first.distributedAuthority,
fetch: () => { fetches += 1; return new Promise(() => {}); },
webSocket: SsrWebSocket
});
const todos = client.operation(TodosArtifact).use();
const values = [];
const unsubscribe = todos.subscribe(snapshot => values.push(snapshot.data.todos?.[0]?.title));
await flushMicrotasks();
const oldSocket = SsrWebSocket.instances[0];
// A refresh supplies independent authority; invalidateAll then replaces it
// with normal data-request output, which intentionally contains no seed.
pageData.set({...second, accessToken: 'rotated-alice'});
pageData.set({...second, accessToken: 'rotated-alice', distributed: undefined, distributedAuthority: undefined});
await flushMicrotasks();
assert.ok(values.every(value => value === 'alice:1'), JSON.stringify(values));
assert.equal(fetches, 0);
assert.equal(oldSocket.closed, true);
const nextSocket = SsrWebSocket.instances.at(-1);
assert.notEqual(nextSocket, oldSocket);
nextSocket.open();
await flushMicrotasks();
assert.equal(nextSocket.sent[0].payload.authorization, 'Bearer rotated-alice');
// A later, different credential cannot borrow the consumed transfer.
pageData.set({accessToken: 'unproven-rotation'});
await flushMicrotasks();
assert.equal(client.replica.scope, undefined);
assert.deepEqual(todos.get().data, {});
unsubscribe(); client.destroy();
});

for (const kind of ['missing', 'replayed', 'historical', 'tampered', 'different-scope', 'logout']) {
test(`credential change with ${kind} hydration still purges the old replica`, async () => {
const harness = serverHarness();
Expand Down
Loading