Skip to content

Commit b758777

Browse files
committed
added the VisibilityMonitor code
1 parent 36e19aa commit b758777

File tree

3 files changed

+177
-3
lines changed

3 files changed

+177
-3
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* @license
3+
* Copyright 2017 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 { assert } from '@firebase/util';
19+
20+
/**
21+
* Base class to be used if you want to emit events. Call the constructor with
22+
* the set of allowed event names.
23+
*/
24+
export abstract class EventEmitter {
25+
private listeners_: {
26+
[eventType: string]: Array<{
27+
callback(...args: unknown[]): void;
28+
context: unknown;
29+
}>;
30+
} = {};
31+
32+
constructor(private allowedEvents_: string[]) {
33+
assert(
34+
Array.isArray(allowedEvents_) && allowedEvents_.length > 0,
35+
'Requires a non-empty array'
36+
);
37+
}
38+
39+
/**
40+
* To be overridden by derived classes in order to fire an initial event when
41+
* somebody subscribes for data.
42+
*
43+
* @returns {Array.<*>} Array of parameters to trigger initial event with.
44+
*/
45+
abstract getInitialEvent(eventType: string): unknown[];
46+
47+
/**
48+
* To be called by derived classes to trigger events.
49+
*/
50+
protected trigger(eventType: string, ...varArgs: unknown[]) {
51+
if (Array.isArray(this.listeners_[eventType])) {
52+
// Clone the list, since callbacks could add/remove listeners.
53+
const listeners = [...this.listeners_[eventType]];
54+
55+
for (let i = 0; i < listeners.length; i++) {
56+
listeners[i].callback.apply(listeners[i].context, varArgs);
57+
}
58+
}
59+
}
60+
61+
on(eventType: string, callback: (a: unknown) => void, context: unknown) {
62+
this.validateEventType_(eventType);
63+
this.listeners_[eventType] = this.listeners_[eventType] || [];
64+
this.listeners_[eventType].push({ callback, context });
65+
66+
const eventData = this.getInitialEvent(eventType);
67+
if (eventData) {
68+
callback.apply(context, eventData);
69+
}
70+
}
71+
72+
off(eventType: string, callback: (a: unknown) => void, context: unknown) {
73+
this.validateEventType_(eventType);
74+
const listeners = this.listeners_[eventType] || [];
75+
for (let i = 0; i < listeners.length; i++) {
76+
if (
77+
listeners[i].callback === callback &&
78+
(!context || context === listeners[i].context)
79+
) {
80+
listeners.splice(i, 1);
81+
return;
82+
}
83+
}
84+
}
85+
86+
private validateEventType_(eventType: string) {
87+
assert(
88+
this.allowedEvents_.find(et => {
89+
return et === eventType;
90+
}),
91+
'Unknown event: ' + eventType
92+
);
93+
}
94+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* @license
3+
* Copyright 2025 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 { assert } from '@firebase/util';
19+
20+
import { EventEmitter } from './EventEmitter';
21+
22+
declare const document: Document;
23+
24+
export class VisibilityMonitor extends EventEmitter {
25+
private visible_: boolean;
26+
27+
static getInstance() {
28+
return new VisibilityMonitor();
29+
}
30+
31+
constructor() {
32+
super(['visible']);
33+
let hidden: string;
34+
let visibilityChange: string;
35+
if (
36+
typeof document !== 'undefined' &&
37+
typeof document.addEventListener !== 'undefined'
38+
) {
39+
if (typeof document['hidden'] !== 'undefined') {
40+
// Opera 12.10 and Firefox 18 and later support
41+
visibilityChange = 'visibilitychange';
42+
hidden = 'hidden';
43+
} else if (typeof document['mozHidden'] !== 'undefined') {
44+
visibilityChange = 'mozvisibilitychange';
45+
hidden = 'mozHidden';
46+
} else if (typeof document['msHidden'] !== 'undefined') {
47+
visibilityChange = 'msvisibilitychange';
48+
hidden = 'msHidden';
49+
} else if (typeof document['webkitHidden'] !== 'undefined') {
50+
visibilityChange = 'webkitvisibilitychange';
51+
hidden = 'webkitHidden';
52+
}
53+
}
54+
55+
// Initially, we always assume we are visible. This ensures that in browsers
56+
// without page visibility support or in cases where we are never visible
57+
// (e.g. chrome extension), we act as if we are visible, i.e. don't delay
58+
// reconnects
59+
this.visible_ = true;
60+
61+
if (visibilityChange) {
62+
document.addEventListener(
63+
visibilityChange,
64+
() => {
65+
const visible = !document[hidden];
66+
if (visible !== this.visible_) {
67+
this.visible_ = visible;
68+
this.trigger('visible', visible);
69+
}
70+
},
71+
false
72+
);
73+
}
74+
}
75+
76+
getInitialEvent(eventType: string): boolean[] {
77+
assert(eventType === 'visible', 'Unknown event type: ' + eventType);
78+
return [this.visible_];
79+
}
80+
}

packages/remote-config/src/client/realtime_handler.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { ERROR_FACTORY, ErrorCode } from '../errors';
2121
import { _FirebaseInstallationsInternal } from '@firebase/installations';
2222
import { Storage } from '../storage/storage';
2323
import { calculateBackoffMillis, FirebaseError } from '@firebase/util';
24-
import { VisibilityMonitor } from '@firebase/database/dist/src/core/util/VisibilityMonitor';
24+
import { VisibilityMonitor } from './VisibilityMonitor';
2525

2626
export class RealtimeHandler {
2727
constructor(
@@ -33,7 +33,7 @@ export class RealtimeHandler {
3333
private readonly apiKey: string,
3434
private readonly appId: string
3535
) {
36-
VisibilityMonitor.getInstance().on('visible', this.onVisibilityChange_, this);
36+
VisibilityMonitor.getInstance().on('visible', this.onVisibilityChange, this);
3737
}
3838

3939
private streamController?: AbortController;
@@ -268,7 +268,7 @@ export class RealtimeHandler {
268268
return new URL(urlString);
269269
}
270270

271-
private onVisibilityChange_(visible: unknown) {
271+
private onVisibilityChange(visible: unknown) {
272272
const wasInBackground = this.isInBackground;
273273
this.isInBackground = !visible;
274274
if (wasInBackground !== this.isInBackground) {

0 commit comments

Comments
 (0)