feat(service-worker): notify clients about version failures (#62718)

Add client notification when an app version fails, improving error
visibility and debugging capabilities. When a version encounters a
critical error, all clients using that version are now notified
with details about the failure.

- Add notifyClientsAboutVersionFailure method call in versionFailed
- Ensure clients receive VERSION_FAILED events with error details
- Improve service worker error transparency for better debugging

PR Close #62718
This commit is contained in:
SkyZeroZx 2025-07-19 17:26:29 -05:00 committed by Jessica Janiuk
parent 94b0ef1453
commit 6d011687ec
6 changed files with 66 additions and 2 deletions

View file

@ -109,7 +109,7 @@ export interface VersionDetectedEvent {
}
// @public
export type VersionEvent = VersionDetectedEvent | VersionInstallationFailedEvent | VersionReadyEvent | NoNewVersionDetectedEvent;
export type VersionEvent = VersionDetectedEvent | VersionInstallationFailedEvent | VersionReadyEvent | VersionFailedEvent | NoNewVersionDetectedEvent;
// @public
export interface VersionInstallationFailedEvent {

View file

@ -70,6 +70,23 @@ export interface VersionReadyEvent {
latestVersion: {hash: string; appData?: object};
}
/**
* An event emitted when a specific version of the app has encountered a critical failure
* that prevents it from functioning correctly.
*
* When a version fails, the service worker will notify all clients currently using that version
* and may degrade to serving only existing clients if the failed version was the latest one.
*
* @see {@link /ecosystem/service-workers/communications Service Worker Communication Guide}
*
* @publicApi
*/
export interface VersionFailedEvent {
type: 'VERSION_FAILED';
version: {hash: string; appData?: object};
error: string;
}
/**
* A union of all event types that can be emitted by
* {@link SwUpdate#versionUpdates}.
@ -80,6 +97,7 @@ export type VersionEvent =
| VersionDetectedEvent
| VersionInstallationFailedEvent
| VersionReadyEvent
| VersionFailedEvent
| NoNewVersionDetectedEvent;
/**

View file

@ -66,6 +66,7 @@ export class SwUpdate {
'VERSION_INSTALLATION_FAILED',
'VERSION_READY',
'NO_NEW_VERSION_DETECTED',
'VERSION_FAILED',
]);
this.unrecoverable = this.sw.eventsOfType<UnrecoverableStateEvent>('UNRECOVERABLE_STATE');
}

View file

@ -13,6 +13,7 @@ import {
NoNewVersionDetectedEvent,
VersionDetectedEvent,
VersionEvent,
VersionFailedEvent,
VersionReadyEvent,
} from '../src/low_level';
import {ngswCommChannelFactory, SwRegistrationOptions} from '../src/provider';
@ -509,6 +510,25 @@ describe('ServiceWorker library', () => {
},
});
});
it('processes version failed events with cache corruption error', (done) => {
update.versionUpdates.subscribe((event) => {
expect(event.type).toEqual('VERSION_FAILED');
expect((event as VersionFailedEvent).version).toEqual({
hash: 'B',
appData: {name: 'test-app'},
});
expect((event as VersionFailedEvent).error).toContain('Cache corruption detected');
done();
});
mock.sendMessage({
type: 'VERSION_FAILED',
version: {
hash: 'B',
appData: {name: 'test-app'},
},
error: 'Cache corruption detected during resource fetch',
});
});
it('activates updates when requested', async () => {
mock.messages.subscribe((msg: {action: string; nonce: number}) => {
expect(msg.action).toEqual('ACTIVATE_UPDATE');

View file

@ -956,7 +956,8 @@ export class Driver implements Debuggable, UpdateSource {
// Therefore, we keep clients on their current version (even if broken) and ensure that no new
// clients will be assigned to it.
// TODO: notify affected apps.
// Notify clients that are using this broken version about the failure.
await this.notifyClientsAboutVersionFailure(brokenHash, err);
// The action taken depends on whether the broken manifest is the active (latest) or not.
// - If the broken version is not the latest, no further action is necessary, since new clients
@ -1298,6 +1299,29 @@ export class Driver implements Debuggable, UpdateSource {
);
}
async notifyClientsAboutVersionFailure(brokenHash: string, error: Error): Promise<void> {
await this.initialized;
// Find all clients using the broken version
const affectedClients = Array.from(this.clientVersionMap.entries())
.filter(([clientId, hash]) => hash === brokenHash)
.map(([clientId]) => clientId);
await Promise.all(
affectedClients.map(async (clientId) => {
const client = await this.scope.clients.get(clientId);
if (client) {
const brokenVersion = this.versions.get(brokenHash);
client.postMessage({
type: 'VERSION_FAILED',
version: this.mergeHashWithAppData(brokenVersion!.manifest, brokenHash),
error: errorToString(error),
});
}
}),
);
}
async broadcast(msg: Object): Promise<void> {
const clients = await this.scope.clients.matchAll();
clients.forEach((client) => {

View file

@ -2564,6 +2564,7 @@ import {envIsSupported} from '../testing/utils';
expect(await makeRequest(scope, '/foo.hash.js', 'client-2')).toBeNull();
expect(mockClient2.messages).toEqual([
jasmine.objectContaining({type: 'UNRECOVERABLE_STATE'}),
jasmine.objectContaining({type: 'VERSION_FAILED'}),
]);
// This should also enter the `SW` into degraded mode, because the broken version was the