Skip to content

Commit 2348a25

Browse files
authored
Use the FID from the server if client FID generation fails (#1925)
Starting from SDK v0.2.0, the server will start returning a valid FID instead of returning an error if the client tries to register an invalid FID, and the SDK will save that FID to the DB, replacing the invalid one. In case of a failure with client side FID generation, getId will wait for a valid FID from the server before resolving instead of throwing or returning an invalid FID.
1 parent be8940f commit 2348a25

12 files changed

+101
-52
lines changed

packages/firebase/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"@firebase/database": "0.4.6",
4444
"@firebase/firestore": "1.4.2",
4545
"@firebase/functions": "0.4.10",
46-
"@firebase/installations": "0.1.7",
46+
"@firebase/installations": "0.2.0",
4747
"@firebase/messaging": "0.4.3",
4848
"@firebase/polyfill": "0.3.14",
4949
"@firebase/storage": "0.3.4",

packages/installations/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@firebase/installations",
3-
"version": "0.1.7",
3+
"version": "0.2.0",
44
"main": "dist/index.cjs.js",
55
"module": "dist/index.esm.js",
66
"esm2017": "dist/index.esm2017.js",

packages/installations/src/api/create-installation.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ describe('createInstallation', () => {
5858
token:
5959
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
6060
expiresIn: '604800s'
61-
}
61+
},
62+
fid: FID
6263
};
6364
fetchSpy = stub(self, 'fetch');
6465
});

packages/installations/src/api/create-installation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export async function createInstallation(
5555
if (response.ok) {
5656
const responseValue: CreateInstallationResponse = await response.json();
5757
const registeredInstallationEntry: RegisteredInstallationEntry = {
58-
fid,
58+
fid: responseValue.fid,
5959
registrationStatus: RequestStatus.COMPLETED,
6060
refreshToken: responseValue.refreshToken,
6161
authToken: extractAuthTokenInfoFromResponse(responseValue.authToken)

packages/installations/src/helpers/buffer-to-base64-url-safe.test.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,4 @@ describe('bufferToBase64', () => {
2828
BASE_64_REPRESENTATION
2929
);
3030
});
31-
32-
it('returns a base64 representation of an ArrayBuffer', () => {
33-
expect(bufferToBase64UrlSafe(TYPED_ARRAY_REPRESENTATION.buffer)).to.equal(
34-
BASE_64_REPRESENTATION
35-
);
36-
});
3731
});

packages/installations/src/helpers/buffer-to-base64-url-safe.ts

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

