├── .gitignore ├── zoic_dev_tool ├── devtools.js ├── zoic_new.png ├── devtools.html ├── manifest.json ├── styles.css ├── panel.html ├── README.md └── main.js ├── deno.json ├── src ├── types.ts ├── tests │ ├── test_server.ts │ ├── performanceMetrics_test.ts │ ├── lfu_test.ts │ ├── lru_test.ts │ ├── zoic_test.ts │ └── doublyLinkedList_test.ts ├── performanceMetrics.ts ├── lfu.ts ├── lru.ts └── doublyLinkedLists.ts ├── deps.ts ├── .github └── workflows │ └── deno.yml ├── LICENSE.txt ├── README.md └── zoic.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | src/tests/coverage_report/ 3 | deno.lock 4 | -------------------------------------------------------------------------------- /zoic_dev_tool/devtools.js: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create('Zoic', null, '/panel.html', null); -------------------------------------------------------------------------------- /zoic_dev_tool/zoic_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/zoic/HEAD/zoic_dev_tool/zoic_new.png -------------------------------------------------------------------------------- /zoic_dev_tool/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /zoic_dev_tool/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Zoic Dev Tools", 4 | "version": "1.0", 5 | "description": "Developer tool for Zoic, a caching middleware library for Oak/Deno.", 6 | "author": "Zoic Team", 7 | "devtools_page": "devtools.html" 8 | } -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Zoic", 3 | "version": "1.0.7", 4 | "description": "Caching middleware library for Oak", 5 | "exports": { 6 | ".": "./zoic.ts" 7 | }, 8 | "tasks": { 9 | "test": "deno test ./src/tests/ --allow-net --allow-import" 10 | }, 11 | "imports": {} 12 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Options { 2 | cache?: string; 3 | port?: number; 4 | hostname?: string; 5 | expire?: string | number; 6 | respondOnHit?: boolean; 7 | capacity?: number; 8 | } 9 | 10 | export interface CacheValue { 11 | headers: { [k:string]:string }; 12 | body: Uint8Array; 13 | status: number; 14 | } 15 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | assert, 3 | assertEquals, 4 | assertInstanceOf, 5 | assertThrows, 6 | assertRejects, 7 | assertExists 8 | } from "https://deno.land/std@0.224.0/testing/asserts.ts"; 9 | export { decode as base64decode, encode as base64encode } from 'https://deno.land/std@0.89.0/encoding/base64.ts'; 10 | export { Context, Application, Router } from "https://deno.land/x/oak@v17.1.4/mod.ts"; 11 | export { connect } from "https://deno.land/x/redis@v0.37.1/mod.ts"; 12 | export { oakCors } from "https://deno.land/x/cors@v1.2.2/mod.ts"; 13 | export { describe, it } from "https://deno.land/std@0.224.0/testing/bdd.ts"; 14 | 15 | export type { ApplicationListenEvent } from "https://deno.land/x/oak@v17.1.4/application.ts"; 16 | export type { Redis } from "https://deno.land/x/redis@v0.37.1/mod.ts"; 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/deno.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: tests 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Setup repo 12 | uses: actions/checkout@v3 13 | 14 | - name: Setup Deno 15 | uses: denoland/setup-deno@main 16 | with: 17 | deno-version: canary 18 | 19 | - name: Run lint 20 | run: deno lint **/*.ts 21 | 22 | - name: Run tests 23 | run: deno test --coverage=./src/tests/coverage_report ./src/tests/ --allow-net --allow-import 24 | 25 | - name: Create coverage report 26 | run: deno coverage ./src/tests/coverage_report --lcov > coverage_report.lcov 27 | 28 | - name: Upload coverage report 29 | uses: codecov/codecov-action@v3 30 | with: 31 | file: ./coverage_report.lcov 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Zoic Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/tests/test_server.ts: -------------------------------------------------------------------------------- 1 | import { Application, Router } from "../../deps.ts"; 2 | import type { ApplicationListenEvent } from "../../deps.ts"; 3 | 4 | // Test server setup utility with proper cleanup 5 | export class TestServer { 6 | private app: Application; 7 | private controller: AbortController; 8 | private router: Router; 9 | private port?: number; 10 | 11 | constructor() { 12 | this.app = new Application(); 13 | this.controller = new AbortController(); 14 | this.router = new Router(); 15 | this.app.use(this.router.routes()); 16 | } 17 | 18 | public getRouter(): Router { 19 | return this.router; 20 | } 21 | 22 | public getPort(): number { 23 | return this.port ?? 80; 24 | } 25 | 26 | public start(): Promise { 27 | this.controller = this.createAbortController(); 28 | return new Promise((resolve) => { 29 | const listener = (evt: ApplicationListenEvent) => { 30 | this.port = evt.port; 31 | resolve(evt.port); 32 | }; 33 | 34 | this.app.addEventListener("listen", listener, { once: true }); 35 | 36 | this.app.listen({ 37 | port: 0, 38 | signal: this.controller.signal, 39 | }); 40 | }); 41 | } 42 | 43 | public stop(): void { 44 | this.controller.abort(); 45 | } 46 | 47 | private createAbortController(): AbortController { 48 | return new AbortController(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/performanceMetrics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Keep tracks of in-memory cache performance 3 | */ 4 | class PerfMetrics { 5 | cacheType: 'LRU' | 'LFU' | 'Redis'; 6 | memoryUsed: number; 7 | numberOfEntries: number; 8 | readsProcessed: number; 9 | writesProcessed: number; 10 | currentHitLatency: number; 11 | currentMissLatency: number; 12 | missLatencyTotal: number; 13 | hitLatencyTotal: number; 14 | 15 | constructor() { 16 | this.cacheType = 'LRU'; 17 | this.memoryUsed = 0; 18 | this.numberOfEntries = 0; 19 | this.readsProcessed = 0; 20 | this.writesProcessed = 0; 21 | this.currentHitLatency = 0; 22 | this.currentMissLatency = 0; 23 | this.missLatencyTotal = 0; 24 | this.hitLatencyTotal = 0; 25 | } 26 | 27 | public addEntry = (): number => this.numberOfEntries++; 28 | public deleteEntry = (): number => this.numberOfEntries--; 29 | public readProcessed = (): number => this.readsProcessed++; 30 | public writeProcessed = (): number => this.writesProcessed++; 31 | public clearEntires = (): number => this.numberOfEntries = 0; 32 | public increaseBytes = (bytes: number): number => this.memoryUsed += bytes; 33 | public decreaseBytes = (bytes: number): number => this.memoryUsed -= bytes; 34 | public updateLatency = (latency: number, hitOrMiss: 'hit' | 'miss'): void => { 35 | if (hitOrMiss === 'hit'){ 36 | this.hitLatencyTotal += latency; 37 | this.currentHitLatency = latency; 38 | return; 39 | } 40 | if (hitOrMiss === 'miss'){ 41 | this.missLatencyTotal += latency; 42 | this.currentMissLatency = latency; 43 | return; 44 | } 45 | 46 | throw new TypeError('Hit or miss not specified'); 47 | }; 48 | } 49 | 50 | export default PerfMetrics; 51 | -------------------------------------------------------------------------------- /zoic_dev_tool/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Cabin:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Fira+Sans:wght@200&display=swap'); 2 | 3 | 4 | body { 5 | background-color: #35465c; 6 | } 7 | 8 | button { 9 | border-radius: 28px; 10 | font-family: Arial; 11 | color: #ffffff; 12 | font-size: 15px; 13 | background: #35465c; 14 | border: 2px solid white; 15 | padding: 5px 10px 5px 10px; 16 | text-decoration: none; 17 | } 18 | 19 | button:hover { 20 | background: #26384d; 21 | text-decoration: none; 22 | } 23 | 24 | .logoText { 25 | margin-top: 150px; 26 | } 27 | 28 | #loadingError{ 29 | color: rgb(215, 16, 16); 30 | } 31 | 32 | #localHostInputID { 33 | width: 250px; 34 | } 35 | 36 | #root { 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: center; 40 | align-items: center; 41 | } 42 | 43 | .metricsContainer { 44 | background-color: white; 45 | margin: 10px; 46 | display: flex; 47 | flex-direction: column; 48 | align-items: center; 49 | justify-content: center; 50 | text-align: center; 51 | } 52 | 53 | .metricContainer { 54 | width: 150px; 55 | border: 1px solid rgba(0, 0, 0, 0); 56 | border-radius: 2px; 57 | padding: 5px; 58 | display: inline-block; 59 | border-right: 0px; 60 | text-align: right; 61 | } 62 | 63 | .valueContainer { 64 | width: 150px; 65 | border: 1px solid rgba(0, 0, 0, 0); 66 | border-radius: 2px; 67 | padding: 5px; 68 | display: inline-block; 69 | font-weight: bold; 70 | border-left: 0px; 71 | text-align: left; 72 | } 73 | 74 | #hank { 75 | text-align: center; 76 | font-size: 100px; 77 | margin-bottom: 7px; 78 | margin-top: 20px; 79 | } 80 | 81 | #lol { 82 | text-align: center; 83 | color: white; 84 | } 85 | 86 | div { 87 | font-family: 'Cabin', sans-serif; 88 | } 89 | 90 | 91 | .field { 92 | display: flex; 93 | flex-direction: row; 94 | } 95 | 96 | .catContainer { 97 | display: flex; 98 | justify-content: center; 99 | align-items: center; 100 | } 101 | 102 | .text { 103 | text-align: left; 104 | } -------------------------------------------------------------------------------- /zoic_dev_tool/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |
8 | zoic logo 9 |

devtools panel

10 |
11 | 12 | 13 | 14 |
15 |

16 |
17 |
18 |
Cache type:
19 |
...
20 |
21 |
22 |
Memory used:
23 |
...
24 |
25 |
26 |
Number of entries:
27 |
...
28 |
29 |
30 |
Reads processed:
31 |
...
32 |
33 | 34 |
35 |
Writes processed:
36 |
...
37 |
38 | 39 |
40 |
Average cache hit latency:
41 |
...
42 |
43 | 44 |
45 |
Average cache miss latency:
46 |
...
47 |
48 |
49 |
50 |
51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /zoic_dev_tool/README.md: -------------------------------------------------------------------------------- 1 |

Zoic logo 4 |

5 |
6 |
7 |

Developer Tool

