Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions core/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ const createApp = async (ssrContext, config, storeCode = null): Promise<{app: Vu
store.state.version = process.env.APPVERSION
store.state.config = config // @deprecated
store.state.__DEMO_MODE__ = (config.demomode === true)
if (ssrContext) Vue.prototype.$ssrRequestContext = ssrContext
if (ssrContext) {
// @deprecated - we shouldn't share server context between requests
Vue.prototype.$ssrRequestContext = {output: {cacheTags: ssrContext.output.cacheTags}}

Vue.prototype.$cacheTags = ssrContext.output.cacheTags
}
if (!store.state.config) store.state.config = globalConfig // @deprecated - we should avoid the `config`
const storeView = await prepareStoreView(storeCode) // prepare the default storeView
store.state.storeView = storeView
Expand All @@ -87,6 +92,10 @@ const createApp = async (ssrContext, config, storeCode = null): Promise<{app: Vu
Object.keys(coreMixins).forEach(key => {
Vue.mixin(coreMixins[key])
})

Object.keys(coreFilters).forEach(key => {
Vue.filter(key, coreFilters[key])
})
})

// @todo remove this part when we'll get rid of global multistore mixin
Expand All @@ -99,10 +108,6 @@ const createApp = async (ssrContext, config, storeCode = null): Promise<{app: Vu
})
}

Object.keys(coreFilters).forEach(key => {
Vue.filter(key, coreFilters[key])
})

