diff --git a/core/app.ts b/core/app.ts index 163c7a51dd..0cde365b6e 100755 --- a/core/app.ts +++ b/core/app.ts @@ -47,7 +47,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 @@ -66,6 +71,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 @@ -78,10 +87,6 @@ const createApp = async (ssrContext, config, storeCode = null): Promise<{app: Vu }) } - Object.keys(coreFilters).forEach(key => { - Vue.filter(key, coreFilters[key]) - }) - let vueOptions = { router: routerProxy, store, diff --git a/core/client-entry.ts b/core/client-entry.ts index 037d6f84c9..3fe088d2a1 100755 --- a/core/client-entry.ts +++ b/core/client-entry.ts @@ -82,7 +82,7 @@ const invokeClientEntry = async () => { } return next() // do not resolve asyncData on server render - already been done } - if (Vue.prototype.$ssrRequestContext) Vue.prototype.$ssrRequestContext.output.cacheTags = new Set() + if (!Vue.prototype.$cacheTags) Vue.prototype.$cacheTags = new Set() const matched = router.getMatchedComponents(to) if (to) { // this is from url if (globalConfig.storeViews.multistore === true) { diff --git a/core/filters/price.js b/core/filters/price.js index eda9d596ae..c69462fb41 100644 --- a/core/filters/price.js +++ b/core/filters/price.js @@ -18,6 +18,9 @@ export function price (value) { return value; } const storeView = currentStoreView(); + if (!storeView.i18n) { + return value; + } const { defaultLocale, currencySign, priceFormat } = storeView.i18n const formattedValue = formatValue(value, defaultLocale); diff --git a/core/lib/store/storage.ts b/core/lib/store/storage.ts index e591731724..4d0bd780ac 100644 --- a/core/lib/store/storage.ts +++ b/core/lib/store/storage.ts @@ -1,6 +1,7 @@ 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 @@ -34,6 +35,13 @@ function roughSizeOfObject (object) { return bytes } +interface CacheTimeouts { + getItem: any, + iterate: any, + setItem: any, + base: any +} + class LocalForageCacheDriver { private _collectionName: string; private _dbName: string; @@ -44,6 +52,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 @@ -54,7 +68,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) @@ -133,6 +148,10 @@ class LocalForageCacheDriver { return this._dbName } + 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. @@ -142,7 +161,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) }) @@ -163,31 +182,33 @@ 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] })() @@ -195,14 +216,15 @@ class LocalForageCacheDriver { 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) }) @@ -249,7 +271,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] })() @@ -291,7 +314,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) @@ -310,7 +334,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) } @@ -320,7 +344,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] })() diff --git a/core/modules/catalog-next/store/category/getters.ts b/core/modules/catalog-next/store/category/getters.ts index 9cff3f2ca5..9b537f9954 100644 --- a/core/modules/catalog-next/store/category/getters.ts +++ b/core/modules/catalog-next/store/category/getters.ts @@ -1,3 +1,4 @@ +import { nonReactiveState } from './index'; import { GetterTree } from 'vuex' import RootState from '@vue-storefront/core/types/RootState' import CategoryState from './CategoryState' @@ -13,12 +14,20 @@ import { Category } from '../../types/Category' import { parseCategoryPath } from '@vue-storefront/core/modules/breadcrumbs/helpers' import { _prepareCategoryPathIds, getSearchOptionsFromRouteParams } from '../../helpers/categoryHelpers'; import { removeStoreCodeFromRoute } from '@vue-storefront/core/lib/multistore' +import cloneDeep from 'lodash-es/cloneDeep' + +function mapCategoryProducts (productsSkus, productsData) { + return productsSkus.map(prodSku => { + const product = productsData.find(prodData => prodData.sku === prodSku) + return cloneDeep(product) + }) +} const getters: GetterTree = { getCategories: (state): Category[] => Object.values(state.categoriesMap), getCategoriesMap: (state): { [id: string]: Category} => state.categoriesMap, getNotFoundCategoryIds: (state): string[] => state.notFoundCategoryIds, - getCategoryProducts: (state) => state.products, + getCategoryProducts: (state) => mapCategoryProducts(state.products, nonReactiveState.products), getCategoryFrom: (state, getters) => (path: string = '') => { return getters.getCategories.find(category => (removeStoreCodeFromRoute(path) as string).replace(/^(\/)/gm, '') === category.url_path) }, diff --git a/core/modules/catalog-next/store/category/index.ts b/core/modules/catalog-next/store/category/index.ts index c01fda5da1..8dd910ea1c 100644 --- a/core/modules/catalog-next/store/category/index.ts +++ b/core/modules/catalog-next/store/category/index.ts @@ -18,3 +18,7 @@ export const categoryModule: Module = { actions, mutations } + +export const nonReactiveState = { + products: [] +} diff --git a/core/modules/catalog-next/store/category/mutations.ts b/core/modules/catalog-next/store/category/mutations.ts index 802b74dee7..d31e527240 100644 --- a/core/modules/catalog-next/store/category/mutations.ts +++ b/core/modules/catalog-next/store/category/mutations.ts @@ -1,12 +1,15 @@ +import { nonReactiveState } from './index'; import Vue from 'vue' import { MutationTree } from 'vuex' import * as types from './mutation-types' import CategoryState from './CategoryState' import { Category } from '../../types/Category' +import cloneDeep from 'lodash-es/cloneDeep' const mutations: MutationTree = { [types.CATEGORY_SET_PRODUCTS] (state, products = []) { - state.products = products + nonReactiveState.products = cloneDeep(products) + state.products = cloneDeep(products).map(prod => prod.sku) }, [types.CATEGORY_ADD_PRODUCTS] (state, products = []) { state.products.push(...products) diff --git a/core/modules/catalog/helpers/search.ts b/core/modules/catalog/helpers/search.ts index 07089ef776..c35d70c8b9 100644 --- a/core/modules/catalog/helpers/search.ts +++ b/core/modules/catalog/helpers/search.ts @@ -57,7 +57,7 @@ export const storeProductToCache = (product, cacheByKey) => { }; export const preConfigureProduct = ({ product, populateRequestCacheTags }) => { - const shouldPopulateCacheTags = populateRequestCacheTags && Vue.prototype.$ssrRequestContext; + const shouldPopulateCacheTags = populateRequestCacheTags && Vue.prototype.$cacheTags; const isFirstVariantAsDefaultInURL = config.products.setFirstVarianAsDefaultInURL && product.hasOwnProperty('configurable_children') && @@ -66,7 +66,7 @@ export const preConfigureProduct = ({ product, populateRequestCacheTags }) => { product.info = {}; if (shouldPopulateCacheTags) { - Vue.prototype.$ssrRequestContext.output.cacheTags.add(`P${product.id}`); + Vue.prototype.$cacheTags.add(`P${product.id}`); } if (!product.parentSku) { diff --git a/core/modules/catalog/store/category/actions.ts b/core/modules/catalog/store/category/actions.ts index d1f0cd8f70..ea228e6476 100644 --- a/core/modules/catalog/store/category/actions.ts +++ b/core/modules/catalog/store/category/actions.ts @@ -119,8 +119,8 @@ const actions: ActionTree = { 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 = [] diff --git a/core/scripts/server.ts b/core/scripts/server.ts index c6652e60b8..1f4b1be3ac 100755 --- a/core/scripts/server.ts +++ b/core/scripts/server.ts @@ -179,7 +179,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) @@ -228,6 +228,9 @@ app.get('*', (req, res, next) => { console.log(`whole request [${req.url}]: ${Date.now() - s}ms`) next() }).catch(errorHandler) + .finally(() => { + ssr.clearContext(context) + }) } const dynamicCacheHandler = () => { diff --git a/core/scripts/utils/ssr-renderer.js b/core/scripts/utils/ssr-renderer.js index 4e1def36e3..d5b3e61b29 100644 --- a/core/scripts/utils/ssr-renderer.js +++ b/core/scripts/utils/ssr-renderer.js @@ -97,7 +97,7 @@ function initSSRRequestContext (app, req, res, config) { filter: (output, context) => { return output }, appendHead: (context) => { return ''; }, template: 'default', - cacheTags: null + cacheTags: new Set() }, server: { app: app, @@ -112,10 +112,17 @@ function initSSRRequestContext (app, req, res, config) { }; } +function clearContext (context) { + Object.keys(context.server).forEach(key => delete context.server[key]) + delete context.output['cacheTags'] + delete context['meta'] +} + module.exports = { createRenderer, initTemplatesCache, initSSRRequestContext, applyAdvancedOutputProcessing, - compileTemplate: compile + compileTemplate: compile, + clearContext } diff --git a/core/server-entry.ts b/core/server-entry.ts index 7c4c5364e4..530bef6ec1 100755 --- a/core/server-entry.ts +++ b/core/server-entry.ts @@ -70,7 +70,6 @@ export default async context => { RouterManager.flushRouteQueue() context.initialState = initialState return new Promise((resolve, reject) => { - context.output.cacheTags = new Set() const meta = (app as any).$meta() router.push(context.url) context.meta = meta diff --git a/package.json b/package.json index a007fa378f..51b43249f4 100755 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "static-server": "cross-env TS_NODE_PROJECT=\"tsconfig-build.json\" ts-node ./core/scripts/static-server.ts", "generate": "cross-env TS_NODE_PROJECT=\"tsconfig-build.json\" ts-node ./core/scripts/generate.ts", "start": "cross-env NODE_ENV=production TS_NODE_PROJECT=\"tsconfig-build.json\" pm2 start ecosystem.json $PM2_ARGS", - "start:inspect": "cross-env NODE_ENV=production TS_NODE_PROJECT=\"tsconfig-build.json\" node --inspect ./core/scripts/server.js", + "start:inspect": "cross-env NODE_ENV=production TS_NODE_PROJECT=\"tsconfig-build.json\" node --inspect -r ts-node/register ./core/scripts/server", "installer": "node ./core/scripts/installer", "installer:ci": "yarn installer --default-config", "all": "cross-env NODE_ENV=development node ./core/scripts/all",