<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
- <option value="$PROJECT_DIR$/cache4k" />
<option value="$PROJECT_DIR$/externals" />
<option value="$PROJECT_DIR$/fontparser" />
</set>
implementation("com.aventrix.jnanoid:jnanoid:2.0.0")
implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.0.0")
implementation("org.mongodb:bson-kotlinx:5.0.0")
- implementation(project(":cache4k"))
implementation("org.slf4j:slf4j-api:2.0.7")
implementation("ch.qos.logback:logback-classic:1.4.14")
+++ /dev/null
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [2021] [Yang Chen]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+++ /dev/null
-plugins {
- kotlin("jvm")
-}
-
-group = "io.github.reactivecircus.cache4k"
-
-repositories {
- mavenCentral()
-}
-
-dependencies {
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0")
- implementation("org.jetbrains.kotlinx:atomicfu:0.23.2")
- implementation("co.touchlab:stately-iso-collections:2.0.6")
-}
+++ /dev/null
-package io.github.reactivecircus.cache4k
-
-import kotlin.time.Duration
-import kotlin.time.TimeSource
-
-/**
- * An in-memory key-value cache with support for time-based (expiration) and size-based evictions.
- */
-public interface Cache<in Key : Any, Value : Any> {
-
- /**
- * Returns the value associated with [key] in this cache, or null if there is no
- * cached value for [key].
- */
- public fun get(key: Key): Value?
-
- /**
- * Returns the value associated with [key] in this cache if exists,
- * otherwise gets the value by invoking [loader], associates the value with [key] in the cache,
- * and returns the cached value.
- *
- * Any exceptions thrown by the [loader] will be propagated to the caller of this function.
- */
- public suspend fun get(key: Key, loader: suspend () -> Value): Value
-
- /**
- * Associates [value] with [key] in this cache. If the cache previously contained a
- * value associated with [key], the old value is replaced by [value].
- */
- public fun put(key: Key, value: Value)
-
- /**
- * Invokes [loader] atomically on the previous value of [key], or null if absent,
- * associates the result of [loader] with [key] in the cache, invalidating if the result
- * is null, and returns the result.
- *
- * Any exceptions thrown by the [loader] will be propagated to the caller of this function.
- */
- public suspend fun processAtomic(key: Key, loader: suspend (Value?) -> Value?): Value?
-
- /**
- * Discards any cached value for key [key].
- */
- public fun invalidate(key: Key)
-
- /**
- * Discards all entries in the cache.
- */
- public fun invalidateAll()
-
- /**
- * Returns a defensive copy of cache entries as [Map].
- */
- public fun asMap(): Map<in Key, Value>
-
- /**
- * Main entry point for creating a [Cache].
- */
- public interface Builder<K : Any, V : Any> {
-
- /**
- * Specifies that each entry should be automatically removed from the cache once a fixed duration
- * has elapsed after the entry's creation or the most recent replacement of its value.
- *
- * When [duration] is zero, the cache's max size will be set to 0
- * meaning no values will be cached.
- */
- public fun expireAfterWrite(duration: Duration): Builder<K, V>
-
- /**
- * Specifies that each entry should be automatically removed from the cache once a fixed duration
- * has elapsed after the entry's creation, the most recent replacement of its value, or its last
- * access.
- *
- * When [duration] is zero, the cache's max size will be set to 0
- * meaning no values will be cached.
- */
- public fun expireAfterAccess(duration: Duration): Builder<K, V>
-
- /**
- * Specifies the maximum number of entries the cache may contain.
- * Cache eviction policy is based on LRU - i.e. least recently accessed entries get evicted first.
- *
- * When [size] is 0, entries will be discarded immediately and no values will be cached.
- *
- * If not set, cache size will be unlimited.
- */
- public fun maximumCacheSize(size: Long): Builder<K, V>
-
- /**
- * Specifies a [TimeSource] to be used for expiry checks.
- * If not specified, [TimeSource.Monotonic] will be used.
- */
- public fun timeSource(timeSource: TimeSource): Builder<K, V>
-
- /**
- * Specifies a [CacheEventListener] to be used to handle cache events.
- */
- public fun eventListener(listener: CacheEventListener<K, V>): Builder<K, V>
-
- /**
- * Builds a new instance of [Cache] with the specified configurations.
- */
- public fun build(): Cache<K, V>
-
- public companion object {
-
- /**
- * Returns a new [Cache.Builder] instance.
- */
- public operator fun <K : Any, V : Any> invoke(): Builder<K, V> = CacheBuilderImpl()
- }
- }
-}
-
-/**
- * A default implementation of [Cache.Builder].
- */
-internal class CacheBuilderImpl<K : Any, V : Any> : Cache.Builder<K, V> {
-
- private var expireAfterWriteDuration = Duration.INFINITE
-
- private var expireAfterAccessDuration = Duration.INFINITE
- private var maxSize = UNSET_LONG
- private var timeSource: TimeSource? = null
- private var eventListener: CacheEventListener<K, V>? = null
-
- override fun expireAfterWrite(duration: Duration): CacheBuilderImpl<K, V> = apply {
- require(duration.isPositive()) {
- "expireAfterWrite duration must be positive"
- }
- this.expireAfterWriteDuration = duration
- }
-
- override fun expireAfterAccess(duration: Duration): CacheBuilderImpl<K, V> = apply {
- require(duration.isPositive()) {
- "expireAfterAccess duration must be positive"
- }
- this.expireAfterAccessDuration = duration
- }
-
- override fun maximumCacheSize(size: Long): CacheBuilderImpl<K, V> = apply {
- require(size >= 0) {
- "maximum size must not be negative"
- }
- this.maxSize = size
- }
-
- override fun timeSource(timeSource: TimeSource): Cache.Builder<K, V> = apply {
- this.timeSource = timeSource
- }
-
- override fun eventListener(listener: CacheEventListener<K, V>): Cache.Builder<K, V> = apply {
- eventListener = listener
- }
-
- override fun build(): Cache<K, V> {
- return RealCache(
- expireAfterWriteDuration,
- expireAfterAccessDuration,
- maxSize,
- timeSource ?: TimeSource.Monotonic,
- eventListener,
- )
- }
-
- companion object {
- internal const val UNSET_LONG: Long = -1
- }
-}
+++ /dev/null
-package io.github.reactivecircus.cache4k
-
-/**
- * An event resulting from a mutative [Cache] operation.
- */
-public sealed interface CacheEvent<Key : Any, Value : Any> {
- public val key: Key
-
- public class Created<Key : Any, Value : Any>(
- override val key: Key,
- public val value: Value,
- ) : CacheEvent<Key, Value> {
- override fun toString(): String {
- return "Created(key=$key, value=$value)"
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this::class != other::class) return false
-
- other as Created<*, *>
-
- return key == other.key && value == other.value
- }
-
- override fun hashCode(): Int {
- var result = key.hashCode()
- result = 31 * result + value.hashCode()
- return result
- }
- }
-
- public class Updated<Key : Any, Value : Any>(
- override val key: Key,
- public val oldValue: Value,
- public val newValue: Value,
- ) : CacheEvent<Key, Value> {
- override fun toString(): String {
- return "Updated(key=$key, oldValue=$oldValue, newValue=$newValue)"
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this::class != other::class) return false
-
- other as Updated<*, *>
-
- return key == other.key && oldValue == other.oldValue && newValue == other.newValue
- }
-
- override fun hashCode(): Int {
- var result = key.hashCode()
- result = 31 * result + oldValue.hashCode()
- result = 31 * result + newValue.hashCode()
- return result
- }
- }
-
- public class Removed<Key : Any, Value : Any>(
- override val key: Key,
- public val value: Value,
- ) : CacheEvent<Key, Value> {
- override fun toString(): String {
- return "Removed(key=$key, value=$value)"
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this::class != other::class) return false
-
- other as Removed<*, *>
-
- return key == other.key && value == other.value
- }
-
- override fun hashCode(): Int {
- var result = key.hashCode()
- result = 31 * result + value.hashCode()
- return result
- }
- }
-
- public class Expired<Key : Any, Value : Any>(
- override val key: Key,
- public val value: Value,
- ) : CacheEvent<Key, Value> {
- override fun toString(): String {
- return "Expired(key=$key, value=$value)"
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this::class != other::class) return false
-
- other as Expired<*, *>
-
- return key == other.key && value == other.value
- }
-
- override fun hashCode(): Int {
- var result = key.hashCode()
- result = 31 * result + value.hashCode()
- return result
- }
- }
-
- public class Evicted<Key : Any, Value : Any>(
- override val key: Key,
- public val value: Value,
- ) : CacheEvent<Key, Value> {
- override fun toString(): String {
- return "Evicted(key=$key, value=$value)"
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this::class != other::class) return false
-
- other as Evicted<*, *>
-
- return key == other.key && value == other.value
- }
-
- override fun hashCode(): Int {
- var result = key.hashCode()
- result = 31 * result + value.hashCode()
- return result
- }
- }
-}
-
-/**
- * Definition of the contract for implementing listeners to receive [CacheEvent]s from a [Cache].
- */
-public fun interface CacheEventListener<Key : Any, Value : Any> {
- /**
- * Invoked on [CacheEvent] firing.
- *
- * Cache entry event firing behaviors for mutative methods:
- *
- * | Initial value | Operation | New value | Event |
- * |:-----------------|:-------------------------|:----------|:---------------------------------|
- * | {} | put(K, V) | {K: V} | Created(K, V) |
- * | {K: V1} | put(K, V2) | {K: V2} | Updated(K, V1, V2) |
- * | {K: V} | invalidate(K) | {} | Removed(K, V) |
- * | {K1: V1, K2: V2} | invalidateAll() | {} | Removed(K1, V1), Removed(K2, V2) |
- * | {K: V} | any operation, K expired | {} | Expired(K, V) |
- * | {K1: V1} | put(K2, V2), K1 evicted | {K2: V2} | Created(K2, V2), Evicted(K1, V1) |
- *
- */
- public fun onEvent(event: CacheEvent<Key, Value>)
-}
+++ /dev/null
-package io.github.reactivecircus.cache4k
-
-import java.util.concurrent.ConcurrentHashMap
-
-internal typealias ConcurrentMutableMap<Key, Value> = ConcurrentHashMap<Key, Value>
+++ /dev/null
-package io.github.reactivecircus.cache4k
-
-import kotlinx.atomicfu.AtomicLong
-import kotlinx.atomicfu.atomic
-import kotlinx.atomicfu.update
-import kotlin.time.AbstractLongTimeSource
-import kotlin.time.Duration
-import kotlin.time.DurationUnit
-
-/**
- * A time source that has programmatically updatable readings with support for multi-threaded access in Kotlin/Native.
- *
- * Implementation is identical to [kotlin.time.TestTimeSource] except the internal [reading] is an [AtomicLong].
- */
-public class FakeTimeSource : AbstractLongTimeSource(unit = DurationUnit.NANOSECONDS) {
-
- private val reading = atomic(0L)
-
- override fun read(): Long = reading.value
-
- /**
- * Advances the current reading value of this time source by the specified [duration].
- *
- * [duration] value is rounded down towards zero when converting it to a [Long] number of nanoseconds.
- * For example, if the duration being added is `0.6.nanoseconds`, the reading doesn't advance because
- * the duration value is rounded to zero nanoseconds.
- *
- * @throws IllegalStateException when the reading value overflows as the result of this operation.
- */
- public operator fun plusAssign(duration: Duration) {
- val delta = duration.toDouble(unit)
- val longDelta = delta.toLong()
- reading.update { currentReading ->
- if (longDelta != Long.MIN_VALUE && longDelta != Long.MAX_VALUE) {
- // when delta fits in long, add it as long
- val newReading = currentReading + longDelta
- if (currentReading xor longDelta >= 0 && currentReading xor newReading < 0) {
- overflow(duration)
- }
- newReading
- } else {
- // when delta is greater than long, add it as double
- val newReading = currentReading + delta
- if (newReading > Long.MAX_VALUE || newReading < Long.MIN_VALUE) {
- overflow(duration)
- }
- newReading.toLong()
- }
- }
- }
-
- private fun overflow(duration: Duration) {
- throw IllegalStateException(
- "FakeTimeSource will overflow if its reading ${reading}ns is advanced by $duration."
- )
- }
-}
+++ /dev/null
-package io.github.reactivecircus.cache4k
-
-import kotlinx.atomicfu.locks.reentrantLock
-import kotlinx.atomicfu.locks.withLock
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-
-/**
- * Provides a mechanism for performing key-based synchronization.
- */
-internal class KeyedSynchronizer<Key : Any> {
-
- private val keyBasedMutexes = ConcurrentMutableMap<Key, MutexEntry>()
-
- private val mapLock = reentrantLock()
-
- /**
- * Executes the given [action] under a mutex associated with the [key].
- * When called concurrently, all actions associated with the same [key] are mutually exclusive.
- */
- suspend fun <T> synchronizedFor(key: Key, action: suspend () -> T): T {
- return getMutex(key).withLock {
- try {
- action()
- } finally {
- removeMutex(key)
- }
- }
- }
-
- /**
- * Try to get a [MutexEntry] for the given [key] from the map.
- * If one cannot be found, create a new [MutexEntry], save it to the map, and return it.
- */
- private fun getMutex(key: Key): Mutex {
- mapLock.withLock {
- val mutexEntry = keyBasedMutexes[key] ?: MutexEntry(Mutex(), 0)
- // increment the counter to indicate a new thread is using the lock
- mutexEntry.counter++
- // save the lock entry to the map if it has just been created
- if (keyBasedMutexes[key] == null) {
- keyBasedMutexes.put(key, mutexEntry)
- }
-
- return mutexEntry.mutex
- }
- }
-
- /**
- * Remove the [MutexEntry] associated with the given [key] from the map
- * if no other thread is using the mutex.
- */
- private fun removeMutex(key: Key) {
- mapLock.withLock {
- // decrement the counter to indicate the lock is no longer needed for this thread,
- // then remove the lock entry from map if no other thread is still holding this lock
- val mutexEntry = keyBasedMutexes[key] ?: return
- mutexEntry.counter--
- if (mutexEntry.counter == 0) {
- keyBasedMutexes.remove(key)
- }
- }
- }
-}
-
-private class MutexEntry(
- val mutex: Mutex,
- var counter: Int
-)
+++ /dev/null
-package io.github.reactivecircus.cache4k
-
-import co.touchlab.stately.collections.IsoMutableSet
-import kotlinx.atomicfu.AtomicRef
-import kotlinx.atomicfu.atomic
-import kotlinx.atomicfu.update
-import kotlin.time.Duration
-import kotlin.time.TimeMark
-import kotlin.time.TimeSource
-
-/**
- * A Kotlin Multiplatform [Cache] implementation powered by touchlab/Stately.
- *
- * Two types of evictions are supported:
- *
- * 1. Time-based evictions (expiration)
- * 2. Size-based evictions
- *
- * Time-based evictions are enabled by specifying [expireAfterWriteDuration] and/or [expireAfterAccessDuration].
- * When [expireAfterWriteDuration] is specified, entries will be automatically removed from the cache
- * once a fixed duration has elapsed after the entry's creation
- * or most recent replacement of its value.
- * When [expireAfterAccessDuration] is specified, entries will be automatically removed from the cache
- * once a fixed duration has elapsed after the entry's creation,
- * the most recent replacement of its value, or its last access.
- *
- * Note that creation and replacement of an entry is also considered an access.
- *
- * Size-based evictions are enabled by specifying [maxSize]. When the size of the cache entries grows
- * beyond [maxSize], least recently accessed entries will be evicted.
- */
-internal class RealCache<Key : Any, Value : Any>(
- val expireAfterWriteDuration: Duration,
- val expireAfterAccessDuration: Duration,
- val maxSize: Long,
- val timeSource: TimeSource,
- private val eventListener: CacheEventListener<Key, Value>?,
-) : Cache<Key, Value> {
-
- private val cacheEntries = ConcurrentMutableMap<Key, CacheEntry<Key, Value>>()
-
- /**
- * Whether to perform size based evictions.
- */
- private val evictsBySize = maxSize >= 0
-
- /**
- * Whether to perform write-time based expiration.
- */
- private val expiresAfterWrite = expireAfterWriteDuration.isFinite()
-
- /**
- * Whether to perform access-time (both read and write) based expiration.
- */
- private val expiresAfterAccess = expireAfterAccessDuration.isFinite()
-
- /**
- * A key-based synchronizer for running cache loaders.
- */
- private val loadersSynchronizer = KeyedSynchronizer<Key>()
-
- /**
- * A queue of unique cache entries ordered by write time.
- * Used for performing write-time based cache expiration.
- */
- private val writeQueue: IsoMutableSet<CacheEntry<Key, Value>>? =
- takeIf { expiresAfterWrite }?.let {
- ReorderingIsoMutableSet()
- }
-
- /**
- * A queue of unique cache entries ordered by access time.
- * Used for performing both write-time and read-time based cache expiration
- * as well as size-based eviction.
- *
- * Note that a write is also considered an access.
- */
- private val accessQueue: IsoMutableSet<CacheEntry<Key, Value>>? =
- takeIf { expiresAfterAccess || evictsBySize }?.let {
- ReorderingIsoMutableSet()
- }
-
- override fun get(key: Key): Value? {
- return cacheEntries[key]?.let {
- if (it.isExpired()) {
- // clean up expired entries and return null
- expireEntries()
- null
- } else {
- // update eviction metadata
- recordRead(it)
- it.value.value
- }
- }
- }
-
- override suspend fun get(key: Key, loader: suspend () -> Value): Value {
- return loadersSynchronizer.synchronizedFor(key) {
- cacheEntries[key]?.let {
- if (it.isExpired()) {
- // clean up expired entries
- expireEntries()
- null
- } else {
- // update eviction metadata
- recordRead(it)
- it.value.value
- }
- } ?: loader().let { loadedValue ->
- val existingValue = get(key)
- if (existingValue != null) {
- existingValue
- } else {
- put(key, loadedValue)
- loadedValue
- }
- }
- }
- }
-
- override fun put(key: Key, value: Value) {
- expireEntries()
-
- val existingEntry = cacheEntries[key]
- val oldValue = existingEntry?.value?.value
- if (existingEntry != null) {
- // cache entry found
- recordWrite(existingEntry)
- existingEntry.value.value = value
- } else {
- // create a new cache entry
- val nowTimeMark = timeSource.markNow()
- val newEntry = CacheEntry(
- key = key,
- value = atomic(value),
- accessTimeMark = atomic(nowTimeMark),
- writeTimeMark = atomic(nowTimeMark),
- )
- recordWrite(newEntry)
- cacheEntries.put(key, newEntry)
- }
- onEvent(
- oldValue?.let {
- CacheEvent.Updated(key = key, oldValue = it, newValue = value)
- } ?: CacheEvent.Created(key = key, value = value)
- )
-
- evictEntries()
- }
-
- override suspend fun processAtomic(key: Key, loader: suspend (Value?) -> Value?): Value? {
- return loadersSynchronizer.synchronizedFor(key) {
- val previous = cacheEntries[key]?.let {
- if (it.isExpired()) {
- // clean up expired entries
- expireEntries()
- null
- } else {
- // update eviction metadata
- recordRead(it)
- it.value.value
- }
- }
-
- val updated = loader(previous)
- if (updated == null) {
- if (previous != null) invalidate(key)
- null
- } else {
- if (previous !== updated) put(key, updated)
- updated
- }
- }
- }
-
- override fun invalidate(key: Key) {
- expireEntries()
- cacheEntries.remove(key)?.also {
- writeQueue?.remove(it)
- accessQueue?.remove(it)
- onEvent(
- CacheEvent.Removed(
- key = it.key,
- value = it.value.value,
- )
- )
- }
- }
-
- override fun invalidateAll() {
- if (eventListener != null) {
- cacheEntries.values.forEach { entry ->
- onEvent(
- CacheEvent.Removed(
- key = entry.key,
- value = entry.value.value,
- )
- )
- }
- }
- cacheEntries.clear()
- writeQueue?.clear()
- accessQueue?.clear()
- }
-
- override fun asMap(): Map<in Key, Value> {
- return cacheEntries.values.associate { entry ->
- entry.key to entry.value.value
- }
- }
-
- /**
- * Remove all expired entries.
- */
- private fun expireEntries() {
- val queuesToProcess = listOfNotNull(
- if (expiresAfterWrite) writeQueue else null,
- if (expiresAfterAccess) accessQueue else null
- )
-
- queuesToProcess.forEach { queue ->
- queue.access {
- val iterator = queue.iterator()
- for (entry in iterator) {
- if (entry.isExpired()) {
- cacheEntries.remove(entry.key)
- // remove the entry from the current queue
- iterator.remove()
- onEvent(
- CacheEvent.Expired(
- key = entry.key,
- value = entry.value.value,
- )
- )
- } else {
- // found unexpired entry, no need to look any further
- break
- }
- }
- }
- }
- }
-
- /**
- * Check whether the [CacheEntry] has expired based on either access time or write time.
- */
- private fun CacheEntry<Key, Value>.isExpired(): Boolean {
- return expiresAfterAccess && (accessTimeMark.value + expireAfterAccessDuration).hasPassedNow() ||
- expiresAfterWrite && (writeTimeMark.value + expireAfterWriteDuration).hasPassedNow()
- }
-
- /**
- * Evict least recently accessed entries until [cacheEntries] is no longer over capacity.
- */
- private fun evictEntries() {
- if (!evictsBySize) {
- return
- }
-
- checkNotNull(accessQueue)
-
- while (cacheEntries.size > maxSize) {
- accessQueue.access {
- it.firstOrNull()?.run {
- cacheEntries.remove(key)
- writeQueue?.remove(this)
- accessQueue.remove(this)
- onEvent(
- CacheEvent.Evicted(
- key = key,
- value = value.value,
- )
- )
- }
- }
- }
- }
-
- /**
- * Update the eviction metadata on the [cacheEntry] which has just been read.
- */
- private fun recordRead(cacheEntry: CacheEntry<Key, Value>) {
- if (expiresAfterAccess) {
- val accessTimeMark = cacheEntry.accessTimeMark.value
- cacheEntry.accessTimeMark.update { accessTimeMark + accessTimeMark.elapsedNow() }
- }
- accessQueue?.add(cacheEntry)
- }
-
- /**
- * Update the eviction metadata on the [CacheEntry] which is about to be written.
- * Note that a write is also considered an access.
- */
- private fun recordWrite(cacheEntry: CacheEntry<Key, Value>) {
- if (expiresAfterAccess) {
- val accessTimeMark = cacheEntry.accessTimeMark.value
- cacheEntry.accessTimeMark.update { (accessTimeMark + accessTimeMark.elapsedNow()) }
- }
- if (expiresAfterWrite) {
- val writeTimeMark = cacheEntry.writeTimeMark.value
- cacheEntry.writeTimeMark.update { (writeTimeMark + writeTimeMark.elapsedNow()) }
- }
- accessQueue?.add(cacheEntry)
- writeQueue?.add(cacheEntry)
- }
-
- private fun onEvent(event: CacheEvent<Key, Value>) {
- eventListener?.onEvent(event)
- }
-}
-
-/**
- * A cache entry holds the [key] and [value] pair,
- * along with the metadata needed to perform cache expiration and eviction.
- */
-private class CacheEntry<Key : Any, Value : Any>(
- val key: Key,
- val value: AtomicRef<Value>,
- val accessTimeMark: AtomicRef<TimeMark>,
- val writeTimeMark: AtomicRef<TimeMark>,
-)
+++ /dev/null
-package io.github.reactivecircus.cache4k
-
-import co.touchlab.stately.collections.IsoMutableSet
-
-/**
- * A custom [IsoMutableSet] that updates the insertion order when an element is re-inserted,
- * i.e. an inserted element will always be placed at the end
- * regardless of whether the element already exists.
- */
-internal class ReorderingIsoMutableSet<T> : IsoMutableSet<T>(), MutableSet<T> {
- override fun add(element: T): Boolean = access {
- val exists = remove(element)
- super.add(element)
- // respect the contract "true if this set did not already contain the specified element"
- !exists
- }
-}
rootProject.name = "factbooks"
-include("cache4k")
include("externals")
include("fontparser")
//include("fightgame")
import info.mechyrdia.data.FileStorage
import info.mechyrdia.data.StoragePath
-import io.github.reactivecircus.cache4k.Cache
import io.ktor.util.*
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import java.time.Instant
-import kotlin.time.Duration.Companion.hours
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicLong
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.math.max
val StoragePathAttributeKey = AttributeKey<StoragePath>("Mechyrdia.StoragePath")
abstract class FileDependentCache<T : Any> {
- private inner class Entry(updated: Instant, data: T) {
- var updated: Instant = updated
- private set
+ private inner class Entry(updated: Instant?, data: T?) {
+ private val updatedAtomic = AtomicLong((updated ?: Instant.MIN).toEpochMilli())
+ val updated: Instant
+ get() = Instant.ofEpochMilli(updatedAtomic.get())
- var data: T = data
- private set
+ private val dataAtomic = AtomicReference(data)
+ val data: T?
+ get() = dataAtomic.get()
- suspend fun updateIfNeeded(path: StoragePath): Entry? {
- val fileUpdated = FileStorage.instance.statFile(path)?.updated ?: return null
- if (updated < fileUpdated) {
- updated = fileUpdated
- data = processFile(path) ?: return null
+ private val updateLock = Mutex()
+
+ private fun clear() {
+ updatedAtomic.set(Instant.MIN.toEpochMilli())
+ dataAtomic.set(null)
+ }
+
+ suspend fun updateIfNeeded(path: StoragePath): Entry {
+ return updateLock.withLock {
+ FileStorage.instance.statFile(path)?.updated?.toEpochMilli()?.let { fileUpdated ->
+ if (updatedAtomic.getAndUpdate { max(it, fileUpdated) } < fileUpdated)
+ dataAtomic.set(processFile(path))
+ this
+ } ?: apply { clear() }
}
-
- return this
}
}
- private suspend fun Entry(path: StoragePath): Entry? {
- val (updated, data) = coroutineScope {
- val updated = async { FileStorage.instance.statFile(path)?.updated }
- val data = async { processFile(path) }
- updated.await() to data.await()
+ private val cacheLock = Mutex()
+ private val cache = ConcurrentHashMap<StoragePath, Entry>()
+
+ private suspend fun Entry(path: StoragePath) = cacheLock.withLock {
+ cache.computeIfAbsent(path) {
+ Entry(null, null)
}
-
- if (updated == null || data == null) return null
- return Entry(updated, data)
}
- private val cache = Cache.Builder<StoragePath, Entry>()
- .maximumCacheSize(160)
- .expireAfterAccess(36.hours)
- .build()
-
protected abstract suspend fun processFile(path: StoragePath): T?
suspend fun get(path: StoragePath): T? {
- return cache.processAtomic(path) { prev ->
- if (prev == null)
- Entry(path)
- else prev.updateIfNeeded(path)
- }?.data
+ return Entry(path).updateIfNeeded(path).data
}
}