let vueOptions = {
router,
store,
Expand Down
2 changes: 1 addition & 1 deletion core/client-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const invokeClientEntry = async () => {
}
return // do not resolve asyncData on server render - already been done
}
if (Vue.prototype.$ssrRequestContext) Vue.prototype.$ssrRequestContext.output.cacheTags = new Set<string>()
if (!Vue.prototype.$cacheTags) Vue.prototype.$cacheTags = new Set<string>()
const matched = router.getMatchedComponents(to)
if (to) { // this is from url
if (globalConfig.storeViews.multistore === true) {
Expand Down
4 changes: 3 additions & 1 deletion core/filters/price.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export function price (value) {
}
let formattedVal = Math.abs(parseFloat(value)).toFixed(2)
const storeView = currentStoreView()

if (!storeView.i18n) {
return value;
}
const prependCurrency = (price) => {
return storeView.i18n.currencySign + price
}
Expand Down
4 changes: 2 additions & 2 deletions core/modules/catalog/store/category/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ const actions: ActionTree<CategoryState, RootState> = {
if (setCurrentCategory) {
commit(types.CATEGORY_UPD_CURRENT_CATEGORY, mainCategory)
}
if (populateRequestCacheTags && mainCategory && Vue.prototype.$ssrRequestContext) {
Vue.prototype.$ssrRequestContext.output.cacheTags.add(`C${mainCategory.id}`)
if (populateRequestCacheTags && mainCategory && Vue.prototype.$cacheTags) {
Vue.prototype.$cacheTags.add(`C${mainCategory.id}`)
}
if (setCurrentCategoryPath) {
let currentPath = []
Expand Down
4 changes: 2 additions & 2 deletions core/modules/catalog/store/product/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,8 @@ const actions: ActionTree<ProductState, RootState> = {
return quickSearchByQuery({ query, start, size, entityType, sort, excludeFields, includeFields }).then((resp) => {
if (resp.items && resp.items.length) { // preconfigure products; eg: after filters
for (let product of resp.items) {
if (populateRequestCacheTags && Vue.prototype.$ssrRequestContext) {
Vue.prototype.$ssrRequestContext.output.cacheTags.add(`P${product.id}`)
if (populateRequestCacheTags && Vue.prototype.$cacheTags) {
Vue.prototype.$cacheTags.add(`P${product.id}`);
}
product.errors = {} // this is an object to store validation result for custom options and others
product.info = {}
Expand Down
16 changes: 15 additions & 1 deletion core/modules/catalog/store/product/getters.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { PagedProductList } from './../../types/ProductState';
import { nonReactiveState } from './index';
import { GetterTree } from 'vuex'
import RootState from '@vue-storefront/core/types/RootState'
import ProductState from '../../types/ProductState'
import cloneDeep from 'lodash-es/cloneDeep'

function mapCategoryProducts (productsFromState, productsData) {
return productsFromState.map(prodState => {
if (typeof prodState === 'string') {
const product = productsData.find(prodData => prodData.sku === prodState)
return cloneDeep(product)
}
return prodState
})
}

const getters: GetterTree<ProductState, RootState> = {
productParent: (state) => state.parent,
Expand All @@ -9,7 +22,8 @@ const getters: GetterTree<ProductState, RootState> = {
productOriginal: (state) => state.original,
currentOptions: (state) => state.current_options,
breadcrumbs: (state) => state.breadcrumbs,
productGallery: (state) => state.productGallery
productGallery: (state) => state.productGallery,
list: (state) => mapCategoryProducts((state.list as PagedProductList).items, nonReactiveState.list)
}

export default getters
4 changes: 4 additions & 0 deletions core/modules/catalog/store/product/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ export const productModule: Module<ProductState, RootState> = {
actions,
mutations
}

export const nonReactiveState = {
list: []
}
16 changes: 12 additions & 4 deletions core/modules/catalog/store/product/mutations.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { isServer } from '@vue-storefront/core/helpers';
import { nonReactiveState } from './index';
import Vue from 'vue'
import { MutationTree } from 'vuex'
import * as types from './mutation-types'
import ProductState, { PagedProductList } from '../../types/ProductState'
import cloneDeep from 'lodash-es/cloneDeep'

const mutations: MutationTree<ProductState> = {
[types.CATALOG_SET_BREADCRUMBS] (state, payload) {
Expand Down Expand Up @@ -29,11 +32,16 @@ const mutations: MutationTree<ProductState> = {
},
[types.CATALOG_UPD_PRODUCTS] (state, { products, append }) {
if (append === false) {
state.list = products
nonReactiveState.list = cloneDeep(products.items)
state.list = isServer ? products : cloneDeep({...products, items: products.items.map(prod => prod.sku)})
} else {
(state.list as PagedProductList).start = products.start as number
(state.list as PagedProductList).perPage = products.perPage as number
(state.list as PagedProductList).items = (state.list as PagedProductList).items.concat(products.items)
const pagedProductList = state.list as PagedProductList
pagedProductList.start = products.start as number
pagedProductList.perPage = products.perPage as number
nonReactiveState.list = cloneDeep(nonReactiveState.list.concat(products.items))
pagedProductList.items = isServer
? pagedProductList.items.concat(products.items)
: cloneDeep(pagedProductList.items.concat(products.items.map(prod => prod.sku)))
}
},
[types.CATALOG_SET_PRODUCT_CURRENT] (state, product) {
Expand Down
6 changes: 3 additions & 3 deletions core/pages/Category.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ export default {
computed: {
...mapGetters('category', ['getCurrentCategory', 'getCurrentCategoryProductQuery', 'getAllCategoryFilters', 'getCategoryBreadcrumbs', 'getCurrentCategoryPath']),
products () {
return this.$store.state.product.list.items
return this.$store.getters['product/list']
},
productsCounter () {
return this.$store.state.product.list.items ? this.$store.state.product.list.items.length : 0
return this.products ? this.products.length : 0
},
productsTotal () {
return this.$store.state.product.list.total
Expand All @@ -41,7 +41,7 @@ export default {
return this.getCurrentCategoryProductQuery
},
isCategoryEmpty () {
return (!(this.$store.state.product.list.items) || this.$store.state.product.list.items.length === 0)
return (!(this.products) || this.products.length === 0)
},
category () {
return this.getCurrentCategory
Expand Down
13 changes: 11 additions & 2 deletions core/scripts/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ if (isProd) {
})
}

function clearSSRContext (context) {
Object.keys(context.server).forEach(key => delete context.server[key])
delete context.output['cacheTags']
delete context['meta']
}

function invalidateCache (req, res) {
if (config.server.useOutputCache) {
if (req.query.tag && req.query.key) { // clear cache pages for specific query tag
Expand Down Expand Up @@ -171,7 +177,7 @@ app.get('*', (req, res, next) => {
append: (context) => { return '' },
appendHead: (context) => { return '' },
template: 'default',
cacheTags: null
cacheTags: new Set()
},
server: {
app: app,
Expand All @@ -189,7 +195,7 @@ app.get('*', (req, res, next) => {
res.setHeader('Content-Type', 'text/html')
}
let tagsArray = []
if (config.server.useOutputCacheTagging && context.output.cacheTags !== null) {
if (config.server.useOutputCacheTagging && context.output.cacheTags && context.output.cacheTags.size > 0) {
tagsArray = Array.from(context.output.cacheTags)
const cacheTags = tagsArray.join(' ')
res.setHeader('X-VS-Cache-Tags', cacheTags)
Expand Down Expand Up @@ -218,6 +224,9 @@ app.get('*', (req, res, next) => {
console.log(`whole request [${req.url}]: ${Date.now() - s}ms`)
next()
}).catch(errorHandler)
.finally(() => {
clearSSRContext(context)
})
}

const dynamicCacheHandler = () => {
Expand Down
1 change: 0 additions & 1 deletion core/server-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ export default async context => {
}
const { app, router, store } = await createApp(context, context.vs && context.vs.config ? context.vs.config : buildTimeConfig, storeCode)
return new Promise((resolve, reject) => {
context.output.cacheTags = new Set<string>()
const meta = (app as any).$meta()
router.push(context.url)
context.meta = meta
Expand Down
54 changes: 40 additions & 14 deletions core/store/lib/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Vue from 'vue'
import * as localForage from 'localforage'
import { Logger } from '@vue-storefront/core/lib/logger'
import { isServer } from '@vue-storefront/core/helpers'
import cloneDeep from 'lodash-es/cloneDeep'

const CACHE_TIMEOUT = 800
const CACHE_TIMEOUT_ITERATE = 2000
Expand Down Expand Up @@ -33,6 +34,13 @@ function roughSizeOfObject (object) {
return bytes
}

interface CacheTimeouts {
getItem: any,
iterate: any,
setItem: any,
base: any
}

class LocalForageCacheDriver {
private _collectionName: string;
private _dbName: string;
Expand All @@ -43,6 +51,12 @@ class LocalForageCacheDriver {
private _useLocalCacheByDefault: boolean;
private cacheErrorsCount: any;
private _storageQuota: number;
private _cacheTimeouts: CacheTimeouts = {
getItem: null,
iterate: null,
setItem: null,
base: null
}

public constructor (collection, useLocalCacheByDefault = true, storageQuota = 0) {
const collectionName = collection._config.storeName
Expand All @@ -53,7 +67,8 @@ class LocalForageCacheDriver {
const storageQuota = this._storageQuota
const iterateFnc = this.iterate.bind(this)
const removeItemFnc = this.removeItem.bind(this)
setInterval(() => {
clearInterval(this._cacheTimeouts.base)
this._cacheTimeouts.base = setInterval(() => {
let storageSize = 0
this.iterate((item, id, number) => {
storageSize += roughSizeOfObject(item)
Expand Down Expand Up @@ -135,6 +150,10 @@ class LocalForageCacheDriver {
}
}

public getLocalCache (key) {
return typeof this._localCache[key] !== 'undefined' ? cloneDeep(this._localCache[key]) : null
}

// Retrieve an item from the store. Unlike the original async_storage
// library in Gaia, we don't modify return values at all. If a key's value
// is `undefined`, we pass that value to the callback function.
Expand All @@ -144,7 +163,7 @@ class LocalForageCacheDriver {
if (this._useLocalCacheByDefault && this._localCache[key]) {
// Logger.debug('Local cache fallback for GET', key)()
return new Promise((resolve, reject) => {
const value = typeof this._localCache[key] !== 'undefined' ? this._localCache[key] : null
const value = this.getLocalCache(key)
if (isCallbackCallable) callback(null, value)
resolve(value)
})
Expand All @@ -165,46 +184,50 @@ class LocalForageCacheDriver {
// Logger.debug('No local cache fallback for GET', key)()
const promise = this._localForageCollection.ready().then(() => this._localForageCollection.getItem(key).then(result => {
const endTime = new Date().getTime()
const clonedResult = cloneDeep(result)
if ((endTime - startTime) >= CACHE_TIMEOUT) {
Logger.error('Cache promise resolved after [ms]' + key + (endTime - startTime))()
}
if (!this._localCache[key] && result) {
this._localCache[key] = result // populate the local cache for the next call
if (!this._localCache[key] && clonedResult) {
this._localCache[key] = clonedResult // populate the local cache for the next call
}
if (!isResolved) {
if (isCallbackCallable) {
callback(null, result)
callback(null, clonedResult)
}
isResolved = true
} else {
Logger.debug('Skipping return value as it was previously resolved')()
}
return result
return clonedResult
}).catch(err => {
this._lastError = err
if (!isResolved) {
if (isCallbackCallable) callback(null, typeof this._localCache[key] !== 'undefined' ? this._localCache[key] : null)
const value = this.getLocalCache(key)
if (isCallbackCallable) callback(null, value)
}
Logger.error(err)()
isResolved = true
}))

setTimeout(() => {
clearTimeout(this._cacheTimeouts.getItem)
this._cacheTimeouts.getItem = setTimeout(() => {
if (!isResolved) { // this is cache time out check
if (!this._persistenceErrorNotified) {
Logger.error('Cache not responding for ' + key + '.', 'cache', { timeout: CACHE_TIMEOUT, errorsCount: this.cacheErrorsCount[this._collectionName] })()
this._persistenceErrorNotified = true
this.recreateDb()
}
this.cacheErrorsCount[this._collectionName] = this.cacheErrorsCount[this._collectionName] ? this.cacheErrorsCount[this._collectionName] + 1 : 1
if (isCallbackCallable) callback(null, typeof this._localCache[key] !== 'undefined' ? this._localCache[key] : null)
const value = this.getLocalCache(key)
if (isCallbackCallable) callback(null, value)
}
}, CACHE_TIMEOUT)
return promise
}
} else {
return new Promise((resolve, reject) => {
const value = typeof this._localCache[key] !== 'undefined' ? this._localCache[key] : null
const value = this.getLocalCache(key)
if (isCallbackCallable) callback(null, value)
resolve(value)
})
Expand Down Expand Up @@ -251,7 +274,8 @@ class LocalForageCacheDriver {
if (isCallbackCallable) callback(err, null)
}
})
setTimeout(() => {
clearTimeout(this._cacheTimeouts.iterate)
this._cacheTimeouts.iterate = setTimeout(() => {
if (!isResolved) { // this is cache time out check
if (!this._persistenceErrorNotified) {
Logger.error('Cache not responding. (iterate)', 'cache', { timeout: CACHE_TIMEOUT, errorsCount: this.cacheErrorsCount[this._collectionName] })()
Expand Down Expand Up @@ -293,7 +317,8 @@ class LocalForageCacheDriver {
// saved, or something like that.
public setItem (key, value, callback?, memoryOnly = false) {
const isCallbackCallable = (typeof callback !== 'undefined' && callback)
this._localCache[key] = value
const copiedValue = cloneDeep(value)
this._localCache[key] = copiedValue
if (memoryOnly) {
return new Promise((resolve, reject) => {
if (isCallbackCallable) callback(null, null)
Expand All @@ -312,7 +337,7 @@ class LocalForageCacheDriver {
})
} else {
let isResolved = false
const promise = this._localForageCollection.ready().then(() => this._localForageCollection.setItem(key, value).then(result => {
const promise = this._localForageCollection.ready().then(() => this._localForageCollection.setItem(key, copiedValue).then(result => {
if (isCallbackCallable) {
callback(null, result)
}
Expand All @@ -322,7 +347,8 @@ class LocalForageCacheDriver {
this._lastError = err
throw err
}))
setTimeout(() => {
clearTimeout(this._cacheTimeouts.iterate)
this._cacheTimeouts.setItem = setTimeout(() => {
if (!isResolved) { // this is cache time out check
if (!this._persistenceErrorNotified) {
Logger.error('Cache not responding for ' + key + '.', 'cache', { timeout: CACHE_TIMEOUT, errorsCount: this.cacheErrorsCount[this._collectionName] })()
Expand Down