Skip to content

Instantly share code, notes, and snippets.

@mikeapr4
Last active April 16, 2021 20:46
Show Gist options
  • Save mikeapr4/8436ce26952bae0e20072eb6d10ff43a to your computer and use it in GitHub Desktop.
Save mikeapr4/8436ce26952bae0e20072eb6d10ff43a to your computer and use it in GitHub Desktop.
more advanced version of a reactive method-style getter caching, with a size limit on the cache
// This utility is to keep track of a list of keys (entity IDs no doubt) in last access
// order. The oldest entities can be popped off the list, and new entities can be added.
// The premise is a bidirectional linked list along with an ID hash to an point in the
// linked list. The reason behind the complexity is performance.
export default () => {
let first = null;
let last = null;
const hash = {};
let size = 0;
return {
size: () => size,
popStale(qty = 1) {
if (size < qty) {
return null;
}
const ret = [];
let x = first;
while (ret.length < qty) {
ret.push(x.id);
delete hash[x.id];
x = x.n ? hash[x.n] : null;
}
size -= qty;
if (first === last) {
last = null;
}
first = x;
if (first) {
first.p = null;
}
return ret;
},
pushAccess(id) {
if (!id) {
return;
}
const place = hash[id];
if (place) {
// If it already exists, remove it
const { p, n } = place;
if (p && n) { // middle
hash[p].n = n;
hash[n].p = p;
} else if (p) { // last
last = hash[p];
hash[p].n = null;
} else if (n) { // first
first = hash[n];
hash[n].p = null;
} else { // first and last
first = null;
last = null;
}
} else {
size += 1;
}
hash[id] = { id, p: last ? last.id : null, n: null };
if (last) {
last.n = id;
}
last = hash[id];
first = first || last;
},
};
};
import cacheQueue from 'cache-queue';
describe('Cache Queue Util', () => {
it('should not store an ID more than once', () => {
const q = cacheQueue();
[1, 2, 3, 4, 5, 4, 3, 2, 1, 1].forEach((n) => q.pushAccess(n));
expect(q.size()).to.equal(5);
expect(q.popStale(2)).to.deep.equal([5, 4]);
expect(q.size()).to.equal(3);
q.pushAccess(3);
q.pushAccess(7);
expect(q.size()).to.equal(4);
expect(q.popStale()).to.deep.equal([2]);
expect(q.popStale(3)).to.deep.equal([1, 3, 7]);
expect(q.size()).to.equal(0);
});
});
import cacheQueue from 'cache-queue';
// recreation of a computed property
const computed = (vm, func) => {
vm.$watch(
func,
null, // callback not used for lazy
{ lazy: true }, // lazy means we can just flag as dirty
);
// eslint-disable-next-line no-underscore-dangle
const watcher = vm._watchers[vm._watchers.length - 1];
// add an accessor
watcher.lazyValue = () => {
if (watcher.dirty) {
watcher.evaluate();
}
watcher.depend(); // means any calling computed will have transitive deps
return watcher.value;
};
return watcher;
};
// Cache Wrapper for Method-style Getter
// *Parameter: limit
const methodStyleCache = (getVM, getter, limit) => {
let watchers = {};
let q;
return (...args) => {
// Remove old watchers if the getter is rebuilt
Object.values(watchers).forEach((w) => w.teardown());
watchers = {};
q = cacheQueue();
const innerMethod = getter.call(null, ...args);
return (id) => {
let watcher = watchers[id];
if (!watcher) {
watcher = computed(getVM(), () => innerMethod(id));
watchers[id] = watcher;
// *If the size of the cache goes above 150% of the limit,
// drop the excess stale entries
if (limit && q.size() >= (limit * 1.5)) {
q.popStale(limit * 0.5).forEach((staleId) => {
const staleWatcher = watchers[staleId];
delete watchers[staleId];
staleWatcher.teardown();
});
}
}
// *Each access adjusts the queue
if (limit) {
q.pushAccess(id);
}
return watcher.lazyValue();
};
};
};
// Cacher that can be attached as a plugin to get a store reference
const methodGetterCacher = () => {
let store;
const getVM = () => store._vm;
return {
plugin: (st) => { store = st; },
cache: (getter, limit) => methodStyleCache(getVM, getter, limit),
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment