Skip to content

Commit 0cdb120

Browse files
committed
Nice to have changes (more in CHANGELOG.md)
1 parent b8d30cd commit 0cdb120

File tree

6 files changed

+73
-33
lines changed

6 files changed

+73
-33
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
### TODO
1+
# Changelog
2+
## 0.2.0
3+
- `fetchAndActivate` now locks semaphore to prevent concurent fetch attempts
4+
- Added timer to auto fetch config when cache expires
5+
- Added `asConverted` on `Value` and `getAllConverted` on `RemoteConfig` which returns auto-converted values

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eneris/firebase-nodejs-client",
3-
"version": "0.1.1",
3+
"version": "0.2.0",
44
"description": "Firebase NodeJS client",
55
"author": "Eneris<[email protected]>",
66
"license": "Apache-2.0",

src/lib/remoteConfig.ts

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export default class RemoteConfig<T = Record<string, string>> extends EventEmitt
4141
private readonly options: RemoteConfigOptions<T>
4242
private readonly storage: StorageInterface<RemoteConfigStore<T>>
4343
private readonly request: AxiosInstance
44+
private refreshTimer: NodeJS.Timer
45+
private semaphoreFetch: Promise<void>
4446

4547
constructor(app: FirebaseApp, options: RemoteConfigOptions<T> = {}) {
4648
super()
@@ -69,12 +71,9 @@ export default class RemoteConfig<T = Record<string, string>> extends EventEmitt
6971
set: (key, value) => this.app.storage.set(storagePrefix + key, value),
7072
} as StorageInterface<RemoteConfigStore<T>>
7173

72-
this.on('fetch', () => console.log('Constructor - fetch'))
73-
this.on('activate', () => console.log('Constructor - activate'))
74-
}
74+
this.fetchAndActivate()
7575

76-
get defaultConfig() {
77-
return this.options.defaultConfig
76+
this.refreshTimer = setInterval(this.fetchAndActivate, this.options.cacheMaxAge + 1000)
7877
}
7978

8079
get isCacheValid() {
@@ -83,68 +82,75 @@ export default class RemoteConfig<T = Record<string, string>> extends EventEmitt
8382
return this.storage.get('config') && cacheAge < this.options.cacheMaxAge
8483
}
8584

86-
getActiveConfig(): T {
85+
get defaultConfig() {
86+
return this.options.defaultConfig
87+
}
88+
89+
private get activeConfig() {
8790
return this.storage.get('config')
8891
}
8992

90-
getDefaultConfig(): T {
91-
return this.options.defaultConfig
93+
set defaultConfig(defaultConfig: RemoteConfigOptions<T>['defaultConfig']) {
94+
this.options.defaultConfig = defaultConfig
9295
}
9396

9497
getValue<K extends keyof T>(key: K): Value {
95-
const config = this.getActiveConfig()
98+
const config = this.activeConfig
9699

100+
// Has remote version
97101
if (config && config[key] !== undefined) {
98-
return new Value('remote', String(config[key]))
102+
return new Value('remote', config[key] as string)
99103
}
100104

101-
const defaultConfig = this.getDefaultConfig()
105+
const defaultConfig = this.defaultConfig
102106

107+
// Has default version
103108
if (defaultConfig && defaultConfig[key] !== undefined) {
104-
return new Value('default', String(defaultConfig[key]))
109+
return new Value('default', defaultConfig[key] as string)
105110
}
106111

112+
// Not found, return static
107113
return new Value('static')
108114
}
109115

110-
getAll() {
116+
getAll(): Record<string, Value> {
111117
const config = {
112-
...(this.getActiveConfig() || {}),
113-
...(this.getDefaultConfig() || {})
118+
...(this.activeConfig || {}),
119+
...(this.defaultConfig || {})
114120
}
115121

116122
return Object.keys(config).reduce((result, key) => {
117123
result[key] = this.getValue(key as keyof T)
124+
118125
return result
119126
}, {})
120127
}
121128

122-
getBoolean<K extends keyof T>(key: K): boolean {
123-
return this.getValue(key).asBoolean()
124-
}
125-
126-
getNumber<K extends keyof T>(key: K): number {
127-
return this.getValue(key).asNumber()
128-
}
129+
getAllConverted(): T {
130+
const config = this.getAll()
129131

130-
getString<K extends keyof T>(key: K): string {
131-
return this.getValue(key).asString()
132-
}
132+
Object.keys(config).forEach((key) => {
133+
config[key] = config[key].asConverted()
134+
})
133135

134-
getJSON<K extends keyof T, V = Record<string, unknown>>(key: K): V {
135-
return this.getValue(key).asJSON<V>()
136+
return config as T
136137
}
137138

138139
async fetchAndActivate(ignoreCache = false): Promise<void> {
139140
const etag = this.storage.get('etag')
140141

141-
142142
if (!ignoreCache && this.isCacheValid) {
143143
this.emit('fetch')
144-
console.log('config cache is valid')
145144
return Promise.resolve()
146145
}
147146

147+
if (this.semaphoreFetch) {
148+
return this.semaphoreFetch;
149+
}
150+
151+
let resolveSemaphore = null
152+
this.semaphoreFetch = new Promise((res) => { resolveSemaphore = res })
153+
148154
const installation = await this.installations.getInstallation()
149155

150156
const response = await this.request.post(`:fetch`, {
@@ -212,5 +218,9 @@ export default class RemoteConfig<T = Record<string, string>> extends EventEmitt
212218
throw new Error(`Failed to fetch RemoteConfig status: ${status}`)
213219

214220
}
221+
222+
this.semaphoreFetch = null
223+
224+
resolveSemaphore?.()
215225
}
216226
}

src/utils/storage.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export interface StorageInterface<T = Record<string, any>> {
22
get<K extends keyof T>(key: K): T[K]
3-
set<K extends keyof T>(key: K, value: T[K]): this
3+
set<K extends keyof T>(key: K, value: T[K]): void
44
}
55

66
export default class MemoryStorage<T = Record<string, any>> implements StorageInterface<T> {
@@ -12,6 +12,5 @@ export default class MemoryStorage<T = Record<string, any>> implements StorageIn
1212

1313
set(key, value) {
1414
this.data[key] = value
15-
return this
1615
}
1716
}

src/utils/value.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,39 @@ export default class Value {
5656
}
5757

5858
asJSON<T = Record<string, unknown>>(): T {
59+
if (typeof this._value !== 'string') {
60+
return this._value
61+
}
62+
5963
try {
6064
return JSON.parse(this._value) as T
6165
} catch {
6266
return null
6367
}
6468
}
6569

70+
asConverted(): any {
71+
if (typeof this._value !== 'string') {
72+
return this._value
73+
}
74+
75+
if (['true', 'false'].includes(this._value)) {
76+
return Boolean(this._value)
77+
}
78+
79+
const asNumber = Number(this._value)
80+
81+
if (asNumber !== NaN) {
82+
return asNumber
83+
}
84+
85+
try {
86+
return JSON.parse(this._value)
87+
} catch {
88+
return this._value
89+
}
90+
}
91+
6692
getSource(): ValueSource {
6793
return this._source
6894
}

wiki/RemoteConfig.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
- `fetchAndActivate` - attempts to download config from remote server if cache is missing or invalid
99
- `getValue` - Returns instance of [`Value`](Value.md) for given key
1010
- `getAll` - Returns all properties as collection of [`Value`](Value.md) instances
11+
- `getAllConverted` - Returns all properties converted into their "best guess" types

0 commit comments

Comments
 (0)