├── .gitignore ├── .prettierrc.json ├── Qache ├── Node.js ├── Qache.js ├── README.md └── package.json ├── README.md ├── __tests__ └── Qache.test.js ├── package-lock.json ├── package.json └── qache-app ├── .babelrc ├── .gitignore ├── .prettierrc.json ├── client ├── App.tsx ├── components │ ├── demo-app │ │ ├── DemoApp.tsx │ │ ├── LandingPage.tsx │ │ ├── LineGraph.tsx │ │ ├── Navigation.tsx │ │ ├── ProductDetails.tsx │ │ ├── ProductDisplay.tsx │ │ ├── SidebarData.js │ │ ├── SubMenuData.tsx │ │ └── images │ │ │ └── transparentlogowithslogan.png │ └── home │ │ ├── Docs.tsx │ │ ├── Home.tsx │ │ ├── Introduction.tsx │ │ ├── Navbar.tsx │ │ └── Team.tsx ├── images │ ├── demo.gif │ ├── navigate.gif │ ├── preview.gif │ ├── qache.gif │ ├── qache.png │ ├── qacheslogan.png │ └── storage.jpg ├── index.tsx └── styles │ ├── demo-styles │ ├── LandingPage.scss │ ├── Navigation.scss │ ├── ProductDetails.scss │ ├── ProductDisplay.scss │ └── SubMenuData.scss │ ├── home-styles │ ├── Docs.scss │ ├── Introduction.scss │ └── Navbar.scss │ ├── index.css │ ├── index.scss │ └── variables.scss ├── dist ├── bundle.js ├── bundle.js.LICENSE.txt ├── images │ ├── demo.gif │ ├── navigate.gif │ ├── preview.gif │ ├── qache.gif │ ├── qache.png │ └── storage.jpg ├── index.html └── main.css ├── index.d.ts ├── interfaces.ts ├── package-lock.json ├── package.json ├── postcss.config.js ├── server ├── database │ └── db.js ├── models │ ├── CategoryModel.js │ └── ProductModel.js ├── resolvers │ └── resolvers.js ├── server.js └── typeDefs │ └── schema.js ├── tailwind.config.js ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/.vscode 3 | **/*.env 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 80, 7 | "jsxSingleQuote": true, 8 | "useTabs": false, 9 | "useEditorConfig": true, 10 | "htmlWhitespaceSensitivity": "css", 11 | "bracketSpacing": true, 12 | "arrowParens": "always" 13 | } 14 | -------------------------------------------------------------------------------- /Qache/Node.js: -------------------------------------------------------------------------------- 1 | class Node { 2 | constructor(keyRef, value) { 3 | this.keyRef = keyRef; 4 | this.value = value; 5 | this.prev = this.next = null; 6 | this.expires = Infinity; 7 | this.accessCount = 1; 8 | } 9 | } 10 | 11 | module.exports = Node; 12 | -------------------------------------------------------------------------------- /Qache/Qache.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | class Qache { 4 | constructor({ 5 | timeToLive = Infinity, 6 | maxSize = Infinity, 7 | evictionPolicy = 'LRU', 8 | } = {}) { 9 | //set timeToLive in options or default to 10min 10 | this.TTL = timeToLive; //10 minute default timeToLive 11 | this.maxSize = maxSize; // 5 node default maximum size 12 | this.policyType = evictionPolicy; 13 | 14 | this.content = {}; // STORE OF NODES 15 | this.size = 0; // current size of cache 16 | this.tail = this.head = null; // pointers to head(dequeue)/tail(enqueue) of queue 17 | } 18 | 19 | get(key) { 20 | this.cleanUp(key); 21 | return this._getDataFromQueue(key); 22 | } 23 | 24 | set(key, value) { 25 | if (this.content[key]) { 26 | this.update(key, value); 27 | console.log( 28 | `Updated Qache key "${key}" with value ${value}. In the future, use the "update" method.` 29 | ); 30 | } else this._addToQueueAndCache(key, value); 31 | } 32 | 33 | update(key, newValue) { 34 | if (this.content[key]) { 35 | this._refresh(key); 36 | this.content[key].value = newValue; 37 | } else { 38 | this.set(key, newValue); 39 | console.log( 40 | `Set Qache key "${key}" with value ${newValue}. In the future, use the "set" method to adding nodes to the cache.` 41 | ); 42 | } 43 | } 44 | 45 | delete(key) { 46 | if (this.content[key]) this._removeFromQueueAndCache(this.content[key]); 47 | } 48 | 49 | //Creates a list, with a unique key identifier. Option to add items to this list on creation. 50 | // It will be strongly advised to only create a list once it's full and complete content is available for storage. 51 | listCreate(listKey, items) { 52 | if (listKey === undefined) { 53 | console.log('Error, listCreate requires a unique cache key'); 54 | } else { 55 | //Check if a list exists for this key, if not, create one. 56 | if (this.content[listKey] === undefined) { 57 | console.log('adding to queue and cache'); 58 | this._addToQueueAndCache(listKey, []); 59 | } 60 | // for each item given, we push that item into cache, THEN refresh expiration. 61 | items.forEach((item) => this.content[listKey].value.push(item)); 62 | } 63 | } 64 | //Check if list exists, if exists, assumed fresh and complete, returns by range or if no range specified, returns all. 65 | listRange(listKey, start = 0, end = Infinity) { 66 | this.cleanUp(listKey); 67 | 68 | if (this.content[listKey] === undefined) return null; 69 | 70 | const { value } = this.content[listKey]; 71 | this._refresh(listKey); 72 | 73 | return start === 0 && end >= value.length ? value : value.slice(start, end); 74 | } 75 | 76 | //**Update an item if found, push item if not found.** 77 | 78 | listUpsert(item, filterObj, ...listKeys) { 79 | //Remind user that a key is required for this method 80 | if (listKeys === undefined) { 81 | console.log('Error, listPush requires atleast one unique cache key'); 82 | } else { 83 | //Check if a list exists for this key, if not skip 84 | for (const listKey of listKeys) { 85 | if (this.content[listKey] === undefined) { 86 | continue; 87 | } 88 | //We push that item into cache, THEN refresh expiration. 89 | let found = false; 90 | for (const oldItem of this.content[listKey].value) { 91 | for (const key in filterObj) { 92 | if (oldItem[key] === filterObj[key]) { 93 | found = true; 94 | Object.assign(oldItem, item); 95 | } 96 | } 97 | } 98 | if (!found) this.content[listKey].value.push(item); 99 | this.content[listKey].expires = Date.now() + this.TTL; 100 | } 101 | } 102 | } 103 | // Push a newly created item to ONE OR MANY lists 104 | listPush(item, ...listKeys) { 105 | //Remind user that a key is required for this method 106 | if (listKeys === undefined) { 107 | console.log('Error, listPush requires atleast one unique cache key'); 108 | } else { 109 | //Check if a list exists for this key, if not, skip 110 | for (const listKey of listKeys) { 111 | if (this.content[listKey] === undefined) { 112 | continue; 113 | } 114 | //We push that item into cache, THEN refresh expiration. 115 | this.content[listKey].value.push(item); 116 | this.content[listKey].expires = Date.now() + this.TTL; 117 | 118 | this._refresh(listKey); 119 | } 120 | } 121 | } 122 | 123 | // Support for Delete Mutations to keep cache fresher longer 124 | // The array of listKeys should be every key the developer wants to remove a specific item from. 125 | // It should be every list the item belongs to, ideally. 126 | // EX. cache.listRemove({name: "Fancy Chair"}, "livingRoomFurniture", "kitchenFurniture"}) 127 | // removes all items with name === "Fancy Chair" from cache lists with keys "livingRoomFurniture" and "kitchenFurniture" 128 | listRemoveItem(filterObject, ...listKey) { 129 | // Option to specify if each list only contains the item once. 130 | let unique = false; 131 | if (filterObject.hasOwnProperty('unique')) { 132 | unique = filterObject.unique; 133 | delete filterObject.unique; 134 | } 135 | // Some intuition that if the ID key exists the item must be unique to each list. 136 | if ( 137 | filterObject.hasOwnProperty('id') || 138 | filterObject.hasOwnProperty('ID') || 139 | filterObject.hasOwnProperty('_id') 140 | ) { 141 | unique = true; 142 | } 143 | //Loops through each listKey 144 | for (const key of listKey) { 145 | // **Cleanup protocol** - remove item if past expiration, if not, refresh expiration. 146 | this.cleanUp(key); 147 | if (this.content[key] !== undefined) { 148 | this.content[key].expires = Date.now() + this.TTL; 149 | } else { 150 | // If key is undefined, skip key. 151 | continue; 152 | } 153 | // **Cleanup protocol** 154 | 155 | //Loops through each list to find the item. using a unique identifier, such as the items id 156 | const currentList = this.content[key].value; 157 | for (const item of currentList) { 158 | let missing = false; 159 | //Loop through filterObject, and if one filter is missing set off flag, and skip to next item in list. 160 | for (let filter in filterObject) { 161 | if (item[filter] !== filterObject[filter]) { 162 | missing = true; 163 | break; 164 | } 165 | } 166 | //if flag was never set off, remove item from list 167 | if (!missing) { 168 | const index = currentList.indexOf(item); 169 | currentList.splice(index, 1); 170 | if (unique) break; 171 | } 172 | } 173 | this._refresh(key); 174 | } 175 | } 176 | //Very similar to listRemoveItem but updates the item instead of deleting it from list 177 | listUpdate(newItem, filterObject, ...listKey) { 178 | // Option to specify if each list only contains the item once. 179 | let unique = false; 180 | 181 | // Some intuition that if the ID key exists the item must be unique to each list. 182 | if ( 183 | filterObject.hasOwnProperty('id') || 184 | filterObject.hasOwnProperty('ID') || 185 | filterObject.hasOwnProperty('_id') 186 | ) { 187 | unique = true; 188 | } 189 | // If our inuition isn't what the dev wants, they can specify otherwise. 190 | if (filterObject.hasOwnProperty('unique')) { 191 | unique = filterObject.unique; 192 | // delete this key because we don't want to loop over it for item validation 193 | delete filterObject.unique; 194 | } 195 | //Loops through each listKey 196 | for (const key of listKey) { 197 | // **Cleanup protocol** - remove item if past expiration, if not, refresh expiration. 198 | this.cleanUp(key); 199 | if (this.content[key] !== undefined) { 200 | this.content[key].expires = Date.now() + this.TTL; 201 | } else { 202 | // If key is undefined, skip key. 203 | continue; 204 | } 205 | // **Cleanup protocol** 206 | //Loops through each list to find the item. using a unique identifier, such as the items id 207 | const currentList = this.content[key].value; 208 | for (const item of currentList) { 209 | let missing = false; 210 | //Loop through filterObject, and if one filter is missing set off flag, and skip to next item in list. 211 | for (let filter in filterObject) { 212 | if (item[filter] !== filterObject[filter]) { 213 | missing = true; 214 | break; 215 | } 216 | } 217 | //if flag was never set off, update item in list 218 | if (!missing) { 219 | Object.assign(item, newItem); 220 | if (unique) break; 221 | } 222 | } 223 | this._refresh(key); 224 | } 225 | } 226 | 227 | //If list exists, assumed fresh complete, returns filtered results 228 | // FILTEROBJECT - looks like - {username: "xyz", age: 23} 229 | listFetch(listKey, filterObject) { 230 | this.cleanUp(listKey); 231 | // Check if list exists, if not return null. 232 | if (this.content[listKey] === undefined) return null; 233 | 234 | this._refresh(listKey); 235 | 236 | const returnList = []; 237 | // Option to specify if each list only contains the item once. 238 | let unique = false; 239 | // Some intuition that if the ID key exists the item must be unique to each list. 240 | if ( 241 | filterObject.hasOwnProperty('id') || 242 | filterObject.hasOwnProperty('ID') || 243 | filterObject.hasOwnProperty('_id') 244 | ) { 245 | unique = true; 246 | } 247 | // If our inuition isn't what the dev wants, they can specify otherwise. 248 | if (filterObject.hasOwnProperty('unique')) { 249 | unique = filterObject.unique; 250 | // delete this key because we don't want to loop over it for item validation 251 | delete filterObject.unique; 252 | } 253 | // If list does exist, loop through list and find item by filter Object 254 | for (const item of this.content[listKey].value) { 255 | //create a flag to set off if a filter is not matching 256 | let missing = false; 257 | //Loop through filterObject, and if one filter is missing set off flag, and skip to next item in list. 258 | for (let filter in filterObject) { 259 | if (item[filter] !== filterObject[filter]) { 260 | missing = true; 261 | break; 262 | } 263 | } 264 | //if flag was never set off, add item to filtered list 265 | if (!missing) { 266 | returnList.push(item); 267 | if (unique) break; 268 | } 269 | } 270 | //refresh list expiration since it has been accessed 271 | this.content[listKey].expires = Date.now() + this.TTL; 272 | 273 | //if filtered list is empty, return null 274 | if (returnList.length === 0) return null; 275 | 276 | //if non empty return results 277 | return returnList; 278 | } 279 | // Option to invalidate certain lists, or items and remove them from cache, for certain mutations. 280 | invalidate(...keys) { 281 | //Clears cache if no keys are specified 282 | if (keys.length === 0) { 283 | this.clear(); 284 | } 285 | //Clears specific keys and adjusts size property if key exists. 286 | for (let key of keys) { 287 | this.delete(key); 288 | } 289 | } 290 | 291 | /* {UTILITY METHODS} */ 292 | //Cleans up stale data 293 | cleanUp(key) { 294 | //Evict a stale key if key is provided 295 | if (key !== undefined && this.content[key] !== undefined) { 296 | if (this.content[key].expires < Date.now()) { 297 | this.delete(key); 298 | } 299 | } 300 | //Option to cleanUp all keys if need arises 301 | else { 302 | for (const key in this.content) { 303 | if (this.content[key].expires < Date.now()) { 304 | this.delete(key); 305 | } 306 | } 307 | } 308 | } 309 | //count amount of keys 310 | size() { 311 | return this.size; 312 | } 313 | 314 | // wipe the cache 315 | clear() { 316 | this.content = {}; 317 | this.size = 0; 318 | this.tail = this.head = null; 319 | } 320 | 321 | log() { 322 | console.log(Object.keys(this.content)); 323 | console.log(`Size: ${this.size}`); 324 | console.log(`Size is valid: ${this._isSizeValid()}`); 325 | } 326 | 327 | /** 328 | * This function performs the following in order: 329 | * 1. Checks if the node exists in the cache and removes it if found. This behavior enforces that a new node with the same value as an existing node in memory overwrites the existing one and is enqueued to the tail since it is the most recently used data. 330 | * 2. Alternatively checks to see if the cache is full and then deletes the data from the cache and queue. 331 | * 3. Enqueues a new node at the tail of the queue 332 | * @param {string} key 333 | * @param {object} value 334 | */ 335 | 336 | _addToQueueAndCache(key, value) { 337 | let nodeInCache = this.content[key]; 338 | 339 | if (this.policyType === 'LRU') { 340 | // the node is already in the cache, so we must remove the old one so that our new node is inserted at the tail of the queue. 341 | if (nodeInCache) { 342 | // we only remove from queue and NOT cache since we are just enqueueing this node 343 | this._refresh(key); 344 | } 345 | // when the cache is full, we dequeue the head from the cache/queue 346 | else if (this.size === this.maxSize) { 347 | if (this.maxSize === 0) return; 348 | this._removeFromQueueAndCache(this.head); 349 | } 350 | //key doesn't exist 351 | if (!nodeInCache) { 352 | nodeInCache = new Node(key, value); 353 | nodeInCache.expires = Date.now() + this.TTL; 354 | this.size++; 355 | // enqueue node if it exists, otherwise enqueue new node with value 356 | this._enqueue(nodeInCache); 357 | // assign key to new node 358 | this.content[key] = this.tail; 359 | } 360 | } else if (this.policyType === 'LFU') { 361 | // key exists in cache 362 | if (nodeInCache) { 363 | this._refresh(key, value); 364 | //key doesn't exist, and cache at max size 365 | } else if (this.size === this.maxSize) { 366 | if (this.maxSize === 0) return; 367 | this._removeFromQueueAndCache(this.head); 368 | } 369 | // key doesn't exist 370 | if (!nodeInCache) { 371 | //create new node 372 | nodeInCache = new Node(key, value); 373 | nodeInCache.expires = Date.now() + this.TTL; 374 | this.size++; 375 | 376 | //Place node at head/cold side of queue 377 | this._enqueue(nodeInCache); 378 | 379 | // assign key to new node 380 | this.content[key] = this.head; 381 | } 382 | } 383 | } 384 | 385 | /** 386 | * Move accessed node in cache to the tail of the queue (remove it from queue and then enqueue it) 387 | * @param {object} key 388 | */ 389 | _refreshRecent(node) { 390 | this._removeFromQueue(node); 391 | this._enqueue(node); 392 | } 393 | 394 | _refreshFrequent(node) { 395 | this._bubbleSort(node); 396 | } 397 | 398 | _refresh(key) { 399 | const existingNode = this.content[key]; 400 | 401 | if (existingNode) { 402 | existingNode.accessCount++; 403 | 404 | if (this.policyType === 'LRU') { 405 | this._refreshRecent(existingNode); 406 | } else if (this.policyType === 'LFU') { 407 | this._refreshFrequent(existingNode); 408 | } else { 409 | throw new Error('Policy type does not exist'); 410 | } 411 | } 412 | } 413 | 414 | _bubbleSort(node) { 415 | // 0 node list 416 | if (!node) return; 417 | // 1 node list OR 2+ node list where node is tail 418 | if (node === this.tail) return; 419 | 420 | // 2+ node list 421 | while (node.next && node.next.accessCount < node.accessCount) { 422 | if (node === this.head) { 423 | this.head = node.next; 424 | 425 | //2 node list where node is head 426 | if (!node.next.next) { 427 | this.tail = node; 428 | 429 | node.next.prev = null; 430 | node.next.next = node; 431 | 432 | node.next = null; 433 | node.prev = this.head; 434 | break; 435 | } 436 | 437 | //3+ node list where node is head 438 | const temp = node.next.next; 439 | temp.prev = node; 440 | 441 | node.next.prev = null; 442 | node.next.next = node; 443 | 444 | node.next = temp; 445 | node.prev = this.head; 446 | } else { 447 | //3 node list where node is not head 448 | // A > B > C swap B with C 449 | if (!node.next.next) { 450 | this.tail = node; 451 | 452 | const temp = node.next; 453 | 454 | node.prev.next = temp; 455 | 456 | temp.prev = node.prev; 457 | temp.next = node; 458 | 459 | node.prev = temp; 460 | node.next = null; 461 | 462 | break; 463 | } 464 | //swap with next 465 | // node 466 | // A > B > C > D 467 | //Point A's 'next' to C 468 | node.prev.next = node.next; 469 | //Point D's 'prev' to B 470 | node.next.next.prev = node; 471 | 472 | //Swap C's prev and next pointers 473 | const temp = node.next.next; 474 | node.next.prev = node.prev; 475 | node.next.next = node; 476 | 477 | //Swap B's prev and next pointers 478 | node.prev = node.next; 479 | node.next = temp; 480 | } 481 | node = node.next; 482 | } 483 | } 484 | 485 | /** 486 | * Add node to the tail of the DLL (enqueue it) 487 | * When we call enqueue, we assume we are enqueing a new node or existing node that is "floating" without pointing to any other nodes 488 | * @param {object} key 489 | */ 490 | _enqueue(node) { 491 | // insert new node at tail of the linked list (queue) 492 | if (this.policyType === 'LRU') { 493 | if (this.tail) { 494 | node.prev = this.tail; 495 | this.tail.next = node; 496 | this.tail = node; 497 | } else this.tail = this.head = node; 498 | // queue is empty. point head & tail ➡ new Node 499 | } else if (this.policyType === 'LFU') { 500 | if (this.head) { 501 | node.next = this.head; 502 | this.head.prev = node; 503 | this.head = node; 504 | } else this.tail = this.head = node; 505 | } 506 | } 507 | /** 508 | * Removes a node from the queue and deletes the corresponding data from the cache 509 | * @param {object} key 510 | */ 511 | 512 | _removeFromQueueAndCache(node) { 513 | delete this.content[node.keyRef]; 514 | this._removeFromQueue(node); 515 | this.size--; 516 | } 517 | 518 | /** 519 | * Removes a node from the queue by detaching itself from its neighbors and connecting those neighbors' pointers. 520 | * @param {Node} node 521 | */ 522 | _removeFromQueue(node) { 523 | // if this node is the only node in the queue 524 | if (!node.next && !node.prev) { 525 | this.head = this.tail = null; 526 | } 527 | // if node is at the tail of the queue 528 | else if (!node.next) { 529 | node.prev.next = null; 530 | this.tail = node.prev; 531 | } 532 | // if node is at the head of the queue 533 | else if (!node.prev) { 534 | node.next.prev = null; 535 | this.head = node.next; 536 | } 537 | // node is between two nodes, so connect neighboring pointers 538 | else { 539 | node.next.prev = node.prev; 540 | node.prev.next = node.next; 541 | } 542 | // remove node's pointers to former neighbors in queue 543 | node.prev = node.next = null; 544 | } 545 | 546 | /** 547 | * Refresh a node in the queue and return its value 548 | * @param {Node} node 549 | * @returns 550 | */ 551 | _getDataFromQueue(key) { 552 | const nodeInCache = this.content[key]; 553 | 554 | if (!nodeInCache) { 555 | console.log(`There is no key: ${key} in the cache.`); 556 | return null; 557 | } 558 | 559 | this._refresh(key); 560 | 561 | return nodeInCache.value; 562 | } 563 | 564 | /** 565 | * Used for testing 566 | * @returns {Boolean} 567 | */ 568 | _isSizeValid() { 569 | return this.size === Object.keys(this.content).length; 570 | } 571 | } 572 | 573 | module.exports = Qache; 574 | -------------------------------------------------------------------------------- /Qache/README.md: -------------------------------------------------------------------------------- 1 | # Qache 2 | 3 | ## What is Qache? 4 | Qache is a utility class for handling server side caching of MongoDB queries made with GraphQL. 5 | 6 | ## Installation 7 | Using npm: 8 | ```shell 9 | $ npm i qache 10 | ``` 11 | In Node.js: 12 | 13 | ```js 14 | // Load the Qache class 15 | var Qache = require('qache'); 16 | 17 | // Instantiate a cache 18 | var cache = new Qache(); 19 | ``` 20 | ## Qache Properties 21 | - `content` 22 | - `size` 23 | - `maxSize` 24 | - `TTL` 25 | 26 | ## Qache Methods 27 | - `get(key)` 28 | - `set(key, value)` 29 | - `listCreate(listKey, ...item)` 30 | - `listRange(listKey, start, end)` 31 | - `listPush(item, ...listKeys)` 32 | - `listRemoveItem(filterObject, ...listKey)` 33 | - `listUpdate(filterObject, newItem, ...listKey)` 34 | - `listFetch(listKey,filterObject)` 35 | - `invalidate(...keys)` 36 | 37 | ## Support 38 | Tested in Node.js 16.13.0 39 | 40 | [Automated unit tests](https://github.com/oslabs-beta/Qache/tree/dev/__tests__) are available. 41 | 42 | See the complete [package source](https://github.com/oslabs-beta/Qache) for more details, as well as a fullstack demo application. 43 | -------------------------------------------------------------------------------- /Qache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qache", 3 | "version": "1.0.5", 4 | "description": "Qache is a utility class for handling server side caching of API queries made with any database.", 5 | "main": "Qache.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/oslabs-beta/Qache.git" 12 | }, 13 | "keywords": [ 14 | "cache", 15 | "qache", 16 | "graphql", 17 | "graphql-cache", 18 | "simple", 19 | "cache", 20 | "oslabs", 21 | "os-labs" 22 | ], 23 | "author": "Qache", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/oslabs-beta/Qache/issues" 27 | }, 28 | "homepage": "https://github.com/oslabs-beta/Qache#readme" 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | # Qache 4 | 5 | ## What is Qache? 6 | Qache is a modular utility class for handling server side caching of Data. 7 | 8 | Accelerated by [OS Labs](https://github.com/open-source-labs) and developed by [Nader Almogazy](https://github.com/nader12334), [Steven Du](https://github.com/stebed), [Leo Crossman](https://github.com/leocrossman), and [Evan Preedy](https://github.com/ep1815) 9 | 10 | ### Features 11 | - Server-side caching implemented in one line: ```const qache = new Qache({});``` 12 | - Easy-to-use, modular caching methods, to get started... ```qache.set(key, value)``` then ```qache.get(key)``` 13 | - Full Suite of list methods, allowing users to create, read, update, and delete cache data on a per page, per category, or site-wide level. 14 | - Lazy Invalidation! Optionally reset your cache after certain events if you can't figure out a way to maintain cache validity. Simply ```qache.invalidate()``` 15 | - Get diagnostics on your Cache, and see all it's contents! ```qache.log()``` 16 | - Cache Eviction Policies: 17 | - timeToLive: Qache tracks when each node stored was last accessed. By default data has an infinite timeToLive, to change this, pass into your options object `{timeToLive: NumberInMilliseconds}` 18 | - maxSize: Qache tracks how many keys/nodes exist, and upon reaching it's max, will begin to evict older data in order to add new data. This property is always true, and defaults to 5 nodes. To change this property, pass into your options object ```{maxSize: Number}``` 19 | - LRU(default): Upon reaching max size, Qache will evict the Least Recently Accessed Node in cache 20 | - LFU: Upon reaching max size, Qache will evict the Least Frequently Used Node in Cache, determined by tracking access counts of each node. To switch to this policy, pass into your options object ```{evictionPolicy: "LFU"}``` 21 | 22 | ### Contribute to Qache 23 | All code changes happen through Github Pull Requests and we actively welcome them. To submit your pull request, follow the steps below: 24 | ## Pull Requests 25 | 1. Fork the repo and create your branch from `master`. 26 | 2. If you've added code that should be tested, add tests. 27 | 3. Be sure to comment on any code added. 28 | 5. Ensure the test suite passes. 29 | 6. Make sure your code lints. 30 | 7. Issue a Pull Request! 31 | 32 | ## Setting up the Dev Server, Demo, and Mongo DB 33 | 1. Clone this repo. 34 | 2. `cd qache-app` 35 | 3. Run `npm install` 36 | 4. `npm start` 37 | 38 | ## Coding Style 39 | 2 spaces for indentation 40 | 80 character line length 41 | 42 | A note on using the dev server: you must access the `/graphql` route on port 8080. 43 | -------------------------------------------------------------------------------- /__tests__/Qache.test.js: -------------------------------------------------------------------------------- 1 | const Cache = require('../Qache/Qache'); 2 | const Node = require('../Qache/Node'); 3 | 4 | const testUsers = [ 5 | { 6 | username: 'nader12334', 7 | firstName: 'nader', 8 | lastName: 'almogazy', 9 | age: 27, 10 | }, 11 | { 12 | username: 'leocrossman', 13 | firstName: 'leo', 14 | lastName: 'crossman', 15 | age: 23, 16 | }, 17 | { 18 | username: 'ep1815', 19 | firstName: 'evan', 20 | lastName: 'preedy', 21 | age: 23, 22 | }, 23 | { 24 | username: 'stebed', 25 | firstName: 'steven', 26 | lastName: 'du', 27 | age: Number.MAX_VALUE, 28 | }, 29 | ]; 30 | 31 | describe('Qache Tests', () => { 32 | describe('Node', () => { 33 | let node; 34 | beforeEach(() => { 35 | node = new Node('testKey', { testData: 'testVal' }); 36 | }); 37 | it('should have properties: keyRef, value, prev, next, expires, accessCount', () => { 38 | expect(node.keyRef).toBeDefined(); 39 | expect(node.value).toBeDefined(); 40 | expect(node.prev).toBeDefined(); 41 | expect(node.next).toBeDefined(); 42 | expect(node.expires).toBeDefined(); 43 | expect(node.accessCount).toBeDefined(); 44 | }); 45 | }); 46 | 47 | describe('Qache', () => { 48 | let cache, users, userNode; 49 | // this beforeEach will change soonish... will be replaced with whatever's relevant or just removed. 50 | beforeEach(() => { 51 | users = [...testUsers]; 52 | users2 = testUsers.map((user) => user.username + 2); 53 | users3 = testUsers.map((user) => user.username + 3); 54 | users4 = testUsers.map((user) => user.username + 4); 55 | userNode = new Node('users', users); 56 | userNode2 = new Node('users2', users2); 57 | userNode3 = new Node('users3', users3); 58 | userNode4 = new Node('users4', users4); 59 | cache = new Cache(); 60 | }); 61 | 62 | it('should have properties: TTL, maxSize, content, size, policyType, head, tail', () => { 63 | expect(cache.TTL).toBeDefined(); 64 | expect(cache.maxSize).toBeDefined(); 65 | expect(cache.content).toBeDefined(); 66 | expect(cache.size).toBeDefined(); 67 | expect(cache.policyType).toBeDefined(); 68 | expect(cache.head).toBeDefined(); 69 | expect(cache.tail).toBeDefined(); 70 | }); 71 | 72 | it('should have initial property values: head = tail = null, size = 0, content = {}, maxSize = options.maxSize, policyType = options.evictionPolicy', () => { 73 | expect(cache.head).toBe(null); 74 | expect(cache.tail).toBe(null); 75 | expect(cache.size).toBe(0); 76 | expect(cache.content).toEqual({}); 77 | expect(cache.policyType).toEqual('LRU'); 78 | }); 79 | 80 | describe('set()', () => { 81 | it(`should be a method on the 'Qache' class`, () => { 82 | expect(cache.set).toBeDefined(); 83 | expect(typeof cache.set).toBe('function'); 84 | }); 85 | 86 | it('should take in a data type and store a refrence to the value in the cache as a node', () => { 87 | const testKeys = [ 88 | 'string', 89 | 'number', 90 | 'boolean', 91 | 'array', 92 | 'object', 93 | 'null', 94 | 'NaN', 95 | ].map((type) => type + 'Key'); 96 | const testTypes = [ 97 | 'testString', 98 | 7, 99 | true, 100 | [1, 2, 3], 101 | { a: 'asdf' }, 102 | null, 103 | NaN, 104 | ]; 105 | for (let i = 0; i < testTypes.length; i++) { 106 | const key = testKeys[i]; 107 | const typeVal = testTypes[i]; 108 | cache.set(key, typeVal); 109 | expect(cache.content[key]).toBeInstanceOf(Node); 110 | expect(cache.content[key].value).toBe(typeVal); 111 | cache = new Cache(); 112 | } 113 | }); 114 | 115 | it('should take in a cache key/value, add the node to the queue, and add the node to the cache at the key when cache is empty', () => { 116 | expect(cache.head).toBe(null); 117 | expect(cache.tail).toBe(null); 118 | const key = 'users'; 119 | cache.set(key, users); 120 | let node = cache.content[key]; 121 | expect(node).toBeInstanceOf(Node); 122 | expect(node).toEqual(userNode); 123 | expect(node.keyRef).toBe(key); 124 | expect(node.value).toBe(users); 125 | expect(node.prev).toBe(null); 126 | expect(node.next).toBe(null); 127 | expect(node.accessCount).toBe(1); 128 | expect(cache.head).toBe(node); 129 | expect(cache.tail).toBe(node); 130 | }); 131 | 132 | it('should increment cache size when size < maxSize', () => { 133 | expect(cache.size).toBe(0); 134 | cache.set('users', users); 135 | expect(cache.size).toBe(1); 136 | cache.set('users2', users2); 137 | expect(cache.size).toBe(2); 138 | }); 139 | 140 | it('should not alter cache size when size equals maxSize', () => { 141 | cache.maxSize = 0; 142 | cache.set('users', users); 143 | expect(cache.size).toBe(0); 144 | cache = new Cache(); 145 | cache.maxSize = 3; 146 | cache.set('users', users); 147 | expect(cache.size).toBe(1); 148 | cache.set('users2', users2); 149 | expect(cache.size).toBe(2); 150 | cache.set('users3', users3); 151 | expect(cache.size).toBe(3); 152 | cache.set('users4', users4); 153 | expect(cache.size).toBe(3); 154 | expect(cache.size).toEqual(Object.keys(cache.content).length); 155 | }); 156 | }); 157 | 158 | describe('get()', () => { 159 | it(`should be a method on the 'Qache' class`, () => { 160 | expect(cache.get).toBeDefined(); 161 | expect(typeof cache.get).toBe('function'); 162 | }); 163 | 164 | it('should get a value from a key in the cache when there is one element in the cache', () => { 165 | cache.set('users', users); 166 | expect(cache.get('users')).toEqual(userNode.value); 167 | }); 168 | it('should get a value from a key in the cache when there are two elements in the cache', () => { 169 | cache.set('users', users); 170 | cache.set('users2', users2); 171 | expect(cache.get('users2')).toEqual(userNode2.value); 172 | }); 173 | it('should get a value from a key in the cache when there are 3+ elements in the cache', () => { 174 | cache.set('users', users); 175 | cache.set('users2', users2); 176 | cache.set('users3', users3); 177 | expect(cache.get('users3')).toEqual(userNode3.value); 178 | }); 179 | 180 | it('should get the reference to the value in the node and not a copy of the value', () => { 181 | cache.content['users'] = userNode; 182 | expect(cache.get('users')).toBe(userNode.value); 183 | }); 184 | 185 | it('should increment accessCount on the cache node', () => { 186 | userNode.accessCount = 0; 187 | cache.content['users'] = userNode; 188 | expect(cache.get('users')).toBe(users); 189 | expect(userNode.accessCount).toBe(1); 190 | expect(cache.get('users')).toBe(users); 191 | expect(userNode.accessCount).toBe(2); 192 | }); 193 | }); 194 | 195 | describe('delete()', () => { 196 | beforeEach(() => { 197 | users = [...testUsers]; 198 | users2 = testUsers.map((user) => user.username + 2); 199 | users3 = testUsers.map((user) => user.username + 3); 200 | users4 = testUsers.map((user) => user.username + 4); 201 | userNode = new Node('users', users); 202 | userNode2 = new Node('users2', users2); 203 | userNode3 = new Node('users3', users3); 204 | userNode4 = new Node('users4', users4); 205 | cache = new Cache(); 206 | }); 207 | 208 | it(`should be a method on the 'Qache' class`, () => { 209 | expect(cache.delete).toBeDefined(); 210 | expect(typeof cache.delete).toBe('function'); 211 | }); 212 | 213 | it('should take in a cache key and remove the corresponding node from the cache', () => { 214 | cache.set('users', users); 215 | cache.delete('users'); 216 | expect(cache.get('users')).toBe(null); 217 | expect(cache.content['users']).toBe(undefined); 218 | }); 219 | 220 | it('should take in a cache key and remove the corresponding node from the queue', () => { 221 | cache.set('users', users); 222 | cache.set('users2', users2); 223 | cache.set('users3', users3); 224 | cache.set('users4', users4); 225 | cache.delete('users2'); 226 | let curr = cache.head; 227 | while (curr) { 228 | expect(curr).not.toEqual(userNode2); 229 | curr = curr.next; 230 | } 231 | }); 232 | 233 | it('should remove a node from the cache and set head/tail to null when the node was the only one in cache before deletion', () => { 234 | cache.set('users', users); 235 | cache.delete('users'); 236 | expect(cache.head).toBe(null); 237 | expect(cache.tail).toBe(null); 238 | }); 239 | 240 | it('should assign the next node to the head of the linked list when the node at the head is deleted', () => { 241 | cache.set('users', users); 242 | cache.set('users2', users2); 243 | cache.set('users3', users3); 244 | cache.set('users4', users4); 245 | cache.delete('users'); 246 | userNode2.prev = null; 247 | userNode2.next = userNode3; 248 | userNode3.prev = userNode2; 249 | userNode3.next = userNode4; 250 | userNode4.prev = userNode3; 251 | expect(cache.head).toEqual(userNode2); 252 | }); 253 | it('should assign the previous node to the tail of the linked list when the node at the tail is deleted', () => { 254 | cache.set('users', users); 255 | cache.set('users2', users2); 256 | cache.set('users3', users3); 257 | cache.set('users4', users4); 258 | cache.delete('users4'); 259 | userNode.next = userNode2; 260 | userNode2.prev = userNode; 261 | userNode2.next = userNode3; 262 | userNode3.prev = userNode2; 263 | userNode3.next = null; 264 | expect(cache.tail).toEqual(userNode3); 265 | }); 266 | }); 267 | 268 | describe('invalidate()', () => { 269 | beforeEach(() => { 270 | users = [...testUsers]; 271 | users2 = testUsers.map((user) => user.username + 2); 272 | users3 = testUsers.map((user) => user.username + 3); 273 | users4 = testUsers.map((user) => user.username + 4); 274 | userNode = new Node('users', users); 275 | userNode2 = new Node('users2', users2); 276 | userNode3 = new Node('users3', users3); 277 | userNode4 = new Node('users4', users4); 278 | cache = new Cache(); 279 | }); 280 | 281 | it(`should be a method on the 'Qache' class`, () => { 282 | expect(cache.invalidate).toBeDefined(); 283 | expect(typeof cache.invalidate).toBe('function'); 284 | }); 285 | 286 | it('should clear the entire cache if no arguments are passed in', () => { 287 | cache.set('users', users); 288 | cache.set('users2', users2); 289 | cache.set('users3', users3); 290 | cache.set('users4', users4); 291 | cache.invalidate(); 292 | expect(cache.size).toBe(0); 293 | expect(cache.head).toBe(null); 294 | expect(cache.tail).toBe(null); 295 | expect(Object.keys(cache.content).length).toBe(0); 296 | }); 297 | 298 | it('should remove a single item from the cache when it is the only item in the cache', () => { 299 | cache.set('users', users); 300 | cache.invalidate('users'); 301 | expect(cache.content['users']).toBe(undefined); 302 | }); 303 | 304 | it('should remove a single item from the cache when there are two items in the cache and the item is at the head, as well as reassign the head node to the next node in the queue', () => { 305 | cache.set('users', users); 306 | cache.set('users2', users2); 307 | cache.invalidate('users'); 308 | expect(cache.content['users']).toBe(undefined); 309 | expect(cache.head).toBe(cache.tail); 310 | }); 311 | it('should remove a single item from the cache when there are two items in the cache and the item is at the tail, as well as reassign the tail node to the previous node in the queue', () => { 312 | cache.set('users', users); 313 | cache.set('users2', users2); 314 | cache.invalidate('users2'); 315 | expect(cache.content['users2']).toBe(undefined); 316 | expect(cache.tail).toBe(cache.head); 317 | }); 318 | it('should remove a single item from the cache when there are 3+ items in the cache', () => { 319 | cache.set('users', users); 320 | cache.set('users2', users2); 321 | cache.set('users3', users3); 322 | cache.invalidate('users2'); 323 | expect(cache.content['users2']).toBe(undefined); 324 | }); 325 | it('should remove all keys passed in from the cache', () => { 326 | const userArr = ['users2', 'users3', 'users']; 327 | cache.set('users', users); 328 | cache.set('users2', users2); 329 | cache.set('users3', users3); 330 | cache.invalidate(...userArr); 331 | for (const user of userArr) { 332 | expect(cache.content[user]).toBe(undefined); 333 | } 334 | }); 335 | }); 336 | 337 | describe('cleanUp()', () => { 338 | beforeEach(() => { 339 | users = [...testUsers]; 340 | users2 = testUsers.map((user) => user.username + 2); 341 | users3 = testUsers.map((user) => user.username + 3); 342 | users4 = testUsers.map((user) => user.username + 4); 343 | userNode = new Node('users', users); 344 | userNode2 = new Node('users2', users2); 345 | userNode3 = new Node('users3', users3); 346 | userNode4 = new Node('users4', users4); 347 | cache = new Cache(); 348 | }); 349 | 350 | it(`should be a method on the 'Qache' class`, () => { 351 | expect(cache.invalidate).toBeDefined(); 352 | expect(typeof cache.invalidate).toBe('function'); 353 | }); 354 | 355 | it('should evict a provided key if the key exceeds its TTL', () => { 356 | cache.set('users', users); 357 | cache.set('users2', users2); 358 | cache.set('users3', users3); 359 | cache.set('users4', users4); 360 | cache.content['users'].expires = Date.now() - 100; 361 | cache.cleanUp('users'); 362 | expect(cache.content['users']).toBe(undefined); 363 | }); 364 | 365 | it('should not evict a provided key if the key does not exceed its TTL', () => { 366 | cache.set('users', users); 367 | cache.set('users2', users2); 368 | cache.set('users3', users3); 369 | cache.set('users4', users4); 370 | cache.content['users'].expires = Date.now() + 100; 371 | cache.cleanUp('users'); 372 | expect(cache.content['users']).not.toBe(undefined); 373 | }); 374 | 375 | it('should evict all provided keys when all the keys exceed their respective TTL', () => { 376 | cache.set('users', users); 377 | cache.set('users2', users2); 378 | cache.set('users3', users3); 379 | cache.set('users4', users4); 380 | for (const user of ['users', 'users2', 'users3', 'users4']) { 381 | cache.content[user].expires = Date.now() - 100; 382 | cache.cleanUp(user); 383 | expect(cache.content[user]).toBe(undefined); 384 | } 385 | }); 386 | 387 | it('should evict some provided keys on successive calls when some of the keys exceed their respective TTL', () => { 388 | const users = ['users', 'users2', 'users3', 'users4']; 389 | cache.set('users', users); 390 | cache.set('users2', users2); 391 | cache.set('users3', users3); 392 | cache.set('users4', users4); 393 | for (let i = 0; i < users.length; i++) { 394 | const user = users[i]; 395 | cache.content[user].expires = 396 | i % 2 === 0 ? Date.now() - 100 : Date.now() + 100; 397 | cache.cleanUp(user); 398 | i % 2 === 0 399 | ? expect(cache.content[user]).toBe(undefined) 400 | : expect(cache.content[user]).not.toBe(undefined); 401 | } 402 | }); 403 | 404 | it('should evict all keys in the cache whose expiration time has passed and no key is passed in', () => { 405 | const users = ['users', 'users2', 'users3', 'users4']; 406 | cache.set('users', users); 407 | cache.set('users2', users2); 408 | cache.set('users3', users3); 409 | cache.set('users4', users4); 410 | console.log(cache.content); 411 | users.forEach((user) => { 412 | cache.content[user].expires = Date.now() - 100; 413 | }); 414 | cache.cleanUp(); 415 | for (const user of users) { 416 | expect(cache.content[user]).toBe(undefined); 417 | } 418 | }); 419 | }); 420 | 421 | describe('LRU Eviction Policy Tests:', () => { 422 | describe('set()', () => { 423 | beforeEach(() => { 424 | users = [...testUsers]; 425 | users2 = testUsers.map((user) => user.username + 2); 426 | users3 = testUsers.map((user) => user.username + 3); 427 | users4 = testUsers.map((user) => user.username + 4); 428 | userNode = new Node('users', users); 429 | userNode2 = new Node('users2', users2); 430 | userNode3 = new Node('users3', users3); 431 | userNode4 = new Node('users4', users4); 432 | cache = new Cache(); 433 | }); 434 | 435 | it('should add a node to the queue and cache when there is one element in the cache and update the cache tail to point to the added node', () => { 436 | cache.set('users', users); 437 | cache.set('users2', users2); 438 | userNode.next = userNode2; 439 | userNode2.prev = userNode; 440 | expect(cache.head).toEqual(userNode); 441 | expect(cache.tail).toEqual(userNode2); 442 | expect(cache.tail).toBeInstanceOf(Node); 443 | }); 444 | it('should add a node to the queue and cache when there are two elements in the cache', () => { 445 | cache.set('users', users); 446 | cache.set('users2', users2); 447 | cache.set('users3', users3); 448 | userNode.next = userNode2; 449 | userNode2.prev = userNode; 450 | userNode2.next = userNode3; 451 | userNode3.prev = userNode2; 452 | expect(cache.head).toEqual(userNode); 453 | expect(cache.tail).toEqual(userNode3); 454 | expect(cache.head.next).toEqual(userNode2); 455 | expect(cache.tail.prev).toEqual(userNode2); 456 | expect(cache.head.next.next).toBe(cache.tail); 457 | expect(cache.tail.prev.prev).toBe(cache.head); 458 | expect(cache.content['users2']).toEqual(userNode2); 459 | }); 460 | it('should add a node to the queue and cache when there are 3+ elements in the cache', () => { 461 | cache.set('users', users); 462 | cache.set('users2', users2); 463 | cache.set('users3', users3); 464 | cache.set('users4', users4); 465 | userNode.next = userNode2; 466 | userNode2.prev = userNode; 467 | userNode2.next = userNode3; 468 | userNode3.prev = userNode2; 469 | userNode3.next = userNode4; 470 | userNode4.prev = userNode3; 471 | expect(cache.head).toEqual(userNode); 472 | expect(cache.head.next).toEqual(userNode2); 473 | expect(cache.head.next.next).toEqual(userNode3); 474 | expect(cache.head.next.next.next).toEqual(cache.tail); 475 | expect(cache.tail).toEqual(userNode4); 476 | expect(cache.tail.prev).toEqual(userNode3); 477 | expect(cache.tail.prev.prev).toEqual(userNode2); 478 | expect(cache.tail.prev.prev.prev).toEqual(userNode); 479 | expect(cache.content['users']).toEqual(userNode); 480 | expect(cache.content['users2']).toEqual(userNode2); 481 | expect(cache.content['users3']).toEqual(userNode3); 482 | expect(cache.content['users4']).toEqual(userNode4); 483 | }); 484 | 485 | it('should add a node to the tail and remove + reassign the head when cache size equals maxSize', () => { 486 | cache.maxSize = 3; 487 | cache.set('users', users); 488 | cache.set('users2', users2); 489 | cache.set('users3', users3); 490 | cache.set('users4', users4); 491 | userNode2.prev = null; 492 | userNode2.next = userNode3; 493 | userNode3.prev = userNode2; 494 | userNode3.next = userNode4; 495 | userNode4.prev = userNode3; 496 | expect(cache.head).toEqual(userNode2); 497 | expect(cache.head.next).toEqual(userNode3); 498 | expect(cache.head.next.next).toBe(cache.tail); 499 | expect(cache.tail).toEqual(userNode4); 500 | expect(cache.tail.prev).toEqual(userNode3); 501 | expect(cache.tail.prev.prev).toBe(cache.head); 502 | }); 503 | }); 504 | 505 | describe('get()', () => { 506 | beforeEach(() => { 507 | users = [...testUsers]; 508 | users2 = testUsers.map((user) => user.username + 2); 509 | users3 = testUsers.map((user) => user.username + 3); 510 | users4 = testUsers.map((user) => user.username + 4); 511 | userNode = new Node('users', users); 512 | userNode2 = new Node('users2', users2); 513 | userNode3 = new Node('users3', users3); 514 | userNode4 = new Node('users4', users4); 515 | const nodes = [userNode, userNode2, userNode3, userNode4]; 516 | nodes.forEach((node) => { 517 | node.accessCount++; 518 | }); 519 | cache = new Cache(); 520 | }); 521 | 522 | it('should refresh node position in the queue by moving it to the tail', () => { 523 | cache.set('users', users); 524 | cache.get('users'); 525 | expect(cache.tail).toBe(cache.content['users']); 526 | expect(cache.tail).toEqual(userNode); 527 | }); 528 | }); 529 | 530 | describe('update()', () => { 531 | beforeEach(() => { 532 | users = [...testUsers]; 533 | users2 = testUsers.map((user) => user.username + 2); 534 | users3 = testUsers.map((user) => user.username + 3); 535 | users4 = testUsers.map((user) => user.username + 4); 536 | userNode = new Node('users', users); 537 | userNode2 = new Node('users2', users2); 538 | userNode3 = new Node('users3', users3); 539 | userNode4 = new Node('users4', users4); 540 | cache = new Cache(); 541 | }); 542 | 543 | it('should move an existing node to the tail of the cache when a new value is passed into update with an existing key', () => { 544 | cache.set('users', users); 545 | cache.set('users2', users2); 546 | cache.update('users', users3); 547 | userNode3.keyRef = 'users'; 548 | userNode3.accessCount = 2; 549 | userNode2.prev = null; 550 | userNode2.next = userNode3; 551 | userNode3.prev = userNode2; 552 | userNode3.next = null; 553 | expect(cache.size).toBe(2); 554 | expect(cache.head).toEqual(userNode2); 555 | expect(cache.tail).toBe(cache.head.next); 556 | }); 557 | }); 558 | }); 559 | describe('LFU Eviction Policy Tests:', () => { 560 | describe('set()', () => { 561 | beforeEach(() => { 562 | users = [...testUsers]; 563 | users2 = testUsers.map((user) => user.username + 2); 564 | users3 = testUsers.map((user) => user.username + 3); 565 | users4 = testUsers.map((user) => user.username + 4); 566 | userNode = new Node('users', users); 567 | userNode2 = new Node('users2', users2); 568 | userNode3 = new Node('users3', users3); 569 | userNode4 = new Node('users4', users4); 570 | cache = new Cache(); 571 | }); 572 | it('should evict the node with the lowest access count', () => {}); 573 | }); 574 | }); 575 | }); 576 | }); 577 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qache", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "devDependencies": { 7 | "jest": "^27.4.7" 8 | }, 9 | "scripts": { 10 | "deploy": "git push heroku `git subtree split --prefix qache-app deployment`:main --force", 11 | "test": "jest --watchAll" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/oslabs-beta/Qache.git" 16 | }, 17 | "author": "", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/oslabs-beta/Qache/issues" 21 | }, 22 | "homepage": "https://github.com/oslabs-beta/Qache#readme" 23 | } 24 | -------------------------------------------------------------------------------- /qache-app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "@babel/preset-react" 10 | ], 11 | "plugins": ["react-require"] 12 | } 13 | -------------------------------------------------------------------------------- /qache-app/.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | coverage/ 3 | **/node_modules 4 | *.log 5 | **/.vscode 6 | 7 | #.env files contain passwords/api keys 8 | **/*.env 9 | 10 | # OS generated files 11 | .DS_Store 12 | .DS_Store? 13 | ._* 14 | .Spotlight-V100 15 | .Trashes 16 | ehthumbs.db 17 | Thumbs.db 18 | -------------------------------------------------------------------------------- /qache-app/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 80, 7 | "jsxSingleQuote": true, 8 | "useTabs": false, 9 | "useEditorConfig": true, 10 | "htmlWhitespaceSensitivity": "css", 11 | "bracketSpacing": true, 12 | "arrowParens": "always" 13 | } 14 | -------------------------------------------------------------------------------- /qache-app/client/App.tsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom'; 2 | import Home from './components/home/Home'; 3 | import DemoApp from './components/demo-app/DemoApp'; 4 | import Docs from './components/home/Docs' 5 | 6 | const App = () => { 7 | return ( 8 | <> 9 | 10 | } /> 11 | } /> 12 | } /> 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /qache-app/client/components/demo-app/DemoApp.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Routes, Route } from 'react-router-dom'; 3 | import { Metric } from '../../../interfaces'; 4 | import Navigation from './Navigation'; 5 | import LandingPage from './LandingPage'; 6 | import ProductDisplay from './ProductDisplay'; 7 | import '../../styles/demo-styles/Navigation.scss'; 8 | 9 | const DemoApp = () => { 10 | const [sidebar, setSidebar] = useState(false); 11 | const [refresh, setRefresh] = useState(false); 12 | const hideSidebar = () => setSidebar(false); 13 | const [metrics, setMetrics] = useState({ 14 | Bedroom: { 15 | labels: [], 16 | data: [], 17 | }, 18 | Mattresses: { 19 | labels: [], 20 | data: [], 21 | }, 22 | Furniture: { 23 | labels: [], 24 | data: [], 25 | }, 26 | Storage: { 27 | labels: [], 28 | data: [], 29 | }, 30 | 'Living Room': { 31 | labels: [], 32 | data: [], 33 | }, 34 | Kitchen: { 35 | labels: [], 36 | data: [], 37 | }, 38 | Bathroom: { 39 | labels: [], 40 | data: [], 41 | }, 42 | Appliances: { 43 | labels: [], 44 | data: [], 45 | }, 46 | Couches: { 47 | labels: [], 48 | data: [], 49 | }, 50 | Deals: { 51 | labels: [], 52 | data: [], 53 | }, 54 | }); 55 | 56 | return ( 57 | <> 58 |
{ 62 | hideSidebar(); 63 | } 64 | : undefined 65 | } 66 | className={sidebar ? 'sidebar-overlay' : ''} 67 | /> 68 | 71 | 72 | } /> 73 | 84 | } 85 | /> 86 | 97 | } 98 | /> 99 | 110 | } 111 | /> 112 | 123 | } 124 | /> 125 | 136 | } 137 | /> 138 | 149 | } 150 | /> 151 | 162 | } 163 | /> 164 | 175 | } 176 | /> 177 | 188 | } 189 | /> 190 | 201 | } 202 | /> 203 | 204 | 205 | ); 206 | }; 207 | 208 | export default DemoApp; 209 | -------------------------------------------------------------------------------- /qache-app/client/components/demo-app/LandingPage.tsx: -------------------------------------------------------------------------------- 1 | import '../../styles/demo-styles/LandingPage.scss'; 2 | import LineGraph from './LineGraph'; 3 | import storage from '../../images/storage.jpg'; 4 | 5 | const LandingPage = () => { 6 | return ( 7 | <> 8 |
9 |
10 |
11 | Storage picture 12 |
13 |

Find the right products for your dream room using our demo!

14 |
15 |
16 | 17 |
18 |

19 | Experience our caching solution
when searching for products! 20 |

21 | 29 |
30 |
31 |
32 | 33 | ); 34 | }; 35 | 36 | export default LandingPage; 37 | -------------------------------------------------------------------------------- /qache-app/client/components/demo-app/LineGraph.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Chart as ChartJS, 3 | LineElement, 4 | PointElement, 5 | CategoryScale, 6 | LinearScale, 7 | Title, 8 | Tooltip, 9 | Legend, 10 | } from 'chart.js'; 11 | 12 | import { Line } from 'react-chartjs-2'; 13 | 14 | import { Metric, Dataset } from '../../../interfaces'; 15 | 16 | ChartJS.register( 17 | CategoryScale, 18 | LinearScale, 19 | LineElement, 20 | PointElement, 21 | Title, 22 | Tooltip, 23 | Legend 24 | ); 25 | 26 | const LineGraph = ({ 27 | metrics, 28 | width, 29 | height, 30 | }: { 31 | metrics: Metric; 32 | width: number; 33 | height: number; 34 | }) => { 35 | const state: { 36 | labels: string[] | undefined; 37 | datasets: Dataset[]; 38 | } = { 39 | labels: [], 40 | datasets: [ 41 | { 42 | label: ' ms', 43 | fill: false, 44 | lineTension: 0.35, 45 | backgroundColor: 'rgba(75,192,192,1)', 46 | borderColor: 'rgba(0,0,0,1)', 47 | borderWidth: 2, 48 | data: [], 49 | }, 50 | ], 51 | }; 52 | 53 | const options = { 54 | responsive: true, 55 | plugins: { 56 | legend: { 57 | display: false, 58 | }, 59 | }, 60 | }; 61 | 62 | state.labels = metrics.labels; 63 | state.datasets[0].data = metrics.data; 64 | 65 | return ( 66 | <> 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default LineGraph; 73 | -------------------------------------------------------------------------------- /qache-app/client/components/demo-app/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse } from 'axios'; 2 | import { useState } from 'react'; 3 | import '../../styles/demo-styles/Navigation.scss'; 4 | import { useLocation } from 'react-router-dom'; 5 | import { HashLink as Link } from 'react-router-hash-link'; 6 | import { FaBars, FaSearch } from 'react-icons/fa'; 7 | import { AiOutlineClose } from 'react-icons/ai'; 8 | import { IoMdRefresh } from 'react-icons/io'; 9 | import { SidebarData } from './SidebarData'; 10 | import { IconContext } from 'react-icons'; 11 | import SubMenu from './SubMenuData'; 12 | 13 | const Navigation = ({ props }: { props: any }) => { 14 | const { 15 | refresh, 16 | setRefresh, 17 | sidebar, 18 | setSidebar, 19 | hideSidebar, 20 | }: { 21 | refresh: boolean; 22 | setRefresh: React.Dispatch>; 23 | sidebar: boolean; 24 | setSidebar: React.Dispatch>; 25 | hideSidebar: () => void; 26 | } = props; 27 | 28 | const [productMenu, setProductMenu] = useState(false); 29 | const [roomMenu, setRoomMenu] = useState(false); 30 | 31 | const showSidebar = () => setSidebar(true); 32 | 33 | const showSubMenu = (title: String) => { 34 | if (title === 'Products') setProductMenu(true); 35 | if (title === 'Rooms') setRoomMenu(true); 36 | }; 37 | 38 | let location = useLocation(); 39 | const invalidateCache = () => { 40 | const body ={query: ` 41 | { 42 | invalidate 43 | } 44 | `} 45 | const uri = process.env.NODE_ENV === 'development' ? 'http://localhost:3000/graphql': '/graphql' 46 | axios.post(uri, body) 47 | } 48 | 49 | return ( 50 | <> 51 | 52 | 123 | 124 | 125 | ); 126 | }; 127 | 128 | export default Navigation; 129 | -------------------------------------------------------------------------------- /qache-app/client/components/demo-app/ProductDetails.tsx: -------------------------------------------------------------------------------- 1 | import { Product } from '../../../interfaces'; 2 | import '../../styles/demo-styles/ProductDetails.scss'; 3 | import axios, { AxiosResponse } from 'axios' 4 | 5 | const ProductDetails = ({ productData }: { productData: Product[] }) => { 6 | 7 | /* Add to cart button: when clicked, sends product.id to cart 8 | which should add that product to the cart for checkout */ 9 | // const addToCart = (product: { inCart: boolean; }) => { 10 | // product.inCart = true; 11 | // } 12 | 13 | // /* Remove from cart button: when clicked, sends product.id to cart 14 | // which should remove that product from the cart */ 15 | // const removeFromCart = (product: { inCart: boolean; }) => { 16 | // product.inCart = false; 17 | // } 18 | return ( 19 |
20 | {productData.map((product, key) => ( 21 |
22 | {product.onSale ? ( 23 | <> 24 | 25 | 50% OFF 26 | 27 | {product.name 28 | 29 | ) : ( 30 | <> 31 | {product.name 32 | 33 | )} 34 |
35 |

{product.name}

36 | {product.onSale ? ( 37 | 38 | $ 39 | 40 | {product.price.toFixed(2)} 41 | 42 | 43 | $ 44 | 45 | 46 | {(Math.floor((product.price / 2) * 100) / 100).toFixed(2)} 47 | 48 | 49 | 50 | ) : ( 51 | 52 | $ 53 | {product.price.toFixed(2)} 54 | 55 | )} 56 | {product.quantity ? ( 57 | {product.quantity} in stock 58 | ) : ( 59 | Out of stock 60 | )} 61 |

{product.description}

62 | {/* 73 | */} 74 |
75 |
76 | ))} 77 |
78 | ); 79 | }; 80 | 81 | export default ProductDetails; 82 | -------------------------------------------------------------------------------- /qache-app/client/components/demo-app/ProductDisplay.tsx: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse } from 'axios'; 2 | import { useState, useEffect } from 'react'; 3 | import { Product, Metric } from '../../../interfaces'; 4 | import ProductDetails from './ProductDetails'; 5 | import LineGraph from './LineGraph'; 6 | import '../../styles/demo-styles/ProductDisplay.scss'; 7 | 8 | const ProductDisplay = ({ props }: { props: any }) => { 9 | const { 10 | category, 11 | setMetrics, 12 | metrics, 13 | refresh, 14 | }: { 15 | category: string; 16 | setMetrics: React.Dispatch>; 17 | metrics: Metric; 18 | refresh: boolean; 19 | setRefresh: React.Dispatch>; 20 | } = props; 21 | const [productData, setProductData] = useState([]); 22 | const [speed, setSpeed] = useState([]); 23 | 24 | let body = { 25 | query: ` 26 | { 27 | getProductsBy(category: "${category}") { 28 | name 29 | description 30 | imageUrl 31 | quantity 32 | price 33 | onSale 34 | category 35 | } 36 | }`, 37 | }; 38 | 39 | useEffect(() => { 40 | if (category === 'Deals') { 41 | body = { 42 | query: `{ 43 | filterProductsBy(filter: { 44 | onSale: true 45 | }) { 46 | name 47 | description 48 | imageUrl 49 | quantity 50 | price 51 | onSale 52 | } 53 | }`, 54 | }; 55 | } 56 | const t1 = Date.now(); // time before axios post starts 57 | const uri = process.env.NODE_ENV === 'development' ? 'http://localhost:3000/graphql': '/graphql' 58 | axios 59 | .post(uri, body) 60 | .then(({ data }: AxiosResponse) => { 61 | const t2 = Date.now(); 62 | setSpeed([...speed, t2 - t1]); 63 | if (data.data.getProductsBy) setProductData(data.data.getProductsBy); 64 | else if (data.data.filterProductsBy) 65 | setProductData(data.data.filterProductsBy); 66 | }); 67 | }, [category, refresh]); 68 | 69 | useEffect(() => { 70 | if (speed.length) { 71 | const newMetrics = { ...metrics }; 72 | let prevLabel = 73 | newMetrics[category].labels[newMetrics[category].labels.length - 1]; 74 | if (!prevLabel) prevLabel = '0'; 75 | newMetrics[category].labels.push(String(Number(prevLabel) + 1)); 76 | newMetrics[category].data.push(speed[speed.length - 1]); 77 | setMetrics(newMetrics); 78 | } 79 | }, [speed]); 80 | 81 | return ( 82 |
83 |
84 |
85 | Server Latency 86 | Fetches 87 | Cache Speed for {category} 88 | 89 |
90 |
91 | This Chart represents the latency to the server, where the 92 | content for this page was fetched from. 93 |
94 |
95 | When the server needs to receive data from the database, these fetches 96 | can take very long times. 97 |
98 |
99 | Feel free to click the refresh button at the top right, and experience 100 | the speeds our caching solution provides. 101 |
102 |
103 | Our library allows caching data per page, category, single 104 | pieces of information, whatever you need! 105 |
106 |
107 | In addition we allow support for mutations including 108 | create, delete, and update - where only the 109 | relevant data in the cache is updated, immediately! 110 |
111 |
112 |
113 |
114 | {productData ? ( 115 | 116 | ) : ( 117 | No items found 118 | )} 119 |
120 | ); 121 | }; 122 | 123 | export default ProductDisplay; 124 | -------------------------------------------------------------------------------- /qache-app/client/components/demo-app/SidebarData.js: -------------------------------------------------------------------------------- 1 | import { FaChair, FaToilet, FaCouch, FaBed } from 'react-icons/fa'; 2 | import { AiFillHome } from 'react-icons/ai'; 3 | import { IoMdFlash } from 'react-icons/io'; 4 | import { RiArrowUpSFill, RiArrowDownSFill } from 'react-icons/ri'; 5 | import { BiCabinet } from 'react-icons/bi'; 6 | import { GiKnifeFork, GiSofa } from 'react-icons/gi'; 7 | import { MdKitchen } from 'react-icons/md'; 8 | import { FiMonitor } from 'react-icons/fi'; 9 | 10 | export const SidebarData = [ 11 | { 12 | title: 'Home', 13 | path: '/demo-app', 14 | icon: , 15 | cName: 'nav-text', 16 | }, 17 | { 18 | title: 'Products', 19 | path: '#', 20 | icon: , 21 | iconClosed: , 22 | iconOpened: , 23 | subNav: [ 24 | { 25 | title: 'Appliances', 26 | path: 'products/appliances', 27 | icon: , 28 | }, 29 | { 30 | title: 'Couches', 31 | path: 'products/couches', 32 | icon: , 33 | }, 34 | { 35 | title: 'Furniture', 36 | path: 'products/furnitures', 37 | icon: , 38 | }, 39 | { 40 | title: 'Mattresses', 41 | path: 'products/mattresses', 42 | icon: , 43 | }, 44 | { 45 | title: 'Storage', 46 | path: 'products/storage', 47 | icon: , 48 | }, 49 | ], 50 | cName: 'nav-text', 51 | }, 52 | { 53 | title: 'Rooms', 54 | path: '#', 55 | icon: , 56 | iconClosed: , 57 | iconOpened: , 58 | subNav: [ 59 | { 60 | title: 'Bathroom', 61 | path: 'rooms/bathroom', 62 | icon: , 63 | }, 64 | { 65 | title: 'Bedroom', 66 | path: 'rooms/bedroom', 67 | icon: , 68 | }, 69 | { 70 | title: 'Kitchen', 71 | path: 'rooms/kitchen', 72 | icon: , 73 | }, 74 | { 75 | title: 'Living Room', 76 | path: 'rooms/living-room', 77 | icon: , 78 | }, 79 | ], 80 | cName: 'nav-text', 81 | }, 82 | { 83 | title: 'Deals', 84 | path: 'deals', 85 | icon: , 86 | cName: 'nav-text', 87 | }, 88 | ]; 89 | -------------------------------------------------------------------------------- /qache-app/client/components/demo-app/SubMenuData.tsx: -------------------------------------------------------------------------------- 1 | import '../../styles/demo-styles/SubMenuData.scss'; 2 | import { useState, useEffect } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import { Item } from '../../../interfaces'; 5 | 6 | const SubMenu = ({ 7 | item, 8 | sidebar, 9 | productMenu, 10 | setProductMenu, 11 | roomMenu, 12 | setRoomMenu, 13 | hideSidebar, 14 | }: { 15 | item: Item; 16 | sidebar: boolean; 17 | productMenu: boolean; 18 | setProductMenu: React.Dispatch>; 19 | roomMenu: boolean; 20 | setRoomMenu: React.Dispatch>; 21 | hideSidebar: () => void; 22 | }) => { 23 | const [subnav, setSubnav] = useState(false); 24 | 25 | const showSubNav = () => setSubnav(!subnav); 26 | 27 | useEffect(() => { 28 | setSubnav(false); 29 | setProductMenu(false); 30 | setRoomMenu(false); 31 | }, [sidebar]); 32 | 33 | useEffect(() => { 34 | if (item.title === 'Products') setSubnav(true); 35 | }, [productMenu]); 36 | 37 | useEffect(() => { 38 | if (item.title === 'Rooms') setSubnav(true); 39 | }, [roomMenu]); 40 | 41 | return ( 42 |
43 | 48 |
49 | {item.icon} 50 | {item.title} 51 |
52 |
53 | {item.subNav && subnav 54 | ? item.iconOpened 55 | : item.subNav 56 | ? item.iconClosed 57 | : null} 58 |
59 | 60 | 61 | {item.subNav 62 | ? subnav && 63 | item.subNav.map((item: Item, index: number) => { 64 | return ( 65 | 71 |
{item.icon}
72 | {item.title} 73 | 74 | ); 75 | }) 76 | : null} 77 |
78 | ); 79 | }; 80 | 81 | export default SubMenu; 82 | -------------------------------------------------------------------------------- /qache-app/client/components/demo-app/images/transparentlogowithslogan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/components/demo-app/images/transparentlogowithslogan.png -------------------------------------------------------------------------------- /qache-app/client/components/home/Docs.tsx: -------------------------------------------------------------------------------- 1 | import { HashLink as Link } from 'react-router-hash-link'; 2 | import Navbar from './Navbar'; 3 | import '../../styles/home-styles/Docs.scss' 4 | 5 | const scrollWithOffset = (el:any) => { 6 | const yCoordinate = el.getBoundingClientRect().top + window.pageYOffset; 7 | const yOffset = -120; 8 | window.scrollTo({ top: yCoordinate + yOffset, behavior: 'smooth' }); 9 | } 10 | 11 | 12 | const Docs = () => { 13 | return ( 14 | <> 15 | 16 |
17 | {/* SIDE NAVIGATION BAR */} 18 |
19 | Quick Start 20 |
    21 |
  • scrollWithOffset(el)}>Install
  • 22 |
  • scrollWithOffset(el)}>Instantiate
  • 23 |
  • scrollWithOffset(el)}>Set Your First Key
  • 24 |
  • scrollWithOffset(el)}>Get Your Data
  • 25 |
