Skip to content

Commit b8462f2

Browse files
Don't double-block AsyncQueue on auth token retrieval (#5434)
1 parent deda8cd commit b8462f2

File tree

4 files changed

+176
-16
lines changed

4 files changed

+176
-16
lines changed

.changeset/cyan-eagles-carry.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@firebase/firestore": patch
3+
---
4+
5+
Fixes a deadlock during asynchronous initialization of both Firestore and Auth.

packages/firestore/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"devDependencies": {
8484
"@firebase/app": "0.7.0",
8585
"@firebase/app-compat": "0.1.1",
86+
"@firebase/auth": "0.17.1",
8687
"@rollup/plugin-alias": "3.1.2",
8788
"@rollup/plugin-json": "4.1.0",
8889
"@types/eslint": "7.2.10",

packages/firestore/src/api/credentials.ts

+13-16
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ export interface CredentialsProvider {
108108
shutdown(): void;
109109
}
110110

111-
/**
112-
* A CredentialsProvider that always yields an empty token.
111+
/**
112+
* A CredentialsProvider that always yields an empty token.
113113
* @internal
114114
*/
115115
export class EmptyCredentialsProvider implements CredentialsProvider {
@@ -261,18 +261,21 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
261261
);
262262
};
263263

264-
const registerAuth = (auth: FirebaseAuthInternal): void => {
264+
const awaitNextToken: () => void = () => {
265+
const currentTokenAttempt = nextToken;
265266
asyncQueue.enqueueRetryable(async () => {
266-
logDebug('FirebaseCredentialsProvider', 'Auth detected');
267-
this.auth = auth;
268-
this.auth.addAuthTokenListener(this.tokenListener);
269-
270-
// Call the change listener inline to block on the user change.
271-
await nextToken.promise;
267+
await currentTokenAttempt.promise;
272268
await guardedChangeListener(this.currentUser);
273269
});
274270
};
275271

272+
const registerAuth = (auth: FirebaseAuthInternal): void => {
273+
logDebug('FirebaseCredentialsProvider', 'Auth detected');
274+
this.auth = auth;
275+
this.auth.addAuthTokenListener(this.tokenListener);
276+
awaitNextToken();
277+
};
278+
276279
this.authProvider.onInit(auth => registerAuth(auth));
277280

278281
// Our users can initialize Auth right after Firestore, so we give it
@@ -292,13 +295,7 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
292295
}
293296
}, 0);
294297

295-
asyncQueue.enqueueRetryable(async () => {
296-
// If we have not received a token, wait for the first one.
297-
if (this.tokenCounter === 0) {
298-
await nextToken.promise;
299-
await guardedChangeListener(this.currentUser);
300-
}
301-
});
298+
awaitNextToken();
302299
}
303300

