Skip to content

Add types for Service Worker Controller #673

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 12, 2018
135 changes: 78 additions & 57 deletions packages/messaging/src/controllers/sw-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,61 +14,67 @@
* limitations under the License.
*/

import './sw-types';

import { FirebaseApp } from '@firebase/app-types';

import { ERROR_CODES } from '../models/errors';
import { DEFAULT_PUBLIC_VAPID_KEY } from '../models/fcm-details';
import * as WorkerPageMessage from '../models/worker-page-message';
import {
InternalMessage,
MessageParameter,
MessageType
} from '../models/worker-page-message';
import { ControllerInterface } from './controller-interface';

// Let TS know that this is a service worker
declare const self: ServiceWorkerGlobalScope;

const FCM_MSG = 'FCM_MSG';

export type BgMessageHandler = (input: any) => Promise<any>;
export type BgMessageHandler = (input: Payload) => Promise<void>;

export interface NotificationDetails extends NotificationOptions {
title: string;
click_action?: string;
}

export interface Payload {
notification?: NotificationDetails;
}

export class SWController extends ControllerInterface {
private bgMessageHandler_: BgMessageHandler | null = null;

constructor(app: FirebaseApp) {
super(app);

self.addEventListener(
'push',
(e: any) => {
this.onPush(e);
},
false
);
self.addEventListener(
'pushsubscriptionchange',
(e: any) => {
this.onSubChange(e);
},
false
);
self.addEventListener(
'notificationclick',
(e: any) => {
this.onNotificationClick(e);
},
false
);
self.addEventListener('push', e => {
this.onPush(e);
});
self.addEventListener('pushsubscriptionchange', e => {
this.onSubChange(e);
});
self.addEventListener('notificationclick', e => {
this.onNotificationClick(e);
});
}

// Visible for testing
// TODO: Make private
onPush(event: any): void {
onPush(event: PushEvent): void {
event.waitUntil(this.onPush_(event));
}

// Visible for testing
// TODO: Make private
onSubChange(event: any): void {
onSubChange(event: PushSubscriptionChangeEvent): void {
event.waitUntil(this.onSubChange_(event));
}

// Visible for testing
// TODO: Make private
onNotificationClick(event: any): void {
onNotificationClick(event: NotificationEvent): void {
event.waitUntil(this.onNotificationClick_(event));
}

Expand All @@ -84,8 +90,12 @@ export class SWController extends ControllerInterface {
* If there is no notification data in the payload then no notification will be
* shown.
*/
private async onPush_(event: any): Promise<void> {
let msgPayload: any;
private async onPush_(event: PushEvent): Promise<void> {
if (!event.data) {
return;
}

let msgPayload: Payload;
try {
msgPayload = event.data.json();
} catch (err) {
Expand All @@ -105,15 +115,18 @@ export class SWController extends ControllerInterface {

const notificationDetails = this.getNotificationData_(msgPayload);
if (notificationDetails) {
const notificationTitle = (notificationDetails as any).title || '';
const notificationTitle = notificationDetails.title || '';
const reg = await this.getSWRegistration_();
return reg.showNotification(notificationTitle, notificationDetails);
} else if (this.bgMessageHandler_) {
return this.bgMessageHandler_(msgPayload);
await this.bgMessageHandler_(msgPayload);
return;
}
}

private async onSubChange_(event: any): Promise<void> {
private async onSubChange_(
event: PushSubscriptionChangeEvent
): Promise<void> {
let registration: ServiceWorkerRegistration;
try {
registration = await this.getSWRegistration_();
Expand All @@ -140,12 +153,12 @@ export class SWController extends ControllerInterface {
}

// Attempt to delete the token if we know it's bad
await this.deleteToken(tokenDetails['fcmToken']);
await this.deleteToken(tokenDetails.fcmToken);
throw err;
}
}

private async onNotificationClick_(event: any): Promise<void> {
private async onNotificationClick_(event: NotificationEvent): Promise<void> {
if (
!event.notification ||
!event.notification.data ||
Expand All @@ -160,13 +173,13 @@ export class SWController extends ControllerInterface {

event.notification.close();

const msgPayload = event.notification.data[FCM_MSG];
if (!msgPayload['notification']) {
const msgPayload: Payload = event.notification.data[FCM_MSG];
if (!msgPayload.notification) {
// Nothing to do.
return;
}

const clickAction = msgPayload['notification']['click_action'];
const clickAction = msgPayload.notification.click_action;
if (!clickAction) {
// Nothing to do.
return;
Expand All @@ -175,7 +188,7 @@ export class SWController extends ControllerInterface {
let windowClient = await this.getWindowClient_(clickAction);
if (!windowClient) {
// Unable to find window client so need to open one.
windowClient = await (self as any).clients.openWindow(clickAction);
windowClient = await self.clients.openWindow(clickAction);
} else {
windowClient = await windowClient.focus();
}
Expand All @@ -186,10 +199,10 @@ export class SWController extends ControllerInterface {
}

// Delete notification data from payload before sending to the page.
delete msgPayload['notification'];
delete msgPayload.notification;

const internalMsg = WorkerPageMessage.createNewMsg(
WorkerPageMessage.TYPES_OF_MSG.NOTIFICATION_CLICKED,
const internalMsg = createNewMsg(
MessageType.NOTIFICATION_CLICKED,
msgPayload
);

Expand All @@ -200,7 +213,7 @@ export class SWController extends ControllerInterface {

// Visible for testing
// TODO: Make private
getNotificationData_(msgPayload: any): NotificationOptions | undefined {
getNotificationData_(msgPayload: Payload): NotificationDetails | undefined {
if (!msgPayload) {
return;
}
Expand Down Expand Up @@ -250,16 +263,16 @@ export class SWController extends ControllerInterface {
*/
// Visible for testing
// TODO: Make private
async getWindowClient_(url: string): Promise<any> {
async getWindowClient_(url: string): Promise<WindowClient | null> {
// Use URL to normalize the URL when comparing to windowClients.
// This at least handles whether to include trailing slashes or not
const parsedURL = new URL(url, (self as any).location).href;
const parsedURL = new URL(url, self.location.href).href;

const clientList = await getClientList();

let suitableClient = null;
let suitableClient: WindowClient | null = null;
for (let i = 0; i < clientList.length; i++) {
const parsedClientUrl = new URL(clientList[i].url, (self as any).location)
const parsedClientUrl = new URL(clientList[i].url, self.location.href)
.href;
if (parsedClientUrl === parsedURL) {
suitableClient = clientList[i];
Expand All @@ -279,7 +292,10 @@ export class SWController extends ControllerInterface {
*/
// Visible for testing
// TODO: Make private
async attemptToMessageClient_(client: any, message: any): Promise<void> {
async attemptToMessageClient_(
client: WindowClient,
message: InternalMessage
): Promise<void> {
// NOTE: This returns a promise in case this API is abstracted later on to
// do additional work
if (!client) {
Expand All @@ -299,7 +315,7 @@ export class SWController extends ControllerInterface {
const clientList = await getClientList();

return clientList.some(
(client: any) => client.visibilityState === 'visible'
(client: WindowClient) => client.visibilityState === 'visible'
);
}

Expand All @@ -311,16 +327,13 @@ export class SWController extends ControllerInterface {
*/
// Visible for testing
// TODO: Make private
async sendMessageToWindowClients_(msgPayload: any): Promise<void> {
async sendMessageToWindowClients_(msgPayload: Payload): Promise<void> {
const clientList = await getClientList();

const internalMsg = WorkerPageMessage.createNewMsg(
WorkerPageMessage.TYPES_OF_MSG.PUSH_MSG_RECEIVED,
msgPayload
);
const internalMsg = createNewMsg(MessageType.PUSH_MSG_RECEIVED, msgPayload);

await Promise.all(
clientList.map((client: any) =>
clientList.map(client =>
this.attemptToMessageClient_(client, internalMsg)
)
);
Expand All @@ -331,7 +344,7 @@ export class SWController extends ControllerInterface {
* @return he service worker registration to be used for the push service.
*/
async getSWRegistration_(): Promise<ServiceWorkerRegistration> {
return (self as any).registration;
return self.registration;
}

/**
Expand All @@ -355,9 +368,17 @@ export class SWController extends ControllerInterface {
}
}

function getClientList(): Promise<any[]> {
return (self as any).clients.matchAll({
function getClientList(): Promise<WindowClient[]> {
return self.clients.matchAll({
type: 'window',
includeUncontrolled: true
});
// TS doesn't know that "type: 'window'" means it'll return WindowClient[]
}) as Promise<WindowClient[]>;
}

function createNewMsg(msgType: MessageType, msgData: Payload): InternalMessage {
return {
[MessageParameter.TYPE_OF_MSG]: msgType,
[MessageParameter.DATA]: msgData
};
}
Loading