Skip to content

Commit b3d830c

Browse files
committed
fix leak of STAT_EVENT subscription in WebChannelConnection
1 parent bbdd95e commit b3d830c

File tree

1 file changed

+47
-36
lines changed

1 file changed

+47
-36
lines changed

packages/firestore/src/platform/browser/webchannel_connection.ts

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,28 @@ const RPC_STREAM_SERVICE = 'google.firestore.v1.Firestore';
5454

5555
const XHR_TIMEOUT_SECS = 15;
5656

57+
// Closure events are guarded and exceptions are swallowed, so catch any
58+
// exception and rethrow using a setTimeout so they become visible again.
59+
// Note that eventually this function could go away if we are confident
60+
// enough the code is exception free.
61+
const unguardedEventListen = <T>(
62+
target: EventTarget,
63+
type: string | number,
64+
fn: (param: T) => void
65+
): void => {
66+
// TODO(dimond): closure typing seems broken because WebChannel does
67+
// not implement goog.events.Listenable
68+
target.listen(type, (param: unknown) => {
69+
try {
70+
fn(param as T);
71+
} catch (e) {
72+
setTimeout(() => {
73+
throw e;
74+
}, 0);
75+
}
76+
});
77+
};
78+
5779
export class WebChannelConnection extends RestConnection {
5880
private readonly forceLongPolling: boolean;
5981
private readonly autoDetectLongPolling: boolean;
@@ -71,6 +93,29 @@ export class WebChannelConnection extends RestConnection {
7193
this.longPollingOptions = info.longPollingOptions;
7294
}
7395

96+
/**
97+
* Track if the STAT_EVENT listener has been initialized.
98+
*/
99+
static statEventListenerInitialized: boolean = false;
100+
101+
/**
102+
* Initialize STAT_EVENT listener once. Subsequent calls are a no-op.
103+
* getStatEventTarget() returns the same target every time.
104+
*/
105+
static ensureStatEventListenerInitialized(): void {
106+
if (!WebChannelConnection.statEventListenerInitialized) {
107+
const requestStats = getStatEventTarget();
108+
unguardedEventListen<StatEvent>(requestStats, Event.STAT_EVENT, event => {
109+
if (event.stat === Stat.PROXY) {
110+
logDebug(LOG_TAG, `STAT_EVENT: detected buffering proxy`);
111+
} else if (event.stat === Stat.NOPROXY) {
112+
logDebug(LOG_TAG, `STAT_EVENT: detected no buffering proxy`);
113+
}
114+
});
115+
WebChannelConnection.statEventListenerInitialized = true;
116+
}
117+
}
118+
74119
protected performRPCRequest<Req, Resp>(
75120
rpcName: string,
76121
url: string,
@@ -183,7 +228,6 @@ export class WebChannelConnection extends RestConnection {
183228
'/channel'
184229
];
185230
const webchannelTransport = this.createWebChannelTransport();
186-
const requestStats = getStatEventTarget();
187231
const request: WebChannelOptions = {
188232
// Required for backend stickiness, routing behavior is based on this
189233
// parameter.
@@ -286,28 +330,6 @@ export class WebChannelConnection extends RestConnection {
286330
closeFn: () => channel.close()
287331
});
288332

289-
// Closure events are guarded and exceptions are swallowed, so catch any
290-
// exception and rethrow using a setTimeout so they become visible again.
291-
// Note that eventually this function could go away if we are confident
292-
// enough the code is exception free.
293-
const unguardedEventListen = <T>(
294-
target: EventTarget,
295-
type: string | number,
296-
fn: (param: T) => void
297-
): void => {
298-
// TODO(dimond): closure typing seems broken because WebChannel does
299-
// not implement goog.events.Listenable
300-
target.listen(type, (param: unknown) => {
301-
try {
302-
fn(param as T);
303-
} catch (e) {
304-
setTimeout(() => {
305-
throw e;
306-
}, 0);
307-
}
308-
});
309-
};
310-
311333
unguardedEventListen(channel, WebChannel.EventType.OPEN, () => {
312334
if (!closed) {
313335
logDebug(
@@ -410,19 +432,8 @@ export class WebChannelConnection extends RestConnection {
410432
}
411433
);
412434

413-
unguardedEventListen<StatEvent>(requestStats, Event.STAT_EVENT, event => {
414-
if (event.stat === Stat.PROXY) {
415-
logDebug(
416-
LOG_TAG,
417-
`RPC '${rpcName}' stream ${streamId} detected buffering proxy`
418-
);
419-
} else if (event.stat === Stat.NOPROXY) {
420-
logDebug(
421-
LOG_TAG,
422-
`RPC '${rpcName}' stream ${streamId} detected no buffering proxy`
423-
);
424-
}
425-
});
435+
// Ensure that event listeners are configured for STAT_EVENTs.
436+
WebChannelConnection.ensureStatEventListenerInitialized();
426437

427438
setTimeout(() => {
428439
// Technically we could/should wait for the WebChannel opened event,

0 commit comments

Comments
 (0)