26 | Commands 27 |
    28 |
  • scrollWithOffset(el)}>get
  • 29 |
  • scrollWithOffset(el)}>set
  • 30 |
  • scrollWithOffset(el)}>update
  • 31 |
  • scrollWithOffset(el)}>delete
  • 32 |
  • scrollWithOffset(el)}>listCreate
  • 33 |
  • scrollWithOffset(el)}>listRange
  • 34 |
  • scrollWithOffset(el)}>invalidate
  • 35 |
  • scrollWithOffset(el)}>listPush
  • 36 |
  • scrollWithOffset(el)}>listFetch
  • 37 |
  • scrollWithOffset(el)}>listUpdate
  • 38 |
  • scrollWithOffset(el)}>listUpsert
  • 39 |
  • scrollWithOffset(el)}>listRemoveItem
  • 40 |
41 | scrollWithOffset(el)}>FAQ 42 | {/* CONTENT SECTION */} 43 |
44 |
45 |
46 | Quick Start 47 | 48 |
49 |

Installation

50 |

Installation is a very simple process. Start with a quick npm install qache

51 |
52 | npm install qache 53 |
54 |

This will add qache to your servers dependencies. Then, choose a home for your cache, somewhere where it can be easily referenced. It is recommended you keep it in the same file that you make your database calls.

