Skip to content

Commit 2092cce

Browse files
authored
Add onIdChange function to Installations (#2486)
1 parent 9c28e7b commit 2092cce

File tree

13 files changed

+473
-38
lines changed

13 files changed

+473
-38
lines changed

packages/firebase/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,12 @@ declare namespace firebase.installations {
13521352
* Deletes the Firebase Installation and all associated data.
13531353
*/
13541354
delete(): Promise<void>;
1355+
1356+
/**
1357+
* Sets a new callback that will get called when Installlation ID changes.
1358+
* Returns an unsubscribe function that will remove the callback when called.
1359+
*/
1360+
onIdChange(callback: (installationId: string) => void): () => void;
13551361
}
13561362
}
13571363

packages/installations-types/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export interface FirebaseInstallations {
3737
* Deletes the Firebase Installation and all associated data.
3838
*/
3939
delete(): Promise<void>;
40+
41+
/**
42+
* Sets a new callback that will get called when Installlation ID changes.
43+
* Returns an unsubscribe function that will remove the callback when called.
44+
*/
45+
onIdChange(callback: (installationId: string) => void): () => void;
4046
}
4147

4248
export type FirebaseInstallationsName = 'installations';

packages/installations/src/functions/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
export { getId } from './get-id';
19-
export { getToken } from './get-token';
20-
export { deleteInstallation } from './delete-installation';
18+
export * from './get-id';
19+
export * from './get-token';
20+
export * from './delete-installation';
21+
export * from './on-id-change';
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @license
3+
* Copyright 2019 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect } from 'chai';
19+
import { stub } from 'sinon';
20+
import '../testing/setup';
21+
import { onIdChange } from './on-id-change';
22+
import * as FidChangedModule from '../helpers/fid-changed';
23+
import { getFakeDependencies } from '../testing/fake-generators';
24+
import { FirebaseDependencies } from '../interfaces/firebase-dependencies';
25+
26+
describe('onIdChange', () => {
27+
let dependencies: FirebaseDependencies;
28+
29+
beforeEach(() => {
30+
dependencies = getFakeDependencies();
31+
stub(FidChangedModule);
32+
});
33+
34+
it('calls addCallback with the given callback and app key when called', () => {
35+
const callback = stub();
36+
onIdChange(dependencies, callback);
37+
expect(FidChangedModule.addCallback).to.have.been.calledOnceWith(
38+
dependencies.appConfig,
39+
callback
40+
);
41+
});
42+
43+
it('calls removeCallback with the given callback and app key when unsubscribe is called', () => {
44+
const callback = stub();
45+
const unsubscribe = onIdChange(dependencies, callback);
46+
unsubscribe();
47+
expect(FidChangedModule.removeCallback).to.have.been.calledOnceWith(
48+
dependencies.appConfig,
49+
callback
50+
);
51+
});
52+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* @license
3+
* Copyright 2019 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { addCallback, removeCallback } from '../helpers/fid-changed';
19+
import { FirebaseDependencies } from '../interfaces/firebase-dependencies';
20+
21+
export type IdChangeCallbackFn = (installationId: string) => void;
22+
export type IdChangeUnsubscribeFn = () => void;
23+
24+
/**
25+
* Sets a new callback that will get called when Installation ID changes.
26+
* Returns an unsubscribe function that will remove the callback when called.
27+
*/
28+
export function onIdChange(
29+
{ appConfig }: FirebaseDependencies,
30+
callback: IdChangeCallbackFn
31+
): IdChangeUnsubscribeFn {
32+
addCallback(appConfig, callback);
33+
34+
return () => {
35+
removeCallback(appConfig, callback);
36+
};
37+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* @license
3+
* Copyright 2019 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect } from 'chai';
19+
import { stub } from 'sinon';
20+
import '../testing/setup';
21+
import { AppConfig } from '../interfaces/app-config';
22+
import {
23+
fidChanged,
24+
addCallback,
25+
removeCallback
26+
} from '../helpers/fid-changed';
27+
import { getFakeAppConfig } from '../testing/fake-generators';
28+
29+
const FID = 'evil-lies-in-every-man';
30+
31+
describe('onIdChange', () => {
32+
describe('with single app', () => {
33+
let appConfig: AppConfig;
34+
35+
beforeEach(() => {
36+
appConfig = getFakeAppConfig();
37+
});
38+
39+
it('calls the provided callback when FID changes', () => {
40+
const stubFn = stub();
41+
addCallback(appConfig, stubFn);
42+
43+
fidChanged(appConfig, FID);
44+
45+
expect(stubFn).to.have.been.calledOnceWith(FID);
46+
});
47+
48+
it('calls multiple callbacks', () => {
49+
const stubA = stub();
50+
addCallback(appConfig, stubA);
51+
const stubB = stub();
52+
addCallback(appConfig, stubB);
53+
54+
fidChanged(appConfig, FID);
55+
56+
expect(stubA).to.have.been.calledOnceWith(FID);
57+
expect(stubB).to.have.been.calledOnceWith(FID);
58+
});
59+
60+
it('does not call removed callbacks', () => {
61+
const stubFn = stub();
62+
addCallback(appConfig, stubFn);
63+
64+
removeCallback(appConfig, stubFn);
65+
fidChanged(appConfig, FID);
66+
67+
expect(stubFn).not.to.have.been.called;
68+
});
69+
70+
it('does not throw when removeCallback is called multiple times', () => {
71+
const stubFn = stub();
72+
addCallback(appConfig, stubFn);
73+
74+
removeCallback(appConfig, stubFn);
75+
removeCallback(appConfig, stubFn);
76+
fidChanged(appConfig, FID);
77+
78+
expect(stubFn).not.to.have.been.called;
79+
});
80+
});
81+
82+
describe('with multiple apps', () => {
83+
let appConfigA: AppConfig;
84+
let appConfigB: AppConfig;
85+
86+
beforeEach(() => {
87+
appConfigA = getFakeAppConfig();
88+
appConfigB = getFakeAppConfig({ appName: 'differentAppName' });
89+
});
90+
91+
it('calls the correct callback when FID changes', () => {
92+
const stubA = stub();
93+
addCallback(appConfigA, stubA);
94+
const stubB = stub();
95+
addCallback(appConfigB, stubB);
96+
97+
fidChanged(appConfigA, FID);
98+
99+
expect(stubA).to.have.been.calledOnceWith(FID);
100+
expect(stubB).not.to.have.been.called;
101+
});
102+
});
103+
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* @license
3+
* Copyright 2019 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { getKey } from '../util/get-key';
19+
import { AppConfig } from '../interfaces/app-config';
20+
import { IdChangeCallbackFn } from '../functions';
21+
22+
const fidChangeCallbacks: Map<string, Set<IdChangeCallbackFn>> = new Map();
23+
24+
/**
25+
* Calls the onIdChange callbacks with the new FID value, and broadcasts the
26+
* change to other tabs.
27+
*/
28+
export function fidChanged(appConfig: AppConfig, fid: string): void {
29+
const key = getKey(appConfig);
30+
31+
callFidChangeCallbacks(key, fid);
32+
broadcastFidChange(key, fid);
33+
}
34+
35+
export function addCallback(
36+
appConfig: AppConfig,
37+
callback: IdChangeCallbackFn
38+
): void {
39+
// Open the broadcast channel if it's not already open,
40+
// to be able to listen to change events from other tabs.
41+
getBroadcastChannel();
42+
43+
const key = getKey(appConfig);
44+
45+
let callbackSet = fidChangeCallbacks.get(key);
46+
if (!callbackSet) {
47+
callbackSet = new Set();
48+
fidChangeCallbacks.set(key, callbackSet);
49+
}
50+
callbackSet.add(callback);
51+
}
52+
53+
export function removeCallback(
54+
appConfig: AppConfig,
55+
callback: IdChangeCallbackFn
56+
): void {
57+
const key = getKey(appConfig);
58+
59+
const callbackSet = fidChangeCallbacks.get(key);
60+
61+
if (!callbackSet) {
62+
return;
63+
}
64+
65+
callbackSet.delete(callback);
66+
if (callbackSet.size === 0) {
67+
fidChangeCallbacks.delete(key);
68+
}
69+
70+
// Close broadcast channel if there are no more callbacks.
71+
closeBroadcastChannel();
72+
}
73+
74+
function callFidChangeCallbacks(key: string, fid: string): void {
75+
const callbacks = fidChangeCallbacks.get(key);
76+
if (!callbacks) {
77+
return;
78+
}
79+
80+
for (const callback of callbacks) {
81+
callback(fid);
82+
}
83+
}
84+
85+
function broadcastFidChange(key: string, fid: string): void {
86+
const channel = getBroadcastChannel();
87+
if (channel) {
88+
channel.postMessage({ key, fid });
89+
}
90+
closeBroadcastChannel();
91+
}
92+
93+
let broadcastChannel: BroadcastChannel | null = null;
94+
/** Opens and returns a BroadcastChannel if it is supported by the browser. */
95+
function getBroadcastChannel(): BroadcastChannel | null {
96+
if (!broadcastChannel && 'BroadcastChannel' in self) {
97+
broadcastChannel = new BroadcastChannel('[Firebase] FID Change');
98+
broadcastChannel.onmessage = e => {
99+
callFidChangeCallbacks(e.data.key, e.data.fid);
100+
};
101+
}
102+
return broadcastChannel;
103+
}
104+
105+
function closeBroadcastChannel(): void {
106+
if (fidChangeCallbacks.size === 0 && broadcastChannel) {
107+
broadcastChannel.close();
108+
broadcastChannel = null;
109+
}
110+
}

0 commit comments

Comments
 (0)