Skip to content

Commit 635fb00

Browse files
arturovtthePunderWoman
authored andcommitted
refactor(service-worker): pull less RxJS symbols (angular#60657)
In this commit, we reduce the number of imported RxJS symbols since they are redundant. - We replace `merge` with a manual observable because `merge` internally pulls in `from()`. - We remove `defer` and `throwError`, replacing them with `new Observable(s => s.error(..))`. - We replace `toPromise()` with `new Promise`, as `toPromise()` is deprecated. - We convert `readyToRegister` to a promise to avoid using RxJS operators like `delay`. PR Close angular#60657
1 parent 8f68d1b commit 635fb00

File tree

6 files changed

+178
-163
lines changed

6 files changed

+178
-163
lines changed

goldens/public-api/service-worker/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { EnvironmentProviders } from '@angular/core';
88
import * as i0 from '@angular/core';
9+
import { Injector } from '@angular/core';
910
import { ModuleWithProviders } from '@angular/core';
1011
import { Observable } from 'rxjs';
1112

packages/service-worker/src/low_level.ts

Lines changed: 73 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {concat, ConnectableObservable, defer, Observable, of, throwError} from 'rxjs';
10-
import {filter, map, publish, switchMap, take, tap} from 'rxjs/operators';
9+
import {ApplicationRef, type Injector} from '@angular/core';
10+
import {Observable, Subject} from 'rxjs';
11+
import {filter, map, switchMap, take} from 'rxjs/operators';
1112

1213
export const ERR_SW_NOT_SUPPORTED = 'Service workers are disabled or not supported by this browser';
1314

@@ -125,19 +126,6 @@ type OperationCompletedEvent =
125126
error: string;
126127
};
127128

