Skip to content

Commit

Permalink
improvement(caching): implement client-side caching for listing and p…
Browse files Browse the repository at this point in the history
…rofile data (#52)
  • Loading branch information
24thsaint committed Jan 21, 2020
1 parent a27271f commit e0067f3
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 11 deletions.
42 changes: 42 additions & 0 deletions src/models/Cache.test.ts
@@ -0,0 +1,42 @@
import { Cache } from './Cache'

test('Properly initialize initial object state', () => {
const cache = new Cache()
expect(JSON.stringify(cache)).toEqual(JSON.stringify({ cache: {} }))
})

test('Add cache entry', () => {
const cache = new Cache()
const realDateNow = Date.now.bind(global.Date)
const dateNowStub = jest.fn(() => 3600000)
global.Date.now = dateNowStub
cache.store('test-key', 'test-value')

expect(cache.retrieve('test-key')).toEqual('test-value')
expect(JSON.stringify(cache)).toEqual(
JSON.stringify({
cache: {
'test-key': {
data: 'test-value',
expiresOn: 3600000 + 3600000,
},
},
})
)
})

test('Cache expiration', () => {
const cache = new Cache()
const realDateNow = Date.now.bind(global.Date)
const dateNowStub = jest.fn(() => 3600000)
global.Date.now = dateNowStub
cache.store('test-key', 'test-value')

const dateExpireStub = jest.fn(() => 3600000 + 3600000)
global.Date.now = dateExpireStub

expect(cache.hasExpired('test-key')).toBe(true)
expect(cache.retrieve('test-key')).toBe(false)

global.Date.now = realDateNow
})
50 changes: 50 additions & 0 deletions src/models/Cache.ts
@@ -0,0 +1,50 @@
interface CacheInterface {
[key: string]: CacheData
}

interface CacheData {
data: any
expiresOn: number
}

class Cache {
private cache: CacheInterface = {}

constructor(props?) {
if (props) {
Object.assign(this, props)
}
}

public store(key: string, data: any) {
const expiresOn = Date.now() + 3600000
this.cache[key] = { data, expiresOn }
}

public retrieve(key: string) {
if (this.cache[key]) {
if (this.hasExpired(key)) {
delete this.cache[key]
return false
}
/**
* Cloning object to avoid unintentional mutations
*/
return JSON.parse(JSON.stringify(this.cache[key].data))
} else {
return false
}
}

public delete(key: string) {
delete this.cache[key]
}

public hasExpired(key: string) {
const cacheData = this.cache[key]
return cacheData.expiresOn <= Date.now()
}
}

const cacheInstance = new Cache()
export { cacheInstance, Cache }
29 changes: 18 additions & 11 deletions src/models/Listing.ts
Expand Up @@ -15,6 +15,7 @@ import {
} from '../interfaces/Listing'
import Location from '../interfaces/Location'
import Rating, { RatingSummary } from '../interfaces/Rating'
import { cacheInstance } from './Cache'
import currency from './Currency'
import Profile from './Profile'

Expand All @@ -28,31 +29,37 @@ export interface ListingResponse {

class Listing implements ListingInterface {
public static async retrieve(id: string): Promise<ListingResponse> {
const kimitzuListingRequest = await Axios.get(
`${config.kimitzuHost}/kimitzu/listing?hash=${id}`
)
const kimitzuListing = kimitzuListingRequest.data
let rawListingData

/**
* Load profile from cache.
*/
const vendor = await Profile.retrieve(kimitzuListing.vendorID.peerID, false)
if (cacheInstance.retrieve(id)) {
rawListingData = cacheInstance.retrieve(id)
} else {
const kimitzuListingRequest = await Axios.get(
`${config.kimitzuHost}/kimitzu/listing?hash=${id}`
)
rawListingData = kimitzuListingRequest.data
cacheInstance.store(id, rawListingData)
rawListingData = cacheInstance.retrieve(id)
}

const vendor = await Profile.retrieve(rawListingData.vendorID.peerID, false)

const imageData = kimitzuListing.item.images.map((image: Image) => {
const imageData = rawListingData.item.images.map((image: Image) => {
return { src: `${config.openBazaarHost}/ob/images/${image.medium}` }
})

const currentUser = await Profile.retrieve('', false)

const listing = new Listing(kimitzuListing)
const listing = new Listing(rawListingData)
listing.currentUser = currentUser
listing.isOwner = currentUser.peerID === listing.vendorID.peerID

/**
* Return vendor profile, listing information, and image sources separately
* to avoid complicated mutations in the listing object.
*/
return { vendor, listing, imageData }
const processedListingData = { vendor, listing, imageData }
return processedListingData
}

public currentUser: Profile = new Profile()
Expand Down
10 changes: 10 additions & 0 deletions src/models/Profile.ts
Expand Up @@ -21,6 +21,7 @@ import Rating, { RatingSummary } from '../interfaces/Rating'
import isElectron from 'is-electron'
import defaults from '../constants/Defaults'
import decodeHtml from '../utils/Unescape'
import { cacheInstance } from './Cache'

const profileDefaults = defaults.profile
const LOCATION_TYPES = ['primary', 'shipping', 'billing', 'return']
Expand Down Expand Up @@ -163,6 +164,12 @@ class Profile implements ProfileSchema {
public static async retrieve(id?: string, force?: boolean): Promise<Profile> {
let profile: Profile

const cacheId = id ? id : '~self'
const profileCache = cacheInstance.retrieve(cacheId)
if (profileCache) {
return profileCache
}

try {
if (id) {
const peerRequest = await Axios.get(
Expand Down Expand Up @@ -204,6 +211,7 @@ class Profile implements ProfileSchema {
profile.moderatorInfo.fee.fixedFee.amount = profile.moderatorInfo.fee.fixedFee.amount / 100
}

cacheInstance.store(cacheId, profile)
return profile
}

Expand Down Expand Up @@ -329,6 +337,7 @@ class Profile implements ProfileSchema {
}

public async save() {
cacheInstance.delete('~self')
this.location = this.getAddress('primary')
await Axios.post(`${config.openBazaarHost}/ob/profile`, this)
await Profile.publish()
Expand Down Expand Up @@ -356,6 +365,7 @@ class Profile implements ProfileSchema {
}

public async update() {
cacheInstance.delete('~self')
this.location = this.getAddress('primary')
this.preSave()
await Axios.put(`${config.openBazaarHost}/ob/profile`, this)
Expand Down

0 comments on commit e0067f3

Please sign in to comment.