304301
getToken(): Promise<Token | null> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* @license
3+
* Copyright 2021 Google LLC
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 { FirebaseApp, initializeApp } from '@firebase/app';
19+
import { getAuth, signInAnonymously } from '@firebase/auth';
20+
import { expect } from 'chai';
21+
22+
import {
23+
getDoc,
24+
getFirestore,
25+
doc,
26+
enableIndexedDbPersistence,
27+
disableNetwork,
28+
setDoc
29+
} from '../../../src';
30+
31+
// eslint-disable-next-line @typescript-eslint/no-require-imports
32+
const firebaseConfig = require('../../../../../config/project.json');
33+
34+
let appCount = 0;
35+
36+
function getNextApp(): FirebaseApp {
37+
const name = 'initialization-test-app-' + appCount++;
38+
return initializeApp(firebaseConfig, name);
39+
}
40+
41+
describe('Initialization', () => {
42+
let app: FirebaseApp;
43+
44+
beforeEach(() => {
45+
app = getNextApp();
46+
});
47+
48+
it('getAuth() before getFirestore()', () => {
49+
getAuth(app);
50+
const firestore = getFirestore(app);
51+
const testDoc = doc(firestore, 'coll/doc');
52+
return getDoc(testDoc);
53+
});
54+
55+
it('getFirestore() before getAuth()', () => {
56+
const firestore = getFirestore(app);
57+
getAuth(app);
58+
const testDoc = doc(firestore, 'coll/doc');
59+
return getDoc(testDoc);
60+
});
61+
62+
it('lazy-loaded getAuth()', () => {
63+
const firestore = getFirestore(app);
64+
void Promise.resolve(() => getAuth(app));
65+
const testDoc = doc(firestore, 'coll/doc');
66+
return getDoc(testDoc);
67+
});
68+
69+
it('getDoc() before getAuth()', () => {
70+
const firestore = getFirestore(app);
71+
const testDoc = doc(firestore, 'coll/doc');
72+
const promise = getDoc(testDoc);
73+
getAuth(app);
74+
return promise;
75+
});
76+
77+
it('uses user from getAuth()', async () => {
78+
const firestore = getFirestore(app);
79+
const testDoc = doc(firestore, 'coll/doc');
80+
void disableNetwork(firestore); // Go offline to enforce latency-compensation
81+
void setDoc(testDoc, { foo: 'bar' });
82+
83+
const auth = getAuth(app);
84+
void signInAnonymously(auth);
85+
86+
// setDoc() did not actually persist the document until the sign in attempt.
87+
// Hence there was no user change and we can read the same document back.
88+
const cachedDoc = await getDoc(testDoc);
89+
expect(cachedDoc.exists()).to.be.true;
90+
});
91+
92+
it('uses user from asynchronously loaded getAuth()', async () => {
93+
const firestore = getFirestore(app);
94+
const testDoc = doc(firestore, 'coll/doc');
95+
void disableNetwork(firestore); // Go offline to enforce latency-compensation
96+
void setDoc(testDoc, { foo: 'bar' });
97+
98+
void Promise.resolve().then(() => {
99+
const auth = getAuth(app);
100+
return signInAnonymously(auth);
101+
});
102+
103+
// setDoc() did not actually persist the document until the sign in attempt.
104+
// Hence there was no user change and we can read the same document back.
105+
const cachedDoc = await getDoc(testDoc);
106+
expect(cachedDoc.exists()).to.be.true;
107+
});
108+
109+
it('uses user from getAuth()', async () => {
110+
const firestore = getFirestore(app);
111+
const testDoc = doc(firestore, 'coll/doc');
112+
void disableNetwork(firestore); // Go offline to enforce latency-compensation
113+
void setDoc(testDoc, { foo: 'bar' });
114+
115+
// Wait for the document. This waits for client initialization.
116+
const cachedDoc = await getDoc(testDoc);
117+
expect(cachedDoc.exists()).to.be.true;
118+
119+
const auth = getAuth(app);
120+
void signInAnonymously(auth);
121+
122+
try {
123+
await getDoc(testDoc);
124+
expect.fail('Document should not have been found after user change');
125+
} catch (e) {}
126+
});
127+
128+
// eslint-disable-next-line no-restricted-properties
129+
(typeof indexedDB !== 'undefined' ? describe : describe.skip)(
130+
'with IndexedDB',
131+
() => {
132+
it('getAuth() before explicitly initializing Firestore', () => {
133+
getAuth(app);
134+
const firestore = getFirestore(app);
135+
void enableIndexedDbPersistence(firestore);
136+
const testDoc = doc(firestore, 'coll/doc');
137+
return getDoc(testDoc);
138+
});
139+
140+
it('explicitly initialize Firestore before getAuth()', () => {
141+
const firestore = getFirestore(app);
142+
void enableIndexedDbPersistence(firestore);
143+
getAuth(app);
144+
const testDoc = doc(firestore, 'coll/doc');
145+
return getDoc(testDoc);
146+
});
147+
148+
it('getFirestore() followed by getAuth() followed by explicitly initialization', () => {
149+
const firestore = getFirestore(app);
150+
getAuth(app);
151+
void enableIndexedDbPersistence(firestore);
152+
const testDoc = doc(firestore, 'coll/doc');
153+
return getDoc(testDoc);
154+
});
155+
}
156+
);
157+
});

0 commit comments

Comments
 (0)