Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

DataCache.kt
/**
 * Dracon - An IntelliJ-Pijul integration.
 * Copyright 2021 JonathanxD <jhrldev@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.github.jonathanxd.dracon.cache

import com.intellij.openapi.project.Project
import com.intellij.openapi.project.getProjectDataPath
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.time.Instant
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.locks.ReentrantLock
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream

/**
 * Denotes the current version of data cache, must be updated when data cache format changes.
 */
const val DATA_CACHE_VERSION: Long = 3

@Suppress("UnstableApiUsage")
class DataCache<K: Any, V: Any>(val project: Project,
                                val name: String) {

    private val cachePath = project.getProjectDataPath("com.github.jonathanxd.dracon")
    private val cacheFile = cachePath.resolve("$name.gz")
    private val manager = DataCacheManager<K, CachedValue<V>>(this.cacheFile, DATA_CACHE_VERSION)
    private val lock = ReentrantLock()
    private val updateExecutor = Executors.newSingleThreadExecutor()

    private val inMemory = ConcurrentHashMap<K, CachedValue<V>>()

    init {
        this.manager.load()?.let {
            this.inMemory.putAll(it)
        }
    }

    fun lock() {
        this.lock.lock()
    }

    fun unlock() {
        this.lock.unlock()
    }

    fun unload(key: K) {
        this.lock.lock()
        try {
            this.inMemory.remove(key)
            this.manager.write(this.inMemory)
        } finally {
            this.lock.unlock()
        }
    }

    fun queryOrLoad(key: K, compute: (K) -> V): V {
        this.lock.lock()
        try {
            return if (!this.inMemory.containsKey(key)) {
                val computeEntry = CachedValue(Instant.now(), compute(key))

                this.inMemory[key] = computeEntry
                this.manager.write(this.inMemory)
                computeEntry
            } else {
                this.inMemory[key]!!
            }.value
        } finally {
            this.lock.unlock();
        }
    }

    fun <I> queryOrLoad(key: K,
                        compute: (K) -> I,
                        filter: (I) -> Boolean,
                        iMapper: (I) -> V,
                        vMapper: (V) -> I): I {
        this.lock.lock()
        try {
            return if (!this.inMemory.containsKey(key)) {
                val computeEntry = compute(key)

                if (!filter(computeEntry)) {
                    return computeEntry
                }

                this.inMemory[key] = CachedValue(Instant.now(), iMapper(computeEntry!!))
                this.manager.write(this.inMemory)
                computeEntry
            } else {
                vMapper(this.inMemory[key]!!.value)
            }
        } finally {
            this.lock.unlock();
        }
    }

    fun queryOrLoadAsync(key: K, compute: (K) -> V): CompletableFuture<V> {
        if (this.inMemory.containsKey(key)) {
            return CompletableFuture.completedFuture(this.inMemory[key]!!.value)
        } else {
            return CompletableFuture.supplyAsync({
                this.lock.lock()
                try {
                    val computeEntry = CachedValue(Instant.now(), compute(key))

                    this.inMemory[key] = computeEntry
                    this.manager.write(this.inMemory)
                    computeEntry
                } finally {
                    this.lock.unlock()
                }.value
            }, this.updateExecutor)
        }
    }

    fun queryOrLoadAsyncExpanded(key: K, compute: (K) -> Pair<V, List<Pair<K, V>>>): CompletableFuture<V> {
        if (this.inMemory.containsKey(key)) {
            return CompletableFuture.completedFuture(this.inMemory[key]!!.value)
        } else {
            return CompletableFuture.supplyAsync({
                this.lock.lock()
                try {
                    if (this.inMemory.containsKey(key)) {
                        return@supplyAsync this.inMemory[key]!!.value
                    }

                    val instant = Instant.now()
                    val computed = compute(key)
                    val computeEntry = CachedValue(instant, computed.first)

                    this.inMemory[key] = computeEntry

                    computed.second.forEach { (k, v) ->
                        this.inMemory[k] = CachedValue(instant, v)
                    }

                    this.manager.write(this.inMemory)
                    computeEntry
                } finally {
                    this.lock.unlock()
                }.value
            }, this.updateExecutor)
        }
    }

    fun <I> queryOrLoadAsync(key: K,
                             compute: (K) -> I,
                             filter: (I) -> Boolean,
                             iMapper: (I) -> V,
                             vMapper: (V) -> I): CompletableFuture<I> {
        if (this.inMemory.containsKey(key)) {
            return CompletableFuture.completedFuture(vMapper(this.inMemory[key]!!.value))
        } else {
            return CompletableFuture.supplyAsync({
                this.lock.lock()
                try {
                    val computeEntry = compute(key)

                    if (!filter(computeEntry)) {
                        computeEntry
                    } else {
                        this.inMemory[key] = CachedValue(Instant.now(), iMapper(computeEntry!!))
                        this.manager.write(this.inMemory)
                        computeEntry
                    }
                } finally {
                    this.lock.unlock()
                }
            }, this.updateExecutor)
        }
    }

    /**
     * @see CacheService.invalidate
     */
    fun invalidate() {
        this.lock.lock()
        try {
            this.inMemory.clear()
            this.manager.write(this.inMemory)
        } finally {
            this.lock.unlock();
        }
    }

    /**
     * @see CacheService.invalidate
     */
    fun invalidate(key: K) {
        this.lock.lock()
        try {
            this.inMemory.remove(key)
            this.manager.write(this.inMemory)
        } finally {
            this.lock.unlock();
        }
    }

    /**
     * @see CacheService.updateCache
     */
    fun updateCache(key: K, newValueCompute: () -> V) {
        val instant = Instant.now()
        this.lock()
        try {
            val inMemoryValue = this.inMemory[key]
            if (inMemoryValue == null || inMemoryValue.instant.isBefore(instant)) {
                this.inMemory[key] = CachedValue(instant, newValueCompute())
            }
        } finally {
            this.unlock()
        }
    }

    /**
     * @see CacheService.updateCache
     */
    fun updateCache(keys: List<K>, newValueCompute: (K) -> V) {
        val instant = Instant.now()
        this.lock()
        try {
            for (key in keys) {
                val inMemoryValue = this.inMemory[key]
                if (inMemoryValue == null || inMemoryValue.instant.isBefore(instant)) {
                    this.inMemory[key] = CachedValue(instant, newValueCompute(key))
                }
            }
        } finally {
            this.unlock()
        }
    }

}

// Do async, accept MISS while locked
// cache storage is very slow due to compression
class DataCacheManager<K, V>(val path: Path, val version: Long) {

    init {
        Files.createDirectories(this.path.parent)
    }

    fun load(): Map<K, V>? {
        if (!Files.exists(this.path)) return null
        try {
            Files.newInputStream(this.path).use { stream ->
                GZIPInputStream(stream).use { gz ->
                    ObjectInputStream(gz).use { reader ->
                        val version = reader.readLong()
                        return if (version != this.version) {
                            emptyMap()
                        } else {
                            reader.readObject() as Map<K, V>
                        }
                    }
                }
            }
        } catch (t: Throwable) {
            t.printStackTrace()
            Files.delete(this.path)
            return null
        }
    }

    fun write(data: Map<K, V>) {
        Files.newOutputStream(this.path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING).use { writer ->
            GZIPOutputStream(writer).use { compress ->
                ObjectOutputStream(compress).use { oos ->
                    oos.writeLong(this.version)
                    oos.writeObject(data)
                }
            }
        }
    }
}