55 |
56 |
57 |
58 |

Instantiate

59 |

Once you have picked a file to set up your cache, the process is simple.

60 |
61 | const Qache = require('qache') //require in qache
62 | const cache = new Qache() //This line will create a default instance of our Qache 63 |
64 |

65 | Awesome! With just those two lines, every time you run your server, you have access to powerful caching capabilities.
66 | Now you have some decisions to make: in particular, some settings to apply to your cache. 67 |

68 |
69 | 70 | const policies = {'{'}
71 |   timeToLive: 1000*60*10 //time is expected in ms, this line represents 10min
72 |   maxSize: 100
73 |   evictionPolicy: "LFU"
74 | {'}'}
75 | const cache = new Qache(policies)// Pass your policies into the constructor like so 76 |
77 |
78 |

79 | All of these fields are optional, but important to consider.
80 | The eviction policy options are LRU (Least Recently Used - this is selected by default) and LFU (Least Frequently Used).
81 | maxSize and timeToLive are infinte by default. If you have a large amount of data in your database and can't cache everything, it is recommended that you put some limitations on either maxSize or timeToLive. 82 |

83 |
84 |
85 |

Set Your First Key

86 |

87 | To start caching data all we have to do is set our first key!
88 | Keep in mind, we need to catch the data after the database response, but before the return. 89 |