18-
export function bufferToBase64UrlSafe(
19-
buffer: ArrayBuffer | Uint8Array
20-
): string {
21-
const array = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
18+
export function bufferToBase64UrlSafe(array: Uint8Array): string {
2219
const b64 = btoa(String.fromCharCode(...array));
2320
return b64.replace(/\+/g, '-').replace(/\//g, '_');
2421
}

packages/installations/src/helpers/generate-fid.test.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import { expect } from 'chai';
1919
import { stub } from 'sinon';
2020
import '../testing/setup';
21-
import { generateFid } from './generate-fid';
21+
import { generateFid, VALID_FID_PATTERN } from './generate-fid';
2222

2323
/** A few random values to generate a FID from. */
2424
// prettier-ignore
@@ -53,8 +53,6 @@ const EXPECTED_FIDS = [
5353
'f_____________________'
5454
];
5555

56-
const VALID_FID = /^[cdef][A-Za-z0-9_-]{21}$/;
57-
5856
describe('generateFid', () => {
5957
it('deterministically generates FIDs based on crypto.getRandomValues', () => {
6058
let randomValueIndex = 0;
@@ -77,7 +75,10 @@ describe('generateFid', () => {
7775
it('generates valid FIDs', () => {
7876
for (let i = 0; i < 1000; i++) {
7977
const fid = generateFid();
80-
expect(VALID_FID.test(fid)).to.equal(true, `${fid} is not a valid FID`);
78+
expect(VALID_FID_PATTERN.test(fid)).to.equal(
79+
true,
80+
`${fid} is not a valid FID`
81+
);
8182
}
8283
});
8384

@@ -117,4 +118,11 @@ describe('generateFid', () => {
117118
});
118119
}
119120
}).timeout(30000);
121+
122+
it('returns an empty string if FID generation fails', () => {
123+
stub(crypto, 'getRandomValues').throws();
124+
125+
const fid = generateFid();
126+
expect(fid).to.equal('');
127+
});
120128
});

packages/installations/src/helpers/generate-fid.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,32 @@
1717

1818
import { bufferToBase64UrlSafe } from './buffer-to-base64-url-safe';
1919

20-
/** Generates a new FID using random values from Web Crypto API. */
20+
export const VALID_FID_PATTERN = /^[cdef][\w-]{21}$/;
21+
export const INVALID_FID = '';
22+
23+
/**
24+
* Generates a new FID using random values from Web Crypto API.
25+
* Returns an empty string if FID generation fails for any reason.
26+
*/
2127
export function generateFid(): string {
22-
// A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5
23-
// bytes. our implementation generates a 17 byte array instead.
24-
const fidByteArray = new Uint8Array(17);
25-
crypto.getRandomValues(fidByteArray);
28+
try {
29+
// A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5
30+
// bytes. our implementation generates a 17 byte array instead.
31+
const fidByteArray = new Uint8Array(17);
32+
const crypto =
33+
self.crypto || ((self as unknown) as { msCrypto: Crypto }).msCrypto;
34+
crypto.getRandomValues(fidByteArray);
35+
36+
// Replace the first 4 random bits with the constant FID header of 0b0111.
37+
fidByteArray[0] = 0b01110000 + (fidByteArray[0] % 0b00010000);
2638

27-
// Replace the first 4 random bits with the constant FID header of 0b0111.
28-
fidByteArray[0] = 0b01110000 + (fidByteArray[0] % 0b00010000);
39+
const fid = encode(fidByteArray);
2940

30-
return encode(fidByteArray);
41+
return VALID_FID_PATTERN.test(fid) ? fid : INVALID_FID;
42+
} catch {
43+
// FID generation errored
44+
return INVALID_FID;
45+
}
3146
}
3247

3348
/** Converts a FID Uint8Array to a base64 string representation. */

packages/installations/src/helpers/get-installation-entry.test.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { getFakeAppConfig } from '../testing/get-fake-app';
2929
import '../testing/setup';
3030
import { ERROR_FACTORY, ErrorCode } from '../util/errors';
3131
import { sleep } from '../util/sleep';
32-
import * as fidGenerator from './generate-fid';
32+
import * as generateFidModule from './generate-fid';
3333
import { getInstallationEntry } from './get-installation-entry';
3434
import { get, set } from './idb-manager';
3535

@@ -53,7 +53,8 @@ describe('getInstallationEntry', () => {
5353
async (_, installationEntry): Promise<RegisteredInstallationEntry> => {
5454
await sleep(100); // Request would take some time
5555
const registeredInstallationEntry: RegisteredInstallationEntry = {
56-
fid: installationEntry.fid,
56+
// Returns new FID if client FID is invalid.
57+
fid: installationEntry.fid || FID,
5758
registrationStatus: RequestStatus.COMPLETED,
5859
refreshToken: 'refreshToken',
5960
authToken: {
@@ -179,9 +180,10 @@ describe('getInstallationEntry', () => {
179180
let generateInstallationEntrySpy: SinonStub<[], string>;
180181

181182
beforeEach(() => {
182-
generateInstallationEntrySpy = stub(fidGenerator, 'generateFid').returns(
183-
FID
184-
);
183+
generateInstallationEntrySpy = stub(
184+
generateFidModule,
185+
'generateFid'
186+
).returns(FID);
185187
});
186188

187189
it('returns a new pending InstallationEntry and triggers createInstallation', async () => {
@@ -255,6 +257,25 @@ describe('getInstallationEntry', () => {
255257

256258
expect(createInstallationSpy).to.be.calledOnce;
257259
});
260+
261+
it('waits for the FID from the server if FID generation fails', async () => {
262+
clock.restore();
263+
// Needed to allow the createInstallation request to complete.
264+
clock = useFakeTimers({ shouldAdvanceTime: true });
265+
266+
// FID generation fails.
267+
generateInstallationEntrySpy.returns(generateFidModule.INVALID_FID);
268+
269+
const getInstallationEntryPromise = getInstallationEntry(appConfig);
270+
271+
const {
272+
installationEntry,
273+
registrationPromise
274+
} = await getInstallationEntryPromise;
275+
276+
expect(installationEntry.fid).to.equal(FID);
277+
expect(registrationPromise).to.be.undefined;
278+
});
258279
});
259280

260281
describe('when there is an unregistered InstallationEntry in the database', () => {

packages/installations/src/helpers/get-installation-entry.ts

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,18 @@ import { AppConfig } from '../interfaces/app-config';
2020
import {
2121
InProgressInstallationEntry,
2222
InstallationEntry,
23-
RequestStatus
23+
RequestStatus,
24+
RegisteredInstallationEntry
2425
} from '../interfaces/installation-entry';
2526
import { PENDING_TIMEOUT_MS } from '../util/constants';
2627
import { ERROR_FACTORY, ErrorCode, isServerError } from '../util/errors';
2728
import { sleep } from '../util/sleep';
28-
import { generateFid } from './generate-fid';
29+
import { generateFid, INVALID_FID } from './generate-fid';
2930
import { remove, set, update } from './idb-manager';
3031

3132
export interface InstallationEntryWithRegistrationPromise {
3233
installationEntry: InstallationEntry;
33-
registrationPromise?: Promise<void>;
34+
registrationPromise?: Promise<RegisteredInstallationEntry>;
3435
}
3536

3637
/**
@@ -40,26 +41,33 @@ export interface InstallationEntryWithRegistrationPromise {
4041
export async function getInstallationEntry(
4142
appConfig: AppConfig
4243
): Promise<InstallationEntryWithRegistrationPromise> {
43-
let registrationPromise: Promise<void> | undefined;
44+
let registrationPromise: Promise<RegisteredInstallationEntry> | undefined;
45+
46+
const installationEntry = await update(
47+
appConfig,
48+
(oldEntry?: InstallationEntry): InstallationEntry => {
49+
const installationEntry = updateOrCreateInstallationEntry(oldEntry);
50+
const entryWithPromise = triggerRegistrationIfNecessary(
51+
appConfig,
52+
installationEntry
53+
);
54+
registrationPromise = entryWithPromise.registrationPromise;
55+
return entryWithPromise.installationEntry;
56+
}
57+
);
58+
59+
if (installationEntry.fid === INVALID_FID) {
60+
// FID generation failed. Waiting for the FID from the server.
61+
return { installationEntry: await registrationPromise! };
62+
}
4463

4564
return {
46-
installationEntry: await update(
47-
appConfig,
48-
(oldEntry?: InstallationEntry): InstallationEntry => {
49-
const installationEntry = updateOrCreateFid(oldEntry);
50-
const entryWithPromise = triggerRegistrationIfNecessary(
51-
appConfig,
52-
installationEntry
53-
);
54-
registrationPromise = entryWithPromise.registrationPromise;
55-
return entryWithPromise.installationEntry;
56-
}
57-
),
65+
installationEntry,
5866
registrationPromise
5967
};
6068
}
6169

62-
function updateOrCreateFid(
70+
function updateOrCreateInstallationEntry(
6371
oldEntry: InstallationEntry | undefined
6472
): InstallationEntry {
6573
const entry: InstallationEntry = oldEntry || {
@@ -124,13 +132,13 @@ function triggerRegistrationIfNecessary(
124132
async function registerInstallation(
125133
appConfig: AppConfig,
126134
installationEntry: InProgressInstallationEntry
127-
): Promise<void> {
135+
): Promise<RegisteredInstallationEntry> {
128136
try {
129137
const registeredInstallationEntry = await createInstallation(
130138
appConfig,
131139
installationEntry
132140
);
133-
await set(appConfig, registeredInstallationEntry);
141+
return set(appConfig, registeredInstallationEntry);
134142
} catch (e) {
135143
if (isServerError(e) && e.serverCode === 409) {
136144
// Server returned a "FID can not be used" error.
@@ -148,7 +156,9 @@ async function registerInstallation(
148156
}
149157

150158
/** Call if FID registration is pending. */
151-
async function waitUntilFidRegistration(appConfig: AppConfig): Promise<void> {
159+
async function waitUntilFidRegistration(
160+
appConfig: AppConfig
161+
): Promise<RegisteredInstallationEntry> {
152162
// Unfortunately, there is no way of reliably observing when a value in
153163
// IndexedDB changes (yet, see https://github.com/WICG/indexed-db-observers),
154164
// so we need to poll.
@@ -164,6 +174,8 @@ async function waitUntilFidRegistration(appConfig: AppConfig): Promise<void> {
164174
if (entry.registrationStatus === RequestStatus.NOT_STARTED) {
165175
throw ERROR_FACTORY.create(ErrorCode.CREATE_INSTALLATION_FAILED);
166176
}
177+
178+
return entry;
167179
}
168180

169181
/**

packages/installations/src/interfaces/api-response.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
export interface CreateInstallationResponse {
1919
readonly refreshToken: string;
2020
readonly authToken: GenerateAuthTokenResponse;
21+
readonly fid: string;
2122
}
2223

2324
export interface GenerateAuthTokenResponse {

packages/performance/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
},
2828
"dependencies": {
2929
"@firebase/logger": "0.1.17",
30-
"@firebase/installations": "0.1.7",
30+
"@firebase/installations": "0.2.0",
3131
"@firebase/util": "0.2.20",
3232
"@firebase/performance-types": "0.0.2",
3333
"tslib": "1.9.3"

0 commit comments

Comments
 (0)