8 |
9 |
10 | 11 | ## Table of Contents 12 | 1. [Description](#description) 13 | 2. [Installation](#installation) 14 | 3. [Usage and Configuration](#usage) 15 | 4. [Authors](#authors) 16 | 17 | 18 | ## Description 19 | 20 | The Zoic Developer Tool is a Chrome Developer Tools extension for monitoring metrics in a ZoicCache instance. With the Zoic Developer Tool, you can monitor and inspect memory usage, reads processed, writes processed, latency, and more. 21 | 22 | ## Installation 23 | The Zoic Developer Tool is currently available as a Chrome Developer Tools extension. The easiest to get it is to [add it from the Chrome Web Store.](https://chrome.google.com/webstore/detail/zoic-dev-tools/cnoohkfilnjedjeamhmpokfgaadgkgcl) 24 | 25 | The Zoic Developer Tool's latest build can be also be added manually as a Chrome extension. In the Chrome Extensions Page (`chrome://extensions/`), click on "Load unpacked" and navigate to `.../zoic/zoic_dev_tool/` and click "Select". (You may need to toggle on "Developer mode" to do this.) The extension should now be loaded and available in the Chrome Developer Tools. 26 | 27 | The Zoic Developer Tool will also be available for download via the Chrome Web Store soon. 28 | 29 | ## Usage and Configuration 30 | 31 | To configure the dev tools, you must first link your server address via the input field on the dev tool panel. 32 | - First: Specify your server address, and endpoint at which you will serve the cache metrics from. (Ex: `http://localhost:3000/zoicMetrics`) 33 | - Second: In your server routes, create a new route matching the endpoint specified in the dev tool. In this route add middleware `Zoic.getMetrics`. 34 | 35 | 36 | NOTE: This route WILL have CORS enabled. 37 | 38 | #### Example configuration: 39 | ```typescript 40 | const cache = new Zoic(); 41 | 42 | router.get('/zoicMetrics', cache.getMetrics); 43 | ``` 44 | 45 | ## Authors 46 | 47 | - [Joe Borrow](https://github.com/jmborrow) 48 | - [Celena Chan](https://github.com/celenachan) 49 | - [Aaron Dreyfuss](https://github.com/AaronDreyfuss) 50 | - [Hank Jackson](https://github.com/hankthetank27) 51 | - [Jasper Narvil](https://github.com/jnarvil3) 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/tests/performanceMetrics_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertInstanceOf, 4 | type Context 5 | } from "../../deps.ts"; 6 | import Zoic from '../../zoic.ts'; 7 | import PerfMetrics from '../performanceMetrics.ts'; 8 | import { TestServer } from './test_server.ts'; 9 | 10 | Deno.test("Cache should contain correct metrics", async (t) => { 11 | const cache = new Zoic({capacity:5}); 12 | 13 | await t.step("should have a metrics property with an object as its value", () => { 14 | assertInstanceOf(cache.metrics, PerfMetrics); 15 | }); 16 | 17 | await t.step("should initialize each metric to correct type", () => { 18 | assertEquals(cache.metrics.numberOfEntries, 0); 19 | assertEquals(cache.metrics.readsProcessed, 0); 20 | assertEquals(cache.metrics.writesProcessed, 0); 21 | assertEquals(cache.metrics.currentHitLatency, 0); 22 | assertEquals(cache.metrics.currentMissLatency, 0); 23 | assertEquals(cache.metrics.missLatencyTotal, 0); 24 | assertEquals(cache.metrics.hitLatencyTotal, 0); 25 | }); 26 | }); 27 | 28 | Deno.test("Each metric property updated accurately", async (t) => { 29 | const cache = new Zoic({capacity:3}); 30 | const server = new TestServer(); 31 | const router = server.getRouter(); 32 | 33 | // Setup routes for tests 1-4 34 | Array.from({ length: 4 }, (_, i) => i + 1).forEach(i => { 35 | router.get(`/test${i}`, cache.use, (ctx: Context) => { 36 | ctx.response.body = 'testing123'; 37 | }); 38 | }); 39 | 40 | const port = await server.start(); 41 | 42 | try { 43 | const baseUrl = `http://localhost:${port}/test`; 44 | 45 | // Sequential requests with immediate body consumption 46 | for (const response of [ 47 | await fetch(baseUrl + '1'), 48 | await fetch(baseUrl + '1'), 49 | await fetch(baseUrl + '2'), 50 | await fetch(baseUrl + '2'), 51 | await fetch(baseUrl + '2'), 52 | await fetch(baseUrl + '2'), 53 | await fetch(baseUrl + '3'), 54 | await fetch(baseUrl + '4') 55 | ]) { 56 | await response.body?.cancel(); 57 | } 58 | 59 | await t.step("should handle numberOfEntries correctly", () => { 60 | assertEquals(cache.metrics.numberOfEntries, 3); 61 | }); 62 | 63 | await t.step("should have a readProcessed method that updates readsProcessed correctly", () => { 64 | assertEquals(cache.metrics.readsProcessed, 4); 65 | }); 66 | 67 | await t.step("should have a writeProcessed method that updates writesProcessed correctly", () => { 68 | assertEquals(cache.metrics.writesProcessed, 4); 69 | }); 70 | 71 | await t.step("should have an increaseBytes method that updates memoryUsed correctly", () => { 72 | assertEquals(cache.metrics.memoryUsed, 390); 73 | }); 74 | } finally { 75 | server.stop(); 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /zoic_dev_tool/main.js: -------------------------------------------------------------------------------- 1 | const root = document.querySelector('body'); 2 | const metrics = document.createElement('div'); 3 | 4 | root.appendChild(metrics); 5 | metrics.setAttribute('class', 'metricsContainer'); 6 | 7 | const cacheType = document.querySelector('#cacheType'); 8 | const memoryUsed = document.querySelector('#memUsed'); 9 | const entries = document.querySelector('#entries'); 10 | const hits = document.querySelector('#hits'); 11 | const misses = document.querySelector('#misses'); 12 | const avHitLatency = document.querySelector('#avHitLatency'); 13 | const avMissLatency = document.querySelector('#avMissLatency'); 14 | 15 | document.querySelector('#localHostInputID').setAttribute('size',document.querySelector('#localHostInputID').getAttribute('placeholder').length); 16 | const urlForm = document.querySelector('#localHostInputID'); 17 | urlForm.value = 'http://localhost:3000/zoicMetrics'; 18 | 19 | let fetchInterval; 20 | const button = document.querySelector('button'); 21 | button.addEventListener('click', () => { 22 | if (fetchInterval) clearInterval(fetchInterval); 23 | const serverURL = document.querySelector('#localHostInputID').value; 24 | urlForm.value = ''; 25 | document.querySelector('#loadingError').innerHTML = ''; 26 | 27 | hits.innerHTML = 'loading...'; 28 | misses.innerHTML = 'loading...'; 29 | entries.innerHTML = 'loading...'; 30 | cacheType.innerHTML = 'loading...'; 31 | memoryUsed.innerHTML = 'loading...'; 32 | avHitLatency.innerHTML = 'loading...'; 33 | avMissLatency.innerHTML = 'loading...'; 34 | 35 | fetchInterval = setInterval(() => { 36 | fetch(`${serverURL}`, { 37 | method:'get', 38 | mode: 'cors' 39 | }) 40 | .then(response => response.json()) 41 | .then(metricsData => { 42 | 43 | if(metricsData) { 44 | const { 45 | cache_type, 46 | memory_used, 47 | number_of_entries, 48 | reads_processed, 49 | writes_processed, 50 | average_hit_latency, 51 | average_miss_latency 52 | } = metricsData; 53 | 54 | cacheType.innerHTML = `${cache_type}`; 55 | memoryUsed.innerHTML = `${(memory_used / 1048576)?.toString()?.slice(0, 6) || '0'}mb` 56 | entries.innerHTML = `${number_of_entries || '0'}`; 57 | hits.innerHTML = `${reads_processed || '0'}`; 58 | misses.innerHTML = `${writes_processed || '0'}`; 59 | avHitLatency.innerHTML = `${average_hit_latency?.toString()?.slice(0, 5) || '0'}ms`; 60 | avMissLatency.innerHTML = `${average_miss_latency?.toString()?.slice(0, 6) || '0'}ms`; 61 | } 62 | }).catch((err) =>{ 63 | document.querySelector('#loadingError').innerHTML = "Error loading URL."; 64 | clearInterval(fetchInterval); 65 | return console.log(err); 66 | }); 67 | }, 500); 68 | }); -------------------------------------------------------------------------------- /src/lfu.ts: -------------------------------------------------------------------------------- 1 | import { FreqDoublyLinkedList, type Node } from './doublyLinkedLists.ts' 2 | import type { CacheValue } from './types.ts' 3 | import type PerfMetrics from './performanceMetrics.ts' 4 | 5 | 6 | /** 7 | * Spec as per: 8 | * http://dhruvbird.com/lfu.pdf 9 | */ 10 | 11 | class LFU { 12 | freqList: FreqDoublyLinkedList; 13 | cache: Record; 14 | length: number; 15 | capacity: number; 16 | expire: number; 17 | metrics: PerfMetrics; 18 | 19 | constructor(expire: number, metrics: PerfMetrics, capacity: number) { 20 | this.freqList = new FreqDoublyLinkedList(); 21 | this.cache = {}; 22 | this.length = 0; 23 | this.capacity = capacity; 24 | this.expire = expire; 25 | this.metrics = metrics; 26 | } 27 | 28 | /** 29 | * Adds new item to cache. 30 | * @param key 31 | * @param value 32 | * @returns 33 | */ 34 | public put(key: string, value: CacheValue, byteSize: number): CacheValue | undefined { 35 | if (this.cache[key]){ 36 | this.metrics.decreaseBytes(this.cache[key].byteSize); 37 | this.metrics.increaseBytes(byteSize); 38 | 39 | this.cache[key].value = value; 40 | return this.get(key); 41 | } 42 | 43 | this.cache[key] = this.freqList.addNewFreq(key, value, byteSize, new Date()); 44 | this.metrics.increaseBytes(byteSize); 45 | 46 | if (this.length < this.capacity) { 47 | this.length++; 48 | return; 49 | } 50 | 51 | const deletedNode: Node | undefined = this.freqList.deleteLeastFreq(); 52 | if (!deletedNode) { 53 | throw new Error('Node is null. Ensure cache capcity is greater than 0.'); 54 | } 55 | 56 | delete this.cache[deletedNode.key]; 57 | this.metrics.decreaseBytes(deletedNode.byteSize); 58 | } 59 | 60 | public get(key: string): CacheValue | undefined { 61 | if (!this.cache[key]) { 62 | return; 63 | } 64 | 65 | //if entry is stale, deletes and exits 66 | const currentTime = new Date(); 67 | const timeElapsed = Math.abs(currentTime.getTime() - this.cache[key].timeStamp.getTime()) / 1000; 68 | 69 | if (timeElapsed > this.expire) { 70 | this.metrics.decreaseBytes(this.cache[key].byteSize); 71 | this.delete(key); 72 | return; 73 | } 74 | 75 | const node = this.freqList.increaseFreq(this.cache[key]); 76 | if (node){ 77 | this.cache[key] = node; 78 | return node.value; 79 | } 80 | } 81 | 82 | public delete(key: string): Node | undefined { 83 | const node = this.cache[key]; 84 | if (!node) { 85 | return; 86 | } 87 | 88 | this.freqList.deleteValNode(node); 89 | 90 | delete this.cache[key]; 91 | this.length--; 92 | 93 | return node; 94 | } 95 | 96 | public clear(): void { 97 | this.freqList = new FreqDoublyLinkedList(); 98 | this.cache = {}; 99 | this.length = 0; 100 | this.metrics.clearEntires(); 101 | } 102 | } 103 | 104 | export default LFU; 105 | -------------------------------------------------------------------------------- /src/lru.ts: -------------------------------------------------------------------------------- 1 | import { ValueDoublyLinkedList, type Node } from './doublyLinkedLists.ts' 2 | import type { CacheValue } from './types.ts' 3 | import type PerfMetrics from './performanceMetrics.ts' 4 | 5 | /** 6 | * Cache implementing a least recently used eviction policy. 7 | * O(1) insert, lookup, and deletion time. 8 | */ 9 | class LRU { 10 | list: ValueDoublyLinkedList; 11 | cache: Record; 12 | length: number; 13 | capacity: number; 14 | expire: number; 15 | metrics: PerfMetrics; 16 | 17 | constructor (expire: number, metrics: PerfMetrics, capacity: number) { 18 | this.list = new ValueDoublyLinkedList(); 19 | this.cache = {}; 20 | this.length = 0; 21 | this.capacity = capacity; 22 | this.expire = expire; 23 | this.metrics = metrics; 24 | this.get = this.get.bind(this); 25 | this.put = this.put.bind(this); 26 | this.delete = this.delete.bind(this); 27 | } 28 | 29 | 30 | /** 31 | * Adds new item to cache. 32 | * @param key 33 | * @param value 34 | * @returns 35 | */ 36 | public put(key: string, value: CacheValue, byteSize: number): CacheValue | undefined { 37 | //if key alreadys exits in cache, replace key value with new value, and move to list head. 38 | if (this.cache[key]){ 39 | this.metrics.decreaseBytes(this.cache[key].byteSize); 40 | this.metrics.increaseBytes(byteSize); 41 | 42 | this.cache[key].value = value; 43 | return this.get(key); 44 | } 45 | 46 | //add new item to list head. 47 | this.cache[key] = this.list.addHead(key, value, byteSize, new Date()); 48 | this.metrics.increaseBytes(byteSize); 49 | 50 | //evalutes if least recently used item should be evicted. 51 | if (this.length < this.capacity) { 52 | this.length++; 53 | return; 54 | } 55 | 56 | const deletedNode: Node | null = this.list.deleteTail(); 57 | if (deletedNode === null) { 58 | throw new Error('Node is null. Ensure cache capcity is greater than 0.'); 59 | } 60 | 61 | delete this.cache[deletedNode.key]; 62 | this.metrics.decreaseBytes(deletedNode.byteSize); 63 | } 64 | 65 | /** 66 | * Gets item from cache and moves to head -- most recently used. 67 | * @param key 68 | * @returns 69 | */ 70 | public get(key: string): CacheValue | undefined { 71 | //If no matching cache value (cache miss), return next(); 72 | if (!this.cache[key]) { 73 | return; 74 | } 75 | 76 | //if entry is stale, deletes and exits 77 | const currentTime = new Date(); 78 | const timeElapsed = Math.abs(currentTime.getTime() - this.cache[key].timeStamp.getTime()) / 1000; 79 | if (timeElapsed > this.expire) { 80 | this.metrics.decreaseBytes(this.cache[key].byteSize); 81 | this.delete(key); 82 | return; 83 | } 84 | 85 | // if current key is already node at head of list, return immediately. 86 | if (this.cache[key] === this.list.head) { 87 | return this.list.head.value; 88 | } 89 | 90 | //create new node, then delete node at current key, to replace at list head. 91 | const node = this.cache[key]; 92 | this.delete(key); 93 | this.cache[key] = this.list.addHead(node.key, node.value, node.byteSize, node.timeStamp); 94 | this.length++; 95 | 96 | //Return the newly cached node, which should now be the head, to the top-level caching layer. 97 | return node.value; 98 | } 99 | 100 | /** 101 | * Removes item from any location in the cache. 102 | * @param key 103 | * @returns 104 | */ 105 | public delete(key: string): Node | undefined { 106 | const node = this.cache[key]; 107 | if (!node) { 108 | return; 109 | } 110 | 111 | this.list.delete(node); 112 | delete this.cache[key]; 113 | this.length--; 114 | 115 | return node; 116 | } 117 | 118 | /** 119 | * Clears entire cache contents. 120 | */ 121 | public clear(): void { 122 | this.list = new ValueDoublyLinkedList(); 123 | this.cache = {}; 124 | this.length = 0; 125 | this.metrics.clearEntires(); 126 | } 127 | } 128 | 129 | export default LRU; 130 | -------------------------------------------------------------------------------- /src/doublyLinkedLists.ts: -------------------------------------------------------------------------------- 1 | import type { CacheValue } from './types.ts' 2 | 3 | /** 4 | * Class definition for linked list containing cached values for both LRU and LFU. 5 | */ 6 | export class Node { 7 | next: Node | null; 8 | prev: Node | null; 9 | value: CacheValue; 10 | key: string; 11 | count: number; 12 | byteSize: number; 13 | timeStamp: Date; 14 | parent?: FreqNode; 15 | 16 | constructor( 17 | key: string, 18 | value: CacheValue, 19 | byteSize: number, 20 | timeStamp: Date, 21 | parent?: FreqNode 22 | ) { 23 | this.next = null; 24 | this.prev = null; 25 | this.value = value; 26 | this.key = key; 27 | this.count = 1; 28 | this.byteSize = byteSize; 29 | this.timeStamp = timeStamp; 30 | this.parent = parent; 31 | } 32 | } 33 | 34 | export class ValueDoublyLinkedList { 35 | head: Node | null; 36 | tail: Node | null; 37 | 38 | constructor(){ 39 | this.head = null; 40 | this.tail = null; 41 | } 42 | 43 | public addHead( 44 | key: string, 45 | value: CacheValue, 46 | byteSize: number, 47 | timeStamp: Date, 48 | parent?: FreqNode 49 | ): Node { 50 | const node = new Node(key, value, byteSize, timeStamp, parent); 51 | if (!this.head) { 52 | this.head = node; 53 | this.tail = this.head; 54 | } else { 55 | node.next = this.head; 56 | this.head.prev = node; 57 | this.head = node; 58 | } 59 | return this.head; 60 | } 61 | 62 | public delete(node: Node | null): Node | undefined { 63 | if (!node) { 64 | return; 65 | } 66 | 67 | if (node.prev) { 68 | node.prev.next = node.next; 69 | } else { 70 | this.head = node.next; 71 | } 72 | 73 | if (node.next) { 74 | node.next.prev = node.prev; 75 | } else { 76 | this.tail = node.prev; 77 | } 78 | 79 | return node; 80 | } 81 | 82 | public deleteTail(): Node | null { 83 | if (!this.tail) { 84 | return null; 85 | } 86 | 87 | const deleted = this.tail; 88 | if (this.head === this.tail) { 89 | // handle single-node case 90 | this.head = this.tail = null; 91 | } else { 92 | // handle multiple-node case 93 | this.tail = this.tail.prev; 94 | if (this.tail) { 95 | this.tail.next = null; 96 | } 97 | } 98 | 99 | // cleanup for deleted node's references 100 | deleted.prev = null; 101 | deleted.next = null; 102 | 103 | return deleted; 104 | } 105 | } 106 | 107 | /** 108 | * Class definition for linked list containing lists a given freqency value for LFU. 109 | */ 110 | export class FreqNode { 111 | freqValue: number; 112 | valList: ValueDoublyLinkedList; 113 | next: FreqNode | null; 114 | prev: FreqNode | null; 115 | 116 | constructor(freqValue: number) { 117 | this.freqValue = freqValue; 118 | this.valList = new ValueDoublyLinkedList(); 119 | this.next = null; 120 | this.prev = null; 121 | } 122 | } 123 | 124 | 125 | export class FreqDoublyLinkedList { 126 | head: FreqNode | null; 127 | tail: FreqNode | null; 128 | 129 | constructor() { 130 | //head being lowest freq item, tail being highest. 131 | this.head = null; 132 | this.tail = null; 133 | } 134 | 135 | public addNewFreq(key: string, value: CacheValue, byteSize: number, timeStamp: Date): Node { 136 | if (!this.head) { 137 | this.head = new FreqNode(1); 138 | this.tail = this.head; 139 | } else if (this.head.freqValue !== 1) { 140 | const freqNode = new FreqNode(1); 141 | this.head.prev = freqNode; 142 | freqNode.next = this.head; 143 | this.head = freqNode; 144 | } 145 | 146 | return this.head.valList.addHead(key, value, byteSize, timeStamp, this.head); 147 | } 148 | 149 | public increaseFreq(node: Node): Node | undefined { 150 | if (!node.parent) { 151 | return; 152 | } 153 | 154 | const { key, value, byteSize, timeStamp, parent } = node; 155 | 156 | //is highest freq 157 | if (!parent.next){ 158 | const freqNode = new FreqNode(parent.freqValue + 1); 159 | parent.next = freqNode; 160 | freqNode.prev = parent; 161 | this.tail = freqNode; 162 | 163 | //freq + 1 does not exist 164 | } else if (parent.next.freqValue !== parent.freqValue + 1){ 165 | const freqNode = new FreqNode(parent.freqValue + 1); 166 | const temp = parent.next; 167 | 168 | parent.next = freqNode; 169 | freqNode.prev = parent; 170 | freqNode.next = temp; 171 | temp.prev = freqNode; 172 | } 173 | 174 | this.deleteValNode(node); 175 | return parent.next.valList.addHead(key, value, byteSize, timeStamp, parent.next); 176 | } 177 | 178 | public deleteLeastFreq = (): Node | undefined => this.head ? 179 | this.deleteValNode(this.head.valList.tail) 180 | : undefined; 181 | 182 | public deleteValNode(node: Node | null): Node | undefined { 183 | if (!node || !node.parent) { 184 | return; 185 | } 186 | 187 | const { valList } = node.parent; 188 | valList.delete(node); 189 | if (!valList.head) { 190 | this.delete(node.parent); 191 | } 192 | 193 | return node; 194 | } 195 | 196 | public delete(freqNode: FreqNode | null): FreqNode | undefined { 197 | if (!freqNode) { 198 | return; 199 | } 200 | 201 | if (freqNode.prev) { 202 | freqNode.prev.next = freqNode.next; 203 | } else { 204 | this.head = freqNode.next; 205 | } 206 | 207 | if (freqNode.next) { 208 | freqNode.next.prev = freqNode.prev; 209 | } else { 210 | this.tail = freqNode.prev; 211 | } 212 | 213 | return freqNode; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Caching middleware library for Oak

