Skip to content

Commit a77cffc

Browse files
mjc1283jordangarcia
authored andcommitted
Simplify resource manager (#4)
- ResourceLoader interface is just a function that returns either the resource or a promise of the resource - Removed stream & emitter functionality
1 parent 0aac784 commit a77cffc

File tree

8 files changed

+83
-601
lines changed

8 files changed

+83
-601
lines changed

packages/js-web-sdk/packages/js-web-sdk/src/DatafileLoaders.ts

Lines changed: 24 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { OptimizelyDatafile } from './Datafile'
2-
import { ResourceLoader, ResourceEmitter } from './ResourceStream'
2+
import { ResourceLoader } from './ResourceManager'
33
const fetch = require('node-fetch')
44

55
export class ProvidedDatafileLoader implements ResourceLoader<OptimizelyDatafile> {
@@ -9,13 +9,8 @@ export class ProvidedDatafileLoader implements ResourceLoader<OptimizelyDatafile
99
this.datafile = config.datafile
1010
}
1111

12-
load(emitter: ResourceEmitter<OptimizelyDatafile>): void {
13-
emitter.data({
14-
resourceKey: 'datafile',
15-
resource: this.datafile,
16-
metadata: { source: 'fresh' },
17-
})
18-
emitter.ready()
12+
public load() {
13+
return this.datafile;
1914
}
2015
}
2116

@@ -30,74 +25,49 @@ type FetchUrlCacheEntry = {
3025
export class FetchUrlDatafileLoader implements ResourceLoader<OptimizelyDatafile> {
3126
private sdkKey: string
3227
private localStorageKey: string
33-
private preferCached: boolean
34-
private backgroundLoadIfCacheHit: boolean
28+
29+
// 1 week in ms = 7 days * 24 hours * 60 minutes * 60 seconds * 1000 ms
30+
private static MAX_CACHE_AGE_MS: number = 7 * 24 * 60 * 60 * 1000
3531

3632
constructor(config: {
3733
sdkKey: string
3834
localStorageKey?: string
39-
preferCached?: boolean
40-
backgroundLoadIfCacheHit?: boolean
4135
}) {
4236
this.sdkKey = config.sdkKey
4337
this.localStorageKey = config.localStorageKey || 'optly_fs_datafile'
44-
45-
this.backgroundLoadIfCacheHit = !!config.backgroundLoadIfCacheHit
46-
this.preferCached = !!config.preferCached
4738
}
4839

49-
load(emitter: ResourceEmitter<OptimizelyDatafile>): void {
40+
public load() {
5041
const cacheResult = this.getFromCache()
51-
if (cacheResult && this.shouldUseCache(cacheResult)) {
52-
emitter.data({
53-
resourceKey: 'datafile',
54-
resource: cacheResult.datafile,
55-
metadata: { source: 'cache' },
56-
})
57-
if (this.preferCached) {
58-
emitter.ready()
59-
}
60-
if (!this.backgroundLoadIfCacheHit) {
61-
// no need to load anything else, we're done
62-
return
63-
}
64-
}
65-
this.fetchDatafile().then(
42+
const freshDatafileFetch = this.fetchDatafile().then(
6643
datafile => {
67-
emitter.data({
68-
resourceKey: 'datafile',
69-
resource: datafile,
70-
metadata: { source: 'fresh' },
71-
})
72-
emitter.ready()
73-
const cacheEntry: FetchUrlCacheEntry = {
74-
datafile,
75-
metadata: {
76-
timestampCached: new Date().getTime(),
77-
},
78-
}
79-
this.saveToCache(cacheEntry)
80-
},
81-
response => {
82-
emitter.error({
83-
resourceKey: 'datafile',
84-
reason: 'failed to load',
85-
})
86-
},
44+
this.saveToCache(datafile)
45+
return datafile;
46+
}
8747
)
48+
if (cacheResult && this.shouldUseCache(cacheResult)) {
49+
return cacheResult.datafile;
50+
}
51+
return freshDatafileFetch;
8852
}
8953

90-
saveToCache(toSave: FetchUrlCacheEntry): void {
54+
saveToCache(datafileToSave: OptimizelyDatafile): void {
9155
if (typeof window !== 'undefined') {
56+
const cacheEntry: FetchUrlCacheEntry = {
57+
datafile: datafileToSave,
58+
metadata: {
59+
timestampCached: Date.now(),
60+
},
61+
}
9262
// use setTimeout as to not block on a potentially expensive JSON.stringify
9363
setTimeout(() => {
94-
window.localStorage.setItem(this.localStorageKey, JSON.stringify(toSave))
64+
window.localStorage.setItem(this.localStorageKey, JSON.stringify(cacheEntry))
9565
}, 0)
9666
}
9767
}
9868

9969
shouldUseCache(cacheResult: FetchUrlCacheEntry): boolean {
100-
return true
70+
return (Date.now() - cacheResult.metadata.timestampCached) <= FetchUrlDatafileLoader.MAX_CACHE_AGE_MS
10171
}
10272

10373
async fetchDatafile(): Promise<OptimizelyDatafile> {

packages/js-web-sdk/packages/js-web-sdk/src/OptimizelySDKWrapper.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import { StaticUserIdLoader, UserId } from './UserIdLoaders'
44
import { find } from './utils'
55
import { ProvidedDatafileLoader, FetchUrlDatafileLoader } from './DatafileLoaders'
66
import { ProvidedAttributesLoader } from './UserAttributesLoaders'
7-
import { ResourceManager } from './ResourceManager'
8-
import { ResourceLoader } from './ResourceStream'
7+
import { ResourceLoader, ResourceManager } from './ResourceManager'
98

109
// export types
1110
export { OptimizelyDatafile }
@@ -126,22 +125,14 @@ export class OptimizelySDKWrapper implements IOptimizelySDKWrapper {
126125
userId: this.setupUserIdLoader(config),
127126
})
128127

129-
if (this.resourceManager.allResourcesReady()) {
128+
if (this.resourceManager.allResourcesLoaded()) {
130129
this.onInitialized()
131130
this.initializingPromise = Promise.resolve()
132131
} else {
133-
// TODO handle unsubscribe
134-
this.initializingPromise = new Promise((resolve, reject) => {
135-
this.resourceManager.stream.subscribe({
136-
ready: () => {
137-
this.onInitialized()
138-
resolve()
139-
},
140-
error: ({ resourceKey, reason }) => {
141-
reject(`"${resourceKey}" failed to load: ${reason}`)
142-
},
132+
this.initializingPromise = this.resourceManager.allResourcePromises()
133+
.then(() => {
134+
this.onInitialized();
143135
})
144-
})
145136
}
146137
}
147138

@@ -479,8 +470,6 @@ export class OptimizelySDKWrapper implements IOptimizelySDKWrapper {
479470
} else if (config.sdkKey) {
480471
datafileLoader = new FetchUrlDatafileLoader({
481472
sdkKey: config.sdkKey,
482-
preferCached: true,
483-
backgroundLoadIfCacheHit: true,
484473
})
485474
} else if (config.UNSTABLE_datafileLoader) {
486475
datafileLoader = config.UNSTABLE_datafileLoader
@@ -515,9 +504,9 @@ export class OptimizelySDKWrapper implements IOptimizelySDKWrapper {
515504
}
516505

517506
private onInitialized() {
518-
const datafile = this.resourceManager.datafile.getValue()
519-
this.userId = this.resourceManager.userId.getValue() || null
520-
this.attributes = this.resourceManager.attributes.getValue() || {}
507+
const datafile = this.resourceManager.datafile.value
508+
this.userId = this.resourceManager.userId.value || null
509+
this.attributes = this.resourceManager.attributes.value || {}
521510
if (datafile) {
522511
this.datafile = datafile
523512
}

packages/js-web-sdk/packages/js-web-sdk/src/ResourceManager.ts

Lines changed: 43 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,116 +2,78 @@ import { OptimizelyDatafile } from './OptimizelySDKWrapper'
22
import { UserAttributes } from '@optimizely/optimizely-sdk'
33
import { UserId } from './UserIdLoaders'
44

5-
import {
6-
ResourceEmitter,
7-
ResourceStream,
8-
SingleResourceStream,
9-
ResourceLoader,
10-
CombinedResourceStream,
11-
} from './ResourceStream'
12-
13-
interface Resource<K> {
14-
getValue(): K | undefined
15-
hasValue(): boolean
16-
isReady(): boolean
17-
hasFailed(): boolean
18-
getMetadata(): ResourceEmitter.DataMessage.Metadata | undefined
19-
getFailureReason(): string
5+
export interface ResourceLoader<K> {
6+
load: () => K | Promise<K>
207
}
218

22-
type ResourceLoaderState<K> = {
23-
resource: K | undefined
24-
metadata: ResourceEmitter.DataMessage.Metadata | undefined
25-
isReady: boolean
26-
failed: boolean
27-
failureReason: string
28-
}
29-
30-
class BaseResource<K> implements Resource<K> {
31-
public stream: ResourceStream<K>
32-
protected state: ResourceLoaderState<K>
9+
class Resource<K> {
10+
private loader: ResourceLoader<K>
3311

34-
constructor(loader: ResourceLoader<K>) {
35-
this.state = {
36-
resource: undefined,
37-
metadata: undefined,
38-
isReady: false,
39-
failed: false,
40-
failureReason: '',
41-
}
12+
private _value?: K
4213

43-
this.stream = new SingleResourceStream(loader)
44-
this.stream.subscribe({
45-
data: msg => {
46-
this.state.resource = msg.resource
47-
this.state.metadata = msg.metadata
48-
},
49-
ready: () => {
50-
this.state.isReady = true
51-
},
52-
error: msg => {
53-
this.state.failed = true
54-
this.state.failureReason = msg.reason
55-
},
56-
})
57-
}
14+
private _hasLoaded: boolean
5815

59-
getValue(): K | undefined {
60-
return this.state.resource
61-
}
16+
public readonly promise: Promise<K>
6217

63-
hasValue(): boolean {
64-
return this.getValue() !== void 0
18+
public get value(): K | undefined {
19+
return this._value
6520
}
6621

67-
isReady(): boolean {
68-
return this.state.isReady
22+
public get hasLoaded(): boolean {
23+
return this._hasLoaded
6924
}
7025

71-
hasFailed(): boolean {
72-
return this.state.failed
73-
throw new Error('Method not implemented.')
26+
constructor(loader: ResourceLoader<K>) {
27+
this.loader = loader;
28+
this._hasLoaded = false;
29+
this.promise = this.load();
7430
}
7531

76-
getMetadata(): ResourceEmitter.DataMessage.Metadata | undefined {
77-
return this.state.metadata
32+
private updateStateFromLoadResult(value: K) {
33+
this._value = value;
34+
this._hasLoaded = true;
7835
}
7936

80-
getFailureReason(): string {
81-
return this.state.failureReason
37+
private load(): Promise<K> {
38+
const maybeValue = this.loader.load()
39+
// TODO: test does this work with polyfilled promise?
40+
if (maybeValue instanceof Promise) {
41+
return maybeValue.then(value => {
42+
this.updateStateFromLoadResult(value);
43+
return value
44+
})
45+
}
46+
this.updateStateFromLoadResult(maybeValue);
47+
return Promise.resolve(maybeValue);
8248
}
8349
}
8450

85-
export class ResourceManager {
86-
protected resourceKeys = ['datafile', 'attributes', 'userId']
51+
type OptimizelyResource = OptimizelyDatafile | UserAttributes | UserId
8752

88-
public datafile: BaseResource<OptimizelyDatafile>
89-
public attributes: BaseResource<UserAttributes>
90-
public userId: BaseResource<UserId>
53+
export class ResourceManager {
54+
public datafile: Resource<OptimizelyDatafile>
55+
public attributes: Resource<UserAttributes>
56+
public userId: Resource<UserId>
9157

92-
public stream: ResourceStream<any>
58+
private resources: Resource<OptimizelyResource>[]
9359

9460
constructor(loaders: {
9561
datafile: ResourceLoader<OptimizelyDatafile>
9662
attributes: ResourceLoader<UserAttributes>
9763
userId: ResourceLoader<UserId>
9864
}) {
99-
this.datafile = new BaseResource(loaders.datafile)
100-
this.attributes = new BaseResource(loaders.attributes)
101-
this.userId = new BaseResource(loaders.userId)
102-
103-
this.stream = new CombinedResourceStream([this.datafile.stream, this.attributes.stream, this.userId.stream])
104-
}
65+
this.datafile = new Resource(loaders.datafile)
66+
this.attributes = new Resource(loaders.attributes)
67+
this.userId = new Resource(loaders.userId)
10568

106-
allResourcesHaveValues(): boolean {
107-
return this.everyResource(resource => resource.hasValue())
69+
this.resources = [this.datafile, this.attributes, this.userId];
10870
}
10971

110-
allResourcesReady(): boolean {
111-
return this.everyResource(resource => resource.isReady())
72+
public allResourcesLoaded(): boolean {
73+
return this.resources.every(resource => resource.hasLoaded);
11274
}
11375

114-
private everyResource(fn: (resource: Resource<any>) => boolean): boolean {
115-
return this.resourceKeys.every(key => fn(this[key]))
76+
public allResourcePromises(): Promise<OptimizelyResource[]> {
77+
return Promise.all([...this.resources.map(resource => resource.promise)])
11678
}
11779
}

0 commit comments

Comments
 (0)