Skip to content

Commit

Permalink
Merge pull request #568 from benjamn/introducing-@wry/caches
Browse files Browse the repository at this point in the history
Introducing a new `@wry/caches` package, exporting `StrongCache` and `WeakCache`
  • Loading branch information
benjamn committed Nov 10, 2023
2 parents 5140d5c + 46c6217 commit feb2f83
Show file tree
Hide file tree
Showing 16 changed files with 759 additions and 1 deletion.
43 changes: 43 additions & 0 deletions packages/caches/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules
jspm_packages

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history

# Ignore generated TypeScript files.
lib

# Cache for rollup-plugin-typescript2
.rpt2_cache
4 changes: 4 additions & 0 deletions packages/caches/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/node_modules
/lib/tests
tsconfig.json
tsconfig.es5.json
9 changes: 9 additions & 0 deletions packages/caches/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# @wry/caches

Various cache implementations, including but not limited to

* `StrongCache`: A standard `Map`-like cache with a least-recently-used (LRU)
eviction policy and a callback hook for removed entries.

* `WeakCache`: Another LRU cache that holds its keys only weakly, so entries can be removed
once no longer retained elsewhere in the application.
31 changes: 31 additions & 0 deletions packages/caches/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions packages/caches/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@wry/caches",
"private": true,
"version": "0.1.0",
"author": "Ben Newman <ben@eloper.dev>",
"description": "Various cache implementations",
"license": "MIT",
"type": "module",
"main": "lib/bundle.cjs",
"module": "lib/index.js",
"types": "lib/index.d.ts",
"keywords": [],
"homepage": "https://github.com/benjamn/wryware",
"repository": {
"type": "git",
"url": "git+https://github.com/benjamn/wryware.git"
},
"bugs": {
"url": "https://github.com/benjamn/wryware/issues"
},
"scripts": {
"build": "npm run clean:before && npm run tsc && npm run rollup && npm run clean:after",
"clean:before": "rimraf lib",
"tsc": "npm run tsc:es5 && npm run tsc:esm",
"tsc:es5": "tsc -p tsconfig.es5.json",
"tsc:esm": "tsc -p tsconfig.json",
"rollup": "rollup -c rollup.config.js",
"clean:after": "rimraf lib/es5",
"prepare": "npm run build",
"test:cjs": "../../shared/test.sh lib/tests/bundle.cjs",
"test:esm": "../../shared/test.sh lib/tests/bundle.js",
"test": "npm run test:esm && npm run test:cjs"
},
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": ">=8"
}
}
1 change: 1 addition & 0 deletions packages/caches/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "../../shared/rollup.config.js";
8 changes: 8 additions & 0 deletions packages/caches/src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface CommonCache<K, V> {
has(key: K): boolean;
get(key: K): V | undefined;
set(key: K, value: V): V;
delete(key: K): boolean;
clean(): void;
readonly size: number;
}
3 changes: 3 additions & 0 deletions packages/caches/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type { CommonCache } from "./common.js";
export { StrongCache } from "./strong.js";
export { WeakCache } from "./weak.js";
121 changes: 121 additions & 0 deletions packages/caches/src/strong.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type { CommonCache } from "./common";

interface Node<K, V> {
key: K;
value: V;
newer: Node<K, V> | null;
older: Node<K, V> | null;
}

function defaultDispose() {}