4 |
5 |
6 | 7 | ## Table of Contents 8 | 9 | 1. [Description](#description) 10 | 2. [Getting Started](#get-started) 11 | 3. [Middleware and caching](#middleware) 12 | 4. [Authors](#authors) 13 | 5. [License](#license) 14 | 15 | ## Description 16 | 17 | Zoic is an easy-to-use middleware library for caching responses from RESTful API endpoints in Oak, built for the Deno JavaScript runtime. Zoic provides both LRU and LFU in-memory caches, as well as support for Redis caches. Developers can use Zoic to easily cache HTTP responses with one simple middleware function that automatically handles both caching response data in the event of a cache miss, and sending responses on cache hits. 18 | 19 | ### Zoic Developer Tool 20 | 21 | The Zoic Developer Tool allows developers to monitor cache metrics in real time. The easiest to get it is to [add it from the Chrome Web Store.](https://chrome.google.com/webstore/detail/zoic-dev-tools/cnoohkfilnjedjeamhmpokfgaadgkgcl) 22 | Checkout the [Zoic Developer Tool README](./zoic_dev_tool/README.md/) for more details. 23 | 24 | ## Getting Started 25 | 26 | To get started, first make sure you have [Deno](https://deno.land) installed and configured. 27 | 28 | ### Quick Start 29 | 30 | In your application, import the Zoic module from the deno.land [module](https://deno.land/x/zoic). 31 | 32 | ```typescript 33 | import { Zoic } from 'https://deno.land/x/zoic/zoic.ts'; 34 | ``` 35 | 36 | ### Create a cache 37 | 38 | Initialize a new `Zoic` object, passing in your user defined `options` object. If no `options` object is passed, `Zoic` will set all properties to their default values. 39 | 40 | - `cache`: Sets cache eviction policy, options being `'LRU'`, `'LFU'`, and `'Redis'`. *Default value:* `'LRU'` 41 | - `expire`: Sets cache invalidation/expiration time for each entry. This can be set in human readable time, as a comma separated string, denoting hours with value followed by `'h'`, minutes followed by `'m'`, and seconds followed by `'s'`. Alternatively, you may pass in the time as a `number` representing seconds. *Default value:* `Infinity` 42 | - `capacity`: Sets the maximum number of entries. *Default value:* `Infinity` 43 | - `respondOnHit`: Determines if cache hits should be sent as an HTTP response immediately upon retrieval. If this is set to `false`, the cached response data will be attached to Oak `Context.state` property, `context.state.zoicResponse`. It is recommended to leave this set to `true`, unless additional processing on the response data is desired in the event of a cache hit. *Default value:* `true` 44 | 45 | 46 | Example: 47 | 48 | ```typescript 49 | const cache = new Zoic({ 50 | cache: 'LFU', 51 | expire: '5m, 3s', 52 | capacity: 50, 53 | }); 54 | ``` 55 | 56 | ### Redis cache 57 | 58 | To use an instance of Redis as your cache, initialize a new `Zoic` object, passing in `'Redis'` as the `cache` property on your options object. You also must specify the port your instance of Redis is running on, via the `port` property. Optionally, you may pass the hostname as well. This value defaults to `'127.0.0.1'`. 59 |
60 |
61 | NOTE: Options `expire` and `capacity` do not have an effect on `Zoic` if using Redis, as these would be defined in your Redis configuration. 62 |
63 |
64 | Example: 65 | ```typescript 66 | const cache = new Zoic({ 67 | cache: 'redis', 68 | port: 6379 69 | }); 70 | ``` 71 | 72 | 73 | ## Middleware and caching 74 | 75 | ### Zoic.use() 76 | `Zoic.use()` is responsible for both sending cached responses, and storing responses in the cache. When `.use()` is called in a middleware chain, it will check if data exists in the cache at a key representing that route's endpoint. If the query is successful, it will send an HTTP response with the cached body, headers, and status. If the query is unsucessful, `.use()` will automatically listen for when the subsequent middleware in that route has been executed, and will cache the contents of the HTTP response before being sent to the client. This way, the developer only needs to place `.use()` in their middleware chain at the point where they would like the response to be sent in the event of a cache hit, making it extremely easy to use. 77 |
78 |
79 | NOTE: if the user has selected `false` for `respondOnHit` when initializing `Zoic`, the response data will be stored on `ctx.state.zoicResponse` instead of being sent as an HTTP response. 80 |
81 |
82 | Example: 83 | 84 | ```typescript 85 | const cache = new Zoic(); 86 | 87 | router.get('/userInfo/:name', cache.use, controller.dbRead, ctx => { 88 | ctx.response.headers.set('Content-Type', 'application/json'); 89 | ctx.response.body = ctx.state.somethingFromYourDB; 90 | }); 91 | ``` 92 | ### Zoic.put() 93 | `Zoic.put()` will add responses to the cache without first querying to see if an entry already exists. The primary use being to replace data at an already existing keys, or manually add responses without anything being returned. Like with `.use()`, `.put()` will automatically store the response body, headers, and status at the end of a middleware chain before the response is sent. 94 |
95 |
96 | Example: 97 | 98 | ```typescript 99 | const cache = new Zoic(); 100 | 101 | router.put('/userInfo/:name', cache.put, controller.dbWrite, ctx => { 102 | ctx.response.body = ctx.state.someDataYouChanged; 103 | }); 104 | ``` 105 | ### Zoic.clear() 106 | `Zoic.clear()` clears the contents of the cache. 107 |
108 |
109 | Example: 110 | 111 | ```typescript 112 | const cache = new Zoic(); 113 | 114 | // On its own.. 115 | router.post('/userInfo/:name', cache.clear); 116 | 117 | // In conjunction with another function... 118 | router.post('/otherUserInfo/', cache.clear, controller.dbWrite, ctx => { 119 | ctx.response.body = ctx.state.someFreshData; 120 | }); 121 | ``` 122 | ## Authors 123 | 124 | - [Joe Borrow](https://github.com/jmborrow) 125 | - [Celena Chan](https://github.com/celenachan) 126 | - [Aaron Dreyfuss](https://github.com/AaronDreyfuss) 127 | - [Hank Jackson](https://github.com/hankthetank27) 128 | - [Jasper Narvil](https://github.com/jnarvil3) 129 | - [unkn0wn-root](https://github.com/unkn0wn-root) 130 | 131 | ## License 132 | 133 | This product is licensed under the MIT License - see the LICENSE file for details. 134 | 135 | This is an open source product. 136 | -------------------------------------------------------------------------------- /src/tests/lfu_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assert, 4 | describe, 5 | it 6 | } from "../../deps.ts"; 7 | import PerfMetrics from '../performanceMetrics.ts'; 8 | import LFU from '../lfu.ts' 9 | 10 | describe("LFU tests", () => { 11 | const lfu = new LFU(100, new PerfMetrics, 7); 12 | 13 | it("Adds new items to the cache", () => { 14 | lfu.put('item1', {headers:{}, body: new Uint8Array([1]), status:200}, 10); 15 | lfu.put('item2', {headers:{}, body: new Uint8Array([2]), status:200}, 10); 16 | lfu.put('item3', {headers:{}, body: new Uint8Array([3]), status:200}, 10); 17 | lfu.put('item4', {headers:{}, body: new Uint8Array([4]), status:200}, 10); 18 | lfu.put('item5', {headers:{}, body: new Uint8Array([5]), status:200}, 10); 19 | assertEquals(lfu.cache.item1.value.body, new Uint8Array([1])); 20 | assertEquals(lfu.cache.item2.value.body, new Uint8Array([2])); 21 | assertEquals(lfu.cache.item3.value.body, new Uint8Array([3])); 22 | assertEquals(lfu.cache.item4.value.body, new Uint8Array([4])); 23 | assertEquals(lfu.cache.item5.value.body, new Uint8Array([5])); 24 | assertEquals(lfu.freqList.head?.freqValue, 1); 25 | assertEquals(lfu.freqList.tail?.freqValue, 1); 26 | assertEquals(lfu.freqList.head?.valList.head?.key, 'item5'); 27 | assertEquals(lfu.freqList.head?.valList.tail?.key, 'item1'); 28 | assertEquals(lfu.length, 5); 29 | }); 30 | 31 | it("Returns undefined when get is called on a non-existing key", () => { 32 | assertEquals(lfu.get('asdf'), undefined); 33 | }); 34 | 35 | it("Gets items and increases the frequency accessed", () => { 36 | 37 | const item1 = lfu.get('item4'); 38 | assertEquals(item1?.body, new Uint8Array([4])) 39 | assertEquals(lfu.freqList.head?.valList.head?.key, 'item5'); 40 | assertEquals(lfu.freqList.head?.next?.valList.head?.key, 'item4'); 41 | 42 | const item2 = lfu.get('item5'); 43 | assertEquals(item2?.body, new Uint8Array([5])); 44 | assertEquals(lfu.freqList.head?.valList.head?.key, 'item3'); 45 | assertEquals(lfu.freqList.head?.next?.valList.head?.key, 'item5'); 46 | 47 | const item3 = lfu.get('item4'); 48 | assertEquals(item3?.body, new Uint8Array([4])); 49 | assertEquals(lfu.freqList.head?.valList.head?.key, 'item3'); 50 | assertEquals(lfu.freqList.head?.next?.valList.head?.key, 'item5'); 51 | assertEquals(lfu.freqList.head?.next?.next?.valList.head?.key, 'item4'); 52 | 53 | const item4 = lfu.get('item3'); 54 | assertEquals(item4?.body, new Uint8Array([3])); 55 | assertEquals(lfu.freqList.head?.valList.head?.key, 'item2'); 56 | assertEquals(lfu.freqList.head?.next?.valList.head?.key, 'item3'); 57 | assertEquals(lfu.freqList.head?.next?.next?.valList.head?.key, 'item4'); 58 | 59 | const item5 = lfu.get('item4'); 60 | assertEquals(item5?.body, new Uint8Array([4])); 61 | assertEquals(lfu.freqList.head?.valList.head?.key, 'item2'); 62 | assertEquals(lfu.freqList.head?.next?.valList.head?.key, 'item3'); 63 | assertEquals(lfu.freqList.head?.next?.next?.valList.head?.key, 'item4'); 64 | 65 | const item6 = lfu.get('item4'); 66 | assertEquals(item6?.body, new Uint8Array([4])); 67 | assertEquals(lfu.freqList.head?.valList.head?.key, 'item2'); 68 | assertEquals(lfu.freqList.head?.next?.valList.head?.key, 'item3'); 69 | assertEquals(lfu.freqList.head?.next?.next?.valList.head?.key, 'item4'); 70 | 71 | const item7 = lfu.get('item5'); 72 | assertEquals(item7?.body, new Uint8Array([5])); 73 | assertEquals(lfu.freqList.head?.valList.head?.key, 'item2'); 74 | assertEquals(lfu.freqList.head?.next?.valList.head?.key, 'item3'); 75 | assertEquals(lfu.freqList.head?.next?.next?.valList.head?.key, 'item5'); 76 | assertEquals(lfu.freqList.head?.next?.next?.next?.valList.head?.key, 'item4'); 77 | assertEquals(lfu.freqList.head?.freqValue, 1); 78 | assertEquals(lfu.freqList.head?.next?.freqValue, 2); 79 | assertEquals(lfu.freqList.head?.next?.next?.freqValue, 3); 80 | assertEquals(lfu.freqList.head?.next?.next?.next?.freqValue, 5); 81 | 82 | lfu.get('item2'); 83 | lfu.get('item1'); 84 | assertEquals(lfu.freqList.head?.valList.head?.key, 'item1'); 85 | assertEquals(lfu.freqList.head?.next?.valList.head?.key, 'item5'); 86 | assertEquals(lfu.freqList.head?.next?.next?.valList.head?.key, 'item4'); 87 | assertEquals(lfu.freqList.head?.freqValue, 2); 88 | assertEquals(lfu.freqList.head?.next?.freqValue, 3); 89 | assertEquals(lfu.freqList.head?.next?.next?.freqValue, 5); 90 | }); 91 | 92 | it("Should properly put an item as a new freqency at value 1 when the current head is greater than 1", () => { 93 | lfu.put('item6', {headers:{}, body: new Uint8Array([6]), status:200}, 10); 94 | assertEquals(lfu.freqList.head?.valList.head?.key, 'item6'); 95 | assertEquals(lfu.freqList.head?.next?.valList.head?.key, 'item1'); 96 | assertEquals(lfu.freqList.head?.next?.next?.valList.head?.key, 'item5'); 97 | assertEquals(lfu.freqList.head?.next?.next?.next?.valList.head?.key, 'item4'); 98 | assertEquals(lfu.freqList.head?.freqValue, 1); 99 | assertEquals(lfu.freqList.head?.next?.freqValue, 2); 100 | assertEquals(lfu.freqList.head?.next?.next?.freqValue, 3); 101 | assertEquals(lfu.freqList.head?.next?.next?.next?.freqValue, 5); 102 | }); 103 | 104 | it("Deletes the from the least freqently accessed bucket, the least recently used item when over capacity", () => { 105 | lfu.put('item7', {headers:{}, body: new Uint8Array([7]), status:200}, 10); 106 | lfu.put('item8', {headers:{}, body: new Uint8Array([8]), status:200}, 10); 107 | assertEquals(lfu.freqList.head?.valList.head?.key, 'item8'); 108 | assertEquals(lfu.freqList.head?.valList.head?.next?.key, 'item7'); 109 | assertEquals(lfu.freqList.head?.valList.head?.prev, null); 110 | 111 | lfu.get('item7'); 112 | lfu.put('item9', {headers:{}, body: new Uint8Array([9]), status:200}, 10); 113 | assertEquals(lfu.freqList.head?.valList.head?.key, 'item9'); 114 | assertEquals(lfu.freqList.head?.valList.head?.next, null); 115 | assertEquals(lfu.freqList.head?.valList.head?.prev, null); 116 | 117 | lfu.get('item9'); 118 | lfu.put('item10', {headers:{}, body: new Uint8Array([10]), status:200}, 10); 119 | assertEquals(lfu.freqList.head?.valList.head?.key, 'item9'); 120 | assertEquals(lfu.freqList.head?.valList.head?.next?.key, 'item7'); 121 | assertEquals(lfu.freqList.head?.valList.head?.next?.next?.key, 'item1'); 122 | assertEquals(lfu.freqList.head?.valList.head?.prev, null); 123 | }); 124 | 125 | it("Updates an entry when put is called with an existing key", () => { 126 | lfu.put('item4', {headers:{}, body: new Uint8Array([100]), status:200}, 10); 127 | assertEquals(lfu.get('item4')?.body, new Uint8Array([100])); 128 | }); 129 | 130 | it("Expires entry after set time", async () => { 131 | const timeout = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 132 | const shortLru = new LFU(1, new PerfMetrics, 8); 133 | shortLru.put('item1', {headers:{}, body: new Uint8Array([1]), status:200}, 10); 134 | await timeout(3000); 135 | assert(!shortLru.get('item1')); 136 | assert(!shortLru.freqList.head); 137 | shortLru.put('item2', {headers:{}, body: new Uint8Array([2]), status:200}, 10); 138 | await timeout(99); 139 | assert(shortLru.get('item2')); 140 | assert(shortLru.freqList.head); 141 | }); 142 | 143 | 144 | it("Should properly clear cache when clear method is called", () => { 145 | lfu.put('item1', {headers:{}, body: new Uint8Array([1]), status:200}, 10); 146 | lfu.put('item2', {headers:{}, body: new Uint8Array([2]), status:200}, 10); 147 | lfu.put('item3', {headers:{}, body: new Uint8Array([3]), status:200}, 10); 148 | lfu.put('item4', {headers:{}, body: new Uint8Array([4]), status:200}, 10); 149 | lfu.put('item5', {headers:{}, body: new Uint8Array([5]), status:200}, 10); 150 | lfu.clear(); 151 | assertEquals(lfu.freqList.head, null); 152 | assertEquals(lfu.freqList.tail, null); 153 | assertEquals(lfu.cache, {}); 154 | assertEquals(lfu.length, 0); 155 | }); 156 | }) 157 | -------------------------------------------------------------------------------- /src/tests/lru_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assert, 4 | describe, 5 | it 6 | } from "../../deps.ts"; 7 | import PerfMetrics from '../performanceMetrics.ts'; 8 | import LRU from '../lru.ts' 9 | 10 | describe("LRU tests", () => { 11 | const lru = new LRU(50, new PerfMetrics, 6); 12 | 13 | it("Adds new items to the cache", () => { 14 | lru.put('item1', {headers:{}, body: new Uint8Array([1]), status:200}, 10); 15 | lru.put('item2', {headers:{}, body: new Uint8Array([2]), status:200}, 10); 16 | lru.put('item3', {headers:{}, body: new Uint8Array([3]), status:200}, 10); 17 | lru.put('item4', {headers:{}, body: new Uint8Array([4]), status:200}, 10); 18 | lru.put('item5', {headers:{}, body: new Uint8Array([5]), status:200}, 10); 19 | assertEquals(lru.cache.item1.value.body, new Uint8Array([1])); 20 | assertEquals(lru.cache.item2.value.body, new Uint8Array([2])); 21 | assertEquals(lru.cache.item3.value.body, new Uint8Array([3])); 22 | assertEquals(lru.cache.item4.value.body, new Uint8Array([4])); 23 | assertEquals(lru.cache.item5.value.body, new Uint8Array([5])); 24 | assertEquals(lru.list.head?.key, 'item5'); 25 | assertEquals(lru.list.tail?.key, 'item1'); 26 | assertEquals(lru.length, 5); 27 | }); 28 | 29 | it("Returns undefined when get is called on a non-existing key", () => { 30 | assertEquals(lru.get('asdf'), undefined); 31 | }); 32 | 33 | it("Gets items from the cache, and moves them to the head", () => { 34 | const item = lru.get('item3'); 35 | assertEquals(lru.list.head?.value, item); 36 | assertEquals(lru.list.head?.next?.key, 'item5'); 37 | assertEquals(lru.list.head?.next?.next?.key, 'item4'); 38 | assertEquals(lru.list.head?.next?.next?.next?.key, 'item2'); 39 | assertEquals(lru.list.head?.next?.next?.next?.next?.key, 'item1'); 40 | assertEquals(lru.list.head?.next?.next?.next?.next, lru.list.tail); 41 | assertEquals(lru.list.head?.next?.next?.next?.next?.next, null); 42 | assertEquals(lru.cache.item1.value.body, new Uint8Array([1])); 43 | assertEquals(lru.cache.item2.value.body, new Uint8Array([2])); 44 | assertEquals(lru.cache.item3.value.body, new Uint8Array([3])); 45 | assertEquals(lru.cache.item4.value.body, new Uint8Array([4])); 46 | assertEquals(lru.cache.item5.value.body, new Uint8Array([5])); 47 | }); 48 | 49 | it("Deletes items from the front of the cache", () => { 50 | lru.delete('item3'); 51 | assertEquals(lru.cache.item3, undefined); 52 | assertEquals(lru.list.head?.key, 'item5'); 53 | assertEquals(lru.list.head?.next?.key, 'item4'); 54 | assertEquals(lru.list.head?.next?.next?.key, 'item2'); 55 | assertEquals(lru.list.head?.next?.next?.next?.key, 'item1'); 56 | assertEquals(lru.list.head?.next?.next?.next, lru.list.tail) 57 | assertEquals(lru.cache.item1.value.body, new Uint8Array([1])); 58 | assertEquals(lru.cache.item2.value.body, new Uint8Array([2])); 59 | assertEquals(lru.cache.item4.value.body, new Uint8Array([4])); 60 | assertEquals(lru.cache.item5.value.body, new Uint8Array([5])); 61 | }); 62 | 63 | it("Deletes items from the end of the cache", () => { 64 | lru.delete('item1'); 65 | assertEquals(lru.cache.item1, undefined); 66 | assertEquals(lru.list.head?.key, 'item5'); 67 | assertEquals(lru.list.head?.next?.key, 'item4'); 68 | assertEquals(lru.list.head?.next?.next?.key, 'item2'); 69 | assertEquals(lru.list.head?.next?.next, lru.list.tail); 70 | assertEquals(lru.cache.item2.value.body, new Uint8Array([2])); 71 | assertEquals(lru.cache.item4.value.body, new Uint8Array([4])); 72 | assertEquals(lru.cache.item5.value.body, new Uint8Array([5])); 73 | }); 74 | 75 | it("Deletes items from the middle of the cache", () => { 76 | lru.delete('item4'); 77 | assertEquals(lru.cache.item4, undefined); 78 | assertEquals(lru.list.head?.key, 'item5'); 79 | assertEquals(lru.list.head?.next?.key, 'item2'); 80 | assertEquals(lru.list.head?.next, lru.list.tail); 81 | assertEquals(lru.cache.item2.value.body, new Uint8Array([2])); 82 | assertEquals(lru.cache.item5.value.body, new Uint8Array([5])); 83 | }); 84 | 85 | it("Adds an item to the head after deleting items", () => { 86 | lru.put('item666', {headers:{}, body: new Uint8Array([1]), status:200}, 10); 87 | assertEquals(lru.list.head?.key, 'item666'); 88 | assertEquals(lru.list.head?.next?.key, 'item5'); 89 | assertEquals(lru.list.head?.next?.next?.key, 'item2'); 90 | assertEquals(lru.list.head?.next?.next?.next, null); 91 | }) 92 | 93 | it("Deletes the last item when over capacity", () => { 94 | lru.put('item30', {headers:{}, body: new Uint8Array([3]), status:200}, 10); 95 | lru.put('item40', {headers:{}, body: new Uint8Array([4]), status:200}, 10); 96 | lru.put('item50', {headers:{}, body: new Uint8Array([5]), status:200}, 10); 97 | lru.put('item60', {headers:{}, body: new Uint8Array([6]), status:200}, 10); 98 | lru.put('item70', {headers:{}, body: new Uint8Array([7]), status:200}, 10); 99 | lru.put('item80', {headers:{}, body: new Uint8Array([8]), status:200}, 10); 100 | lru.put('item90', {headers:{}, body: new Uint8Array([9]), status:200}, 10); 101 | lru.put('item99', {headers:{}, body: new Uint8Array([1]), status:200}, 10); 102 | assertEquals(lru.cache.item30, undefined); 103 | assertEquals(lru.cache.item50.value.body, new Uint8Array([5])); 104 | assertEquals(lru.cache.item60.value.body, new Uint8Array([6])); 105 | assertEquals(lru.cache.item80.value.body, new Uint8Array([8])); 106 | assertEquals(lru.list.head?.key, 'item99'); 107 | assertEquals(lru.list.head?.next?.key, 'item90'); 108 | assertEquals(lru.list.head?.next?.next?.key, 'item80'); 109 | assertEquals(lru.list.head?.next?.next?.next?.key, 'item70'); 110 | assertEquals(lru.list.head?.next?.next?.next?.next?.key, 'item60'); 111 | assertEquals(lru.list.head?.next?.next?.next?.next?.next?.key, 'item50'); 112 | assertEquals(lru.list.head?.next?.next?.next?.next?.next?.next, null); 113 | assertEquals(lru.list.tail?.prev?.prev?.prev?.prev?.prev?.key, 'item99'); 114 | assertEquals(lru.list.tail?.prev?.prev?.prev?.prev?.key, 'item90'); 115 | assertEquals(lru.list.tail?.prev?.prev?.prev?.key, 'item80'); 116 | assertEquals(lru.list.tail?.prev?.prev?.key, 'item70'); 117 | assertEquals(lru.list.tail?.prev?.key, 'item60'); 118 | assertEquals(lru.list.tail?.key, 'item50'); 119 | assertEquals(lru.cache.item80.value.body, new Uint8Array([8])); 120 | assertEquals(lru.cache.item90.value.body, new Uint8Array([9])); 121 | assertEquals(lru.cache.item99.value.body, new Uint8Array([1])); 122 | assertEquals(lru.cache.item40, undefined); 123 | }); 124 | 125 | it("Updates an entry when put is called with an existing key", () => { 126 | lru.put('item70', {headers:{}, body: new Uint8Array([100]), status:200}, 10); 127 | assertEquals(lru.get('item70')?.body, new Uint8Array([100])); 128 | }) 129 | 130 | it("Expires entry after set time", async () => { 131 | const timeout = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 132 | const shortLru = new LRU(1, new PerfMetrics, 8); 133 | shortLru.put('item1', {headers:{}, body: new Uint8Array([1]), status:200}, 10); 134 | await timeout(1001); 135 | assert(!shortLru.get('item1')); 136 | assert(!shortLru.list.head); 137 | shortLru.put('item2', {headers:{}, body: new Uint8Array([2]), status:200}, 10); 138 | await timeout(99); 139 | assert(shortLru.get('item2')); 140 | assert(shortLru.list.head); 141 | }) 142 | 143 | it("Should properly clear cache when clear method is called", () => { 144 | lru.put('item1', {headers:{}, body: new Uint8Array([1]), status:200}, 10); 145 | lru.put('item2', {headers:{}, body: new Uint8Array([2]), status:200}, 10); 146 | lru.put('item3', {headers:{}, body: new Uint8Array([3]), status:200}, 10); 147 | lru.put('item4', {headers:{}, body: new Uint8Array([4]), status:200}, 10); 148 | lru.put('item5', {headers:{}, body: new Uint8Array([5]), status:200}, 10); 149 | lru.clear(); 150 | assertEquals(lru.list.head, null); 151 | assertEquals(lru.list.tail, null); 152 | assertEquals(lru.cache, {}); 153 | assertEquals(lru.length, 0); 154 | }); 155 | }) 156 | -------------------------------------------------------------------------------- /src/tests/zoic_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | assertThrows, 4 | assertEquals, 5 | assertInstanceOf, 6 | assertRejects, 7 | type Context 8 | } from "../../deps.ts"; 9 | import Zoic from "../../zoic.ts"; 10 | import LRU from "../lru.ts"; 11 | import LFU from "../lfu.ts"; 12 | import { TestServer } from "./test_server.ts"; 13 | 14 | Deno.test("Arguments passed into the performance metrics change", async (t) => { 15 | const lruTestCacheInstance = new Zoic({ 16 | capacity: 10, 17 | expire: "2h, 3s, 5m, 80d", 18 | cache: "LrU", 19 | }); 20 | const lfuTestCacheInstance = new Zoic({ 21 | capacity: 10, 22 | expire: "2h, 3s, 5m, 80d", 23 | cache: "lfu", 24 | }); 25 | 26 | await t.step("Should return an object", () => { 27 | assert(typeof lruTestCacheInstance === "object"); 28 | }); 29 | 30 | await t.step("Should set the right capacity", () => { 31 | assertEquals(lruTestCacheInstance.capacity, 10); 32 | }); 33 | 34 | await t.step("Should parse unordered strings for expiration", () => { 35 | assertEquals(lruTestCacheInstance.expire, 6919503); 36 | }); 37 | 38 | await t.step("Should return a promise", () => { 39 | assertInstanceOf(lruTestCacheInstance.cache, Promise); 40 | }); 41 | 42 | await t.step("Should resolve promise to correct cache type", async () => { 43 | const lruCache = await lruTestCacheInstance.cache; 44 | const lfuCache = await lfuTestCacheInstance.cache; 45 | assertInstanceOf(lruCache, LRU); 46 | assertInstanceOf(lfuCache, LFU); 47 | }); 48 | }); 49 | 50 | Deno.test("Zoic should handle default args appropriately", async (t) => { 51 | const testCacheInstance = new Zoic(); 52 | 53 | await t.step("should handle when nothing input for expiration time", () => { 54 | assertEquals(testCacheInstance.expire, Infinity); 55 | }); 56 | 57 | await t.step("should handle when nothing input for capacity", () => { 58 | assertEquals(testCacheInstance.capacity, Infinity); 59 | }); 60 | 61 | await t.step("should handle when nothing input for cache type", async () => { 62 | const cache = await testCacheInstance.cache; 63 | assert(cache instanceof LRU); 64 | }); 65 | }); 66 | 67 | Deno.test( 68 | "Zoic should handle poorly formatted args appropriately", 69 | async (t) => { 70 | await t.step( 71 | "should handle poorly formatted inputs to expiration time", 72 | () => { 73 | assertThrows( 74 | () => 75 | new Zoic({ 76 | capacity: 10, 77 | expire: "this should not work", 78 | cache: "LRU", 79 | }), 80 | TypeError, 81 | 'Invalid format. Use number followed by d, h, m, or s (e.g., "1d,12h").', 82 | ); 83 | }, 84 | ); 85 | 86 | await t.step( 87 | "should handle poorly formatted inputs to cache type", 88 | async () => { 89 | const testCache = new Zoic({ 90 | capacity: 10, 91 | cache: "LBU", 92 | }); 93 | await assertRejects( 94 | () => testCache.cache, 95 | TypeError, 96 | "Invalid cache type.", 97 | ); 98 | }, 99 | ); 100 | 101 | await t.step("Should handle poorly formatted inputs to capacity", () => { 102 | assertThrows( 103 | () => new Zoic({ capacity: 0 }), 104 | Error, 105 | "Cache capacity must exceed 0 entires.", 106 | ); 107 | }); 108 | 109 | await t.step( 110 | "Should handle poorly formatted inputs to expiration time", 111 | () => { 112 | assertThrows( 113 | () => new Zoic({ expire: 31536001 }), 114 | TypeError, 115 | "Cache expiration must be between 1 second and 1 year.", 116 | ); 117 | assertThrows( 118 | () => new Zoic({ expire: 0 }), 119 | TypeError, 120 | "Cache expiration must be between 1 second and 1 year.", 121 | ); 122 | }, 123 | ); 124 | }, 125 | ); 126 | 127 | Deno.test("Should update in-memory cache appropriately", async (t) => { 128 | const server = new TestServer(); 129 | const router = server.getRouter(); 130 | 131 | await t.step("Caches response body as a Uint8Array", async () => { 132 | const cache = new Zoic({ capacity: 5 }); 133 | const lru = await cache.cache; 134 | 135 | if (cache.redisTypeCheck(lru)) return assert(false); 136 | 137 | router.get("/test", cache.use, (ctx: Context) => { 138 | ctx.response.body = "testing123"; 139 | }); 140 | 141 | const port = await server.start(); 142 | const response = await fetch(`http://localhost:${port}/test`); 143 | assertEquals(response.status, 200); 144 | assertEquals(await response.text(), "testing123"); 145 | 146 | const cacheBody = lru.get("/test")?.body; 147 | assertInstanceOf(cacheBody, Uint8Array); 148 | assertEquals( 149 | new TextDecoder("utf-8").decode(lru.get("/test")?.body), 150 | "testing123", 151 | ); 152 | 153 | server.stop(); 154 | }); 155 | 156 | await t.step("Cache stores and sends response", async () => { 157 | const cache = new Zoic({ capacity: 5 }); 158 | const lru = await cache.cache; 159 | 160 | if (cache.redisTypeCheck(lru)) return assert(false); 161 | 162 | router.get("/test1", cache.use, (ctx: Context) => { 163 | ctx.response.body = "testing123"; 164 | }); 165 | 166 | const port = await server.start(); 167 | const response1 = await fetch(`http://localhost:${port}/test1`); 168 | const response2 = await fetch(`http://localhost:${port}/test1`); 169 | 170 | assertEquals(await response1.text(), "testing123"); 171 | assertEquals(await response2.text(), "testing123"); 172 | 173 | server.stop(); 174 | }); 175 | 176 | await t.step("Stores a new value when the entry is stale", async () => { 177 | const timeout = (ms: number) => 178 | new Promise((resolve) => setTimeout(resolve, ms)); 179 | const cache = new Zoic({ capacity: 5, expire: 1 }); 180 | const lru = await cache.cache; 181 | 182 | router.get("/test3", cache.use, (ctx: Context) => { 183 | ctx.response.body = "testing123"; 184 | }); 185 | 186 | router.post("/test3", cache.use, async (ctx: Context) => { 187 | const body = await ctx.request.body.json(); 188 | ctx.response.body = body; 189 | }); 190 | 191 | const port = await server.start(); 192 | const getResponse = await fetch(`http://localhost:${port}/test3`); 193 | assertEquals(await getResponse.text(), "testing123"); 194 | 195 | await timeout(1001); 196 | assert(!lru.get("/test3")); 197 | 198 | const postResponse = await fetch(`http://localhost:${port}/test3`, { 199 | method: "POST", 200 | headers: { 201 | "Content-Type": "application/json", 202 | }, 203 | body: JSON.stringify({ test: "testingChange" }), 204 | }); 205 | 206 | assertEquals(await postResponse.json(), { test: "testingChange" }); 207 | 208 | server.stop(); 209 | }); 210 | 211 | await t.step("Should get metrics", async () => { 212 | const cache = new Zoic({ capacity: 5, expire: 1 }); 213 | router.get("/testMetrics", cache.getMetrics); 214 | 215 | const port = await server.start(); 216 | const response = await fetch(`http://localhost:${port}/testMetrics`); 217 | assertEquals(await response.json(), { 218 | cache_type: "LRU", 219 | memory_used: 0, 220 | number_of_entries: 0, 221 | reads_processed: 0, 222 | writes_processed: 0, 223 | average_hit_latency: null, 224 | average_miss_latency: null, 225 | }); 226 | 227 | server.stop(); 228 | }); 229 | 230 | await t.step("Should clear cache", async () => { 231 | const cache = new Zoic({ capacity: 5 }); 232 | const lru = await cache.cache; 233 | 234 | if (cache.redisTypeCheck(lru)) return assert(false); 235 | 236 | router.get("/test20", cache.use, (ctx: Context) => { 237 | ctx.response.body = "testing123"; 238 | }); 239 | 240 | router.get("/test21", cache.clear, (ctx: Context) => { 241 | ctx.response.body = "testing400"; 242 | }); 243 | 244 | const port = await server.start(); 245 | const response1 = await fetch(`http://localhost:${port}/test20`); 246 | assertEquals(lru.length, 1); 247 | assertEquals(await response1.text(), "testing123"); 248 | 249 | const response2 = await fetch(`http://localhost:${port}/test21`); 250 | assertEquals(await response2.text(), "testing400"); 251 | assertEquals(lru.length, 0); 252 | 253 | server.stop(); 254 | }); 255 | 256 | await t.step("Should not respond if respondOnHit is false", async () => { 257 | const cache = new Zoic({ capacity: 5, respondOnHit: false }); 258 | const lru = await cache.cache; 259 | 260 | if (cache.redisTypeCheck(lru)) return assert(false); 261 | 262 | router.get("/test100", cache.use, (ctx: Context) => { 263 | ctx.response.body = String(Number(ctx.state.zoicResponse?.body) + 1 || 1); 264 | }); 265 | 266 | const port = await server.start(); 267 | const response1 = await fetch(`http://localhost:${port}/test100`); 268 | const response2 = await fetch(`http://localhost:${port}/test100`); 269 | 270 | assertEquals(await response1.text(), "1"); 271 | assertEquals(await response2.text(), "2"); 272 | 273 | server.stop(); 274 | }); 275 | 276 | await t.step("Put method modifies existing entry", async () => { 277 | const cache = new Zoic({ capacity: 5 }); 278 | const lru = await cache.cache; 279 | 280 | if (cache.redisTypeCheck(lru)) return assert(false); 281 | 282 | router.get("/test69", cache.use, (ctx: Context) => { 283 | ctx.response.body = "testing123"; 284 | }); 285 | 286 | router.post("/test69", cache.put, (ctx: Context) => { 287 | ctx.response.body = "modTest"; 288 | }); 289 | 290 | const port = await server.start(); 291 | const response1 = await fetch(`http://localhost:${port}/test69`); 292 | assertEquals(await response1.text(), "testing123"); 293 | 294 | const response2 = await fetch(`http://localhost:${port}/test69`, { 295 | method: "POST", 296 | }); 297 | assertEquals(await response2.text(), "modTest"); 298 | 299 | const response3 = await fetch(`http://localhost:${port}/test69`); 300 | assertEquals(await response3.text(), "modTest"); 301 | 302 | server.stop(); 303 | }); 304 | }); 305 | -------------------------------------------------------------------------------- /src/tests/doublyLinkedList_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertExists, 4 | describe, 5 | it 6 | } from "../../deps.ts"; 7 | import { ValueDoublyLinkedList, FreqDoublyLinkedList } from '../doublyLinkedLists.ts'; 8 | 9 | describe("ValDoublyLinkedList tests", () => { 10 | 11 | const list = new ValueDoublyLinkedList(); 12 | const time = new Date(); 13 | 14 | it("Should properly handle an empty list", () => { 15 | assertEquals(list.head, null); 16 | assertEquals(list.tail, null); 17 | }); 18 | 19 | it("Should properly add a sinlge node", () => { 20 | list.addHead('C', {headers: {}, body: new Uint8Array([1]), status: 200}, 50, time); 21 | assertEquals(list.head, list.tail); 22 | assertEquals(list.head?.next, null); 23 | assertEquals(list.head?.prev, null); 24 | assertEquals(list.tail?.next, null); 25 | assertEquals(list.tail?.prev, null); 26 | }); 27 | 28 | it("Should properly add nodes to the start of the linked list", () => { 29 | list.addHead('B', {headers: {}, body: new Uint8Array([2]), status: 200}, 100, time); 30 | list.addHead('A', {headers: {}, body: new Uint8Array([3]), status: 200}, 200, time); 31 | assertEquals(list.tail?.key, 'C'); 32 | assertEquals(list.head?.key, 'A'); 33 | assertEquals(list.head?.next?.key, 'B'); 34 | assertEquals(list.head?.next?.next?.key, 'C'); 35 | assertEquals(list.head?.next?.next?.next, null); 36 | assertEquals(list.head?.next?.next, list.tail); 37 | assertEquals(list.tail?.next, null); 38 | }); 39 | 40 | it("Should store properties corretly on nodes", () => { 41 | assertEquals(list.head?.value.body, new Uint8Array([3])); 42 | assertEquals(list.head?.next?.value.body, new Uint8Array([2])); 43 | assertEquals(list.head?.next?.next?.value.body, new Uint8Array([1])); 44 | assertEquals(list.head?.value.status, 200); 45 | assertEquals(list.head?.next?.value.status, 200); 46 | assertEquals(list.head?.next?.next?.value.status, 200); 47 | assertEquals(list.head?.byteSize, 200); 48 | assertEquals(list.head?.next?.byteSize, 100); 49 | assertEquals(list.head?.next?.next?.byteSize, 50); 50 | }); 51 | 52 | it("Should properly delete nodes with delete method", () => { 53 | const list2 = new ValueDoublyLinkedList(); 54 | const time2 = new Date(); 55 | 56 | const node1 = list2.addHead('A', {headers: {}, body: new Uint8Array([1]), status: 200}, 200, time2); 57 | const node2 = list2.addHead('B', {headers: {}, body: new Uint8Array([2]), status: 200}, 200, time2); 58 | const node3 = list2.addHead('C', {headers: {}, body: new Uint8Array([3]), status: 200}, 200, time2); 59 | const node4 = list2.addHead('D', {headers: {}, body: new Uint8Array([4]), status: 200}, 200, time2); 60 | 61 | assertEquals(list2.head, node4); 62 | assertEquals(list2.tail, node1); 63 | 64 | list2.delete(node2); 65 | 66 | assertEquals(list2.head, node4); 67 | assertEquals(list2.head?.next, node3); 68 | assertEquals(list2.head?.next?.next, node1); 69 | assertEquals(list2.head?.next?.next?.next, null); 70 | 71 | assertEquals(list2.tail, node1); 72 | assertEquals(list2.tail?.prev, node3); 73 | assertEquals(list2.tail?.prev?.prev, node4); 74 | assertEquals(list2.tail?.prev?.prev?.prev, null); 75 | 76 | list2.delete(node4); 77 | 78 | assertEquals(list2.head, node3); 79 | assertEquals(list2.head?.next, node1); 80 | assertEquals(list2.head?.next?.next, null); 81 | 82 | assertEquals(list2.tail, node1); 83 | assertEquals(list2.tail?.prev, node3); 84 | assertEquals(list2.tail?.prev?.prev, null); 85 | 86 | list2.delete(node1); 87 | 88 | assertEquals(list2.head, node3); 89 | assertEquals(list2.tail, node3); 90 | assertEquals(list2.head, list2.tail); 91 | 92 | list2.delete(node3); 93 | 94 | assertEquals(list2.head, null); 95 | assertEquals(list2.tail, null); 96 | }) 97 | 98 | it("Should properly delete nodes from the tail of the linked list with nodes remaining", () => { 99 | list.deleteTail(); 100 | assertEquals(list.tail?.key, 'B'); 101 | assertEquals(list.head?.key, 'A'); 102 | }); 103 | 104 | it("Should properly delete all nodes from the list.", () => { 105 | list.deleteTail(); 106 | list.deleteTail(); 107 | assertEquals(list.tail, null); 108 | assertEquals(list.head, null); 109 | }); 110 | }); 111 | 112 | 113 | 114 | describe('FreqDoublyLinkedList tests', () => { 115 | 116 | const freqList = new FreqDoublyLinkedList() 117 | const time = new Date(); 118 | 119 | it("Should properly handle an empty list", () => { 120 | assertEquals(freqList.head, null); 121 | assertEquals(freqList.tail, null); 122 | }); 123 | 124 | it("Should properly add a sinlge node", () => { 125 | freqList.addNewFreq('C', {headers: {}, body: new Uint8Array([1]), status: 200}, 50, time); 126 | assertEquals(freqList.head, freqList.tail); 127 | assertEquals(freqList.head?.next, null); 128 | assertEquals(freqList.head?.prev, null); 129 | assertEquals(freqList.tail?.next, null); 130 | assertEquals(freqList.tail?.prev, null); 131 | }); 132 | 133 | it("Should properly add nodes to the start of the linked list", () => { 134 | freqList.addNewFreq('B', {headers: {}, body: new Uint8Array([2]), status: 200}, 100, time); 135 | freqList.addNewFreq('A', {headers: {}, body: new Uint8Array([3]), status: 200}, 200, time); 136 | assertEquals(freqList.tail?.freqValue, 1); 137 | assertEquals(freqList.head?.freqValue, 1); 138 | assertEquals(freqList.head?.next, null); 139 | assertEquals(freqList.tail?.next, null); 140 | assertEquals(freqList.head, freqList.tail); 141 | assertEquals(freqList.head?.valList.head?.key, 'A'); 142 | assertEquals(freqList.head?.valList.head?.next?.key, 'B'); 143 | assertEquals(freqList.head?.valList.head?.next?.next?.key, 'C'); 144 | }); 145 | 146 | it("Should properly update list when node frequency value is increased", () => { 147 | const headNode1 = freqList.head?.valList.head; 148 | 149 | assertExists(headNode1); 150 | const newNode = freqList.increaseFreq(headNode1); 151 | assertEquals(freqList.head?.freqValue, 1); 152 | assertEquals(freqList.head?.next?.freqValue, 2); 153 | assertEquals(freqList.head?.next?.next, null); 154 | assertEquals(freqList.head?.valList.head?.key, 'B') 155 | assertEquals(freqList.head?.valList.head?.next?.key, 'C') 156 | assertEquals(freqList.head?.next?.valList.head?.key, 'A') 157 | 158 | assertExists(newNode); 159 | freqList.increaseFreq(newNode); 160 | assertEquals(freqList.head?.freqValue, 1); 161 | assertEquals(freqList.head?.next?.freqValue, 3); 162 | assertEquals(freqList.head?.next?.next, null); 163 | assertEquals(freqList.head?.valList.head?.key, 'B') 164 | assertEquals(freqList.head?.valList.head?.next?.key, 'C') 165 | assertEquals(freqList.head?.next?.valList.head?.key, 'A') 166 | 167 | const headNode2 = freqList.head?.valList.head; 168 | assertExists(headNode2); 169 | freqList.increaseFreq(headNode2); 170 | assertEquals(freqList.head?.freqValue, 1); 171 | assertEquals(freqList.head?.next?.freqValue, 2); 172 | assertEquals(freqList.head?.next?.next?.freqValue, 3); 173 | assertEquals(freqList.head?.next?.next?.next, null); 174 | assertEquals(freqList.head?.valList.head?.key, 'C') 175 | assertEquals(freqList.head?.next?.valList.head?.key, 'B') 176 | assertEquals(freqList.head?.next?.next?.valList.head?.key, 'A') 177 | 178 | const headNode3 = freqList.head?.valList.head; 179 | assertExists(headNode3) 180 | freqList.increaseFreq(headNode3); 181 | assertEquals(freqList.head?.freqValue, 2); 182 | assertEquals(freqList.head?.next?.freqValue, 3); 183 | assertEquals(freqList.head?.valList.head?.key, 'C') 184 | assertEquals(freqList.head?.valList.head?.next?.key, 'B') 185 | assertEquals(freqList.head?.next?.valList.head?.key, 'A') 186 | 187 | const headNode4 = freqList.head?.valList.head; 188 | assertExists(headNode4) 189 | freqList.increaseFreq(headNode4); 190 | const headNode5 = freqList.head?.valList.head; 191 | assertExists(headNode5) 192 | freqList.increaseFreq(headNode5); 193 | assertEquals(freqList.head?.freqValue, 3); 194 | assertEquals(freqList.head?.next, null); 195 | assertEquals(freqList.head, freqList.tail); 196 | assertEquals(freqList.head?.valList.head?.key, 'B') 197 | assertEquals(freqList.head?.valList.head?.next?.key, 'C') 198 | assertEquals(freqList.head?.valList.head?.next?.next?.key, 'A') 199 | }); 200 | 201 | it("Should properly delete nodes with delete method", () => { 202 | const freqList2 = new FreqDoublyLinkedList(); 203 | const time2 = new Date(); 204 | 205 | const node1 = freqList2.addNewFreq('A', {headers: {}, body: new Uint8Array([1]), status: 200}, 200, time2); 206 | const node2 = freqList2.addNewFreq('B', {headers: {}, body: new Uint8Array([2]), status: 200}, 200, time2); 207 | const node3 = freqList2.addNewFreq('C', {headers: {}, body: new Uint8Array([3]), status: 200}, 200, time2); 208 | const node4 = freqList2.addNewFreq('D', {headers: {}, body: new Uint8Array([4]), status: 200}, 200, time2); 209 | 210 | const freqNode1 = node1.parent; 211 | 212 | const freqNode2 = freqList2.increaseFreq(node2)?.parent; 213 | 214 | const tempNode3_1 = freqList2.increaseFreq(node3) 215 | assertExists(tempNode3_1); 216 | const freqNode3 = freqList2.increaseFreq(tempNode3_1)?.parent; 217 | 218 | const tempNode4_1 = freqList2.increaseFreq(node4); 219 | assertExists(tempNode4_1) 220 | const tempNode4_2 = freqList2.increaseFreq(tempNode4_1); 221 | assertExists(tempNode4_2); 222 | const freqNode4 = freqList2.increaseFreq(tempNode4_2)?.parent; 223 | 224 | assertExists(freqNode1); 225 | assertExists(freqNode2); 226 | assertExists(freqNode3); 227 | assertExists(freqNode4); 228 | 229 | assertEquals(freqList2.head, freqNode1); 230 | assertEquals(freqList2.tail, freqNode4); 231 | 232 | freqList2.delete(freqNode2); 233 | 234 | assertEquals(freqList2.head, freqNode1); 235 | assertEquals(freqList2.head?.next, freqNode3); 236 | assertEquals(freqList2.head?.next?.next, freqNode4); 237 | assertEquals(freqList2.head?.next?.next?.next, null); 238 | 239 | assertEquals(freqList2.tail, freqNode4); 240 | assertEquals(freqList2.tail?.prev, freqNode3); 241 | assertEquals(freqList2.tail?.prev?.prev, freqNode1); 242 | assertEquals(freqList2.tail?.prev?.prev?.prev, null); 243 | 244 | freqList2.delete(freqNode4); 245 | 246 | assertEquals(freqList2.head, freqNode1); 247 | assertEquals(freqList2.head?.next, freqNode3); 248 | assertEquals(freqList2.head?.next?.next, null); 249 | 250 | assertEquals(freqList2.tail, freqNode3); 251 | assertEquals(freqList2.tail?.prev, freqNode1); 252 | assertEquals(freqList2.tail?.prev?.prev, null); 253 | 254 | freqList2.delete(freqNode1); 255 | 256 | assertEquals(freqList2.head, freqNode3); 257 | assertEquals(freqList2.tail, freqNode3); 258 | assertEquals(freqList2.head, freqList2.tail); 259 | 260 | freqList2.delete(freqNode3); 261 | 262 | assertEquals(freqList2.head, null); 263 | assertEquals(freqList2.tail, null); 264 | }); 265 | 266 | 267 | it("Should properly delete value nodes from sublist, and delete freq nodes when value sublist is empty", () => { 268 | freqList.addNewFreq('D', {headers: {}, body: new Uint8Array([1]), status: 200}, 50, time); 269 | freqList.addNewFreq('E', {headers: {}, body: new Uint8Array([1]), status: 200}, 50, time); 270 | 271 | const node = freqList.head?.valList.head; 272 | assertExists(node) 273 | const deletedNode = freqList.deleteValNode(node); 274 | assertEquals(deletedNode?.key, 'E'); 275 | assertEquals(freqList.head?.valList.head?.key, 'D'); 276 | assertEquals(freqList.head?.next?.valList.head?.key, 'B'); 277 | assertEquals(freqList.head?.next?.next, null); 278 | assertEquals(freqList.head?.prev, null); 279 | assertEquals(freqList.tail?.valList.head?.key, 'B'); 280 | assertEquals(freqList.tail?.prev?.valList.head?.key, 'D'); 281 | assertEquals(freqList.tail?.prev?.prev, null); 282 | assertEquals(freqList.tail?.next, null); 283 | 284 | const deletedTail0 = freqList.deleteLeastFreq(); 285 | assertEquals(deletedTail0?.key, 'D'); 286 | assertEquals(freqList.head?.valList.head?.key, 'B'); 287 | assertEquals(freqList.tail?.valList.head?.key, 'B'); 288 | assertEquals(freqList.head?.next, null) 289 | assertEquals(freqList.head?.prev, null) 290 | assertEquals(freqList.tail?.next, null) 291 | assertEquals(freqList.tail?.prev, null) 292 | 293 | const deletedTail1 = freqList.deleteLeastFreq(); 294 | assertEquals(deletedTail1?.key, 'A'); 295 | assertEquals(freqList.head?.valList.head?.key, 'B'); 296 | assertEquals(freqList.tail?.valList.head?.key, 'B'); 297 | assertEquals(freqList.head?.next, null) 298 | assertEquals(freqList.head?.prev, null) 299 | assertEquals(freqList.tail?.next, null) 300 | assertEquals(freqList.tail?.prev, null) 301 | 302 | const deletedTail2 = freqList.deleteLeastFreq(); 303 | assertEquals(deletedTail2?.key, 'C'); 304 | assertEquals(freqList.head?.valList.head?.key, 'B'); 305 | assertEquals(freqList.tail?.valList.head?.key, 'B'); 306 | assertEquals(freqList.head?.next, null) 307 | assertEquals(freqList.head?.prev, null) 308 | assertEquals(freqList.tail?.next, null) 309 | assertEquals(freqList.tail?.prev, null) 310 | 311 | const deletedTail3 = freqList.deleteLeastFreq(); 312 | assertEquals(deletedTail3?.key, 'B'); 313 | assertEquals(freqList.head, null); 314 | assertEquals(freqList.tail, null); 315 | assertEquals(freqList.head?.valList.head, undefined); 316 | 317 | const deletedTail4 = freqList.deleteLeastFreq(); 318 | assertEquals(deletedTail4?.key, undefined); 319 | }); 320 | }); 321 | -------------------------------------------------------------------------------- /zoic.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Context, 3 | base64decode, 4 | base64encode, 5 | connect, 6 | oakCors 7 | } from './deps.ts'; 8 | import type { Redis } from "./deps.ts"; 9 | import type { Options, CacheValue } from './src/types.ts' 10 | import PerfMetrics from './src/performanceMetrics.ts' 11 | import LRU from './src/lru.ts' 12 | import LFU from './src/lfu.ts' 13 | 14 | /** 15 | * Class to initalize new instance of cache. 16 | * Takes options to define if cache eviction policy, expiration time for cache itmes, and if response should be returned on cache hit. 17 | * 18 | * ### Example 19 | * 20 | * ```ts 21 | * 22 | * import { Zoic } from "https://deno.land/x/zoic" 23 | * 24 | * const cache = new Zoic({ 25 | * cache: 'LRU', 26 | * expire: '2h, 5m, 3s', 27 | * capacity: 200 28 | * }); 29 | * 30 | * router.get('/dbRead', cache.use, controller.dbRead, ctx => { 31 | * ctx.response.body = ctx.state.somethingFromDb;}); 32 | * 33 | * ``` 34 | * 35 | * ### Wtih Redis 36 | * Note: with Reids options "expire" and "capacity" do not apply. 37 | * ```ts 38 | * 39 | * const cache = new Zoic({ 40 | * cache:'Redis', 41 | * port: 6379 42 | * }) 43 | * 44 | * ``` 45 | * 46 | * @param option (cache options) 47 | * @returns LRU | Redis (new cache) 48 | */ 49 | export class Zoic { 50 | capacity: number; 51 | expire: number; 52 | metrics: PerfMetrics; 53 | respondOnHit: boolean; 54 | cache: Promise < LRU | LFU | Redis >; 55 | 56 | constructor (options?: Options) { 57 | if (options?.capacity !== undefined && options.capacity <= 0){ 58 | throw new Error('Cache capacity must exceed 0 entires.'); 59 | } 60 | this.capacity = options?.capacity || Infinity; 61 | this.expire = this.parseExpTime(options?.expire); 62 | this.metrics = new PerfMetrics(); 63 | this.respondOnHit = this.setRespondOnHit(options); 64 | this.cache = this.initCacheType( 65 | this.expire, 66 | this.metrics, 67 | options?.cache?.toUpperCase(), 68 | options?.port, 69 | options?.hostname 70 | ); 71 | 72 | this.use = this.use.bind(this); 73 | this.put = this.put.bind(this); 74 | this.clear = this.clear.bind(this); 75 | this.getMetrics = this.getMetrics.bind(this); 76 | this.endPerformanceMark = this.endPerformanceMark.bind(this); 77 | } 78 | 79 | /** 80 | * Sets cache eviction policty. Defaults to LRU. 81 | * @param expire 82 | * @param cache 83 | * @returns LRU | Redis 84 | */ 85 | private async initCacheType(expire: number, metrics: PerfMetrics, cache?: string, redisPort?: number, hostname?: string) { 86 | // The client will enter the specific cache function they want as a string, which is passed as an arg here. 87 | if (!cache || cache === 'LRU'){ 88 | this.metrics.cacheType = 'LRU'; 89 | return new LRU(expire, metrics, this.capacity); 90 | } else if (cache === 'LFU'){ 91 | this.metrics.cacheType = 'LFU'; 92 | return new LFU(expire, metrics, this.capacity); 93 | } else if (cache === 'REDIS'){ 94 | if (!redisPort) { 95 | throw new Error('Redis requires port number passed in as an options property.'); 96 | } 97 | const redis = await connect({ 98 | hostname: hostname || '127.0.0.1', 99 | port: redisPort 100 | }); 101 | this.metrics.cacheType = 'Redis'; 102 | return redis; 103 | } 104 | throw new TypeError('Invalid cache type.'); 105 | } 106 | 107 | /** 108 | * Parses expire option into time in seconds. 109 | * @param numberString 110 | * @returns number 111 | */ 112 | private parseExpTime(input?: string | number): number { 113 | if (input === undefined) return Infinity; 114 | 115 | if (typeof input === 'number') { 116 | return this.validateSeconds(input); 117 | } 118 | 119 | const timeMap: Record = { 120 | 'd': 86400, 121 | 'h': 3600, 122 | 'm': 60, 123 | 's': 1 124 | }; 125 | 126 | const seconds = input 127 | .trim() 128 | .split(',') 129 | .reduce((total, part) => { 130 | const unit = part.slice(-1); 131 | const value = parseInt(part.slice(0, -1)); 132 | 133 | if (isNaN(value) || !(unit in timeMap)) { 134 | throw new TypeError( 135 | 'Invalid format. Use number followed by d, h, m, or s (e.g., "1d,12h").' 136 | ); 137 | } 138 | 139 | return total + value * timeMap[unit]; 140 | }, 0); 141 | 142 | return this.validateSeconds(seconds); 143 | } 144 | 145 | private validateSeconds(seconds: number): number { 146 | if (seconds <= 0 || seconds > 31536000) { 147 | throw new TypeError('Cache expiration must be between 1 second and 1 year.'); 148 | } 149 | return seconds; 150 | } 151 | 152 | /** 153 | * Sets respond on hit value, defaults to true. 154 | * @param options 155 | * @returns 156 | */ 157 | private setRespondOnHit(options?: Options) { 158 | if (options?.respondOnHit === undefined) return true; 159 | return options.respondOnHit; 160 | } 161 | 162 | /** 163 | * typecheck for Redis cache 164 | * @param cache 165 | * @returns 166 | */ 167 | public redisTypeCheck(cache: LRU | LFU | Redis): cache is Redis { 168 | return (cache as Redis).isConnected !== undefined; 169 | } 170 | 171 | /** 172 | * Marks end of latency test for cache hit or miss, and updates read or write processed 173 | * @param queryRes 174 | */ 175 | public endPerformanceMark(queryRes: 'hit' | 'miss') { 176 | performance.mark('endingMark'); 177 | this.metrics.updateLatency( 178 | performance.measure('latency_timer', 'startingMark', 'endingMark').duration, 179 | queryRes 180 | ); 181 | queryRes === 'hit' 182 | ? this.metrics.readProcessed() 183 | : this.metrics.writeProcessed(); 184 | } 185 | 186 | /** 187 | * Primary caching middleware method on user end. 188 | * Resposible for querying cache and either returning results to client/attaching results to ctx.state.zoic (depending on user options) 189 | * or, in the case of a miss, signalling to make response cachable. 190 | * @param ctx 191 | * @param next 192 | * @returns Promise | void 193 | */ 194 | public async use(ctx: Context, next: () => Promise): Promise { 195 | try { 196 | const cache = await this.cache; 197 | 198 | //starting mark for cache hit/miss latency performance test. 199 | performance.mark('startingMark'); 200 | 201 | //defines key via api endpoint 202 | const key: string = ctx.request.url.pathname + ctx.request.url.search; 203 | //query cache 204 | const cacheQueryResults = await cache.get(key); 205 | if (!cacheQueryResults) { 206 | // If declared cache size is less than current cache size, we increment the count of entries. 207 | if (this.metrics.numberOfEntries < this.capacity) this.metrics.addEntry(); 208 | 209 | //makes response cacheable via patch 210 | this.cacheResponse(ctx); 211 | return next(); 212 | } 213 | 214 | //if cache is Redis parse base64string, decoding body header and status 215 | if (this.redisTypeCheck(cache) && typeof cacheQueryResults === 'string') { 216 | const parsedResults = cacheQueryResults.split('\n'); 217 | const { headers, status } = JSON.parse(atob(parsedResults[0])); 218 | const body = base64decode(parsedResults[1]); 219 | if (this.respondOnHit) { 220 | ctx.response.body = body; 221 | ctx.response.status = status; 222 | Object.keys(headers).forEach(key => { 223 | ctx.response.headers.set(key, headers[key]); 224 | }); 225 | this.endPerformanceMark('hit'); 226 | return; 227 | } 228 | //attach query results to ctx.state.zoic if not respondOnHit 229 | ctx.state.zoicResponse = { 230 | body: body, 231 | headers: headers, 232 | status: status 233 | }; 234 | 235 | this.endPerformanceMark('hit'); 236 | return next(); 237 | } 238 | 239 | //if in-memory cache... 240 | if (!this.redisTypeCheck(cache) && typeof cacheQueryResults !== 'string'){ 241 | const { body, headers, status } = cacheQueryResults; 242 | if (this.respondOnHit) { 243 | ctx.response.body = body; 244 | ctx.response.status = status; 245 | Object.keys(headers).forEach(key => { 246 | ctx.response.headers.set(key, headers[key]); 247 | }); 248 | this.endPerformanceMark('hit'); 249 | return; 250 | } 251 | 252 | ///attach query results to ctx.state.zoic if not respondOnHit 253 | ctx.state.zoicResponse = { 254 | body: JSON.parse(new TextDecoder().decode(body)), 255 | headers: headers, 256 | status: status 257 | }; 258 | this.endPerformanceMark('hit'); 259 | return next(); 260 | } 261 | throw new Error('Cache query failed'); 262 | } catch (err) { 263 | ctx.response.status = 400; 264 | ctx.response.body = 'Error in Zoic.use. Check server logs for details.'; 265 | console.log(`Error in Zoic.use: ${err}`); 266 | } 267 | } 268 | 269 | /** 270 | * Makes response store to cache at the end of middleware chain in the case of a cache miss. 271 | * This is done by patching 'toDomResponse' to send results to cache before returning to client. 272 | * @param ctx 273 | * @returns void 274 | */ 275 | private async cacheResponse(ctx: Context) { 276 | try { 277 | const cache = await this.cache; 278 | const redisTypeCheck = this.redisTypeCheck; 279 | const endPerformanceMark = this.endPerformanceMark; 280 | const toDomResponsePrePatch = ctx.response.toDomResponse; 281 | 282 | //patch toDomResponse to cache response body before returning results to client 283 | ctx.response.toDomResponse = async function() { 284 | //defines key via api endpoint and adds response body to cache 285 | const key: string = ctx.request.url.pathname + ctx.request.url.search; 286 | 287 | //extract native http response from toDomResponse to get correct headers and readable body 288 | const nativeResponse = await toDomResponsePrePatch.apply(this); 289 | 290 | //redis cache stores body as a base64 string encoded from a buffer 291 | if (redisTypeCheck(cache)) { 292 | //make response body string, and then stringify response object for storage in redis 293 | const body = await nativeResponse.clone().arrayBuffer(); 294 | const headerAndStatus = { 295 | headers: Object.fromEntries(nativeResponse.headers.entries()), 296 | status: nativeResponse.status 297 | }; 298 | cache.set( 299 | key, 300 | `${ btoa(JSON.stringify(headerAndStatus)) }\n${ base64encode(new Uint8Array(body)) }` 301 | ); 302 | } 303 | 304 | //if in-memory store as native js... 305 | if (!redisTypeCheck(cache)) { 306 | //make response body unit8array and read size for metrics 307 | const arrBuffer = await nativeResponse.clone().arrayBuffer(); 308 | const responseToCache: CacheValue = { 309 | body: new Uint8Array(arrBuffer), 310 | headers: Object.fromEntries(nativeResponse.headers.entries()), 311 | status: nativeResponse.status 312 | }; 313 | 314 | //count bytes for perf metrics 315 | const headerBytes = Object.entries(responseToCache.headers) 316 | .reduce((acc: number, headerArr: Array) => { 317 | return acc += (headerArr[0].length * 2) + (headerArr[1].length * 2); 318 | }, 0); 319 | 320 | //34 represents size of obj keys + status code. 321 | const resByteLength = (key.length * 2) + responseToCache.body.byteLength + headerBytes + 34; 322 | cache.put(key, responseToCache, resByteLength); 323 | } 324 | 325 | //ending mark for a cache miss latency performance test. 326 | endPerformanceMark('miss'); 327 | 328 | return nativeResponse; 329 | } 330 | 331 | return; 332 | } catch (err) { 333 | ctx.response.status = 400; 334 | ctx.response.body = 'Error in Zoic.#cacheResponse. Check server logs for details.'; 335 | console.log(`Error in Zoic.#cacheResponse: ${err}`); 336 | } 337 | } 338 | 339 | /** 340 | * Manually clears all current cache entries. 341 | */ 342 | public async clear(ctx: Context, next: () => Promise): Promise { 343 | try { 344 | const cache = await this.cache; 345 | this.redisTypeCheck(cache) 346 | ? cache.flushdb() 347 | : cache.clear(); 348 | 349 | this.metrics.clearEntires(); 350 | return next(); 351 | } catch (err) { 352 | ctx.response.status = 400; 353 | ctx.response.body = 'Error in Zoic.clear. Check server logs for details.'; 354 | console.log(`Error in Zoic.clear: ${err}`); 355 | } 356 | } 357 | 358 | /** 359 | * Retrives cache metrics. Designed for use with Chrome extension. 360 | * @param ctx 361 | */ 362 | public async getMetrics(ctx: Context): Promise { 363 | try { 364 | //wrap functionality of sending metrics inside of oakCors to enable route specific cors by passing in as 'next'. 365 | const enableRouteCors = oakCors(); 366 | return await enableRouteCors(ctx, async () => { 367 | const cache = await this.cache; 368 | const { 369 | cacheType, 370 | memoryUsed, 371 | numberOfEntries, 372 | readsProcessed, 373 | writesProcessed, 374 | missLatencyTotal, 375 | hitLatencyTotal 376 | } = this.metrics; 377 | 378 | ctx.response.headers.set('Access-Control-Allow-Origin', '*'); 379 | //fetch stats from redis client if needed. 380 | if (this.redisTypeCheck(cache)) { 381 | const redisInfo = await cache.info(); 382 | const redisSize = await cache.dbsize(); 383 | const infoArr: string[] = redisInfo.split('\r\n'); 384 | 385 | ctx.response.body = { 386 | cache_type: cacheType, 387 | number_of_entries: redisSize, 388 | memory_used: infoArr?.find((line: string) => line.match(/used_memory/))?.split(':')[1], 389 | reads_processed: infoArr?.find((line: string) => line.match(/keyspace_hits/))?.split(':')[1], 390 | writes_processed: infoArr?.find((line: string) => line.match(/keyspace_misses/))?.split(':')[1], 391 | average_hit_latency: hitLatencyTotal / readsProcessed, 392 | average_miss_latency: missLatencyTotal / writesProcessed 393 | } 394 | return; 395 | } 396 | 397 | //set in-memory stats 398 | ctx.response.body = { 399 | cache_type: cacheType, 400 | memory_used: memoryUsed, 401 | number_of_entries: numberOfEntries, 402 | reads_processed: readsProcessed, 403 | writes_processed: writesProcessed, 404 | average_hit_latency: hitLatencyTotal / readsProcessed, 405 | average_miss_latency: missLatencyTotal / writesProcessed 406 | } 407 | }) 408 | } catch (err) { 409 | ctx.response.status = 400; 410 | ctx.response.body = 'Error in Zoic.getMetrics. Check server logs for details.'; 411 | console.log(`Error in Zoic.getMetrics: ${err}`); 412 | } 413 | } 414 | 415 | /** 416 | * Manually sets response to cache. 417 | * @param ctx 418 | * @param next 419 | * @returns 420 | */ 421 | public put(ctx: Context, next: () => Promise): Promise | undefined { 422 | try { 423 | performance.mark('startingMark'); 424 | 425 | if (this.metrics.numberOfEntries < this.capacity) { 426 | this.metrics.addEntry() 427 | } 428 | 429 | this.cacheResponse(ctx); 430 | 431 | return next(); 432 | } catch (err) { 433 | ctx.response.status = 400; 434 | ctx.response.body = 'Error in Zoic.put. Check server logs for details.'; 435 | console.log(`Error in Zoic.put: ${err}`); 436 | } 437 | } 438 | } 439 | 440 | export default Zoic; 441 | --------------------------------------------------------------------------------