From 570057971707e8a67c4f2dce717efb4a1d71b338 Mon Sep 17 00:00:00 2001 From: MohamadRobatjazi Date: Thu, 13 Feb 2025 14:30:16 +0330 Subject: [PATCH 01/12] feat(Repository): creating a new layer for fetching data --- src/bootstrap.js | 7 ++-- src/composables/todo.composable.js | 8 ++-- src/composables/user.composable.js | 8 ++-- .../api.repository.js} | 4 +- src/repositories/base.repository.js | 15 +++++++ .../crud.repository.js} | 39 +++++++------------ .../middleware/AuthenticateUser.js | 2 +- .../todo.repository.js} | 12 +++--- .../user.repository.js} | 12 +++--- src/services/authentication.service.js | 7 ++-- src/services/reserve.service.js | 25 ------------ src/views/UsersView.vue | 2 +- 12 files changed, 61 insertions(+), 80 deletions(-) rename src/{services/api.service.js => repositories/api.repository.js} (98%) create mode 100644 src/repositories/base.repository.js rename src/{services/crud.service.js => repositories/crud.repository.js} (50%) rename src/{services => repositories}/middleware/AuthenticateUser.js (100%) rename src/{services/todo.service.js => repositories/todo.repository.js} (62%) rename src/{services/user.service.js => repositories/user.repository.js} (81%) delete mode 100644 src/services/reserve.service.js diff --git a/src/bootstrap.js b/src/bootstrap.js index f533f24..90da528 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -1,12 +1,13 @@ -import ApiService from '@/services/api.service'; +import ApiService from '@/repositories/api.repository'; + import TokenService from '@/services/token.service'; import ThemeService from '@/services/theme.service'; import LanguageService from '@/services/language.service'; -import AuthenticateUser from '@/services/middleware/AuthenticateUser'; +import AuthenticateUser from '@/repositories/middleware/AuthenticateUser'; -import HttpHeader from '@/enums/HttpHeader'; import MimeType from '@/enums/MimeType'; +import HttpHeader from '@/enums/HttpHeader'; ApiService.setHeader(HttpHeader.CONTENT_TYPE, MimeType.APPLICATION_JSON); ApiService.addResponseMiddleware(AuthenticateUser); diff --git a/src/composables/todo.composable.js b/src/composables/todo.composable.js index d4fbcff..96ff09b 100644 --- a/src/composables/todo.composable.js +++ b/src/composables/todo.composable.js @@ -1,7 +1,7 @@ import { ref, computed } from 'vue'; // Service -import TodoService from '@/services/todo.service'; +import TodoRepository from '@/repositories/todo.repository'; // Composables import { useLoading } from '@/composables/loading.composable'; @@ -22,7 +22,7 @@ export function useFetchTodos() { function fetchTodos(config) { startLoading(); - return TodoService.getAll(config) + return TodoRepository.getAll(config) .then(function (response) { todos.value = response.data; return response; @@ -40,7 +40,7 @@ export function useFetchTodos() { }; } -export function useFetchTodo(initialValue = TodoService.getDefault()) { +export function useFetchTodo(initialValue = TodoRepository.getDefault()) { const { isLoading, startLoading, endLoading } = useLoading(); const todo = ref(initialValue); @@ -48,7 +48,7 @@ export function useFetchTodo(initialValue = TodoService.getDefault()) { function fetchTodoById(id) { startLoading(); - return TodoService.getOneById(id) + return TodoRepository.getOneById(id) .then(function (response) { todo.value = response.data; return response; diff --git a/src/composables/user.composable.js b/src/composables/user.composable.js index 309f1df..cef6e73 100644 --- a/src/composables/user.composable.js +++ b/src/composables/user.composable.js @@ -1,7 +1,7 @@ import { ref, computed } from 'vue'; // Service -import UserService from '@/services/user.service'; +import UserRepository from '@/repositories/user.repository'; // Composables import { useLoading } from '@/composables/loading.composable'; @@ -19,7 +19,7 @@ export function useFetchUsers() { function fetchUsers() { startLoading(); - return UserService.getAll() + return UserRepository.getAll() .then(function (response) { users.value = response.data; return response; @@ -37,7 +37,7 @@ export function useFetchUsers() { }; } -export function useFetchUser(initialValue = UserService.getDefault()) { +export function useFetchUser(initialValue = UserRepository.getDefault()) { const { isLoading, startLoading, endLoading } = useLoading(); const user = ref(initialValue); @@ -45,7 +45,7 @@ export function useFetchUser(initialValue = UserService.getDefault()) { function fetchUserById(id) { startLoading(); - return UserService.getOneById(id) + return UserRepository.getOneById(id) .then(function (response) { user.value = response.data; return response; diff --git a/src/services/api.service.js b/src/repositories/api.repository.js similarity index 98% rename from src/services/api.service.js rename to src/repositories/api.repository.js index 047b07a..b2cc8d7 100644 --- a/src/services/api.service.js +++ b/src/repositories/api.repository.js @@ -17,7 +17,7 @@ const instance = axios.create({ timeout: import.meta.env.VITE_API_TIMEOUT }); -class ApiService { +class ApiRepository { /** * Set header for all or specific http method * @@ -142,4 +142,4 @@ class ApiService { } } -export default ApiService; +export default ApiRepository; diff --git a/src/repositories/base.repository.js b/src/repositories/base.repository.js new file mode 100644 index 0000000..e257ba1 --- /dev/null +++ b/src/repositories/base.repository.js @@ -0,0 +1,15 @@ +class BaseRepository { + /** + * Repository url + * + * @throws {Error} + * @returns {String} + */ + get URL() { + throw new Error( + 'You have to implement the static method "URL", for each class that extend BaseRepositories!' + ); + } +} + +export default BaseRepository; \ No newline at end of file diff --git a/src/services/crud.service.js b/src/repositories/crud.repository.js similarity index 50% rename from src/services/crud.service.js rename to src/repositories/crud.repository.js index d3e062b..e013cd8 100644 --- a/src/services/crud.service.js +++ b/src/repositories/crud.repository.js @@ -1,26 +1,15 @@ -import ApiService from './api.service'; - -class CrudService { - /** - * Service url - * - * @throws {Error} - * @returns {String} - */ - static get URL() { - throw new Error( - 'You have to implement the static method "URL", for each class that extend CrudServices!' - ); - } +import ApiRepository from './api.repository'; +import BaseRepository from './base.repository'; +class CrudRepository extends BaseRepository { /** * Get items * * @param {AxiosRequestConfig} [config] * @returns {Promise} */ - static getAll(config) { - return ApiService.get(this.URL, config); + getAll(config) { + return ApiRepository.get(this.URL, config); } /** @@ -30,8 +19,8 @@ class CrudService { * @param {AxiosRequestConfig} [config] * @returns {Promise} */ - static getOneById(id, config) { - return ApiService.get(`${this.URL}/${id}`, config); + getOneById(id, config) { + return ApiRepository.get(`${this.URL}/${id}`, config); } /** @@ -41,8 +30,8 @@ class CrudService { * @param {AxiosRequestConfig} [config] * @returns {Promise} */ - static create(data, config) { - return ApiService.post(`${this.URL}/create`, data, config); + create(data, config) { + return ApiRepository.post(`${this.URL}/create`, data, config); } /** @@ -53,8 +42,8 @@ class CrudService { * @param {AxiosRequestConfig} [config] * @returns {Promise} */ - static update(id, data, config) { - return ApiService.post(`${this.URL}/${id}/update`, data, config); + update(id, data, config) { + return ApiRepository.post(`${this.URL}/${id}/update`, data, config); } /** @@ -65,9 +54,9 @@ class CrudService { * @param {AxiosRequestConfig} [config] * @returns {Promise} */ - static delete(id, data, config) { - return ApiService.post(`${this.URL}/${id}/delete`, data, config); + delete(id, data, config) { + return ApiRepository.post(`${this.URL}/${id}/delete`, data, config); } } -export default CrudService; +export default CrudRepository; \ No newline at end of file diff --git a/src/services/middleware/AuthenticateUser.js b/src/repositories/middleware/AuthenticateUser.js similarity index 100% rename from src/services/middleware/AuthenticateUser.js rename to src/repositories/middleware/AuthenticateUser.js index e88d99b..440eb59 100644 --- a/src/services/middleware/AuthenticateUser.js +++ b/src/repositories/middleware/AuthenticateUser.js @@ -1,6 +1,6 @@ import AuthenticationService from '@/services/authentication.service'; -import router from '@/router/index'; +import router from '@/router/index'; import HttpStatusCode from '@/enums/HttpStatusCode'; /** diff --git a/src/services/todo.service.js b/src/repositories/todo.repository.js similarity index 62% rename from src/services/todo.service.js rename to src/repositories/todo.repository.js index 503dc62..762ec4b 100644 --- a/src/services/todo.service.js +++ b/src/repositories/todo.repository.js @@ -1,12 +1,12 @@ -import CrudService from './crud.service'; +import CrudRepository from './crud.repository'; -class TodoService extends CrudService { +class TodoRepository extends CrudRepository { /** - * Service url + * Repository url * * @returns {String} */ - static get URL() { + get URL() { return 'todos'; } @@ -15,7 +15,7 @@ class TodoService extends CrudService { * * @returns {Object} */ - static getDefault() { + getDefault() { return { userId: undefined, id: undefined, @@ -25,4 +25,4 @@ class TodoService extends CrudService { } } -export default TodoService; +export default new TodoRepository(); \ No newline at end of file diff --git a/src/services/user.service.js b/src/repositories/user.repository.js similarity index 81% rename from src/services/user.service.js rename to src/repositories/user.repository.js index a96ac53..ca6c1f9 100644 --- a/src/services/user.service.js +++ b/src/repositories/user.repository.js @@ -1,12 +1,12 @@ -import CrudService from './crud.service'; +import CrudRepository from './crud.repository'; -class UserService extends CrudService { +class UserRepository extends CrudRepository { /** - * Service url + * Repository url * * @returns {String} */ - static get URL() { + get URL() { return 'users'; } @@ -15,7 +15,7 @@ class UserService extends CrudService { * * @returns {Object} */ - static getDefault() { + getDefault() { return { id: undefined, name: undefined, @@ -42,4 +42,4 @@ class UserService extends CrudService { } } -export default UserService; +export default new UserRepository(); \ No newline at end of file diff --git a/src/services/authentication.service.js b/src/services/authentication.service.js index d4258fd..755c4ce 100644 --- a/src/services/authentication.service.js +++ b/src/services/authentication.service.js @@ -1,4 +1,5 @@ -import ApiService from './api.service'; +import ApiRepository from '@/repositories/api.repository'; + import TokenService from './token.service'; import PermissionService from './permission.service'; @@ -14,12 +15,12 @@ class AuthenticationService { * @returns {Promise} */ static login(data, config) { - return ApiService.post('users', data, config).then((response) => { + return ApiRepository.post('users', data, config).then((response) => { const token = btoa(JSON.stringify(response.data)); TokenService.set(token); PermissionService.set(['dashboard']); - ApiService.setHeader(HttpHeader.AUTHORIZATION, `Bearer ${TokenService.get()}`); + ApiRepository.setHeader(HttpHeader.AUTHORIZATION, `Bearer ${TokenService.get()}`); return response; }); diff --git a/src/services/reserve.service.js b/src/services/reserve.service.js deleted file mode 100644 index 48c07f2..0000000 --- a/src/services/reserve.service.js +++ /dev/null @@ -1,25 +0,0 @@ -import ApiService from './api.service'; - -class ReserveService { - /** - * Service url - * - * @returns {String} - */ - static get URL() { - return 'reserves'; - } - - /** - * Get reserve by confirmation code - * - * @param {Number|String} confirmationCode - * @param {AxiosRequestConfig} [config] - * @returns {Promise} - */ - static getOneByConfirmationCode(confirmationCode, config) { - return ApiService.get(`${this.URL}/${confirmationCode}`, config); - } -} - -export default ReserveService; diff --git a/src/views/UsersView.vue b/src/views/UsersView.vue index d73d2c6..ebf414a 100644 --- a/src/views/UsersView.vue +++ b/src/views/UsersView.vue @@ -39,5 +39,5 @@ users }; } - } + }; From 7d610dfe715388e7877954b7c511a9cb7eb56acd Mon Sep 17 00:00:00 2001 From: MohamadRobatjazi Date: Fri, 14 Feb 2025 12:38:28 +0330 Subject: [PATCH 02/12] feat(Service): creating local and session storage services --- src/services/language.service.js | 6 +-- ...ge.service.js => local-storage.service.js} | 12 ++--- src/services/permission.service.js | 8 ++-- src/services/session-storage.service.js | 46 +++++++++++++++++++ src/services/theme.service.js | 6 +-- src/services/token.service.js | 10 ++-- 6 files changed, 67 insertions(+), 21 deletions(-) rename src/services/{storage.service.js => local-storage.service.js} (82%) create mode 100644 src/services/session-storage.service.js diff --git a/src/services/language.service.js b/src/services/language.service.js index 5da1481..ebfa7c8 100644 --- a/src/services/language.service.js +++ b/src/services/language.service.js @@ -1,4 +1,4 @@ -import StorageService from './storage.service'; +import LocalStorageService from './local-storage.service'; import { createI18n as createVueI18N } from 'vue-i18n'; @@ -31,7 +31,7 @@ class LanguageService { * @returns void */ static set(value) { - StorageService.set(this.STORAGE_KEY, value); + LocalStorageService.set(this.STORAGE_KEY, value); location.reload(); } @@ -41,7 +41,7 @@ class LanguageService { * @returns {String} */ static get() { - return StorageService.get(this.STORAGE_KEY) ?? this.DEFAULT; + return LocalStorageService.get(this.STORAGE_KEY) ?? this.DEFAULT; } /** diff --git a/src/services/storage.service.js b/src/services/local-storage.service.js similarity index 82% rename from src/services/storage.service.js rename to src/services/local-storage.service.js index a784bf7..3db5e3b 100644 --- a/src/services/storage.service.js +++ b/src/services/local-storage.service.js @@ -1,4 +1,4 @@ -class StorageService { +class LocalStorageService { /** * Set an item * @@ -6,7 +6,7 @@ class StorageService { * @param {*} value * @returns void */ - static set(name, value) { + set(name, value) { const stringifyValue = JSON.stringify(value); localStorage.setItem(name, stringifyValue); } @@ -17,7 +17,7 @@ class StorageService { * @param {String} name * @returns {*} */ - static get(name) { + get(name) { const value = localStorage.getItem(name); return value ? JSON.parse(value) : undefined; } @@ -28,7 +28,7 @@ class StorageService { * @param {String} name * @returns void */ - static delete(name) { + delete(name) { localStorage.removeItem(name); } @@ -38,9 +38,9 @@ class StorageService { * @param {String} name * @returns {Boolean} */ - static has(name) { + has(name) { return Boolean(this.get(name)); } } -export default StorageService; +export default new LocalStorageService(); \ No newline at end of file diff --git a/src/services/permission.service.js b/src/services/permission.service.js index c62e10f..ae1f526 100644 --- a/src/services/permission.service.js +++ b/src/services/permission.service.js @@ -1,11 +1,11 @@ -import StorageService from './storage.service'; +import LocalStorageService from './local-storage.service'; class PermissionService { /** * @private * @type {Object} */ - static _permissions = StorageService.get(this.STORAGE_KEY) || {}; + static _permissions = LocalStorageService.get(this.STORAGE_KEY) || {}; /** * Storage key @@ -27,7 +27,7 @@ class PermissionService { this._permissions[permission] = true; } - StorageService.set(this.STORAGE_KEY, this._permissions); + LocalStorageService.set(this.STORAGE_KEY, this._permissions); } /** @@ -67,7 +67,7 @@ class PermissionService { */ static clear() { this._permissions = {}; - StorageService.delete(this.STORAGE_KEY); + LocalStorageService.delete(this.STORAGE_KEY); } } diff --git a/src/services/session-storage.service.js b/src/services/session-storage.service.js new file mode 100644 index 0000000..48fa5bc --- /dev/null +++ b/src/services/session-storage.service.js @@ -0,0 +1,46 @@ +class SessionStorageService { + /** + * Set an item + * + * @param {String} name + * @param {*} value + * @returns void + */ + set(name, value) { + const stringifyValue = JSON.stringify(value); + sessionStorage.setItem(name, stringifyValue); + } + + /** + * Get an item + * + * @param {String} name + * @returns {*} + */ + get(name) { + const value = sessionStorage.getItem(name); + return value ? JSON.parse(value) : undefined; + } + + /** + * Delete an item + * + * @param {String} name + * @returns void + */ + delete(name) { + sessionStorage.removeItem(name); + } + + /** + * Determine if an item exists + * + * @param {String} name + * @returns {Boolean} + */ + has(name) { + return Boolean(this.get(name)); + } +} + +export default new SessionStorageService(); \ No newline at end of file diff --git a/src/services/theme.service.js b/src/services/theme.service.js index 1c2ca34..fcffeb3 100644 --- a/src/services/theme.service.js +++ b/src/services/theme.service.js @@ -1,4 +1,4 @@ -import StorageService from './storage.service'; +import LocalStorageService from './local-storage.service'; class ThemeService { /** @@ -26,7 +26,7 @@ class ThemeService { * @returns void */ static set(value) { - StorageService.set(this.STORAGE_KEY, value); + LocalStorageService.set(this.STORAGE_KEY, value); this.updateDOM(); } @@ -46,7 +46,7 @@ class ThemeService { * @returns {String} */ static get() { - return StorageService.get(this.STORAGE_KEY) ?? this.DEFAULT; + return LocalStorageService.get(this.STORAGE_KEY) ?? this.DEFAULT; } } diff --git a/src/services/token.service.js b/src/services/token.service.js index 54a1300..3d7f450 100644 --- a/src/services/token.service.js +++ b/src/services/token.service.js @@ -1,4 +1,4 @@ -import StorageService from './storage.service'; +import LocalStorageService from './local-storage.service'; class TokenService { /** @@ -17,7 +17,7 @@ class TokenService { * @returns void */ static set(value) { - StorageService.set(this.STORAGE_KEY, value); + LocalStorageService.set(this.STORAGE_KEY, value); } /** @@ -26,7 +26,7 @@ class TokenService { * @returns {String} */ static get() { - return StorageService.get(this.STORAGE_KEY); + return LocalStorageService.get(this.STORAGE_KEY); } /** @@ -35,7 +35,7 @@ class TokenService { * @returns {Boolean} */ static isExist() { - return StorageService.has(this.STORAGE_KEY); + return LocalStorageService.has(this.STORAGE_KEY); } /** @@ -44,7 +44,7 @@ class TokenService { * @returns void */ static clear() { - StorageService.delete(this.STORAGE_KEY); + LocalStorageService.delete(this.STORAGE_KEY); } } From 439dc745c187c9b39c254ef2c47cebaba166e10a Mon Sep 17 00:00:00 2001 From: MohamadRobatjazi Date: Sat, 15 Feb 2025 21:07:15 +0330 Subject: [PATCH 03/12] feat(Strategy): create base strategy --- src/repositories/strategy/BaseStrategy.js | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/repositories/strategy/BaseStrategy.js diff --git a/src/repositories/strategy/BaseStrategy.js b/src/repositories/strategy/BaseStrategy.js new file mode 100644 index 0000000..3f2a299 --- /dev/null +++ b/src/repositories/strategy/BaseStrategy.js @@ -0,0 +1,67 @@ +// Utils +import { isEmptyObject } from '@/utils'; + +// Services +import LocalStorageService from '@/services/local-storage.service'; + +class BaseStrategy { + /** + * @type {Object} + * @private + */ + _cache; + + /** + * @type {String} + * @private + */ + _cacheTag; + + /** + * @type {Object} + * @private + */ + _driver; + + constructor(cacheTag = 'global', driver = LocalStorageService) { + this._cacheTag = cacheTag; + this._driver = driver; + + this._cache = this._getCache() ?? {}; + } + + /** + * Retrieves cache from the storage driver. + * + * @private + * @returns {Object|null} + */ + _getCache() { + return this._driver.get(this._cacheTag); + } + + /** + * Updates the cache in storage. If cache is empty, it clears storage. + * + * @private + */ + _setCache() { + if (isEmptyObject(this._cache)) { + this._clearCache(); + return; + } + + this._driver.set(this._cacheTag, this._cache); + } + + /** + * Clears the cache from storage. + * + * @private + */ + _clearCache() { + this._driver.delete(this._cacheTag); + } +} + +export default BaseStrategy; \ No newline at end of file From 5015b44f97e68c2a18f9b2f1f6f892833a84d6fd Mon Sep 17 00:00:00 2001 From: MohamadRobatjazi Date: Sat, 15 Feb 2025 21:07:44 +0330 Subject: [PATCH 04/12] feat(Strategy): create "CacheFirst" strategy --- .../strategy/CacheFirstStrategy.js | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/repositories/strategy/CacheFirstStrategy.js diff --git a/src/repositories/strategy/CacheFirstStrategy.js b/src/repositories/strategy/CacheFirstStrategy.js new file mode 100644 index 0000000..a6c06b5 --- /dev/null +++ b/src/repositories/strategy/CacheFirstStrategy.js @@ -0,0 +1,90 @@ +import BaseStrategy from './BaseStrategy'; + +class CacheFirstStrategy extends BaseStrategy { + /** + * Cache time-to-live (in milliseconds) + * + * @type {Number} + * @private + */ + _ttl; + + constructor(cacheTag, driver, ttl = 60_000) { + super(cacheTag, driver); + + this._ttl = ttl; + } + + /** + * Stores a new value in the cache. + * + * @param {String} key + * @param {*} value + */ + put(key, value) { + this._cache[key] = { + value, + expire_at: Date.now() + this._ttl + }; + + this._setCache(); + } + + /** + * Retrieves the stored value or fetches a new value using the callback. + * + * @param {String} key + * @param {Function} callback + * @returns {Promise<*>} + */ + get(key, callback) { + const data = this._cache[key]; + + if (data !== undefined) { + const { value, expire_at } = data; + + if (Date.now() < expire_at) { + return Promise.resolve(value); + } else { + this.delete(key); + } + } + + return new Promise((resolve, reject) => { + callback().then((response) => { + this.put(key, response); + resolve(response); + }).catch(reject); + }); + } + + /** + * Checks if a specific value exists in the cache. + * + * @param {String} key + * @returns {Boolean} + */ + has(key) { + return Boolean(this._cache[key]); + } + + /** + * Deletes a specific value from the cache. + * + * @param {String} key + */ + delete(key) { + delete this._cache[key]; + this._setCache(); + } + + /** + * Clears all values from the cache. + */ + clear() { + this._cache = {}; + this._clearCache(); + } +} + +export default CacheFirstStrategy; \ No newline at end of file From 1ab19179d4419869fbbf9cb08094bb19f917d7b6 Mon Sep 17 00:00:00 2001 From: MohamadRobatjazi Date: Sat, 15 Feb 2025 21:26:34 +0330 Subject: [PATCH 05/12] refactor(Strategy): improve code --- src/repositories/strategy/BaseStrategy.js | 5 +---- src/repositories/strategy/CacheFirstStrategy.js | 5 ++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/repositories/strategy/BaseStrategy.js b/src/repositories/strategy/BaseStrategy.js index 3f2a299..1ae23e2 100644 --- a/src/repositories/strategy/BaseStrategy.js +++ b/src/repositories/strategy/BaseStrategy.js @@ -1,9 +1,6 @@ // Utils import { isEmptyObject } from '@/utils'; -// Services -import LocalStorageService from '@/services/local-storage.service'; - class BaseStrategy { /** * @type {Object} @@ -23,7 +20,7 @@ class BaseStrategy { */ _driver; - constructor(cacheTag = 'global', driver = LocalStorageService) { + constructor(cacheTag, driver) { this._cacheTag = cacheTag; this._driver = driver; diff --git a/src/repositories/strategy/CacheFirstStrategy.js b/src/repositories/strategy/CacheFirstStrategy.js index a6c06b5..ef59ca4 100644 --- a/src/repositories/strategy/CacheFirstStrategy.js +++ b/src/repositories/strategy/CacheFirstStrategy.js @@ -1,5 +1,8 @@ import BaseStrategy from './BaseStrategy'; +// Services +import LocalStorageService from '@/services/local-storage.service'; + class CacheFirstStrategy extends BaseStrategy { /** * Cache time-to-live (in milliseconds) @@ -9,7 +12,7 @@ class CacheFirstStrategy extends BaseStrategy { */ _ttl; - constructor(cacheTag, driver, ttl = 60_000) { + constructor(cacheTag = 'global', driver = LocalStorageService, ttl = 60_000) { super(cacheTag, driver); this._ttl = ttl; From 89e3bbdfa15240a139b2b6855ff6e8577f14e4aa Mon Sep 17 00:00:00 2001 From: MohamadRobatjazi Date: Sat, 15 Feb 2025 21:37:22 +0330 Subject: [PATCH 06/12] feat(Strategy): create "NetworkFirst" strategy --- .../strategy/NetworkFirstStrategy.js | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/repositories/strategy/NetworkFirstStrategy.js diff --git a/src/repositories/strategy/NetworkFirstStrategy.js b/src/repositories/strategy/NetworkFirstStrategy.js new file mode 100644 index 0000000..31bc589 --- /dev/null +++ b/src/repositories/strategy/NetworkFirstStrategy.js @@ -0,0 +1,96 @@ +import BaseStrategy from './BaseStrategy'; + +// Services +import LocalStorageService from '@/services/local-storage.service'; + +class NetworkFirstStrategy extends BaseStrategy { + /** + * Cache time-to-live (in milliseconds) + * + * @type {Number} + * @private + */ + _ttl; + + constructor(cacheTag = 'global', driver = LocalStorageService, ttl = 60_000) { + super(cacheTag, driver); + + this._ttl = ttl; + } + + /** + * Stores a new value in the cache. + * + * @param {String} key + * @param {*} value + */ + put(key, value) { + this._cache[key] = { + value, + expire_at: Date.now() + this._ttl + }; + + this._setCache(); + } + + /** + * Attempts to fetch data from the network first, falling back to cache if network fails. + * + * @param {String} key + * @param {Function} callback + * @returns {Promise<*>} + */ + get(key, callback) { + const data = this._cache[key]; + + return new Promise((resolve, reject) => { + callback().then((response) => { + this.put(key, response); + resolve(response); + }).catch((reason) => { + if (data !== undefined) { + const { value, expire_at } = data; + + if (Date.now() < expire_at) { + resolve(value); + } else { + this.delete(key); + reject(reason); + } + } else { + reject(reason); + } + }); + }); + } + + /** + * Checks if a specific value exists in the cache. + * + * @param {String} key + * @returns {Boolean} + */ + has(key) { + return Boolean(this._cache[key]); + } + + /** + * Deletes a specific value from the cache. + * + * @param {String} key + */ + delete(key) { + delete this._cache[key]; + this._setCache(); + } + + /** + * Clears all values from the cache. + */ + clear() { + this._cache = {}; + this._clearCache(); + } +} + +export default NetworkFirstStrategy; \ No newline at end of file From 430f9ef9822072434816adfd27c8d09487c8515a Mon Sep 17 00:00:00 2001 From: MohamadRobatjazi Date: Sat, 15 Feb 2025 22:22:49 +0330 Subject: [PATCH 07/12] feat(Strategy): create "StaleWhileRevalidate" strategy --- .../strategy/StaleWhileRevalidateStrategy.js | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/repositories/strategy/StaleWhileRevalidateStrategy.js diff --git a/src/repositories/strategy/StaleWhileRevalidateStrategy.js b/src/repositories/strategy/StaleWhileRevalidateStrategy.js new file mode 100644 index 0000000..220072c --- /dev/null +++ b/src/repositories/strategy/StaleWhileRevalidateStrategy.js @@ -0,0 +1,94 @@ +import BaseStrategy from './BaseStrategy'; + +// Services +import LocalStorageService from '@/services/local-storage.service'; + +class StaleWhileRevalidateStrategy extends BaseStrategy { + /** + * Cache time-to-live (in milliseconds) + * + * @type {Number} + * @private + */ + _ttl; + + constructor(cacheTag = 'global', driver = LocalStorageService, ttl = 60_000) { + super(cacheTag, driver); + + this._ttl = ttl; + } + + /** + * Stores a new value in the cache. + * + * @param {String} key + * @param {*} value + */ + put(key, value) { + this._cache[key] = { + value, + expire_at: Date.now() + this._ttl + }; + + this._setCache(); + } + + /** + * Tries to fetch data from the cache first, and simultaneously revalidates the cache by fetching from the network. + * + * @param {String} key + * @param {Function} callback + * @returns {Promise<*>} + */ + get(key, callback) { + const data = this._cache[key]; + + if (data !== undefined) { + const { value, expire_at } = data; + + if (Date.now() < expire_at) { + callback().then((response) => this.put(key, response)); + return Promise.resolve(value); + } else { + this.delete(key); + } + } + + return new Promise((resolve, reject) => { + callback().then((response) => { + this.put(key, response); + resolve(response); + }).catch(reject); + }); + } + + /** + * Checks if a specific value exists in the cache. + * + * @param {String} key + * @returns {Boolean} + */ + has(key) { + return Boolean(this._cache[key]); + } + + /** + * Deletes a specific value from the cache. + * + * @param {String} key + */ + delete(key) { + delete this._cache[key]; + this._setCache(); + } + + /** + * Clears all values from the cache. + */ + clear() { + this._cache = {}; + this._clearCache(); + } +} + +export default StaleWhileRevalidateStrategy; \ No newline at end of file From 7ef28590fa42f709e5adcd18168e8d21c7be1514 Mon Sep 17 00:00:00 2001 From: MohamadRobatjazi Date: Sat, 15 Feb 2025 22:34:12 +0330 Subject: [PATCH 08/12] refactor(Strategy): refactor and improve js dock --- src/repositories/strategy/BaseStrategy.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/repositories/strategy/BaseStrategy.js b/src/repositories/strategy/BaseStrategy.js index 1ae23e2..da9af02 100644 --- a/src/repositories/strategy/BaseStrategy.js +++ b/src/repositories/strategy/BaseStrategy.js @@ -27,8 +27,10 @@ class BaseStrategy { this._cache = this._getCache() ?? {}; } + // Private + /** - * Retrieves cache from the storage driver. + * Get cache by tag * * @private * @returns {Object|null} @@ -38,9 +40,10 @@ class BaseStrategy { } /** - * Updates the cache in storage. If cache is empty, it clears storage. + * Set cache by tag * * @private + * @returns void */ _setCache() { if (isEmptyObject(this._cache)) { @@ -52,9 +55,10 @@ class BaseStrategy { } /** - * Clears the cache from storage. + * Clear cache by tag * * @private + * @returns void */ _clearCache() { this._driver.delete(this._cacheTag); From 14cf3c403b74ca34eb0aa75c5159ec6fc097f74f Mon Sep 17 00:00:00 2001 From: MohamadRobatjazi Date: Sat, 15 Feb 2025 23:04:37 +0330 Subject: [PATCH 09/12] refactor(CacheFirstStrategy): improve code --- .../strategy/CacheFirstStrategy.js | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/repositories/strategy/CacheFirstStrategy.js b/src/repositories/strategy/CacheFirstStrategy.js index ef59ca4..81c5ecb 100644 --- a/src/repositories/strategy/CacheFirstStrategy.js +++ b/src/repositories/strategy/CacheFirstStrategy.js @@ -5,7 +5,7 @@ import LocalStorageService from '@/services/local-storage.service'; class CacheFirstStrategy extends BaseStrategy { /** - * Cache time-to-live (in milliseconds) + * Cache time-to-live (milliseconds) * * @type {Number} * @private @@ -19,12 +19,17 @@ class CacheFirstStrategy extends BaseStrategy { } /** - * Stores a new value in the cache. + * Store a value in cache * * @param {String} key * @param {*} value + * @returns void */ put(key, value) { + if (value === undefined) { + return; + } + this._cache[key] = { value, expire_at: Date.now() + this._ttl @@ -34,7 +39,7 @@ class CacheFirstStrategy extends BaseStrategy { } /** - * Retrieves the stored value or fetches a new value using the callback. + * Retrieve value from cache or fetch new data * * @param {String} key * @param {Function} callback @@ -53,6 +58,10 @@ class CacheFirstStrategy extends BaseStrategy { } } + if (typeof callback !== 'function') { + return Promise.reject(new Error('Callback must be a function')); + } + return new Promise((resolve, reject) => { callback().then((response) => { this.put(key, response); @@ -62,27 +71,33 @@ class CacheFirstStrategy extends BaseStrategy { } /** - * Checks if a specific value exists in the cache. + * Check if a valid value exists in cache * * @param {String} key * @returns {Boolean} */ has(key) { - return Boolean(this._cache[key]); + const data = this._cache[key]; + return data !== undefined && Date.now() < data.expire_at; } /** - * Deletes a specific value from the cache. + * Remove a value from cache * * @param {String} key + * @returns void */ delete(key) { - delete this._cache[key]; - this._setCache(); + if (this._cache[key]) { + delete this._cache[key]; + this._setCache(); + } } /** - * Clears all values from the cache. + * Clear all cached data + * + * @returns void */ clear() { this._cache = {}; From 4a48a3982c61c3b0d9a720906db36f576fca0c1a Mon Sep 17 00:00:00 2001 From: MohamadRobatjazi Date: Sat, 15 Feb 2025 23:11:21 +0330 Subject: [PATCH 10/12] refactor(NetworkFirstStrategy): improve code --- .../strategy/NetworkFirstStrategy.js | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/repositories/strategy/NetworkFirstStrategy.js b/src/repositories/strategy/NetworkFirstStrategy.js index 31bc589..635726a 100644 --- a/src/repositories/strategy/NetworkFirstStrategy.js +++ b/src/repositories/strategy/NetworkFirstStrategy.js @@ -5,7 +5,7 @@ import LocalStorageService from '@/services/local-storage.service'; class NetworkFirstStrategy extends BaseStrategy { /** - * Cache time-to-live (in milliseconds) + * Cache time-to-live (milliseconds) * * @type {Number} * @private @@ -19,12 +19,17 @@ class NetworkFirstStrategy extends BaseStrategy { } /** - * Stores a new value in the cache. + * Store a value in cache * * @param {String} key * @param {*} value + * @returns void */ put(key, value) { + if (value === undefined) { + return; + } + this._cache[key] = { value, expire_at: Date.now() + this._ttl @@ -34,13 +39,17 @@ class NetworkFirstStrategy extends BaseStrategy { } /** - * Attempts to fetch data from the network first, falling back to cache if network fails. + * Tries network first, falls back to cache if failed. * * @param {String} key * @param {Function} callback * @returns {Promise<*>} */ get(key, callback) { + if (typeof callback !== 'function') { + return Promise.reject(new Error('Callback must be a function')); + } + const data = this._cache[key]; return new Promise((resolve, reject) => { @@ -55,37 +64,42 @@ class NetworkFirstStrategy extends BaseStrategy { resolve(value); } else { this.delete(key); - reject(reason); } - } else { - reject(reason); } + + reject(reason); }); }); } /** - * Checks if a specific value exists in the cache. + * Check if a valid value exists in cache * * @param {String} key * @returns {Boolean} */ has(key) { - return Boolean(this._cache[key]); + const data = this._cache[key]; + return data !== undefined && Date.now() < data.expire_at; } /** - * Deletes a specific value from the cache. + * Remove a value from cache * * @param {String} key + * @returns void */ delete(key) { - delete this._cache[key]; - this._setCache(); + if (this._cache[key]) { + delete this._cache[key]; + this._setCache(); + } } /** - * Clears all values from the cache. + * Clear all cached data + * + * @returns void */ clear() { this._cache = {}; From 69d240009c7f63b9ff4bec752d77f9b002250970 Mon Sep 17 00:00:00 2001 From: MohamadRobatjazi Date: Mon, 17 Feb 2025 11:17:52 +0330 Subject: [PATCH 11/12] refactor(BaseStrategy): refactor and improve code --- src/repositories/strategy/BaseStrategy.js | 50 ++++++++++++++++++----- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/repositories/strategy/BaseStrategy.js b/src/repositories/strategy/BaseStrategy.js index da9af02..e4f8bf3 100644 --- a/src/repositories/strategy/BaseStrategy.js +++ b/src/repositories/strategy/BaseStrategy.js @@ -1,6 +1,15 @@ // Utils import { isEmptyObject } from '@/utils'; +// Services +import LocalStorageService from '@/services/local-storage.service'; + +const Default = { + ttl: 60_000, + driver: LocalStorageService, + cacheTag: 'global' +}; + class BaseStrategy { /** * @type {Object} @@ -9,10 +18,10 @@ class BaseStrategy { _cache; /** - * @type {String} + * @type {Number} * @private */ - _cacheTag; + _ttl; /** * @type {Object} @@ -20,32 +29,51 @@ class BaseStrategy { */ _driver; - constructor(cacheTag, driver) { - this._cacheTag = cacheTag; + /** + * @type {String} + * @private + */ + _cacheTag; + + /** + * @param {Object} config + * @param {Number} [config.ttl] + * @param {Object} [config.driver] + * @param {String} [config.cacheTag] + */ + constructor(config) { + // Todo => checking the type of configurations + const { ttl, driver, cacheTag } = { + ...Default, + ...(typeof config === 'object' ? config : {}) + }; + + this._ttl = ttl; this._driver = driver; + this._cacheTag = cacheTag; - this._cache = this._getCache() ?? {}; + this._cache = this._initializeCache(); } // Private /** - * Get cache by tag + * Get cache bu tag * * @private - * @returns {Object|null} + * @returns {*|{}} */ - _getCache() { - return this._driver.get(this._cacheTag); + _initializeCache() { + return this._driver.get(this._cacheTag) ?? {}; } /** - * Set cache by tag + * Save cache by tag * * @private * @returns void */ - _setCache() { + _saveCache() { if (isEmptyObject(this._cache)) { this._clearCache(); return; From a7698dc2f9eb3d8c9c4075e9e28f71ebd13bdd41 Mon Sep 17 00:00:00 2001 From: MohamadRobatjazi Date: Mon, 17 Feb 2025 11:51:27 +0330 Subject: [PATCH 12/12] refactor(Strategy): refactor and improve code --- .../strategy/CacheFirstStrategy.js | 67 +++++++---------- .../strategy/NetworkFirstStrategy.js | 55 +++++++------- .../strategy/StaleWhileRevalidateStrategy.js | 73 ++++++++++--------- 3 files changed, 93 insertions(+), 102 deletions(-) diff --git a/src/repositories/strategy/CacheFirstStrategy.js b/src/repositories/strategy/CacheFirstStrategy.js index 81c5ecb..e5705d2 100644 --- a/src/repositories/strategy/CacheFirstStrategy.js +++ b/src/repositories/strategy/CacheFirstStrategy.js @@ -1,32 +1,25 @@ import BaseStrategy from './BaseStrategy'; -// Services -import LocalStorageService from '@/services/local-storage.service'; - class CacheFirstStrategy extends BaseStrategy { /** - * Cache time-to-live (milliseconds) - * - * @type {Number} - * @private + * @param {Object} config + * @param {Number} [config.ttl] + * @param {Object} [config.driver] + * @param {String} [config.cacheTag] */ - _ttl; - - constructor(cacheTag = 'global', driver = LocalStorageService, ttl = 60_000) { - super(cacheTag, driver); - - this._ttl = ttl; + constructor(config) { + super(config); } /** - * Store a value in cache + * Store a value by cache key * * @param {String} key * @param {*} value - * @returns void + * @return void */ put(key, value) { - if (value === undefined) { + if (value === null || value === undefined) { return; } @@ -35,34 +28,30 @@ class CacheFirstStrategy extends BaseStrategy { expire_at: Date.now() + this._ttl }; - this._setCache(); + this._saveCache(); } /** - * Retrieve value from cache or fetch new data + * Retrieve value from cache by cache key or fetch new data * * @param {String} key * @param {Function} callback - * @returns {Promise<*>} + * @return {Promise<*>} */ get(key, callback) { - const data = this._cache[key]; + return new Promise((resolve, reject) => { + if (this.has(key)) { + resolve(this._cache[key].value); + return; + } - if (data !== undefined) { - const { value, expire_at } = data; + this.delete(key); - if (Date.now() < expire_at) { - return Promise.resolve(value); - } else { - this.delete(key); + if (typeof callback !== 'function') { + reject(new Error('Callback must be a function')); + return; } - } - if (typeof callback !== 'function') { - return Promise.reject(new Error('Callback must be a function')); - } - - return new Promise((resolve, reject) => { callback().then((response) => { this.put(key, response); resolve(response); @@ -71,10 +60,10 @@ class CacheFirstStrategy extends BaseStrategy { } /** - * Check if a valid value exists in cache + * Check if a valid value exists by cache key * * @param {String} key - * @returns {Boolean} + * @return {Boolean} */ has(key) { const data = this._cache[key]; @@ -82,22 +71,22 @@ class CacheFirstStrategy extends BaseStrategy { } /** - * Remove a value from cache + * Remove a value by cache key * * @param {String} key - * @returns void + * @return void */ delete(key) { if (this._cache[key]) { delete this._cache[key]; - this._setCache(); + this._saveCache(); } } /** - * Clear all cached data + * Clear all cached data by tag * - * @returns void + * @return void */ clear() { this._cache = {}; diff --git a/src/repositories/strategy/NetworkFirstStrategy.js b/src/repositories/strategy/NetworkFirstStrategy.js index 635726a..e6338cd 100644 --- a/src/repositories/strategy/NetworkFirstStrategy.js +++ b/src/repositories/strategy/NetworkFirstStrategy.js @@ -1,32 +1,25 @@ import BaseStrategy from './BaseStrategy'; -// Services -import LocalStorageService from '@/services/local-storage.service'; - class NetworkFirstStrategy extends BaseStrategy { /** - * Cache time-to-live (milliseconds) - * - * @type {Number} - * @private + * @param {Object} config + * @param {Number} [config.ttl] + * @param {Object} [config.driver] + * @param {String} [config.cacheTag] */ - _ttl; - - constructor(cacheTag = 'global', driver = LocalStorageService, ttl = 60_000) { - super(cacheTag, driver); - - this._ttl = ttl; + constructor(config) { + super(config); } /** - * Store a value in cache + * Store a value by cache key * * @param {String} key * @param {*} value - * @returns void + * @return void */ put(key, value) { - if (value === undefined) { + if (value === null || value === undefined) { return; } @@ -35,7 +28,7 @@ class NetworkFirstStrategy extends BaseStrategy { expire_at: Date.now() + this._ttl }; - this._setCache(); + this._saveCache(); } /** @@ -43,16 +36,17 @@ class NetworkFirstStrategy extends BaseStrategy { * * @param {String} key * @param {Function} callback - * @returns {Promise<*>} + * @return {Promise<*>} */ get(key, callback) { - if (typeof callback !== 'function') { - return Promise.reject(new Error('Callback must be a function')); - } + return new Promise((resolve, reject) => { + if (typeof callback !== 'function') { + reject(new Error('Callback must be a function')); + return; + } - const data = this._cache[key]; + const data = this._cache[key]; - return new Promise((resolve, reject) => { callback().then((response) => { this.put(key, response); resolve(response); @@ -62,6 +56,7 @@ class NetworkFirstStrategy extends BaseStrategy { if (Date.now() < expire_at) { resolve(value); + return; } else { this.delete(key); } @@ -73,10 +68,10 @@ class NetworkFirstStrategy extends BaseStrategy { } /** - * Check if a valid value exists in cache + * Check if a valid value exists by cache key * * @param {String} key - * @returns {Boolean} + * @return {Boolean} */ has(key) { const data = this._cache[key]; @@ -84,22 +79,22 @@ class NetworkFirstStrategy extends BaseStrategy { } /** - * Remove a value from cache + * Remove a value by cache key * * @param {String} key - * @returns void + * @return void */ delete(key) { if (this._cache[key]) { delete this._cache[key]; - this._setCache(); + this._saveCache(); } } /** - * Clear all cached data + * Clear all cached data by tag * - * @returns void + * @return void */ clear() { this._cache = {}; diff --git a/src/repositories/strategy/StaleWhileRevalidateStrategy.js b/src/repositories/strategy/StaleWhileRevalidateStrategy.js index 220072c..5e30c1c 100644 --- a/src/repositories/strategy/StaleWhileRevalidateStrategy.js +++ b/src/repositories/strategy/StaleWhileRevalidateStrategy.js @@ -1,36 +1,34 @@ import BaseStrategy from './BaseStrategy'; -// Services -import LocalStorageService from '@/services/local-storage.service'; - class StaleWhileRevalidateStrategy extends BaseStrategy { /** - * Cache time-to-live (in milliseconds) - * - * @type {Number} - * @private + * @param {Object} config + * @param {Number} [config.ttl] + * @param {Object} [config.driver] + * @param {String} [config.cacheTag] */ - _ttl; - - constructor(cacheTag = 'global', driver = LocalStorageService, ttl = 60_000) { - super(cacheTag, driver); - - this._ttl = ttl; + constructor(config) { + super(config); } /** - * Stores a new value in the cache. + * Store a value by cache key * * @param {String} key * @param {*} value + * @return void */ put(key, value) { + if (value === null || value === undefined) { + return; + } + this._cache[key] = { value, expire_at: Date.now() + this._ttl }; - this._setCache(); + this._saveCache(); } /** @@ -38,23 +36,26 @@ class StaleWhileRevalidateStrategy extends BaseStrategy { * * @param {String} key * @param {Function} callback - * @returns {Promise<*>} + * @return {Promise<*>} */ get(key, callback) { - const data = this._cache[key]; + return new Promise((resolve, reject) => { + if (this.has(key)) { + callback().then((response) => { + this.put(key, response); + }); - if (data !== undefined) { - const { value, expire_at } = data; + resolve(this._cache[key].value); + return; + } + + this.delete(key); - if (Date.now() < expire_at) { - callback().then((response) => this.put(key, response)); - return Promise.resolve(value); - } else { - this.delete(key); + if (typeof callback !== 'function') { + reject(new Error('Callback must be a function')); + return; } - } - return new Promise((resolve, reject) => { callback().then((response) => { this.put(key, response); resolve(response); @@ -63,27 +64,33 @@ class StaleWhileRevalidateStrategy extends BaseStrategy { } /** - * Checks if a specific value exists in the cache. + * Check if a valid value exists by cache key * * @param {String} key - * @returns {Boolean} + * @return {Boolean} */ has(key) { - return Boolean(this._cache[key]); + const data = this._cache[key]; + return data !== undefined && Date.now() < data.expire_at; } /** - * Deletes a specific value from the cache. + * Remove a value by cache key * * @param {String} key + * @return void */ delete(key) { - delete this._cache[key]; - this._setCache(); + if (this._cache[key]) { + delete this._cache[key]; + this._saveCache(); + } } /** - * Clears all values from the cache. + * Clear all cached data by tag + * + * @return void */ clear() { this._cache = {};