90 |
91 | 92 | getProductById: async function(args){'{'}
93 |   const data = await Product.findOne(args.id); //Your standard DB query
94 |   cache.set(args.id, data); //add this new data to cache
95 |   return data;
96 | {'}'} 97 |
98 |
99 |

Congrats! You just added your first piece of data to cache... but something is missing.

100 |
101 |
102 |

Get Your Data

103 |

What would be the point of adding data to cache, without using it.

104 |
105 | 106 | getProductById: async function(args){'{'}
107 |   const cacheResponse = cache.get(args.id); //Check the cache under the unique key id
108 |   if(cacheResponse) return cacheResponse; // If the cache has what we're looking for, we return it.

109 |   const data = await Product.findOne(args.id); //Your standard DB query
110 |   cache.set(args.id, data); //add this new data to cache
111 |   return data;
112 | {'}'} 113 |
114 |
115 |

116 | Now that's what we're talking about! In 5 lines, we check our cache, and set the data to cache if it wasn't found the first time.
117 | Below you can find a complete list of the commands, and be sure to consider maintaining the validity of your data.
118 | There are commands for every mutation! 119 |

120 |
121 |
122 | 123 |
124 | Commands 125 |
126 |

get(key)

127 |

Time Complexity: O(1)

128 |

Used to retrieve data stored under a predetermined key. See "Set" below

129 |
130 |
131 |

set(key, value)

132 |

Time Complexity: O(1)

133 |

Intended for storing singleton data, i.e. data with a unique identifier. This function has many uses, such as storing data by id, or by a username, so it can easily save you many trips to a database to retrieve those pieces of data.

134 |
135 |
136 |

update(key, value)

137 |

Time Complexity: O(1)

138 |

Used to update data of a key that already exists

139 |
140 |
141 |

delete(key)

142 |

Time Complexity: O(1)

143 |

Removes a key that should no longer be in cache, such as if an item was deleted out of the database, and the user wants the stored data in cache to stay fresh.

144 |
145 |
146 |

invalidate(...keys)

147 |

Time Complexity: O(n) where n = number of keys requiring deletion

148 |

invalidate, or "lazy invalidation". This command can take in one or more keys, and delete the data, and the nodes associated with each key. If you run into an event that leads to a complicated situation when trying to keep data fresh and valid, consider invalidating the cache after such an event. We call this method lazy, because it is to be used as a last resort. We believe our methods can keep most sets of data fresh in cache for a long time.

149 |
150 |
151 |

listCreate(listKey, ...item)

152 |

Time Complexity: O(1)

153 |

Intended to store data in a systematic way. By page, by category, or by anything else that can identify a group. After you have retrieved a certain dataset, store it under a key that identifies that group, to retrieve it faster later.

154 |
155 |
156 |

listRange(listKey, start, end)

157 |

Time Complexity: O(1) if no range specified, otherwise O(n) where n is the length of the list

158 |

Used to retrieve a previously stored list. You can return an entire list by just passing in the list's unique key, or you can get a range, to return e.g. only the first 10, or the last 20.

159 |
160 |
161 |

listFetch(listKey, filterObject)

162 |

Time Complexity: O(n) where n is the length of the list

163 |

Used similarly to listRange, this function takes a filterObject. Pass in the key of whatever you want to filter by, and the value you're looking for, e.g. {'{'}category: "couch"{'}'} to retrieve all results that match that description.

164 |
165 |
166 |

listPush(item, ...listKeys)

167 |

Time Complexity: O(1)

168 |

This function can be used to add new items to an existing list. For example, if we just added a new listing and don't want to invalidate our cache, we can just listPush the new listing to the existing list.

169 |
170 |
171 |

listUpdate(updatedItem, filterObject, ...listKey)

172 |

Time Complexity: O(n) where n is the length of the list

173 |

Used to update the value of items currently in a list. Similar to the previous function, if we want to maintain freshness of our cache, we can use this function to update each list that a given item belonged to. This method takes in a filterObject, which can be used with a unique identifier to find the item in need of updating.

174 |
175 |
176 |

listUpsert(item, filterObject, ...listKeys)

177 |

Time Complexity: O(n) where n is the length of the list

178 |

This function is dual-purpose. Similar to both listPush and listUpdate, this method will scan one or multiple lists for an item. Then, when the item is found, the function updates it, and if the item is not found, the item is added to the given list(s). Very useful in tricky situations, when you don't know if an item has been added in your cache.

179 |
180 |
181 |

listRemoveItem(filterObject, ...listKey)

182 |

Time Complexity: O(n) where n is the length of the list

183 |

As the name suggests, this function removes an item from one or several lists in your cache. It is useful when an item has been deleted from the database, and you want to reflect that instantly in your cache.

184 |
185 |
186 | 187 |
188 | FAQ 189 |
190 |

How is Qache different from other key-value stores?

191 |