export class StrongCache<K = any, V = any> implements CommonCache<K, V> {
private map = new Map<K, Node<K, V>>();
private newest: Node<K, V> | null = null;
private oldest: Node<K, V> | null = null;

constructor(
private max = Infinity,
public dispose: (value: V, key: K) => void = defaultDispose,
) {}

public has(key: K): boolean {
return this.map.has(key);
}

public get(key: K): V | undefined {
const node = this.getNode(key);
return node && node.value;
}

public get size() {
return this.map.size;
}

private getNode(key: K): Node<K, V> | undefined {
const node = this.map.get(key);

if (node && node !== this.newest) {
const { older, newer } = node;

if (newer) {
newer.older = older;
}

if (older) {
older.newer = newer;
}

node.older = this.newest;
node.older!.newer = node;

node.newer = null;
this.newest = node;

if (node === this.oldest) {
this.oldest = newer;
}
}

return node;
}

public set(key: K, value: V): V {
let node = this.getNode(key);
if (node) {
return node.value = value;
}

node = {
key,
value,
newer: null,
older: this.newest
};

if (this.newest) {
this.newest.newer = node;
}

this.newest = node;
this.oldest = this.oldest || node;

this.map.set(key, node);

return node.value;
}

public clean() {
while (this.oldest && this.map.size > this.max) {
this.delete(this.oldest.key);
}
}

public delete(key: K): boolean {
const node = this.map.get(key);
if (node) {
if (node === this.newest) {
this.newest = node.older;
}

if (node === this.oldest) {
this.oldest = node.newer;
}

if (node.newer) {
node.newer.older = node.older;
}

if (node.older) {
node.older.newer = node.newer;
}

this.map.delete(key);
this.dispose(node.value, key);

return true;
}

return false;
}
}
2 changes: 2 additions & 0 deletions packages/caches/src/tests/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import "./strong.js";
import "./weak.js";
108 changes: 108 additions & 0 deletions packages/caches/src/tests/strong.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as assert from "assert";
import { StrongCache } from "../strong.js";

describe("least-recently-used cache", function () {
it("can hold lots of elements", function () {
const cache = new StrongCache;
const count = 1000000;

for (let i = 0; i < count; ++i) {
cache.set(i, String(i));
}

cache.clean();

assert.strictEqual((cache as any).map.size, count);
assert.ok(cache.has(0));
assert.ok(cache.has(count - 1));
assert.strictEqual(cache.get(43), "43");
});

it("evicts excess old elements", function () {
const max = 10;
const evicted = [];
const cache = new StrongCache(max, (value, key) => {
assert.strictEqual(String(key), value);
evicted.push(key);
});

const count = 100;
const keys = [];
for (let i = 0; i < count; ++i) {
cache.set(i, String(i));
keys.push(i);
}

cache.clean();

assert.strictEqual((cache as any).map.size, max);
assert.strictEqual(evicted.length, count - max);

for (let i = count - max; i < count; ++i) {
assert.ok(cache.has(i));
}
});

it("can cope with small max values", function () {
const cache = new StrongCache(2);

function check(...sequence: number[]) {
cache.clean();

let entry = (cache as any).newest;
const forwards = [];
while (entry) {
forwards.push(entry.key);
entry = entry.older;
}
assert.deepEqual(forwards, sequence);

const backwards = [];
entry = (cache as any).oldest;
while (entry) {
backwards.push(entry.key);
entry = entry.newer;
}
backwards.reverse();
assert.deepEqual(backwards, sequence);

sequence.forEach(function (n) {
assert.strictEqual((cache as any).map.get(n).value, n + 1);
});

if (sequence.length > 0) {
assert.strictEqual((cache as any).newest.key, sequence[0]);
assert.strictEqual((cache as any).oldest.key,
sequence[sequence.length - 1]);
}
}

cache.set(1, 2);
check(1);

cache.set(2, 3);
check(2, 1);

cache.set(3, 4);
check(3, 2);

cache.get(2);
check(2, 3);

cache.set(4, 5);
check(4, 2);

assert.strictEqual(cache.has(1), false);
assert.strictEqual(cache.get(2), 3);
assert.strictEqual(cache.has(3), false);
assert.strictEqual(cache.get(4), 5);

cache.delete(2);
check(4);
cache.delete(4);
check();

assert.strictEqual((cache as any).newest, null);
assert.strictEqual((cache as any).oldest, null);
});
});

0 comments on commit feb2f83

Please sign in to comment.