{"id":19882808,"url":"https://github.com/reactivecircus/cache4k","last_synced_at":"2025-04-09T05:12:42.064Z","repository":{"id":39762600,"uuid":"298499491","full_name":"ReactiveCircus/cache4k","owner":"ReactiveCircus","description":"In-memory Cache for Kotlin Multiplatform.","archived":false,"fork":false,"pushed_at":"2024-09-18T01:53:46.000Z","size":5851,"stargazers_count":267,"open_issues_count":9,"forks_count":20,"subscribers_count":10,"default_branch":"main","last_synced_at":"2024-09-18T05:34:14.842Z","etag":null,"topics":["android","cache","ios","javascript","kotlin-multiplatform","macos"],"latest_commit_sha":null,"homepage":"https://reactivecircus.github.io/cache4k/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ReactiveCircus.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-09-25T07:25:01.000Z","updated_at":"2024-09-18T01:51:07.000Z","dependencies_parsed_at":"2023-02-14T07:01:04.597Z","dependency_job_id":"627e1190-99e7-41f0-a04e-178862ded1d5","html_url":"https://github.com/ReactiveCircus/cache4k","commit_stats":{"total_commits":247,"total_committers":13,"mean_commits":19.0,"dds":"0.42105263157894735","last_synced_commit":"700a6cf097210bf5ac3be3ddc5e107fdc2e10160"},"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ReactiveCircus%2Fcache4k","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ReactiveCircus%2Fcache4k/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ReactiveCircus%2Fcache4k/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ReactiveCircus%2Fcache4k/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ReactiveCircus","download_url":"https://codeload.github.com/ReactiveCircus/cache4k/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247980844,"owners_count":21027808,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","cache","ios","javascript","kotlin-multiplatform","macos"],"created_at":"2024-11-12T17:18:34.175Z","updated_at":"2025-04-09T05:12:42.041Z","avatar_url":"https://github.com/ReactiveCircus.png","language":"Kotlin","readme":"# cache4k\n\n![CI](https://github.com/ReactiveCircus/cache4k/workflows/CI/badge.svg)\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.reactivecircus.cache4k/cache4k/badge.svg)](https://search.maven.org/search?q=g:io.github.reactivecircus.cache4k)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n\nIn-memory Cache for Kotlin Multiplatform.\n\n**Work in progress.**\n\n**cache4k** provides a simple in-memory key-value cache for **Kotlin Multiplatform**, with support for time-based (expiration) and size-based evictions.\n\n**Note that only the new Kotlin Native memory model is supported.**\n\nThe following targets are currently supported:\n\n- jvm\n- js\n- wasmJs\n- iosX64\n- iosArm64\n- iosSimulatorArm64\n- macosX64\n- macosArm64\n- tvosX64\n- tvosArm64\n- tvosSimulatorArm64\n- watchosX64\n- watchosArm64\n- watchosSimulatorArm64\n- linuxX64\n- linuxArm64\n- mingwX64\n\n## Download\n\nDependencies are hosted on [Maven Central](https://search.maven.org/artifact/io.github.reactivecircus.cache4k/cache4k).\n\n### Android\n\n```kotlin\ndependencies {\n    implementation(\"io.github.reactivecircus.cache4k:cache4k:x.y.z\")\n}\n```\n\n### Multiplatform\n\n```kotlin\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(\"io.github.reactivecircus.cache4k:cache4k:x.y.z\")\n            }\n        }\n    }\n}\n```\n\n## Usage\n\n### Writing and reading cache entries\n\nTo create a new `Cache` instance using `Long` for the key and `String` for the value:\n\n```kotlin\nval cache = Cache.Builder\u003cLong, String\u003e().build()\n```\n\nTo start writing entries to the cache:\n\n```kotlin\ncache.put(1, \"dog\")\ncache.put(2, \"cat\")\n```\n\nTo read a cache entry by key:\n\n```kotlin\ncache.get(1) // returns \"dog\"\ncache.get(2) // returns \"cat\"\ncache.get(3) // returns null\n```\n\nTo overwrite an existing cache entry:\n\n```kotlin\ncache.put(1, \"dog\")\ncache.put(1, \"bird\")\ncache.get(1) // returns \"bird\"\n```\n\n### Cache loader\n\n**Cache** provides an API for getting cached value by key and using the provided `loader: suspend () -\u003e Value` lambda to compute and cache the value automatically if none exists.\n\n```kotlin\nrunBlockingTest {\n    val cache = Cache.Builder\u003cLong, User\u003e().build()\n\n    val userId = 1L\n    val user = cache.get(userId) {\n        fetchUserById(userId) // potentially expensive call (might be a suspend function)\n    }\n\n    // value successfully computed by the loader will be cached automatically\n    assertThat(user).isEqualTo(cache.get(userId))\n}\n```\n\nNote that loader is executed on the caller's coroutine context. Concurrent calls from multiple threads using the same key will be blocked. Assuming the 1st call successfully computes a new value, none of the loader from the other calls will be executed and the cached value computed by the first loader will be returned for those calls.\n\nAny exceptions thrown by the `loader` will be propagated to the caller of this function.\n\n### Expirations and evictions\n\nBy default, **Cache** has an unlimited number of entries which never expire. But a cache can be configured to support both **time-based expirations** and **size-based evictions**.\n\n#### Time-based expiration\n\nExpiration time can be specified for entries in the cache.\n\n##### Expire after access\n\nTo set the maximum time an entry can live in the cache since the last access (also known as **\ntime-to-idle**), where \"access\" means **reading the cache**, **adding a new cache entry**, or **\nreplacing an existing entry with a new one**:\n\n```kotlin\nval cache = Cache.Builder\u003cLong, String\u003e()\n    .expireAfterAccess(24.hours)\n    .build()\n```\n\nAn entry in this cache will be removed if it has not been read or replaced **after 24 hours** since it's been written into the cache.\n\n##### Expire after write\n\nTo set the maximum time an entry can live in the cache since the last write (also known as **\ntime-to-live**), where \"write\" means **adding a new cache entry** or **replacing an existing entry with a new one**:\n\n```kotlin\nval cache = Cache.Builder\u003cLong, String\u003e()\n    .expireAfterWrite(30.minutes)\n    .build()\n```\n\nAn entry in this cache will be removed if it has not been replaced **after 30 minutes** since it's been written into the cache.\n\n_Note that cache entries are **not** removed immediately upon expiration at exact time. Expirations are checked in each interaction with the `cache`._\n\n### Size-based eviction\n\nTo set the maximum number of entries to be kept in the cache:\n\n```kotlin\nval cache = Cache.Builder\u003cLong, String\u003e()\n    .maximumCacheSize(100)\n    .build()\n```\n\nOnce there are more than **100** entries in this cache, the **least recently used one** will be removed, where \"used\" means **reading the cache**, **adding a new cache entry**, or **replacing an\nexisting entry with a new one**.\n\n### Getting all cache entries as a Map\n\nTo get a copy of the current cache entries as a `Map`:\n\n```kotlin\nval cache = Cache.Builder\u003cLong, String\u003e()\n    .build()\n\ncache.put(1, \"dog\")\ncache.put(2, \"cat\")\n\nassertThat(cache.asMap())\n    .isEqualTo(mapOf(1L to \"dog\", 2L to \"cat\"))\n```\n\n_Note that calling `asMap()` has no effect on the access expiry of the cache._\n\n### Deleting cache entries\n\nCache entries can also be deleted explicitly.\n\nTo delete a cache entry for a given key:\n\n```kotlin\nval cache = Cache.Builder\u003cLong, String\u003e().build()\ncache.put(1, \"dog\")\n\ncache.invalidate(1)\n\nassertThat(cache.get(1)).isNull()\n```\n\nTo delete all entries in the cache:\n\n```kotlin\ncache.invalidateAll()\n```\n\n### Event listener\n\nYou can set an event listener as a lambda:\n\n```kotlin\nval cache1 = Cache.Builder\u003cLong, String\u003e()\n    .eventListener { event -\u003e\n        println(\"onEvent: $event\")\n    }\n    .build()\n```\n\nOr declare it as a class and share logic across many stores:\n\n```kotlin\nclass FileDeleteEventListener : CacheEventListener\u003cLong, File\u003e {\n    override fun onEvent(event: CacheEvent\u003cLong, File\u003e) {\n        when(event) {\n            is CacheEvent.Created -\u003e {}\n            is CacheEvent.Updated -\u003e event.oldValue.delete()\n            is CacheEvent.Evicted -\u003e event.value.delete()\n            is CacheEvent.Expired -\u003e event.value.delete()\n            is CacheEvent.Removed -\u003e event.value.delete()\n        }\n    }\n}\nval fileDeleteEventListener = FileDeleteEventListener()\n\nval cache1 = Cache.Builder\u003cLong, File\u003e()\n    .eventListener(fileDeleteEventListener)\n    .build()\n\nval cache2 = Cache.Builder\u003cLong, File\u003e()\n    .eventListener(fileDeleteEventListener)\n    .build()\n```\n\nCache entry event firing behaviors for mutative methods:\n\n| Initial value    | Operation                | New value | Event                            |\n|:-----------------|:-------------------------|:----------|:---------------------------------|\n| {}               | put(K, V)                | {K: V}    | Created(K, V)                    |\n| {K: V1}          | put(K, V2)               | {K: V2}   | Updated(K, V1, V2)               |\n| {K: V}           | invalidate(K)            | {}        | Removed(K, V)                    |\n| {K1: V1, K2: V2} | invalidateAll()          | {}        | Removed(K1, V1), Removed(K2, V2) |\n| {K: V}           | any operation, K expired | {}        | Expired(K, V)                    |\n| {K1: V1}         | put(K2, V2), K1 evicted  | {K2: V2}  | Created(K2, V2), Evicted(K1, V1) |\n\n### Unit testing cache expirations\n\nTo test logic that depends on cache expiration, pass in a `FakeTimeSource` when building a `Cache`\nso you can programmatically advance the reading of the time source:\n\n```kotlin\n@Test\nfun cacheEntryEvictedAfterExpiration() {\n    private val fakeTimeSource = FakeTimeSource()\n    val cache = Cache.Builder\u003cLong, String\u003e()\n        .timeSource(fakeTimeSource)\n        .expireAfterWrite(1.minutes)\n        .build()\n\n    cache.put(1, \"dog\")\n\n    // just before expiry\n    fakeTimeSource += 1.minutes - 1.nanoseconds\n\n    assertThat(cache.get(1))\n        .isEqualTo(\"dog\")\n\n    // now expires\n    fakeTimeSource += 1.nanoseconds\n\n    assertThat(cache.get(1))\n        .isNull()\n}\n```\n\n## Credits\n\nThe library was ported from a kotlin / JVM cache which I contributed to [dropbox/Store](https://github.com/dropbox/Store) to help unblock Store's multiplatform support (\nit was reverted before the 1.0 release as multiplatform wasn't a priority). Many thanks to Store's owners and contributors for reviewing and improving the original implementation.\n\n## License\n\n```\nCopyright 2021 Yang Chen\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n\n-----\n\n![YourKit](https://www.yourkit.com/images/yklogo.png)\n\nYourKit supports open source projects with innovative and intelligent tools\nfor monitoring and profiling Java and .NET applications.\nYourKit is the creator of \u003ca href=\"https://www.yourkit.com/java/profiler/\"\u003eYourKit Java Profiler\u003c/a\u003e,\n\u003ca href=\"https://www.yourkit.com/.net/profiler/\"\u003eYourKit .NET Profiler\u003c/a\u003e,\nand \u003ca href=\"https://www.yourkit.com/youmonitor/\"\u003eYourKit YouMonitor\u003c/a\u003e.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freactivecircus%2Fcache4k","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Freactivecircus%2Fcache4k","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freactivecircus%2Fcache4k/lists"}