500 lines
17 KiB
JavaScript
500 lines
17 KiB
JavaScript
"use strict";
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.unsafeMakeWith = exports.refreshing = exports.pending = exports.makeWith = exports.makeMapKey = exports.makeKeySet = exports.makeEntryStats = exports.makeCacheStats = exports.makeCacheState = exports.make = exports.isMapKey = exports.initialCacheState = exports.complete = exports.MapKeyTypeId = exports.ConsumerCacheTypeId = exports.CacheTypeId = void 0;
|
|
var Context = _interopRequireWildcard(require("../Context.js"));
|
|
var Deferred = _interopRequireWildcard(require("../Deferred.js"));
|
|
var Duration = _interopRequireWildcard(require("../Duration.js"));
|
|
var Either = _interopRequireWildcard(require("../Either.js"));
|
|
var Equal = _interopRequireWildcard(require("../Equal.js"));
|
|
var Exit = _interopRequireWildcard(require("../Exit.js"));
|
|
var _Function = require("../Function.js");
|
|
var Hash = _interopRequireWildcard(require("../Hash.js"));
|
|
var MutableHashMap = _interopRequireWildcard(require("../MutableHashMap.js"));
|
|
var MutableQueue = _interopRequireWildcard(require("../MutableQueue.js"));
|
|
var MutableRef = _interopRequireWildcard(require("../MutableRef.js"));
|
|
var Option = _interopRequireWildcard(require("../Option.js"));
|
|
var _Predicate = require("../Predicate.js");
|
|
var effect = _interopRequireWildcard(require("./core-effect.js"));
|
|
var core = _interopRequireWildcard(require("./core.js"));
|
|
var Data = _interopRequireWildcard(require("./data.js"));
|
|
var _fiberId = require("./fiberId.js");
|
|
var fiberRuntime = _interopRequireWildcard(require("./fiberRuntime.js"));
|
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
/** @internal */
|
|
const complete = (key, exit, entryStats, timeToLiveMillis) => Data.struct({
|
|
_tag: "Complete",
|
|
key,
|
|
exit,
|
|
entryStats,
|
|
timeToLiveMillis
|
|
});
|
|
/** @internal */
|
|
exports.complete = complete;
|
|
const pending = (key, deferred) => Data.struct({
|
|
_tag: "Pending",
|
|
key,
|
|
deferred
|
|
});
|
|
/** @internal */
|
|
exports.pending = pending;
|
|
const refreshing = (deferred, complete) => Data.struct({
|
|
_tag: "Refreshing",
|
|
deferred,
|
|
complete
|
|
});
|
|
/** @internal */
|
|
exports.refreshing = refreshing;
|
|
const MapKeyTypeId = exports.MapKeyTypeId = /*#__PURE__*/Symbol.for("effect/Cache/MapKey");
|
|
class MapKeyImpl {
|
|
current;
|
|
[MapKeyTypeId] = MapKeyTypeId;
|
|
previous = undefined;
|
|
next = undefined;
|
|
constructor(current) {
|
|
this.current = current;
|
|
}
|
|
[Hash.symbol]() {
|
|
return (0, _Function.pipe)(Hash.hash(this.current), Hash.combine(Hash.hash(this.previous)), Hash.combine(Hash.hash(this.next)), Hash.cached(this));
|
|
}
|
|
[Equal.symbol](that) {
|
|
if (this === that) {
|
|
return true;
|
|
}
|
|
return isMapKey(that) && Equal.equals(this.current, that.current) && Equal.equals(this.previous, that.previous) && Equal.equals(this.next, that.next);
|
|
}
|
|
}
|
|
/** @internal */
|
|
const makeMapKey = current => new MapKeyImpl(current);
|
|
/** @internal */
|
|
exports.makeMapKey = makeMapKey;
|
|
const isMapKey = u => (0, _Predicate.hasProperty)(u, MapKeyTypeId);
|
|
exports.isMapKey = isMapKey;
|
|
class KeySetImpl {
|
|
head = undefined;
|
|
tail = undefined;
|
|
add(key) {
|
|
if (key !== this.tail) {
|
|
if (this.tail === undefined) {
|
|
this.head = key;
|
|
this.tail = key;
|
|
} else {
|
|
const previous = key.previous;
|
|
const next = key.next;
|
|
if (next !== undefined) {
|
|
key.next = undefined;
|
|
if (previous !== undefined) {
|
|
previous.next = next;
|
|
next.previous = previous;
|
|
} else {
|
|
this.head = next;
|
|
this.head.previous = undefined;
|
|
}
|
|
}
|
|
this.tail.next = key;
|
|
key.previous = this.tail;
|
|
this.tail = key;
|
|
}
|
|
}
|
|
}
|
|
remove() {
|
|
const key = this.head;
|
|
if (key !== undefined) {
|
|
const next = key.next;
|
|
if (next !== undefined) {
|
|
key.next = undefined;
|
|
this.head = next;
|
|
this.head.previous = undefined;
|
|
} else {
|
|
this.head = undefined;
|
|
this.tail = undefined;
|
|
}
|
|
}
|
|
return key;
|
|
}
|
|
}
|
|
/** @internal */
|
|
const makeKeySet = () => new KeySetImpl();
|
|
/**
|
|
* Constructs a new `CacheState` from the specified values.
|
|
*
|
|
* @internal
|
|
*/
|
|
exports.makeKeySet = makeKeySet;
|
|
const makeCacheState = (map, keys, accesses, updating, hits, misses) => ({
|
|
map,
|
|
keys,
|
|
accesses,
|
|
updating,
|
|
hits,
|
|
misses
|
|
});
|
|
/**
|
|
* Constructs an initial cache state.
|
|
*
|
|
* @internal
|
|
*/
|
|
exports.makeCacheState = makeCacheState;
|
|
const initialCacheState = () => makeCacheState(MutableHashMap.empty(), makeKeySet(), MutableQueue.unbounded(), MutableRef.make(false), 0, 0);
|
|
/** @internal */
|
|
exports.initialCacheState = initialCacheState;
|
|
const CacheSymbolKey = "effect/Cache";
|
|
/** @internal */
|
|
const CacheTypeId = exports.CacheTypeId = /*#__PURE__*/Symbol.for(CacheSymbolKey);
|
|
const cacheVariance = {
|
|
/* c8 ignore next */
|
|
_Key: _ => _,
|
|
/* c8 ignore next */
|
|
_Error: _ => _,
|
|
/* c8 ignore next */
|
|
_Value: _ => _
|
|
};
|
|
/** @internal */
|
|
const ConsumerCacheSymbolKey = "effect/ConsumerCache";
|
|
/** @internal */
|
|
const ConsumerCacheTypeId = exports.ConsumerCacheTypeId = /*#__PURE__*/Symbol.for(ConsumerCacheSymbolKey);
|
|
const consumerCacheVariance = {
|
|
/* c8 ignore next */
|
|
_Key: _ => _,
|
|
/* c8 ignore next */
|
|
_Error: _ => _,
|
|
/* c8 ignore next */
|
|
_Value: _ => _
|
|
};
|
|
/** @internal */
|
|
const makeCacheStats = options => options;
|
|
/** @internal */
|
|
exports.makeCacheStats = makeCacheStats;
|
|
const makeEntryStats = loadedMillis => ({
|
|
loadedMillis
|
|
});
|
|
exports.makeEntryStats = makeEntryStats;
|
|
class CacheImpl {
|
|
capacity;
|
|
context;
|
|
fiberId;
|
|
lookup;
|
|
timeToLive;
|
|
[CacheTypeId] = cacheVariance;
|
|
[ConsumerCacheTypeId] = consumerCacheVariance;
|
|
cacheState;
|
|
constructor(capacity, context, fiberId, lookup, timeToLive) {
|
|
this.capacity = capacity;
|
|
this.context = context;
|
|
this.fiberId = fiberId;
|
|
this.lookup = lookup;
|
|
this.timeToLive = timeToLive;
|
|
this.cacheState = initialCacheState();
|
|
}
|
|
get(key) {
|
|
return core.map(this.getEither(key), Either.merge);
|
|
}
|
|
get cacheStats() {
|
|
return core.sync(() => makeCacheStats({
|
|
hits: this.cacheState.hits,
|
|
misses: this.cacheState.misses,
|
|
size: MutableHashMap.size(this.cacheState.map)
|
|
}));
|
|
}
|
|
getOption(key) {
|
|
return core.suspend(() => Option.match(MutableHashMap.get(this.cacheState.map, key), {
|
|
onNone: () => {
|
|
const mapKey = makeMapKey(key);
|
|
this.trackAccess(mapKey);
|
|
this.trackMiss();
|
|
return core.succeed(Option.none());
|
|
},
|
|
onSome: value => this.resolveMapValue(value)
|
|
}));
|
|
}
|
|
getOptionComplete(key) {
|
|
return core.suspend(() => Option.match(MutableHashMap.get(this.cacheState.map, key), {
|
|
onNone: () => {
|
|
const mapKey = makeMapKey(key);
|
|
this.trackAccess(mapKey);
|
|
this.trackMiss();
|
|
return core.succeed(Option.none());
|
|
},
|
|
onSome: value => this.resolveMapValue(value, true)
|
|
}));
|
|
}
|
|
contains(key) {
|
|
return core.sync(() => MutableHashMap.has(this.cacheState.map, key));
|
|
}
|
|
entryStats(key) {
|
|
return core.sync(() => {
|
|
const option = MutableHashMap.get(this.cacheState.map, key);
|
|
if (Option.isSome(option)) {
|
|
switch (option.value._tag) {
|
|
case "Complete":
|
|
{
|
|
const loaded = option.value.entryStats.loadedMillis;
|
|
return Option.some(makeEntryStats(loaded));
|
|
}
|
|
case "Pending":
|
|
{
|
|
return Option.none();
|
|
}
|
|
case "Refreshing":
|
|
{
|
|
const loaded = option.value.complete.entryStats.loadedMillis;
|
|
return Option.some(makeEntryStats(loaded));
|
|
}
|
|
}
|
|
}
|
|
return Option.none();
|
|
});
|
|
}
|
|
getEither(key) {
|
|
return core.suspend(() => {
|
|
const k = key;
|
|
let mapKey = undefined;
|
|
let deferred = undefined;
|
|
let value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k));
|
|
if (value === undefined) {
|
|
deferred = Deferred.unsafeMake(this.fiberId);
|
|
mapKey = makeMapKey(k);
|
|
if (MutableHashMap.has(this.cacheState.map, k)) {
|
|
value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k));
|
|
} else {
|
|
MutableHashMap.set(this.cacheState.map, k, pending(mapKey, deferred));
|
|
}
|
|
}
|
|
if (value === undefined) {
|
|
this.trackAccess(mapKey);
|
|
this.trackMiss();
|
|
return core.map(this.lookupValueOf(key, deferred), Either.right);
|
|
} else {
|
|
return core.flatMap(this.resolveMapValue(value), Option.match({
|
|
onNone: () => this.getEither(key),
|
|
onSome: value => core.succeed(Either.left(value))
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
invalidate(key) {
|
|
return core.sync(() => {
|
|
MutableHashMap.remove(this.cacheState.map, key);
|
|
});
|
|
}
|
|
invalidateWhen(key, when) {
|
|
return core.sync(() => {
|
|
const value = MutableHashMap.get(this.cacheState.map, key);
|
|
if (Option.isSome(value) && value.value._tag === "Complete") {
|
|
if (value.value.exit._tag === "Success") {
|
|
if (when(value.value.exit.value)) {
|
|
MutableHashMap.remove(this.cacheState.map, key);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
get invalidateAll() {
|
|
return core.sync(() => {
|
|
this.cacheState.map = MutableHashMap.empty();
|
|
});
|
|
}
|
|
refresh(key) {
|
|
return effect.clockWith(clock => core.suspend(() => {
|
|
const k = key;
|
|
const deferred = Deferred.unsafeMake(this.fiberId);
|
|
let value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k));
|
|
if (value === undefined) {
|
|
if (MutableHashMap.has(this.cacheState.map, k)) {
|
|
value = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k));
|
|
} else {
|
|
MutableHashMap.set(this.cacheState.map, k, pending(makeMapKey(k), deferred));
|
|
}
|
|
}
|
|
if (value === undefined) {
|
|
return core.asVoid(this.lookupValueOf(key, deferred));
|
|
} else {
|
|
switch (value._tag) {
|
|
case "Complete":
|
|
{
|
|
if (this.hasExpired(clock, value.timeToLiveMillis)) {
|
|
const found = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k));
|
|
if (Equal.equals(found, value)) {
|
|
MutableHashMap.remove(this.cacheState.map, k);
|
|
}
|
|
return core.asVoid(this.get(key));
|
|
}
|
|
// Only trigger the lookup if we're still the current value, `completedResult`
|
|
return (0, _Function.pipe)(this.lookupValueOf(key, deferred), effect.when(() => {
|
|
const current = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k));
|
|
if (Equal.equals(current, value)) {
|
|
const mapValue = refreshing(deferred, value);
|
|
MutableHashMap.set(this.cacheState.map, k, mapValue);
|
|
return true;
|
|
}
|
|
return false;
|
|
}), core.asVoid);
|
|
}
|
|
case "Pending":
|
|
{
|
|
return Deferred.await(value.deferred);
|
|
}
|
|
case "Refreshing":
|
|
{
|
|
return Deferred.await(value.deferred);
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
set(key, value) {
|
|
return effect.clockWith(clock => core.sync(() => {
|
|
const now = clock.unsafeCurrentTimeMillis();
|
|
const k = key;
|
|
const lookupResult = Exit.succeed(value);
|
|
const mapValue = complete(makeMapKey(k), lookupResult, makeEntryStats(now), now + Duration.toMillis(Duration.decode(this.timeToLive(lookupResult))));
|
|
MutableHashMap.set(this.cacheState.map, k, mapValue);
|
|
}));
|
|
}
|
|
get size() {
|
|
return core.sync(() => {
|
|
return MutableHashMap.size(this.cacheState.map);
|
|
});
|
|
}
|
|
get values() {
|
|
return core.sync(() => {
|
|
const values = [];
|
|
for (const entry of this.cacheState.map) {
|
|
if (entry[1]._tag === "Complete" && entry[1].exit._tag === "Success") {
|
|
values.push(entry[1].exit.value);
|
|
}
|
|
}
|
|
return values;
|
|
});
|
|
}
|
|
get entries() {
|
|
return core.sync(() => {
|
|
const values = [];
|
|
for (const entry of this.cacheState.map) {
|
|
if (entry[1]._tag === "Complete" && entry[1].exit._tag === "Success") {
|
|
values.push([entry[0], entry[1].exit.value]);
|
|
}
|
|
}
|
|
return values;
|
|
});
|
|
}
|
|
get keys() {
|
|
return core.sync(() => {
|
|
const keys = [];
|
|
for (const entry of this.cacheState.map) {
|
|
if (entry[1]._tag === "Complete" && entry[1].exit._tag === "Success") {
|
|
keys.push(entry[0]);
|
|
}
|
|
}
|
|
return keys;
|
|
});
|
|
}
|
|
resolveMapValue(value, ignorePending = false) {
|
|
return effect.clockWith(clock => {
|
|
switch (value._tag) {
|
|
case "Complete":
|
|
{
|
|
this.trackAccess(value.key);
|
|
if (this.hasExpired(clock, value.timeToLiveMillis)) {
|
|
MutableHashMap.remove(this.cacheState.map, value.key.current);
|
|
return core.succeed(Option.none());
|
|
}
|
|
this.trackHit();
|
|
return core.map(value.exit, Option.some);
|
|
}
|
|
case "Pending":
|
|
{
|
|
this.trackAccess(value.key);
|
|
this.trackHit();
|
|
if (ignorePending) {
|
|
return core.succeed(Option.none());
|
|
}
|
|
return core.map(Deferred.await(value.deferred), Option.some);
|
|
}
|
|
case "Refreshing":
|
|
{
|
|
this.trackAccess(value.complete.key);
|
|
this.trackHit();
|
|
if (this.hasExpired(clock, value.complete.timeToLiveMillis)) {
|
|
if (ignorePending) {
|
|
return core.succeed(Option.none());
|
|
}
|
|
return core.map(Deferred.await(value.deferred), Option.some);
|
|
}
|
|
return core.map(value.complete.exit, Option.some);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
trackHit() {
|
|
this.cacheState.hits = this.cacheState.hits + 1;
|
|
}
|
|
trackMiss() {
|
|
this.cacheState.misses = this.cacheState.misses + 1;
|
|
}
|
|
trackAccess(key) {
|
|
MutableQueue.offer(this.cacheState.accesses, key);
|
|
if (MutableRef.compareAndSet(this.cacheState.updating, false, true)) {
|
|
let loop = true;
|
|
while (loop) {
|
|
const key = MutableQueue.poll(this.cacheState.accesses, MutableQueue.EmptyMutableQueue);
|
|
if (key === MutableQueue.EmptyMutableQueue) {
|
|
loop = false;
|
|
} else {
|
|
this.cacheState.keys.add(key);
|
|
}
|
|
}
|
|
let size = MutableHashMap.size(this.cacheState.map);
|
|
loop = size > this.capacity;
|
|
while (loop) {
|
|
const key = this.cacheState.keys.remove();
|
|
if (key !== undefined) {
|
|
if (MutableHashMap.has(this.cacheState.map, key.current)) {
|
|
MutableHashMap.remove(this.cacheState.map, key.current);
|
|
size = size - 1;
|
|
loop = size > this.capacity;
|
|
}
|
|
} else {
|
|
loop = false;
|
|
}
|
|
}
|
|
MutableRef.set(this.cacheState.updating, false);
|
|
}
|
|
}
|
|
hasExpired(clock, timeToLiveMillis) {
|
|
return clock.unsafeCurrentTimeMillis() > timeToLiveMillis;
|
|
}
|
|
lookupValueOf(input, deferred) {
|
|
return effect.clockWith(clock => core.suspend(() => {
|
|
const key = input;
|
|
return (0, _Function.pipe)(this.lookup(input), core.provideContext(this.context), core.exit, core.flatMap(exit => {
|
|
const now = clock.unsafeCurrentTimeMillis();
|
|
const stats = makeEntryStats(now);
|
|
const value = complete(makeMapKey(key), exit, stats, now + Duration.toMillis(Duration.decode(this.timeToLive(exit))));
|
|
MutableHashMap.set(this.cacheState.map, key, value);
|
|
return core.zipRight(Deferred.done(deferred, exit), exit);
|
|
}), core.onInterrupt(() => core.zipRight(Deferred.interrupt(deferred), core.sync(() => {
|
|
MutableHashMap.remove(this.cacheState.map, key);
|
|
}))));
|
|
}));
|
|
}
|
|
}
|
|
/** @internal */
|
|
const make = options => {
|
|
const timeToLive = Duration.decode(options.timeToLive);
|
|
return makeWith({
|
|
capacity: options.capacity,
|
|
lookup: options.lookup,
|
|
timeToLive: () => timeToLive
|
|
});
|
|
};
|
|
/** @internal */
|
|
exports.make = make;
|
|
const makeWith = options => core.map(fiberRuntime.all([core.context(), core.fiberId]), ([context, fiberId]) => new CacheImpl(options.capacity, context, fiberId, options.lookup, exit => Duration.decode(options.timeToLive(exit))));
|
|
/** @internal */
|
|
exports.makeWith = makeWith;
|
|
const unsafeMakeWith = (capacity, lookup, timeToLive) => new CacheImpl(capacity, Context.empty(), _fiberId.none, lookup, exit => Duration.decode(timeToLive(exit)));
|
|
exports.unsafeMakeWith = unsafeMakeWith;
|
|
//# sourceMappingURL=cache.js.map
|