128-
function errorObservable(message: string): Observable<any> {
129-
return defer(() => throwError(new Error(message)));
130-
}
131-
132-
// Manually creating the observable imports fewer symbols
133-
// from RxJS than using `fromEvent(...)`.
134-
const fromEvent = <T extends Event>(serviceWorker: ServiceWorkerContainer, eventName: string) =>
135-
new Observable<T>((subscriber) => {
136-
const onEvent: EventListener = (event) => subscriber.next(event as T);
137-
serviceWorker.addEventListener(eventName, onEvent);
138-
return () => serviceWorker.removeEventListener(eventName, onEvent);
139-
});
140-
141129
/**
142130
* @publicApi
143131
*/
@@ -148,44 +136,70 @@ export class NgswCommChannel {
148136

149137
readonly events: Observable<TypedEvent>;
150138

151-
constructor(private serviceWorker: ServiceWorkerContainer | undefined) {
139+
constructor(
140+
private serviceWorker: ServiceWorkerContainer | undefined,
141+
injector?: Injector,
142+
) {
152143
if (!serviceWorker) {
153-
this.worker = this.events = this.registration = errorObservable(ERR_SW_NOT_SUPPORTED);
144+
this.worker =
145+
this.events =
146+
this.registration =
147+
new Observable<never>((subscriber) => subscriber.error(new Error(ERR_SW_NOT_SUPPORTED)));
154148
} else {
155-
const controllerChangeEvents = fromEvent<Event>(serviceWorker, 'controllerchange');
156-
const controllerChanges = controllerChangeEvents.pipe(map(() => serviceWorker.controller));
157-
const currentController = defer(() => of(serviceWorker.controller));
158-
const controllerWithChanges = concat(currentController, controllerChanges);
159-
160-
this.worker = controllerWithChanges.pipe(filter((c): c is ServiceWorker => !!c));
149+
let currentWorker: ServiceWorker | null = null;
150+
const workerSubject = new Subject<ServiceWorker>();
151+
this.worker = new Observable((subscriber) => {
152+
if (currentWorker !== null) {
153+
subscriber.next(currentWorker);
154+
}
155+
return workerSubject.subscribe((v) => subscriber.next(v));
156+
});
157+
const updateController = () => {
158+
const {controller} = serviceWorker;
159+
if (controller === null) {
160+
return;
161+
}
162+
currentWorker = controller;
163+
workerSubject.next(currentWorker);
164+
};
165+
serviceWorker.addEventListener('controllerchange', updateController);
166+
updateController();
161167

162168
this.registration = <Observable<ServiceWorkerRegistration>>(
163169
this.worker.pipe(switchMap(() => serviceWorker.getRegistration()))
164170
);
165171

166-
const rawEvents = fromEvent<MessageEvent>(serviceWorker, 'message');
167-
const rawEventPayload = rawEvents.pipe(map((event) => event.data));
168-
const eventsUnconnected = rawEventPayload.pipe(filter((event) => event && event.type));
169-
const events = eventsUnconnected.pipe(publish()) as ConnectableObservable<IncomingEvent>;
170-
events.connect();
171-
172-
this.events = events;
172+
const _events = new Subject<TypedEvent>();
173+
this.events = _events.asObservable();
174+
175+
const messageListener = (event: MessageEvent) => {
176+
const {data} = event;
177+
if (data?.type) {
178+
_events.next(data);
179+
}
180+
};
181+
serviceWorker.addEventListener('message', messageListener);
182+
183+
// The injector is optional to avoid breaking changes.
184+
const appRef = injector?.get(ApplicationRef, null, {optional: true});
185+
appRef?.onDestroy(() => {
186+
serviceWorker.removeEventListener('controllerchange', updateController);
187+
serviceWorker.removeEventListener('message', messageListener);
188+
});
173189
}
174190
}
175191

176192
postMessage(action: string, payload: Object): Promise<void> {
177-
return this.worker
178-
.pipe(
179-
take(1),
180-
tap((sw: ServiceWorker) => {
181-
sw.postMessage({
182-
action,
183-
...payload,
184-
});
185-
}),
186-
)
187-
.toPromise()
188-
.then(() => undefined);
193+
return new Promise<void>((resolve) => {
194+
this.worker.pipe(take(1)).subscribe((sw) => {
195+
sw.postMessage({
196+
action,
197+
...payload,
198+
});
199+
200+
resolve();
201+
});
202+
});
189203
}
190204

191205
postMessageWithOperation(
@@ -217,18 +231,23 @@ export class NgswCommChannel {
217231
}
218232

219233
waitForOperationCompleted(nonce: number): Promise<boolean> {
220-
return this.eventsOfType<OperationCompletedEvent>('OPERATION_COMPLETED')
221-
.pipe(
222-
filter((event) => event.nonce === nonce),
223-
take(1),
224-
map((event) => {
225-
if (event.result !== undefined) {
226-
return event.result;
227-
}
228-
throw new Error(event.error!);
229-
}),
230-
)
231-
.toPromise() as Promise<boolean>;
234+
return new Promise<boolean>((resolve, reject) => {
235+
this.eventsOfType<OperationCompletedEvent>('OPERATION_COMPLETED')
236+
.pipe(
237+
filter((event) => event.nonce === nonce),
238+
take(1),
239+
map((event) => {
240+
if (event.result !== undefined) {
241+
return event.result;
242+
}
243+
throw new Error(event.error!);
244+
}),
245+
)
246+
.subscribe({
247+
next: resolve,
248+
error: reject,
249+
});
250+
});
232251
}
233252

234253
get isEnabled(): boolean {

packages/service-worker/src/provider.ts

Lines changed: 53 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,78 +7,75 @@
77
*/
88

99
import {
10-
APP_INITIALIZER,
1110
ApplicationRef,
1211
EnvironmentProviders,
12+
inject,
1313
InjectionToken,
1414
Injector,
1515
makeEnvironmentProviders,
1616
NgZone,
17+
provideAppInitializer,
1718
} from '@angular/core';
18-
import {merge, from, Observable, of} from 'rxjs';
19-
import {delay, take} from 'rxjs/operators';
19+
import type {Observable} from 'rxjs';
2020

2121
import {NgswCommChannel} from './low_level';
2222
import {SwPush} from './push';
2323
import {SwUpdate} from './update';
2424

2525
export const SCRIPT = new InjectionToken<string>(ngDevMode ? 'NGSW_REGISTER_SCRIPT' : '');
2626

27-
export function ngswAppInitializer(
28-
injector: Injector,
29-
script: string,
30-
options: SwRegistrationOptions,
31-
): Function {
32-
return () => {
33-
if (typeof ngServerMode !== 'undefined' && ngServerMode) {
34-
return;
35-
}
27+
export function ngswAppInitializer(): void {
28+
if (typeof ngServerMode !== 'undefined' && ngServerMode) {
29+
return;
30+
}
3631

37-
if (!('serviceWorker' in navigator && options.enabled !== false)) {
38-
return;
39-
}
32+
const options = inject(SwRegistrationOptions);
4033

41-
const ngZone = injector.get(NgZone);
42-
const appRef = injector.get(ApplicationRef);
34+
if (!('serviceWorker' in navigator && options.enabled !== false)) {
35+
return;
36+
}
4337

44-
// Set up the `controllerchange` event listener outside of
45-
// the Angular zone to avoid unnecessary change detections,
46-
// as this event has no impact on view updates.
47-
ngZone.runOutsideAngular(() => {
48-
// Wait for service worker controller changes, and fire an INITIALIZE action when a new SW
49-
// becomes active. This allows the SW to initialize itself even if there is no application
50-
// traffic.
51-
const sw = navigator.serviceWorker;
52-
const onControllerChange = () => sw.controller?.postMessage({action: 'INITIALIZE'});
38+
const script = inject(SCRIPT);
39+
const ngZone = inject(NgZone);
40+
const appRef = inject(ApplicationRef);
5341

54-
sw.addEventListener('controllerchange', onControllerChange);
42+
// Set up the `controllerchange` event listener outside of
43+
// the Angular zone to avoid unnecessary change detections,
44+
// as this event has no impact on view updates.
45+
ngZone.runOutsideAngular(() => {
46+
// Wait for service worker controller changes, and fire an INITIALIZE action when a new SW
47+
// becomes active. This allows the SW to initialize itself even if there is no application
48+
// traffic.
49+
const sw = navigator.serviceWorker;
50+
const onControllerChange = () => sw.controller?.postMessage({action: 'INITIALIZE'});
5551

56-
appRef.onDestroy(() => {
57-
sw.removeEventListener('controllerchange', onControllerChange);
58-
});
52+
sw.addEventListener('controllerchange', onControllerChange);
53+
54+
appRef.onDestroy(() => {
55+
sw.removeEventListener('controllerchange', onControllerChange);
5956
});
57+
});
6058

61-
let readyToRegister$: Observable<unknown>;
59+
// Run outside the Angular zone to avoid preventing the app from stabilizing (especially
60+
// given that some registration strategies wait for the app to stabilize).
61+
ngZone.runOutsideAngular(() => {
62+
let readyToRegister: Promise<void>;
6263

63-
if (typeof options.registrationStrategy === 'function') {
64-
readyToRegister$ = options.registrationStrategy();
64+
const {registrationStrategy} = options;
65+
if (typeof registrationStrategy === 'function') {
66+
readyToRegister = new Promise((resolve) => registrationStrategy().subscribe(() => resolve()));
6567
} else {
66-
const [strategy, ...args] = (
67-
options.registrationStrategy || 'registerWhenStable:30000'
68-
).split(':');
68+
const [strategy, ...args] = (registrationStrategy || 'registerWhenStable:30000').split(':');
6969

7070
switch (strategy) {
7171
case 'registerImmediately':
72-
readyToRegister$ = of(null);
72+
readyToRegister = Promise.resolve();
7373
break;
7474
case 'registerWithDelay':
75-
readyToRegister$ = delayWithTimeout(+args[0] || 0);
75+
readyToRegister = delayWithTimeout(+args[0] || 0);
7676
break;
7777
case 'registerWhenStable':
78-
const whenStable$ = from(injector.get(ApplicationRef).whenStable());
79-
readyToRegister$ = !args[0]
80-
? whenStable$
81-
: merge(whenStable$, delayWithTimeout(+args[0]));
78+
readyToRegister = Promise.race([appRef.whenStable(), delayWithTimeout(+args[0])]);
8279
break;
8380
default:
8481
// Unknown strategy.
@@ -89,30 +86,28 @@ export function ngswAppInitializer(
8986
}
9087

9188
// Don't return anything to avoid blocking the application until the SW is registered.
92-
// Also, run outside the Angular zone to avoid preventing the app from stabilizing (especially
93-
// given that some registration strategies wait for the app to stabilize).
9489
// Catch and log the error if SW registration fails to avoid uncaught rejection warning.
95-
ngZone.runOutsideAngular(() =>
96-
readyToRegister$
97-
.pipe(take(1))
98-
.subscribe(() =>
99-
navigator.serviceWorker
100-
.register(script, {scope: options.scope})
101-
.catch((err) => console.error('Service worker registration failed with:', err)),
102-
),
90+
readyToRegister.then(() =>
91+
navigator.serviceWorker
92+
.register(script, {scope: options.scope})
93+
.catch((err) => console.error('Service worker registration failed with:', err)),
10394
);
104-
};
95+
});
10596
}
10697

107-
function delayWithTimeout(timeout: number): Observable<unknown> {
108-
return of(null).pipe(delay(timeout));
98+
function delayWithTimeout(timeout: number): Promise<void> {
99+
return new Promise((resolve) => setTimeout(resolve, timeout));
109100
}
110101

111-
export function ngswCommChannelFactory(opts: SwRegistrationOptions): NgswCommChannel {
102+
export function ngswCommChannelFactory(
103+
opts: SwRegistrationOptions,
104+
injector: Injector,
105+
): NgswCommChannel {
112106
const isBrowser = !(typeof ngServerMode !== 'undefined' && ngServerMode);
113107

114108
return new NgswCommChannel(
115109
isBrowser && opts.enabled !== false ? navigator.serviceWorker : undefined,
110+
injector,
116111
);
117112
}
118113

@@ -205,13 +200,8 @@ export function provideServiceWorker(
205200
{
206201
provide: NgswCommChannel,
207202
useFactory: ngswCommChannelFactory,
208-
deps: [SwRegistrationOptions],
209-
},
210-
{
211-
provide: APP_INITIALIZER,
212-
useFactory: ngswAppInitializer,
213-
deps: [Injector, SCRIPT, SwRegistrationOptions],
214-
multi: true,
203+
deps: [SwRegistrationOptions, Injector],
215204
},
205+
provideAppInitializer(ngswAppInitializer),
216206
]);
217207
}

0 commit comments

Comments
 (0)