Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* @see {@link https://github.com/asamuzaK/cssColor/blob/main/LICENSE}
*/

import { GenerationalCache } from './js/cache';
import { cssCalc } from './js/css-calc';
import { isGradient, resolveGradient } from './js/css-gradient';
import { cssVar } from './js/css-var';
Expand All @@ -21,6 +22,7 @@ export { convert } from './js/convert';
export { resolve } from './js/resolve';
/* utils */
export const utils = {
GenerationalCache,
cssCalc,
cssVar,
extractDashedIdent,
Expand Down
61 changes: 44 additions & 17 deletions src/js/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,50 @@ export class NullObject extends CacheItem {
* Generational Cache implementation
*/
export class GenerationalCache<K, V> {
private max: number;
private current: Map<K, V>;
private old: Map<K, V>;
#max: number;
#boundary: number;
#current: Map<K, V>;
#old: Map<K, V>;

constructor(max: number) {
this.max = Math.ceil(max / 2);
this.current = new Map<K, V>();
this.old = new Map<K, V>();
this.#current = new Map<K, V>();
this.#old = new Map<K, V>();
if (Number.isFinite(max) && max > 4) {
this.#max = max;
this.#boundary = Math.ceil(max / 2);
} else {
this.#max = 4;
this.#boundary = 2;
}
}

get size() {
return this.#current.size + this.#old.size;
}

get max(): number {
return this.#max;
}

set max(value: number) {
if (Number.isFinite(value) && value > 4) {
this.#max = value;
this.#boundary = Math.ceil(value / 2);
} else {
this.#max = 4;
this.#boundary = 2;
}
this.#current.clear();
this.#old.clear();
}

get(key: K): V | undefined {
let value = this.current.get(key);
let value = this.#current.get(key);
if (value !== undefined) {
return value;
}

value = this.old.get(key);
value = this.#old.get(key);
if (value !== undefined) {
this.set(key, value);
return value;
Expand All @@ -74,26 +101,26 @@ export class GenerationalCache<K, V> {
}

set(key: K, value: V): void {
this.current.set(key, value);
this.#current.set(key, value);

if (this.current.size >= this.max) {
this.old = this.current;
this.current = new Map<K, V>();
if (this.#current.size >= this.#boundary) {
this.#old = this.#current;
this.#current = new Map<K, V>();
}
}

has(key: K): boolean {
return this.current.has(key) || this.old.has(key);
return this.#current.has(key) || this.#old.has(key);
}

delete(key: K): void {
this.current.delete(key);
this.old.delete(key);
this.#current.delete(key);
this.#old.delete(key);
}

clear(): void {
this.current.clear();
this.old.clear();
this.#current.clear();
this.#old.clear();
}
}

Expand Down
165 changes: 157 additions & 8 deletions test/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,163 @@ import { afterEach, assert, beforeEach, describe, it } from 'vitest';
import * as cache from '../src/js/cache';

describe('generational cache', () => {
it('should be instance', () => {
const { GenerationalCache, genCache } = cache;
assert.strictEqual(genCache instanceof GenerationalCache, true, 'instance');
assert.strictEqual(typeof genCache.clear, 'function', 'clear');
assert.strictEqual(typeof genCache.delete, 'function', 'delete');
assert.strictEqual(typeof genCache.get, 'function', 'get');
assert.strictEqual(typeof genCache.has, 'function', 'has');
assert.strictEqual(typeof genCache.set, 'function', 'set');
it('should initialize with 4 for the max generation size', () => {
const genCache = new cache.GenerationalCache(2);
assert.strictEqual(genCache.max, 4, 'max generation size should be 4');
});

it('should initialize with the given max generation size', () => {
const genCache = new cache.GenerationalCache(5);
assert.strictEqual(
genCache.max,
5,
'max generation size should be given value'
);
});

it('should set max generation size and clear cache', () => {
const genCache = new cache.GenerationalCache(2);
genCache.set('foo', 'bar');
assert.strictEqual(genCache.size, 1, 'cache is added');
genCache.max = 5;
assert.strictEqual(
genCache.max,
5,
'max generation size should be given value'
);
assert.strictEqual(genCache.size, 0, 'cache is cleared');
});

it('should set max generation size and clear cache', () => {
const genCache = new cache.GenerationalCache(5);
genCache.set('foo', 'bar');
assert.strictEqual(genCache.size, 1, 'cache is added');
genCache.max = 2;
assert.strictEqual(genCache.max, 4, 'max generation size should be 4');
assert.strictEqual(genCache.size, 0, 'cache is cleared');
});

it('should be within max generation size', () => {
const genCache = new cache.GenerationalCache(9);
const boundary = Math.ceil(genCache.max / 2);
const sizes = [];
for (let i = 1; i < 20; i++) {
genCache.set(`key${i}`, i);
sizes.push(genCache.size);
if (i < genCache.max) {
assert.strictEqual(genCache.size, i, `${i}`);
} else {
assert.strictEqual(genCache.size, (i % boundary) + boundary, `${i}`);
}
}
assert.deepEqual(
sizes,
[1, 2, 3, 4, 5, 6, 7, 8, 9, 5, 6, 7, 8, 9, 5, 6, 7, 8, 9]
);
});

it('should set and get values', () => {
const genCache = new cache.GenerationalCache(10);
genCache.set('key1', 'value1');
genCache.set('key2', { foo: 'bar' });

assert.strictEqual(
genCache.get('key1'),
'value1',
'should get primitive value'
);
assert.deepEqual(
genCache.get('key2'),
{ foo: 'bar' },
'should get object value'
);
assert.strictEqual(
genCache.get('unknown'),
undefined,
'should return undefined for missing keys'
);
});

it('should check existence with has()', () => {
const genCache = new cache.GenerationalCache(10);
genCache.set('key1', 'value1');

assert.strictEqual(genCache.has('key1'), true, 'should have key1');
assert.strictEqual(genCache.has('key2'), false, 'should not have key2');
});

it('should delete values', () => {
const genCache = new cache.GenerationalCache(10);
genCache.set('key1', 'value1');
genCache.delete('key1');

assert.strictEqual(genCache.has('key1'), false, 'key1 should be deleted');
assert.strictEqual(
genCache.get('key1'),
undefined,
'deleted key should return undefined'
);
});

it('should clear all values', () => {
const genCache = new cache.GenerationalCache(10);
genCache.set('key1', 'value1');
genCache.set('key2', 'value2');
genCache.clear();

assert.strictEqual(genCache.has('key1'), false, 'key1 should be cleared');
assert.strictEqual(genCache.has('key2'), false, 'key2 should be cleared');
});

it('should shift generations and evict old items', () => {
const genCache = new cache.GenerationalCache(4);
genCache.set('k1', 'v1');
genCache.set('k2', 'v2');
assert.strictEqual(
genCache.has('k1'),
true,
'k1 should exist in old generation'
);
assert.strictEqual(
genCache.has('k2'),
true,
'k2 should exist in old generation'
);

genCache.set('k3', 'v3');
genCache.set('k4', 'v4');
assert.strictEqual(genCache.has('k1'), false, 'k1 should be evicted');
assert.strictEqual(genCache.has('k2'), false, 'k2 should be evicted');
assert.strictEqual(
genCache.has('k3'),
true,
'k3 should survive in old generation'
);
assert.strictEqual(
genCache.has('k4'),
true,
'k4 should survive in old generation'
);
});

it('should promote accessed old items to current generation', () => {
const genCache = new cache.GenerationalCache(4);
genCache.set('k1', 'v1');
genCache.set('k2', 'v2');
const val = genCache.get('k1');
assert.strictEqual(val, 'v1', 'should get promoted value');
genCache.set('k3', 'v3');
assert.strictEqual(
genCache.has('k1'),
true,
'k1 should survive because it was promoted'
);
assert.strictEqual(genCache.has('k2'), false, 'k2 should be evicted');
assert.strictEqual(
genCache.has('k3'),
true,
'k3 should survive in old generation'
);
});
});

Expand Down
Loading