├── .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 | [](https://github.com/oslabs-beta/QLache-Demo/blob/main/LICENSE)
4 | [](https://www.npmjs.com/package/qlache)
5 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------