{"id":20492558,"url":"https://github.com/redmadrobot/mapmemory","last_synced_at":"2025-04-13T17:04:48.989Z","repository":{"id":52286653,"uuid":"310079685","full_name":"RedMadRobot/mapmemory","owner":"RedMadRobot","description":"Simple in-memory cache conception built on Map.","archived":false,"fork":false,"pushed_at":"2024-06-28T17:50:40.000Z","size":388,"stargazers_count":22,"open_issues_count":1,"forks_count":2,"subscribers_count":7,"default_branch":"main","last_synced_at":"2024-06-28T19:06:15.844Z","etag":null,"topics":["cache","in-memory","kotlin","libaray","map"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RedMadRobot.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-11-04T18:02:46.000Z","updated_at":"2024-06-28T17:50:45.000Z","dependencies_parsed_at":"2024-06-28T18:59:19.944Z","dependency_job_id":"e3dcdd15-bc90-4948-82b7-aae5769e84ee","html_url":"https://github.com/RedMadRobot/mapmemory","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RedMadRobot%2Fmapmemory","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RedMadRobot%2Fmapmemory/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RedMadRobot%2Fmapmemory/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RedMadRobot%2Fmapmemory/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RedMadRobot","download_url":"https://codeload.github.com/RedMadRobot/mapmemory/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224819479,"owners_count":17375274,"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":["cache","in-memory","kotlin","libaray","map"],"created_at":"2024-11-15T17:29:37.205Z","updated_at":"2024-11-15T17:29:38.171Z","avatar_url":"https://github.com/RedMadRobot.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"## MapMemory \u003cGitHub path=\"RedMadRobot/mapmemory\"/\u003e\n\n[![Version](https://img.shields.io/maven-central/v/com.redmadrobot.mapmemory/mapmemory?style=flat-square)][mavenCentral]\n[![Build Status](https://img.shields.io/github/actions/workflow/status/RedMadRobot/mapmemory/main.yml?branch=main\u0026style=flat-square)][ci]\n[![License](https://img.shields.io/github/license/RedMadRobot/mapmemory?style=flat-square)][license]\n\nSimple in-memory cache conception built on `Map`.\n\n---\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n\n- [Installation](#installation)\n- [Conception](#conception)\n- [Usage](#usage)\n  - [Collections](#collections)\n  - [Reusable properties](#reusable-properties)\n  - [Scoped and Shared values](#scoped-and-shared-values)\n  - [Reactive Style](#reactive-style)\n- [Advanced usage](#advanced-usage)\n  - [MapMemory Lifetime](#mapmemory-lifetime)\n  - [Testing](#testing)\n- [Migration Guide](#migration-guide)\n  - [Upgrading from v1.1](#upgrading-from-v11)\n- [Contributing](#contributing)\n- [License](#license)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n### Installation\n\nAdd dependencies:\n\n```kotlin\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation(\"com.redmadrobot.mapmemory:mapmemory:2.1\")\n\n    // or if you want to work with MapMemory in reactive style, add one of\n    implementation(\"com.redmadrobot.mapmemory:mapmemory-coroutines:2.1\")\n    implementation(\"com.redmadrobot.mapmemory:mapmemory-rxjava2:2.1\")\n    implementation(\"com.redmadrobot.mapmemory:mapmemory-rxjava3:2.1\")\n\n    // if you want to test code that uses MapMemory\n    testImplementation(\"com.redmadrobot.mapmemory:mapmemory-test:2.1\")\n}\n```\n\n### Conception\n\nKotlin provides delegates to access values in a map:\n\n```kotlin\nval map = mapOf(\"answer\" to 42)\nval answer: Int by map\nprintln(answer) // 42\n```\n\nThis library uses this idea to implement in-memory storage.\n\nThere are two simple principles:\n\n- **MapMemory** is a singleton, and it is shared between many consumers\n- **MapMemory** holds data but doesn't know **what** data it holds\n\n### Usage\n\n\u003e[!TIP]\n\u003e\n\u003eIf you use any kind of DI framework, you should provide `MapMemory` with the desired scope.\n\u003e For example, if you want your data to live forever, use singleton scope:\n\u003e\n\u003e ```kotlin\n\u003e @Provides\n\u003e @Singleton\n\u003e fun provideMapMemory(): MapMemory = MapMemory()\n\u003e ```\n\u003e\n\u003e If you don't use any DI framework, you should take care of the `MapMemory` lifetime.\n\nImagine, you have `UsersRepository` used to get users' information from API.\nYou want to remember the last requested user.\nLet's store it in a `MapMemory`:\n\n```kotlin\nclass UsersRepository(\n    private val api: Api,\n    memory: MapMemory,              // (1) Inject MapMemory into the constructor\n) {\n\n    var lastUser: User? by memory   // (2) Declare in-memory property using delegate\n        private set\n\n    suspend fun getUser(email: String): User {\n        return api.getUser(email)\n            .also { lastUser = it } // (3) Use the property\n    }\n}\n```\n\n`MapMemory` is a singleton, but `UsersRepository` is not.\nProperty `lastUser` is tied to `MapMemory` lifetime, so it will survive `UsersRepository` recreation.\n\nYou can specify the default value that will be used when the value you're trying to read is not set.\nFor example, we don't want a nullable `User`, but want to get placeholder object `User.EMPTY` instead:\n\n```kotlin\nvar lastUser: User by memory { User.EMPTY }\n```\n\n#### Collections\n\nYou can write the following code to store a mutable list in `MapMemory`:\n\n```kotlin\nval users: MutableList\u003cUser\u003e by memory { mutableListOf() }\n```\n\nBoilerplate.\nFortunately, there are shorthand accessors to store lists and maps:\n\n```kotlin\nval users by memory.mutableList\u003cUser\u003e()\n```\n\nAccessors `mutableList` and `mutableMap` use concurrent collections under the hood.\n\n| Accessor        | Default value      | Description           |\n|-----------------|--------------------|-----------------------|\n| `map()`         | Empty map          | Store map             |\n| `mutableMap()`  | Empty mutable map  | Store values in map   |\n| `list()`        | Empty list         | Store list            |\n| `mutableList()` | Empty mutable list | Store values in list  |\n\nFeel free to create your accessors if needed.\n\n#### Reusable properties\n\nIf you don't want some value to be removed from memory on [MapMemory.clear] and want to clear the value instead, you can create a reusable property.\nSuch properties use the given `clear` lambda to clear the current value.\n\n```kotlin\nclass Counter {\n    fun reset() { /*...*/\n    }\n}\n\nval counter: Counter by memory(clear = { it.reset() }) { Counter() }\n```\n\nReusable properties are especially useful for reactive types like `Flow` because you don't need to re-subscribe to `Flow` after `MapMemory` is cleared.\n\n\u003e [!NOTE] \n\u003e\n\u003e Many of the default accessors already return reusable properties.\n\u003e See the accessor's description to check if it returns reusable property.\n\n#### Scoped and Shared values\n\nLet's look at how MapMemory works under the hood.\nWe have a class with an in-memory property declared using delegate:\n\n```kotlin\npackage com.example\n\nclass TokenStorage(memory: MapMemory) {\n    var authToken: String by memory\n}\n```\n\n`MapMemory` is `MutableMap\u003cString, Any\u003e`.\nDelegate accesses map value by a key retrieved from the property name.\nThis behavior differs for two types of in-memory property delegates:\n- **Scoped** to the class where the property is declared.\n  Property key is a combination of class and property name: `com.example.TokenStorage#authToken`\n- **Shared** between all classes by the specified key.\n  All properties are scoped by default, you can share it with the function `shared`.\n\nProperty `authToken` is scoped to class `TokenStorage`, but we can share it:\n\n```kotlin\n// It is a good practice to declare constants for shared keys.\nconst val KEY_AUTH_TOKEN = \"authToken\"\n\nclass TokenStorage(memory: MapMemory) {\n    var authToken: String by memory.shared(KEY_AUTH_TOKEN)\n}\n\nclass Authenticator(memory: MapMemory) {\n    // Property name may be different\n    var savedToken: String by memory.shared(KEY_AUTH_TOKEN)\n}\n```\n\nBoth `TokenStorage` and `Authenticator` will use the same value.\n\n\u003e [!Warning]\n\u003e \n\u003e Keep in mind that this is just an example.\n\u003e In real code, it may be more reasonable to inject `TokenStorage` into `Authenticator` instead of sharing in-memory property by key.\n\n#### Reactive Style\n\nReactive subscription to values is useful to keep data shared between several screens up to date.\n\nTo use MapMemory in reactive style, replace dependency `mapmemory` with one of the following:\n\n- `mapmemory-coroutines`\n- `mapmemory-rxjava2`\n- `mapmemory-rxjava3`\n\nThese modules provide accessors for reactive types:\n\n```kotlin\n// with coroutines\nval selectedOption: MutableStateFlow\u003cOption\u003e by memory.stateFlow(Option.DEFAULT)\n\n// with RxJava\nval selectedOption: BehaviorSubject\u003cOption\u003e by memory.behaviorSubject()\n```\n\n\u003e [!WARNING]\n\u003e\n\u003e You can use only one of these dependencies at the same time\n\u003e Otherwise build will fail due to duplicates in the classpath.\n\nMapMemory provides the type `ReactiveMutableMap`.\nIt works similarly to `MutableMap` but enables you to observe data reactively.\nThere are methods to observe one or all map values.\nYou can implement a cache-first approach using `ReactiveMutableMap`:\n\n\u003cdetails open\u003e\n  \u003csummary\u003eCoroutines\u003c/summary\u003e\n\n  ```kotlin\n  class UsersRepository(\n      api: Api,\n      memory: MapMemory,\n  ) {\n      private val usersCache by memory.reactiveMutableMap\u003cString, User\u003e()\n  \n      /** Returns stream of users from cache. */\n      fun getUsersFlow(): Flow\u003cList\u003cUser\u003e\u003e = usersCache.valuesFlow\n  \n      /** Returns stream of one user from cache. */\n      fun getUserFlow(id: String): Flow\u003cUser\u003e = usersCache.getValueFlow(id)\n  \n      /** Update users in cache. */\n      suspend fun fetchUsers() {\n          val users: List\u003cUser\u003e = api.getUsers()\n          usersCache.replaceAll(users.associateBy { it.id })\n      }\n  }\n  ```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eJxJava\u003c/summary\u003e\n\n  ```kotlin\n  class UsersRepository(\n      api: Api,\n      memory: MapMemory,\n  ) {\n      private val usersCache by memory.reactiveMutableMap\u003cString, User\u003e()\n  \n      /** Returns stream of users from cache. */\n      fun getUsersObservable(): Observable\u003cList\u003cUser\u003e\u003e = usersCache.valuesObservable\n  \n      /** Returns stream of one user from cache. */\n      fun getUserObservable(id: String): Observable\u003cUser\u003e = usersCache.getValueObservable(id)\n  \n      /** Update users in cache. */\n      fun fetchUsers() {\n          val users: List\u003cUser\u003e = api.getUsers()\n          usersCache.replaceAll(users.associateBy { it.id })\n      }\n  }\n  ```\n\n\u003c/details\u003e\n\n##### Coroutines\n\n`mapmemory-coroutines` add accessors for coroutines types:\n\n| Accessor               | Default value                           | Description                      |\n|------------------------|-----------------------------------------|----------------------------------|\n| `stateFlow()`          | StateFlow with specified `initialValue` | Store stream of values           |\n| `sharedFlow()`         | Empty flow                              | Store stream of values           |\n| `reactiveMutableMap()` | Empty map                               | Store values in **reactive map** |\n\n\u003e [!NOTE]\n\u003e\n\u003e Coroutines implementation of reactive map uses `SharedFlow` under the hood, so it will be triggered even if its content has not been changed.\n\n##### RxJava\n\n`mapmemory-rxjava2` and `mapmemory-rxjava3` adds accessors for RxJava types:\n\n| Accessor               | Default value   | Description                         |\n|------------------------|-----------------|-------------------------------------|\n| `behaviorSubject()`    | Empty subject   | Store stream of values              |\n| `publishSubject()`     | Empty subject   | Store stream of values              |\n| `maybe()`              | `Maybe.empty()` | Reactive analog to store \"nullable\" |\n| `reactiveMutableMap()` | Empty map       | Store values in **reactive map**    |\n\n### Advanced usage\n\n#### MapMemory Lifetime\n\nIt may be useful to create `MapMemory` instances with a different lifetime.\nYou can use it to control the lifetime of the data stored within.\n\n```kotlin\n/** MapMemory, available during a session and cleared on logout. */\n@Singleton\nclass SessionMemory @Inject constructor() : MapMemory()\n\n/** MapMemory, available during the app lifetime. */\n@Singleton\nclass AppMemory @Inject constructor() : MapMemory()\n```\n\nKeep in mind that you should manually clear `SessionMemory` on logout.\n\n\u003e [!TIP]\n\u003e Instead of creating subclasses, you can provide MapMemory with [qualifiers].\n\n##### KAPT: 'IllegalStateException: Couldn't find declaration file' on delegate with inline getValue operator\n\n\u003e [!NOTE]\n\u003e This bug was fixed in Kotlin 1.8.20. Consider updating to the newest Kotlin.\n\nThere is the bug in Kotlin Compiler that affects MapMemory if you create subclasses - [KT-46317](https://youtrack.jetbrains.com/issue/KT-46317).\nYou can use the module `mapmemory-kapt-bug-workaround` as a workaround:\n\n```kotlin\ndependencies {\n    implementation(\"com.redmadrobot.mapmemory:mapmemory-kapt-bug-workaround:[latest-version]\")\n}\n```\n\n```diff\n- val someValue: String by memory\n+ val someValue: String by memory.value()\n```\n\n#### Testing\n\nModule `mapmemory-test` provides utilities helping to test code that uses MapMemory.\n\nImagine you want to build memory filled with mock data for the following class:\n\n```kotlin\npackage com.example\n\nclass UserCache(memory: MapMemory) {\n    var name: String by memory\n    var ages: Int by memory\n}\n```\n\nYou can put it by key:\n\n```kotlin\nval memory = MapMemory()\nmemory[\"com.example.UserCache#name\"] = \"John Doe\"\nmemory[\"com.example.UserCache#ages\"] = 42\n```\n\nIt is easy to make a mistake and this approach requires knowing how MapMemory works under the hood.\nUsing `mapMemoryOf` and `scopedKeyOf` you can build mock `MapMemory` much easier:\n\n```kotlin\nval memory = mapMemoryOf(\n    scopedKeyOf(UserCache::name) to \"John Doe\",\n    scopedKeyOf(UserCache::ages) to 42,\n)\n```\n\nYou can also get or set scoped values using type-safe functions `putScoped` and `getScoped`:\n\n```kotlin\nmemory.putScoped(UserCache::name, \"Jane Doe\")\nmemory.getScoped(UserCache::name)\n```\n\nThere is an alternate syntax to use if properties in class are private and can't be accessed via reference:\n\n```kotlin\nscopedKeyOf\u003cUserStorage\u003e(\"name\")\nmemory.putScoped\u003cUserStorage\u003e(\"name\", \"Jane Doe\")\nmemory.getScoped\u003cUserStorage\u003e(\"name\")\n```\n\n### Migration Guide\n\n#### Upgrading from v1.1\n\n\u003e [!NOTE]  \n\u003e To make an upgrade to the latest version easier, you should:\n\u003e\n\u003e 1. Upgrade to v2.0\n\u003e 2. Resolve all deprecations\n\u003e 3. Upgrade to the latest version\n\n##### Potentially breaking changes\n\n**Collections accessors**\n\nNow accessors `map` and `list` return delegates to access immutable collections.\nUse `mutableMap` and `mutableList` for mutable versions of collections.\n\n**Closed access to getOrPutProperty**\n\nExtension `getOrPutProperty` became internal (it was already in the `internal` package), use the operator `MapMemory.invoke` instead.\n\n```diff\n-var counter: Int by memory.getOrPutProperty { 0 }\n+var counter: Int by memory { 0 }\n```\n\n**Scoped and Shared values**\n\nRead [\"Scoped and Shared values\"](#scoped-and-shared-values) section.\nIf you are sharing properties between classes by name, you should specify the sharing key explicitly.\n\n##### API Changes\n\n**Accessor `.nullable()` is deprecated**\n\nAccessor `nullable()` is not needed now.\nYou can simply declare a nullable field:\n\n```diff\n-val selectedOption: String? by memory.nullable()\n+val selectedOption: String? by memory\n```\n\n**`.withDefault { ... }` is banned from use**\n\n`withDefault` is no longer compatible with MapMemory, so you should use the operator `invoke` instead:\n\n```diff\n-var counter: Int by memory.withDefault { 0 }\n+var counter: Int by memory { 0 }\n```\n\n##### ReactiveMap -\u003e ReactiveMutableMap\n\n```diff\n-var users by memory.reactiveMap\u003cUser\u003e()\n+var users by memory.reactiveMutableMap\u003cString, User\u003e()\n```\n\n**Naming changes**\n\nThe word `stream` in method names was replaced with implementation-specific words to clarify the API.\n\nCoroutines:\n- `getStream` -\u003e `getFlow` and `getValueFlow`\n- `getAllStream` -\u003e `valuesFlow`\n\nRxJava:\n- `getStream` -\u003e `getValueObservable`\n- `getAllStream` -\u003e `valuesObservable`\n\n### Contributing\n\nMerge requests are welcome.\nFor major changes, please open an issue first to discuss what you would like to change.\n\n## License\n\n[MIT][license]\n\n[mavenCentral]: https://search.maven.org/search?q=g:com.redmadrobot.mapmemory\n[ci]: https://github.com/RedMadRobot/mapmemory/actions\n[qualifiers]: https://dagger.dev/dev-guide/#qualifiers\n[license]: LICENSE\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fredmadrobot%2Fmapmemory","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fredmadrobot%2Fmapmemory","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fredmadrobot%2Fmapmemory/lists"}