fix(service-worker): assign initializing client's app version, when a request is for worker script (#58131)

When a new version of app is available in a service worker, and a client with old version exists, web workers initialized from a client with old version will now be properly assigned with the same version.

Before this change, a web worker was assigned with the newest version.

Fixes #57971

PR Close #58131
This commit is contained in:
Tomasz Domański 2024-10-09 10:06:10 +02:00 committed by Jessica Janiuk
parent 0c5b697afe
commit 7cd89ad2c6
4 changed files with 87 additions and 6 deletions

View file

@ -700,7 +700,14 @@ export class Driver implements Debuggable, UpdateSource {
//
// NOTE: For navigation requests, we care about the `resultingClientId`. If it is undefined or
// the empty string (which is the case for sub-resource requests), we look at `clientId`.
const clientId = event.resultingClientId || event.clientId;
//
// NOTE: If a request is a worker script, we should use the `clientId`, as worker is a part
// of requesting client.
const isWorkerScriptRequest =
event.request.destination === 'worker' && event.resultingClientId && event.clientId;
const clientId = isWorkerScriptRequest
? event.clientId
: event.resultingClientId || event.clientId;
if (clientId) {
// Check if there is an assigned client id.
if (this.clientVersionMap.has(clientId)) {
@ -729,6 +736,18 @@ export class Driver implements Debuggable, UpdateSource {
appVersion = this.lookupVersionByHash(this.latestHash, 'assignVersion');
}
if (isWorkerScriptRequest) {
if (!this.clientVersionMap.has(event.resultingClientId)) {
// New worker hasn't been seen before; set this client to requesting client version
this.clientVersionMap.set(event.resultingClientId, hash);
await this.sync();
} else if (this.clientVersionMap.get(event.resultingClientId)! !== hash) {
throw new Error(
`Version mismatch between worker client ${event.resultingClientId} and requesting client ${clientId}`,
);
}
}
// TODO: make sure the version is valid.
return appVersion;
} else {
@ -763,6 +782,17 @@ export class Driver implements Debuggable, UpdateSource {
throw new Error(`Invariant violated (assignVersion): latestHash was null`);
}
if (isWorkerScriptRequest) {
if (!this.clientVersionMap.has(event.resultingClientId)) {
// New worker hasn't been seen before; set this client to latest hash as well
this.clientVersionMap.set(event.resultingClientId, this.latestHash);
} else if (this.clientVersionMap.get(event.resultingClientId)! !== this.latestHash) {
throw new Error(
`Version mismatch between worker client ${event.resultingClientId} and requesting client ${clientId}`,
);
}
}
// Pin this client ID to the current latest version, indefinitely.
this.clientVersionMap.set(clientId, this.latestHash);
await this.sync();

View file

@ -472,6 +472,24 @@ import {envIsSupported} from '../testing/utils';
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo v2');
});
it('returns old content for worker initialized from old version', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
expect(await makeRequest(scope, '/baz.txt')).toEqual('this is baz');
await driver.initialized;
const client = scope.clients.getMock('default')!;
expect(client.messages).toEqual([]);
scope.updateServerState(serverUpdate);
expect(await driver.checkForUpdate()).toEqual(true);
// Worker request came from default client, old version should be served
expect(await makeWorkerRequest(scope, '/foo.txt')).toEqual('this is foo');
// Worker has been initialized from default client, further requests from worker should be served from old version
expect(await makeRequest(scope, '/foo.txt', 'worker')).toEqual('this is foo');
expect(await makeRequest(scope, '/baz.txt', 'worker')).toEqual('this is baz');
});
it('handles empty client ID', async () => {
// Initialize the SW.
expect(await makeNavigationRequest(scope, '/foo/file1', '')).toEqual('this is foo');
@ -2657,6 +2675,26 @@ async function makeRequest(
return null;
}
async function makeWorkerRequest(
scope: SwTestHarness,
url: string,
clientId = 'default',
resultingClientId = 'worker',
init?: Object,
): Promise<string | null> {
const [resPromise, done] = scope.handleFetch(
new MockRequest(url, {...init, destination: 'worker'}),
clientId,
resultingClientId,
);
await done;
const res = await resPromise;
if (res !== undefined && res.ok) {
return res.text();
}
return null;
}
function makeNavigationRequest(
scope: SwTestHarness,
url: string,

View file

@ -122,7 +122,10 @@ export class MockRequest extends MockBody implements Request {
url: string;
constructor(input: string | Request, init: RequestInit = {}) {
constructor(
input: string | Request,
init: RequestInit & {destination?: RequestDestination} = {},
) {
super((init.body as string | null) ?? null);
if (typeof input !== 'string') {
throw 'Not implemented';
@ -150,6 +153,9 @@ export class MockRequest extends MockBody implements Request {
if (init.method !== undefined) {
this.method = init.method;
}
if (init.destination !== undefined) {
this.destination = init.destination;
}
}
clone(): Request {

View file

@ -192,7 +192,11 @@ export class SwTestHarnessImpl
this.skippedWaiting = true;
}
handleFetch(req: Request, clientId = ''): [Promise<Response | undefined>, Promise<void>] {
handleFetch(
req: Request,
clientId = '',
resultingClientId?: string,
): [Promise<Response | undefined>, Promise<void>] {
if (!this.eventHandlers.has('fetch')) {
throw new Error('No fetch handler registered');
}
@ -203,9 +207,12 @@ export class SwTestHarnessImpl
this.clients.add(clientId, isNavigation ? req.url : this.scopeUrl);
}
const event = isNavigation
? new MockFetchEvent(req, '', clientId)
: new MockFetchEvent(req, clientId, '');
const event =
clientId && resultingClientId
? new MockFetchEvent(req, clientId, resultingClientId)
: isNavigation
? new MockFetchEvent(req, '', clientId)
: new MockFetchEvent(req, clientId, '');
this.eventHandlers.get('fetch')!.call(this, event);
return [event.response, event.ready];