-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #568 from benjamn/introducing-@wry/caches
Introducing a new `@wry/caches` package, exporting `StrongCache` and `WeakCache`
- Loading branch information
Showing
16 changed files
with
759 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/node_modules | ||
/lib/tests | ||
tsconfig.json | ||
tsconfig.es5.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from "../../shared/rollup.config.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import "./strong.js"; | ||
import "./weak.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
Oops, something went wrong.