diff --git a/package-lock.json b/package-lock.json index 6c9e001..491e5bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.3.2", "bootstrap": "^5.3.1", "bootstrap-icons": "^1.11.2", + "idb": "^8.0.0", "pasoonate": "^1.2.5", "pinia": "^2.0.28", "vue": "^3.3.4", @@ -1528,6 +1529,12 @@ "node": ">=8" } }, + "node_modules/idb": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz", + "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -3548,6 +3555,11 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "idb": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz", + "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==" + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", diff --git a/package.json b/package.json index 65ebf18..95c4792 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "axios": "^1.3.2", "bootstrap": "^5.3.1", "bootstrap-icons": "^1.11.2", + "idb": "^8.0.0", "pasoonate": "^1.2.5", "pinia": "^2.0.28", "vue": "^3.3.4", diff --git a/src/composables/todo.composable.js b/src/composables/todo.composable.js index d4fbcff..0d5111b 100644 --- a/src/composables/todo.composable.js +++ b/src/composables/todo.composable.js @@ -8,8 +8,22 @@ import { useLoading } from '@/composables/loading.composable'; // Utils import { keyBy } from '@/utils'; - -export function useFetchTodos() { +import StrategiesManager from "@/utils/cache/strategiesManager"; +import CacheStrategies from "@/enums/CacheStrategies"; +import CacheDriverType from "@/enums/CacheDriverType"; +/** + * Hook to fetch all todos with caching options. + * @param {Object} options - Options for caching. + * @param {boolean} [options.cache] - Enable or disable caching. + * @param {"LocalStorage"|"CacheStorage"|"IndexedDB"} [options.cacheType] - Cache type. + * @param {"justCache"|"cacheFirstThenUpdate"} [options.strategy] - Caching strategy. + */ +export function useFetchTodos(options = {}) { + const { + cache = false, + cacheType = CacheDriverType.LOCAL_STORAGE, + strategy = CacheStrategies.CACHE_FIRST_THEN_UPDATE, + } = options; const { isLoading, startLoading, endLoading } = useLoading(); const todos = ref([]); @@ -22,7 +36,7 @@ export function useFetchTodos() { function fetchTodos(config) { startLoading(); - return TodoService.getAll(config) + return TodoService.getAll(config,cache, cacheType, strategy) .then(function (response) { todos.value = response.data; return response; diff --git a/src/composables/user.composable.js b/src/composables/user.composable.js index 309f1df..33cca58 100644 --- a/src/composables/user.composable.js +++ b/src/composables/user.composable.js @@ -8,18 +8,34 @@ import { useLoading } from '@/composables/loading.composable'; // Utils import { keyBy } from '@/utils'; - -export function useFetchUsers() { +import StrategiesManager from "@/utils/cache/strategiesManager"; +import CacheDriverType from "@/enums/CacheDriverType"; +import CacheStrategies from "@/enums/CacheStrategies"; +/** + * Hook to fetch all todos with caching options. + * @param {Object} options - Options for caching. + * @param {boolean} [options.cache=false] - Enable or disable caching. + * @param {"LocalStorage"|"CacheStorage"|"IndexedDB"} [options.cacheType=CacheDriverType.LOCAL_STORAGE] - Cache type. + * @param {"justCache"|"cacheFirstThenUpdate"} [options.strategy=CacheStrategies.CACHE_FIRST_THEN_UPDATE] - Caching strategy. + */ +export function useFetchUsers(options = {}) { + const { + cache = false, + cacheType = CacheDriverType.INDEXED_DB, + strategy = CacheStrategies.CACHE_FIRST_THEN_UPDATE, + } = options; const { isLoading, startLoading, endLoading } = useLoading(); const users = ref([]); const usersKeyById = computed(() => keyBy(users.value, 'id')); - - function fetchUsers() { + /** + * @param {AxiosRequestConfig} [config] + */ + function fetchUsers(config) { startLoading(); - return UserService.getAll() + return UserService.getAll(config, cache, cacheType, strategy) .then(function (response) { users.value = response.data; return response; diff --git a/src/enums/CacheDriverType.js b/src/enums/CacheDriverType.js new file mode 100644 index 0000000..2d4bcf3 --- /dev/null +++ b/src/enums/CacheDriverType.js @@ -0,0 +1,6 @@ +const driverType={ + LOCAL_STORAGE:"LocalStorage", + CACHE_STORAGE:"CacheStorage", + INDEXED_DB:"IndexedDB" +} +export default Object.freeze(driverType); diff --git a/src/enums/CacheStrategies.js b/src/enums/CacheStrategies.js new file mode 100644 index 0000000..5b7ba10 --- /dev/null +++ b/src/enums/CacheStrategies.js @@ -0,0 +1,8 @@ +const strategies = { + JUST_CACHE: 'justCache', + CACHE_FIRST_THEN_UPDATE: 'cacheFirstThenUpdate', +}; + + + +export default Object.freeze(strategies); diff --git a/src/services/api.service.js b/src/services/api.service.js index 047b07a..7cc055b 100644 --- a/src/services/api.service.js +++ b/src/services/api.service.js @@ -2,6 +2,9 @@ import axios from 'axios'; import HttpMethod from '@/enums/HttpMethod'; +import StrategiesManager from "@/utils/cache/strategiesManager" +import CacheManager from "@/utils/cache/cacheManager"; + /** * @callback onFulfilledRequest * @param {AxiosRequestConfig} config @@ -18,6 +21,17 @@ const instance = axios.create({ }); class ApiService { + + /** + * Generate cache key based on URL and query params. + * @param {string} url - The URL. + * @param {Object} [params] - The query parameters. + * @returns {string} Cache key. + */ + static generateCacheKey(url, params = {}) { + const queryParams = new URLSearchParams(params).toString(); + return queryParams ? `${url}?${queryParams}` : url; + } /** * Set header for all or specific http method * @@ -89,10 +103,30 @@ class ApiService { * Custom request * * @param {AxiosRequestConfig} config + * @param {Boolean} cache + * @param {"LocalStorage"|"CacheStorage"|"IndexedDB"} cacheType + * @param {"justCache"|"cacheFirstThenUpdate"} strategy - Caching strategy. * @returns {Promise} */ - static request(config) { - return instance.request(config); + static async request(config,cache,cacheType,strategy) { + const url = config.url + const key = this.generateCacheKey(url, config?.params ?config.params : {}); + if (cache && config.method==="get") { + return StrategiesManager.executeStrategy(strategy, { + key, + cacheType, + url, + config, + axiosInstance: instance, + }); + } + + + const response = await instance.get(url, config); + if (cache && config.method==="get") { + await CacheManager.set(key, response.data, cacheType); + } + return response; } /** @@ -100,10 +134,29 @@ class ApiService { * * @param {String} url * @param {AxiosRequestConfig} [config] + * @param {Boolean} cache + * @param {"LocalStorage"|"CacheStorage"|"IndexedDB"} cacheType + * @param {"justCache"|"cacheFirstThenUpdate"} strategy - Caching strategy. * @returns {Promise} */ - static get(url, config) { - return instance.get(url, config); + static async get(url, config,cache,cacheType,strategy = StrategiesManager.strategies.CACHE_FIRST_THEN_UPDATE) { + const key = this.generateCacheKey(url, config?.params ?config.params : {}); + if (cache) { + return StrategiesManager.executeStrategy(strategy, { + key, + cacheType, + url, + config, + axiosInstance: instance, + }); + } + + + const response = await instance.get(url, config); + if (cache) { + await CacheManager.set(key, response.data, cacheType); + } + return response; } /** diff --git a/src/services/crud.service.js b/src/services/crud.service.js index d3e062b..dbd5ecd 100644 --- a/src/services/crud.service.js +++ b/src/services/crud.service.js @@ -17,10 +17,13 @@ class CrudService { * Get items * * @param {AxiosRequestConfig} [config] + * @param {Boolean} cache + * @param {"LocalStorage"|"CacheStorage"|"IndexedDB"} cacheType + * @param {"justCache"|"cacheFirstThenUpdate"} strategy - Caching strategy. * @returns {Promise} */ - static getAll(config) { - return ApiService.get(this.URL, config); + static getAll(config,cache, cacheType, strategy) { + return ApiService.get(this.URL, config,cache, cacheType, strategy); } /** @@ -28,10 +31,13 @@ class CrudService { * * @param {Number|String} id * @param {AxiosRequestConfig} [config] + * @param {Boolean} cache + * @param {"LocalStorage"|"CacheStorage"|"IndexedDB"} cacheType + * @param {"justCache"|"cacheFirstThenUpdate"} strategy - Caching strategy. * @returns {Promise} */ - static getOneById(id, config) { - return ApiService.get(`${this.URL}/${id}`, config); + static getOneById(id, config,cache, cacheType, strategy) { + return ApiService.get(`${this.URL}/${id}`, config,cache, cacheType, strategy); } /** diff --git a/src/utils/cache/cacheManager.js b/src/utils/cache/cacheManager.js new file mode 100644 index 0000000..b2d7f26 --- /dev/null +++ b/src/utils/cache/cacheManager.js @@ -0,0 +1,70 @@ +import {openDB} from "idb"; +import CacheDriverType from "@/enums/CacheDriverType"; + +class CacheManager { + + + /** + * Get data from the cache. + * @param {string} key - The cache key. + * @param {"LocalStorage"|"CacheStorage"|"IndexedDB"} cacheType - The cache type. + * @returns {Promise} Cached data or null if not found. + */ + static async get(key, cacheType) { + + if (cacheType === CacheDriverType.LOCAL_STORAGE) { + return JSON.parse(localStorage.getItem(key)) || null; + } else if (cacheType === CacheDriverType.CACHE_STORAGE) { + const cache = await caches.open('api-cache'); + const response = await cache.match(key); + return response ? await response.json() : null; + } else if (cacheType === CacheDriverType.INDEXED_DB) { + const db = await this._getIndexedDB(); + const data = await db.get('api-cache', key); + return data || null; + } else { + throw new Error(`Unsupported cache type: ${cacheType}`); + } + } + + /** + * Set data to the cache. + * @param {string} key - The cache key. + * @param {any} data - The data to cache. + * @param {"LocalStorage"|"CacheStorage"|"IndexedDB"} cacheType - The cache type. + * @returns {Promise} + */ + static async set(key, data, cacheType) { + if (cacheType === CacheDriverType.LOCAL_STORAGE) { + localStorage.setItem(key, JSON.stringify(data)); + } else if (cacheType === CacheDriverType.CACHE_STORAGE) { + const cache = await caches.open('api-cache'); + const response = new Response(JSON.stringify(data)); + await cache.put(key, response); + } else if (cacheType === CacheDriverType.INDEXED_DB) { + const db = await this._getIndexedDB(); + await db.put('api-cache', data, key); + } else { + throw new Error(`Unsupported cache type: ${cacheType}`); + } + } + + /** + * Internal method to get IndexedDB instance. + * @returns {Promise} + * @private + */ + static async _getIndexedDB() { + if (!this.dbPromise) { + this.dbPromise = openDB('cache-db', 1, { + upgrade(db) { + if (!db.objectStoreNames.contains('api-cache')) { + db.createObjectStore('api-cache'); + } + }, + }); + } + return this.dbPromise; + } +} + export default CacheManager; diff --git a/src/utils/cache/cacheStorageManager.js b/src/utils/cache/cacheStorageManager.js new file mode 100644 index 0000000..10286c7 --- /dev/null +++ b/src/utils/cache/cacheStorageManager.js @@ -0,0 +1,101 @@ +/** + * CacheStorageManager for managing browser Cache Storage operations. + */ +class CacheStorageManager { + /** @type {string} */ + static cacheName = 'default-cache'; + + /** + * Configures the cache name. + * @param {string} cacheName - The name of the cache. + * @example + * CacheStorageManager.configure('custom-cache'); + */ + static configure(cacheName) { + this.cacheName = cacheName; + } + + /** + * Retrieves the cache instance. + * @returns {Promise} A promise that resolves to the cache instance. + * @private + */ + static async getCache() { + return await caches.open(this.cacheName); + } + + /** + * Sets data in the cache storage. + * @param {string} key - The key to identify the data. + * @param {*} data - The data to be stored (automatically stringified). + * @returns {Promise} A promise that resolves when the data is saved. + * @example + * await CacheStorageManager.set('exampleKey', { value: 42 }); + */ + static async set(key, data) { + const cache = await this.getCache(); + const request = new Request(key); + const response = new Response(JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' }, + }); + await cache.put(request, response); + } + + /** + * Gets data from the cache storage. + * @param {string} key - The key to identify the data. + * @returns {Promise<*>} A promise that resolves to the retrieved data, or null if not found. + * @example + * const data = await CacheStorageManager.get('exampleKey'); + * (data); + */ + static async get(key) { + const cache = await this.getCache(); + const request = new Request(key); + const response = await cache.match(request); + if (!response) return null; + return await response.json(); + } + + /** + * Deletes data from the cache storage. + * @param {string} key - The key to identify the data to be deleted. + * @returns {Promise} A promise that resolves when the data is deleted. + * @example + * await CacheStorageManager.delete('exampleKey'); + */ + static async delete(key) { + const cache = await this.getCache(); + const request = new Request(key); + await cache.delete(request); + } + + /** + * Clears all data from the cache storage. + * @returns {Promise} A promise that resolves when the cache is cleared. + * @example + * await CacheStorageManager.clear(); + */ + static async clear() { + await caches.delete(this.cacheName); + } + + /** + * Retrieves all keys from the cache storage. + * @returns {Promise} A promise that resolves to an array of keys. + * @example + * const keys = await CacheStorageManager.getAllKeys(); + * console.log(keys); + */ + static async getAllKeys() { + const cache = await this.getCache(); + const keys = []; + const requests = await cache.keys(); + for (const request of requests) { + keys.push(new URL(request.url).pathname); // Extracts the key from the URL + } + return keys; + } +} + +export default CacheStorageManager; diff --git a/src/utils/cache/indexedDBManager.js b/src/utils/cache/indexedDBManager.js new file mode 100644 index 0000000..3d93b07 --- /dev/null +++ b/src/utils/cache/indexedDBManager.js @@ -0,0 +1,125 @@ +import { openDB } from 'idb'; + +/** + * IndexedDBManager for managing IndexedDB operations. + */ +class IndexedDBManager { + /** @type {string} */ + static dbName = 'my-app-db'; + + /** @type {string} */ + static storeName = 'api-cache'; + + /** @type {Promise | null} */ + static dbPromise = null; + + /** + * Configures the database and object store names. + * @param {string} dbName - The name of the database. + * @param {string} storeName - The name of the object store. + * @example + * IndexedDBManager.configure('custom-db', 'custom-store'); + */ + static configure(dbName, storeName) { + this.dbName = dbName; + this.storeName = storeName; + } + + /** + * Initializes the IndexedDB database. + * @returns {Promise} A promise that resolves to the database instance. + * @private + */ + static async initDB() { + if (!this.dbPromise) { + this.dbPromise = openDB(this.dbName, 1, { + upgrade(db) { + if (!db.objectStoreNames.contains(IndexedDBManager.storeName)) { + db.createObjectStore(IndexedDBManager.storeName, { keyPath: 'key' }); + } + }, + }); + } + return this.dbPromise; + } + + /** + * Saves data to IndexedDB. + * @param {string} key - The key to identify the data. + * @param {*} data - The data to be saved. + * @returns {Promise} A promise that resolves when the data is saved. + * @example + * await IndexedDBManager.save('exampleKey', { value: 42 }); + */ + static async save(key, data) { + const db = await this.initDB(); + const tx = db.transaction(this.storeName, 'readwrite'); + const store = tx.objectStore(this.storeName); + await store.put({ key, data }); + await tx.done; + } + + /** + * Loads data from IndexedDB. + * @param {string} key - The key to identify the data. + * @returns {Promise<*>} A promise that resolves to the loaded data, or null if not found. + * @example + * const data = await IndexedDBManager.load('exampleKey'); + * console.log(data); + */ + static async load(key) { + const db = await this.initDB(); + const tx = db.transaction(this.storeName, 'readonly'); + const store = tx.objectStore(this.storeName); + const result = await store.get(key); + await tx.done; + return result ? result.data : null; + } + + /** + * Deletes data from IndexedDB. + * @param {string} key - The key to identify the data to be deleted. + * @returns {Promise} A promise that resolves when the data is deleted. + * @example + * await IndexedDBManager.delete('exampleKey'); + */ + static async delete(key) { + const db = await this.initDB(); + const tx = db.transaction(this.storeName, 'readwrite'); + const store = tx.objectStore(this.storeName); + await store.delete(key); + await tx.done; + } + + /** + * Clears all data from the object store. + * @returns {Promise} A promise that resolves when the store is cleared. + * @example + * await IndexedDBManager.clear(); + */ + static async clear() { + const db = await this.initDB(); + const tx = db.transaction(this.storeName, 'readwrite'); + const store = tx.objectStore(this.storeName); + await store.clear(); + await tx.done; + } + + /** + * Retrieves all keys from the object store. + * @returns {Promise} A promise that resolves to an array of keys. + * @example + * const keys = await IndexedDBManager.getAllKeys(); + * console.log(keys); + */ + static async getAllKeys() { + const db = await this.initDB(); + const tx = db.transaction(this.storeName, 'readonly'); + const store = tx.objectStore(this.storeName); + const keys = await store.getAllKeys(); + await tx.done; + return keys; + } +} + +export default IndexedDBManager; diff --git a/src/utils/cache/localStorageManager.js b/src/utils/cache/localStorageManager.js new file mode 100644 index 0000000..5e0222c --- /dev/null +++ b/src/utils/cache/localStorageManager.js @@ -0,0 +1,94 @@ +/** + * LocalStorageManager for managing browser localStorage operations. + */ +class LocalStorageManager { + /** @type {string} */ + static prefix = 'default-'; + + /** + * Configures a prefix for all keys to avoid conflicts. + * @param {string} prefix - The prefix to use for all keys. + * @example + * LocalStorageManager.configure('custom-'); + */ + static configure(prefix) { + this.prefix = prefix; + } + + /** + * Constructs the full key with the prefix. + * @param {string} key - The key to construct. + * @returns {string} The full key with the prefix. + * @private + */ + static constructKey(key) { + return `${this.prefix}${key}`; + } + + /** + * Sets data in localStorage. + * @param {string} key - The key to identify the data. + * @param {*} data - The data to be stored (automatically stringified). + * @example + * LocalStorageManager.set('exampleKey', { value: 42 }); + */ + static set(key, data) { + const fullKey = this.constructKey(key); + localStorage.setItem(fullKey, JSON.stringify(data)); + } + + /** + * Gets data from localStorage. + * @param {string} key - The key to identify the data. + * @returns {*} The retrieved data, or null if not found. + * @example + * const data = LocalStorageManager.get('exampleKey'); + * (data); + */ + static get(key) { + const fullKey = this.constructKey(key); + const storedData = localStorage.getItem(fullKey); + return storedData ? JSON.parse(storedData) : null; + } + + /** + * Deletes data from localStorage. + * @param {string} key - The key to identify the data to be deleted. + * @example + * LocalStorageManager.delete('exampleKey'); + */ + static delete(key) { + const fullKey = this.constructKey(key); + localStorage.removeItem(fullKey); + } + + /** + * Clears all keys with the configured prefix from localStorage. + * @example + * LocalStorageManager.clear(); + */ + static clear() { + const keys = Object.keys(localStorage); + keys.forEach((storedKey) => { + if (storedKey.startsWith(this.prefix)) { + localStorage.removeItem(storedKey); + } + }); + } + + /** + * Retrieves all keys with the configured prefix from localStorage. + * @returns {string[]} An array of keys without the prefix. + * @example + * const keys = LocalStorageManager.getAllKeys(); + * console.log(keys); + */ + static getAllKeys() { + const keys = Object.keys(localStorage); + return keys + .filter((storedKey) => storedKey.startsWith(this.prefix)) + .map((storedKey) => storedKey.replace(this.prefix, '')); + } +} + +export default LocalStorageManager; diff --git a/src/utils/cache/strategiesManager.js b/src/utils/cache/strategiesManager.js new file mode 100644 index 0000000..b6cfa4f --- /dev/null +++ b/src/utils/cache/strategiesManager.js @@ -0,0 +1,78 @@ +import CacheManager from "@/utils/cache/cacheManager"; +import CacheStrategies from "@/enums/CacheStrategies"; +import CacheDriverType from "@/enums/CacheDriverType"; + +class StrategiesManager { + + /** + * Handle the `justCache` strategy. + * @param {string} key - The cache key. + * @param {"LocalStorage"|"CacheStorage"|"IndexedDB"} cacheType - The cache type. + * @returns {Promise} Cached data. + * @throws {Error} If data is not found in the cache. + */ + static async justCache(key, cacheType) { + const cachedData = await CacheManager.get(key, cacheType); + if (cachedData) { + return { data: cachedData, fromCache: true }; + } + throw new Error('Data not found in cache'); + } + + /** + * Handle the `cacheFirstThenUpdate` strategy. + * @param {string} key - The cache key. + * @param {"LocalStorage"|"CacheStorage"|"IndexedDB"} cacheType - The cache type. + * @param {string} url - The URL to fetch data from. + * @param {AxiosRequestConfig} config - Axios config. + * @param {AxiosInstance} axiosInstance - The Axios instance. + * @returns {Promise} Cached data if available, otherwise fetched data. + */ + static async cacheFirstThenUpdate(key, cacheType, url, config, axiosInstance) { + const cachedData = await CacheManager.get(key, cacheType); + if (cachedData) { + // Return cached data immediately + setTimeout(async () => { + try { + const response = await axiosInstance.get(url, config); + await CacheManager.set(key, response.data, cacheType); + } catch (err) { + console.error('Failed to update cache:', err); + } + }, 0); + return { data: cachedData, fromCache: true }; + } + // If no cached data, fetch from API + const response = await axiosInstance.get(url, config); + await CacheManager.set(key, response.data, cacheType); + return response; + } + + /** + * Execute the specified caching strategy. + * @param {string} strategy - The strategy to execute. + * @param {Object} options - The options required for the strategy. + * @returns {Promise} The response data. + */ + static async executeStrategy(strategy, options) { + + switch (strategy) { + case CacheStrategies.JUST_CACHE: + return this.justCache(options.key, options.cacheType); + + case CacheStrategies.CACHE_FIRST_THEN_UPDATE: + return this.cacheFirstThenUpdate( + options.key, + options.cacheType, + options.url, + options.config, + options.axiosInstance + ); + + default: + throw new Error(`Unsupported caching strategy: ${strategy}`); + } + } +} + +export default StrategiesManager diff --git a/src/views/TodosView.vue b/src/views/TodosView.vue index 2124989..eae016d 100644 --- a/src/views/TodosView.vue +++ b/src/views/TodosView.vue @@ -59,6 +59,8 @@ // Stores import { useUserStore } from '@/stores/user.store'; + import CacheDriverType from "@/enums/CacheDriverType"; + import CacheStrategies from "@/enums/CacheStrategies"; export default { name: 'TodosView', @@ -70,7 +72,11 @@ }, setup() { - const { todosLoading, todos, fetchTodos } = useFetchTodos(); + const { todosLoading, todos, fetchTodos } = useFetchTodos({ + cache:true, + cacheType:CacheDriverType.INDEXED_DB, + strategy:CacheStrategies.CACHE_FIRST_THEN_UPDATE + }); const { page, itemsPerPage, paginationParams } = useRoutePagination(); const filters = useApplyFilters({