Skip to content

Commit 08e55ab

Browse files
committed
Add conditional delays to auth-next (#2934)
* Add conditional delays to auth-next * [AUTOMATED]: Prettier Code Styling * Use typescript asserts keyword for typesafe assertions * [AUTOMATED]: Prettier Code Styling * Rebase conflicts & PR feedback * More PR Feedback * Strip debug asserts from prod builds * [AUTOMATED]: Prettier Code Styling * [AUTOMATED]: API Reports * Fix logic in assertion * Revert merge artifacts * PR Feedback
1 parent 87ae70b commit 08e55ab

File tree

13 files changed

+344
-39
lines changed

13 files changed

+344
-39
lines changed

packages-exp/auth-exp/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@
2929
"@firebase/app-types-exp": "0.x"
3030
},
3131
"dependencies": {
32+
"@firebase/logger": "^0.2.2",
3233
"@firebase/util": "^0.2.44",
3334
"tslib": "1.11.1"
3435
},
3536
"license": "Apache-2.0",
3637
"devDependencies": {
38+
"@rollup/plugin-strip": "^1.3.2",
3739
"rollup": "1.32.1",
3840
"rollup-plugin-json": "4.0.0",
3941
"rollup-plugin-replace": "2.2.0",

packages-exp/auth-exp/rollup.config.js

+12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18+
import strip from '@rollup/plugin-strip';
1819
import typescriptPlugin from 'rollup-plugin-typescript2';
1920
import typescript from 'typescript';
2021
import pkg from './package.json';
@@ -23,10 +24,20 @@ const deps = Object.keys(
2324
Object.assign({}, pkg.peerDependencies, pkg.dependencies)
2425
);
2526

27+
/**
28+
* Common plugins for all builds
29+
*/
30+
const commonPlugins = [
31+
strip({
32+
functions: ['debugAssert.*']
33+
})
34+
];
35+
2636
/**
2737
* ES5 Builds
2838
*/
2939
const es5BuildPlugins = [
40+
...commonPlugins,
3041
typescriptPlugin({
3142
typescript
3243
})
@@ -57,6 +68,7 @@ const es5Builds = [
5768
* ES2017 Builds
5869
*/
5970
const es2017BuildPlugins = [
71+
...commonPlugins,
6072
typescriptPlugin({
6173
typescript,
6274
tsconfigOverride: {

packages-exp/auth-exp/src/api/index.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ describe('performApiRequest', () => {
175175
Endpoint.SIGN_UP,
176176
request
177177
);
178-
clock.tick(DEFAULT_API_TIMEOUT_MS + 1);
178+
clock.tick(DEFAULT_API_TIMEOUT_MS.get() + 1);
179179
await expect(promise).to.be.rejectedWith(
180180
FirebaseError,
181181
'Firebase: The operation has timed out. (auth/timeout).'

packages-exp/auth-exp/src/api/index.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
ServerErrorMap,
2626
SERVER_ERROR_MAP
2727
} from './errors';
28+
import { Delay } from '../core/util/delay';
2829

2930
export enum HttpMethod {
3031
POST = 'POST',
@@ -53,7 +54,7 @@ export enum Endpoint {
5354
WITHDRAW_MFA = '/v2/accounts/mfaEnrollment:withdraw'
5455
}
5556

56-
export const DEFAULT_API_TIMEOUT_MS = 30_000;
57+
export const DEFAULT_API_TIMEOUT_MS = new Delay(30_000, 60_000);
5758

5859
export async function performApiRequest<T, V>(
5960
auth: Auth,
@@ -101,7 +102,7 @@ export async function performApiRequest<T, V>(
101102
appName: auth.name
102103
})
103104
);
104-
}, DEFAULT_API_TIMEOUT_MS)
105+
}, DEFAULT_API_TIMEOUT_MS.get())
105106
)
106107
]);
107108
if (response.ok) {

packages-exp/auth-exp/src/core/auth/auth_impl.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ class AuthImpl implements Auth {
9797
}
9898

9999
private get assertedPersistence(): PersistenceUserManager {
100-
return assert(this.persistenceManager, this.name);
100+
assert(this.persistenceManager, this.name);
101+
return this.persistenceManager;
101102
}
102103
}
103104

@@ -110,8 +111,9 @@ export function initializeAuth(
110111
const { apiKey, authDomain } = app.options;
111112

112113
// TODO: platform needs to be determined using heuristics
114+
assert(apiKey, app.name, AuthErrorCode.INVALID_API_KEY);
113115
const config: Config = {
114-
apiKey: assert(apiKey, app.name, AuthErrorCode.INVALID_API_KEY),
116+
apiKey,
115117
authDomain,
116118
apiHost: DEFAULT_API_HOST,
117119
apiScheme: DEFAULT_API_SCHEME,

packages-exp/auth-exp/src/core/user/token_manager.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import { IdTokenResponse } from '../../model/id_token';
1919
import { PersistedBlob } from '../persistence';
20-
import { assertType } from '../util/assert';
20+
import { assert } from '../util/assert';
2121

2222
/**
2323
* The number of milliseconds before the official expiration time of a token
@@ -79,13 +79,16 @@ export class StsTokenManager {
7979

8080
const manager = new StsTokenManager();
8181
if (refreshToken) {
82-
manager.refreshToken = assertType(refreshToken, 'string', appName);
82+
assert(typeof refreshToken === 'string', appName);
83+
manager.refreshToken = refreshToken;
8384
}
8485
if (accessToken) {
85-
manager.accessToken = assertType(accessToken, 'string', appName);
86+
assert(typeof accessToken === 'string', appName);
87+
manager.accessToken = accessToken;
8688
}
8789
if (expirationTime) {
88-
manager.expirationTime = assertType(expirationTime, 'number', appName);
90+
assert(typeof expirationTime === 'number', appName);
91+
manager.expirationTime = expirationTime;
8992
}
9093
return manager;
9194
}

packages-exp/auth-exp/src/core/user/user_impl.ts

+21-8
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { IdTokenResult } from '../../model/id_token';
2020
import { User } from '../../model/user';
2121
import { PersistedBlob } from '../persistence';
2222
import { ProviderId } from '../providers';
23-
import { assert, assertType } from '../util/assert';
23+
import { assert } from '../util/assert';
2424
import { StsTokenManager } from './token_manager';
2525

2626
export interface UserParameters {
@@ -34,6 +34,16 @@ export interface UserParameters {
3434
photoURL?: string;
3535
}
3636

37+
function assertStringOrUndefined(
38+
assertion: unknown,
39+
appName: string
40+
): asserts assertion is string | undefined {
41+
assert(
42+
typeof assertion === 'string' || typeof assertion === 'undefined',
43+
appName
44+
);
45+
}
46+
3747
export class UserImpl implements User {
3848
// For the user object, provider is always Firebase.
3949
readonly providerId = ProviderId.FIREBASE;
@@ -111,16 +121,19 @@ export class UserImpl implements User {
111121
plainObjectTokenManager as PersistedBlob
112122
);
113123

114-
const stringOrUndef = ['string', 'undefined'];
115-
124+
assert(typeof uid === 'string', auth.name);
125+
assertStringOrUndefined(displayName, auth.name);
126+
assertStringOrUndefined(email, auth.name);
127+
assertStringOrUndefined(phoneNumber, auth.name);
128+
assertStringOrUndefined(photoURL, auth.name);
116129
return new UserImpl({
117-
uid: assertType(uid, 'string', auth.name),
130+
uid,
118131
auth,
119132
stsTokenManager,
120-
displayName: assertType(displayName, stringOrUndef, auth.name),
121-
email: assertType(email, stringOrUndef, auth.name),
122-
phoneNumber: assertType(phoneNumber, stringOrUndef, auth.name),
123-
photoURL: assertType(photoURL, stringOrUndef, auth.name)
133+
displayName,
134+
email,
135+
phoneNumber,
136+
photoURL
124137
});
125138
}
126139
}

packages-exp/auth-exp/src/core/util/assert.ts

+54-18
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,65 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
18+
import { AuthErrorCode, AUTH_ERROR_FACTORY } from '../errors';
19+
import { logError } from './log';
1920

20-
export function assert<T>(
21-
expression: T | null | undefined,
21+
/**
22+
* Unconditionally fails, throwing a developer facing INTERNAL_ERROR
23+
*
24+
* @param appName App name for tagging the error
25+
* @throws FirebaseError
26+
*/
27+
export function fail(appName: string, errorCode: AuthErrorCode): never {
28+
throw AUTH_ERROR_FACTORY.create(errorCode, { appName });
29+
}
30+
31+
/**
32+
* Verifies the given condition and fails if false, throwing a developer facing error
33+
*
34+
* @param assertion
35+
* @param appName
36+
*/
37+
export function assert(
38+
assertion: unknown,
2239
appName: string,
23-
code = AuthErrorCode.INTERNAL_ERROR
24-
): T {
25-
if (!expression) {
26-
throw AUTH_ERROR_FACTORY.create(code, { appName });
40+
errorCode = AuthErrorCode.INTERNAL_ERROR
41+
): asserts assertion {
42+
if (!assertion) {
43+
fail(appName, errorCode);
2744
}
45+
}
2846

29-
return expression;
47+
/**
48+
* Unconditionally fails, throwing an internal error with the given message.
49+
*
50+
* @param failure type of failure encountered
51+
* @throws Error
52+
*/
53+
export function debugFail(failure: string): never {
54+
// Log the failure in addition to throw an exception, just in case the
55+
// exception is swallowed.
56+
const message = `INTERNAL ASSERTION FAILED: ` + failure;
57+
logError(message);
58+
59+
// NOTE: We don't use FirebaseError here because these are internal failures
60+
// that cannot be handled by the user. (Also it would create a circular
61+
// dependency between the error and assert modules which doesn't work.)
62+
throw new Error(message);
3063
}
3164

32-
export function assertType<T>(
33-
expression: unknown,
34-
expected: string | string[],
35-
appName: string
36-
): T {
37-
if (typeof expected === 'string') {
38-
expected = [expected];
65+
/**
66+
* Fails if the given assertion condition is false, throwing an Error with the
67+
* given message if it did.
68+
*
69+
* @param assertion
70+
* @param message
71+
*/
72+
export function debugAssert(
73+
assertion: unknown,
74+
message: string
75+
): asserts assertion {
76+
if (!assertion) {
77+
debugFail(message);
3978
}
40-
41-
assert(expected.includes(typeof expression), appName);
42-
return expression as T;
4379
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @license
3+
* Copyright 2020 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 * as util from '@firebase/util';
19+
import { expect } from 'chai';
20+
import { restore, stub } from 'sinon';
21+
import { Delay, _OFFLINE_DELAY_MS } from './delay';
22+
import * as navigator from './navigator';
23+
24+
describe('Delay.get()', () => {
25+
const SHORT_DELAY = 30_000;
26+
const LONG_DELAY = 60_000;
27+
28+
afterEach(restore);
29+
30+
it('should return the short delay in browser environments', () => {
31+
const delay = new Delay(SHORT_DELAY, LONG_DELAY);
32+
expect(delay.get()).to.eq(SHORT_DELAY);
33+
});
34+
35+
it('should return the long delay in Cordova environments', () => {
36+
const mock = stub(util, 'isMobileCordova');
37+
mock.callsFake(() => true);
38+
const delay = new Delay(SHORT_DELAY, LONG_DELAY);
39+
expect(delay.get()).to.eq(LONG_DELAY);
40+
});
41+
42+
it('should return the long delay in React Native environments', () => {
43+
const mock = stub(util, 'isReactNative');
44+
mock.callsFake(() => true);
45+
const delay = new Delay(SHORT_DELAY, LONG_DELAY);
46+
expect(delay.get()).to.eq(LONG_DELAY);
47+
});
48+
49+
it('should return quicker when offline', () => {
50+
const mock = stub(navigator, 'isOnline');
51+
mock.callsFake(() => false);
52+
const delay = new Delay(SHORT_DELAY, LONG_DELAY);
53+
expect(delay.get()).to.eq(_OFFLINE_DELAY_MS);
54+
});
55+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* @license
3+
* Copyright 2020 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 { isMobileCordova, isReactNative } from '@firebase/util';
19+
import { isOnline } from './navigator';
20+
import { debugAssert } from './assert';
21+
22+
export const _OFFLINE_DELAY_MS = 5000;
23+
24+
/**
25+
* A structure to help pick between a range of long and short delay durations
26+
* depending on the current environment. In general, the long delay is used for
27+
* mobile environments whereas short delays are used for desktop environments.
28+
*/
29+
export class Delay {
30+
// The default value for the offline delay timeout in ms.
31+
32+
private readonly isMobile: boolean;
33+
constructor(
34+
private readonly shortDelay: number,
35+
private readonly longDelay: number
36+
) {
37+
// Internal error when improperly initialized.
38+
debugAssert(
39+
longDelay > shortDelay,
40+
'Short delay should be less than long delay!'
41+
);
42+
this.isMobile = isMobileCordova() || isReactNative();
43+
}
44+
45+
get(): number {
46+
if (!isOnline()) {
47+
// Pick the shorter timeout.
48+
return Math.min(_OFFLINE_DELAY_MS, this.shortDelay);
49+
}
50+
// If running in a mobile environment, return the long delay, otherwise
51+
// return the short delay.
52+
// This could be improved in the future to dynamically change based on other
53+
// variables instead of just reading the current environment.
54+
return this.isMobile ? this.longDelay : this.shortDelay;
55+
}
56+
}

0 commit comments

Comments
 (0)