192 | Qache's main draws are its ease of use and gentle learning curve. But behind that, lies a powerful and modular suite of methods to handle complex situations.
193 | Everything is handled in the same server being serviced by the cache. This has several important consequences:
194 |

  • Qache horizontally scales in tandem with servers. Each server AUTOMATICALLY has its own instance of qache! Keep in mind however that you will need to adopt an approach to handle cache updates, such as employing a service worker process.
  • 195 |
  • Qache requires very little upkeep after it's initial set up.
  • 196 |
  • In most cases Qache fits perfectly into an ACID compliant system, and can be integrated into existing systems with ease.
  • 197 |

    198 |
    199 |
    200 |

    Will a Qache ever be a bottleneck for my server?

    201 |

    202 | It is nearly impossible for Qache to be the bottleneck of your server. We set out to break apart the bottlenecks that come with high site traffic, or large amounts of database queries, even more specifically GraphQL queries, which are known to sometimes make multiple DB queries in a single request.
    203 | We accomplished that task, and surpassed our expectations.
    204 | To be more specific, every Qache read operation can be performed in constant time, or if neccesary, e.g. for filtered results, linear time. This means Qache can get through millions of requests without ever getting bogged down. 205 |

    206 |
    207 |
    208 |

    Is there a way to control how much memory is used?

    209 |

    210 | We have built in a few powerful settings that allow for you to use up as much or as little memory as you want by setting a max key limit or a short expiration date.
    211 | If those settings don't suffice, you can take further steps to store your data in stringified form, and parse it when you receive it. 212 |

    213 |
    214 |
    215 |

    What's a good strategy for keeping data in cache accurate?

    216 |

    217 | It varies from case to case, but the greatest rule of thumb is this: if a request is making an update, it should also update the cache. And the same goes for deleting and creating. Whatever database action is occurring, mirror it with a cache action.
    218 | If the idea of an inaccurate cache scares you, cache.invalidate() after every mutation will ease your worries! 219 |

    220 |
    221 |
    222 |

    How is Qache pronounced?

    223 |

    224 | kwaash :{')'} 225 |

    226 |
    227 |
    228 | 229 |
    230 |
    231 | 232 | ); 233 | }; 234 | 235 | export default Docs; 236 | -------------------------------------------------------------------------------- /qache-app/client/components/home/Home.tsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom'; 2 | import Navbar from './Navbar'; 3 | import Introduction from './Introduction'; 4 | 5 | const Home = () => { 6 | return ( 7 | <> 8 | 9 | 10 | } /> 11 | 12 | 13 | ) 14 | }; 15 | 16 | export default Home; -------------------------------------------------------------------------------- /qache-app/client/components/home/Introduction.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import '../../styles/home-styles/Introduction.scss'; 3 | import { MdOutlineArrowForwardIos } from 'react-icons/md'; 4 | import { AiOutlineArrowRight } from 'react-icons/ai'; 5 | import LineGraph from '../demo-app/LineGraph'; 6 | import Team from './Team'; 7 | import preview from '../../images/preview.gif'; 8 | import demo from '../../images/demo.gif'; 9 | import navigate from '../../images/navigate.gif'; 10 | import qache from '../../images/qache.gif'; 11 | import { motion } from 'framer-motion'; 12 | import { useInView } from 'react-intersection-observer'; 13 | 14 | const Introduction = () => { 15 | const [ref1, inView1] = useInView({ 16 | triggerOnce: true, 17 | }); 18 | const [ref2, inView2] = useInView({ 19 | triggerOnce: true, 20 | }); 21 | const [ref3, inView3] = useInView({ 22 | triggerOnce: true, 23 | }); 24 | const [ref4, inView4] = useInView({ 25 | triggerOnce: true, 26 | }); 27 | 28 | const copyToClipboard = () => { 29 | const range = document.createRange(); 30 | range.selectNode(document.getElementsByClassName('copy-button')[0]); 31 | window.getSelection()?.removeAllRanges(); 32 | window.getSelection()?.addRange(range); 33 | document.execCommand('copy'); 34 | window.getSelection()?.removeAllRanges(); 35 | }; 36 | 37 | return ( 38 | <> 39 |
    40 |
    41 |

    42 | Qache 43 |

    44 |

    A modular caching solution for your database

    45 |
    46 |
    50 | 51 | 55 | npm i qache 56 | 57 |
    58 |
    59 | Get Started 60 |
    61 |
    62 |
    63 |
    64 | 65 |
    66 |

    67 | 72 | Qache 1.0.5 73 | {' '} 74 | is now available. 75 |

    76 |
    77 | 78 |
    79 | 88 | 89 | 94 | 99 | 100 | 101 | 102 | 103 | 104 | 112 | 113 | 114 | 115 | 123 | 124 | 125 | 126 | 127 | 128 |
    129 |

    Qache Your Data

    130 |

    131 | Qache is a modular utility class for handling server side caching of 132 | data. It works with any database and API architecture including 133 | GraphQL and RESTful APIs. 134 |

    135 |
    136 |
    137 | 145 |
    146 |
    147 | 148 |
    149 |
    150 |

    Demo our Qache Tool

    151 |
    152 | 159 | 165 |

    Preview

    166 |

    167 | We built a Demo application to see in real time how our caching 168 | solution would work on a mock database we created with MongoDB 169 | and GraphQL. Behind the scenes, we've implemented our Qache 170 | methods to handle query performance, optimizing it with server 171 | side caching. 172 |

    173 |
    174 |
    175 |
    176 |
    177 |
    178 |
    179 | 186 | 192 |

    Browsing Through

    193 |

    194 | In our Demo application, you can browse through the navigation 195 | bar and sidebar, allowing you to go through our products, rooms, 196 | and deals. 197 |

    198 |
    199 |
    200 |
    201 |
    202 |
    203 |
    204 | 211 | 217 |

    Latency Graph

    218 |

    219 | Our Demo application shows a line graph for each of the pages. 220 | The landing page has an example line graph of what will be shown 221 | when you visit any of the pages. The line graph shows the speed 222 | in miliseconds on the y-axis and the number of fetches/queries 223 | to the database and our server side cache on the x-axis. 224 |

    225 |
    226 |
    227 |
    228 |
    229 |
    230 |
    231 | 238 | 244 |

    Qache Tool

    245 |

    246 | The first point on the line graph describes how fast our query 247 | takes to fetch the data from the database and returning it back 248 | to the client which displays all the products on the page. 249 | Behind the scenes, our qache tool stashes that response data 250 | into the cache. Above the graph is a refresh button that allows 251 | you to refetch the data. It creates a new data point in the line 252 | graph every time you click on it, but instead of fetching the 253 | data from the database again, it checks if the data lies in the 254 | server side cache first, and if it is, it will return the data 255 | from the cache, cutting the time needed to go to the database. 256 |

    257 |
    258 |
    259 |
    260 |
    261 | 262 | 263 | ); 264 | }; 265 | 266 | export default Introduction; 267 | -------------------------------------------------------------------------------- /qache-app/client/components/home/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import '../../styles/home-styles/Navbar.scss'; 2 | import { HashLink as Link } from 'react-router-hash-link'; 3 | import { FaGithub, FaLinkedin } from 'react-icons/fa'; 4 | import { IconContext } from 'react-icons'; 5 | import qache from '../../images/qache.png'; 6 | 7 | const scrollWithOffset = (el:any) => { 8 | const yCoordinate = el.getBoundingClientRect().top + window.pageYOffset; 9 | const yOffset = -120; 10 | window.scrollTo({ top: yCoordinate + yOffset, behavior: 'smooth' }); 11 | } 12 | 13 | const Navbar = () => { 14 | return ( 15 | <> 16 | 17 | 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default Navbar; 73 | -------------------------------------------------------------------------------- /qache-app/client/components/home/Team.tsx: -------------------------------------------------------------------------------- 1 | import { FaGithub, FaLinkedin } from 'react-icons/fa'; 2 | 3 | const shuffleArray = (array: {}[]) => { 4 | const outputArray: {}[] = []; 5 | const used: any = new Set(); 6 | while (outputArray.length !== array.length) { 7 | const rand: number = Math.floor(Math.random() * array.length); 8 | if (!used.has(array[rand])) { 9 | outputArray.push(array[rand]); 10 | used.add(array[rand]); 11 | } else continue; 12 | } 13 | return outputArray; 14 | }; 15 | 16 | const unshuffledPeople = [ 17 | { 18 | name: 'Nader Almogazy', 19 | role: 'Full-Stack Engineer', 20 | imageUrl: 21 | 'https://media-exp1.licdn.com/dms/image/C4E03AQF8fJfVlCNFPg/profile-displayphoto-shrink_800_800/0/1639494320094?e=1649289600&v=beta&t=9tnZvGoJuLvmwoWfCPmmJCTGHCiP6wy06sbLKskn4k8', 22 | githubUrl: 'https://github.com/nader12334', 23 | linkedinUrl: 'https://www.linkedin.com/in/naderalmogazy/', 24 | }, 25 | { 26 | name: 'Steven Du', 27 | role: 'Full-Stack Engineer', 28 | imageUrl: 29 | 'https://media-exp1.licdn.com/dms/image/C5603AQFDqqzbMsHLkw/profile-displayphoto-shrink_800_800/0/1614478246591?e=1649289600&v=beta&t=rF9o-U-6HfdMOjy_qSy5iqperqrRcU0s9N47fvOam3c', 30 | githubUrl: 'https://github.com/stebed', 31 | linkedinUrl: 'https://www.linkedin.com/in/stevendu/', 32 | }, 33 | { 34 | name: 'Evan Preedy', 35 | role: 'Full-Stack Engineer', 36 | imageUrl: 37 | 'https://media-exp1.licdn.com/dms/image/C4D03AQHJYHhjminKOQ/profile-displayphoto-shrink_800_800/0/1583715406957?e=1649289600&v=beta&t=Xe-g-MbDEGdm9EQA0Wsx0LBqrxMDpysZjMxAKXixZKY', 38 | githubUrl: 'https://github.com/ep1815', 39 | linkedinUrl: 'https://www.linkedin.com/in/evan-preedy/', 40 | }, 41 | { 42 | name: 'Leo Crossman', 43 | role: 'Full-Stack Engineer', 44 | imageUrl: 45 | 'https://media-exp1.licdn.com/dms/image/C5603AQGv8VLWelgLgA/profile-displayphoto-shrink_800_800/0/1540586637094?e=1649289600&v=beta&t=0fynsTHefEKnH-6JtLjh0Djy5-cQXmX6E_QtzptnO2w', 46 | githubUrl: 'https://github.com/leocrossman', 47 | linkedinUrl: 'https://www.linkedin.com/in/leocrossman', 48 | }, 49 | ]; 50 | 51 | const people: {}[] = shuffleArray(unshuffledPeople); 52 | 53 | const Team = () => { 54 | return ( 55 |
    56 |
    57 |
    58 |
    59 |

    60 | Meet our team 61 |

    62 |

    63 | Connect with our engineers! 64 |

    65 |
    66 | 130 |
    131 |
    132 |
    133 | ); 134 | }; 135 | 136 | export default Team; 137 | -------------------------------------------------------------------------------- /qache-app/client/images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/demo.gif -------------------------------------------------------------------------------- /qache-app/client/images/navigate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/navigate.gif -------------------------------------------------------------------------------- /qache-app/client/images/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/preview.gif -------------------------------------------------------------------------------- /qache-app/client/images/qache.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/qache.gif -------------------------------------------------------------------------------- /qache-app/client/images/qache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/qache.png -------------------------------------------------------------------------------- /qache-app/client/images/qacheslogan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/qacheslogan.png -------------------------------------------------------------------------------- /qache-app/client/images/storage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/storage.jpg -------------------------------------------------------------------------------- /qache-app/client/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './App'; 5 | import './styles/index.css'; 6 | import './styles/index.scss'; 7 | 8 | // import { store } from './Redux/store'; 9 | // import { Provider } from 'react-redux'; 10 | 11 | ReactDOM.render( 12 | // 13 | 14 | 15 | 16 | 17 | , 18 | // , 19 | document.getElementById('app') 20 | ); 21 | -------------------------------------------------------------------------------- /qache-app/client/styles/demo-styles/LandingPage.scss: -------------------------------------------------------------------------------- 1 | .landingPage-container { 2 | display: flex; 3 | flex-direction: column; 4 | h1{ 5 | text-align: center; 6 | } 7 | .section-container { 8 | display: flex; 9 | justify-content: space-evenly; 10 | flex-direction: row; 11 | } 12 | .imageContainer{ 13 | display: flex; 14 | position: relative; 15 | justify-content: center; 16 | align-items: center; 17 | overflow: hidden; 18 | width: 50%; 19 | } 20 | img { 21 | object-fit: cover; 22 | flex-shrink: 0; 23 | height: 80%; 24 | } 25 | 26 | .img-caption { 27 | display: flex; 28 | justify-content: flex-start; 29 | align-items: center; 30 | position: absolute; 31 | overflow: hidden; 32 | top: 75%; 33 | height: 5rem; 34 | backdrop-filter: blur(25px); 35 | h2 { 36 | font-weight: 300; 37 | } 38 | } 39 | 40 | .graph-container { 41 | display: flex; 42 | flex-direction: column; 43 | align-items: stretch; 44 | h2 { 45 | margin: 0; 46 | font-size: 2rem; 47 | text-align: center; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /qache-app/client/styles/demo-styles/Navigation.scss: -------------------------------------------------------------------------------- 1 | @import '../variables.scss'; 2 | 3 | #nav-container { 4 | display: flex; 5 | justify-content: flex-start; 6 | align-items: center; 7 | height: 10%; 8 | background-color: $white; 9 | white-space: nowrap; 10 | .navbar { 11 | display: flex; 12 | justify-content: space-evenly; 13 | align-items: center; 14 | height: 100%; 15 | width: 100%; 16 | height: 80px; 17 | padding-top: 1%; 18 | padding-left: 5%; 19 | padding-right: 5%; 20 | background-color: $offWhite; 21 | border-bottom: solid 1px #dfdfdf; 22 | 23 | .menu-bars { 24 | display: flex; 25 | justify-content: space-between; 26 | margin-left: 0; 27 | font-size: 2rem; 28 | background: none; 29 | } 30 | 31 | a { 32 | font-size: 1.5rem; 33 | font-weight: bold; 34 | text-decoration: none; 35 | color: black; 36 | cursor: pointer; 37 | 38 | &:hover, 39 | &:focus { 40 | text-decoration: underline; 41 | } 42 | } 43 | 44 | .search { 45 | width: 40%; 46 | display: flex; 47 | position: relative; 48 | input { 49 | width: 100%; 50 | padding: 10px; 51 | padding-left: 1.75rem; 52 | font-size: large; 53 | background-color: $offWhite; 54 | } 55 | .icon { 56 | position: absolute; 57 | top: 14px; 58 | left: 7px; 59 | } 60 | } 61 | 62 | .not-active { 63 | visibility: hidden; 64 | } 65 | 66 | .active { 67 | visibility: visible; 68 | background-color: transparent; 69 | border: none; 70 | cursor: pointer; 71 | font-size: xx-large; 72 | 73 | &:active { 74 | transform: translateY(1.5px); 75 | background: none; 76 | background-color: transparent; 77 | } 78 | 79 | &:active .tooltip { 80 | box-shadow: none; 81 | } 82 | 83 | .tooltip { 84 | padding: 10px; 85 | position: absolute; 86 | width: auto; 87 | white-space: nowrap; 88 | word-wrap: no-wrap; 89 | box-shadow: 1px 1px 20px #aaa; 90 | border-radius: 5px; 91 | background-color: #fff; 92 | top: 85px; 93 | left: 90.75%; 94 | transform: translate(-50%); 95 | transform-style: preserve-3d; 96 | z-index: 200; 97 | font-size: small; 98 | display: none; 99 | 100 | &:after { 101 | content: ''; 102 | position: absolute; 103 | display: block; 104 | width: 10px; 105 | height: 10px; 106 | transform-origin: 50% 50%; 107 | transform: rotate(45deg) translateX(-50%); 108 | background-color: #fff; 109 | left: 50%; 110 | top: -1px; 111 | z-index: 400; 112 | } 113 | } 114 | 115 | &:hover .tooltip { 116 | display: block; 117 | } 118 | 119 | .spinner:hover { 120 | animation: spin infinite 1s linear; 121 | 122 | @keyframes spin { 123 | from { 124 | transform: rotate(0deg); 125 | } 126 | to { 127 | transform: rotate(360deg); 128 | } 129 | } 130 | } 131 | } 132 | } 133 | 134 | .nav-menu { 135 | display: flex; 136 | justify-content: center; 137 | position: fixed; 138 | width: 250px; 139 | height: 300vh; 140 | top: 0; 141 | left: -100%; 142 | transition: 750ms; 143 | background-color: $offWhite; 144 | overflow-y: scroll !important; 145 | } 146 | 147 | .nav-menu.active { 148 | padding-left: 1.5%; 149 | left: 0; 150 | z-index: 10; 151 | transition: 250ms; 152 | } 153 | 154 | ::-webkit-scrollbar { 155 | width: 0px; 156 | background: transparent; /* make scrollbar transparent */ 157 | } 158 | 159 | .nav-menu-items { 160 | width: 100%; 161 | 162 | .navbar-toggle { 163 | display: flex; 164 | justify-content: flex-start; 165 | align-items: center; 166 | height: 5rem; 167 | margin-left: 1rem; 168 | font-size: large; 169 | background-color: $offWhite; 170 | cursor: pointer; 171 | } 172 | } 173 | } 174 | 175 | // dim the background when the sidebar is open 176 | .sidebar-overlay::after { 177 | content: ''; 178 | position: fixed; 179 | width: 100%; 180 | height: 100%; 181 | background-color: rgba(0, 0, 0, 0.5); 182 | left: 0; 183 | top: 0; 184 | z-index: 10; 185 | } 186 | -------------------------------------------------------------------------------- /qache-app/client/styles/demo-styles/ProductDetails.scss: -------------------------------------------------------------------------------- 1 | .products-wrap { 2 | display: flex; 3 | flex-wrap: wrap; 4 | width: 100%; 5 | z-index: -1; 6 | 7 | .product-container { 8 | img { 9 | opacity: 0.85; 10 | } 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | width: 21%; 15 | padding: 2%; 16 | border-radius: 3px; 17 | background-color: rgba(255, 255, 255, 0.85); 18 | border-top: 1px solid lightgrey; 19 | 20 | .sale { 21 | background: transparent; 22 | height: 3rem; 23 | width: 3rem; 24 | position: relative; 25 | left: 50%; 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | text-align: center; 30 | transform: rotate(20deg); 31 | animation: beat 1s ease infinite alternate; 32 | overflow: hidden; 33 | z-index: 1; 34 | } 35 | .sale:before { 36 | content: ''; 37 | position: absolute; 38 | top: 0; 39 | left: 0; 40 | height: 3rem; 41 | width: 3rem; 42 | background: green; 43 | color: white; 44 | transform: rotate(135deg); 45 | z-index: -1; 46 | } 47 | @keyframes beat { 48 | from { 49 | transform: rotate(20deg) scale(1); 50 | } 51 | to { 52 | transform: rotate(20deg) scale(1.1); 53 | } 54 | } 55 | 56 | .product-info { 57 | display: flex; 58 | width: 100%; 59 | flex-direction: column; 60 | 61 | strong { 62 | color: red; 63 | 64 | .onSale { 65 | text-decoration: line-through; 66 | } 67 | } 68 | .newPrice { 69 | color: green; 70 | } 71 | 72 | .noSale { 73 | color: black; 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /qache-app/client/styles/demo-styles/ProductDisplay.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .productDisplay-container { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | flex-direction: column; 8 | padding-left: 5%; 9 | padding-right: 5%; 10 | 11 | h1 { 12 | width: 100%; 13 | margin: 20px; 14 | padding-left: 200px; 15 | padding-bottom: 20px; 16 | } 17 | 18 | .cache-line { 19 | display: flex; 20 | width: 100%; 21 | justify-content: space-evenly; 22 | 23 | .talkingPoints{ 24 | width: 20%; 25 | margin-top: 20px; 26 | font-size: 1vw; 27 | padding: 1.25%; 28 | } 29 | .lineGraphContainer{ 30 | position: relative; 31 | width: 80%; 32 | margin-top: 20px; 33 | font-size: 1vw; 34 | canvas{ 35 | width: 100%; 36 | height: 100%; 37 | padding: 5%; 38 | } 39 | .title{ 40 | position: absolute; 41 | left: 48%; 42 | top: 5%; 43 | } 44 | .yLabel{ 45 | position: absolute; 46 | top: 50%; 47 | left: -3%; 48 | transform: rotate(270deg); 49 | } 50 | .xLabel{ 51 | position: absolute; 52 | top: 90%; 53 | left: 50%; 54 | } 55 | } 56 | 57 | .image-container { 58 | display: flex; 59 | justify-content: center; 60 | align-items: center; 61 | overflow-y: hidden; 62 | border-radius: 3px; 63 | 64 | img { 65 | object-fit: cover; 66 | width: 400px; 67 | height: 400px; 68 | } 69 | } 70 | } 71 | 72 | img { 73 | width: 100%; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /qache-app/client/styles/demo-styles/SubMenuData.scss: -------------------------------------------------------------------------------- 1 | .sidebarLink { 2 | display: flex; 3 | justify-content: start; 4 | align-items: center; 5 | height: 5rem; 6 | padding: 0.5rem 0 0.5rem 1rem; 7 | text-decoration: none; 8 | border-radius: 0.25rem; 9 | font-size: x-large; 10 | color: black; 11 | 12 | svg { 13 | display: inline-block; 14 | position: relative; 15 | vertical-align: baseline; 16 | top: 0.25rem; 17 | } 18 | span { 19 | display: inline-block; 20 | margin-left: 1rem; 21 | &:hover { 22 | text-decoration: underline; 23 | } 24 | } 25 | } 26 | 27 | .dropdownLink { 28 | height: 60px; 29 | padding-left: 3rem; 30 | display: flex; 31 | align-items: center; 32 | text-decoration: none; 33 | border-radius: 0.25rem; 34 | font-size: large; 35 | color: black; 36 | 37 | &:hover { 38 | text-decoration: underline; 39 | } 40 | 41 | .subIcon { 42 | display: flex; 43 | align-items: center; 44 | padding-right: 1rem; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /qache-app/client/styles/home-styles/Docs.scss: -------------------------------------------------------------------------------- 1 | @import '../variables.scss'; 2 | 3 | body { 4 | .code-block{ 5 | margin: 10px 30px; 6 | padding: 10px; 7 | width: 80%; 8 | min-height: 100px; 9 | color: rgb(223, 223, 223); 10 | display: inline-block; 11 | font-size: small; 12 | background-color: rgb(19, 19, 19); 13 | border-radius: 5px; 14 | border: 1px solid gray 15 | } 16 | .gr{ 17 | color: rgba(138, 138, 138, 0.616) 18 | } 19 | .g{ 20 | color: rgb(101, 223, 101) 21 | } 22 | .r{ 23 | color: rgb(241, 54, 54) 24 | } 25 | .b{ 26 | color: rgb(91, 189, 228) 27 | } 28 | .y{ 29 | color: rgb(230, 228, 119) 30 | } 31 | .p{ 32 | color: rgb(176, 111, 236); 33 | } 34 | #docs { 35 | display: flex; 36 | width: 100%; 37 | background-color: $softBlack; 38 | color: $white; 39 | 40 | strong { 41 | color: $cyan; 42 | margin: 10px 10px; 43 | } 44 | 45 | .title { 46 | font-size: x-large; 47 | color: $offerWhite; 48 | font-weight: 600; 49 | margin-left: 20px; 50 | padding-bottom: 5px; 51 | border-bottom: 1px solid gray; 52 | } 53 | .time { 54 | color: $cyan; 55 | font-size: large; 56 | margin: 5px 20px; 57 | } 58 | .body { 59 | font-size: large; 60 | margin-left: 20px; 61 | } 62 | 63 | #docs-nav { 64 | padding: 10px; 65 | font-size: larger; 66 | position: fixed; 67 | top: 130px; 68 | width: 200px; 69 | height: 100vh; 70 | border-right: 1px solid gray; 71 | li { 72 | margin-left: 30px; 73 | color: $offerWhite; 74 | } 75 | } 76 | 77 | #docs-content { 78 | display: flex; 79 | width: auto; 80 | padding-top: 130px; 81 | margin-left: 230px; 82 | flex-direction: column; 83 | font-size: x-large; 84 | strong{ 85 | font-size: xx-large; 86 | margin-left: -10px; 87 | } 88 | .section { 89 | background-color: $offBlack; 90 | margin: 0px 5px 10px 5px; 91 | width: 90%; 92 | border-radius: 10px; 93 | padding: 10px 0px 20px 0px; 94 | 95 | .title { 96 | margin: 0; 97 | padding-left: 20px; 98 | color: $offWhite; 99 | } 100 | 101 | .time { 102 | margin-bottom: 10px; 103 | } 104 | .body { 105 | margin-top: 10px; 106 | color: $offerWhite; 107 | } 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /qache-app/client/styles/home-styles/Introduction.scss: -------------------------------------------------------------------------------- 1 | @import '../variables.scss'; 2 | 3 | .introduction-container { 4 | display: flex; 5 | height: 40%; 6 | justify-content: space-around; 7 | align-items: center; 8 | background-color: $softBlack; 9 | padding: 2%; 10 | padding-top: 120px; 11 | 12 | .header-texts { 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | color: $white; 17 | 18 | h1 { 19 | font-size: 3.5rem; 20 | font-weight: bold; 21 | margin-bottom: 2%; 22 | } 23 | 24 | p { 25 | font-weight: 200; 26 | font-size: 1.5rem; 27 | letter-spacing: 0.05rem; 28 | } 29 | 30 | h1, 31 | p { 32 | margin: 0; 33 | } 34 | 35 | p, 36 | .buttons-container { 37 | display: flex; 38 | justify-content: space-between; 39 | padding-top: 2%; 40 | padding-bottom: 2%; 41 | } 42 | 43 | strong { 44 | color: $cyan; 45 | } 46 | 47 | .buttons-container { 48 | display: flex; 49 | align-items: center; 50 | width: 50%; 51 | 52 | .copy-button { 53 | display: flex; 54 | background-color: $cyan; 55 | padding: 20px 25px; 56 | border-radius: 5px; 57 | transition: $quick; 58 | cursor: pointer; 59 | 60 | .forward-arrow { 61 | display: inline-block; 62 | } 63 | 64 | code { 65 | display: inline-block; 66 | color: $black; 67 | font-weight: 600; 68 | } 69 | 70 | &:hover { 71 | background-color: $white; 72 | } 73 | 74 | &:active { 75 | transform: scale(0.95); 76 | } 77 | } 78 | 79 | /* Curl Top Right */ 80 | .hvr-curl-top-right { 81 | -webkit-transform: perspective(1px) translateZ(0); 82 | transform: perspective(1px) translateZ(0); 83 | box-shadow: 0 0 1px rgba(0, 0, 0, 0); 84 | } 85 | 86 | .hvr-curl-top-right::before { 87 | pointer-events: none; 88 | position: absolute; 89 | content: ''; 90 | height: 0; 91 | width: 0; 92 | top: 0; 93 | right: 0; 94 | background: white; 95 | border-radius: 5px; 96 | /* IE9 */ 97 | background: linear-gradient( 98 | 225deg, 99 | white 45%, 100 | #aaa 50%, 101 | #ccc 56%, 102 | white 80% 103 | ); 104 | box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.4); 105 | -webkit-transition-duration: 0.3s; 106 | transition-duration: 0.3s; 107 | -webkit-transition-property: width, height; 108 | transition-property: width, height; 109 | } 110 | 111 | .hvr-curl-top-right:hover::before, 112 | .hvr-curl-top-right:focus::before, 113 | .hvr-curl-top-right:active::before { 114 | width: 25px; 115 | height: 25px; 116 | border-radius: 5px; 117 | } 118 | } 119 | } 120 | 121 | .blue-button, 122 | a { 123 | display: inline-block; 124 | color: $cyan; 125 | text-decoration: none; 126 | vertical-align: bottom; 127 | cursor: pointer; 128 | 129 | &:hover { 130 | color: $white; 131 | } 132 | 133 | &:active { 134 | transform: scale(0.95); 135 | } 136 | } 137 | } 138 | 139 | .version-banner { 140 | display: block; 141 | text-align: center; 142 | background-color: $black; 143 | color: $white; 144 | height: 5%; 145 | padding: 0.5%; 146 | 147 | a { 148 | color: $cyan; 149 | text-decoration: underline; 150 | 151 | &:hover { 152 | color: $white; 153 | } 154 | } 155 | } 156 | 157 | .overview-container { 158 | display: flex; 159 | max-width: 100vw; 160 | max-height: 45%; 161 | background-color: $offWhite; 162 | 163 | .bg { 164 | position: absolute; 165 | z-index: 1; 166 | width: 100%; 167 | max-height: 45%; 168 | filter: blur(10px); 169 | } 170 | 171 | .overview-texts, 172 | .overview-graph { 173 | display: flex; 174 | flex-direction: column; 175 | max-width: 50%; 176 | width: 50%; 177 | padding: 3.5% 5%; 178 | } 179 | 180 | .overview-texts { 181 | justify-content: center; 182 | align-items: flex-start; 183 | 184 | h3 { 185 | font-size: 2rem; 186 | font-weight: 300; 187 | margin-bottom: 2%; 188 | } 189 | 190 | p { 191 | font-size: 1.25rem; 192 | width: 75%; 193 | margin-top: 1%; 194 | margin-bottom: 1%; 195 | } 196 | } 197 | 198 | .overview-graph { 199 | background-color: $offWhite; 200 | backdrop-filter: blur(10px); 201 | 202 | canvas { 203 | backdrop-filter: blur(10px); 204 | background-color: rgba(255, 255, 255, 0.5); 205 | border-radius: 5px; 206 | } 207 | } 208 | } 209 | 210 | .features-container { 211 | display: flex; 212 | flex-direction: column; 213 | align-items: center; 214 | justify-content: center; 215 | padding: 1%; 216 | background-color: $offWhite; 217 | 218 | .demo-feature-container { 219 | display: flex; 220 | flex-direction: column; 221 | color: $black; 222 | background-color: #f5f5f4; 223 | width: 75%; 224 | padding: 3%; 225 | box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; 226 | 227 | h1 { 228 | font-size: 2.5rem; 229 | font-weight: 600; 230 | margin-top: 0; 231 | margin-bottom: 5%; 232 | text-align: center; 233 | } 234 | 235 | hr { 236 | border: 1px solid $zinc; 237 | } 238 | 239 | .demo-container { 240 | display: flex; 241 | padding: 1%; 242 | 243 | img { 244 | max-width: 50%; 245 | margin-right: 2.5%; 246 | } 247 | 248 | .text { 249 | width: 100%; 250 | } 251 | 252 | h3 { 253 | font-size: 1.75rem; 254 | font-weight: 400; 255 | margin-bottom: 1%; 256 | } 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /qache-app/client/styles/home-styles/Navbar.scss: -------------------------------------------------------------------------------- 1 | @import '../variables.scss'; 2 | 3 | .navbar-container { 4 | display: flex; 5 | position: fixed; 6 | justify-content: space-between; 7 | width: 100%; 8 | height: 120px; 9 | padding-right: 1%; 10 | padding-left: 1%; 11 | z-index: 1; 12 | top: 0; 13 | background-color: $black; 14 | white-space: nowrap; 15 | 16 | .image-container { 17 | display: flex; 18 | width: 200px; 19 | margin-right: 10; 20 | img { 21 | height: 100%; 22 | object-fit: contain; 23 | box-sizing: border-box; 24 | vertical-align: middle; 25 | } 26 | } 27 | 28 | .left-nav, 29 | .right-nav { 30 | display: flex; 31 | 32 | 33 | .nav-list { 34 | display: flex; 35 | align-items: center; 36 | margin-left: auto; 37 | 38 | li{ 39 | margin: 0 10px; 40 | } 41 | 42 | .nav-link { 43 | text-decoration: none; 44 | font-size: large; 45 | font-weight: 550; 46 | color: $white; 47 | display: block; 48 | position: relative; 49 | padding: 0.5rem 0; 50 | margin-left: 2rem; 51 | 52 | &::after { 53 | content: ''; 54 | position: absolute; 55 | bottom: 0; 56 | left: 0; 57 | width: 100%; 58 | height: 0.1rem; 59 | background-color: $cyan; 60 | transition: opacity $quick, transform $quick; 61 | opacity: 1; 62 | transform: scale(0); 63 | transform-origin: center; 64 | } 65 | 66 | &:hover, &:hover::after, &:focus::after { 67 | color: $cyan; 68 | transform: scale(1); 69 | } 70 | } 71 | } 72 | 73 | .nav-list:nth-child(1) { 74 | justify-content: flex-end; 75 | 76 | .nav-icons { 77 | height: 100%; 78 | 79 | path:hover, svg:hover { 80 | fill: $cyan; 81 | transition: fill $quick ease-in; 82 | } 83 | } 84 | } 85 | } 86 | .left-nav{ 87 | margin-right: auto; 88 | } 89 | .right-nav{ 90 | margin-right: auto; 91 | margin-right: 3%; 92 | .nav-list { 93 | display: flex; 94 | align-items: center; 95 | margin-left: auto; 96 | 97 | svg{ 98 | height: 100%; 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /qache-app/client/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; -------------------------------------------------------------------------------- /qache-app/client/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Display&display=swap'); 2 | 3 | html { 4 | scroll-behavior: smooth; 5 | } 6 | 7 | body { 8 | box-sizing: border-box; 9 | margin: 0px; 10 | padding: 0px; 11 | font-family: 'Noto Sans Display', sans-serif; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | height: 100vh; 15 | width: 100vw; 16 | overflow-x: hidden; 17 | } 18 | 19 | canvas { 20 | background-color: #fafafa; 21 | z-index: 1; 22 | } 23 | 24 | #app { 25 | height: 100%; 26 | } 27 | -------------------------------------------------------------------------------- /qache-app/client/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $cyan: #21f5ee; 2 | $bluegreen: #009c97; 3 | $black: #111111; 4 | $softBlack: #18181b; 5 | $offBlack: #27272a; 6 | $lightSoftBlack: rgb(31, 41, 55); 7 | $white: #ffffff; 8 | $offWhite: #fafafa; 9 | $offerWhite: #e0e0e0; 10 | $zinc: #e4e4e7; 11 | $quick: 300ms; -------------------------------------------------------------------------------- /qache-app/dist/bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | * Chart.js v3.7.0 9 | * https://www.chartjs.org 10 | * (c) 2021 Chart.js Contributors 11 | * Released under the MIT License 12 | */ 13 | 14 | /*! ***************************************************************************** 15 | Copyright (c) Microsoft Corporation. 16 | 17 | Permission to use, copy, modify, and/or distribute this software for any 18 | purpose with or without fee is hereby granted. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 21 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 22 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 23 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 24 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 25 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 26 | PERFORMANCE OF THIS SOFTWARE. 27 | ***************************************************************************** */ 28 | 29 | /** 30 | * React Router DOM v6.2.1 31 | * 32 | * Copyright (c) Remix Software Inc. 33 | * 34 | * This source code is licensed under the MIT license found in the 35 | * LICENSE.md file in the root directory of this source tree. 36 | * 37 | * @license MIT 38 | */ 39 | 40 | /** 41 | * React Router v6.2.1 42 | * 43 | * Copyright (c) Remix Software Inc. 44 | * 45 | * This source code is licensed under the MIT license found in the 46 | * LICENSE.md file in the root directory of this source tree. 47 | * 48 | * @license MIT 49 | */ 50 | 51 | /** @license React v0.20.2 52 | * scheduler.production.min.js 53 | * 54 | * Copyright (c) Facebook, Inc. and its affiliates. 55 | * 56 | * This source code is licensed under the MIT license found in the 57 | * LICENSE file in the root directory of this source tree. 58 | */ 59 | 60 | /** @license React v17.0.2 61 | * react-dom.production.min.js 62 | * 63 | * Copyright (c) Facebook, Inc. and its affiliates. 64 | * 65 | * This source code is licensed under the MIT license found in the 66 | * LICENSE file in the root directory of this source tree. 67 | */ 68 | 69 | /** @license React v17.0.2 70 | * react-jsx-runtime.production.min.js 71 | * 72 | * Copyright (c) Facebook, Inc. and its affiliates. 73 | * 74 | * This source code is licensed under the MIT license found in the 75 | * LICENSE file in the root directory of this source tree. 76 | */ 77 | 78 | /** @license React v17.0.2 79 | * react.production.min.js 80 | * 81 | * Copyright (c) Facebook, Inc. and its affiliates. 82 | * 83 | * This source code is licensed under the MIT license found in the 84 | * LICENSE file in the root directory of this source tree. 85 | */ 86 | -------------------------------------------------------------------------------- /qache-app/dist/images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/dist/images/demo.gif -------------------------------------------------------------------------------- /qache-app/dist/images/navigate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/dist/images/navigate.gif -------------------------------------------------------------------------------- /qache-app/dist/images/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/dist/images/preview.gif -------------------------------------------------------------------------------- /qache-app/dist/images/qache.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/dist/images/qache.gif -------------------------------------------------------------------------------- /qache-app/dist/images/qache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/dist/images/qache.png -------------------------------------------------------------------------------- /qache-app/dist/images/storage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/dist/images/storage.jpg -------------------------------------------------------------------------------- /qache-app/dist/index.html: -------------------------------------------------------------------------------- 1 | Qache
    -------------------------------------------------------------------------------- /qache-app/dist/main.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css2?family=Noto+Sans+Display&display=swap); 2 | .navbar-container{display:flex;position:fixed;justify-content:space-between;width:100%;height:120px;padding-right:1%;padding-left:1%;z-index:1;top:0;background-color:#111;white-space:nowrap}.navbar-container .image-container{display:flex;width:200px;margin-right:10}.navbar-container .image-container img{height:100%;object-fit:contain;box-sizing:border-box;vertical-align:middle}.navbar-container .left-nav,.navbar-container .right-nav{display:flex}.navbar-container .left-nav .nav-list,.navbar-container .right-nav .nav-list{display:flex;align-items:center;margin-left:auto}.navbar-container .left-nav .nav-list li,.navbar-container .right-nav .nav-list li{margin:0 10px}.navbar-container .left-nav .nav-list .nav-link,.navbar-container .right-nav .nav-list .nav-link{text-decoration:none;font-size:large;font-weight:550;color:#fff;display:block;position:relative;padding:0.5rem 0;margin-left:2rem}.navbar-container .left-nav .nav-list .nav-link::after,.navbar-container .right-nav .nav-list .nav-link::after{content:'';position:absolute;bottom:0;left:0;width:100%;height:0.1rem;background-color:#21f5ee;transition:opacity 300ms,transform 300ms;opacity:1;transform:scale(0);transform-origin:center}.navbar-container .left-nav .nav-list .nav-link:hover,.navbar-container .left-nav .nav-list .nav-link:hover::after,.navbar-container .left-nav .nav-list .nav-link:focus::after,.navbar-container .right-nav .nav-list .nav-link:hover,.navbar-container .right-nav .nav-list .nav-link:hover::after,.navbar-container .right-nav .nav-list .nav-link:focus::after{color:#21f5ee;transform:scale(1)}.navbar-container .left-nav .nav-list:nth-child(1),.navbar-container .right-nav .nav-list:nth-child(1){justify-content:flex-end}.navbar-container .left-nav .nav-list:nth-child(1) .nav-icons,.navbar-container .right-nav .nav-list:nth-child(1) .nav-icons{height:100%}.navbar-container .left-nav .nav-list:nth-child(1) .nav-icons path:hover,.navbar-container .left-nav .nav-list:nth-child(1) .nav-icons svg:hover,.navbar-container .right-nav .nav-list:nth-child(1) .nav-icons path:hover,.navbar-container .right-nav .nav-list:nth-child(1) .nav-icons svg:hover{fill:#21f5ee;transition:fill 300ms ease-in}.navbar-container .left-nav{margin-right:auto}.navbar-container .right-nav{margin-right:auto;margin-right:3%}.navbar-container .right-nav .nav-list{display:flex;align-items:center;margin-left:auto}.navbar-container .right-nav .nav-list svg{height:100%} 3 | 4 | .introduction-container{display:flex;height:40%;justify-content:space-around;align-items:center;background-color:#18181b;padding:2%;padding-top:120px}.introduction-container .header-texts{display:flex;flex-direction:column;align-items:center;color:#fff}.introduction-container .header-texts h1{font-size:3.5rem;font-weight:bold;margin-bottom:2%}.introduction-container .header-texts p{font-weight:200;font-size:1.5rem;letter-spacing:0.05rem}.introduction-container .header-texts h1,.introduction-container .header-texts p{margin:0}.introduction-container .header-texts p,.introduction-container .header-texts .buttons-container{display:flex;justify-content:space-between;padding-top:2%;padding-bottom:2%}.introduction-container .header-texts strong{color:#21f5ee}.introduction-container .header-texts .buttons-container{display:flex;align-items:center;width:50%}.introduction-container .header-texts .buttons-container .copy-button{display:flex;background-color:#21f5ee;padding:20px 25px;border-radius:5px;transition:300ms;cursor:pointer}.introduction-container .header-texts .buttons-container .copy-button .forward-arrow{display:inline-block}.introduction-container .header-texts .buttons-container .copy-button code{display:inline-block;color:#111;font-weight:600}.introduction-container .header-texts .buttons-container .copy-button:hover{background-color:#fff}.introduction-container .header-texts .buttons-container .copy-button:active{transform:scale(0.95)}.introduction-container .header-texts .buttons-container .hvr-curl-top-right{-webkit-transform:perspective(1px) translateZ(0);transform:perspective(1px) translateZ(0);box-shadow:0 0 1px rgba(0,0,0,0)}.introduction-container .header-texts .buttons-container .hvr-curl-top-right::before{pointer-events:none;position:absolute;content:'';height:0;width:0;top:0;right:0;background:white;border-radius:5px;background:linear-gradient(225deg, white 45%, #aaa 50%, #ccc 56%, white 80%);box-shadow:-1px 1px 1px rgba(0,0,0,0.4);-webkit-transition-duration:0.3s;transition-duration:0.3s;-webkit-transition-property:width, height;transition-property:width, height}.introduction-container .header-texts .buttons-container .hvr-curl-top-right:hover::before,.introduction-container .header-texts .buttons-container .hvr-curl-top-right:focus::before,.introduction-container .header-texts .buttons-container .hvr-curl-top-right:active::before{width:25px;height:25px;border-radius:5px}.introduction-container .blue-button,.introduction-container a{display:inline-block;color:#21f5ee;text-decoration:none;vertical-align:bottom;cursor:pointer}.introduction-container .blue-button:hover,.introduction-container a:hover{color:#fff}.introduction-container .blue-button:active,.introduction-container a:active{transform:scale(0.95)}.version-banner{display:block;text-align:center;background-color:#111;color:#fff;height:5%;padding:0.5%}.version-banner a{color:#21f5ee;text-decoration:underline}.version-banner a:hover{color:#fff}.overview-container{display:flex;max-width:100vw;max-height:45%;background-color:#fafafa}.overview-container .bg{position:absolute;z-index:1;width:100%;max-height:45%;filter:blur(10px)}.overview-container .overview-texts,.overview-container .overview-graph{display:flex;flex-direction:column;max-width:50%;width:50%;padding:3.5% 5%}.overview-container .overview-texts{justify-content:center;align-items:flex-start}.overview-container .overview-texts h3{font-size:2rem;font-weight:300;margin-bottom:2%}.overview-container .overview-texts p{font-size:1.25rem;width:75%;margin-top:1%;margin-bottom:1%}.overview-container .overview-graph{background-color:#fafafa;backdrop-filter:blur(10px)}.overview-container .overview-graph canvas{backdrop-filter:blur(10px);background-color:rgba(255,255,255,0.5);border-radius:5px}.features-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:1%;background-color:#fafafa}.features-container .demo-feature-container{display:flex;flex-direction:column;color:#111;background-color:#f5f5f4;width:75%;padding:3%;box-shadow:rgba(149,157,165,0.2) 0px 8px 24px}.features-container .demo-feature-container h1{font-size:2.5rem;font-weight:600;margin-top:0;margin-bottom:5%;text-align:center}.features-container .demo-feature-container hr{border:1px solid #e4e4e7}.features-container .demo-feature-container .demo-container{display:flex;padding:1%}.features-container .demo-feature-container .demo-container img{max-width:50%;margin-right:2.5%}.features-container .demo-feature-container .demo-container .text{width:100%}.features-container .demo-feature-container .demo-container h3{font-size:1.75rem;font-weight:400;margin-bottom:1%} 5 | 6 | #nav-container{display:flex;justify-content:flex-start;align-items:center;height:10%;background-color:#fff;white-space:nowrap}#nav-container .navbar{display:flex;justify-content:space-evenly;align-items:center;height:100%;width:100%;height:80px;padding-top:1%;padding-left:5%;padding-right:5%;background-color:#fafafa;border-bottom:solid 1px #dfdfdf}#nav-container .navbar .menu-bars{display:flex;justify-content:space-between;margin-left:0;font-size:2rem;background:none}#nav-container .navbar a{font-size:1.5rem;font-weight:bold;text-decoration:none;color:black;cursor:pointer}#nav-container .navbar a:hover,#nav-container .navbar a:focus{text-decoration:underline}#nav-container .navbar .search{width:40%;display:flex;position:relative}#nav-container .navbar .search input{width:100%;padding:10px;padding-left:1.75rem;font-size:large;background-color:#fafafa}#nav-container .navbar .search .icon{position:absolute;top:14px;left:7px}#nav-container .navbar .not-active{visibility:hidden}#nav-container .navbar .active{visibility:visible;background-color:transparent;border:none;cursor:pointer;font-size:xx-large}#nav-container .navbar .active:active{transform:translateY(1.5px);background:none;background-color:transparent}#nav-container .navbar .active:active .tooltip{box-shadow:none}#nav-container .navbar .active .tooltip{padding:10px;position:absolute;width:auto;white-space:nowrap;word-wrap:no-wrap;box-shadow:1px 1px 20px #aaa;border-radius:5px;background-color:#fff;top:85px;left:90.75%;transform:translate(-50%);transform-style:preserve-3d;z-index:200;font-size:small;display:none}#nav-container .navbar .active .tooltip:after{content:'';position:absolute;display:block;width:10px;height:10px;transform-origin:50% 50%;transform:rotate(45deg) translateX(-50%);background-color:#fff;left:50%;top:-1px;z-index:400}#nav-container .navbar .active:hover .tooltip{display:block}#nav-container .navbar .active .spinner:hover{animation:spin infinite 1s linear}@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}#nav-container .nav-menu{display:flex;justify-content:center;position:fixed;width:250px;height:300vh;top:0;left:-100%;transition:750ms;background-color:#fafafa;overflow-y:scroll !important}#nav-container .nav-menu.active{padding-left:1.5%;left:0;z-index:10;transition:250ms}#nav-container ::-webkit-scrollbar{width:0px;background:transparent}#nav-container .nav-menu-items{width:100%}#nav-container .nav-menu-items .navbar-toggle{display:flex;justify-content:flex-start;align-items:center;height:5rem;margin-left:1rem;font-size:large;background-color:#fafafa;cursor:pointer}.sidebar-overlay::after{content:'';position:fixed;width:100%;height:100%;background-color:rgba(0,0,0,0.5);left:0;top:0;z-index:10} 7 | 8 | .sidebarLink{display:flex;justify-content:start;align-items:center;height:5rem;padding:0.5rem 0 0.5rem 1rem;text-decoration:none;border-radius:0.25rem;font-size:x-large;color:black}.sidebarLink svg{display:inline-block;position:relative;vertical-align:baseline;top:0.25rem}.sidebarLink span{display:inline-block;margin-left:1rem}.sidebarLink span:hover{text-decoration:underline}.dropdownLink{height:60px;padding-left:3rem;display:flex;align-items:center;text-decoration:none;border-radius:0.25rem;font-size:large;color:black}.dropdownLink:hover{text-decoration:underline}.dropdownLink .subIcon{display:flex;align-items:center;padding-right:1rem} 9 | 10 | .landingPage-container{display:flex;flex-direction:column}.landingPage-container h1{text-align:center}.landingPage-container .section-container{display:flex;justify-content:space-evenly;flex-direction:row}.landingPage-container .imageContainer{display:flex;position:relative;justify-content:center;align-items:center;overflow:hidden;width:50%}.landingPage-container img{object-fit:cover;flex-shrink:0;height:80%}.landingPage-container .img-caption{display:flex;justify-content:flex-start;align-items:center;position:absolute;overflow:hidden;top:75%;height:5rem;backdrop-filter:blur(25px)}.landingPage-container .img-caption h2{font-weight:300}.landingPage-container .graph-container{display:flex;flex-direction:column;align-items:stretch}.landingPage-container .graph-container h2{margin:0;font-size:2rem;text-align:center} 11 | 12 | .products-wrap{display:flex;flex-wrap:wrap;width:100%;z-index:-1}.products-wrap .product-container{display:flex;flex-direction:column;align-items:center;width:21%;padding:2%;border-radius:3px;background-color:rgba(255,255,255,0.85);border-top:1px solid lightgrey}.products-wrap .product-container img{opacity:0.85}.products-wrap .product-container .sale{background:transparent;height:3rem;width:3rem;position:relative;left:50%;display:flex;align-items:center;justify-content:center;text-align:center;transform:rotate(20deg);animation:beat 1s ease infinite alternate;overflow:hidden;z-index:1}.products-wrap .product-container .sale:before{content:'';position:absolute;top:0;left:0;height:3rem;width:3rem;background:green;color:white;transform:rotate(135deg);z-index:-1}@keyframes beat{from{transform:rotate(20deg) scale(1)}to{transform:rotate(20deg) scale(1.1)}}.products-wrap .product-container .product-info{display:flex;width:100%;flex-direction:column}.products-wrap .product-container .product-info strong{color:red}.products-wrap .product-container .product-info strong .onSale{text-decoration:line-through}.products-wrap .product-container .product-info .newPrice{color:green}.products-wrap .product-container .product-info .noSale{color:black} 13 | 14 | .productDisplay-container{display:flex;justify-content:center;align-items:center;flex-direction:column;padding-left:5%;padding-right:5%}.productDisplay-container h1{width:100%;margin:20px;padding-left:200px;padding-bottom:20px}.productDisplay-container .cache-line{display:flex;width:100%;justify-content:space-evenly}.productDisplay-container .cache-line .talkingPoints{width:20%;margin-top:20px;font-size:1vw;padding:1.25%}.productDisplay-container .cache-line .lineGraphContainer{position:relative;width:80%;margin-top:20px;font-size:1vw}.productDisplay-container .cache-line .lineGraphContainer canvas{width:100%;height:100%;padding:5%}.productDisplay-container .cache-line .lineGraphContainer .title{position:absolute;left:48%;top:5%}.productDisplay-container .cache-line .lineGraphContainer .yLabel{position:absolute;top:50%;left:-3%;transform:rotate(270deg)}.productDisplay-container .cache-line .lineGraphContainer .xLabel{position:absolute;top:90%;left:50%}.productDisplay-container .cache-line .image-container{display:flex;justify-content:center;align-items:center;overflow-y:hidden;border-radius:3px}.productDisplay-container .cache-line .image-container img{object-fit:cover;width:400px;height:400px}.productDisplay-container img{width:100%} 15 | 16 | body .code-block{margin:10px 30px;padding:10px;width:80%;min-height:100px;color:#dfdfdf;display:inline-block;font-size:small;background-color:#131313;border-radius:5px;border:1px solid gray}body .gr{color:rgba(138,138,138,0.616)}body .g{color:#65df65}body .r{color:#f13636}body .b{color:#5bbde4}body .y{color:#e6e477}body .p{color:#b06fec}body #docs{display:flex;width:100%;background-color:#18181b;color:#fff}body #docs strong{color:#21f5ee;margin:10px 10px}body #docs .title{font-size:x-large;color:#e0e0e0;font-weight:600;margin-left:20px;padding-bottom:5px;border-bottom:1px solid gray}body #docs .time{color:#21f5ee;font-size:large;margin:5px 20px}body #docs .body{font-size:large;margin-left:20px}body #docs #docs-nav{padding:10px;font-size:larger;position:fixed;top:130px;width:200px;height:100vh;border-right:1px solid gray}body #docs #docs-nav li{margin-left:30px;color:#e0e0e0}body #docs #docs-content{display:flex;width:auto;padding-top:130px;margin-left:230px;flex-direction:column;font-size:x-large}body #docs #docs-content strong{font-size:xx-large;margin-left:-10px}body #docs #docs-content .section{background-color:#27272a;margin:0px 5px 10px 5px;width:90%;border-radius:10px;padding:10px 0px 20px 0px}body #docs #docs-content .section .title{margin:0;padding-left:20px;color:#fafafa}body #docs #docs-content .section .time{margin-bottom:10px}body #docs #docs-content .section .body{margin-top:10px;color:#e0e0e0} 17 | 18 | /* 19 | ! tailwindcss v3.0.18 | MIT License | https://tailwindcss.com 20 | *//* 21 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 22 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 23 | */ 24 | 25 | *, 26 | ::before, 27 | ::after { 28 | box-sizing: border-box; /* 1 */ 29 | border-width: 0; /* 2 */ 30 | border-style: solid; /* 2 */ 31 | border-color: #e5e7eb; /* 2 */ 32 | } 33 | 34 | ::before, 35 | ::after { 36 | --tw-content: ''; 37 | } 38 | 39 | /* 40 | 1. Use a consistent sensible line-height in all browsers. 41 | 2. Prevent adjustments of font size after orientation changes in iOS. 42 | 3. Use a more readable tab size. 43 | 4. Use the user's configured `sans` font-family by default. 44 | */ 45 | 46 | html { 47 | line-height: 1.5; /* 1 */ 48 | -webkit-text-size-adjust: 100%; /* 2 */ 49 | -moz-tab-size: 4; /* 3 */ 50 | -o-tab-size: 4; 51 | tab-size: 4; /* 3 */ 52 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ 53 | } 54 | 55 | /* 56 | 1. Remove the margin in all browsers. 57 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 58 | */ 59 | 60 | body { 61 | margin: 0; /* 1 */ 62 | line-height: inherit; /* 2 */ 63 | } 64 | 65 | /* 66 | 1. Add the correct height in Firefox. 67 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 68 | 3. Ensure horizontal rules are visible by default. 69 | */ 70 | 71 | hr { 72 | height: 0; /* 1 */ 73 | color: inherit; /* 2 */ 74 | border-top-width: 1px; /* 3 */ 75 | } 76 | 77 | /* 78 | Add the correct text decoration in Chrome, Edge, and Safari. 79 | */ 80 | 81 | abbr:where([title]) { 82 | -webkit-text-decoration: underline dotted; 83 | text-decoration: underline dotted; 84 | } 85 | 86 | /* 87 | Remove the default font size and weight for headings. 88 | */ 89 | 90 | h1, 91 | h2, 92 | h3, 93 | h4, 94 | h5, 95 | h6 { 96 | font-size: inherit; 97 | font-weight: inherit; 98 | } 99 | 100 | /* 101 | Reset links to optimize for opt-in styling instead of opt-out. 102 | */ 103 | 104 | a { 105 | color: inherit; 106 | text-decoration: inherit; 107 | } 108 | 109 | /* 110 | Add the correct font weight in Edge and Safari. 111 | */ 112 | 113 | b, 114 | strong { 115 | font-weight: bolder; 116 | } 117 | 118 | /* 119 | 1. Use the user's configured `mono` font family by default. 120 | 2. Correct the odd `em` font sizing in all browsers. 121 | */ 122 | 123 | code, 124 | kbd, 125 | samp, 126 | pre { 127 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ 128 | font-size: 1em; /* 2 */ 129 | } 130 | 131 | /* 132 | Add the correct font size in all browsers. 133 | */ 134 | 135 | small { 136 | font-size: 80%; 137 | } 138 | 139 | /* 140 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 141 | */ 142 | 143 | sub, 144 | sup { 145 | font-size: 75%; 146 | line-height: 0; 147 | position: relative; 148 | vertical-align: baseline; 149 | } 150 | 151 | sub { 152 | bottom: -0.25em; 153 | } 154 | 155 | sup { 156 | top: -0.5em; 157 | } 158 | 159 | /* 160 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 161 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 162 | 3. Remove gaps between table borders by default. 163 | */ 164 | 165 | table { 166 | text-indent: 0; /* 1 */ 167 | border-color: inherit; /* 2 */ 168 | border-collapse: collapse; /* 3 */ 169 | } 170 | 171 | /* 172 | 1. Change the font styles in all browsers. 173 | 2. Remove the margin in Firefox and Safari. 174 | 3. Remove default padding in all browsers. 175 | */ 176 | 177 | button, 178 | input, 179 | optgroup, 180 | select, 181 | textarea { 182 | font-family: inherit; /* 1 */ 183 | font-size: 100%; /* 1 */ 184 | line-height: inherit; /* 1 */ 185 | color: inherit; /* 1 */ 186 | margin: 0; /* 2 */ 187 | padding: 0; /* 3 */ 188 | } 189 | 190 | /* 191 | Remove the inheritance of text transform in Edge and Firefox. 192 | */ 193 | 194 | button, 195 | select { 196 | text-transform: none; 197 | } 198 | 199 | /* 200 | 1. Correct the inability to style clickable types in iOS and Safari. 201 | 2. Remove default button styles. 202 | */ 203 | 204 | button, 205 | [type='button'], 206 | [type='reset'], 207 | [type='submit'] { 208 | -webkit-appearance: button; /* 1 */ 209 | background-color: transparent; /* 2 */ 210 | background-image: none; /* 2 */ 211 | } 212 | 213 | /* 214 | Use the modern Firefox focus style for all focusable elements. 215 | */ 216 | 217 | :-moz-focusring { 218 | outline: auto; 219 | } 220 | 221 | /* 222 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 223 | */ 224 | 225 | :-moz-ui-invalid { 226 | box-shadow: none; 227 | } 228 | 229 | /* 230 | Add the correct vertical alignment in Chrome and Firefox. 231 | */ 232 | 233 | progress { 234 | vertical-align: baseline; 235 | } 236 | 237 | /* 238 | Correct the cursor style of increment and decrement buttons in Safari. 239 | */ 240 | 241 | ::-webkit-inner-spin-button, 242 | ::-webkit-outer-spin-button { 243 | height: auto; 244 | } 245 | 246 | /* 247 | 1. Correct the odd appearance in Chrome and Safari. 248 | 2. Correct the outline style in Safari. 249 | */ 250 | 251 | [type='search'] { 252 | -webkit-appearance: textfield; /* 1 */ 253 | outline-offset: -2px; /* 2 */ 254 | } 255 | 256 | /* 257 | Remove the inner padding in Chrome and Safari on macOS. 258 | */ 259 | 260 | ::-webkit-search-decoration { 261 | -webkit-appearance: none; 262 | } 263 | 264 | /* 265 | 1. Correct the inability to style clickable types in iOS and Safari. 266 | 2. Change font properties to `inherit` in Safari. 267 | */ 268 | 269 | ::-webkit-file-upload-button { 270 | -webkit-appearance: button; /* 1 */ 271 | font: inherit; /* 2 */ 272 | } 273 | 274 | /* 275 | Add the correct display in Chrome and Safari. 276 | */ 277 | 278 | summary { 279 | display: list-item; 280 | } 281 | 282 | /* 283 | Removes the default spacing and border for appropriate elements. 284 | */ 285 | 286 | blockquote, 287 | dl, 288 | dd, 289 | h1, 290 | h2, 291 | h3, 292 | h4, 293 | h5, 294 | h6, 295 | hr, 296 | figure, 297 | p, 298 | pre { 299 | margin: 0; 300 | } 301 | 302 | fieldset { 303 | margin: 0; 304 | padding: 0; 305 | } 306 | 307 | legend { 308 | padding: 0; 309 | } 310 | 311 | ol, 312 | ul, 313 | menu { 314 | list-style: none; 315 | margin: 0; 316 | padding: 0; 317 | } 318 | 319 | /* 320 | Prevent resizing textareas horizontally by default. 321 | */ 322 | 323 | textarea { 324 | resize: vertical; 325 | } 326 | 327 | /* 328 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 329 | 2. Set the default placeholder color to the user's configured gray 400 color. 330 | */ 331 | 332 | input::-moz-placeholder, textarea::-moz-placeholder { 333 | opacity: 1; /* 1 */ 334 | color: #9ca3af; /* 2 */ 335 | } 336 | 337 | input:-ms-input-placeholder, textarea:-ms-input-placeholder { 338 | opacity: 1; /* 1 */ 339 | color: #9ca3af; /* 2 */ 340 | } 341 | 342 | input::placeholder, 343 | textarea::placeholder { 344 | opacity: 1; /* 1 */ 345 | color: #9ca3af; /* 2 */ 346 | } 347 | 348 | /* 349 | Set the default cursor for buttons. 350 | */ 351 | 352 | button, 353 | [role="button"] { 354 | cursor: pointer; 355 | } 356 | 357 | /* 358 | Make sure disabled buttons don't get the pointer cursor. 359 | */ 360 | :disabled { 361 | cursor: default; 362 | } 363 | 364 | /* 365 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 366 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 367 | This can trigger a poorly considered lint error in some tools but is included by design. 368 | */ 369 | 370 | img, 371 | svg, 372 | video, 373 | canvas, 374 | audio, 375 | iframe, 376 | embed, 377 | object { 378 | display: block; /* 1 */ 379 | vertical-align: middle; /* 2 */ 380 | } 381 | 382 | /* 383 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 384 | */ 385 | 386 | img, 387 | video { 388 | max-width: 100%; 389 | height: auto; 390 | } 391 | 392 | /* 393 | Ensure the default browser behavior of the `hidden` attribute. 394 | */ 395 | 396 | [hidden] { 397 | display: none; 398 | } 399 | 400 | *, ::before, ::after { 401 | --tw-translate-x: 0; 402 | --tw-translate-y: 0; 403 | --tw-rotate: 0; 404 | --tw-skew-x: 0; 405 | --tw-skew-y: 0; 406 | --tw-scale-x: 1; 407 | --tw-scale-y: 1; 408 | --tw-pan-x: ; 409 | --tw-pan-y: ; 410 | --tw-pinch-zoom: ; 411 | --tw-scroll-snap-strictness: proximity; 412 | --tw-ordinal: ; 413 | --tw-slashed-zero: ; 414 | --tw-numeric-figure: ; 415 | --tw-numeric-spacing: ; 416 | --tw-numeric-fraction: ; 417 | --tw-ring-inset: ; 418 | --tw-ring-offset-width: 0px; 419 | --tw-ring-offset-color: #fff; 420 | --tw-ring-color: rgb(59 130 246 / 0.5); 421 | --tw-ring-offset-shadow: 0 0 #0000; 422 | --tw-ring-shadow: 0 0 #0000; 423 | --tw-shadow: 0 0 #0000; 424 | --tw-shadow-colored: 0 0 #0000; 425 | --tw-blur: ; 426 | --tw-brightness: ; 427 | --tw-contrast: ; 428 | --tw-grayscale: ; 429 | --tw-hue-rotate: ; 430 | --tw-invert: ; 431 | --tw-saturate: ; 432 | --tw-sepia: ; 433 | --tw-drop-shadow: ; 434 | --tw-backdrop-blur: ; 435 | --tw-backdrop-brightness: ; 436 | --tw-backdrop-contrast: ; 437 | --tw-backdrop-grayscale: ; 438 | --tw-backdrop-hue-rotate: ; 439 | --tw-backdrop-invert: ; 440 | --tw-backdrop-opacity: ; 441 | --tw-backdrop-saturate: ; 442 | --tw-backdrop-sepia: ; 443 | } 444 | 445 | .sr-only { 446 | position: absolute; 447 | width: 1px; 448 | height: 1px; 449 | padding: 0; 450 | margin: -1px; 451 | overflow: hidden; 452 | clip: rect(0, 0, 0, 0); 453 | white-space: nowrap; 454 | border-width: 0; 455 | } 456 | 457 | .mx-auto { 458 | margin-left: auto; 459 | margin-right: auto; 460 | } 461 | 462 | .flex { 463 | display: flex; 464 | } 465 | 466 | .hidden { 467 | display: none; 468 | } 469 | 470 | .h-40 { 471 | height: 10rem; 472 | } 473 | 474 | .h-10 { 475 | height: 2.5rem; 476 | } 477 | 478 | .h-11 { 479 | height: 2.75rem; 480 | } 481 | 482 | .w-40 { 483 | width: 10rem; 484 | } 485 | 486 | .w-10 { 487 | width: 2.5rem; 488 | } 489 | 490 | .w-11 { 491 | width: 2.75rem; 492 | } 493 | 494 | .max-w-7xl { 495 | max-width: 80rem; 496 | } 497 | 498 | .justify-center { 499 | justify-content: center; 500 | } 501 | 502 | .space-y-2 > :not([hidden]) ~ :not([hidden]) { 503 | --tw-space-y-reverse: 0; 504 | margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); 505 | margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); 506 | } 507 | 508 | .space-y-5 > :not([hidden]) ~ :not([hidden]) { 509 | --tw-space-y-reverse: 0; 510 | margin-top: calc(1.25rem * calc(1 - var(--tw-space-y-reverse))); 511 | margin-bottom: calc(1.25rem * var(--tw-space-y-reverse)); 512 | } 513 | 514 | .space-y-6 > :not([hidden]) ~ :not([hidden]) { 515 | --tw-space-y-reverse: 0; 516 | margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); 517 | margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); 518 | } 519 | 520 | .space-y-1 > :not([hidden]) ~ :not([hidden]) { 521 | --tw-space-y-reverse: 0; 522 | margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); 523 | margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); 524 | } 525 | 526 | .space-x-5 > :not([hidden]) ~ :not([hidden]) { 527 | --tw-space-x-reverse: 0; 528 | margin-right: calc(1.25rem * var(--tw-space-x-reverse)); 529 | margin-left: calc(1.25rem * calc(1 - var(--tw-space-x-reverse))); 530 | } 531 | 532 | .rounded-lg { 533 | border-radius: 0.5rem; 534 | } 535 | 536 | .rounded-full { 537 | border-radius: 9999px; 538 | } 539 | 540 | .bg-zinc-900 { 541 | --tw-bg-opacity: 1; 542 | background-color: rgb(24 24 27 / var(--tw-bg-opacity)); 543 | } 544 | 545 | .bg-zinc-800 { 546 | --tw-bg-opacity: 1; 547 | background-color: rgb(39 39 42 / var(--tw-bg-opacity)); 548 | } 549 | 550 | .px-4 { 551 | padding-left: 1rem; 552 | padding-right: 1rem; 553 | } 554 | 555 | .py-10 { 556 | padding-top: 2.5rem; 557 | padding-bottom: 2.5rem; 558 | } 559 | 560 | .px-6 { 561 | padding-left: 1.5rem; 562 | padding-right: 1.5rem; 563 | } 564 | 565 | .text-center { 566 | text-align: center; 567 | } 568 | 569 | .text-3xl { 570 | font-size: 1.875rem; 571 | line-height: 2.25rem; 572 | } 573 | 574 | .text-xl { 575 | font-size: 1.25rem; 576 | line-height: 1.75rem; 577 | } 578 | 579 | .text-lg { 580 | font-size: 1.125rem; 581 | line-height: 1.75rem; 582 | } 583 | 584 | .font-extrabold { 585 | font-weight: 800; 586 | } 587 | 588 | .font-medium { 589 | font-weight: 500; 590 | } 591 | 592 | .leading-6 { 593 | line-height: 1.5rem; 594 | } 595 | 596 | .tracking-tight { 597 | letter-spacing: -0.025em; 598 | } 599 | 600 | .text-white { 601 | --tw-text-opacity: 1; 602 | color: rgb(255 255 255 / var(--tw-text-opacity)); 603 | } 604 | 605 | .text-gray-300 { 606 | --tw-text-opacity: 1; 607 | color: rgb(209 213 219 / var(--tw-text-opacity)); 608 | } 609 | 610 | .text-cyan-500 { 611 | --tw-text-opacity: 1; 612 | color: rgb(6 182 212 / var(--tw-text-opacity)); 613 | } 614 | 615 | .text-gray-400 { 616 | --tw-text-opacity: 1; 617 | color: rgb(156 163 175 / var(--tw-text-opacity)); 618 | } 619 | 620 | .filter { 621 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); 622 | } 623 | 624 | .transition { 625 | transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter; 626 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; 627 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter; 628 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 629 | transition-duration: 150ms; 630 | } 631 | 632 | .hover\:scale-110:hover { 633 | --tw-scale-x: 1.1; 634 | --tw-scale-y: 1.1; 635 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 636 | } 637 | 638 | .hover\:text-gray-300:hover { 639 | --tw-text-opacity: 1; 640 | color: rgb(209 213 219 / var(--tw-text-opacity)); 641 | } 642 | 643 | @media (min-width: 640px) { 644 | 645 | .sm\:grid { 646 | display: grid; 647 | } 648 | 649 | .sm\:grid-cols-2 { 650 | grid-template-columns: repeat(2, minmax(0, 1fr)); 651 | } 652 | 653 | .sm\:gap-6 { 654 | gap: 1.5rem; 655 | } 656 | 657 | .sm\:space-y-4 > :not([hidden]) ~ :not([hidden]) { 658 | --tw-space-y-reverse: 0; 659 | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); 660 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); 661 | } 662 | 663 | .sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) { 664 | --tw-space-y-reverse: 0; 665 | margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse))); 666 | margin-bottom: calc(0px * var(--tw-space-y-reverse)); 667 | } 668 | 669 | .sm\:px-6 { 670 | padding-left: 1.5rem; 671 | padding-right: 1.5rem; 672 | } 673 | 674 | .sm\:text-4xl { 675 | font-size: 2.25rem; 676 | line-height: 2.5rem; 677 | } 678 | } 679 | 680 | @media (min-width: 768px) { 681 | 682 | .md\:max-w-xl { 683 | max-width: 36rem; 684 | } 685 | } 686 | 687 | @media (min-width: 1024px) { 688 | 689 | .lg\:max-w-3xl { 690 | max-width: 48rem; 691 | } 692 | 693 | .lg\:grid-cols-2 { 694 | grid-template-columns: repeat(2, minmax(0, 1fr)); 695 | } 696 | 697 | .lg\:gap-8 { 698 | gap: 2rem; 699 | } 700 | 701 | .lg\:px-8 { 702 | padding-left: 2rem; 703 | padding-right: 2rem; 704 | } 705 | 706 | .lg\:py-4 { 707 | padding-top: 1rem; 708 | padding-bottom: 1rem; 709 | } 710 | } 711 | 712 | @media (min-width: 1280px) { 713 | 714 | .xl\:flex { 715 | display: flex; 716 | } 717 | 718 | .xl\:h-56 { 719 | height: 14rem; 720 | } 721 | 722 | .xl\:w-56 { 723 | width: 14rem; 724 | } 725 | 726 | .xl\:max-w-none { 727 | max-width: none; 728 | } 729 | 730 | .xl\:items-center { 731 | align-items: center; 732 | } 733 | 734 | .xl\:justify-between { 735 | justify-content: space-between; 736 | } 737 | 738 | .xl\:space-y-10 > :not([hidden]) ~ :not([hidden]) { 739 | --tw-space-y-reverse: 0; 740 | margin-top: calc(2.5rem * calc(1 - var(--tw-space-y-reverse))); 741 | margin-bottom: calc(2.5rem * var(--tw-space-y-reverse)); 742 | } 743 | 744 | .xl\:px-10 { 745 | padding-left: 2.5rem; 746 | padding-right: 2.5rem; 747 | } 748 | 749 | .xl\:text-left { 750 | text-align: left; 751 | } 752 | } 753 | html{scroll-behavior:smooth}body{box-sizing:border-box;margin:0px;padding:0px;font-family:'Noto Sans Display', sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;height:100vh;width:100vw;overflow-x:hidden}canvas{background-color:#fafafa;z-index:1}#app{height:100%} 754 | 755 | -------------------------------------------------------------------------------- /qache-app/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.jpg'; 3 | declare module '*.gif'; -------------------------------------------------------------------------------- /qache-app/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | export interface Product { 4 | id?: string; 5 | name: string; 6 | description: string; 7 | imageUrl: string; 8 | quantity: number; 9 | price: number; 10 | onSale: boolean; 11 | category: string; 12 | inCart: boolean; 13 | } 14 | 15 | export interface Metric { 16 | labels?: string[]; 17 | data?: number[]; 18 | 19 | Bedroom?: { 20 | labels: string[]; 21 | data: number[]; 22 | }; 23 | Mattresses?: { 24 | labels: string[]; 25 | data: number[]; 26 | }; 27 | Furniture?: { 28 | labels: string[]; 29 | data: number[]; 30 | }; 31 | Storage?: { 32 | labels: string[]; 33 | data: number[]; 34 | }; 35 | 'Living Room'?: { 36 | labels: string[]; 37 | data: number[]; 38 | }; 39 | Kitchen?: { 40 | labels: string[]; 41 | data: number[]; 42 | }; 43 | Bathroom?: { 44 | labels: string[]; 45 | data: number[]; 46 | }; 47 | Appliances?: { 48 | labels: string[]; 49 | data: number[]; 50 | }; 51 | Couches?: { 52 | labels: string[]; 53 | data: number[]; 54 | }; 55 | Deals?: { 56 | labels: string[]; 57 | data: number[]; 58 | }; 59 | Cart?: { 60 | labels: string[]; 61 | data: number[]; 62 | } 63 | } 64 | 65 | export interface Item { 66 | title: string; 67 | path: string; 68 | icon: Component; 69 | cName?: string; 70 | iconClosed?: Component; 71 | iconOpened?: Component; 72 | subNav?: Item[] | undefined; 73 | } 74 | 75 | export interface Dataset { 76 | label: string; 77 | fill: boolean; 78 | lineTension: number; 79 | backgroundColor: string; 80 | borderColor: string; 81 | borderWidth: number; 82 | data: number[] | undefined; 83 | } 84 | -------------------------------------------------------------------------------- /qache-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "start": "npm run server", 9 | "start:dev": "concurrently --kill-others-on-fail \"npm run server:dev\" \"npm run client\"", 10 | "server": "cross-env NODE_ENV=production node server/server.js", 11 | "server:dev": "cross-env NODE_ENV=development nodemon server/server.js", 12 | "client": "cross-env NODE_ENV=development webpack serve --mode development", 13 | "build:dev": "webpack --mode development", 14 | "build:prod": "NODE_ENV=production webpack --mode production", 15 | "build:css": "npx tailwindcss -i ./client/styles/index.css -o ./dist/main.css --watcmain.css --watch", 16 | "deploy": "cd .. && npm run deploy" 17 | }, 18 | "engines": { 19 | "node": ">=16.0.0 <=17.4.0", 20 | "npm": "8.3.1" 21 | }, 22 | "keywords": [], 23 | "author": "", 24 | "license": "ISC", 25 | "dependencies": { 26 | "@babel/core": "^7.16.10", 27 | "@babel/plugin-transform-react-jsx": "^7.16.7", 28 | "@babel/preset-env": "^7.16.11", 29 | "@babel/preset-react": "^7.16.7", 30 | "@heroicons/react": "^1.0.5", 31 | "@observablehq/plot": "^0.4.0", 32 | "@types/react": "^17.0.38", 33 | "@types/react-dom": "^17.0.11", 34 | "@types/react-router-hash-link": "^2.4.5", 35 | "add": "^2.0.6", 36 | "autoprefixer": "^10.4.2", 37 | "axios": "^0.25.0", 38 | "babel-jest": "^27.4.6", 39 | "babel-loader": "^8.2.3", 40 | "babel-plugin-react-require": "^3.1.3", 41 | "chart.js": "^3.7.0", 42 | "concurrently": "^7.0.0", 43 | "cors": "^2.8.5", 44 | "cross-env": "^7.0.3", 45 | "css-loader": "^6.5.1", 46 | "dotenv": "^14.2.0", 47 | "express": "^4.17.2", 48 | "express-graphql": "^0.12.0", 49 | "file-loader": "^6.2.0", 50 | "framer-motion": "^6.2.4", 51 | "html-webpack-plugin": "^5.5.0", 52 | "jest": "^27.4.7", 53 | "mini-css-extract-plugin": "^2.5.3", 54 | "mongoose": "^6.2.1", 55 | "node-sass": "^7.0.1", 56 | "nodemon": "^2.0.15", 57 | "postcss-loader": "^6.2.1", 58 | "prettier": "^2.5.1", 59 | "qache": "^1.0.5", 60 | "react": "^17.0.2", 61 | "react-chartjs-2": "^4.0.1", 62 | "react-dom": "^17.0.2", 63 | "react-icons": "^4.3.1", 64 | "react-intersection-observer": "^8.33.1", 65 | "react-router-dom": "^6.2.1", 66 | "react-router-hash-link": "^2.4.3", 67 | "sass-loader": "^12.4.0", 68 | "style-loader": "^3.3.1", 69 | "tailwindcss": "^3.0.18", 70 | "ts-loader": "^9.2.6", 71 | "typescript": "^4.5.4", 72 | "url-loader": "^4.1.1", 73 | "webpack": "^5.66.0", 74 | "webpack-bundle-analyzer": "^4.5.0", 75 | "webpack-cli": "^4.9.1", 76 | "webpack-dev-server": "^4.7.3" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /qache-app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss'), 4 | require('autoprefixer') 5 | ] 6 | }; -------------------------------------------------------------------------------- /qache-app/server/database/db.js: -------------------------------------------------------------------------------- 1 | const products = [] 2 | 3 | const categories = [] 4 | 5 | module.exports = products, categories; -------------------------------------------------------------------------------- /qache-app/server/models/CategoryModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const categorySchema = new Schema({ 5 | name: { type: String, required: true, unique: true }, 6 | products: [{ type: Schema.Types.ObjectId, ref: 'product'}], 7 | }); 8 | 9 | const Category = mongoose.model('category', categorySchema); 10 | 11 | module.exports = Category; 12 | -------------------------------------------------------------------------------- /qache-app/server/models/ProductModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const productSchema = new Schema({ 5 | name: { type: String, required: true }, 6 | description: { type: String, required: true }, 7 | imageUrl: { type: String, required: true }, 8 | quantity: { type: Number, required: true }, 9 | price: { type: Number, required: true }, 10 | onSale: { type: Boolean, required: true }, 11 | inCart: {type: Boolean, required: true}, 12 | category: [{ type: Schema.Types.ObjectId, ref: 'category', required: true }], 13 | }); 14 | 15 | const Product = mongoose.model('product', productSchema); 16 | 17 | module.exports = Product; 18 | -------------------------------------------------------------------------------- /qache-app/server/resolvers/resolvers.js: -------------------------------------------------------------------------------- 1 | let Qache; 2 | if (process.env.NODE_ENV === 'development') { 3 | Qache = require('../../../Qache/Qache'); 4 | } else if (process.env.NODE_ENV === 'production') { 5 | Qache = require('qache'); 6 | } 7 | const Product = require('../models/ProductModel'); 8 | const Category = require('../models/CategoryModel'); 9 | 10 | const cache = new Qache({ evictionPolicy: 'LFU' }); 11 | 12 | module.exports = { 13 | // clear cache 14 | invalidate: () => { 15 | try { 16 | cache.invalidate(); 17 | console.log('Cache invalidated!'); 18 | return true; 19 | } catch (error) { 20 | console.log(error); 21 | return false; 22 | } 23 | }, 24 | // creates new product and adds it to DB 25 | addProduct: async (args, parent, info) => { 26 | const { 27 | name, 28 | description, 29 | imageUrl, 30 | quantity, 31 | price, 32 | onSale, 33 | category, 34 | inCart, 35 | } = args.product; 36 | 37 | // creates new product in DB 38 | const newProduct = await Product.create({ 39 | name, 40 | description, 41 | imageUrl, 42 | quantity, 43 | price, 44 | onSale, 45 | category, 46 | inCart, 47 | }); 48 | 49 | newProduct.category.forEach(async (id) => { 50 | const path = await Category.findById(id); 51 | cache.listPush(newProduct, category.name); 52 | path.products.push(newProduct._id); 53 | await path.save(); 54 | }); 55 | return newProduct; 56 | }, 57 | 58 | // creates new category and adds it to DB 59 | addCategory: async (args, parent, info) => { 60 | const { name, products } = args.category; 61 | const newCategory = await Category.create({ name, products }); 62 | return newCategory; 63 | }, 64 | 65 | // returns all existing products in DB 66 | getAllProducts: async (args, parent, info) => { 67 | const data = await Product.find().populate('category'); 68 | return data; 69 | }, 70 | 71 | // returns all existing products in DB that are in given category 72 | getProductsBy: async (args) => { 73 | const t1 = Date.now(); 74 | const { category } = args; 75 | const cacheRes = cache.listRange(category); // checks if the category of products exist in cache first 76 | if (cacheRes) { 77 | const t2 = Date.now(); 78 | console.log(t2 - t1, 'ms'); 79 | console.log('This response came from the CACHE.'); 80 | return cacheRes; 81 | } // if exists, returns the array of products 82 | const dbRes = await Category.findOne({ name: category }).populate( 83 | 'products' 84 | ); 85 | // *** Our website and our database are hosted in the same AWS warehouse. 86 | // *** In order to properly illustrate benefits of our library for the majority of use cases, we sourced average ping time from https://wondernetwork.com/pings/New%20York 87 | // ** medium-high latency simulation = average ping from NYC -> Dallas 58ms (East -> Central) - averages taken from our actual database pings 8.5ms (AWS US-East-1 for both server and database) 88 | // ** medium-high latency simulation - 50 ms 89 | await new Promise((resolve, reject) => { 90 | setTimeout(() => { 91 | resolve('medium-high latency simulation'); 92 | }, 50); 93 | }); 94 | 95 | console.log('This response came from the DATABASE'); 96 | const t3 = Date.now(); 97 | cache.listCreate(category, dbRes.products); // sets products array into cache under the name of category 98 | console.log(t3 - t1, 'ms'); 99 | return dbRes.products; 100 | }, 101 | 102 | // returns all existing categories in DB 103 | getCategories: async (args, parent, info) => { 104 | const data = await Category.find().populate('products'); 105 | return data; 106 | }, 107 | 108 | // returns existing category in DB with corresponding ID 109 | getCategoryBy: async (args, parent, info) => { 110 | const data = await Category.findOne({ id: args.id }).populate('products'); 111 | return data; 112 | }, 113 | 114 | // deletes existing Product in DB with corresponding ID 115 | deleteProduct: async (args, parent, info) => { 116 | await Product.deleteOne({ _id: args.id }); 117 | return; 118 | }, 119 | 120 | // deletes existing Category in DB with corresponding ID 121 | deleteCategory: async (args, parent, info) => { 122 | await Category.deleteOne({ _id: args.id }); 123 | return; 124 | }, 125 | 126 | // patches existing Product in DB with corresponding ID, replacing chosen field(s) with inputted info 127 | updateProduct: async (args, parent, info) => { 128 | let { id } = args.product; 129 | const updatedProduct = await Product.findOneAndUpdate( 130 | { _id: id }, 131 | args.product, 132 | { 133 | new: true, 134 | } 135 | ).populate('category'); 136 | 137 | if (updatedProduct.onSale) 138 | cache.listUpsert(updatedProduct, { id }, 'onSale'); 139 | else cache.listRemoveItem({ id }, 'onSale'); 140 | 141 | const categoryNames = []; 142 | updatedProduct.category.forEach((obj) => categoryNames.push(obj.name)); 143 | cache.listUpdate({ id }, args.product, ...categoryNames); 144 | return updatedProduct; 145 | }, 146 | 147 | // patches existing Category in DB with corresponding ID, replacing chosen field(s) with inputted info 148 | updateCategory: async (args, parent, info) => { 149 | let { id, name, products } = args.category; 150 | let updatedCategory = await Category.findById(id); 151 | if (name) updatedCategory.name = name; 152 | if (products) updatedCategory.products = products; 153 | await Category.findOneAndUpdate({ _id: id }, updatedCategory, { 154 | new: true, 155 | }); 156 | return updatedCategory; 157 | }, 158 | 159 | // filters existing Products based on onSale/inCart field 160 | filterProductsBy: async (args, parent, info) => { 161 | const { onSale } = args.filter; 162 | const cacheRes = cache.listRange('onSale'); 163 | if (cacheRes) { 164 | return cacheRes; 165 | } 166 | const filteredProducts = await Product.find({ onSale }); 167 | 168 | // *** Our website and our database are hosted in the same AWS warehouse. 169 | // *** In order to properly illustrate benefits of our library for the majority of use cases, we sourced average ping time from https://wondernetwork.com/pings/New%20York 170 | // ** medium-high latency simulation = average ping from NYC -> Dallas 58ms (East -> Central) - averages taken from our actual database pings 8.5ms (AWS US-East-1 for both server and database) 171 | // ** medium-high latency simulation - 50 ms 172 | await new Promise((resolve, reject) => { 173 | setTimeout(() => { 174 | resolve('medium-high latency simulation'); 175 | }, 50); 176 | }); 177 | 178 | cache.listCreate('onSale', filteredProducts); 179 | return filteredProducts; 180 | }, 181 | }; 182 | -------------------------------------------------------------------------------- /qache-app/server/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const path = require('path'); 3 | const express = require('express'); 4 | const mongoose = require('mongoose'); 5 | const { graphqlHTTP } = require('express-graphql'); 6 | 7 | const schema = require('./typeDefs/schema'); 8 | const resolvers = require('./resolvers/resolvers'); 9 | 10 | const port = 3000; 11 | const app = express(); 12 | 13 | const cors = require('cors'); 14 | app.use(cors({ credentials: true, origin: 'http://localhost:8080' })); 15 | 16 | app.use(express.json()); 17 | app.use(express.urlencoded({ extended: true })); 18 | 19 | mongoose.connect( 20 | `mongodb+srv://sdu1278:${process.env.PASSWORD}@cluster0.vubqx.mongodb.net/myFirstDatabase?retryWrites=true&w=majority`, 21 | { useNewUrlParser: true, useUnifiedTopology: true, dbName: 'qachengo' } 22 | ); 23 | mongoose.connection.once('open', () => { 24 | console.log('Connected to MongoDB!'); 25 | }); 26 | 27 | app.use( 28 | '/graphql', 29 | graphqlHTTP({ 30 | schema: schema, 31 | rootValue: resolvers, 32 | graphiql: process.env.NODE_ENV === 'development', 33 | }) 34 | ); 35 | 36 | if (process.env.NODE_ENV === 'development') { 37 | app.get('/', (req, res) => { 38 | res.status(200).send('Welcome to Demo App dev server!'); 39 | }); 40 | } else if (process.env.NODE_ENV === 'production') { 41 | app.use(express.static(path.join(__dirname, '../dist'))); 42 | app.get('/', (req, res) => { 43 | return res.status(200).sendFile(path.join(__dirname, '../index.html')); 44 | }); 45 | } 46 | 47 | app.use('*', (req, res) => { 48 | res.status(200).sendFile(path.join(__dirname, '../dist/index.html')); 49 | }); 50 | 51 | app.use((error, req, res, next) => { 52 | const defaultError = { 53 | log: 'Express error handler caught unknown middleware error', 54 | status: 400, 55 | message: { error: 'An error occured' }, 56 | }; 57 | 58 | const errorObj = Object.assign(defaultError, error); 59 | 60 | res.status(errorObj.status).send(errorObj.message); 61 | }); 62 | 63 | app.listen(process.env.PORT || port, (req, res) => 64 | console.log(`Server is listening on port ${process.env.PORT || port}!`) 65 | ); 66 | -------------------------------------------------------------------------------- /qache-app/server/typeDefs/schema.js: -------------------------------------------------------------------------------- 1 | const { buildSchema } = require('graphql'); 2 | 3 | module.exports = buildSchema(` 4 | type Query { 5 | invalidate: Boolean 6 | getAllProducts: [PopulatedProducts] 7 | getProductsBy(category: String!): [Product] 8 | filterProductsBy(filter: FilterProductsInput): [Product] 9 | getCategories: [PopulatedCategories] 10 | getCategoryBy(id: ID!): Category 11 | } 12 | 13 | type Product { 14 | id: ID! 15 | name: String! 16 | description: String! 17 | imageUrl: String! 18 | quantity: Int! 19 | price: Float! 20 | onSale: Boolean! 21 | category: [String!]! 22 | inCart: Boolean 23 | } 24 | 25 | type PopulatedProducts { 26 | id: ID! 27 | name: String! 28 | description: String! 29 | imageUrl: String! 30 | quantity: Int! 31 | price: Float! 32 | onSale: Boolean! 33 | category: [Category]! 34 | inCart: Boolean 35 | } 36 | 37 | type Category { 38 | id: ID! 39 | name: String! 40 | products: [String] 41 | } 42 | 43 | type PopulatedCategories { 44 | id: ID! 45 | name: String! 46 | description: String! 47 | imageUrl: String! 48 | quantity: Int! 49 | price: Float! 50 | onSale: Boolean! 51 | products: [Product]! 52 | inCart: Boolean 53 | } 54 | 55 | type Mutation { 56 | addProduct(product: AddProductInput): Product! 57 | addCategory(category: AddCategoryInput): Category! 58 | deleteProduct(id: ID!): Boolean 59 | deleteCategory(id: ID!): Boolean 60 | updateProduct(product: UpdateProductInput): PopulatedProducts 61 | updateCategory(category: UpdateCategoryInput): Category 62 | } 63 | 64 | input AddProductInput { 65 | name: String! 66 | description: String! 67 | imageUrl: String! 68 | quantity: Int! 69 | price: Float! 70 | onSale: Boolean! 71 | category: [String!]! 72 | inCart: Boolean 73 | } 74 | 75 | input AddCategoryInput { 76 | name: String 77 | products: [String] 78 | } 79 | 80 | input UpdateProductInput { 81 | id: ID! 82 | name: String 83 | description: String 84 | imageUrl: String 85 | quantity: Int 86 | price: Float 87 | onSale: Boolean 88 | category: [String!] 89 | inCart: Boolean 90 | } 91 | 92 | input UpdateCategoryInput { 93 | id: ID! 94 | name: String 95 | products: [String] 96 | } 97 | 98 | input FilterProductsInput { 99 | onSale: Boolean 100 | inCart: Boolean 101 | } 102 | `); 103 | -------------------------------------------------------------------------------- /qache-app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./client/**/*.{html,js,ts,tsx,jsx}"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | } 8 | -------------------------------------------------------------------------------- /qache-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "strict": true, 6 | "noImplicitReturns": true, 7 | "noImplicitAny": true, 8 | "module": "es6", 9 | "moduleResolution": "node", 10 | "target": "es6", 11 | "allowJs": true, 12 | "jsx": "react-jsx", 13 | "suppressImplicitAnyIndexErrors": true, 14 | }, 15 | "include": ["client/*", "src", "index.d.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /qache-app/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | // const BundleAnalyzerPlugin = 6 | // require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 7 | 8 | const config = { 9 | entry: './client/index.tsx', 10 | output: { 11 | path: path.resolve(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/', 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(js|jsx)$/, 19 | use: 'babel-loader', 20 | exclude: /node_modules/, 21 | }, 22 | { 23 | test: /\.css$/, 24 | use: [ 25 | MiniCssExtractPlugin.loader, 26 | { 27 | loader: 'css-loader', 28 | options: { 29 | importLoaders: 1, 30 | }, 31 | }, 32 | 'postcss-loader', 33 | ], 34 | }, 35 | { 36 | test: /\.ts(x)?$/, 37 | loader: 'ts-loader', 38 | exclude: /node_modules/, 39 | }, 40 | { 41 | test: /\.scss$/, 42 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 43 | }, 44 | { 45 | test: /\.(png|svg|jpg|gif)$/, 46 | use: [ 47 | { 48 | loader: 'file-loader', 49 | options: { 50 | name: 'images/[name].[ext]', 51 | }, 52 | }, 53 | ], 54 | }, 55 | { 56 | test: /\.svg$/, 57 | use: 'file-loader', 58 | }, 59 | ], 60 | }, 61 | devServer: { 62 | historyApiFallback: true, 63 | }, 64 | plugins: [ 65 | new HtmlWebpackPlugin({ 66 | templateContent: ({ htmlWebpackPlugin }) => 67 | 'Qache
    ', 68 | filename: 'index.html', 69 | }), 70 | new MiniCssExtractPlugin(), 71 | new webpack.EnvironmentPlugin({ 72 | NODE_ENV: 'production', 73 | }), 74 | // new BundleAnalyzerPlugin({ 75 | // analyzerMode: 'static', 76 | // openAnalyzer: false, 77 | // }), 78 | ], 79 | resolve: { 80 | extensions: ['.tsx', '.ts', '.js', '.jsx'], 81 | }, 82 | }; 83 | 84 | module.exports = config; 85 | --------------------------------------------------------------------------------