├── .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 |

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 |
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 |
--------------------------------------------------------------------------------