Skip to content
This repository was archived by the owner on Oct 12, 2021. It is now read-only.

Commit f7e0730

Browse files
committed
feat(worker): support coordinated updates
Ordinarily, the service worker does not activate a pending update until all application tabs have been closed. For some applications this method of background updates is acceptable. For others, particularly applications with long-lived tabs, this can lead to staleness if the conditions for a background update are never triggered. This change introduces a channel by which the application can subscribe to notifications of pending updates. The application can then prompt the user about the update or choose to just go ahead and apply the update, via a new method activateUpdate().
1 parent 07676ac commit f7e0730

File tree

5 files changed

+208
-9
lines changed

5 files changed

+208
-9
lines changed

service-worker/worker/src/companion/comm.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ function fromPromise<T>(promiseFn: (() => Promise<T>)): Observable<T> {
4242
});
4343
}
4444

45+
export interface UpdateEvent {
46+
type: "pending" | "activation";
47+
version?: string;
48+
}
49+
4550
// A push notification registration, including the endpoint URL and encryption keys.
4651
export class NgPushRegistration {
4752
private ps: PushSubscription;
@@ -88,6 +93,8 @@ export class NgServiceWorker {
8893

8994
push: Observable<any>;
9095

96+
updates: Observable<UpdateEvent>;
97+
9198
constructor(private zone: NgZone) {
9299
// Extract a typed version of navigator.serviceWorker.
93100
this.container = navigator['serviceWorker'] as ServiceWorkerContainer;
@@ -124,6 +131,11 @@ export class NgServiceWorker {
124131
this.push = Observable
125132
.defer(() => this.send({cmd: 'push'}))
126133
.share();
134+
135+
// Setup the updates Observable as a broadcast mechanism for update notifications.
136+
this.updates = Observable
137+
.defer(() => this.send({cmd: 'update'}))
138+
.share();
127139
}
128140

129141
private registrationForWorker(): ((obs: Observable<ServiceWorker>) => Observable<ServiceWorkerRegistration>) {
@@ -205,6 +217,13 @@ export class NgServiceWorker {
205217
});
206218
}
207219

220+
activateUpdate(version: string): Observable<boolean> {
221+
return this.send({
222+
cmd: 'activateUpdate',
223+
version,
224+
});
225+
}
226+
208227
registerForPush(): Observable<NgPushRegistration> {
209228
return this
210229
// Wait for a controlling worker to exist.

service-worker/worker/src/test/e2e/harness/client/src/controller.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ import 'rxjs/add/operator/startWith';
2121
<option value="SW_INSTALL">Install service worker</option>
2222
<option value="COMPANION_PING">Ping from the companion</option>
2323
<option value="COMPANION_REG_PUSH">Register for push notifications</option>
24-
<option value="FORCE_UPDATE">Force an update</option>
24+
<option value="CHECK_FOR_UPDATES">Check for updates</option>
25+
<option value="COMPANION_SUBSCRIBE_TO_UPDATES">Subscribe to update notifications</option>
26+
<option value="FORCE_UPDATE">Force install pending update</option>
2527
<option value="RESET">Reset</option>
2628
</select>
2729
<span *ngIf="alert" id="alert">ASYNC ALERT</span>
30+
<span *ngIf="updateAlert" id="updateAlert">UPDATE ALERT</span>
2831
<input id="actionInput" #actionInput [(ngModel)]="action">
2932
<button id="actionExec" (click)="refresh(actionInput.value)">Exec</button>
3033
</div>
@@ -49,26 +52,43 @@ import 'rxjs/add/operator/startWith';
4952
</div>
5053
<button id="installAction" (click)="installWorker(workerUrl.value)">Install service worker</button>
5154
</div>
55+
<div *ngSwitchCase="'FORCE_UPDATE'">
56+
<div>
57+
<label for="updateVersion">
58+
Version:
59+
</label>
60+
<input #updateVersion id="updateVersion">
61+
</div>
62+
<button id="updateAction" (click)="forceUpdate(updateVersion.value)">Force update</button>
63+
</div>
5264
</div>
5365
66+
<h5>Result</h5>
5467
<pre id="result">{{result}}</pre>
68+
<h5>Updates</h5>
69+
<pre id="updates">{{updates}}</pre>
70+
<h5>Log</h5>
5571
<pre id="log">{{log | json}}</pre>
5672
`
5773
})
5874
export class ControllerCmp {
5975
result: string = null;
76+
updates: string = null;
6077
action: string = '';
6178
alert: boolean = false;
79+
updateAlert: boolean = false;
6280
log: string[] = [];
6381

6482
pushSub = null;
83+
updateSub = null;
6584
pushes = [];
6685

6786
constructor(public sw: NgServiceWorker) {
6887
sw.log().subscribe(message => this.log.push(message));
6988
}
7089

7190
actionSelected(action): void {
91+
console.log('set action', action);
7292
this.action = action;
7393
}
7494

@@ -87,9 +107,10 @@ export class ControllerCmp {
87107
switch (action) {
88108
case 'RESET':
89109
this.alert = false;
110+
this.updateAlert = false;
90111
this.result = 'reset';
91112
break;
92-
case 'FORCE_UPDATE':
113+
case 'CHECK_FOR_UPDATES':
93114
this
94115
.sw
95116
.checkForUpdate()
@@ -136,6 +157,21 @@ export class ControllerCmp {
136157
this.result = value;
137158
this.alert = true;
138159
});
160+
break;
161+
case 'COMPANION_SUBSCRIBE_TO_UPDATES':
162+
this.updateSub = this
163+
.sw
164+
.updates
165+
.scan((acc, value) => acc.concat(value), [])
166+
.startWith([])
167+
.map(value => JSON.stringify(value))
168+
.subscribe(value => {
169+
this.updates = value;
170+
if (value.length > 2) {
171+
this.updateAlert = true;
172+
}
173+
});
174+
break;
139175
default:
140176
this.result = null;
141177
}
@@ -161,6 +197,16 @@ export class ControllerCmp {
161197
})
162198
});
163199
}
200+
201+
forceUpdate(version: string): void {
202+
this
203+
.sw
204+
.activateUpdate(version)
205+
.subscribe(value => {
206+
this.result = JSON.stringify(value);
207+
this.alert = true;
208+
})
209+
}
164210

165211
checkServiceWorker(): void {
166212
this.result = '';

service-worker/worker/src/test/e2e/harness/server/page-object.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@ export class HarnessPageObject {
2323
return element(by.css('#result')).getText() as any as Promise<string>;
2424
}
2525

26+
get updates(): Promise<string> {
27+
browser.wait(protractor.ExpectedConditions.presenceOf(element(by.id('updateAlert'))));
28+
return element(by.id('updates')).getText() as any as Promise<string>;
29+
}
30+
2631
get asyncResult(): Promise<string> {
2732
browser.wait(protractor.ExpectedConditions.presenceOf(element(by.id('alert'))));
2833
return this.result;
2934
}
30-
35+
3136
request(url: string): Promise<string> {
3237
this.selectAction('MAKE_REQUEST');
3338
this.setTextOn('requestUrl', url);
@@ -40,6 +45,13 @@ export class HarnessPageObject {
4045
this.setTextOn('workerUrl', url);
4146
this.clickButton('installAction');
4247
}
48+
49+
forceUpdate(version: string): Promise<string> {
50+
this.selectAction('FORCE_UPDATE');
51+
this.setTextOn('updateVersion', version);
52+
this.clickButton('updateAction');
53+
return this.result;
54+
}
4355

4456
hasActiveWorker(): Promise<boolean> {
4557
this.selectAction('SW_CHECK');
@@ -93,6 +105,8 @@ export class HarnessPageObject {
93105
this.selectAction('RESET');
94106
browser.wait(protractor.ExpectedConditions.not(
95107
protractor.ExpectedConditions.presenceOf(element(by.id('alert')))));
108+
browser.wait(protractor.ExpectedConditions.not(
109+
protractor.ExpectedConditions.presenceOf(element(by.id('updateAlert')))));
96110
}
97111

98112
registerForPush(): Promise<string> {
@@ -103,9 +117,14 @@ export class HarnessPageObject {
103117

104118
checkForUpdate(): Promise<boolean> {
105119
this.reset();
106-
this.selectAction('FORCE_UPDATE');
120+
this.selectAction('CHECK_FOR_UPDATES');
107121
return this
108122
.asyncResult
109123
.then(JSON.parse);
110124
}
125+
126+
subscribeToUpdates(): void {
127+
this.reset();
128+
this.selectAction('COMPANION_SUBSCRIBE_TO_UPDATES');
129+
}
111130
}

service-worker/worker/src/test/e2e/spec/sanity.e2e.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ const UPDATE_MANIFEST = {
3636
}
3737
};
3838

39+
const FORCED_UPDATE_MANIFEST = {
40+
static: {
41+
urls: {
42+
'/hello.txt': 'changed_again',
43+
'/goodbye.txt': 'same',
44+
}
45+
}
46+
};
47+
3948
beforeAll(done => {
4049
create(8080, 'tmp/es5/src/test/e2e/harness/client').then(s => {
4150
server = s;
@@ -186,4 +195,35 @@ describe('world sanity', () => {
186195
.then(result => expect(result).toBe('Should be reloaded'))
187196
.then(() => done());
188197
});
198+
it('notifies the app when an update is available', done => {
199+
server.addResponse('/ngsw-manifest.json', JSON.stringify(FORCED_UPDATE_MANIFEST));
200+
server.addResponse('/hello.txt', 'And again');
201+
server.addResponse('/goodbye.txt', 'Should still not be re-fetched.');
202+
Promise
203+
.resolve()
204+
.then(() => po.subscribeToUpdates())
205+
.then(() => po.checkForUpdate())
206+
.then(updated => expect(updated).toBeTruthy())
207+
.then(() => po.updates)
208+
.then(updates => JSON.parse(updates))
209+
.then(updates => {
210+
expect(updates.length).toBe(1);
211+
const update = updates[0];
212+
expect(update['type']).toBe('pending');
213+
const hash = update['version'];
214+
po.reset();
215+
return po.forceUpdate(hash);
216+
})
217+
.then(() => po.updates)
218+
.then(updates => JSON.parse(updates))
219+
.then(updates => {
220+
expect(updates.length).toBe(2);
221+
expect(updates[1].type).toBe('activation');
222+
})
223+
.then(() => po.request('/hello.txt'))
224+
.then(result => expect(result).toBe('And again'))
225+
.then(() => po.request('/goodbye.txt'))
226+
.then(result => expect(result).toBe('Goodbye world!'))
227+
.then(() => done());
228+
});
189229
});

0 commit comments

Comments
 (0)