fix service worker
This commit is contained in:
@@ -59,6 +59,7 @@
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"serviceWorker": "ngsw-config.json",
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
|
||||
+2
-11
@@ -18,20 +18,11 @@
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"source": "/**/!(*.*)",
|
||||
"source": "**",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "max-age=1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "**/*.@(js|css)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "max-age=846000"
|
||||
"value": "no-cache"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {fader} from './animations';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
import {NavigationComponent} from './widget-modules/components/application-frame/navigation/navigation.component';
|
||||
import {ThemeService} from './services/theme/theme.service';
|
||||
import {AppUpdateService} from './services/app-update.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -15,6 +16,7 @@ import {ThemeService} from './services/theme/theme.service';
|
||||
export class AppComponent implements OnInit {
|
||||
public constructor() {
|
||||
inject(ThemeService);
|
||||
inject(AppUpdateService);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import {ApplicationRef} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {SwUpdate, VersionEvent} from '@angular/service-worker';
|
||||
import {Subject} from 'rxjs';
|
||||
import {vi} from 'vitest';
|
||||
import {AppUpdateService} from './app-update.service';
|
||||
|
||||
describe('AppUpdateService', () => {
|
||||
let stable: Subject<boolean>;
|
||||
let versions: Subject<VersionEvent>;
|
||||
let updates: {isEnabled: boolean; versionUpdates: Subject<VersionEvent>; unrecoverable: Subject<unknown>; checkForUpdate: ReturnType<typeof vi.fn>};
|
||||
let snackBar: {open: ReturnType<typeof vi.fn>};
|
||||
|
||||
beforeEach(() => {
|
||||
stable = new Subject();
|
||||
versions = new Subject();
|
||||
updates = {isEnabled: true, versionUpdates: versions, unrecoverable: new Subject(), checkForUpdate: vi.fn().mockResolvedValue(false)};
|
||||
snackBar = {open: vi.fn().mockReturnValue({onAction: () => new Subject()})};
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: ApplicationRef, useValue: {isStable: stable}},
|
||||
{provide: SwUpdate, useValue: updates},
|
||||
{provide: MatSnackBar, useValue: snackBar},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('waits for stability and checks again when connectivity returns', async () => {
|
||||
TestBed.inject(AppUpdateService);
|
||||
stable.next(false);
|
||||
expect(updates.checkForUpdate).not.toHaveBeenCalled();
|
||||
stable.next(true);
|
||||
expect(updates.checkForUpdate).toHaveBeenCalledTimes(1);
|
||||
await Promise.resolve();
|
||||
window.dispatchEvent(new Event('online'));
|
||||
expect(updates.checkForUpdate).toHaveBeenCalledTimes(2);
|
||||
TestBed.resetTestingModule();
|
||||
window.dispatchEvent(new Event('online'));
|
||||
expect(updates.checkForUpdate).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('does not check when the service worker is disabled', () => {
|
||||
updates.isEnabled = false;
|
||||
TestBed.inject(AppUpdateService);
|
||||
stable.next(true);
|
||||
window.dispatchEvent(new Event('online'));
|
||||
expect(updates.checkForUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('offers a persistent reload action only after the update is ready', () => {
|
||||
TestBed.inject(AppUpdateService);
|
||||
versions.next({type: 'VERSION_DETECTED', version: {hash: 'new'}});
|
||||
expect(snackBar.open).not.toHaveBeenCalled();
|
||||
const ready: VersionEvent = {type: 'VERSION_READY', currentVersion: {hash: 'old'}, latestVersion: {hash: 'new'}};
|
||||
versions.next(ready);
|
||||
versions.next(ready);
|
||||
expect(snackBar.open).toHaveBeenCalledExactlyOnceWith('Eine neue Version ist verfügbar.', 'Jetzt aktualisieren', {duration: 0});
|
||||
});
|
||||
|
||||
it('offers recovery when cached application files cannot be recovered', () => {
|
||||
TestBed.inject(AppUpdateService);
|
||||
updates.unrecoverable.next({reason: 'Missing chunk'});
|
||||
expect(snackBar.open).toHaveBeenCalledWith('Die Anwendung muss neu geladen werden.', 'Jetzt aktualisieren', {duration: 0});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import {ApplicationRef, DestroyRef, Injectable, NgZone, inject} from '@angular/core';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {SwUpdate} from '@angular/service-worker';
|
||||
import {concat, filter, first, fromEvent, interval, merge} from 'rxjs';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class AppUpdateService {
|
||||
private readonly updates = inject(SwUpdate, {optional: true});
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly zone = inject(NgZone);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private checking = false;
|
||||
private prompted = false;
|
||||
|
||||
constructor() {
|
||||
if (!this.updates?.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updates.versionUpdates.pipe(takeUntilDestroyed()).subscribe(event => {
|
||||
if (event.type === 'VERSION_READY') {
|
||||
this.promptReload('Eine neue Version ist verfügbar.');
|
||||
}
|
||||
});
|
||||
this.updates.unrecoverable.pipe(takeUntilDestroyed()).subscribe(() => {
|
||||
this.promptReload('Die Anwendung muss neu geladen werden.');
|
||||
});
|
||||
|
||||
const appRef = inject(ApplicationRef);
|
||||
// Poll only after stabilization so service worker registration is not delayed.
|
||||
this.zone.runOutsideAngular(() => {
|
||||
concat(
|
||||
appRef.isStable.pipe(first(stable => stable)),
|
||||
merge(
|
||||
interval(15 * 60 * 1000),
|
||||
fromEvent(window, 'online'),
|
||||
fromEvent(document, 'visibilitychange').pipe(filter(() => document.visibilityState === 'visible'))
|
||||
)
|
||||
)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => void this.checkForUpdate());
|
||||
});
|
||||
}
|
||||
|
||||
private async checkForUpdate(): Promise<void> {
|
||||
if (this.checking) {
|
||||
return;
|
||||
}
|
||||
this.checking = true;
|
||||
try {
|
||||
await this.updates?.checkForUpdate();
|
||||
} catch (error) {
|
||||
// Offline clients retry when connectivity returns or on the next interval.
|
||||
console.warn('Aktualisierung konnte nicht geprüft werden.', error);
|
||||
} finally {
|
||||
this.checking = false;
|
||||
}
|
||||
}
|
||||
|
||||
private promptReload(message: string): void {
|
||||
if (this.prompted) {
|
||||
return;
|
||||
}
|
||||
this.prompted = true;
|
||||
this.zone.run(() => {
|
||||
this.snackBar
|
||||
.open(message, 'Jetzt aktualisieren', {duration: 0})
|
||||
.onAction()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => window.location.reload());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -160,6 +160,28 @@ body.theme-dark .mat-mdc-text-field-wrapper {
|
||||
background-color: rgba(36, 54, 72, 0.96);
|
||||
}
|
||||
|
||||
body.theme-dark .mat-button-toggle:not(.mat-button-toggle-checked) {
|
||||
--mat-standard-button-toggle-background-color: var(--surface-strong);
|
||||
--mat-standard-button-toggle-text-color: var(--text);
|
||||
--mat-standard-button-toggle-state-layer-color: var(--primary-hover);
|
||||
--mat-standard-button-toggle-disabled-state-background-color: var(--surface-muted);
|
||||
--mat-standard-button-toggle-disabled-state-text-color: var(--text-soft);
|
||||
background-color: var(--surface-strong);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
body.theme-dark .mat-button-toggle:not(.mat-button-toggle-checked) .mat-button-toggle-button {
|
||||
background-color: var(--surface-strong);
|
||||
}
|
||||
|
||||
body.theme-dark .mat-button-toggle:not(.mat-button-toggle-checked) .mat-button-toggle-label-content {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
body.theme-dark .mat-button-toggle:not(.mat-button-toggle-checked):hover {
|
||||
background-color: var(--surface);
|
||||
}
|
||||
|
||||
body.theme-dark .mdc-notched-outline__leading,
|
||||
body.theme-dark .mdc-notched-outline__notch,
|
||||
body.theme-dark .mdc-notched-outline__trailing {
|
||||
|
||||
Reference in New Issue
Block a user