├── .DS_Store ├── .gitignore ├── README.md ├── assets └── pink-logo.svg ├── jest.config.js ├── package.json └── qlache-server ├── helpers ├── doublyLL.ts ├── lfu.ts ├── lru.ts └── mru.ts └── src ├── qlache.js └── qlache.ts /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QLache/c87fac3a060069442b1b4eaa3306bcdcddf85578/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/oslabs-beta/QLache-Demo/blob/main/LICENSE) 4 | [![npm version](https://img.shields.io/badge/npm-v1.0-red)](https://www.npmjs.com/package/qlache) 5 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/oslabs-beta/QLache-Demo/issues) 6 | 7 | # QLache 8 | 9 | QLache is a lightweight GraphQL caching library, while providing easy expiration policy configuration. 10 | 11 | Accelerated by [OS Labs](https://github.com/open-source-labs) and developed by [Karan Maan](https://github.com/modestmaan), [Tyler Heathcote](https://github.com/tylerheathcote), [Francheska Orellana](https://github.com/frorellana), [Cameron Kelly](https://github.com/Cam-Kelly), and [Firas Khansa](https://github.com/gitfuego). 12 | 13 | ## Features 14 | - Server-side caching for GraphQL API calls 15 | - LFU, LRU, and MRU eviction policy support out of the box 16 | - Easily configured cache size 17 | 18 | #### In development 19 | - Partial query caching 20 | - Client-side caching 21 | - Expiration policy configuration 22 | 23 | ## Installation 24 | 25 | QLache is available as a package on npm: 26 | - Download @qlache from npm in your terminal with `npm install qlache` 27 | 28 | ## Documentation 29 | 30 | - Access the [QLache Package Docs](https://www.qlache.dev/docs) 31 | 32 | ## Notes 33 | 34 | - More information, including a [demo](https://www.qlache.dev/demo) of our package, is available at `www.qlache.dev` 35 | - Our website source code is available [here](https://github.com/oslabs-beta/QLache-Demo) 36 | 37 | ## Contribute to QLache 38 | 39 | QLache's open source development is ongoing. To contribute, open an issue or a pull request. 40 | 41 | Thank you for your interest and support in our project! 42 | 43 | -The QLache Team 44 | -------------------------------------------------------------------------------- /assets/pink-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qlache", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/oslabs-beta/QLache.git" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/oslabs-beta/QLache/issues" 17 | }, 18 | "homepage": "https://www.qlache.dev", 19 | "dependencies": { 20 | "express": "^4.18.2", 21 | "graphql": "^16.6.0", 22 | "typescript": "^4.9.4" 23 | }, 24 | "devDependencies": { 25 | "@types/express": "^4.17.15", 26 | "@types/jest": "^29.2.4", 27 | "ts-jest": "^29.0.3", 28 | "ts-node": "^10.9.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /qlache-server/helpers/doublyLL.ts: -------------------------------------------------------------------------------- 1 | export class ValNode { 2 | next: ValNode | null; 3 | prev: ValNode | null; 4 | parent: FreqNode | null; 5 | value: object; 6 | key: string; 7 | 8 | constructor(key: string, value: object) { 9 | this.next = null; 10 | this.prev = null; 11 | this.parent = null; 12 | this.value = value; 13 | this.key = key; 14 | } 15 | 16 | shiftVal(newParent: FreqNode, freqLL: DoublyLinkedListFreq): void { 17 | if (!this.prev && !this.next && this.parent) { 18 | freqLL.deleteFreq(this.parent); 19 | } else if (!this.prev) { 20 | if (this.parent) { 21 | this.parent.valList.head = this.next; 22 | if (this.next) { 23 | this.next.prev = null; 24 | } 25 | } 26 | } else if (!this.next) { 27 | if (this.parent) { 28 | this.parent.valList.tail = this.prev; 29 | if (this.prev) { 30 | this.prev.next = null; 31 | } 32 | } 33 | } else { 34 | this.prev.next = this.next; 35 | this.next.prev = this.prev; 36 | } 37 | 38 | this.parent = newParent; 39 | if (!this.parent.valList.head) { 40 | this.parent.valList.head = this; 41 | this.parent.valList.tail = this; 42 | this.next = null; 43 | this.prev = null; 44 | } else { 45 | this.parent.valList.head.prev = this; 46 | this.next = this.parent.valList.head; 47 | this.parent.valList.head = this; 48 | this.prev = null; 49 | } 50 | } 51 | } 52 | 53 | export class DoublyLinkedListVal { 54 | head: ValNode | null; 55 | tail: ValNode | null; 56 | length: number; 57 | 58 | constructor() { 59 | this.head = null; 60 | this.tail = null; 61 | this.length = 0; 62 | } 63 | 64 | add(key: string, value: object, parent?: FreqNode): ValNode { 65 | const node: ValNode = new ValNode(key, value); 66 | if (!this.head) { 67 | this.head = node; 68 | this.tail = node; 69 | this.length++; 70 | } else { 71 | node.next = this.head; 72 | this.head.prev = node; 73 | this.head = node; 74 | this.length++; 75 | } 76 | if (parent) this.head.parent = parent; 77 | return node; 78 | } 79 | 80 | deleteFromTail(): ValNode | undefined { 81 | if (!this.head || !this.tail) return; 82 | else { 83 | this.length--; 84 | const deleted = this.tail; 85 | if (this.head.next === null) { 86 | this.head = null; 87 | this.tail = null; 88 | return deleted; 89 | } 90 | this.tail = deleted.prev; 91 | if (this.tail) this.tail.next = null; 92 | return deleted; 93 | } 94 | } 95 | deleteFromHead(): ValNode | undefined { 96 | if (!this.head || !this.tail) return; 97 | else { 98 | this.length--; 99 | const deleted = this.head; 100 | if (this.head.next) { 101 | const updated = this.head.next; 102 | this.head.next.prev = null; 103 | this.head.next = null; 104 | this.head = updated; 105 | } else { 106 | this.head = null; 107 | this.tail = null; 108 | } 109 | return deleted; 110 | } 111 | } 112 | findAndDelete(node: ValNode): void { 113 | if (!node.next) { 114 | this.deleteFromTail(); 115 | return; 116 | } 117 | if (node.prev) { 118 | const nextNode = node.next; 119 | node.prev.next = nextNode; 120 | if (nextNode) { 121 | nextNode.prev = node.prev; 122 | } 123 | } else this.deleteFromHead(); 124 | } 125 | } 126 | 127 | export class FreqNode { 128 | next: FreqNode | null; 129 | prev: FreqNode | null; 130 | freqValue: number; 131 | valList: DoublyLinkedListVal; 132 | 133 | constructor(freqValue: number) { 134 | this.next = null; 135 | this.prev = null; 136 | this.freqValue = freqValue; 137 | this.valList = new DoublyLinkedListVal(); 138 | } 139 | } 140 | 141 | export class DoublyLinkedListFreq { 142 | head: FreqNode | null; 143 | tail: FreqNode | null; 144 | 145 | constructor() { 146 | this.head = null; 147 | this.tail = null; 148 | } 149 | 150 | addFreq(prevNode?: FreqNode): FreqNode { 151 | if (!prevNode) { 152 | const node = new FreqNode(1); 153 | if (!this.head) { 154 | this.head = node; 155 | this.tail = node; 156 | } else { 157 | this.head.prev = node; 158 | node.next = this.head; 159 | this.head = node; 160 | } 161 | return node; 162 | } 163 | 164 | const val = prevNode.freqValue + 1; 165 | const node: FreqNode = new FreqNode(val); 166 | node.next = prevNode.next; 167 | node.prev = prevNode; 168 | prevNode.next = node; 169 | node.next ? (node.next.prev = node) : (this.tail = node); 170 | 171 | return node; 172 | } 173 | 174 | deleteFreq(currNode: FreqNode): void { 175 | if (!currNode.prev && !currNode.next) { 176 | this.head = null; 177 | this.tail = null; 178 | } else if (!currNode.next && currNode.prev) { 179 | this.tail = currNode.prev; 180 | this.tail.next = null; 181 | } else if (!currNode.prev && currNode.next) { 182 | this.head = currNode.next; 183 | this.head.prev = null; 184 | } else if (currNode.next && currNode.prev) { 185 | currNode.prev.next = currNode.next; 186 | currNode.next.prev = currNode.prev; 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /qlache-server/helpers/lfu.ts: -------------------------------------------------------------------------------- 1 | import { DoublyLinkedListVal, DoublyLinkedListFreq, ValNode } from "./doublyLL"; 2 | 3 | export class LFU { 4 | list: DoublyLinkedListFreq; 5 | cache: object; 6 | capacity: number; 7 | totalValNodes: number; 8 | 9 | constructor(capacity: number) { 10 | this.list = new DoublyLinkedListFreq(); 11 | this.capacity = capacity; 12 | this.cache = {}; 13 | this.totalValNodes = 0; 14 | } 15 | 16 | get(key: string): object | undefined { 17 | if (this.cache.hasOwnProperty(key)) { 18 | const valNode = this.cache[key]; 19 | const freqNode = valNode.parent; 20 | if (freqNode.next && freqNode.next.freqValue === freqNode.freqValue + 1) { 21 | valNode.shiftVal(freqNode.next, this.list); 22 | } else { 23 | const newParent = this.list.addFreq(freqNode); 24 | valNode.shiftVal(newParent, this.list); 25 | } 26 | return valNode.value; 27 | } else return; 28 | } 29 | 30 | post(key: string, value: object): void { 31 | if (this.totalValNodes === this.capacity) { 32 | const deletedVal = this.list.head?.valList.deleteFromTail(); 33 | 34 | if (deletedVal) delete this.cache[deletedVal.key]; 35 | if (!this.list.head?.valList.head && deletedVal?.parent) 36 | this.list.deleteFreq(deletedVal.parent); 37 | this.totalValNodes--; 38 | } 39 | const valNode: ValNode = new ValNode(key, value); 40 | this.cache[key] = valNode; 41 | if (this.list.head?.freqValue !== 1 || this.list.head === null) { 42 | valNode.shiftVal(this.list.addFreq(), this.list); 43 | } else { 44 | valNode.shiftVal(this.list.head, this.list); 45 | } 46 | this.totalValNodes++; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /qlache-server/helpers/lru.ts: -------------------------------------------------------------------------------- 1 | import { DoublyLinkedListVal } from "./doublyLL"; 2 | 3 | export class LRU { 4 | list: DoublyLinkedListVal; 5 | cache: object; 6 | capacity: number; 7 | 8 | constructor(capacity: number) { 9 | this.list = new DoublyLinkedListVal(); 10 | this.capacity = capacity; 11 | this.cache = {}; 12 | } 13 | 14 | get(key: string): object | undefined { 15 | if (this.cache.hasOwnProperty(key)) { 16 | const value = this.cache[key].value; 17 | this.list.findAndDelete(this.cache[key]); 18 | this.cache[key] = this.list.add(key, value); 19 | 20 | return value; 21 | } else return; 22 | } 23 | 24 | post(key: string, value: object): void { 25 | if (this.list.length === this.capacity) { 26 | const deletedVal = this.list.deleteFromTail(); 27 | if (deletedVal) delete this.cache[deletedVal.key]; 28 | } 29 | const newNode = this.list.add(key, value); 30 | this.cache[key] = newNode; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /qlache-server/helpers/mru.ts: -------------------------------------------------------------------------------- 1 | import { DoublyLinkedListVal } from "./doublyLL"; 2 | 3 | export class MRU { 4 | list: DoublyLinkedListVal; 5 | cache: object; 6 | capacity: number; 7 | constructor(capacity: number) { 8 | this.list = new DoublyLinkedListVal(); 9 | this.capacity = capacity; 10 | this.cache = {}; 11 | } 12 | get(key: string): object | undefined { 13 | if (this.cache.hasOwnProperty(key)) { 14 | const value = this.cache[key].value; 15 | this.list.findAndDelete(this.cache[key]); 16 | this.cache[key] = this.list.add(key, value); 17 | return value; 18 | } else return; 19 | } 20 | 21 | post(key: string, value: object): void { 22 | const newNode = this.list.add(key, value); 23 | this.cache[key] = newNode; 24 | if (this.list.length > this.capacity) { 25 | const deletedVal = this.list.deleteFromHead(); 26 | if (deletedVal) delete this.cache[deletedVal.key]; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /qlache-server/src/qlache.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QLache/c87fac3a060069442b1b4eaa3306bcdcddf85578/qlache-server/src/qlache.js -------------------------------------------------------------------------------- /qlache-server/src/qlache.ts: -------------------------------------------------------------------------------- 1 | import { LRU } from "../helpers/lru.js"; 2 | import { LFU } from "../helpers/lfu.js"; 3 | import { MRU } from "../helpers/mru"; 4 | import { parse, print } from "graphql/language"; 5 | 6 | interface options { 7 | cache?: string; 8 | port?: number; 9 | hostname?: string; 10 | expire?: string | number; 11 | respondOnHit?: boolean; 12 | capacity?: number; 13 | } 14 | 15 | class Qlache { 16 | apiURL: string; 17 | evictionPolicy: LRU | LFU | MRU; 18 | capacity: number; 19 | 20 | constructor(apiURL, type, capacity) { 21 | this.apiURL = apiURL; 22 | this.evictionPolicy = this.setEvictionPolicy(type); 23 | this.capacity = capacity; 24 | this.query = this.query.bind(this); 25 | } 26 | 27 | query(req, res, next) { 28 | const document = parse(req.body.query); 29 | const query: string = print(document); 30 | 31 | const value: object | undefined = this.evictionPolicy.get(query); 32 | if (value === undefined) { 33 | fetch(this.apiURL, { 34 | method: "POST", 35 | headers: { "Content-Type": "application/json" }, 36 | body: JSON.stringify({ 37 | query, 38 | }), 39 | }) 40 | .then((response) => response.json()) 41 | .then((data) => { 42 | this.evictionPolicy.post(query, data); 43 | const queryResponse: object = data; 44 | res.locals.queryRes = queryResponse; 45 | return next(); 46 | }); 47 | } else { 48 | res.locals.queryRes = value; 49 | return next(); 50 | } 51 | } 52 | 53 | setEvictionPolicy(evictionPolicy: string) { 54 | switch (evictionPolicy) { 55 | case "LFU": 56 | return new LFU(this.capacity); 57 | case "LRU": 58 | return new LRU(this.capacity); 59 | case "MRU": 60 | return new MRU(this.capacity); 61 | default: 62 | return new LRU(this.capacity); 63 | } 64 | } 65 | } 66 | 67 | export default Qlache; 68 | --------------------------------------------------------------------------------