Skip to content

Commit 6df26bb

Browse files
authored
[ServerApp] RefCount FirebaseServerApps (#8000)
Update the `FirebaseServerApp` creation to return the same object if an existing object exists with the same configuration. However, the `deleteOnDeref` field is ignored when detecting duplicate apps, since that object reference could vary across multiple SSR rendering passes. The hope is that a `FirebaseServerApp` instance awaiting deletion from a the `deleteOnDeref` feature maybe be reused if another SSR pass occurs in rapid succession, there by speeding up the SSR code.
1 parent abc9d87 commit 6df26bb

File tree

3 files changed

+134
-36
lines changed

3 files changed

+134
-36
lines changed

packages/app/src/api.test.ts

+68-6
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ describe('API tests', () => {
213213
expect(app).to.not.equal(null);
214214
expect(app.automaticDataCollectionEnabled).to.be.false;
215215
await deleteApp(app);
216+
expect((app as FirebaseServerAppImpl).isDeleted).to.be.true;
216217
});
217218

218219
it('creates FirebaseServerApp with automaticDataCollectionEnabled', async () => {
@@ -233,6 +234,7 @@ describe('API tests', () => {
233234
expect(app).to.not.equal(null);
234235
expect(app.automaticDataCollectionEnabled).to.be.true;
235236
await deleteApp(app);
237+
expect((app as FirebaseServerAppImpl).isDeleted).to.be.true;
236238
});
237239

238240
it('creates FirebaseServerApp with releaseOnDeref', async () => {
@@ -251,6 +253,7 @@ describe('API tests', () => {
251253
expect(app).to.not.equal(null);
252254
expect(app.automaticDataCollectionEnabled).to.be.false;
253255
await deleteApp(app);
256+
expect((app as FirebaseServerAppImpl).isDeleted).to.be.true;
254257
});
255258

256259
it('creates FirebaseServerApp with FirebaseApp', async () => {
@@ -274,6 +277,7 @@ describe('API tests', () => {
274277
expect(app).to.not.equal(null);
275278
expect(app.options.apiKey).to.equal('test1');
276279
await deleteApp(app);
280+
expect((app as FirebaseServerAppImpl).isDeleted).to.be.true;
277281
});
278282
});
279283

@@ -285,8 +289,7 @@ describe('API tests', () => {
285289

286290
const options = { apiKey: 'APIKEY' };
287291
const serverAppSettingsOne: FirebaseServerAppSettings = {
288-
automaticDataCollectionEnabled: false,
289-
releaseOnDeref: options
292+
automaticDataCollectionEnabled: true
290293
};
291294

292295
const serverAppSettingsTwo: FirebaseServerAppSettings = {
@@ -295,12 +298,42 @@ describe('API tests', () => {
295298

296299
const appOne = initializeServerApp(options, serverAppSettingsOne);
297300
expect(appOne).to.not.equal(null);
298-
expect(appOne.automaticDataCollectionEnabled).to.be.false;
301+
expect(appOne.automaticDataCollectionEnabled).to.be.true;
299302
const appTwo = initializeServerApp(options, serverAppSettingsTwo);
300303
expect(appTwo).to.not.equal(null);
304+
expect(appTwo.automaticDataCollectionEnabled).to.be.false;
301305
expect(appTwo).to.not.equal(appOne);
302306
await deleteApp(appOne);
303307
await deleteApp(appTwo);
308+
expect((appOne as FirebaseServerAppImpl).isDeleted).to.be.true;
309+
expect((appTwo as FirebaseServerAppImpl).isDeleted).to.be.true;
310+
});
311+
312+
it('create FirebaseServerApps with varying deleteOnDeref, and they still return same object ', async () => {
313+
if (isBrowser()) {
314+
// FirebaseServerApp isn't supported for execution in browser enviornments.
315+
return;
316+
}
317+
318+
const options = { apiKey: 'APIKEY' };
319+
const serverAppSettingsOne: FirebaseServerAppSettings = {
320+
automaticDataCollectionEnabled: false
321+
};
322+
323+
const serverAppSettingsTwo: FirebaseServerAppSettings = {
324+
automaticDataCollectionEnabled: false,
325+
releaseOnDeref: options
326+
};
327+
328+
const appOne = initializeServerApp(options, serverAppSettingsOne);
329+
expect(appOne).to.not.equal(null);
330+
expect(appOne.automaticDataCollectionEnabled).to.be.false;
331+
const appTwo = initializeServerApp(options, serverAppSettingsTwo);
332+
expect(appTwo).to.not.equal(null);
333+
expect(appTwo.automaticDataCollectionEnabled).to.be.false;
334+
expect(appTwo).to.equal(appOne);
335+
await deleteApp(appOne);
336+
await deleteApp(appTwo);
304337
});
305338

306339
it('create duplicate FirebaseServerApps returns the same object', async () => {
@@ -322,11 +355,40 @@ describe('API tests', () => {
322355
expect(appTwo).to.not.equal(null);
323356
expect(appTwo).to.equal(appOne);
324357
await deleteApp(appOne);
358+
await deleteApp(appTwo);
359+
});
325360

326-
// TODO: When Reference Counting works, update test. The following line should be false
327-
// until and the app should be deleted a second time.
361+
it('deleting FirebaseServerApps is ref counted', async () => {
362+
if (isBrowser()) {
363+
// FirebaseServerApp isn't supported for execution in browser enviornments.
364+
return;
365+
}
366+
367+
const options = { apiKey: 'APIKEY' };
368+
const serverAppSettings: FirebaseServerAppSettings = {
369+
automaticDataCollectionEnabled: false,
370+
releaseOnDeref: options
371+
};
372+
373+
const appOne = initializeServerApp(options, serverAppSettings);
374+
expect((appOne as FirebaseServerAppImpl).refCount).to.equal(1);
375+
376+
const appTwo = initializeServerApp(options, serverAppSettings);
377+
expect(appTwo).to.equal(appOne);
378+
expect((appOne as FirebaseServerAppImpl).refCount).to.equal(2);
379+
expect((appTwo as FirebaseServerAppImpl).refCount).to.equal(2);
380+
381+
await deleteApp(appOne);
382+
expect((appOne as FirebaseServerAppImpl).refCount).to.equal(1);
383+
expect((appTwo as FirebaseServerAppImpl).refCount).to.equal(1);
384+
expect((appOne as FirebaseServerAppImpl).isDeleted).to.be.false;
385+
expect((appTwo as FirebaseServerAppImpl).isDeleted).to.be.false;
386+
387+
await deleteApp(appTwo);
388+
expect((appOne as FirebaseServerAppImpl).refCount).to.equal(0);
389+
expect((appTwo as FirebaseServerAppImpl).refCount).to.equal(0);
328390
expect((appOne as FirebaseServerAppImpl).isDeleted).to.be.true;
329-
// await deleteApp(appTwo);
391+
expect((appTwo as FirebaseServerAppImpl).isDeleted).to.be.true;
330392
});
331393

332394
describe('getApp', () => {

packages/app/src/api.ts

+27-18
Original file line numberDiff line numberDiff line change
@@ -235,32 +235,37 @@ export function initializeServerApp(
235235
throw ERROR_FACTORY.create(AppError.INVALID_SERVER_APP_ENVIRONMENT);
236236
}
237237

238+
if (_serverAppConfig.automaticDataCollectionEnabled === undefined) {
239+
_serverAppConfig.automaticDataCollectionEnabled = false;
240+
}
241+
238242
let appOptions: FirebaseOptions;
239243
if (_isFirebaseApp(_options)) {
240244
appOptions = _options.options;
241245
} else {
242246
appOptions = _options;
243247
}
244248

245-
// Mangle the ap name based on a hash of the FirebaseServerAppSettings, and FirebaseOptions
246-
// objects and the authIdToken, if provided.
249+
// Build an app name based on a hash of the configuration options.
247250
const nameObj = {
248-
_serverAppConfig,
251+
..._serverAppConfig,
249252
...appOptions
250253
};
254+
255+
// However, Do not mangle the name based on releaseOnDeref, since it will vary between the
256+
// construction of FirebaseServerApp instances. For example, if the object is the request headers.
257+
if (nameObj.releaseOnDeref !== undefined) {
258+
delete nameObj.releaseOnDeref;
259+
}
260+
251261
const hashCode = (s: string): number => {
252262
return [...s].reduce(
253263
(hash, c) => (Math.imul(31, hash) + c.charCodeAt(0)) | 0,
254264
0
255265
);
256266
};
257267

258-
const serverAppSettings: FirebaseServerAppSettings = {
259-
automaticDataCollectionEnabled: false,
260-
..._serverAppConfig
261-
};
262-
263-
if (serverAppSettings.releaseOnDeref !== undefined) {
268+
if (_serverAppConfig.releaseOnDeref !== undefined) {
264269
if (typeof FinalizationRegistry === 'undefined') {
265270
throw ERROR_FACTORY.create(
266271
AppError.FINALIZATION_REGISTRY_NOT_SUPPORTED,
@@ -272,9 +277,9 @@ export function initializeServerApp(
272277
const nameString = '' + hashCode(JSON.stringify(nameObj));
273278
const existingApp = _serverApps.get(nameString) as FirebaseServerApp;
274279
if (existingApp) {
275-
// TODO:
276-
// 1: Register a new reference to finalization registry.
277-
// 2: Incrememnt reference count.
280+
(existingApp as FirebaseServerAppImpl).incRefCount(
281+
_serverAppConfig.releaseOnDeref
282+
);
278283
return existingApp;
279284
}
280285

@@ -285,7 +290,7 @@ export function initializeServerApp(
285290

286291
const newApp = new FirebaseServerAppImpl(
287292
appOptions,
288-
serverAppSettings,
293+
_serverAppConfig,
289294
nameString,
290295
container
291296
);
@@ -362,16 +367,20 @@ export function getApps(): FirebaseApp[] {
362367
* @public
363368
*/
364369
export async function deleteApp(app: FirebaseApp): Promise<void> {
365-
let foundApp = false;
370+
let cleanupProviders = false;
366371
const name = app.name;
367372
if (_apps.has(name)) {
368-
foundApp = true;
373+
cleanupProviders = true;
369374
_apps.delete(name);
370375
} else if (_serverApps.has(name)) {
371-
foundApp = true;
372-
_serverApps.delete(name);
376+
const firebaseServerApp = app as FirebaseServerAppImpl;
377+
if (firebaseServerApp.decRefCount() <= 0) {
378+
_serverApps.delete(name);
379+
cleanupProviders = true;
380+
}
373381
}
374-
if (foundApp) {
382+
383+
if (cleanupProviders) {
375384
await Promise.all(
376385
(app as FirebaseAppImpl).container
377386
.getProviders()

packages/app/src/firebaseServerApp.ts

+39-12
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export class FirebaseServerAppImpl
3232
{
3333
private readonly _serverConfig: FirebaseServerAppSettings;
3434
private _finalizationRegistry: FinalizationRegistry<object>;
35+
private _refCount: number;
3536

3637
constructor(
3738
options: FirebaseOptions | FirebaseAppImpl,
@@ -65,22 +66,48 @@ export class FirebaseServerAppImpl
6566
...serverConfig
6667
};
6768

68-
this._finalizationRegistry = new FinalizationRegistry(
69-
this.automaticCleanup
70-
);
69+
this._finalizationRegistry = new FinalizationRegistry(() => {
70+
this.automaticCleanup();
71+
});
7172

72-
if (this._serverConfig.releaseOnDeref !== undefined) {
73-
this._finalizationRegistry.register(
74-
this._serverConfig.releaseOnDeref,
75-
this
76-
);
77-
this._serverConfig.releaseOnDeref = undefined; // Don't keep a strong reference to the object.
73+
this._refCount = 0;
74+
this.incRefCount(this._serverConfig.releaseOnDeref);
75+
76+
// Do not retain a hard reference to the dref object, otherwise the FinalizationRegisry
77+
// will never trigger.
78+
this._serverConfig.releaseOnDeref = undefined;
79+
serverConfig.releaseOnDeref = undefined;
80+
}
81+
82+
get refCount(): number {
83+
return this._refCount;
84+
}
85+
86+
// Increment the reference count of this server app. If an object is provided, register it
87+
// with the finalization registry.
88+
incRefCount(obj: object | undefined): void {
89+
if (this.isDeleted) {
90+
return;
91+
}
92+
this._refCount++;
93+
if (obj !== undefined) {
94+
this._finalizationRegistry.register(obj, this);
95+
}
96+
}
97+
98+
// Decrement the reference count.
99+
decRefCount(): number {
100+
if (this.isDeleted) {
101+
return 0;
78102
}
103+
return --this._refCount;
79104
}
80105

81-
private automaticCleanup(serverApp: FirebaseServerAppImpl): void {
82-
// TODO: implement reference counting.
83-
void deleteApp(serverApp);
106+
// Invoked by the FinalizationRegistry callback to note that this app should go through its
107+
// reference counts and delete itself if no reference count remain. The coordinating logic that
108+
// handles this is in deleteApp(...).
109+
private automaticCleanup(): void {
110+
void deleteApp(this);
84111
}
85112

86113
get settings(): FirebaseServerAppSettings {

0 commit comments

Comments
 (0)