{"id":13536769,"url":"https://github.com/irgaly/kottage","last_synced_at":"2025-08-11T02:15:50.828Z","repository":{"id":49325151,"uuid":"361384869","full_name":"irgaly/kottage","owner":"irgaly","description":"Kotlin Multiplatform Key-Value Store Local Cache Storage for Single Source of Truth.","archived":false,"fork":false,"pushed_at":"2025-07-15T16:19:55.000Z","size":2521,"stargazers_count":84,"open_issues_count":24,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-07-15T19:40:13.791Z","etag":null,"topics":["cache","kotlin","kotlin-multiplatform","storage"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/irgaly.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","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,"zenodo":null},"funding":{"github":"irgaly","custom":["https://github.com/irgaly/irgaly"]}},"created_at":"2021-04-25T09:27:03.000Z","updated_at":"2025-07-15T16:11:56.000Z","dependencies_parsed_at":"2024-01-03T20:22:30.176Z","dependency_job_id":"8d90c015-bfcb-4233-a8e0-3fbb62e5b085","html_url":"https://github.com/irgaly/kottage","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"purl":"pkg:github/irgaly/kottage","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irgaly%2Fkottage","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irgaly%2Fkottage/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irgaly%2Fkottage/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irgaly%2Fkottage/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/irgaly","download_url":"https://codeload.github.com/irgaly/kottage/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irgaly%2Fkottage/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":269819032,"owners_count":24480087,"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","status":"online","status_checked_at":"2025-08-11T02:00:10.019Z","response_time":75,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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","kotlin","kotlin-multiplatform","storage"],"created_at":"2024-08-01T09:00:49.168Z","updated_at":"2025-08-11T02:15:50.794Z","avatar_url":"https://github.com/irgaly.png","language":"Kotlin","funding_links":["https://github.com/sponsors/irgaly","https://github.com/irgaly/irgaly"],"categories":["Libraries","数据库"],"sub_categories":["Storage","Spring Cloud框架"],"readme":"# Kottage\n\nKotlin Multiplatform Key-Value Store Local Cache Storage for Single Source of Truth.\n\n# Features\n\n* A Kotlin Multiplatform library\n* Key-Value Store with no schemas, values are stored to SQLite\n* Observing events of item updates as Flow\n* Cache Expiration\n    * Cache Eviction Strategies:\n        * Expiration Time\n        * FIFO Strategy\n        * LRU Strategy\n* KVS Cache mode\n    * Expired items are evicted automatically\n* KVS Storage mode\n    * no item expiration\n* List structures for Paging are supported\n* Support primitive values and `@Serializable` classes\n\n# Requires\n\n* [New memory manager](https://github.com/JetBrains/kotlin/blob/master/kotlin-native/NEW_MM.md)\n  enabled with Kotlin/Native platform.\n* SQLite3 dynamic link library on runtime environment\n    * Windows needs sqlite3.dll on library path or bundled with your application.\n        * Download from https://www.sqlite.org/download.html \u003e sqlite-dll-win64-x64-3410200.zip\n    * Linux needs sqlite3 package on system.\n    * macOS and iOS platform has sqlite3 library, so you don't have to bundle it.\n    * Android has SQLiteHelper class, no dynamic libraries are needed.\n    * JVM has JDBC SQLite driver, no dynamic libraries are needed.\n    * Nodejs bundles SQLite3 binary, no dynamic libraries are needed.\n\n# Usage\n\n## Setup\n\nAdd Kottage as gradle dependency.\n\n### Kotlin Multiplatform:\n\n`build.gradle.kts`\n\n```kotlin\n// For Kotlin Multiplatform:\nplugins {\n    kotlin(\"multiplatform\")\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            implementation(\"io.github.irgaly.kottage:kottage:1.8.0\")\n        }\n    }\n    // ...\n}\n```\n\n### Android or JVM without Kotlin Multiplatform:\n\n`build.gradle.kts`\n\n```kotlin\n// For Kotlin/JVM or Kotlin/Android without Kotlin Multiplatform:\nplugins {\n    id(\"com.android.application\")\n    kotlin(\"android\")\n    // kotlin(\"jvm\") // for JVM Application\n}\n\ndependencies {\n    // You can use as JVM library directly\n    implementation(\"io.github.irgaly.kottage:kottage:1.8.0\")\n    // ...\n}\n```\n\n## Use Kottage\n\nUse Kottage as KVS cache or KVS storage.\n\nFirst, get a Kottage instance. Even though you can use Kottage instance as a singleton, multiple\nKottage instances creation is allowed. Kottage instances and methods are thread safe.\n\n```kotlin\nimport io.github.irgaly.kottage.platform.contextOf\n\n// directory path string for SQLite file\n// For example:\n// * Android File Directory: context.getFilesDir().path\n// * Android Cache Directory: context.getCacheDir().path\nval databaseDirectory: String = ...\nval kottageEnvironment: KottageEnvironment = KottageEnvironment(\n    context = contextOf(context) // for Android, set a KottageContext with Android Context object\n    //context = KottageContext() // for other platforms, set an empty KottageContext\n)\n// Initialize with Kottage database information.\nval kottage: Kottage = Kottage(\n    name = \"kottage-store-name\", // This will be database file name\n    directoryPath = databaseDirectory,\n    environment = kottageEnvironment,\n    scope = scope, // This kottage instance will be automatically close on this CoroutineScope completion\n    json = Json.Default // kotlinx.serialization's json object\n)\n```\n\nThen, use it as KVS Cache.\n\n```kotlin\nimport kotlin.time.Duration.Companion.days\n\n// Open Kottage database as cache mode\nval cache: KottageStorage = kottage.cache(\"timeline_item_cache\") {\n    // There are some options\n    strategy = KottageFifoStrategy(maxEntryCount = 1000) // default strategy in cache mode\n    //strategy = KottageLruStrategy(maxEntryCount = 1000) // LRU cache strategy\n    defaultExpireTime = 30.days // cache item expiration time in kotlin.time.Duration\n}\n\n// Kottage's data accessing methods (get, put...) are suspending function\n// These items will be expired and automatically deleted after 30 days (defaultExpireTime) elapsed\ncache.put(\"item1\", \"item1 value\")\ncache.put(\"item2\", 42)\ncache.put(\"item3\", true)\n\nval value1: String = cache.get\u003cString\u003e(\"item1\")\nval value2: Int = cache.get\u003cInt\u003e(\"item2\")\nval value3: Boolean = cache.get\u003cBoolean\u003e(\"item3\")\ncache.exists(\"item4\") // =\u003e false\ncache.getOrNull\u003cString\u003e(\"item4\") // =\u003e null\n\n// 30 days later... these items are expired\ncache.get\u003cString\u003e(\"item1\") // throws NoSuchElementException\ncache.getOrNull\u003cString\u003e(\"item1\") // =\u003e null\ncache.exists(\"item1\") // =\u003e false\n```\n\nUse it as KVS Storage with no expiration.\n\n```kotlin\n// Open Kottage database as storage mode\nval storage: KottageStorage = kottage.storage(\"app_configs\")\n\n// Kottage's data accessing methods (get, put...) are suspending function\n// These items has no expiration\nstorage.put(\"item1\", \"item1 value\")\nstorage.put(\"item2\", 42)\nstorage.put(\"item3\", true)\n\nval value1: String = storage.get\u003cString\u003e(\"item1\")\nval value2: Int = storage.get\u003cInt\u003e(\"item2\")\nval value3: Boolean = storage.get\u003cBoolean\u003e(\"item3\")\nstorage.exists(\"item4\") // =\u003e false\nstorage.getOrNull\u003cString\u003e(\"item4\") // =\u003e null\n```\n\n### Property Delegation\n\nKottageStorage provides property delegate.\n\n```kotlin\nval storage: KottageStorage = kottage.storage(\"app_configs\")\n\nval myConfig: String by storage.property { \"default value\" }\nval myConfigNullable: String? by storage.nullableProperty()\n\nmyConfig.write(\"value\")\nval config: String = myConfig.read()\n```\n\nFor example, this is strictly typed data access class:\n\n```kotlin\nclass AppConfiguration(kottage: Kottage) {\n    private val storage: KottageStorage = kottage.storage(\"app_configs\")\n    val myConfig: String by storage.property { \"default value\" }\n    val myConfigNullable: String? by storage.nullableProperty()\n}\n\nval configuration: AppConfiguration = AppConfiguration(kottage)\nconfiguration.myConfig.write(\"value\")\nval config: String = configuration.myConfig.read()\n```\n\n### List / Paging\n\nKottage has a List feature for make Paging UIs and for Single Source of Truth.\n\n```kotlin\nimport io.github.irgaly.kottage.kottageListValue\n\nval cache: KottageStorage = kottage.cache(\"timeline_item_cache\")\nval list: KottageList = cache.list(\"timeline_list\")\n\n// KottageList is an interface of KottageStorage that supports List operations.\n\n// Add List Items\nlist.add(\"item_id_1\", TimelineItem(\"item_id_1\", ...))\nlist.addAll(\n    listOf(\n        kottageListValue(\"item_id_2\", TimelineItem(\"item_id_2\", ...)),\n        kottageListValue(\"item_id_3\", TimelineItem(\"item_id_3\", ...)),\n        kottageListValue(\"item_id_4\", TimelineItem(\"item_id_4\", ...)),\n        kottageListValue(\"item_id_5\", TimelineItem(\"item_id_5\", ...))\n    )\n)\n\n// The items are stored in \"timeline_item_cache\" KottageStorage\ncache.exists(\"item_id_1\") // =\u003e true\n\n// You can update items directly\ncache.put(\"item_id_1\", TimelineItem(\"item_id_1\", otherValue = ...))\n\n// Get as Page\nval page0: KottageListPage = list.getPageFrom(positionId = null, pageSize = 2)\nval page1: KottageListPage = list.getPageFrom(positionId = (page0.nextPositionId), pageSize = 2)\nval page2: KottageListPage = list.getPageFrom(positionId = (page1.nextPositionId), pageSize = 2)\n\npage0.items // =\u003e List\u003cKottageListEntry\u003e\npage0.items.map { it.value\u003cTimelineItem\u003e() }\n  // =\u003e [TimelineItem(\"item_id_1\", otherValue = ...), TimelineItem(\"item_id_2\", ...)]\npage1.items.map { it.value\u003cTimelineItem\u003e() }\n  // =\u003e [TimelineItem(\"item_id_3\", ...), TimelineItem(\"item_id_4\", ...)]\npage2.items.map { it.value\u003cTimelineItem\u003e() }\n  // =\u003e [TimelineItem(\"item_id_5\", ...)]\npage2.hasNext // =\u003e false\n\n// KottageList is a Linked List.\nval entry1: KottageListEntry? = list.getFirst()\nval entry2: KottageListEntry? = list.get(checkNotNull(entry1.nextPositionId))\nval entry3: KottageListEntry? = list.get(checkNotNull(entry2.nextPositionId))\n// convenience method to get an entry by index\nval entry4: KottageListEntry? = list.getByIndex(3)\n\nentry1?.value\u003cTimelineItem\u003e() // =\u003e TimelineItem(\"item_id_1\", otherValue = ...)\n```\n\n### Serialization\n\nKottage can store and restore Serializable classes.\n\n```kotlin\n@Serializable\ndata class MyData(val myValue: Int)\n\nval data: MyData = MyData(42)\nval list: List\u003cString\u003e = listOf(\"item1\", \"item2\") // List\u003cString\u003e is Serializable\nval cache: KottageStorage = kottage.cache(\"my_data_cache\")\ncache.put(\"item1\", data)\ncache.put(\"item2\", list)\nval storedData: MyData = cache.get\u003cMyData\u003e(\"item1\")\nval storedList: List\u003cString\u003e = cache.get\u003cList\u003cString\u003e\u003e(\"item2\")\n```\n\n### Type mismatch error\n\nStore and restore works correctly with same type. It throws ClassCastException if restore with wrong\ntypes.\n\n```kotlin\nval cache: KottageStorage = kottage.cache(\"type_items\")\ncache.put(\"item1\", 0) // Store as Number (= SQLite Number = Long, Int, Short, Byte or Boolean)\ncache.put(\"item2\", \"strings\") // Store as String\ncache.get\u003cString\u003e(\"item1\") // throws ClassCastException\ncache.get\u003cInt\u003e(\"item2\") // throws ClassCastException\n```\n\nSerializable types are stored as String. It throws SerializationException if restore with wrong\ntypes.\n\n```kotlin\n@Serializable\ndata class Data(val data: Int)\n\n@Serializable\ndata class Data2(val data2: Int)\n\nval cache: KottageStorage = kottage.cache(\"type_items\")\ncache.put(\"data\", Data(42))\ncache.get\u003cString\u003e(\"data\") // =\u003e \"{\\\"data\\\":42}\"\ncache.get\u003cData2\u003e(\"data\") // throws SerializationException\n```\n\n### Event Observing\n\nKottage supports observing events of item updates for implementing Single Source of Truth.\n\n```kotlin\nval cache: KottageStorage = kottage.cache(\"my_item_cache\")\nval now: Long = ... // Unix Time (UTC) in millis\nlaunch {\n    cache.eventFlow(now).collect { event -\u003e\n        // receive events from flow\n        val eventType: KottageEventType = event.eventType // eventType =\u003e KottageEventType.Create\n        val updatedValue: String = cache.get\u003cString\u003e(event.itemKey) // updatedValue =\u003e \"value\"\n    }\n}\ncache.put(\"key\", \"value\")\n// get events after time\nval events: List\u003cKottageEvent\u003e = cache.getEvents(now)\nval updatedValue: String = cache.get\u003cString\u003e(event.first().itemKey) // updatedValue =\u003e \"value\"\n```\n\nAn eventFlow (KottageEventFlow) can automatically resume from previous emitted event.\nFor example, on Android platform, collect events while Lifecycle is at least STARTED.\n\n```kotlin\nval cache = kottage.cache(\"my_item_cache\")\nval now = ... // Unix Time (UTC) in millis\nval eventFlow = cache.eventFlow(now)\n\n...\n\noverride fun onCreate(...) { // for example: onCreate\n    ...\n    lifecycleScope.launch {\n        repeatOnLifecycle(Lifecycle.State.STARTED) {\n            eventFlow.collect { event -\u003e\n                // eventFlow starts dispatching events from last emitted event on previous subscription.\n            }\n        }\n    }\n}\n```\n\n# Encryption\n\nUser defined encryption are supported. **Only KVS value part is encrypted**, while other part (KVS\nkey, storage name...) remains plain data.\n\n| part                         | stored data |\n|------------------------------|-------------|\n| KVS value                    | encrypted   |\n| KVS key                      | plain       |\n| Kottage name (SQL file name) | plain       |\n| Kottage Storage name         | plain       |\n| Kottage List name            | plain       |\n| KottageListMetaData          | plain       |\n| KottageEvent                 | plain       |\n\n```kotlin\nval storage: KottageStorage = kottage.storage(\"encrypted_storage\") {\n    encoder = object : KottageEncoder {\n        override fun encode(value: ByteArray): ByteArray {\n            // Your encoding logic (plain ByteArray to encrypted ByteArray) here\n            return ...\n        }\n\n        override fun decode(encoded: ByteArray): ByteArray {\n            // Your decoding logic (encrypted ByteArray to plain ByteArray) here\n            return ...\n        }\n    }\n}\n// storage's values are encrypted\nstorage.put(\"long_value\", 100L)\nstorage.put(\"string_value\", \"value\")\nval longValue: Long = storage.get(\"long_value\") // =\u003e 100L\nval stringValue: String = storage.get(\"long_value\") // =\u003e \"value\"\n```\n\n* Recommendation: [Krypto](https://docs.korge.org/krypto/) is a cool library to use encryption\n  feature in Kotlin Multiplatform.\n\n# Supporting Data Types\n\n* Primitives: `Double`, `Float`, `Long`, `Int`, `Short`, `Byte`, `Boolean`\n* Bytes: `ByteArray`\n* Texts: `String`\n* Serializable: kotlinx.serialization's `@Serializable` classes\n\n# Multiplatform\n\nKottage is a Kotlin Multiplatform library. Please feel free to report a issue if it doesn't\nwork correctly on these platforms.\n\n| Platform                          | Target                                                                               | Status                                                        |\n|-----------------------------------|--------------------------------------------------------------------------------------|---------------------------------------------------------------|\n| Kotlin/JVM on Linux/macOS/Windows | jvm                                                                                  | :white_check_mark: Tested                                     |\n| Kotlin/JS on Linux/macOS/Windows  | browser, nodejs                                                                      | :white_check_mark: Tested\u003cbr/\u003ebrowser on macOS Chrome, Safari |\n| Kotlin/Android                    | android                                                                              | :white_check_mark: Tested                                     |\n| Kotlin/Native iOS                 | iosArm64\u003cbr\u003eiosX64(simulator)\u003cbr\u003eiosSimulatorArm64                                   | :white_check_mark: Tested (by iosSimulatorArm64 only)         |\n| Kotlin/Native watchOS             | watchosArm64\u003cbr\u003ewatchosDeviceArm64\u003cbr\u003ewatchosX64(simulator)\u003cbr\u003ewatchosSimulatorArm64 | :white_check_mark: (Tested as iosSimulatorArm64)              |\n| Kotlin/Native tvOS                | tvosArm64\u003cbr\u003etvosX64(simulator)\u003cbr\u003etvosSimulatorArm64                                | :white_check_mark: (Tested as iosSimulatorArm64)              |\n| Kotlin/Native macOS               | macosArm64\u003cbr\u003emacosX64                                                               | :white_check_mark: Tested (by macosArm64 only)                |\n| Kotlin/Native Linux               | linuxX64\u003cbr\u003elinuxArm64                                                               | :white_check_mark: Tested (by linuxX64 only)                  |\n| Kotlin/Native Windows             | mingwX64                                                                             | :white_check_mark: Tested                                     |\n\nThere is also [Kottage for SwiftPM](https://github.com/irgaly/kottage-package) that is **just for\nexperimental** build.\n\n## Kotlin/JS (browser/nodejs) Support\n\nKottage supports Kottage/JS browser and nodejs. Kottage on browser uses IndexedDB as persistent\ndatabase instead of SQLite. Kottage on nodejs uses SQLite.\n\n| Kotlin/JS type | Database  |\n|----------------|-----------|\n| browser        | IndexedDB |\n| nodejs         | SQLite    |\n\nBrowsers will clear IndexedDB data when user's disk storage gets low disk space.\nYou can request browsers your IndexedDB's data not to be cleared by using `StorageManager.persist()`\n.\nSee [Web API documents](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/persist) for\nmore details.\n\n### Kotlin/JS browser Setup\n\nKotlin/JS's library contains both of implementations for browser and for nodejs, so additional\nWebpack config is required for browser to use Kottage.\n\nWhen you run `jsBrowserRun` (jsBrowserDevelopmentRun or jsBrowserProductionRun), some Webpack Errors\noccurs:\n\n```shell\nCompiled with problems:X\n\nWARNING in ../../node_modules/better-sqlite3/lib/database.js 50:10-81\n\nCritical dependency: the request of a dependency is an expression\n\n\nERROR in ../../node_modules/better-sqlite3/lib/database.js 2:11-24\n\nModule not found: Error: Can't resolve 'fs' in '(projectdir)/build/js/node_modules/better-sqlite3/lib'\n\n\nERROR in ../../node_modules/better-sqlite3/lib/database.js 3:13-28\n\nModule not found: Error: Can't resolve 'path' in '(projectdir)/build/js/node_modules/better-sqlite3/lib'\n\nBREAKING CHANGE: webpack \u003c 5 used to include polyfills for node.js core modules by default.\nThis is no longer the case. Verify if you need this module and configure a polyfill for it.\n\nIf you want to include a polyfill, you need to:\n\t- add a fallback 'resolve.fallback: { \"path\": require.resolve(\"path-browserify\") }'\n\t- install 'path-browserify'\nIf you don't want to include a polyfill, you can use an empty module like this:\n\tresolve.fallback: { \"path\": false }\n\n...\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eall error's text:\u003c/summary\u003e\n\n```shell\nCompiled with problems:X\n\nWARNING in ../../node_modules/better-sqlite3/lib/database.js 50:10-81\n\nCritical dependency: the request of a dependency is an expression\n\n\nERROR in ../../node_modules/better-sqlite3/lib/database.js 2:11-24\n\nModule not found: Error: Can't resolve 'fs' in '(projectdir)/build/js/node_modules/better-sqlite3/lib'\n\n\nERROR in ../../node_modules/better-sqlite3/lib/database.js 3:13-28\n\nModule not found: Error: Can't resolve 'path' in '(projectdir)/build/js/node_modules/better-sqlite3/lib'\n\nBREAKING CHANGE: webpack \u003c 5 used to include polyfills for node.js core modules by default.\nThis is no longer the case. Verify if you need this module and configure a polyfill for it.\n\nIf you want to include a polyfill, you need to:\n\t- add a fallback 'resolve.fallback: { \"path\": require.resolve(\"path-browserify\") }'\n\t- install 'path-browserify'\nIf you don't want to include a polyfill, you can use an empty module like this:\n\tresolve.fallback: { \"path\": false }\n\n\nERROR in ../../node_modules/better-sqlite3/lib/methods/backup.js 2:11-24\n\nModule not found: Error: Can't resolve 'fs' in '(projectdir)/build/js/node_modules/better-sqlite3/lib/methods'\n\n\nERROR in ../../node_modules/better-sqlite3/lib/methods/backup.js 3:13-28\n\nModule not found: Error: Can't resolve 'path' in '(projectdir)/build/js/node_modules/better-sqlite3/lib/methods'\n\nBREAKING CHANGE: webpack \u003c 5 used to include polyfills for node.js core modules by default.\nThis is no longer the case. Verify if you need this module and configure a polyfill for it.\n\nIf you want to include a polyfill, you need to:\n\t- add a fallback 'resolve.fallback: { \"path\": require.resolve(\"path-browserify\") }'\n\t- install 'path-browserify'\nIf you don't want to include a polyfill, you can use an empty module like this:\n\tresolve.fallback: { \"path\": false }\n\n\nERROR in ../../node_modules/better-sqlite3/lib/methods/backup.js 4:22-37\n\nModule not found: Error: Can't resolve 'util' in '(projectdir)/build/js/node_modules/better-sqlite3/lib/methods'\n\nBREAKING CHANGE: webpack \u003c 5 used to include polyfills for node.js core modules by default.\nThis is no longer the case. Verify if you need this module and configure a polyfill for it.\n\nIf you want to include a polyfill, you need to:\n\t- add a fallback 'resolve.fallback: { \"util\": require.resolve(\"util/\") }'\n\t- install 'util'\nIf you don't want to include a polyfill, you can use an empty module like this:\n\tresolve.fallback: { \"util\": false }\n\n\nERROR in ../../node_modules/bindings/bindings.js 5:9-22\n\nModule not found: Error: Can't resolve 'fs' in '(projectdir)/build/js/node_modules/bindings'\n\n\nERROR in ../../node_modules/bindings/bindings.js 6:9-24\n\nModule not found: Error: Can't resolve 'path' in '(projectdir)/build/js/node_modules/bindings'\n\nBREAKING CHANGE: webpack \u003c 5 used to include polyfills for node.js core modules by default.\nThis is no longer the case. Verify if you need this module and configure a polyfill for it.\n\nIf you want to include a polyfill, you need to:\n\t- add a fallback 'resolve.fallback: { \"path\": require.resolve(\"path-browserify\") }'\n\t- install 'path-browserify'\nIf you don't want to include a polyfill, you can use an empty module like this:\n\tresolve.fallback: { \"path\": false }\n\n\nERROR in ../../node_modules/file-uri-to-path/index.js 6:10-29\n\nModule not found: Error: Can't resolve 'path' in '(projectdir)/build/js/node_modules/file-uri-to-path'\n\nBREAKING CHANGE: webpack \u003c 5 used to include polyfills for node.js core modules by default.\nThis is no longer the case. Verify if you need this module and configure a polyfill for it.\n\nIf you want to include a polyfill, you need to:\n\t- add a fallback 'resolve.fallback: { \"path\": require.resolve(\"path-browserify\") }'\n\t- install 'path-browserify'\nIf you don't want to include a polyfill, you can use an empty module like this:\n\tresolve.fallback: { \"path\": false }\n\nCompiled with problems:X\n\nERROR in ./kotlin/kottage-project-core.js 72:11-24\n\nModule not found: Error: Can't resolve 'fs' in '(projectdir)/build/js/packages/kottage-project-js-browser/kotlin'\n```\n\n\u003c/details\u003e\n\nTo suppress this errors, add Webpack config file to project directory. This config will ignore\nbetter-sqlite3 module that is not needed in browser application, and exclude packed files.\n\n`{youar application module path}/webpack.config.d/kottage.webpack.config.js`:\n\n```javascript\nconfig.resolve.fallback = {\n    ...config.resolve.fallback,\n    fs: false,\n    os: false\n}\nconfig.externals = {\n    ...config.externals,\n    \"better-sqlite3\": \"better-sqlite3\"\n}\n```\n\nSample application project is available in [sample/js-browser](sample/js-browser).\n\n### Kotlin/JS nodejs Setup\n\nKottage uses [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) on nodejs.\n\nbetter-sqlite3 requires sqlite3 FFI file `better_sqlite3.node` on runtime environment.\nIf there is no FFI file, `Could not locate the bindings file` error occurred:\n\n```shell\nCoroutinesInternalError: Fatal exception in coroutines machinery for AwaitContinuation(DispatchedContinuation[NodeDispatcher@1, [object Object]]){Completed}@2. Please read KDoc to 'handleFatalException' method and report this incident to maintainers\n    at AwaitContinuation.DispatchedTask.handleFatalException_56zdfo_k$ ((projectdir)/DispatchedTask.kt:144:22)\n    at AwaitContinuation.DispatchedTask.run_mw4iiu_k$ ((projectdir)/DispatchedTask.kt:115:13)\n    at ScheduledMessageQueue.MessageQueue.process_mza50i_k$ ((projectdir)/JSDispatcher.kt:153:25)\n    at (projectdir)/JSDispatcher.kt:19:48\n    at processTicksAndRejections (node:internal/process/task_queues:77:11) {\n  cause: Error: Could not locate the bindings file. Tried:\n   → (projectdir)/build/js/node_modules/better-sqlite3/build/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/build/Debug/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/build/Release/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/out/Debug/better_sqlite3.node\n\n...\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eall error's text:\u003c/summary\u003e\n\n```shell\nCoroutinesInternalError: Fatal exception in coroutines machinery for AwaitContinuation(DispatchedContinuation[NodeDispatcher@1, [object Object]]){Completed}@2. Please read KDoc to 'handleFatalException' method and report this incident to maintainers\n    at AwaitContinuation.DispatchedTask.handleFatalException_56zdfo_k$ ((projectdir)/DispatchedTask.kt:144:22)\n    at AwaitContinuation.DispatchedTask.run_mw4iiu_k$ ((projectdir)/DispatchedTask.kt:115:13)\n    at ScheduledMessageQueue.MessageQueue.process_mza50i_k$ ((projectdir)/JSDispatcher.kt:153:25)\n    at (projectdir)/JSDispatcher.kt:19:48\n    at processTicksAndRejections (node:internal/process/task_queues:77:11) {\n  cause: Error: Could not locate the bindings file. Tried:\n   → (projectdir)/build/js/node_modules/better-sqlite3/build/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/build/Debug/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/build/Release/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/out/Debug/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/Debug/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/out/Release/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/Release/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/build/default/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/compiled/18.11.0/darwin/arm64/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/addon-build/release/install-root/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/addon-build/debug/install-root/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/addon-build/default/install-root/better_sqlite3.node\n   → (projectdir)/build/js/node_modules/better-sqlite3/lib/binding/node-v108-darwin-arm64/better_sqlite3.node\n      at bindings ((projectdir)/build/js/node_modules/bindings/bindings.js:126:9)\n      at new Database ((projectdir)/build/js/node_modules/better-sqlite3/lib/database.js:48:64)\n      at Database ((projectdir)/build/js/node_modules/better-sqlite3/lib/database.js:11:10)\n      at $createDriverCOROUTINE$0.doResume_5yljmg_k$ ((projectdir)/DriverFactory.kt:35:13)\n      at DriverFactory.createDriver_qrqvgc_k$ ((projectdir)/DriverFactory.kt:15:20)\n      at createDriver ((projectdir)/DriverFactory.kt:32:12)\n      at createDriver$default ((projectdir)/DriverFactory.kt:27:9)\n      at SqliteDatabaseConnectionFactory$createDatabaseConnection$slambda.doResume_5yljmg_k$ ((projectdir)/SqliteDatabaseConnectionFactory.kt:28:15)\n      at SqliteDatabaseConnectionFactory$createDatabaseConnection$slambda.invoke_uw69q_k$ ((projectdir)/SqliteDatabaseConnectionFactory.kt:21:41)\n      at SqliteDatabaseConnection.l [as sqlDriverProvider_1] ((projectdir)/build/js/packages/kottage-project-js-nodejs/kotlin/kottage-project-kottage.js:28289:16) {\n    tries: [\n      '(projectdir)/build/js/node_modules/better-sqlite3/build/better_sqlite3.node',\n      '(projectdir)/build/js/node_modules/better-sqlite3/build/Debug/better_sqlite3.node',\n      '(projectdir)/build/js/node_modules/better-sqlite3/build/Release/better_sqlite3.node',\n      '(projectdir)/build/js/node_modules/better-sqlite3/out/Debug/better_sqlite3.node',\n      '(projectdir)/build/js/node_modules/better-sqlite3/Debug/better_sqlite3.node',\n      '(projectdir)/build/js/node_modules/better-sqlite3/out/Release/better_sqlite3.node',\n      '(projectdir)/build/js/node_modules/better-sqlite3/Release/better_sqlite3.node',\n      '(projectdir)/build/js/node_modules/better-sqlite3/build/default/better_sqlite3.node',\n      '(projectdir)/build/js/node_modules/better-sqlite3/compiled/18.11.0/darwin/arm64/better_sqlite3.node',\n      '(projectdir)/build/js/node_modules/better-sqlite3/addon-build/release/install-root/better_sqlite3.node',\n      '(projectdir)/build/js/node_modules/better-sqlite3/addon-build/debug/install-root/better_sqlite3.node',\n      '(projectdir)/build/js/node_modules/better-sqlite3/addon-build/default/install-root/better_sqlite3.node',\n      '(projectdir)/build/js/node_modules/better-sqlite3/lib/binding/node-v108-darwin-arm64/better_sqlite3.node'\n    ]\n  }\n}\n```\n\n\u003c/details\u003e\n\nTo prevent this error, you should build FFI file with a custom Gradle Task.\n\n* Make sure your machine environment has `python3`.\n* Register `installBetterSqlite3` task. This task will\n  make `{rootProject}/build/js/node_modules/better-sqlite3/build/Release/better_sqlite3.node`.\n\n`{rootProject}/build.gradle.kts` ([sample build.gradle.kts is here](build.gradle.kts))\n\n```kotlin\nimport org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension\nimport org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin\nimport org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask\n\n...\nplugins.withType\u003cNodeJsRootPlugin\u003e {\n    extensions.configure\u003cNodeJsRootExtension\u003e {\n        // Choose any version you want to use from https://nodejs.org/en/download/releases/\n        nodeVersion = \"20.18.2\"\n        val installBetterSqlite3 by tasks.registering(Exec::class) {\n            val nodeExtension = this@configure\n            val nodeEnv = nodeExtension.requireConfigured()\n            val node = nodeEnv.nodeExecutable.replace(File.separator, \"/\")\n            val nodeDir = nodeEnv.nodeDir.path.replace(File.separator, \"/\")\n            val nodeBinDir = nodeEnv.nodeBinDir.path.replace(File.separator, \"/\")\n            val npmCli = if (OperatingSystem.current().isWindows) {\n                \"$nodeDir/node_modules/npm/bin/npm-cli.js\"\n            } else {\n                \"$nodeDir/lib/node_modules/npm/bin/npm-cli.js\"\n            }\n            val npm = \"\\\"$node\\\" \\\"$npmCli\\\"\"\n            val betterSqlite3 = buildDir.resolve(\"js/node_modules/better-sqlite3\")\n            dependsOn(tasks.withType\u003cKotlinNpmInstallTask\u003e())\n            inputs.files(betterSqlite3.resolve(\"package.json\"))\n            inputs.property(\"node-version\", nodeVersion)\n            outputs.files(betterSqlite3.resolve(\"build/Release/better_sqlite3.node\"))\n            outputs.cacheIf { true }\n            workingDir = betterSqlite3\n            commandLine = if (OperatingSystem.current().isWindows) {\n                listOf(\n                    \"sh\",\n                    \"-c\",\n                    // use pwd command to convert C:/... -\u003e /c/...\n                    \"PATH=\\$(cd $nodeBinDir;pwd):\\$PATH $npm run install --verbose\"\n                )\n            } else {\n                listOf(\n                    \"sh\",\n                    \"-c\",\n                    \"PATH=\\\"$nodeBinDir:\\$PATH\\\" $npm run install --verbose\"\n                )\n            }\n        }\n    }\n}\n...\n```\n\n* Add NodeJsExec Task's dependency\n  setting. ([sample sample/js-nodejs/build.gradle.kts is here](sample/js-nodejs/build.gradle.kts))\n\n`{nodejs project}/build.gradle.kts`\n\n```kotlin\nplugins {\n    kotlin(\"multiplatform\")\n}\n\nkotlin {\n    js(IR) {\n        ...\n    }\n    ...\n}\n...\ntasks.withType\u003cNodeJsExec\u003e().configureEach {\n    dependsOn(rootProject.tasks.named(\"installBetterSqlite3\"))\n}\n...\n```\n\n* Then, execute `jsNodeRun` (jsNodeDevelopmentRun or jsNodeProductionRun) task. There are no FFI\n  errors.\n\n# Kottage Internals\n\nTBA: I'll write details of library here.\n\n* Lazy item expiration.\n* Automated clean up of expired item.\n* Limit item counts.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Firgaly%2Fkottage","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Firgaly%2Fkottage","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Firgaly%2Fkottage/lists"}